From e612775922ec9f8cc4e5cb976bc62b3312a3de0e Mon Sep 17 00:00:00 2001 From: Hyunyoung Song Date: Tue, 21 Jul 2015 17:01:26 -0700 Subject: [PATCH] resolved conflicts for merge of 13ef17a3 to mnc-dr-dev b/22609402 Change-Id: I140cf972d57e14737a6f91c0b4a8ec6c7ff1af2b --- .gitignore | 9 + Android.mk | 15 +- AndroidManifest.xml | 51 +- WallpaperPicker/AndroidManifest.xml | 4 +- .../res/drawable-hdpi/ic_images.png | Bin 1935 -> 354 bytes .../res/drawable-mdpi/ic_images.png | Bin 1332 -> 249 bytes .../res/drawable-xhdpi/ic_images.png | Bin 2480 -> 431 bytes .../res/drawable-xxhdpi/ic_images.png | Bin 2097 -> 621 bytes .../res/drawable-xxxhdpi/ic_images.png | Bin 0 -> 845 bytes .../res/layout/wallpaper_cropper.xml | 3 +- .../res/layout/wallpaper_picker.xml | 42 +- .../wallpaper_picker_image_picker_item.xml | 1 - .../res/layout/wallpaper_picker_item.xml | 1 - .../wallpaper_picker_live_wallpaper_item.xml | 1 - .../wallpaper_picker_third_party_item.xml | 1 - WallpaperPicker/res/values-af/strings.xml | 2 +- WallpaperPicker/res/values-am/strings.xml | 2 +- WallpaperPicker/res/values-ar/strings.xml | 2 +- WallpaperPicker/res/values-az-rAZ/strings.xml | 2 +- WallpaperPicker/res/values-bg/strings.xml | 2 +- WallpaperPicker/res/values-bn-rBD/strings.xml | 2 +- WallpaperPicker/res/values-ca/strings.xml | 2 +- WallpaperPicker/res/values-cs/strings.xml | 2 +- WallpaperPicker/res/values-da/strings.xml | 2 +- WallpaperPicker/res/values-de/strings.xml | 2 +- WallpaperPicker/res/values-el/strings.xml | 2 +- WallpaperPicker/res/values-en-rAU/strings.xml | 2 +- WallpaperPicker/res/values-en-rGB/strings.xml | 2 +- WallpaperPicker/res/values-en-rIN/strings.xml | 2 +- WallpaperPicker/res/values-es-rUS/strings.xml | 2 +- WallpaperPicker/res/values-es/strings.xml | 2 +- WallpaperPicker/res/values-et-rEE/strings.xml | 2 +- WallpaperPicker/res/values-eu-rES/strings.xml | 2 +- WallpaperPicker/res/values-fa/strings.xml | 2 +- WallpaperPicker/res/values-fi/strings.xml | 2 +- WallpaperPicker/res/values-fr-rCA/strings.xml | 2 +- WallpaperPicker/res/values-fr/strings.xml | 2 +- WallpaperPicker/res/values-gl-rES/strings.xml | 2 +- WallpaperPicker/res/values-gu-rIN/strings.xml | 2 +- WallpaperPicker/res/values-hi/strings.xml | 2 +- WallpaperPicker/res/values-hr/strings.xml | 2 +- WallpaperPicker/res/values-hu/strings.xml | 2 +- WallpaperPicker/res/values-hy-rAM/strings.xml | 2 +- WallpaperPicker/res/values-in/strings.xml | 2 +- WallpaperPicker/res/values-is-rIS/strings.xml | 2 +- WallpaperPicker/res/values-it/strings.xml | 2 +- WallpaperPicker/res/values-iw/strings.xml | 2 +- WallpaperPicker/res/values-ja/strings.xml | 2 +- WallpaperPicker/res/values-ka-rGE/strings.xml | 2 +- WallpaperPicker/res/values-kk-rKZ/strings.xml | 2 +- WallpaperPicker/res/values-km-rKH/strings.xml | 2 +- WallpaperPicker/res/values-kn-rIN/strings.xml | 2 +- WallpaperPicker/res/values-ko/strings.xml | 2 +- WallpaperPicker/res/values-ky-rKG/strings.xml | 2 +- WallpaperPicker/res/values-lo-rLA/strings.xml | 2 +- WallpaperPicker/res/values-lt/strings.xml | 2 +- WallpaperPicker/res/values-lv/strings.xml | 2 +- WallpaperPicker/res/values-mk-rMK/strings.xml | 2 +- WallpaperPicker/res/values-ml-rIN/strings.xml | 2 +- WallpaperPicker/res/values-mn-rMN/strings.xml | 2 +- WallpaperPicker/res/values-mr-rIN/strings.xml | 2 +- WallpaperPicker/res/values-ms-rMY/strings.xml | 2 +- WallpaperPicker/res/values-my-rMM/strings.xml | 2 +- WallpaperPicker/res/values-nb/strings.xml | 2 +- WallpaperPicker/res/values-ne-rNP/strings.xml | 2 +- WallpaperPicker/res/values-nl/strings.xml | 2 +- WallpaperPicker/res/values-pa-rIN/strings.xml | 2 +- WallpaperPicker/res/values-pl/strings.xml | 2 +- WallpaperPicker/res/values-pt-rPT/strings.xml | 2 +- WallpaperPicker/res/values-pt/strings.xml | 2 +- WallpaperPicker/res/values-ro/strings.xml | 2 +- WallpaperPicker/res/values-ru/strings.xml | 2 +- WallpaperPicker/res/values-si-rLK/strings.xml | 2 +- WallpaperPicker/res/values-sk/strings.xml | 2 +- WallpaperPicker/res/values-sl/strings.xml | 2 +- WallpaperPicker/res/values-sq-rAL/strings.xml | 2 +- WallpaperPicker/res/values-sr/strings.xml | 2 +- WallpaperPicker/res/values-sv/strings.xml | 2 +- WallpaperPicker/res/values-sw/strings.xml | 2 +- .../res/values-sw720dp-v19/styles.xml | 2 +- WallpaperPicker/res/values-sw720dp/dimens.xml | 28 - WallpaperPicker/res/values-sw720dp/styles.xml | 2 +- WallpaperPicker/res/values-ta-rIN/strings.xml | 2 +- WallpaperPicker/res/values-te-rIN/strings.xml | 2 +- WallpaperPicker/res/values-th/strings.xml | 2 +- WallpaperPicker/res/values-tl/strings.xml | 2 +- WallpaperPicker/res/values-tr/strings.xml | 2 +- WallpaperPicker/res/values-uk/strings.xml | 2 +- WallpaperPicker/res/values-ur-rPK/strings.xml | 2 +- WallpaperPicker/res/values-uz-rUZ/strings.xml | 2 +- WallpaperPicker/res/values-v21/styles.xml | 36 + WallpaperPicker/res/values-vi/strings.xml | 2 +- WallpaperPicker/res/values-zh-rCN/strings.xml | 2 +- WallpaperPicker/res/values-zh-rHK/strings.xml | 2 +- WallpaperPicker/res/values-zh-rTW/strings.xml | 2 +- WallpaperPicker/res/values-zu/strings.xml | 2 +- WallpaperPicker/res/values/config.xml | 1 - WallpaperPicker/res/values/strings.xml | 2 +- WallpaperPicker/res/values/styles.xml | 3 +- WallpaperPicker/src/android/util/Pools.java | 165 -- .../gallery3d/common/BitmapCropTask.java | 405 ++++ .../android/gallery3d/common/BitmapUtils.java | 253 +- .../com/android/gallery3d/common/Utils.java | 269 +-- .../android/gallery3d/exif/ExifInterface.java | 6 +- .../gallery3d/glrenderer/BasicTexture.java | 1 - .../gallery3d/glrenderer/BitmapTexture.java | 4 +- .../gallery3d/glrenderer/GLES20Canvas.java | 3 +- .../android/gallery3d/glrenderer/GLPaint.java | 4 +- .../{util => glrenderer}/IntArray.java | 2 +- .../gallery3d/glrenderer/UploadedTexture.java | 9 +- .../src/com/android/launcher3/CropView.java | 5 +- .../LauncherWallpaperPickerActivity.java | 13 +- .../launcher3/LiveWallpaperListAdapter.java | 15 +- .../launcher3/SavedWallpaperImages.java | 5 +- .../ThirdPartyWallpaperPickerListAdapter.java | 11 +- .../launcher3/WallpaperCropActivity.java | 807 ++----- .../launcher3/WallpaperPickerActivity.java | 391 ++- .../android/launcher3/WallpaperRootView.java | 39 - .../android/launcher3/base/BaseActivity.java | 21 + .../photos/BitmapRegionTileSource.java | 180 +- .../photos/views/BlockingGLTextureView.java | 438 ---- .../photos/views/TiledImageRenderer.java | 27 +- .../android/photos/views/TiledImageView.java | 124 +- proguard.flags | 72 +- protos/backup.proto | 28 +- res/drawable-hdpi/apps_customize_bg.png | Bin 69 -> 0 bytes res/drawable-hdpi/bg_appwidget_error.9.png | Bin 1285 -> 0 bytes res/drawable-hdpi/home_press.9.png | Bin 153 -> 0 bytes res/drawable-hdpi/ic_arrow_back_grey.png | Bin 0 -> 190 bytes .../ic_home_all_apps_holo_dark.png | Bin 1292 -> 0 bytes res/drawable-hdpi/ic_info_launcher.png | Bin 0 -> 2727 bytes .../ic_launcher_clear_active_holo.png | Bin 1181 -> 0 bytes .../ic_launcher_clear_normal_holo.png | Bin 1003 -> 0 bytes .../ic_launcher_info_active_holo.png | Bin 4714 -> 0 bytes .../ic_launcher_info_normal_holo.png | Bin 2692 -> 0 bytes res/drawable-hdpi/ic_launcher_market_holo.png | Bin 1644 -> 0 bytes .../ic_launcher_trashcan_active_holo.png | Bin 2657 -> 0 bytes .../ic_launcher_trashcan_normal_holo.png | Bin 1750 -> 0 bytes .../ic_pageindicator_current.png | Bin 1038 -> 1038 bytes .../ic_pageindicator_current_folder.png | Bin 0 -> 610 bytes .../ic_pageindicator_default.png | Bin 895 -> 895 bytes .../ic_pageindicator_default_folder.png | Bin 0 -> 529 bytes res/drawable-hdpi/ic_remove_launcher.png | Bin 0 -> 1610 bytes res/drawable-hdpi/ic_search_grey.png | Bin 0 -> 743 bytes res/drawable-hdpi/ic_uninstall_launcher.png | Bin 0 -> 1194 bytes res/drawable-hdpi/ic_widget_resize_handle.png | Bin 0 -> 1492 bytes res/drawable-hdpi/on_boarding_welcome.png | Bin 22423 -> 0 bytes res/drawable-hdpi/overscroll_glow_left.9.png | Bin 858 -> 0 bytes res/drawable-hdpi/overscroll_glow_right.9.png | Bin 856 -> 0 bytes res/drawable-hdpi/paged_view_indicator.9.png | Bin 637 -> 0 bytes res/drawable-hdpi/portal_container_holo.9.png | Bin 1384 -> 0 bytes res/drawable-hdpi/portal_ring_inner.png | Bin 0 -> 4386 bytes res/drawable-hdpi/portal_ring_inner_holo.png | Bin 4429 -> 0 bytes res/drawable-hdpi/portal_ring_inner_nolip.png | Bin 0 -> 4107 bytes .../portal_ring_inner_nolip_holo.png | Bin 4118 -> 0 bytes res/drawable-hdpi/portal_ring_outer.png | Bin 0 -> 7887 bytes res/drawable-hdpi/portal_ring_outer_holo.png | Bin 7658 -> 0 bytes res/drawable-hdpi/portal_ring_rest.png | Bin 2646 -> 2794 bytes res/drawable-hdpi/quantum_panel.9.png | Bin 744 -> 0 bytes res/drawable-hdpi/quantum_panel_bitmap.9.png | Bin 0 -> 608 bytes res/drawable-hdpi/quantum_panel_dark.9.png | Bin 646 -> 0 bytes .../quantum_panel_dark_bitmap.9.png | Bin 0 -> 629 bytes res/drawable-hdpi/widget_resize_frame.9.png | Bin 0 -> 405 bytes .../widget_resize_frame_holo.9.png | Bin 880 -> 0 bytes .../widget_resize_handle_bottom.png | Bin 1080 -> 0 bytes .../widget_resize_handle_left.png | Bin 1097 -> 0 bytes .../widget_resize_handle_right.png | Bin 1085 -> 0 bytes .../widget_resize_handle_top.png | Bin 1085 -> 0 bytes res/drawable-hdpi/widget_resize_shadow.9.png | Bin 0 -> 527 bytes .../container_fastscroll_popup_bg.xml | 27 + res/drawable-mdpi/apps_customize_bg.png | Bin 69 -> 0 bytes res/drawable-mdpi/bg_appwidget_error.9.png | Bin 794 -> 0 bytes res/drawable-mdpi/home_press.9.png | Bin 129 -> 0 bytes res/drawable-mdpi/ic_arrow_back_grey.png | Bin 0 -> 151 bytes .../ic_home_all_apps_holo_dark.png | Bin 889 -> 0 bytes res/drawable-mdpi/ic_info_launcher.png | Bin 0 -> 1603 bytes .../ic_launcher_clear_active_holo.png | Bin 949 -> 0 bytes .../ic_launcher_clear_normal_holo.png | Bin 802 -> 0 bytes .../ic_launcher_info_active_holo.png | Bin 2736 -> 0 bytes .../ic_launcher_info_normal_holo.png | Bin 1518 -> 0 bytes res/drawable-mdpi/ic_launcher_market_holo.png | Bin 1124 -> 0 bytes .../ic_launcher_trashcan_active_holo.png | Bin 1783 -> 0 bytes .../ic_launcher_trashcan_normal_holo.png | Bin 1109 -> 0 bytes .../ic_pageindicator_current.png | Bin 704 -> 704 bytes .../ic_pageindicator_current_folder.png | Bin 0 -> 484 bytes .../ic_pageindicator_default.png | Bin 694 -> 694 bytes .../ic_pageindicator_default_folder.png | Bin 0 -> 482 bytes res/drawable-mdpi/ic_remove_launcher.png | Bin 0 -> 1087 bytes res/drawable-mdpi/ic_search_grey.png | Bin 0 -> 497 bytes res/drawable-mdpi/ic_setting_icn.png | Bin 1718 -> 0 bytes res/drawable-mdpi/ic_uninstall_launcher.png | Bin 0 -> 932 bytes res/drawable-mdpi/ic_widget_resize_handle.png | Bin 0 -> 896 bytes res/drawable-mdpi/on_boarding_welcome.png | Bin 15325 -> 0 bytes res/drawable-mdpi/overscroll_glow_left.9.png | Bin 552 -> 0 bytes res/drawable-mdpi/overscroll_glow_right.9.png | Bin 525 -> 0 bytes res/drawable-mdpi/paged_view_indicator.9.png | Bin 264 -> 0 bytes res/drawable-mdpi/portal_container_holo.9.png | Bin 843 -> 0 bytes res/drawable-mdpi/portal_ring_inner.png | Bin 0 -> 2641 bytes res/drawable-mdpi/portal_ring_inner_holo.png | Bin 2779 -> 0 bytes res/drawable-mdpi/portal_ring_inner_nolip.png | Bin 0 -> 2545 bytes .../portal_ring_inner_nolip_holo.png | Bin 2632 -> 0 bytes res/drawable-mdpi/portal_ring_outer.png | Bin 0 -> 4704 bytes res/drawable-mdpi/portal_ring_outer_holo.png | Bin 4798 -> 0 bytes res/drawable-mdpi/portal_ring_rest.png | Bin 1633 -> 1675 bytes res/drawable-mdpi/quantum_panel.9.png | Bin 476 -> 0 bytes res/drawable-mdpi/quantum_panel_bitmap.9.png | Bin 0 -> 436 bytes res/drawable-mdpi/quantum_panel_dark.9.png | Bin 449 -> 0 bytes .../quantum_panel_dark_bitmap.9.png | Bin 0 -> 445 bytes res/drawable-mdpi/widget_resize_frame.9.png | Bin 0 -> 304 bytes .../widget_resize_frame_holo.9.png | Bin 619 -> 0 bytes .../widget_resize_handle_bottom.png | Bin 745 -> 0 bytes .../widget_resize_handle_left.png | Bin 762 -> 0 bytes .../widget_resize_handle_right.png | Bin 772 -> 0 bytes .../widget_resize_handle_top.png | Bin 750 -> 0 bytes res/drawable-mdpi/widget_resize_shadow.9.png | Bin 0 -> 367 bytes res/drawable-v21/quantum_panel.xml | 22 + res/drawable-v21/quantum_panel_dark.xml | 22 + res/drawable-xhdpi/apps_customize_bg.png | Bin 244 -> 0 bytes res/drawable-xhdpi/bg_appwidget_error.9.png | Bin 2387 -> 0 bytes res/drawable-xhdpi/home_press.9.png | Bin 206 -> 0 bytes res/drawable-xhdpi/ic_arrow_back_grey.png | Bin 0 -> 234 bytes .../ic_home_all_apps_holo_dark.png | Bin 1569 -> 0 bytes res/drawable-xhdpi/ic_info_launcher.png | Bin 0 -> 3777 bytes .../ic_launcher_clear_active_holo.png | Bin 1595 -> 0 bytes .../ic_launcher_clear_normal_holo.png | Bin 1392 -> 0 bytes .../ic_launcher_info_active_holo.png | Bin 6841 -> 0 bytes .../ic_launcher_info_normal_holo.png | Bin 3837 -> 0 bytes .../ic_launcher_market_holo.png | Bin 2187 -> 0 bytes .../ic_launcher_trashcan_active_holo.png | Bin 3431 -> 0 bytes .../ic_launcher_trashcan_normal_holo.png | Bin 2240 -> 0 bytes .../ic_pageindicator_current.png | Bin 1414 -> 1414 bytes .../ic_pageindicator_current_folder.png | Bin 0 -> 782 bytes .../ic_pageindicator_default.png | Bin 1168 -> 1168 bytes .../ic_pageindicator_default_folder.png | Bin 0 -> 644 bytes res/drawable-xhdpi/ic_remove_launcher.png | Bin 0 -> 1724 bytes res/drawable-xhdpi/ic_search_grey.png | Bin 0 -> 972 bytes res/drawable-xhdpi/ic_uninstall_launcher.png | Bin 0 -> 1456 bytes .../ic_widget_resize_handle.png | Bin 0 -> 2144 bytes res/drawable-xhdpi/on_boarding_welcome.png | Bin 34253 -> 0 bytes res/drawable-xhdpi/overscroll_glow_left.9.png | Bin 1249 -> 0 bytes .../overscroll_glow_right.9.png | Bin 1248 -> 0 bytes res/drawable-xhdpi/paged_view_indicator.9.png | Bin 897 -> 0 bytes .../portal_container_holo.9.png | Bin 2130 -> 0 bytes res/drawable-xhdpi/portal_ring_inner.png | Bin 0 -> 5983 bytes res/drawable-xhdpi/portal_ring_inner_holo.png | Bin 6711 -> 0 bytes .../portal_ring_inner_nolip.png | Bin 0 -> 5569 bytes .../portal_ring_inner_nolip_holo.png | Bin 6191 -> 0 bytes res/drawable-xhdpi/portal_ring_outer.png | Bin 0 -> 10394 bytes res/drawable-xhdpi/portal_ring_outer_holo.png | Bin 11037 -> 0 bytes res/drawable-xhdpi/portal_ring_rest.png | Bin 3939 -> 3754 bytes res/drawable-xhdpi/quantum_panel.9.png | Bin 928 -> 0 bytes res/drawable-xhdpi/quantum_panel_bitmap.9.png | Bin 0 -> 843 bytes res/drawable-xhdpi/quantum_panel_dark.9.png | Bin 894 -> 0 bytes .../quantum_panel_dark_bitmap.9.png | Bin 0 -> 842 bytes res/drawable-xhdpi/widget_resize_frame.9.png | Bin 0 -> 494 bytes .../widget_resize_frame_holo.9.png | Bin 1323 -> 0 bytes .../widget_resize_handle_bottom.png | Bin 1434 -> 0 bytes .../widget_resize_handle_left.png | Bin 1467 -> 0 bytes .../widget_resize_handle_right.png | Bin 1483 -> 0 bytes .../widget_resize_handle_top.png | Bin 1463 -> 0 bytes res/drawable-xhdpi/widget_resize_shadow.9.png | Bin 0 -> 669 bytes res/drawable-xxhdpi/apps_customize_bg.png | Bin 244 -> 0 bytes res/drawable-xxhdpi/arrow_dashed.png | Bin 25263 -> 0 bytes .../default_widget_preview_holo.9.png | Bin 1342 -> 0 bytes res/drawable-xxhdpi/hotseat_bg_panel.9.png | Bin 1126 -> 0 bytes .../hotseat_scrubber_holo.9.png | Bin 1088 -> 0 bytes res/drawable-xxhdpi/hotseat_track_holo.9.png | Bin 1213 -> 0 bytes res/drawable-xxhdpi/ic_arrow_back_grey.png | Bin 0 -> 308 bytes .../ic_home_all_apps_holo_dark.png | Bin 3300 -> 0 bytes .../ic_home_google_logo_normal_holo.png | Bin 13630 -> 0 bytes .../ic_home_google_logo_pressed_holo.png | Bin 20843 -> 0 bytes .../ic_home_search_pressed_holo.png | Bin 7909 -> 0 bytes .../ic_home_voice_search_pressed_holo.png | Bin 5445 -> 0 bytes res/drawable-xxhdpi/ic_info_launcher.png | Bin 0 -> 6343 bytes .../ic_launcher_clear_active_holo.png | Bin 2357 -> 0 bytes .../ic_launcher_clear_normal_holo.png | Bin 2738 -> 0 bytes .../ic_launcher_info_active_holo.png | Bin 8022 -> 0 bytes .../ic_launcher_info_normal_holo.png | Bin 7833 -> 0 bytes .../ic_launcher_trashcan_active_holo.png | Bin 4493 -> 0 bytes .../ic_launcher_trashcan_normal_holo.png | Bin 4522 -> 0 bytes .../ic_pageindicator_current.png | Bin 2105 -> 2105 bytes .../ic_pageindicator_current_folder.png | Bin 0 -> 1023 bytes .../ic_pageindicator_default.png | Bin 1634 -> 1634 bytes .../ic_pageindicator_default_folder.png | Bin 0 -> 832 bytes res/drawable-xxhdpi/ic_qs_remote_display.png | Bin 1715 -> 0 bytes .../ic_qs_remote_display_connected.png | Bin 1718 -> 0 bytes res/drawable-xxhdpi/ic_remove_launcher.png | Bin 0 -> 2892 bytes res/drawable-xxhdpi/ic_search_grey.png | Bin 0 -> 1473 bytes res/drawable-xxhdpi/ic_uninstall_launcher.png | Bin 0 -> 2045 bytes .../ic_widget_resize_handle.png | Bin 0 -> 2928 bytes .../overscroll_glow_left.9.png | Bin 4562 -> 0 bytes .../overscroll_glow_right.9.png | Bin 2738 -> 0 bytes .../portal_container_holo.9.png | Bin 4096 -> 0 bytes res/drawable-xxhdpi/portal_ring_inner.png | Bin 0 -> 9918 bytes .../portal_ring_inner_holo.png | Bin 10046 -> 0 bytes ...p_holo.png => portal_ring_inner_nolip.png} | Bin res/drawable-xxhdpi/portal_ring_outer.png | Bin 0 -> 19200 bytes .../portal_ring_outer_holo.png | Bin 16576 -> 0 bytes res/drawable-xxhdpi/quantum_panel.9.png | Bin 1439 -> 0 bytes .../quantum_panel_bitmap.9.png | Bin 0 -> 1323 bytes res/drawable-xxhdpi/quantum_panel_dark.9.png | Bin 1366 -> 0 bytes .../quantum_panel_dark_bitmap.9.png | Bin 0 -> 1332 bytes res/drawable-xxhdpi/widget_resize_frame.9.png | Bin 0 -> 626 bytes .../widget_resize_frame_holo.9.png | Bin 4396 -> 0 bytes .../widget_resize_handle_bottom.png | Bin 4470 -> 0 bytes .../widget_resize_handle_left.png | Bin 4428 -> 0 bytes .../widget_resize_handle_right.png | Bin 4438 -> 0 bytes .../widget_resize_handle_top.png | Bin 4473 -> 0 bytes .../widget_resize_shadow.9.png | Bin 0 -> 975 bytes res/drawable-xxxhdpi/ic_arrow_back_grey.png | Bin 0 -> 359 bytes res/drawable-xxxhdpi/ic_info_launcher.png | Bin 0 -> 5724 bytes res/drawable-xxxhdpi/ic_remove_launcher.png | Bin 0 -> 3210 bytes res/drawable-xxxhdpi/ic_search_grey.png | Bin 0 -> 1996 bytes .../ic_uninstall_launcher.png | Bin 0 -> 2445 bytes .../ic_widget_resize_handle.png | Bin 0 -> 2930 bytes res/drawable-xxxhdpi/portal_ring_inner.png | Bin 0 -> 13024 bytes .../portal_ring_inner_nolip.png | Bin 0 -> 12202 bytes res/drawable-xxxhdpi/portal_ring_outer.png | Bin 0 -> 23360 bytes res/drawable-xxxhdpi/portal_ring_rest.png | Bin 0 -> 8628 bytes .../quantum_panel_bitmap.9.png | Bin 0 -> 2546 bytes .../quantum_panel_dark_bitmap.9.png | Bin 0 -> 2531 bytes .../widget_resize_frame.9.png | Bin 0 -> 1569 bytes .../widget_resize_shadow.9.png | Bin 0 -> 2058 bytes res/drawable/all_apps_search_bg.xml | 20 + ...target_selector.xml => bg_screenpanel.xml} | 12 +- .../container_fastscroll_popup_bg.xml | 27 + .../drawable/quantum_panel.xml | 8 +- .../quantum_panel_dark.xml} | 8 +- res/drawable/quantum_panel_shape.xml | 22 + .../quantum_panel_shape_dark.xml} | 15 +- res/drawable/remove_target_selector.xml | 24 - res/drawable/uninstall_target_selector.xml | 24 - .../widgets_row_divider.xml} | 12 +- res/layout-land/launcher.xml | 16 +- res/layout-land/migration_cling.xml | 1 + res/layout-port/launcher.xml | 13 +- res/layout-port/migration_cling.xml | 1 + res/layout-sw720dp/launcher.xml | 17 +- res/layout/all_apps.xml | 57 + res/layout/all_apps_button.xml | 2 +- res/layout/all_apps_container.xml | 38 + ...ist_item.xml => all_apps_empty_search.xml} | 27 +- res/layout/all_apps_icon.xml | 29 + res/layout/all_apps_prediction_bar_icon.xml | 29 + res/layout/all_apps_search_bar.xml | 72 + res/layout/{application.xml => app_icon.xml} | 2 +- res/layout/apps_customize_pane.xml | 62 - res/layout/apps_customize_progressbar.xml | 22 - res/layout/apps_customize_widget.xml | 105 - res/layout/appwidget_error.xml | 13 +- res/layout/dummy_widget.xml | 35 + res/layout/folder_application.xml | 2 +- res/layout/folder_icon.xml | 4 +- res/layout/page_indicator_marker.xml | 4 +- res/layout/rename_folder.xml | 42 - res/layout/search_drop_target_bar.xml | 19 +- res/layout/user_folder.xml | 96 +- res/layout/widget_cell.xml | 74 + res/layout/widgets_list_row_view.xml | 67 + res/layout/widgets_view.xml | 51 + res/mipmap-xxhdpi/on_boarding_welcome.png | Bin 50862 -> 0 bytes res/values-af/strings.xml | 81 +- res/values-am/strings.xml | 81 +- res/values-ar/strings.xml | 81 +- res/values-az-rAZ/strings.xml | 81 +- res/values-be/strings.xml | 6 - res/values-bg/strings.xml | 81 +- res/values-bn-rBD/strings.xml | 81 +- res/values-ca/strings.xml | 81 +- res/values-cs/strings.xml | 81 +- res/values-da/strings.xml | 81 +- res/values-de/strings.xml | 81 +- res/values-el/strings.xml | 81 +- res/values-en-rAU/strings.xml | 81 +- res/values-en-rGB/strings.xml | 81 +- res/values-en-rIN/strings.xml | 81 +- res/values-es-rUS/strings.xml | 81 +- res/values-es/strings.xml | 81 +- res/values-et-rEE/strings.xml | 81 +- res/values-et/strings.xml | 61 - res/values-eu-rES/strings.xml | 81 +- res/values-fa/strings.xml | 81 +- res/values-fi/strings.xml | 81 +- res/values-fr-rCA/strings.xml | 81 +- res/values-fr/strings.xml | 81 +- res/values-gl-rES/strings.xml | 81 +- res/values-gu-rIN/strings.xml | 85 +- res/values-hi/strings.xml | 81 +- res/values-hr/strings.xml | 81 +- res/values-hu/strings.xml | 81 +- res/values-hy-rAM/strings.xml | 81 +- res/values-in/strings.xml | 81 +- res/values-is-rIS/strings.xml | 81 +- res/values-it/strings.xml | 81 +- res/values-iw/strings.xml | 81 +- res/values-ja/strings.xml | 81 +- res/values-ka-rGE/strings.xml | 81 +- res/values-kk-rKZ/strings.xml | 81 +- res/values-km-rKH/strings.xml | 81 +- res/values-kn-rIN/strings.xml | 81 +- res/values-ko/strings.xml | 81 +- res/values-ky-rKG/strings.xml | 81 +- res/values-land/dimens.xml | 5 - res/values-land/styles.xml | 2 +- res/values-lo-rLA/strings.xml | 81 +- res/values-lt/strings.xml | 81 +- res/values-lv/strings.xml | 81 +- res/values-mk-rMK/strings.xml | 81 +- res/values-ml-rIN/strings.xml | 81 +- res/values-mn-rMN/strings.xml | 81 +- res/values-mr-rIN/strings.xml | 81 +- res/values-ms-rMY/strings.xml | 81 +- res/values-ms/strings.xml | 61 - res/values-my-rMM/strings.xml | 81 +- res/values-nb/strings.xml | 81 +- res/values-ne-rNP/strings.xml | 83 +- res/values-nl/strings.xml | 81 +- res/values-pa-rIN/strings.xml | 81 +- res/values-pl/strings.xml | 81 +- res/values-port/dimens.xml | 21 - res/values-pt-rPT/strings.xml | 81 +- res/values-pt/strings.xml | 81 +- res/values-rm/strings.xml | 2 - res/values-ro/strings.xml | 81 +- res/values-ru/strings.xml | 81 +- res/values-si-rLK/strings.xml | 81 +- res/values-sk/strings.xml | 81 +- res/values-sl/strings.xml | 81 +- res/values-sq-rAL/strings.xml | 81 +- res/values-sr/strings.xml | 81 +- res/values-sv/strings.xml | 81 +- res/values-sw/strings.xml | 81 +- res/values-sw600dp/config.xml | 4 - res/values-sw600dp/dimens.xml | 12 +- res/values-sw600dp/styles.xml | 21 - res/values-sw720dp-land/dimens.xml | 4 - res/values-sw720dp/config.xml | 9 +- res/values-sw720dp/dimens.xml | 9 +- res/values-sw720dp/styles.xml | 2 +- res/values-ta-rIN/strings.xml | 83 +- res/values-te-rIN/strings.xml | 81 +- res/values-th/strings.xml | 81 +- res/values-tl/strings.xml | 81 +- res/values-tr/strings.xml | 81 +- res/values-uk/strings.xml | 81 +- res/values-ur-rPK/strings.xml | 81 +- res/values-uz-rUZ/strings.xml | 81 +- res/values-v17/styles.xml | 6 - res/values-vi/strings.xml | 81 +- res/values-zh-rCN/strings.xml | 81 +- res/values-zh-rHK/strings.xml | 81 +- res/values-zh-rTW/strings.xml | 81 +- res/values-zu/strings.xml | 81 +- res/values/attrs.xml | 45 +- res/values/colors.xml | 32 +- res/values/config.xml | 56 +- res/values/dimens.xml | 105 +- res/values/strings.xml | 225 +- res/values/styles.xml | 26 +- res/xml/app_target_browser.xml | 23 + .../app_target_camera.xml} | 12 +- .../app_target_email.xml} | 12 +- res/xml/app_target_gallery.xml | 23 + res/xml/app_target_messenger.xml | 26 + res/xml/app_target_phone.xml | 24 + res/xml/launcher_preferences.xml | 26 + res/xml/update_workspace.xml | 49 - src/com/android/launcher3/AllAppsList.java | 22 +- src/com/android/launcher3/AppInfo.java | 39 +- .../launcher3/AppWidgetResizeFrame.java | 133 +- .../launcher3/AppWidgetsRestoredReceiver.java | 5 + .../launcher3/AppsCustomizeCellLayout.java | 71 - .../launcher3/AppsCustomizePagedView.java | 1567 ------------- .../launcher3/AppsCustomizeTabHost.java | 220 -- .../android/launcher3/AutoInstallsLayout.java | 81 +- .../android/launcher3/BaseContainerView.java | 137 ++ .../android/launcher3/BaseRecyclerView.java | 285 +++ .../BaseRecyclerViewFastScrollBar.java | 235 ++ .../BaseRecyclerViewFastScrollPopup.java | 160 ++ src/com/android/launcher3/BubbleTextView.java | 352 ++- .../android/launcher3/ButtonDropTarget.java | 237 +- src/com/android/launcher3/CellLayout.java | 729 ++---- .../launcher3/CheckLongPressHelper.java | 32 +- .../android/launcher3/ClickShadowView.java | 112 + .../launcher3/CommonAppTypeParser.java | 153 ++ .../android/launcher3/CustomAppWidget.java | 14 + .../launcher3/DefaultLayoutParser.java | 46 +- .../android/launcher3/DeferredHandler.java | 51 +- .../android/launcher3/DeleteDropTarget.java | 514 +--- src/com/android/launcher3/DeviceProfile.java | 682 ++---- src/com/android/launcher3/DragController.java | 117 +- src/com/android/launcher3/DragLayer.java | 151 +- src/com/android/launcher3/DragSource.java | 2 +- src/com/android/launcher3/DragView.java | 88 +- src/com/android/launcher3/DropTarget.java | 65 +- src/com/android/launcher3/DummyWidget.java | 50 + src/com/android/launcher3/DynamicGrid.java | 109 - .../android/launcher3/FastBitmapDrawable.java | 4 +- src/com/android/launcher3/FastBitmapView.java | 58 - .../launcher3/FirstFrameAnimatorHelper.java | 4 +- src/com/android/launcher3/FocusHelper.java | 957 +++----- .../android/launcher3/FocusIndicatorView.java | 52 +- .../android/launcher3/FocusOnlyTabWidget.java | 86 - src/com/android/launcher3/Folder.java | 1012 ++++---- .../launcher3/FolderAutoScrollHelper.java | 59 - src/com/android/launcher3/FolderIcon.java | 53 +- src/com/android/launcher3/FolderInfo.java | 46 +- .../android/launcher3/FolderPagedView.java | 678 ++++++ .../launcher3/HolographicOutlineHelper.java | 106 +- src/com/android/launcher3/Hotseat.java | 183 +- src/com/android/launcher3/IconCache.java | 812 +++++-- src/com/android/launcher3/InfoDropTarget.java | 99 +- src/com/android/launcher3/Insettable.java | 4 + .../launcher3/InstallShortcutReceiver.java | 108 +- .../launcher3/InterruptibleInOutAnimator.java | 4 +- .../launcher3/InvariantDeviceProfile.java | 303 +++ src/com/android/launcher3/ItemInfo.java | 47 +- src/com/android/launcher3/Launcher.java | 2088 +++++++---------- .../android/launcher3/LauncherAnimUtils.java | 13 +- .../android/launcher3/LauncherAppState.java | 193 +- .../launcher3/LauncherAppWidgetHost.java | 57 +- .../launcher3/LauncherAppWidgetHostView.java | 36 +- .../launcher3/LauncherAppWidgetInfo.java | 16 +- .../LauncherAppWidgetProviderInfo.java | 113 + .../launcher3/LauncherBackupAgentHelper.java | 10 +- .../launcher3/LauncherBackupHelper.java | 379 +-- .../android/launcher3/LauncherCallbacks.java | 19 + src/com/android/launcher3/LauncherClings.java | 14 +- src/com/android/launcher3/LauncherFiles.java | 22 +- src/com/android/launcher3/LauncherModel.java | 1950 +++++++-------- .../android/launcher3/LauncherProvider.java | 1175 ++++------ .../LauncherProviderChangeListener.java | 4 + .../android/launcher3/LauncherScroller.java | 5 +- .../android/launcher3/LauncherSettings.java | 133 +- .../LauncherStateTransitionAnimation.java | 762 ++++++ ...ation.java => LauncherTransitionable.java} | 27 +- src/com/android/launcher3/MemoryTracker.java | 2 +- .../launcher3/PackageChangedReceiver.java | 21 - src/com/android/launcher3/PagedView.java | 1133 +++------ .../launcher3/PagedViewCellLayout.java | 492 ---- .../PagedViewCellLayoutChildren.java | 161 -- .../launcher3/PagedViewGridLayout.java | 121 - .../android/launcher3/PagedViewWidget.java | 262 --- .../launcher3/PagedViewWidgetImageView.java | 49 - .../PagedViewWithDraggableItems.java | 174 -- src/com/android/launcher3/Partner.java | 50 +- .../android/launcher3/PendingAddItemInfo.java | 87 +- .../launcher3/PendingAppWidgetHostView.java | 18 +- .../launcher3/PreloadIconDrawable.java | 5 +- .../launcher3/SearchDropTargetBar.java | 143 +- .../android/launcher3/SettingsActivity.java | 76 + .../launcher3/ShortcutAndWidgetContainer.java | 21 +- src/com/android/launcher3/ShortcutInfo.java | 62 +- .../android/launcher3/SmoothPagedView.java | 185 -- src/com/android/launcher3/Stats.java | 216 +- .../android/launcher3/StylusEventHelper.java | 84 + .../launcher3/UninstallDropTarget.java | 132 ++ .../launcher3/UninstallShortcutReceiver.java | 131 -- .../launcher3/UserInitializeReceiver.java | 32 - src/com/android/launcher3/Utilities.java | 273 ++- src/com/android/launcher3/WeightWatcher.java | 6 +- .../launcher3/WidgetPreviewLoader.java | 1142 +++++---- src/com/android/launcher3/Workspace.java | 1465 ++++-------- .../WorkspaceStateTransitionAnimation.java | 587 +++++ .../DragAndDropAccessibilityDelegate.java | 136 ++ .../FolderAccessibilityHelper.java | 56 + .../LauncherAccessibilityDelegate.java | 434 ++++ .../OverviewScreenAccessibilityDelegate.java | 95 + .../WorkspaceAccessibilityHelper.java | 166 ++ .../allapps/AllAppsContainerView.java | 638 +++++ .../launcher3/allapps/AllAppsGridAdapter.java | 453 ++++ .../allapps/AllAppsRecyclerView.java | 327 +++ .../AllAppsRecyclerViewContainerView.java | 72 + .../allapps/AllAppsSearchBarController.java | 100 + .../allapps/AllAppsSearchEditView.java | 65 + .../allapps/AlphabeticalAppsList.java | 590 +++++ .../allapps/DefaultAppSearchAlgorithm.java | 94 + .../allapps/DefaultAppSearchController.java | 269 +++ .../compat/AlphabeticIndexCompat.java | 170 ++ .../compat/AppWidgetManagerCompat.java | 10 +- .../compat/AppWidgetManagerCompatV16.java | 14 +- .../compat/AppWidgetManagerCompatVL.java | 27 +- .../compat/LauncherActivityInfoCompat.java | 9 + .../compat/LauncherActivityInfoCompatV16.java | 45 +- .../compat/LauncherActivityInfoCompatVL.java | 4 +- .../compat/LauncherAppsCompatV16.java | 6 +- .../compat/LauncherAppsCompatVL.java | 4 +- .../compat/PackageInstallerCompat.java | 17 +- .../compat/PackageInstallerCompatV16.java | 151 +- .../compat/PackageInstallerCompatVL.java | 152 +- .../launcher3/compat/UserHandleCompat.java | 5 +- .../launcher3/compat/UserManagerCompat.java | 1 + .../compat/UserManagerCompatV16.java | 5 + .../compat/UserManagerCompatV17.java | 8 +- .../launcher3/compat/UserManagerCompatVL.java | 25 +- .../launcher3/config/ProviderConfig.java | 2 +- .../model/AbstractUserComparator.java | 67 + .../launcher3/model/AppNameComparator.java | 101 + .../launcher3/model/PackageItemInfo.java | 65 + .../WidgetsAndShortcutNameComparator.java | 68 + .../android/launcher3/model/WidgetsModel.java | 170 ++ .../{ => testing}/LauncherExtension.java | 37 +- .../android/launcher3/util/ComponentKey.java | 81 + .../launcher3/util/CursorIconInfo.java | 70 + .../launcher3/util/FlingAnimation.java | 104 + .../android/launcher3/util/FocusLogic.java | 507 ++++ .../launcher3/util/LauncherEdgeEffect.java | 365 +++ .../android/launcher3/util/LongArrayMap.java | 65 + .../util/ManagedProfileHeuristic.java | 350 +++ .../launcher3/util/RevealOutlineProvider.java | 49 + src/com/android/launcher3/util/Thunk.java | 43 + .../util/UiThreadCircularReveal.java | 57 + .../launcher3/util/WallpaperUtils.java | 123 + .../widget/PendingAddShortcutInfo.java | 44 + .../widget/PendingAddWidgetInfo.java | 75 + .../android/launcher3/widget/WidgetCell.java | 230 ++ .../widget/WidgetHostViewLoader.java | 155 ++ .../launcher3/widget/WidgetImageView.java | 97 + .../widget/WidgetsContainerView.java | 370 +++ .../launcher3/widget/WidgetsListAdapter.java | 215 ++ .../launcher3/widget/WidgetsRecyclerView.java | 158 ++ .../WidgetsRowViewHolder.java} | 28 +- tests/Android.mk | 27 +- tests/{stress => }/AndroidManifest.xml | 6 +- tests/res/values/string.xml | 21 + .../launcher3/InvariantDeviceProfileTest.java | 123 + .../launcher3/util/FocusLogicTest.java | 60 + tests/stress/Android.mk | 31 - .../stress/LauncherRotationStressTest.java | 63 - util/com/android/launcher3/DecoderRing.java | 3 - 630 files changed, 23761 insertions(+), 21647 deletions(-) create mode 100644 WallpaperPicker/res/drawable-xxxhdpi/ic_images.png delete mode 100644 WallpaperPicker/res/values-sw720dp/dimens.xml create mode 100644 WallpaperPicker/res/values-v21/styles.xml delete mode 100644 WallpaperPicker/src/android/util/Pools.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java rename WallpaperPicker/src/com/android/gallery3d/{util => glrenderer}/IntArray.java (97%) rename {src => WallpaperPicker/src}/com/android/launcher3/LauncherWallpaperPickerActivity.java (69%) delete mode 100644 WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java create mode 100644 WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java delete mode 100644 WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java delete mode 100644 res/drawable-hdpi/apps_customize_bg.png delete mode 100644 res/drawable-hdpi/bg_appwidget_error.9.png delete mode 100644 res/drawable-hdpi/home_press.9.png create mode 100755 res/drawable-hdpi/ic_arrow_back_grey.png delete mode 100644 res/drawable-hdpi/ic_home_all_apps_holo_dark.png create mode 100644 res/drawable-hdpi/ic_info_launcher.png delete mode 100644 res/drawable-hdpi/ic_launcher_clear_active_holo.png delete mode 100644 res/drawable-hdpi/ic_launcher_clear_normal_holo.png delete mode 100644 res/drawable-hdpi/ic_launcher_info_active_holo.png delete mode 100644 res/drawable-hdpi/ic_launcher_info_normal_holo.png delete mode 100644 res/drawable-hdpi/ic_launcher_market_holo.png delete mode 100644 res/drawable-hdpi/ic_launcher_trashcan_active_holo.png delete mode 100644 res/drawable-hdpi/ic_launcher_trashcan_normal_holo.png create mode 100644 res/drawable-hdpi/ic_pageindicator_current_folder.png create mode 100644 res/drawable-hdpi/ic_pageindicator_default_folder.png create mode 100644 res/drawable-hdpi/ic_remove_launcher.png create mode 100755 res/drawable-hdpi/ic_search_grey.png create mode 100644 res/drawable-hdpi/ic_uninstall_launcher.png create mode 100644 res/drawable-hdpi/ic_widget_resize_handle.png delete mode 100644 res/drawable-hdpi/on_boarding_welcome.png delete mode 100644 res/drawable-hdpi/overscroll_glow_left.9.png delete mode 100644 res/drawable-hdpi/overscroll_glow_right.9.png delete mode 100644 res/drawable-hdpi/paged_view_indicator.9.png delete mode 100644 res/drawable-hdpi/portal_container_holo.9.png create mode 100644 res/drawable-hdpi/portal_ring_inner.png delete mode 100644 res/drawable-hdpi/portal_ring_inner_holo.png create mode 100644 res/drawable-hdpi/portal_ring_inner_nolip.png delete mode 100644 res/drawable-hdpi/portal_ring_inner_nolip_holo.png create mode 100644 res/drawable-hdpi/portal_ring_outer.png delete mode 100644 res/drawable-hdpi/portal_ring_outer_holo.png delete mode 100644 res/drawable-hdpi/quantum_panel.9.png create mode 100644 res/drawable-hdpi/quantum_panel_bitmap.9.png delete mode 100644 res/drawable-hdpi/quantum_panel_dark.9.png create mode 100644 res/drawable-hdpi/quantum_panel_dark_bitmap.9.png create mode 100644 res/drawable-hdpi/widget_resize_frame.9.png delete mode 100644 res/drawable-hdpi/widget_resize_frame_holo.9.png delete mode 100644 res/drawable-hdpi/widget_resize_handle_bottom.png delete mode 100644 res/drawable-hdpi/widget_resize_handle_left.png delete mode 100644 res/drawable-hdpi/widget_resize_handle_right.png delete mode 100644 res/drawable-hdpi/widget_resize_handle_top.png create mode 100644 res/drawable-hdpi/widget_resize_shadow.9.png create mode 100644 res/drawable-ldrtl/container_fastscroll_popup_bg.xml delete mode 100644 res/drawable-mdpi/apps_customize_bg.png delete mode 100644 res/drawable-mdpi/bg_appwidget_error.9.png delete mode 100644 res/drawable-mdpi/home_press.9.png create mode 100755 res/drawable-mdpi/ic_arrow_back_grey.png delete mode 100644 res/drawable-mdpi/ic_home_all_apps_holo_dark.png create mode 100644 res/drawable-mdpi/ic_info_launcher.png delete mode 100644 res/drawable-mdpi/ic_launcher_clear_active_holo.png delete mode 100644 res/drawable-mdpi/ic_launcher_clear_normal_holo.png delete mode 100644 res/drawable-mdpi/ic_launcher_info_active_holo.png delete mode 100644 res/drawable-mdpi/ic_launcher_info_normal_holo.png delete mode 100644 res/drawable-mdpi/ic_launcher_market_holo.png delete mode 100644 res/drawable-mdpi/ic_launcher_trashcan_active_holo.png delete mode 100644 res/drawable-mdpi/ic_launcher_trashcan_normal_holo.png create mode 100644 res/drawable-mdpi/ic_pageindicator_current_folder.png create mode 100644 res/drawable-mdpi/ic_pageindicator_default_folder.png create mode 100644 res/drawable-mdpi/ic_remove_launcher.png create mode 100755 res/drawable-mdpi/ic_search_grey.png delete mode 100644 res/drawable-mdpi/ic_setting_icn.png create mode 100644 res/drawable-mdpi/ic_uninstall_launcher.png create mode 100644 res/drawable-mdpi/ic_widget_resize_handle.png delete mode 100644 res/drawable-mdpi/on_boarding_welcome.png delete mode 100644 res/drawable-mdpi/overscroll_glow_left.9.png delete mode 100644 res/drawable-mdpi/overscroll_glow_right.9.png delete mode 100644 res/drawable-mdpi/paged_view_indicator.9.png delete mode 100644 res/drawable-mdpi/portal_container_holo.9.png create mode 100644 res/drawable-mdpi/portal_ring_inner.png delete mode 100644 res/drawable-mdpi/portal_ring_inner_holo.png create mode 100644 res/drawable-mdpi/portal_ring_inner_nolip.png delete mode 100644 res/drawable-mdpi/portal_ring_inner_nolip_holo.png create mode 100644 res/drawable-mdpi/portal_ring_outer.png delete mode 100644 res/drawable-mdpi/portal_ring_outer_holo.png delete mode 100644 res/drawable-mdpi/quantum_panel.9.png create mode 100644 res/drawable-mdpi/quantum_panel_bitmap.9.png delete mode 100644 res/drawable-mdpi/quantum_panel_dark.9.png create mode 100644 res/drawable-mdpi/quantum_panel_dark_bitmap.9.png create mode 100644 res/drawable-mdpi/widget_resize_frame.9.png delete mode 100644 res/drawable-mdpi/widget_resize_frame_holo.9.png delete mode 100644 res/drawable-mdpi/widget_resize_handle_bottom.png delete mode 100644 res/drawable-mdpi/widget_resize_handle_left.png delete mode 100644 res/drawable-mdpi/widget_resize_handle_right.png delete mode 100644 res/drawable-mdpi/widget_resize_handle_top.png create mode 100644 res/drawable-mdpi/widget_resize_shadow.9.png create mode 100644 res/drawable-v21/quantum_panel.xml create mode 100644 res/drawable-v21/quantum_panel_dark.xml delete mode 100644 res/drawable-xhdpi/apps_customize_bg.png delete mode 100644 res/drawable-xhdpi/bg_appwidget_error.9.png delete mode 100644 res/drawable-xhdpi/home_press.9.png create mode 100755 res/drawable-xhdpi/ic_arrow_back_grey.png delete mode 100644 res/drawable-xhdpi/ic_home_all_apps_holo_dark.png create mode 100644 res/drawable-xhdpi/ic_info_launcher.png delete mode 100644 res/drawable-xhdpi/ic_launcher_clear_active_holo.png delete mode 100644 res/drawable-xhdpi/ic_launcher_clear_normal_holo.png delete mode 100644 res/drawable-xhdpi/ic_launcher_info_active_holo.png delete mode 100644 res/drawable-xhdpi/ic_launcher_info_normal_holo.png delete mode 100644 res/drawable-xhdpi/ic_launcher_market_holo.png delete mode 100644 res/drawable-xhdpi/ic_launcher_trashcan_active_holo.png delete mode 100644 res/drawable-xhdpi/ic_launcher_trashcan_normal_holo.png create mode 100644 res/drawable-xhdpi/ic_pageindicator_current_folder.png create mode 100644 res/drawable-xhdpi/ic_pageindicator_default_folder.png create mode 100644 res/drawable-xhdpi/ic_remove_launcher.png create mode 100755 res/drawable-xhdpi/ic_search_grey.png create mode 100644 res/drawable-xhdpi/ic_uninstall_launcher.png create mode 100644 res/drawable-xhdpi/ic_widget_resize_handle.png delete mode 100644 res/drawable-xhdpi/on_boarding_welcome.png delete mode 100644 res/drawable-xhdpi/overscroll_glow_left.9.png delete mode 100644 res/drawable-xhdpi/overscroll_glow_right.9.png delete mode 100644 res/drawable-xhdpi/paged_view_indicator.9.png delete mode 100644 res/drawable-xhdpi/portal_container_holo.9.png create mode 100644 res/drawable-xhdpi/portal_ring_inner.png delete mode 100644 res/drawable-xhdpi/portal_ring_inner_holo.png create mode 100644 res/drawable-xhdpi/portal_ring_inner_nolip.png delete mode 100644 res/drawable-xhdpi/portal_ring_inner_nolip_holo.png create mode 100644 res/drawable-xhdpi/portal_ring_outer.png delete mode 100644 res/drawable-xhdpi/portal_ring_outer_holo.png delete mode 100644 res/drawable-xhdpi/quantum_panel.9.png create mode 100644 res/drawable-xhdpi/quantum_panel_bitmap.9.png delete mode 100644 res/drawable-xhdpi/quantum_panel_dark.9.png create mode 100644 res/drawable-xhdpi/quantum_panel_dark_bitmap.9.png create mode 100644 res/drawable-xhdpi/widget_resize_frame.9.png delete mode 100644 res/drawable-xhdpi/widget_resize_frame_holo.9.png delete mode 100644 res/drawable-xhdpi/widget_resize_handle_bottom.png delete mode 100644 res/drawable-xhdpi/widget_resize_handle_left.png delete mode 100644 res/drawable-xhdpi/widget_resize_handle_right.png delete mode 100644 res/drawable-xhdpi/widget_resize_handle_top.png create mode 100644 res/drawable-xhdpi/widget_resize_shadow.9.png delete mode 100644 res/drawable-xxhdpi/apps_customize_bg.png delete mode 100644 res/drawable-xxhdpi/arrow_dashed.png delete mode 100644 res/drawable-xxhdpi/default_widget_preview_holo.9.png delete mode 100644 res/drawable-xxhdpi/hotseat_bg_panel.9.png delete mode 100644 res/drawable-xxhdpi/hotseat_scrubber_holo.9.png delete mode 100644 res/drawable-xxhdpi/hotseat_track_holo.9.png create mode 100755 res/drawable-xxhdpi/ic_arrow_back_grey.png delete mode 100644 res/drawable-xxhdpi/ic_home_all_apps_holo_dark.png delete mode 100644 res/drawable-xxhdpi/ic_home_google_logo_normal_holo.png delete mode 100644 res/drawable-xxhdpi/ic_home_google_logo_pressed_holo.png delete mode 100644 res/drawable-xxhdpi/ic_home_search_pressed_holo.png delete mode 100644 res/drawable-xxhdpi/ic_home_voice_search_pressed_holo.png create mode 100644 res/drawable-xxhdpi/ic_info_launcher.png delete mode 100644 res/drawable-xxhdpi/ic_launcher_clear_active_holo.png delete mode 100644 res/drawable-xxhdpi/ic_launcher_clear_normal_holo.png delete mode 100644 res/drawable-xxhdpi/ic_launcher_info_active_holo.png delete mode 100644 res/drawable-xxhdpi/ic_launcher_info_normal_holo.png delete mode 100644 res/drawable-xxhdpi/ic_launcher_trashcan_active_holo.png delete mode 100644 res/drawable-xxhdpi/ic_launcher_trashcan_normal_holo.png create mode 100644 res/drawable-xxhdpi/ic_pageindicator_current_folder.png create mode 100644 res/drawable-xxhdpi/ic_pageindicator_default_folder.png delete mode 100644 res/drawable-xxhdpi/ic_qs_remote_display.png delete mode 100644 res/drawable-xxhdpi/ic_qs_remote_display_connected.png create mode 100644 res/drawable-xxhdpi/ic_remove_launcher.png create mode 100755 res/drawable-xxhdpi/ic_search_grey.png create mode 100644 res/drawable-xxhdpi/ic_uninstall_launcher.png create mode 100644 res/drawable-xxhdpi/ic_widget_resize_handle.png delete mode 100644 res/drawable-xxhdpi/overscroll_glow_left.9.png delete mode 100644 res/drawable-xxhdpi/overscroll_glow_right.9.png delete mode 100644 res/drawable-xxhdpi/portal_container_holo.9.png create mode 100644 res/drawable-xxhdpi/portal_ring_inner.png delete mode 100644 res/drawable-xxhdpi/portal_ring_inner_holo.png rename res/drawable-xxhdpi/{portal_ring_inner_nolip_holo.png => portal_ring_inner_nolip.png} (100%) create mode 100644 res/drawable-xxhdpi/portal_ring_outer.png delete mode 100644 res/drawable-xxhdpi/portal_ring_outer_holo.png delete mode 100644 res/drawable-xxhdpi/quantum_panel.9.png create mode 100644 res/drawable-xxhdpi/quantum_panel_bitmap.9.png delete mode 100644 res/drawable-xxhdpi/quantum_panel_dark.9.png create mode 100644 res/drawable-xxhdpi/quantum_panel_dark_bitmap.9.png create mode 100644 res/drawable-xxhdpi/widget_resize_frame.9.png delete mode 100644 res/drawable-xxhdpi/widget_resize_frame_holo.9.png delete mode 100644 res/drawable-xxhdpi/widget_resize_handle_bottom.png delete mode 100644 res/drawable-xxhdpi/widget_resize_handle_left.png delete mode 100644 res/drawable-xxhdpi/widget_resize_handle_right.png delete mode 100644 res/drawable-xxhdpi/widget_resize_handle_top.png create mode 100644 res/drawable-xxhdpi/widget_resize_shadow.9.png create mode 100755 res/drawable-xxxhdpi/ic_arrow_back_grey.png create mode 100644 res/drawable-xxxhdpi/ic_info_launcher.png create mode 100644 res/drawable-xxxhdpi/ic_remove_launcher.png create mode 100755 res/drawable-xxxhdpi/ic_search_grey.png create mode 100644 res/drawable-xxxhdpi/ic_uninstall_launcher.png create mode 100644 res/drawable-xxxhdpi/ic_widget_resize_handle.png create mode 100644 res/drawable-xxxhdpi/portal_ring_inner.png create mode 100644 res/drawable-xxxhdpi/portal_ring_inner_nolip.png create mode 100644 res/drawable-xxxhdpi/portal_ring_outer.png create mode 100644 res/drawable-xxxhdpi/portal_ring_rest.png create mode 100644 res/drawable-xxxhdpi/quantum_panel_bitmap.9.png create mode 100644 res/drawable-xxxhdpi/quantum_panel_dark_bitmap.9.png create mode 100644 res/drawable-xxxhdpi/widget_resize_frame.9.png create mode 100644 res/drawable-xxxhdpi/widget_resize_shadow.9.png create mode 100644 res/drawable/all_apps_search_bg.xml rename res/drawable/{info_target_selector.xml => bg_screenpanel.xml} (75%) create mode 100644 res/drawable/container_fastscroll_popup_bg.xml rename WallpaperPicker/res/values-sw600dp/config.xml => res/drawable/quantum_panel.xml (76%) rename res/{values-sw340dp-port/config.xml => drawable/quantum_panel_dark.xml} (76%) create mode 100644 res/drawable/quantum_panel_shape.xml rename res/{layout/apps_customize_application.xml => drawable/quantum_panel_shape_dark.xml} (68%) delete mode 100644 res/drawable/remove_target_selector.xml delete mode 100644 res/drawable/uninstall_target_selector.xml rename res/{values-land/config.xml => drawable/widgets_row_divider.xml} (70%) create mode 100644 res/layout/all_apps.xml create mode 100644 res/layout/all_apps_container.xml rename res/layout/{add_list_item.xml => all_apps_empty_search.xml} (58%) create mode 100644 res/layout/all_apps_icon.xml create mode 100644 res/layout/all_apps_prediction_bar_icon.xml create mode 100644 res/layout/all_apps_search_bar.xml rename res/layout/{application.xml => app_icon.xml} (96%) delete mode 100644 res/layout/apps_customize_pane.xml delete mode 100644 res/layout/apps_customize_progressbar.xml delete mode 100644 res/layout/apps_customize_widget.xml create mode 100644 res/layout/dummy_widget.xml delete mode 100644 res/layout/rename_folder.xml create mode 100644 res/layout/widget_cell.xml create mode 100644 res/layout/widgets_list_row_view.xml create mode 100644 res/layout/widgets_view.xml delete mode 100644 res/mipmap-xxhdpi/on_boarding_welcome.png delete mode 100644 res/values-port/dimens.xml delete mode 100644 res/values-sw600dp/styles.xml delete mode 100644 res/values-v17/styles.xml create mode 100644 res/xml/app_target_browser.xml rename res/{anim/fade_out_fast.xml => xml/app_target_camera.xml} (64%) rename res/{anim/fade_in_fast.xml => xml/app_target_email.xml} (65%) create mode 100644 res/xml/app_target_gallery.xml create mode 100644 res/xml/app_target_messenger.xml create mode 100644 res/xml/app_target_phone.xml create mode 100644 res/xml/launcher_preferences.xml delete mode 100644 res/xml/update_workspace.xml delete mode 100644 src/com/android/launcher3/AppsCustomizeCellLayout.java delete mode 100644 src/com/android/launcher3/AppsCustomizePagedView.java delete mode 100644 src/com/android/launcher3/AppsCustomizeTabHost.java create mode 100644 src/com/android/launcher3/BaseContainerView.java create mode 100644 src/com/android/launcher3/BaseRecyclerView.java create mode 100644 src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java create mode 100644 src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java create mode 100644 src/com/android/launcher3/ClickShadowView.java create mode 100644 src/com/android/launcher3/CommonAppTypeParser.java create mode 100644 src/com/android/launcher3/CustomAppWidget.java create mode 100644 src/com/android/launcher3/DummyWidget.java delete mode 100644 src/com/android/launcher3/DynamicGrid.java delete mode 100644 src/com/android/launcher3/FastBitmapView.java delete mode 100644 src/com/android/launcher3/FocusOnlyTabWidget.java delete mode 100644 src/com/android/launcher3/FolderAutoScrollHelper.java create mode 100644 src/com/android/launcher3/FolderPagedView.java create mode 100644 src/com/android/launcher3/InvariantDeviceProfile.java create mode 100644 src/com/android/launcher3/LauncherAppWidgetProviderInfo.java create mode 100644 src/com/android/launcher3/LauncherStateTransitionAnimation.java rename src/com/android/launcher3/{LauncherApplication.java => LauncherTransitionable.java} (55%) delete mode 100644 src/com/android/launcher3/PackageChangedReceiver.java delete mode 100644 src/com/android/launcher3/PagedViewCellLayout.java delete mode 100644 src/com/android/launcher3/PagedViewCellLayoutChildren.java delete mode 100644 src/com/android/launcher3/PagedViewGridLayout.java delete mode 100644 src/com/android/launcher3/PagedViewWidget.java delete mode 100644 src/com/android/launcher3/PagedViewWidgetImageView.java delete mode 100644 src/com/android/launcher3/PagedViewWithDraggableItems.java create mode 100644 src/com/android/launcher3/SettingsActivity.java delete mode 100644 src/com/android/launcher3/SmoothPagedView.java create mode 100644 src/com/android/launcher3/StylusEventHelper.java create mode 100644 src/com/android/launcher3/UninstallDropTarget.java delete mode 100644 src/com/android/launcher3/UninstallShortcutReceiver.java delete mode 100644 src/com/android/launcher3/UserInitializeReceiver.java create mode 100644 src/com/android/launcher3/WorkspaceStateTransitionAnimation.java create mode 100644 src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java create mode 100644 src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java create mode 100644 src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java create mode 100644 src/com/android/launcher3/accessibility/OverviewScreenAccessibilityDelegate.java create mode 100644 src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java create mode 100644 src/com/android/launcher3/allapps/AllAppsContainerView.java create mode 100644 src/com/android/launcher3/allapps/AllAppsGridAdapter.java create mode 100644 src/com/android/launcher3/allapps/AllAppsRecyclerView.java create mode 100644 src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java create mode 100644 src/com/android/launcher3/allapps/AllAppsSearchBarController.java create mode 100644 src/com/android/launcher3/allapps/AllAppsSearchEditView.java create mode 100644 src/com/android/launcher3/allapps/AlphabeticalAppsList.java create mode 100644 src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java create mode 100644 src/com/android/launcher3/allapps/DefaultAppSearchController.java create mode 100644 src/com/android/launcher3/compat/AlphabeticIndexCompat.java create mode 100644 src/com/android/launcher3/model/AbstractUserComparator.java create mode 100644 src/com/android/launcher3/model/AppNameComparator.java create mode 100644 src/com/android/launcher3/model/PackageItemInfo.java create mode 100644 src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java create mode 100644 src/com/android/launcher3/model/WidgetsModel.java rename src/com/android/launcher3/{ => testing}/LauncherExtension.java (90%) create mode 100644 src/com/android/launcher3/util/ComponentKey.java create mode 100644 src/com/android/launcher3/util/CursorIconInfo.java create mode 100644 src/com/android/launcher3/util/FlingAnimation.java create mode 100644 src/com/android/launcher3/util/FocusLogic.java create mode 100644 src/com/android/launcher3/util/LauncherEdgeEffect.java create mode 100644 src/com/android/launcher3/util/LongArrayMap.java create mode 100644 src/com/android/launcher3/util/ManagedProfileHeuristic.java create mode 100644 src/com/android/launcher3/util/RevealOutlineProvider.java create mode 100644 src/com/android/launcher3/util/Thunk.java create mode 100644 src/com/android/launcher3/util/UiThreadCircularReveal.java create mode 100644 src/com/android/launcher3/util/WallpaperUtils.java create mode 100644 src/com/android/launcher3/widget/PendingAddShortcutInfo.java create mode 100644 src/com/android/launcher3/widget/PendingAddWidgetInfo.java create mode 100644 src/com/android/launcher3/widget/WidgetCell.java create mode 100644 src/com/android/launcher3/widget/WidgetHostViewLoader.java create mode 100644 src/com/android/launcher3/widget/WidgetImageView.java create mode 100644 src/com/android/launcher3/widget/WidgetsContainerView.java create mode 100644 src/com/android/launcher3/widget/WidgetsListAdapter.java create mode 100644 src/com/android/launcher3/widget/WidgetsRecyclerView.java rename src/com/android/launcher3/{LauncherAnimatorUpdateListener.java => widget/WidgetsRowViewHolder.java} (52%) rename tests/{stress => }/AndroidManifest.xml (84%) create mode 100644 tests/res/values/string.xml create mode 100644 tests/src/com/android/launcher3/InvariantDeviceProfileTest.java create mode 100644 tests/src/com/android/launcher3/util/FocusLogicTest.java delete mode 100644 tests/stress/Android.mk delete mode 100644 tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java diff --git a/.gitignore b/.gitignore index f830c66eb2..aea5d6102a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,10 @@ db_files +*.iml +.project +.classpath +.project.properties +gen/ +tests/stress/gen/ +WallpaperPicker/gen/ +WallpaperPicker/.project.properties +bin/ diff --git a/Android.mk b/Android.mk index 2a406e76dd..134cfb5b08 100644 --- a/Android.mk +++ b/Android.mk @@ -23,28 +23,23 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional -LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13 - LOCAL_SRC_FILES := $(call all-java-files-under, src) \ $(call all-java-files-under, WallpaperPicker/src) \ - $(call all-renderscript-files-under, src) \ $(call all-proto-files-under, protos) LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/WallpaperPicker/res $(LOCAL_PATH)/res - -LOCAL_AAPT_FLAGS := --auto-add-overlay +LOCAL_PROGUARD_FLAG_FILES := proguard.flags +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 android-support-v7-recyclerview LOCAL_PROTOC_OPTIMIZE_TYPE := nano LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/ +LOCAL_AAPT_FLAGS := --auto-add-overlay -LOCAL_SDK_VERSION := 21 - +LOCAL_SDK_VERSION := current LOCAL_PACKAGE_NAME := Launcher3 #LOCAL_CERTIFICATE := shared LOCAL_OVERRIDES_PACKAGES := Home Launcher2 -LOCAL_PROGUARD_FLAG_FILES := proguard.flags - include $(BUILD_PACKAGE) @@ -83,4 +78,6 @@ $(LOCAL_BUILT_MODULE): $(LOCAL_PATH)/util/etc/launcher_protoutil | $(ACP) $(copy-file-to-new-target) $(hide) chmod 755 $@ +INTERNAL_DALVIK_MODULES += $(LOCAL_INSTALLED_MODULE) + include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fb7ac3fab3..1fb8e8d01a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -28,12 +28,6 @@ android:protectionLevel="dangerous" android:label="@string/permlab_install_shortcut" android:description="@string/permdesc_install_shortcut" /> - @@ -98,8 +93,10 @@ + + + + + - - - - - - - @@ -207,24 +204,6 @@ - - - - - - - - - - - - - - - - diff --git a/WallpaperPicker/AndroidManifest.xml b/WallpaperPicker/AndroidManifest.xml index 86a94d078e..cb1457bdc7 100644 --- a/WallpaperPicker/AndroidManifest.xml +++ b/WallpaperPicker/AndroidManifest.xml @@ -5,6 +5,6 @@ android:versionName="1.0" > - - + + diff --git a/WallpaperPicker/res/drawable-hdpi/ic_images.png b/WallpaperPicker/res/drawable-hdpi/ic_images.png index 0003c6c65e012d4b754744c69b9646bcca905a64..15e511c89c5ac8e03cb881fc5cc7d2e20676d048 100644 GIT binary patch literal 354 zcmV-o0iFJdP)IgK?*415>_Asz{yQU2|~D_jpf=?Ae1UBLF@rwgRlW?5D8Zw-4n4Q^kP4* zK==Q;M9xpjo#p>6jlhz6P!&1yGvT)~HjGl|S~tmi@glK@cXq`C@B?89UH@aG_6J^67_d zIYSU`^^-5I5i|mw54Phl@!8$YrVNvws4qAO^Rw7mltrmWR!Q=V$SR6z zZ&;iBs=Q4}DWMP}Odj>h@8|3N@BQPw&UK&9_4(ZQea>~=*L}{-^(MG)RMAlZ0BrQY zVtgS)t=jr^(0*ji+*t@5mn?vigFy5m;fR6zvlKL)(MIV z^}sm$NB2)&qhbB$^xMSZl~jg5^|)hInN^py=24$h2=1j;&c&`4qHUdU#QP52cA}pY zg0IID#c@}gAZclFS-fI(zvSQN`bFyNVVO3u@9$4f$D>*;CpUqxLgR@fM}4Q0(Z~B6 zK4RB}DM}PZS2pcw8U)Rm)!B(Y4+;JO0X;oEU)!%BS%-sy7Gtb+Hg0~?!2VK{;ObLz zA}wvXHbEx6BM=nQXtXymfKcHFGuX0a3(a)*?s-l^)#gjTa>QsMKx=S^hK7vU<4}MS zO3wh!RR=!ZKkYV+)#(ouMRo1fa8`$Lvwx?DKQk*=2;B1CQ@06aeY0csN?YFcJoGj- zsiH&FFXUb-5(hhg@$&RE z)Y=n-H9J6|7>!R&9f9jpyB0?)GqN;+<-N6RT&#k@TM~HwFoOl&y?X~hUfb!B5$eF# z7x{ygeibmOgT3swK;UU6Ie`!4{Z8cMXd#~s#NBt#(>GoC&t$|IGOE50Ds`&g)Wk&Y znge9=4wt{YNud7@?9AD-@uRPIJgBZlF0`K4Wq5hDU+^|z&Ph%w8r`^Y<4=L$BQQaz zkjFZ2#VBAaVfM15g|-SLit=Ne1GgqohTR5PkFdH7Xfkn!yL2`O(K^rKP2BhNk^n zPCv&R!uy|eAY6T_?%rLTm-p4DUbm&8{YWHFJRU!=`8UF7etv#N}wpsrc&?+?6h+_lW8q{`ZVKUV&c!64Ixvq!t$ljnYpLa z3rk0ol$2O1ur-61cDnP0^v@&Mcrxv=sgYvI)$+=~#^T|Y%PG>p`lLy%j^cgopK2&x z=_qa0n~up+MGNG6j%Ue0pb=@V|O(Kwkz-*}zS%!LV^2e=_Wj z_x=)|n3V@ZZ+vOyZN;JeE+_BVjtTew2+WR`zRHX*Bu!V;wvHoYy-fRUH*X7idV6~2 z-2sj_Tt@u6vD+!?_IImZ&t?ZZ?Fh~*>Vh_#L*(q8ax2xmUo~z#Xmr&&%hz1NX?XS^ z(xSXKvs_i8UCT&*Ol3-&eS7r_?`;PFhr=zF#gMEubIwb>HDta;*BbK#W&$70r zCOmsqL#)jX+H=JU&TZIZVuE4@A3B=jiZPB*MTRt}uR4fd`axhNb-2jnT<0yXE6Z6a zg!1+UYoul#B>2<)R#=K**BiXn?DDAIr0i3^8Dm(p4RVJn_ZMxpTucu4j z)j~7Ji0K;0j3hHD=m-V{0$_=W88aayMOCd+5#=oGf4o2YNO2AaPgmL}> DBl&#n diff --git a/WallpaperPicker/res/drawable-mdpi/ic_images.png b/WallpaperPicker/res/drawable-mdpi/ic_images.png index aabc1237a6d3f0674c85447c35446ff821e14ba9..c4a2229e94c965d954eda74042ec0bf0134f5c46 100644 GIT binary patch literal 249 zcmVKGc!CZfuHXb_5c`ObaRS#^Scz*`3U1&EX^#{sQhg%EAjl*4LW+R-{@p8n z%@rqq6>~g+m`Xejc}@78$CB5ffTf^A)C%S@`FJ7OW7g&Mh`s!-ct-`ry@q900000NkvXXu0mjfH8y21 literal 1332 zcmZ`(YgE#A82&-(yp_~JOwDGR9B)h%UB)cbgNh-hs0k-YQ6Q`kMPs3+q*O9Dv@GrK zwTpso>*Ap-MN6k*PD-sd4_@1`CTTUZoOvJa+dk~P=Xsy=UY_%Qc+YvH5#b@5O>Im8 z0GmTeM5>wX!ABPd_k9B!>{e`bJ!BQ{*|By8Ah?!eUqdc9(9o`>!{K?-xIaXsI{p3M!`RsI>!C z@8rlX%Ge?2jCp6I+udh*wrS7L7M3qXBDU7%h+%bz^EP+6)gCt>D60jcRiM?14}(GL zKQYk@l@pcgP*zF-ltz)1IX=uSFlty)zgk#6{U!sSNJm+k8oEfhTn87Y?EN(nF_)*N zrskDOB^;bfg4IzQsy}}G*vcDm%CXlrG4KRJzk=GZ6Le`J`Yf^Lov($eReAD$f}h_C zB_?K1toP%pzA$1b`||L}$b>*OmeYxi3!iT`2c6j3>(1{dCnw`?4C+X`w?&?0+_!}x zq(mfR2*Vh9#N+W!_h8NGIr2kmv;z%rtX!cmBa_J&p|qi#VrpK&z{G^d#opR}>8eFT z%*y^!SZ{s1abno+6QMoV_G)vpHE7o)a5^-v z;vVe~AMLMZ^7%zFnXGGgxO|7Tbw@|X8Qsi`3Z6v*M;HvtM%wqk{Bg&3-@esUE?3tk zlWl2kZT;B`7kF);ZPzgv7C4~_d%4~=+VSx<`~^}r&e!+z*y!kJ2(b%Xl1fbwky-~| z6oYPA*gXxe_j&I|B$kwv^v=)Ed*pY#Oae`PuQQ#h!U<1UUPT2uGfwcqgGKI0)7iyr z1AqLifj>*BUHYaWDsXF2PZn>4!x$=qNvy1_2snzeOXFU5w^Hf~X-lbdYPC8R9AR_L zLMC9dg$3aVoxaG*&TdtWK2WJtuTFGhn&VP>n|QLG$u?ksi#4RdW`n;fTdwo#bW^vm zIEB#OiYIHvFYh%rHjY zP?(b{5IpPJm)~6VbZ#jwgDEI+*eU9 zUxc&|Q6-Re%h|vWgM))LUXZ2-kfNcPg)tLD92tZWP1mYVJICl!WIhuUQYF0 zQ6`t8V-AZd-fG?on&Xz2m+9cvvGZ~F5@8YqT1-&L<$pFcHQ5Mu&rDClf%A9NA6XCJ zvzcvy2k53o`TQwC^Ot@*i{=uOQ%afJh4zS34t4ILwsAM|ZBSsWe}m!eAzSY8<5~AF zI9dVt8E{4GiYinL*c%H?h2V`FwD)QimRbF!u(B(8JvgYhl{eo6|*!^0!@i-h?8 z>w7Ily4=U+?eNsac2SDr5eSRQZ?~(v>Z3OXr;H$428)@&#wQ8bdIDZpPaFoj7vqVG z_VmX4c;azb40aD5i}l$e_Wh55$7dyTME@@!M)~SZ3s`Slh~jfHzGVv7Ab`bBWZQ@G am`B-EHj^bv>t%1a7YGdsC*BH7$omKLMm=@_ diff --git a/WallpaperPicker/res/drawable-xhdpi/ic_images.png b/WallpaperPicker/res/drawable-xhdpi/ic_images.png index 3f21faf8f03547cd1c53317221847fce1375d153..497479291e69adaac3e2ebfbcfd9a1d788872368 100644 GIT binary patch literal 431 zcmV;g0Z{&lP)Gs!G)8FeYVPrHp73>fed7zd%9HuJrQN=R|WLOJ}%ncCyVWc8$xeQ4VOqF)rH)3zoaXvAk{>piBRWEAlwS&wLv{xZV!C* z%3guvk6Q)gugB%K%L@Q0X9Y^`xRpTudh$CZ@qc4z4JA9^t~rEeD0KM?QbttAoPVBVS`RP>N=W95h0+fgIFEv!)yrGexUsB00!WXQIS8 z#EC$DF)fB9YdqxvO(L8eq(1{0Xmda*x`{!fs)5>71I0|M1(Ls6>LB@>r4G`cfef@+ ZpieoDLFU*oG$a53002ovPDHLkV1n6sy>0*i literal 2480 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 diff --git a/WallpaperPicker/res/drawable-xxhdpi/ic_images.png b/WallpaperPicker/res/drawable-xxhdpi/ic_images.png index d00db9a00ae42e0aea45b79716d613b6ca6ab35b..c8b9f757a03e2775aae85908add656758d42b6fd 100644 GIT binary patch literal 621 zcmeAS@N?(olHy`uVBq!ia0vp^IUvlz0wh)Q=esd5FqwF|IEGX(zP)ak9h@lA{_(P+ z$y6~@@_d)-4f#Vsql+pw9KJ?x= zYsta;S|dy+N=#_mVb(A$r$c=kt|VIAu$*Sa%f9;r#5oQrnBK@v*_!CJ!B974t66L9jiz*+AHnfE zy4U3P&n%6Qe>mf;Zsn4?h2EKx)7IO~-@HABuJKJ2y!QXC_4$jn&-fNCmlw3&AQNaE+<(m3#ou13F!9x{D`h`VR2@Gd zCVixRdgaQry(R@dQ`U?=D0>r3_D z%JOeJu)#Y;mn|tcZ(o}$wX y{e_qPmsMvLHtI4-{B^(3$ly0rwWZF*zYJT17A@duT$2J!Mhu>=elF{r5}E*=p%U%@ literal 2097 zcmV-12+sG3P)AU1@*VcDS`}otrJp3Y~0K)RoE@4u6O;s0^udD>62v$V6~uvJLmc(g~~Reni1a z)uAv^M?nzOUyjO{N_EzLB+cD>+V?ca+obupH%XVgFT71iZ*Fd$=lwi6=UiFSG>Ig2 zA`<~b01-e!1P}o;FUa!DQ=Lk}Bgx#N36G*mv+xc>0Q;kWS_D-H*O(*#Iw@#i8vn=y zaPY05S*TVdfjl634`87CKqpYy^MIa#Py{gWRZu&q6V;Bdkrza7{eI9f(C4V^xgYQj zhzF{n@&a0W7J~1BH~mSN#*tuUxeA~9(E}#+$0&0UU`5M!5R`F?12GI6!o!6Wo`6++ zGf8}6MGV6hG2uKwtb{1>8i+zV45RjdP=t`gN5Gt-8b~nBp&E@019wKDQ28yMlc)wR zARv@mywS)c5%Xl+<+)J;Sc5T+C6+&fNKk|V#fBw&(h2Y*_|HH`k&MX*O z=>+grdlX3+LsBw9_(*}2N)O=ie*(cD%`gHa1l~~SRSjMUAMx`G?RKV!Da8j3uFZpN zPC^6_0VG5K5kNu&5CJ4a01-e!1P}ovL;w*$LIe;2Bt!raKtco%0VG5~dWxFj&(}|0 z)rf%PfXeMN=)*u(ujsLev!D-(z5nvpAzTht#)WyJG zw{G2%p`oFpwY9ZhtY5$W$occC-@g5A zWo6|8^n@HK!dhD*Ab2h@>C&Z-_V3^Sv&-cwjpjrUn%=m4`SSOWCyE6U z97t?hZ5*PQWc_?)y-i58?v|F8lKc1Xe^*~$->xW%EtZq!<>ghJJbCiR9XocE>qxK( zB*-ybqZk@hT^}O0H&p&hK%({Rz!_O;Y`5HlW2*Vq^04Tu! zmw=W-F;E2|ap%K_5C32%^7(xKUb%9m%jtBobG-t(1atv@_elLmv>2_%{}UKYP@Sykb;lr!&c#q&+wxl$H20Wkvv>uct**BR-fK@%yk|aYz zL%&y4RItyj09}R_{1PY&EtSp9&1-Jnym@SVeEd;Fw*t9$@80RArlvJ`KIS!r=v~eb zVaYTPFiOB%7={=TYm!L-6R_%Ajg5`(!e`fju3*=@XV0D%=%)Mpe*b8!=YtSB+~42- zHH0f3mWp*@iRYUv^cKSegxWLCigDC~anu9H49toJvfLJq@k67hnjD%ZA=mV6-@g6mz`(#iIwCau&Me>L zGwY6*fV61o%?2OAvn*e}eB;@(XMgp2y&EAcD~O+D@pJe%iNh z-wL7fvJ2ff5s+ws^0m6Nv-8Boix*qX{H;PIXk!X>)}o@KN(5iQTwutEiGbJ>KL3f8 zZ`np3kW7IphK~SPgb@MC0Vxuh2uLmXDIJa`Ap(d1l6gWvCT?9kZwSa#;7!>xTQ%td zrkawGk&(YuRduE_>hK+hg(l3f@tLu2=*)cn+}zxcrRg6llr*(?He*7ZLN6I5LN(!n zI05{30lygy$~**L!tXmK1=lZFB!e5bswP{zKm=sA b{3pNw;B>*cY?~ss00000NkvXXu0mjfF?#FA diff --git a/WallpaperPicker/res/drawable-xxxhdpi/ic_images.png b/WallpaperPicker/res/drawable-xxxhdpi/ic_images.png new file mode 100644 index 0000000000000000000000000000000000000000..a19002e47aa8051205f9828c050e07b7a6c9f01a GIT binary patch literal 845 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q1xWh(YZ)^zFpGG)IEGX(zP;<~cRO69?W3`W zwTF@4iIy93&yvNSCTprh7_wBddZ=yaUJ)AFerYuZ zpSm}pbd@W^)tACc<-YD|jB;5$RXt#C$JuABAxr&_RE2zE{55mM+NX>m$sSkQLN>EU zsTFzOWX=`Z+54uktTV>EdI94^>r-+XRZKz?8dy{m7^f;Sa(Xy`czO`(m>-x2l7mX% zVlK$fjm|Eezd{Mx8B(Iz3>A>I0kYT2%EnWlzIVjCu!vQGKN>$dCLS=Lu` zayI>Yz^ya4evLA7Zt?Gw+dcR9`y{^nlPoK_;=D4?W5e2Guj}IyKmyV$8t3NqeVlcC zj%mJF0K4?f@|b(qXV~Am*m&u6vhwc3_kaAY`)2X`xRp{sRqdsp(=*zC+xb8;;{QtFN`U~as-(0f`-bOh5`|A0ts1sH2Q8}t$TVcMdsW_K87m?)$ZKYKA>{kiXot=_ETKdnr;@L^2Y3GvMU-z z_ms_Sv}*mleMKYJStW%Vk~lS%<1OWkn0V zyR4gP8=(57`S#Q|jXm=_ZfQ+pzu;Wuk~9BhO+$~lOAgy%3%V z9C6R#n2CMck3ShxGj;qId`Z~;qg9qqGn#o<&V@xyi9eZL=Yb<#)E*JH3zD6t+g^~% zW4d+7*Fk#+=Za$j>Wq@FfebHx#)oVB8a`YXx{x}1sk%bF=rswh6Y*13&0Z9#DEJ~J h6JcOln)20-F(_AeSBb;bE5K~R;OXk;vd$@?2>|>aa?t<) literal 0 HcmV?d00001 diff --git a/WallpaperPicker/res/layout/wallpaper_cropper.xml b/WallpaperPicker/res/layout/wallpaper_cropper.xml index abb8608985..ffe8df0fb0 100644 --- a/WallpaperPicker/res/layout/wallpaper_cropper.xml +++ b/WallpaperPicker/res/layout/wallpaper_cropper.xml @@ -19,7 +19,6 @@ --> - + android:layout_height="match_parent" > + + + android:visibility="invisible" /> + + + - - - - + - + + \ No newline at end of file diff --git a/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml index ae3c43d8eb..dc65244862 100644 --- a/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml +++ b/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml @@ -20,7 +20,6 @@ android:layout_height="@dimen/wallpaperThumbnailHeight" android:focusable="true" android:clickable="true" - android:background="@drawable/wallpaper_tile_fg" android:foreground="@drawable/wallpaper_tile_fg"> "Muurpapier %1$d van %2$d" "Het %1$s gekies" "Vee uit" - "Kies prent" + "My foto\'s" "Muurpapiere" "Snoei muurpapier" diff --git a/WallpaperPicker/res/values-am/strings.xml b/WallpaperPicker/res/values-am/strings.xml index 59c3bf733a..7e793856ec 100644 --- a/WallpaperPicker/res/values-am/strings.xml +++ b/WallpaperPicker/res/values-am/strings.xml @@ -30,7 +30,7 @@ "ልጣፍ %1$d የ%2$d" "%1$s ተመርጧል" "ሰርዝ" - "ምስል ይምረጡ" + "የእኔ ፎቶዎች" "የግድግዳ ወረቀቶች" "ልጣፍ ይከርክሙ" diff --git a/WallpaperPicker/res/values-ar/strings.xml b/WallpaperPicker/res/values-ar/strings.xml index 2075cc875d..84fa104db2 100644 --- a/WallpaperPicker/res/values-ar/strings.xml +++ b/WallpaperPicker/res/values-ar/strings.xml @@ -30,7 +30,7 @@ "‏الخلفية %1$d من %2$d" "تم تحديد %1$s" "حذف" - "اختيار صورة" + "صوري" "الخلفيات" "اقتصاص الخلفية" diff --git a/WallpaperPicker/res/values-az-rAZ/strings.xml b/WallpaperPicker/res/values-az-rAZ/strings.xml index f9d1fcced7..883673dd6c 100644 --- a/WallpaperPicker/res/values-az-rAZ/strings.xml +++ b/WallpaperPicker/res/values-az-rAZ/strings.xml @@ -30,7 +30,7 @@ "Divar kağızı %1$d of %2$d" "%1$s seçilib" "Sil" - "Şəkil seçin" + "Fotolarım" "Divar kağızları" "Divar kağızını kəsin" diff --git a/WallpaperPicker/res/values-bg/strings.xml b/WallpaperPicker/res/values-bg/strings.xml index ce4fc651b0..60ae3022f3 100644 --- a/WallpaperPicker/res/values-bg/strings.xml +++ b/WallpaperPicker/res/values-bg/strings.xml @@ -30,7 +30,7 @@ "Тапет %1$d от %2$d" "Избрахте %1$s" "Изтриване" - "Избиране на изображение" + "Моите снимки" "Тапети" "Подрязване на тапета" diff --git a/WallpaperPicker/res/values-bn-rBD/strings.xml b/WallpaperPicker/res/values-bn-rBD/strings.xml index a4fccb2082..74d7e57bda 100644 --- a/WallpaperPicker/res/values-bn-rBD/strings.xml +++ b/WallpaperPicker/res/values-bn-rBD/strings.xml @@ -30,7 +30,7 @@ "%2$dটির মধ্যে %1$dটি ওয়ালপেপার" "%1$s নির্বাচন করা হয়েছে" "মুছুন" - "চিত্র বাছুন" + "আমার ফটো" "ওয়ালপেপারগুলি" "ওয়ালপেপার কাটছাঁট করুন" diff --git a/WallpaperPicker/res/values-ca/strings.xml b/WallpaperPicker/res/values-ca/strings.xml index 89de81c30f..cbec76250e 100644 --- a/WallpaperPicker/res/values-ca/strings.xml +++ b/WallpaperPicker/res/values-ca/strings.xml @@ -30,7 +30,7 @@ "Fons de pantalla %1$d de %2$d" "S\'ha seleccionat %1$s." "Suprimeix" - "Tria una imatge" + "Les meves fotos" "Fons de pantalla" "Retallar fons de pantalla" diff --git a/WallpaperPicker/res/values-cs/strings.xml b/WallpaperPicker/res/values-cs/strings.xml index a07b9eda2a..da0758e0ab 100644 --- a/WallpaperPicker/res/values-cs/strings.xml +++ b/WallpaperPicker/res/values-cs/strings.xml @@ -30,7 +30,7 @@ "Tapeta %1$d z %2$d" "Vybrána položka %1$s" "Smazat" - "Vybrat obrázek" + "Moje fotografie" "Tapety" "Oříznutí tapety" diff --git a/WallpaperPicker/res/values-da/strings.xml b/WallpaperPicker/res/values-da/strings.xml index 5b233e1d0e..12c0fe87e6 100644 --- a/WallpaperPicker/res/values-da/strings.xml +++ b/WallpaperPicker/res/values-da/strings.xml @@ -30,7 +30,7 @@ "Baggrund %1$d af %2$d" "%1$s blev valgt" "Slet" - "Vælg et billede" + "Mine billeder" "Baggrunde" "Beskær baggrunden" diff --git a/WallpaperPicker/res/values-de/strings.xml b/WallpaperPicker/res/values-de/strings.xml index 11c1cac6a3..3171b3f3a7 100644 --- a/WallpaperPicker/res/values-de/strings.xml +++ b/WallpaperPicker/res/values-de/strings.xml @@ -30,7 +30,7 @@ "Hintergrund %1$d von %2$d" "%1$s ausgewählt" "Löschen" - "Bild auswählen" + "Meine Fotos" "Hintergründe" "Hintergrund zuschneiden" diff --git a/WallpaperPicker/res/values-el/strings.xml b/WallpaperPicker/res/values-el/strings.xml index 8e1e78fb94..2988beced4 100644 --- a/WallpaperPicker/res/values-el/strings.xml +++ b/WallpaperPicker/res/values-el/strings.xml @@ -30,7 +30,7 @@ "Ταπετσαρία %1$d από %2$d" "Επιλέχθηκε το %1$s" "Διαγραφή" - "Επιλογή εικόνας" + "Οι φωτογραφίες μου" "Ταπετσαρίες" "Περικοπή ταπετσαρίας" diff --git a/WallpaperPicker/res/values-en-rAU/strings.xml b/WallpaperPicker/res/values-en-rAU/strings.xml index c057c0cb35..30450ee814 100644 --- a/WallpaperPicker/res/values-en-rAU/strings.xml +++ b/WallpaperPicker/res/values-en-rAU/strings.xml @@ -30,7 +30,7 @@ "Wallpaper %1$d of %2$d" "Selected %1$s" "Delete" - "Pick image" + "My photos" "Wallpapers" "Crop wallpaper" diff --git a/WallpaperPicker/res/values-en-rGB/strings.xml b/WallpaperPicker/res/values-en-rGB/strings.xml index c057c0cb35..30450ee814 100644 --- a/WallpaperPicker/res/values-en-rGB/strings.xml +++ b/WallpaperPicker/res/values-en-rGB/strings.xml @@ -30,7 +30,7 @@ "Wallpaper %1$d of %2$d" "Selected %1$s" "Delete" - "Pick image" + "My photos" "Wallpapers" "Crop wallpaper" diff --git a/WallpaperPicker/res/values-en-rIN/strings.xml b/WallpaperPicker/res/values-en-rIN/strings.xml index c057c0cb35..30450ee814 100644 --- a/WallpaperPicker/res/values-en-rIN/strings.xml +++ b/WallpaperPicker/res/values-en-rIN/strings.xml @@ -30,7 +30,7 @@ "Wallpaper %1$d of %2$d" "Selected %1$s" "Delete" - "Pick image" + "My photos" "Wallpapers" "Crop wallpaper" diff --git a/WallpaperPicker/res/values-es-rUS/strings.xml b/WallpaperPicker/res/values-es-rUS/strings.xml index 768385b9ee..c18ecb2334 100644 --- a/WallpaperPicker/res/values-es-rUS/strings.xml +++ b/WallpaperPicker/res/values-es-rUS/strings.xml @@ -30,7 +30,7 @@ "Fondo de pantalla %1$d de %2$d" "%1$s seleccionado" "Eliminar" - "Elegir imagen" + "Mis fotos" "Fondos de pantalla" "Recortar fondo de pantalla" diff --git a/WallpaperPicker/res/values-es/strings.xml b/WallpaperPicker/res/values-es/strings.xml index 702b6d413f..b7221af2ae 100644 --- a/WallpaperPicker/res/values-es/strings.xml +++ b/WallpaperPicker/res/values-es/strings.xml @@ -30,7 +30,7 @@ "Fondo de pantalla %1$d de %2$d" "%1$s seleccionado" "Eliminar" - "Seleccionar imagen" + "Mis fotos" "Fondos de pantalla" "Recortar fondo de pantalla" diff --git a/WallpaperPicker/res/values-et-rEE/strings.xml b/WallpaperPicker/res/values-et-rEE/strings.xml index c09e223d9e..571a0ffbdd 100644 --- a/WallpaperPicker/res/values-et-rEE/strings.xml +++ b/WallpaperPicker/res/values-et-rEE/strings.xml @@ -30,7 +30,7 @@ "%1$d/%2$d taustapildist" "Valitud on %1$s" "Kustuta" - "Vali kujutis" + "Minu fotod" "Taustapildid" "Taustapildi kärpimine" diff --git a/WallpaperPicker/res/values-eu-rES/strings.xml b/WallpaperPicker/res/values-eu-rES/strings.xml index 327550d523..45bf7a1061 100644 --- a/WallpaperPicker/res/values-eu-rES/strings.xml +++ b/WallpaperPicker/res/values-eu-rES/strings.xml @@ -30,7 +30,7 @@ "%1$d/%2$d horma-papera" "%1$s hautatu da" "Ezabatu" - "Hautatu irudia" + "Nire argazkiak" "Horma-paperak" "Ebaki horma-papera" diff --git a/WallpaperPicker/res/values-fa/strings.xml b/WallpaperPicker/res/values-fa/strings.xml index e1d26a868b..469ec1263c 100644 --- a/WallpaperPicker/res/values-fa/strings.xml +++ b/WallpaperPicker/res/values-fa/strings.xml @@ -30,7 +30,7 @@ "‏کاغذدیواری %1$d از %2$d" "%1$s انتخاب شد" "حذف" - "انتخاب تصویر" + "عکس‌های من" "کاغذدیواری‌ها" "برش کاغذدیواری" diff --git a/WallpaperPicker/res/values-fi/strings.xml b/WallpaperPicker/res/values-fi/strings.xml index 0fa4b7bcfc..c82d3e0edd 100644 --- a/WallpaperPicker/res/values-fi/strings.xml +++ b/WallpaperPicker/res/values-fi/strings.xml @@ -30,7 +30,7 @@ "Taustakuva %1$d/%2$d" "Valittu: %1$s" "Poista" - "Valitse kuva" + "Omat valokuvat" "Taustakuvat" "Rajaa taustakuva" diff --git a/WallpaperPicker/res/values-fr-rCA/strings.xml b/WallpaperPicker/res/values-fr-rCA/strings.xml index e95670678a..92568966ee 100644 --- a/WallpaperPicker/res/values-fr-rCA/strings.xml +++ b/WallpaperPicker/res/values-fr-rCA/strings.xml @@ -30,7 +30,7 @@ "Fond d\'écran %1$d de %2$d" "Sélection : %1$s" "Supprimer" - "Choisir une image" + "Mes photos" "Fonds d\'écran" "Rogner le fond d\'écran" diff --git a/WallpaperPicker/res/values-fr/strings.xml b/WallpaperPicker/res/values-fr/strings.xml index 37846e0f5a..479a7e4303 100644 --- a/WallpaperPicker/res/values-fr/strings.xml +++ b/WallpaperPicker/res/values-fr/strings.xml @@ -30,7 +30,7 @@ "Fond d\'écran %1$d sur %2$d" "%1$s sélectionné" "Supprimer" - "Sélectionner une image" + "Mes photos" "Fonds d\'écran" "Recadrer le fond d\'écran" diff --git a/WallpaperPicker/res/values-gl-rES/strings.xml b/WallpaperPicker/res/values-gl-rES/strings.xml index 9f1fd5014c..0396b2f628 100644 --- a/WallpaperPicker/res/values-gl-rES/strings.xml +++ b/WallpaperPicker/res/values-gl-rES/strings.xml @@ -30,7 +30,7 @@ "Fondo de pantalla %1$d de %2$d" "Seleccionaches %1$s" "Eliminar" - "Escoller imaxe" + "As miñas fotos" "Fondos de pantalla" "Recortar fondo de pantalla" diff --git a/WallpaperPicker/res/values-gu-rIN/strings.xml b/WallpaperPicker/res/values-gu-rIN/strings.xml index acee01cfc9..e201d523f3 100644 --- a/WallpaperPicker/res/values-gu-rIN/strings.xml +++ b/WallpaperPicker/res/values-gu-rIN/strings.xml @@ -30,7 +30,7 @@ "%2$d માંથી %1$d વૉલપેપર" "%1$s પસંદ કર્યો" "કાઢી નાખો" - "છબી ચૂંટો" + "મારા ફોટા" "વૉલપેપર્સ" "વૉલપેપર કાપો" diff --git a/WallpaperPicker/res/values-hi/strings.xml b/WallpaperPicker/res/values-hi/strings.xml index fcb252eb55..60834e103d 100644 --- a/WallpaperPicker/res/values-hi/strings.xml +++ b/WallpaperPicker/res/values-hi/strings.xml @@ -30,7 +30,7 @@ "वॉलपेपर %2$d में से %1$d" "चयनित %1$s" "हटाएं" - "चित्र चुनें" + "मेरी फ़ोटो" "वॉलपेपर" "वॉलपेपर काटें" diff --git a/WallpaperPicker/res/values-hr/strings.xml b/WallpaperPicker/res/values-hr/strings.xml index 3d79e8f0c1..9ed702c5ab 100644 --- a/WallpaperPicker/res/values-hr/strings.xml +++ b/WallpaperPicker/res/values-hr/strings.xml @@ -30,7 +30,7 @@ "%1$d. pozadinska slika od %2$d" "Odabrana je %1$s" "Izbriši" - "Odaberi sliku" + "Moje fotografije" "Pozadine" "Obrezivanje pozadinske slike" diff --git a/WallpaperPicker/res/values-hu/strings.xml b/WallpaperPicker/res/values-hu/strings.xml index 703aa12eb2..d8b08fdfd3 100644 --- a/WallpaperPicker/res/values-hu/strings.xml +++ b/WallpaperPicker/res/values-hu/strings.xml @@ -30,7 +30,7 @@ "%1$d/%2$d. háttérkép" "%1$s kiválasztva" "Törlés" - "Kép kiválasztása" + "Saját fotók" "Háttérképek" "Háttérkép körbevágása" diff --git a/WallpaperPicker/res/values-hy-rAM/strings.xml b/WallpaperPicker/res/values-hy-rAM/strings.xml index f3891dd35a..027b841a73 100644 --- a/WallpaperPicker/res/values-hy-rAM/strings.xml +++ b/WallpaperPicker/res/values-hy-rAM/strings.xml @@ -30,7 +30,7 @@ "%1$d պաստառ՝ %2$d-ից" "Ընտրված է %1$s" "Ջնջել" - "Ընտրել պատկեր" + "Իմ լուսանկարները" "Պաստառներ" "Եզրատել պաստառը" diff --git a/WallpaperPicker/res/values-in/strings.xml b/WallpaperPicker/res/values-in/strings.xml index f216cf3436..91554522f6 100644 --- a/WallpaperPicker/res/values-in/strings.xml +++ b/WallpaperPicker/res/values-in/strings.xml @@ -30,7 +30,7 @@ "Wallpaper %1$d dari %2$d" "%1$s terpilih" "Hapus" - "Pilih gambar" + "Foto saya" "Wallpaper" "Pangkas wallpaper" diff --git a/WallpaperPicker/res/values-is-rIS/strings.xml b/WallpaperPicker/res/values-is-rIS/strings.xml index 6d5d680cf5..69e6cd4987 100644 --- a/WallpaperPicker/res/values-is-rIS/strings.xml +++ b/WallpaperPicker/res/values-is-rIS/strings.xml @@ -30,7 +30,7 @@ "Veggfóður %1$d af %2$d" "%1$s valið" "Eyða" - "Velja mynd" + "Myndirnar mínar" "Veggfóður" "Skera veggfóður" diff --git a/WallpaperPicker/res/values-it/strings.xml b/WallpaperPicker/res/values-it/strings.xml index c29946f85a..f4d38d14e0 100644 --- a/WallpaperPicker/res/values-it/strings.xml +++ b/WallpaperPicker/res/values-it/strings.xml @@ -30,7 +30,7 @@ "Sfondo %1$d di %2$d" "Elemento selezionato: %1$s" "Elimina" - "Scegli l\'immagine" + "Le mie foto" "Sfondi" "Ritaglia sfondo" diff --git a/WallpaperPicker/res/values-iw/strings.xml b/WallpaperPicker/res/values-iw/strings.xml index ddc96e92c5..e29f2372f3 100644 --- a/WallpaperPicker/res/values-iw/strings.xml +++ b/WallpaperPicker/res/values-iw/strings.xml @@ -30,7 +30,7 @@ "‏טפט %1$d מתוך %2$d" "בחרת %1$s" "מחק" - "בחר תמונה" + "התמונות שלי" "טפטים" "חתוך את הטפט" diff --git a/WallpaperPicker/res/values-ja/strings.xml b/WallpaperPicker/res/values-ja/strings.xml index 80b0944531..e86026bf95 100644 --- a/WallpaperPicker/res/values-ja/strings.xml +++ b/WallpaperPicker/res/values-ja/strings.xml @@ -30,7 +30,7 @@ "壁紙: %1$d/%2$d" "選択: %1$s" "削除" - "画像を選択" + "マイフォト" "壁紙" "壁紙のトリミング" diff --git a/WallpaperPicker/res/values-ka-rGE/strings.xml b/WallpaperPicker/res/values-ka-rGE/strings.xml index 2f59712d8b..1ae10214a2 100644 --- a/WallpaperPicker/res/values-ka-rGE/strings.xml +++ b/WallpaperPicker/res/values-ka-rGE/strings.xml @@ -30,7 +30,7 @@ "ფონი %1$d %2$d-დან" "არჩეული %1$s" "წაშლა" - "სურათის ამორჩევა" + "ჩემი ფოტოები" "ფონები" "ფონის ჩამოჭრა" diff --git a/WallpaperPicker/res/values-kk-rKZ/strings.xml b/WallpaperPicker/res/values-kk-rKZ/strings.xml index dcf372cf58..a10e273189 100644 --- a/WallpaperPicker/res/values-kk-rKZ/strings.xml +++ b/WallpaperPicker/res/values-kk-rKZ/strings.xml @@ -30,7 +30,7 @@ "%1$d артқы фон, барлығы %2$d" "%1$s таңдалған" "Жою" - "Суретті таңдау" + "Менің фотосуреттерім" "Артқы фондар" "Артқы фонды кесу" diff --git a/WallpaperPicker/res/values-km-rKH/strings.xml b/WallpaperPicker/res/values-km-rKH/strings.xml index 41722355f1..2a605e86a5 100644 --- a/WallpaperPicker/res/values-km-rKH/strings.xml +++ b/WallpaperPicker/res/values-km-rKH/strings.xml @@ -30,7 +30,7 @@ "ផ្ទាំង​រូបភាព %1$d នៃ %2$d" "បាន​ជ្រើស %1$s" "លុប" - "ជ្រើស​យក​រូបភាព" + "រូបថតរបស់ខ្ញុំ" "ផ្ទាំង​រូបភាព" "ច្រឹប​ផ្ទាំង​រូបភាព" diff --git a/WallpaperPicker/res/values-kn-rIN/strings.xml b/WallpaperPicker/res/values-kn-rIN/strings.xml index 89203bf1c1..b69bda5297 100644 --- a/WallpaperPicker/res/values-kn-rIN/strings.xml +++ b/WallpaperPicker/res/values-kn-rIN/strings.xml @@ -30,7 +30,7 @@ "%2$d ರಲ್ಲಿ %1$d ವಾಲ್‌ಪೇಪರ್‌" "%1$s ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ" "ಅಳಿಸು" - "ಚಿತ್ರವನ್ನು ಆರಿಸಿ" + "ನನ್ನ ಫೋಟೋಗಳು" "ವಾಲ್‌ಪೇಪರ್‌ಗಳು" "ವಾಲ್‌ಪೇಪರ್‌ ಕತ್ತರಿಸಿ" diff --git a/WallpaperPicker/res/values-ko/strings.xml b/WallpaperPicker/res/values-ko/strings.xml index a5a85b773d..30e7cfa23b 100644 --- a/WallpaperPicker/res/values-ko/strings.xml +++ b/WallpaperPicker/res/values-ko/strings.xml @@ -30,7 +30,7 @@ "배경화면 %1$d/%2$d" "%1$s 선택함" "삭제" - "이미지 선택" + "내 사진" "배경화면" "배경화면 잘라내기" diff --git a/WallpaperPicker/res/values-ky-rKG/strings.xml b/WallpaperPicker/res/values-ky-rKG/strings.xml index 696b3ee175..f1ebf095d0 100644 --- a/WallpaperPicker/res/values-ky-rKG/strings.xml +++ b/WallpaperPicker/res/values-ky-rKG/strings.xml @@ -30,7 +30,7 @@ "%2$d ичинен %1$d тушкагаз" "%1$s тандалды" "Жок кылуу" - "Сүрөт тандоо" + "Менин сүрөттөрүм" "Тушкагаздар" "Тушкагазды тегиздөө" diff --git a/WallpaperPicker/res/values-lo-rLA/strings.xml b/WallpaperPicker/res/values-lo-rLA/strings.xml index e468591113..e32cafb7d7 100644 --- a/WallpaperPicker/res/values-lo-rLA/strings.xml +++ b/WallpaperPicker/res/values-lo-rLA/strings.xml @@ -30,7 +30,7 @@ "ພາບພື້ນຫຼັງ %1$d ໃນ %2$d" "ເລືອກ %1$s" "ລຶບ" - "ເລືອກ​ຮູບ​ພາບ" + "ຮູບຂອງຂ້ອຍ" "ພາບພື້ນຫຼັງ" "ຕັດພາບພື້ນຫຼັງ" diff --git a/WallpaperPicker/res/values-lt/strings.xml b/WallpaperPicker/res/values-lt/strings.xml index 86035b8838..c442e3a5ad 100644 --- a/WallpaperPicker/res/values-lt/strings.xml +++ b/WallpaperPicker/res/values-lt/strings.xml @@ -30,7 +30,7 @@ "%1$d ekrano fonas iš %2$d" "Pasirinkta: %1$s" "Ištrinti" - "Pasirinkti vaizdą" + "Mano nuotraukos" "Ekrano fonai" "Apkirpti ekrano foną" diff --git a/WallpaperPicker/res/values-lv/strings.xml b/WallpaperPicker/res/values-lv/strings.xml index 02e5cfb22a..d05a175470 100644 --- a/WallpaperPicker/res/values-lv/strings.xml +++ b/WallpaperPicker/res/values-lv/strings.xml @@ -30,7 +30,7 @@ "%1$d. fona tapete no %2$d" "Atlasīta: %1$s" "Dzēst" - "Izvēlēties attēlu" + "Mani fotoattēli" "Fona tapetes" "Apgriezt fona tapeti" diff --git a/WallpaperPicker/res/values-mk-rMK/strings.xml b/WallpaperPicker/res/values-mk-rMK/strings.xml index b1919ccd44..f9e0963ed0 100644 --- a/WallpaperPicker/res/values-mk-rMK/strings.xml +++ b/WallpaperPicker/res/values-mk-rMK/strings.xml @@ -30,7 +30,7 @@ "Тапет %1$d од %2$d" "Избран %1$s" "Избриши" - "Избери слика" + "Моите фотографии" "Тапети" "Исечи тапет" diff --git a/WallpaperPicker/res/values-ml-rIN/strings.xml b/WallpaperPicker/res/values-ml-rIN/strings.xml index 0b6a4c108e..6be771c0ed 100644 --- a/WallpaperPicker/res/values-ml-rIN/strings.xml +++ b/WallpaperPicker/res/values-ml-rIN/strings.xml @@ -30,7 +30,7 @@ "%1$d / %2$d വാൾപേപ്പർ" "%1$s തിരഞ്ഞെടുത്തു" "ഇല്ലാതാക്കുക" - "ചിത്രം തിരഞ്ഞെടുക്കുക" + "എന്റെ ഫോട്ടോകൾ" "വാൾപേപ്പറുകൾ" "വാൾപേപ്പറിന്റെ വലുപ്പം മാറ്റൽ" diff --git a/WallpaperPicker/res/values-mn-rMN/strings.xml b/WallpaperPicker/res/values-mn-rMN/strings.xml index 71ff9e0f72..b88da5a96e 100644 --- a/WallpaperPicker/res/values-mn-rMN/strings.xml +++ b/WallpaperPicker/res/values-mn-rMN/strings.xml @@ -30,7 +30,7 @@ "%2$d ханын цаасны %1$d нь" "%1$s сонгогдсон" "Устгах" - "Зураг сонгох" + "Миний зураг" "Ханын зураг" "Ханын зургийг тайрах" diff --git a/WallpaperPicker/res/values-mr-rIN/strings.xml b/WallpaperPicker/res/values-mr-rIN/strings.xml index 33cfaa1484..128b21c1ca 100644 --- a/WallpaperPicker/res/values-mr-rIN/strings.xml +++ b/WallpaperPicker/res/values-mr-rIN/strings.xml @@ -30,7 +30,7 @@ "%2$d पैकी %1$d वॉलपेपर" "%1$s निवडले" "हटवा" - "प्रतिमा निवडा" + "माझे फोटो" "वॉलपेपर" "वॉलपेपर कापा" diff --git a/WallpaperPicker/res/values-ms-rMY/strings.xml b/WallpaperPicker/res/values-ms-rMY/strings.xml index 950f4d0e1d..9d824e4fa3 100644 --- a/WallpaperPicker/res/values-ms-rMY/strings.xml +++ b/WallpaperPicker/res/values-ms-rMY/strings.xml @@ -30,7 +30,7 @@ "Kertas dinding %1$d daripada %2$d" "Memilih %1$s" "Padam" - "Pilih imej" + "Foto saya" "Kertas dinding" "Pangkas kertas dinding" diff --git a/WallpaperPicker/res/values-my-rMM/strings.xml b/WallpaperPicker/res/values-my-rMM/strings.xml index 4d5ac47dfd..233611431d 100644 --- a/WallpaperPicker/res/values-my-rMM/strings.xml +++ b/WallpaperPicker/res/values-my-rMM/strings.xml @@ -30,7 +30,7 @@ "နောက်ခံ %1$d မှ %2$d" "ရွေးချယ်ထားသော %1$s" "ဖျက်ပါ" - "ပုံ ရွေးပါ" + "ကျွန်ုပ်၏ ဓာတ်ပုံများ" "နောက်ခံများ" "နောက်ခံအား ဖြတ်ခြင်း" diff --git a/WallpaperPicker/res/values-nb/strings.xml b/WallpaperPicker/res/values-nb/strings.xml index 3589e4bc86..9ae0b989a7 100644 --- a/WallpaperPicker/res/values-nb/strings.xml +++ b/WallpaperPicker/res/values-nb/strings.xml @@ -30,7 +30,7 @@ "Bakgrunn %1$d av %2$d" "Valgt %1$s" "Slett" - "Velg bilde" + "Mine bilder" "Bakgrunner" "Beskjær bakgrunnen" diff --git a/WallpaperPicker/res/values-ne-rNP/strings.xml b/WallpaperPicker/res/values-ne-rNP/strings.xml index 221fa94381..5bca8d809d 100644 --- a/WallpaperPicker/res/values-ne-rNP/strings.xml +++ b/WallpaperPicker/res/values-ne-rNP/strings.xml @@ -30,7 +30,7 @@ "%2$d को %1$d वालपेपर" "चयन गरिएको %1$s" "मेट्नुहोस्" - "तस्बिर छान्नुहोस्" + "मेरा तस्बिरहरू" "वालपेपरहरु" "वालपेपर काँटछाट गर्नुहोस्" diff --git a/WallpaperPicker/res/values-nl/strings.xml b/WallpaperPicker/res/values-nl/strings.xml index 7dd4f22b07..a86f0f6518 100644 --- a/WallpaperPicker/res/values-nl/strings.xml +++ b/WallpaperPicker/res/values-nl/strings.xml @@ -30,7 +30,7 @@ "Achtergrond %1$d van %2$d" "%1$s is geselecteerd" "Verwijderen" - "Afbeelding kiezen" + "Mijn foto\'s" "Achtergronden" "Achtergrond bijsnijden" diff --git a/WallpaperPicker/res/values-pa-rIN/strings.xml b/WallpaperPicker/res/values-pa-rIN/strings.xml index 3be7bf8022..e4225e02d4 100644 --- a/WallpaperPicker/res/values-pa-rIN/strings.xml +++ b/WallpaperPicker/res/values-pa-rIN/strings.xml @@ -30,7 +30,7 @@ "ਵਾਲਪੇਪਰ %2$d ਦਾ %1$d" "%1$s ਚੁਣਿਆ ਗਿਆ" "ਮਿਟਾਓ" - "ਚਿੱਤਰ ਚੁਣੋ" + "ਮੇਰੀਆਂ ਫੋਟੋਆਂ" "ਵਾਲਪੇਪਰ" "ਵਾਲਪੇਪਰ ਕੱਟੋ" diff --git a/WallpaperPicker/res/values-pl/strings.xml b/WallpaperPicker/res/values-pl/strings.xml index bcdbb3dc7b..cd52082d6b 100644 --- a/WallpaperPicker/res/values-pl/strings.xml +++ b/WallpaperPicker/res/values-pl/strings.xml @@ -30,7 +30,7 @@ "Tapeta %1$d z %2$d" "Wybrano %1$s" "Usuń" - "Wybierz obraz" + "Moje zdjęcia" "Tapety" "Przytnij tapetę" diff --git a/WallpaperPicker/res/values-pt-rPT/strings.xml b/WallpaperPicker/res/values-pt-rPT/strings.xml index 83492327f9..82aa4694a1 100644 --- a/WallpaperPicker/res/values-pt-rPT/strings.xml +++ b/WallpaperPicker/res/values-pt-rPT/strings.xml @@ -30,7 +30,7 @@ "Imagem de fundo %1$d de %2$d" "%1$s selecionada" "Eliminar" - "Escolher imagem" + "As minhas fotos" "Imagens de fundo" "Recortar imagem de fundo" diff --git a/WallpaperPicker/res/values-pt/strings.xml b/WallpaperPicker/res/values-pt/strings.xml index 2332fcbd13..74ff3106c3 100644 --- a/WallpaperPicker/res/values-pt/strings.xml +++ b/WallpaperPicker/res/values-pt/strings.xml @@ -30,7 +30,7 @@ "Plano de fundo %1$d de %2$d" "%1$s selecionado" "Excluir" - "Escolher imagem" + "Minhas fotos" "Planos de fundo" "Cortar plano de fundo" diff --git a/WallpaperPicker/res/values-ro/strings.xml b/WallpaperPicker/res/values-ro/strings.xml index 8ee1a5a391..6281b80fc3 100644 --- a/WallpaperPicker/res/values-ro/strings.xml +++ b/WallpaperPicker/res/values-ro/strings.xml @@ -30,7 +30,7 @@ "Imaginea de fundal %1$d din %2$d" "S-a selectat %1$s" "Ștergeți" - "Alegeți imaginea" + "Fotografiile mele" "Imagini de fundal" "Decupați imaginea de fundal" diff --git a/WallpaperPicker/res/values-ru/strings.xml b/WallpaperPicker/res/values-ru/strings.xml index ff43ce8f64..959205a336 100644 --- a/WallpaperPicker/res/values-ru/strings.xml +++ b/WallpaperPicker/res/values-ru/strings.xml @@ -30,7 +30,7 @@ "Обои %1$d из %2$d" "Выбран элемент \"%1$s\"" "Удалить" - "Выбрать обои" + "Мои фото" "Обои" "Кадрировать обои" diff --git a/WallpaperPicker/res/values-si-rLK/strings.xml b/WallpaperPicker/res/values-si-rLK/strings.xml index d175724fda..37ce1fc528 100644 --- a/WallpaperPicker/res/values-si-rLK/strings.xml +++ b/WallpaperPicker/res/values-si-rLK/strings.xml @@ -32,7 +32,7 @@ "තෝරාගත්තේ %1$s" "මකන්න" - "පින්තූරයක් තෝරන්න" + "මගේ ඡායාරූප" "වෝල්පේපර" "වෝල්පේපරය කප්පාදු කිරීම" diff --git a/WallpaperPicker/res/values-sk/strings.xml b/WallpaperPicker/res/values-sk/strings.xml index 33477a124a..9d38de437b 100644 --- a/WallpaperPicker/res/values-sk/strings.xml +++ b/WallpaperPicker/res/values-sk/strings.xml @@ -30,7 +30,7 @@ "Tapeta %1$d z %2$d" "Vybratá položka %1$s" "Odstrániť" - "Vybrať obrázok" + "Moje fotky" "Tapety" "Orezanie tapety" diff --git a/WallpaperPicker/res/values-sl/strings.xml b/WallpaperPicker/res/values-sl/strings.xml index 06a508a8c5..e776cc7d54 100644 --- a/WallpaperPicker/res/values-sl/strings.xml +++ b/WallpaperPicker/res/values-sl/strings.xml @@ -30,7 +30,7 @@ "%1$d. ozadje od %2$d" "Izbrano: %1$s" "Izbriši" - "Izberi sliko" + "Moje fotografije" "Ozadja" "Obrezovanje ozadja" diff --git a/WallpaperPicker/res/values-sq-rAL/strings.xml b/WallpaperPicker/res/values-sq-rAL/strings.xml index f21b7ef838..8a9983b4bd 100644 --- a/WallpaperPicker/res/values-sq-rAL/strings.xml +++ b/WallpaperPicker/res/values-sq-rAL/strings.xml @@ -30,7 +30,7 @@ "Imazhi i sfondit: %1$d nga gjithsej %2$d" "%1$s u përzgjodh" "Fshi" - "Zgjidh imazhin" + "Fotografitë e mia" "Imazhet e sfondit" "Prit imazhin e sfondit" diff --git a/WallpaperPicker/res/values-sr/strings.xml b/WallpaperPicker/res/values-sr/strings.xml index fdfaaf60f0..c37bc060f5 100644 --- a/WallpaperPicker/res/values-sr/strings.xml +++ b/WallpaperPicker/res/values-sr/strings.xml @@ -30,7 +30,7 @@ "Позадина %1$d од %2$d" "Изабрана је %1$s" "Избриши" - "Изабери слику" + "Моје фотографије" "Позадине" "Опсеци позадину" diff --git a/WallpaperPicker/res/values-sv/strings.xml b/WallpaperPicker/res/values-sv/strings.xml index 57db3a64d3..044508b4da 100644 --- a/WallpaperPicker/res/values-sv/strings.xml +++ b/WallpaperPicker/res/values-sv/strings.xml @@ -30,7 +30,7 @@ "Bakgrund %1$d av %2$d" "%1$s har valts" "Ta bort" - "Välj bild" + "Mina foton" "Bakgrunder" "Beskär bakgrund" diff --git a/WallpaperPicker/res/values-sw/strings.xml b/WallpaperPicker/res/values-sw/strings.xml index edea3de5b1..367912aab7 100644 --- a/WallpaperPicker/res/values-sw/strings.xml +++ b/WallpaperPicker/res/values-sw/strings.xml @@ -30,7 +30,7 @@ "Mandhari %1$d ya %2$d" "%1$s iliyochaguliwa" "Futa" - "Chagua picha" + "Picha zangu" "Mandhari" "Punguza mandhari" diff --git a/WallpaperPicker/res/values-sw720dp-v19/styles.xml b/WallpaperPicker/res/values-sw720dp-v19/styles.xml index 91078517db..d8dab223a1 100644 --- a/WallpaperPicker/res/values-sw720dp-v19/styles.xml +++ b/WallpaperPicker/res/values-sw720dp-v19/styles.xml @@ -18,7 +18,7 @@ --> - diff --git a/WallpaperPicker/res/values-ta-rIN/strings.xml b/WallpaperPicker/res/values-ta-rIN/strings.xml index 2b984271ac..7284da8ae5 100644 --- a/WallpaperPicker/res/values-ta-rIN/strings.xml +++ b/WallpaperPicker/res/values-ta-rIN/strings.xml @@ -30,7 +30,7 @@ "வால்பேப்பர் %1$d / %2$d" "%1$s தேர்ந்தெடுக்கப்பட்டது" "நீக்கு" - "படத்தைத் தேர்வுசெய்க" + "எனது படங்கள்" "வால்பேப்பர்கள்" "வால்பேப்பரைச் செதுக்கு" diff --git a/WallpaperPicker/res/values-te-rIN/strings.xml b/WallpaperPicker/res/values-te-rIN/strings.xml index c26f84e286..4c5ad2f080 100644 --- a/WallpaperPicker/res/values-te-rIN/strings.xml +++ b/WallpaperPicker/res/values-te-rIN/strings.xml @@ -30,7 +30,7 @@ "%2$dలో %1$dవ వాల్‌పేపర్" "%1$s ఎంచుకోబడింది" "తొలగించు" - "చిత్రాన్ని ఎంచుకోండి" + "నా ఫోటోలు" "వాల్‌పేపర్‌లు" "వాల్‌పేపర్‌ను కత్తిరించండి" diff --git a/WallpaperPicker/res/values-th/strings.xml b/WallpaperPicker/res/values-th/strings.xml index 6b4c235361..b3d7a8bc7e 100644 --- a/WallpaperPicker/res/values-th/strings.xml +++ b/WallpaperPicker/res/values-th/strings.xml @@ -30,7 +30,7 @@ "วอลเปเปอร์ %1$d จาก %2$d" "เลือก %1$s แล้ว" "ลบ" - "เลือกรูปภาพ" + "รูปภาพของฉัน" "วอลเปเปอร์" "ครอบตัดวอลเปเปอร์" diff --git a/WallpaperPicker/res/values-tl/strings.xml b/WallpaperPicker/res/values-tl/strings.xml index c9fe338d42..db8f4d5426 100644 --- a/WallpaperPicker/res/values-tl/strings.xml +++ b/WallpaperPicker/res/values-tl/strings.xml @@ -30,7 +30,7 @@ "Wallpaper %1$d ng %2$d" "Napili ang %1$s" "Tanggalin" - "Pumili ng larawan" + "Aking mga larawan" "Mga Wallpaper" "I-crop ang wallpaper" diff --git a/WallpaperPicker/res/values-tr/strings.xml b/WallpaperPicker/res/values-tr/strings.xml index 3d03c6be8d..d1e57edbbf 100644 --- a/WallpaperPicker/res/values-tr/strings.xml +++ b/WallpaperPicker/res/values-tr/strings.xml @@ -30,7 +30,7 @@ "%2$d duvar kağıdı arasından duvar kağıdı %1$d" "%1$s seçildi" "Sil" - "Resim seç" + "Fotoğraflarım" "Duvar kağıtları" "Duvar kağıdını kırp" diff --git a/WallpaperPicker/res/values-uk/strings.xml b/WallpaperPicker/res/values-uk/strings.xml index 076c88238b..756e7cf092 100644 --- a/WallpaperPicker/res/values-uk/strings.xml +++ b/WallpaperPicker/res/values-uk/strings.xml @@ -30,7 +30,7 @@ "Фоновий малюнок %1$d з %2$d" "Вибрано %1$s" "Видалити" - "Вибрати зображення" + "Мої фото" "Фонові малюнки" "Обрізати фоновий малюнок" diff --git a/WallpaperPicker/res/values-ur-rPK/strings.xml b/WallpaperPicker/res/values-ur-rPK/strings.xml index 7b12d42399..86d8163d90 100644 --- a/WallpaperPicker/res/values-ur-rPK/strings.xml +++ b/WallpaperPicker/res/values-ur-rPK/strings.xml @@ -30,7 +30,7 @@ "‏وال پیپر ‎%1$d از ‎%2$d" "%1$s کو منتخب کیا گیا" "حذف کریں" - "تصویر منتخب کریں" + "میری تصاویر" "وال پیپرز" "وال پیپر کو تراشیں" diff --git a/WallpaperPicker/res/values-uz-rUZ/strings.xml b/WallpaperPicker/res/values-uz-rUZ/strings.xml index 807d6b4ae6..97d3938ae1 100644 --- a/WallpaperPicker/res/values-uz-rUZ/strings.xml +++ b/WallpaperPicker/res/values-uz-rUZ/strings.xml @@ -30,7 +30,7 @@ "Fon rasmi %2$ddan %1$d" "%1$s tanlandi" "O‘chirish" - "Rasm tanlang" + "Mening rasmlarim" "Fon rasmlari" "Fon rasmini kesish" diff --git a/WallpaperPicker/res/values-v21/styles.xml b/WallpaperPicker/res/values-v21/styles.xml new file mode 100644 index 0000000000..582ab8fed8 --- /dev/null +++ b/WallpaperPicker/res/values-v21/styles.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/WallpaperPicker/res/values-vi/strings.xml b/WallpaperPicker/res/values-vi/strings.xml index d1efbe9892..ae00d585cc 100644 --- a/WallpaperPicker/res/values-vi/strings.xml +++ b/WallpaperPicker/res/values-vi/strings.xml @@ -30,7 +30,7 @@ "Hình nền %1$d / %2$d" "%1$s được chọn" "Xóa" - "Chọn hình ảnh" + "Ảnh của tôi" "Hình nền" "Cắt hình nền" diff --git a/WallpaperPicker/res/values-zh-rCN/strings.xml b/WallpaperPicker/res/values-zh-rCN/strings.xml index 1def9ed41b..8bd5342eb5 100644 --- a/WallpaperPicker/res/values-zh-rCN/strings.xml +++ b/WallpaperPicker/res/values-zh-rCN/strings.xml @@ -30,7 +30,7 @@ "第%1$d张壁纸,共%2$d张" "已选择%1$s" "删除" - "选择图片" + "我的照片" "壁纸" "剪裁壁纸" diff --git a/WallpaperPicker/res/values-zh-rHK/strings.xml b/WallpaperPicker/res/values-zh-rHK/strings.xml index 66ba0af180..e51d60afa2 100644 --- a/WallpaperPicker/res/values-zh-rHK/strings.xml +++ b/WallpaperPicker/res/values-zh-rHK/strings.xml @@ -30,7 +30,7 @@ "第 %1$d 張桌布,共 %2$d 張" "已選取%1$s" "刪除" - "選擇圖片" + "我的相片" "桌布" "裁剪桌布" diff --git a/WallpaperPicker/res/values-zh-rTW/strings.xml b/WallpaperPicker/res/values-zh-rTW/strings.xml index 4184976083..c12350af0e 100644 --- a/WallpaperPicker/res/values-zh-rTW/strings.xml +++ b/WallpaperPicker/res/values-zh-rTW/strings.xml @@ -30,7 +30,7 @@ "第 %1$d 張桌布,共 %2$d 張" "已選取%1$s" "刪除" - "選擇圖片" + "我的相片" "桌布" "裁剪桌布" diff --git a/WallpaperPicker/res/values-zu/strings.xml b/WallpaperPicker/res/values-zu/strings.xml index c7d3f33d1a..537b2f3ca0 100644 --- a/WallpaperPicker/res/values-zu/strings.xml +++ b/WallpaperPicker/res/values-zu/strings.xml @@ -30,7 +30,7 @@ "Isithombe sangemuva esingu-%1$d kwezingu-%2$d" "I-%1$s ekhethiwe" "Susa" - "Khetha isithombe" + "Izithombe zami" "Izithombe zangemuva" "Nqampuna isithombe sangemuva" diff --git a/WallpaperPicker/res/values/config.xml b/WallpaperPicker/res/values/config.xml index 71580b5b07..2f5174ce6e 100644 --- a/WallpaperPicker/res/values/config.xml +++ b/WallpaperPicker/res/values/config.xml @@ -14,7 +14,6 @@ limitations under the License. --> - false false diff --git a/WallpaperPicker/res/values/strings.xml b/WallpaperPicker/res/values/strings.xml index 1ee3513f90..72b1e15df4 100644 --- a/WallpaperPicker/res/values/strings.xml +++ b/WallpaperPicker/res/values/strings.xml @@ -45,7 +45,7 @@ Delete - Pick image + My photos Wallpapers diff --git a/WallpaperPicker/res/values/styles.xml b/WallpaperPicker/res/values/styles.xml index f4008f1594..74aeab9032 100644 --- a/WallpaperPicker/res/values/styles.xml +++ b/WallpaperPicker/res/values/styles.xml @@ -17,7 +17,7 @@ */ --> - + diff --git a/WallpaperPicker/src/android/util/Pools.java b/WallpaperPicker/src/android/util/Pools.java deleted file mode 100644 index 40bab1eaee..0000000000 --- a/WallpaperPicker/src/android/util/Pools.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * Helper class for crating pools of objects. An example use looks like this: - *
- * public class MyPooledClass {
- *
- *     private static final SynchronizedPool sPool =
- *             new SynchronizedPool(10);
- *
- *     public static MyPooledClass obtain() {
- *         MyPooledClass instance = sPool.acquire();
- *         return (instance != null) ? instance : new MyPooledClass();
- *     }
- *
- *     public void recycle() {
- *          // Clear state if needed.
- *          sPool.release(this);
- *     }
- *
- *     . . .
- * }
- * 
- * - * @hide - */ -public final class Pools { - - /** - * Interface for managing a pool of objects. - * - * @param The pooled type. - */ - public static interface Pool { - - /** - * @return An instance from the pool if such, null otherwise. - */ - public T acquire(); - - /** - * Release an instance to the pool. - * - * @param instance The instance to release. - * @return Whether the instance was put in the pool. - * - * @throws IllegalStateException If the instance is already in the pool. - */ - public boolean release(T instance); - } - - private Pools() { - /* do nothing - hiding constructor */ - } - - /** - * Simple (non-synchronized) pool of objects. - * - * @param The pooled type. - */ - public static class SimplePool implements Pool { - private final Object[] mPool; - - private int mPoolSize; - - /** - * Creates a new instance. - * - * @param maxPoolSize The max pool size. - * - * @throws IllegalArgumentException If the max pool size is less than zero. - */ - public SimplePool(int maxPoolSize) { - if (maxPoolSize <= 0) { - throw new IllegalArgumentException("The max pool size must be > 0"); - } - mPool = new Object[maxPoolSize]; - } - - @Override - @SuppressWarnings("unchecked") - public T acquire() { - if (mPoolSize > 0) { - final int lastPooledIndex = mPoolSize - 1; - T instance = (T) mPool[lastPooledIndex]; - mPool[lastPooledIndex] = null; - mPoolSize--; - return instance; - } - return null; - } - - @Override - public boolean release(T instance) { - if (isInPool(instance)) { - throw new IllegalStateException("Already in the pool!"); - } - if (mPoolSize < mPool.length) { - mPool[mPoolSize] = instance; - mPoolSize++; - return true; - } - return false; - } - - private boolean isInPool(T instance) { - for (int i = 0; i < mPoolSize; i++) { - if (mPool[i] == instance) { - return true; - } - } - return false; - } - } - - /** - * Synchronized) pool of objects. - * - * @param The pooled type. - */ - public static class SynchronizedPool extends SimplePool { - private final Object mLock = new Object(); - - /** - * Creates a new instance. - * - * @param maxPoolSize The max pool size. - * - * @throws IllegalArgumentException If the max pool size is less than zero. - */ - public SynchronizedPool(int maxPoolSize) { - super(maxPoolSize); - } - - @Override - public T acquire() { - synchronized (mLock) { - return super.acquire(); - } - } - - @Override - public boolean release(T element) { - synchronized (mLock) { - return super.release(element); - } - } - } -} \ No newline at end of file diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java new file mode 100644 index 0000000000..45118bf45e --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java @@ -0,0 +1,405 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.gallery3d.common; + +import android.app.WallpaperManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class BitmapCropTask extends AsyncTask { + + public interface OnBitmapCroppedHandler { + public void onBitmapCropped(byte[] imageBytes); + } + + private static final int DEFAULT_COMPRESS_QUALITY = 90; + private static final String LOGTAG = "BitmapCropTask"; + + Uri mInUri = null; + Context mContext; + String mInFilePath; + byte[] mInImageBytes; + int mInResId = 0; + RectF mCropBounds = null; + int mOutWidth, mOutHeight; + int mRotation; + boolean mSetWallpaper; + boolean mSaveCroppedBitmap; + Bitmap mCroppedBitmap; + Runnable mOnEndRunnable; + Resources mResources; + BitmapCropTask.OnBitmapCroppedHandler mOnBitmapCroppedHandler; + boolean mNoCrop; + + public BitmapCropTask(Context c, String filePath, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInFilePath = filePath; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(byte[] imageBytes, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mInImageBytes = imageBytes; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(Context c, Uri inUri, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInUri = inUri; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(Context c, Resources res, int inResId, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInResId = inResId; + mResources = res; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mCropBounds = cropBounds; + mRotation = rotation; + mOutWidth = outWidth; + mOutHeight = outHeight; + mSetWallpaper = setWallpaper; + mSaveCroppedBitmap = saveCroppedBitmap; + mOnEndRunnable = onEndRunnable; + } + + public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) { + mOnBitmapCroppedHandler = handler; + } + + public void setNoCrop(boolean value) { + mNoCrop = value; + } + + public void setOnEndRunnable(Runnable onEndRunnable) { + mOnEndRunnable = onEndRunnable; + } + + // Helper to setup input stream + private InputStream regenerateInputStream() { + if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { + Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + + "image byte array given"); + } else { + try { + if (mInUri != null) { + return new BufferedInputStream( + mContext.getContentResolver().openInputStream(mInUri)); + } else if (mInFilePath != null) { + return mContext.openFileInput(mInFilePath); + } else if (mInImageBytes != null) { + return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); + } else { + return new BufferedInputStream(mResources.openRawResource(mInResId)); + } + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); + } + } + return null; + } + + public Point getImageBounds() { + InputStream is = regenerateInputStream(); + if (is != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, options); + Utils.closeSilently(is); + if (options.outWidth != 0 && options.outHeight != 0) { + return new Point(options.outWidth, options.outHeight); + } + } + return null; + } + + public void setCropBounds(RectF cropBounds) { + mCropBounds = cropBounds; + } + + public Bitmap getCroppedBitmap() { + return mCroppedBitmap; + } + public boolean cropBitmap() { + boolean failure = false; + + + WallpaperManager wallpaperManager = null; + if (mSetWallpaper) { + wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext()); + } + + + if (mSetWallpaper && mNoCrop) { + try { + InputStream is = regenerateInputStream(); + if (is != null) { + wallpaperManager.setStream(is); + Utils.closeSilently(is); + } + } catch (IOException e) { + Log.w(LOGTAG, "cannot write stream to wallpaper", e); + failure = true; + } + return !failure; + } else { + // Find crop bounds (scaled to original image size) + Rect roundedTrueCrop = new Rect(); + Matrix rotateMatrix = new Matrix(); + Matrix inverseRotateMatrix = new Matrix(); + + Point bounds = getImageBounds(); + if (mRotation > 0) { + rotateMatrix.setRotate(mRotation); + inverseRotateMatrix.setRotate(-mRotation); + + mCropBounds.roundOut(roundedTrueCrop); + mCropBounds = new RectF(roundedTrueCrop); + + if (bounds == null) { + Log.w(LOGTAG, "cannot get bounds for image"); + failure = true; + return false; + } + + float[] rotatedBounds = new float[] { bounds.x, bounds.y }; + rotateMatrix.mapPoints(rotatedBounds); + rotatedBounds[0] = Math.abs(rotatedBounds[0]); + rotatedBounds[1] = Math.abs(rotatedBounds[1]); + + mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); + inverseRotateMatrix.mapRect(mCropBounds); + mCropBounds.offset(bounds.x/2, bounds.y/2); + + } + + mCropBounds.roundOut(roundedTrueCrop); + + if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { + Log.w(LOGTAG, "crop has bad values for full size image"); + failure = true; + return false; + } + + // See how much we're reducing the size of the image + int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth, + roundedTrueCrop.height() / mOutHeight)); + // Attempt to open a region decoder + BitmapRegionDecoder decoder = null; + InputStream is = null; + try { + is = regenerateInputStream(); + if (is == null) { + Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); + failure = true; + return false; + } + decoder = BitmapRegionDecoder.newInstance(is, false); + Utils.closeSilently(is); + } catch (IOException e) { + Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); + } finally { + Utils.closeSilently(is); + is = null; + } + + Bitmap crop = null; + if (decoder != null) { + // Do region decoding to get crop bitmap + BitmapFactory.Options options = new BitmapFactory.Options(); + if (scaleDownSampleSize > 1) { + options.inSampleSize = scaleDownSampleSize; + } + crop = decoder.decodeRegion(roundedTrueCrop, options); + decoder.recycle(); + } + + if (crop == null) { + // BitmapRegionDecoder has failed, try to crop in-memory + is = regenerateInputStream(); + Bitmap fullSize = null; + if (is != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + if (scaleDownSampleSize > 1) { + options.inSampleSize = scaleDownSampleSize; + } + fullSize = BitmapFactory.decodeStream(is, null, options); + Utils.closeSilently(is); + } + if (fullSize != null) { + // Find out the true sample size that was used by the decoder + scaleDownSampleSize = bounds.x / fullSize.getWidth(); + mCropBounds.left /= scaleDownSampleSize; + mCropBounds.top /= scaleDownSampleSize; + mCropBounds.bottom /= scaleDownSampleSize; + mCropBounds.right /= scaleDownSampleSize; + mCropBounds.roundOut(roundedTrueCrop); + + // Adjust values to account for issues related to rounding + if (roundedTrueCrop.width() > fullSize.getWidth()) { + // Adjust the width + roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); + } + if (roundedTrueCrop.right > fullSize.getWidth()) { + // Adjust the left value + int adjustment = roundedTrueCrop.left - + Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width()); + roundedTrueCrop.left -= adjustment; + roundedTrueCrop.right -= adjustment; + } + if (roundedTrueCrop.height() > fullSize.getHeight()) { + // Adjust the height + roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); + } + if (roundedTrueCrop.bottom > fullSize.getHeight()) { + // Adjust the top value + int adjustment = roundedTrueCrop.top - + Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height()); + roundedTrueCrop.top -= adjustment; + roundedTrueCrop.bottom -= adjustment; + } + + crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, + roundedTrueCrop.top, roundedTrueCrop.width(), + roundedTrueCrop.height()); + } + } + + if (crop == null) { + Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); + failure = true; + return false; + } + if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) { + float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; + rotateMatrix.mapPoints(dimsAfter); + dimsAfter[0] = Math.abs(dimsAfter[0]); + dimsAfter[1] = Math.abs(dimsAfter[1]); + + if (!(mOutWidth > 0 && mOutHeight > 0)) { + mOutWidth = Math.round(dimsAfter[0]); + mOutHeight = Math.round(dimsAfter[1]); + } + + RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); + RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight); + + Matrix m = new Matrix(); + if (mRotation == 0) { + m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); + } else { + Matrix m1 = new Matrix(); + m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); + Matrix m2 = new Matrix(); + m2.setRotate(mRotation); + Matrix m3 = new Matrix(); + m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); + Matrix m4 = new Matrix(); + m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); + + Matrix c1 = new Matrix(); + c1.setConcat(m2, m1); + Matrix c2 = new Matrix(); + c2.setConcat(m4, m3); + m.setConcat(c2, c1); + } + + Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), + (int) returnRect.height(), Bitmap.Config.ARGB_8888); + if (tmp != null) { + Canvas c = new Canvas(tmp); + Paint p = new Paint(); + p.setFilterBitmap(true); + c.drawBitmap(crop, m, p); + crop = tmp; + } + } + + if (mSaveCroppedBitmap) { + mCroppedBitmap = crop; + } + + // Compress to byte array + ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); + if (crop.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) { + // If we need to set to the wallpaper, set it + if (mSetWallpaper && wallpaperManager != null) { + try { + byte[] outByteArray = tmpOut.toByteArray(); + wallpaperManager.setStream(new ByteArrayInputStream(outByteArray)); + if (mOnBitmapCroppedHandler != null) { + mOnBitmapCroppedHandler.onBitmapCropped(outByteArray); + } + } catch (IOException e) { + Log.w(LOGTAG, "cannot write stream to wallpaper", e); + failure = true; + } + } + } else { + Log.w(LOGTAG, "cannot compress bitmap"); + failure = true; + } + } + return !failure; // True if any of the operations failed + } + + @Override + protected Boolean doInBackground(Void... params) { + return cropBitmap(); + } + + @Override + protected void onPostExecute(Boolean result) { + if (mOnEndRunnable != null) { + mOnEndRunnable.run(); + } + } +} \ No newline at end of file diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java index a671ed2b9d..9ac5c1bf7e 100644 --- a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java +++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java @@ -16,87 +16,24 @@ package com.android.gallery3d.common; -import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.os.Build; -import android.util.FloatMath; +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; import android.util.Log; -import java.io.ByteArrayOutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import com.android.gallery3d.exif.ExifInterface; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; public class BitmapUtils { + private static final String TAG = "BitmapUtils"; - private static final int DEFAULT_JPEG_QUALITY = 90; - public static final int UNCONSTRAINED = -1; - - private BitmapUtils(){} - - /* - * Compute the sample size as a function of minSideLength - * and maxNumOfPixels. - * minSideLength is used to specify that minimal width or height of a - * bitmap. - * maxNumOfPixels is used to specify the maximal size in pixels that is - * tolerable in terms of memory usage. - * - * The function returns a sample size based on the constraints. - * Both size and minSideLength can be passed in as UNCONSTRAINED, - * which indicates no care of the corresponding constraint. - * The functions prefers returning a sample size that - * generates a smaller bitmap, unless minSideLength = UNCONSTRAINED. - * - * Also, the function rounds up the sample size to a power of 2 or multiple - * of 8 because BitmapFactory only honors sample size this way. - * For example, BitmapFactory downsamples an image by 2 even though the - * request is 3. So we round up the sample size to avoid OOM. - */ - public static int computeSampleSize(int width, int height, - int minSideLength, int maxNumOfPixels) { - int initialSize = computeInitialSampleSize( - width, height, minSideLength, maxNumOfPixels); - - return initialSize <= 8 - ? Utils.nextPowerOf2(initialSize) - : (initialSize + 7) / 8 * 8; - } - - private static int computeInitialSampleSize(int w, int h, - int minSideLength, int maxNumOfPixels) { - if (maxNumOfPixels == UNCONSTRAINED - && minSideLength == UNCONSTRAINED) return 1; - - int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 : - (int) FloatMath.ceil(FloatMath.sqrt((float) (w * h) / maxNumOfPixels)); - - if (minSideLength == UNCONSTRAINED) { - return lowerBound; - } else { - int sampleSize = Math.min(w / minSideLength, h / minSideLength); - return Math.max(sampleSize, lowerBound); - } - } - - // This computes a sample size which makes the longer side at least - // minSideLength long. If that's not possible, return 1. - public static int computeSampleSizeLarger(int w, int h, - int minSideLength) { - int initialSize = Math.max(w / minSideLength, h / minSideLength); - if (initialSize <= 1) return 1; - - return initialSize <= 8 - ? Utils.prevPowerOf2(initialSize) - : initialSize / 8 * 8; - } // Find the min x that 1 / x >= scale public static int computeSampleSizeLarger(float scale) { - int initialSize = (int) FloatMath.floor(1f / scale); + int initialSize = (int) Math.floor(1f / scale); if (initialSize <= 1) return 1; return initialSize <= 8 @@ -104,157 +41,41 @@ public class BitmapUtils { : initialSize / 8 * 8; } - // Find the max x that 1 / x <= scale. - public static int computeSampleSize(float scale) { - Utils.assertTrue(scale > 0); - int initialSize = Math.max(1, (int) FloatMath.ceil(1 / scale)); - return initialSize <= 8 - ? Utils.nextPowerOf2(initialSize) - : (initialSize + 7) / 8 * 8; + public static int getRotationFromExif(Context context, Uri uri) { + return BitmapUtils.getRotationFromExifHelper(null, 0, context, uri); } - public static Bitmap resizeBitmapByScale( - Bitmap bitmap, float scale, boolean recycle) { - int width = Math.round(bitmap.getWidth() * scale); - int height = Math.round(bitmap.getHeight() * scale); - if (width == bitmap.getWidth() - && height == bitmap.getHeight()) return bitmap; - Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap)); - Canvas canvas = new Canvas(target); - canvas.scale(scale, scale); - Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); - canvas.drawBitmap(bitmap, 0, 0, paint); - if (recycle) bitmap.recycle(); - return target; + public static int getRotationFromExif(Resources res, int resId) { + return BitmapUtils.getRotationFromExifHelper(res, resId, null, null); } - private static Bitmap.Config getConfig(Bitmap bitmap) { - Bitmap.Config config = bitmap.getConfig(); - if (config == null) { - config = Bitmap.Config.ARGB_8888; - } - return config; - } - - public static Bitmap resizeDownBySideLength( - Bitmap bitmap, int maxLength, boolean recycle) { - int srcWidth = bitmap.getWidth(); - int srcHeight = bitmap.getHeight(); - float scale = Math.min( - (float) maxLength / srcWidth, (float) maxLength / srcHeight); - if (scale >= 1.0f) return bitmap; - return resizeBitmapByScale(bitmap, scale, recycle); - } - - public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) { - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - if (w == size && h == size) return bitmap; - - // scale the image so that the shorter side equals to the target; - // the longer side will be center-cropped. - float scale = (float) size / Math.min(w, h); - - Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap)); - int width = Math.round(scale * bitmap.getWidth()); - int height = Math.round(scale * bitmap.getHeight()); - Canvas canvas = new Canvas(target); - canvas.translate((size - width) / 2f, (size - height) / 2f); - canvas.scale(scale, scale); - Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); - canvas.drawBitmap(bitmap, 0, 0, paint); - if (recycle) bitmap.recycle(); - return target; - } - - public static void recycleSilently(Bitmap bitmap) { - if (bitmap == null) return; + private static int getRotationFromExifHelper(Resources res, int resId, Context context, Uri uri) { + ExifInterface ei = new ExifInterface(); + InputStream is = null; + BufferedInputStream bis = null; try { - bitmap.recycle(); - } catch (Throwable t) { - Log.w(TAG, "unable recycle bitmap", t); - } - } - - public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) { - if (rotation == 0) return source; - int w = source.getWidth(); - int h = source.getHeight(); - Matrix m = new Matrix(); - m.postRotate(rotation); - Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true); - if (recycle) source.recycle(); - return bitmap; - } - - public static Bitmap createVideoThumbnail(String filePath) { - // MediaMetadataRetriever is available on API Level 8 - // but is hidden until API Level 10 - Class clazz = null; - Object instance = null; - try { - clazz = Class.forName("android.media.MediaMetadataRetriever"); - instance = clazz.newInstance(); - - Method method = clazz.getMethod("setDataSource", String.class); - method.invoke(instance, filePath); - - // The method name changes between API Level 9 and 10. - if (Build.VERSION.SDK_INT <= 9) { - return (Bitmap) clazz.getMethod("captureFrame").invoke(instance); + if (uri != null) { + is = context.getContentResolver().openInputStream(uri); + bis = new BufferedInputStream(is); + ei.readExif(bis); } else { - byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance); - if (data != null) { - Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - if (bitmap != null) return bitmap; - } - return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance); + is = res.openRawResource(resId); + bis = new BufferedInputStream(is); + ei.readExif(bis); } - } catch (IllegalArgumentException ex) { - // Assume this is a corrupt video file - } catch (RuntimeException ex) { - // Assume this is a corrupt video file. - } catch (InstantiationException e) { - Log.e(TAG, "createVideoThumbnail", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "createVideoThumbnail", e); - } catch (ClassNotFoundException e) { - Log.e(TAG, "createVideoThumbnail", e); - } catch (NoSuchMethodException e) { - Log.e(TAG, "createVideoThumbnail", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "createVideoThumbnail", e); + Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); + if (ori != null) { + return ExifInterface.getRotationForOrientationValue(ori.shortValue()); + } + } catch (IOException e) { + Log.w(TAG, "Getting exif data failed", e); + } catch (NullPointerException e) { + // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid + Log.w(TAG, "Getting exif data failed", e); } finally { - try { - if (instance != null) { - clazz.getMethod("release").invoke(instance); - } - } catch (Exception ignored) { - } + Utils.closeSilently(bis); + Utils.closeSilently(is); } - return null; - } - - public static byte[] compressToBytes(Bitmap bitmap) { - return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY); - } - - public static byte[] compressToBytes(Bitmap bitmap, int quality) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(65536); - bitmap.compress(CompressFormat.JPEG, quality, baos); - return baos.toByteArray(); - } - - public static boolean isSupportedByRegionDecoder(String mimeType) { - if (mimeType == null) return false; - mimeType = mimeType.toLowerCase(); - return mimeType.startsWith("image/") && - (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp")); - } - - public static boolean isRotationSupported(String mimeType) { - if (mimeType == null) return false; - mimeType = mimeType.toLowerCase(); - return mimeType.equals("image/jpeg"); + return 0; } } diff --git a/WallpaperPicker/src/com/android/gallery3d/common/Utils.java b/WallpaperPicker/src/com/android/gallery3d/common/Utils.java index 614a081c8f..8466c22cb3 100644 --- a/WallpaperPicker/src/com/android/gallery3d/common/Utils.java +++ b/WallpaperPicker/src/com/android/gallery3d/common/Utils.java @@ -16,32 +16,16 @@ package com.android.gallery3d.common; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; -import android.os.Build; +import android.graphics.RectF; import android.os.ParcelFileDescriptor; -import android.text.TextUtils; import android.util.Log; import java.io.Closeable; import java.io.IOException; -import java.io.InterruptedIOException; public class Utils { private static final String TAG = "Utils"; - private static final String DEBUG_TAG = "GalleryDebug"; - - private static final long POLY64REV = 0x95AC9329AC4BC9B5L; - private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL; - - private static long[] sCrcTable = new long[256]; - - private static final boolean IS_DEBUG_BUILD = - Build.TYPE.equals("eng") || Build.TYPE.equals("userdebug"); - - private static final String MASK_STRING = "********************************"; // Throws AssertionError if the input is false. public static void assertTrue(boolean cond) { @@ -50,28 +34,6 @@ public class Utils { } } - // Throws AssertionError with the message. We had a method having the form - // assertTrue(boolean cond, String message, Object ... args); - // However a call to that method will cause memory allocation even if the - // condition is false (due to autoboxing generated by "Object ... args"), - // so we don't use that anymore. - public static void fail(String message, Object ... args) { - throw new AssertionError( - args.length == 0 ? message : String.format(message, args)); - } - - // Throws NullPointerException if the input is null. - public static T checkNotNull(T object) { - if (object == null) throw new NullPointerException(); - return object; - } - - // Returns true if two input Object are both null or equal - // to each other. - public static boolean equals(Object a, Object b) { - return (a == b) || (a == null ? false : a.equals(b)); - } - // Returns the next power of two. // Returns the input if it is already power of 2. // Throws IllegalArgumentException if the input is <= 0 or @@ -102,87 +64,6 @@ public class Utils { return x; } - // Returns the input value x clamped to the range [min, max]. - public static float clamp(float x, float min, float max) { - if (x > max) return max; - if (x < min) return min; - return x; - } - - // Returns the input value x clamped to the range [min, max]. - public static long clamp(long x, long min, long max) { - if (x > max) return max; - if (x < min) return min; - return x; - } - - public static boolean isOpaque(int color) { - return color >>> 24 == 0xFF; - } - - public static void swap(int[] array, int i, int j) { - int temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } - - /** - * A function thats returns a 64-bit crc for string - * - * @param in input string - * @return a 64-bit crc value - */ - public static final long crc64Long(String in) { - if (in == null || in.length() == 0) { - return 0; - } - return crc64Long(getBytes(in)); - } - - static { - // http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c - long part; - for (int i = 0; i < 256; i++) { - part = i; - for (int j = 0; j < 8; j++) { - long x = ((int) part & 1) != 0 ? POLY64REV : 0; - part = (part >> 1) ^ x; - } - sCrcTable[i] = part; - } - } - - public static final long crc64Long(byte[] buffer) { - long crc = INITIALCRC; - for (int k = 0, n = buffer.length; k < n; ++k) { - crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8); - } - return crc; - } - - public static byte[] getBytes(String in) { - byte[] result = new byte[in.length() * 2]; - int output = 0; - for (char ch : in.toCharArray()) { - result[output++] = (byte) (ch & 0xFF); - result[output++] = (byte) (ch >> 8); - } - return result; - } - - public static void closeSilently(Closeable c) { - if (c == null) return; - try { - c.close(); - } catch (IOException t) { - Log.w(TAG, "close fail ", t); - } - } - - public static int compare(long a, long b) { - return a < b ? -1 : a == b ? 0 : 1; - } - public static int ceilLog2(float value) { int i; for (i = 0; i < 31; i++) { @@ -199,6 +80,15 @@ public class Utils { return i - 1; } + public static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (IOException t) { + Log.w(TAG, "close fail ", t); + } + } + public static void closeSilently(ParcelFileDescriptor fd) { try { if (fd != null) fd.close(); @@ -215,126 +105,25 @@ public class Utils { } } - public static float interpolateAngle( - float source, float target, float progress) { - // interpolate the angle from source to target - // We make the difference in the range of [-179, 180], this is the - // shortest path to change source to target. - float diff = target - source; - if (diff < 0) diff += 360f; - if (diff > 180) diff -= 360f; - - float result = source + diff * progress; - return result < 0 ? result + 360f : result; - } - - public static float interpolateScale( - float source, float target, float progress) { - return source + progress * (target - source); - } - - public static String ensureNotNull(String value) { - return value == null ? "" : value; - } - - public static float parseFloatSafely(String content, float defaultValue) { - if (content == null) return defaultValue; - try { - return Float.parseFloat(content); - } catch (NumberFormatException e) { - return defaultValue; + public static RectF getMaxCropRect( + int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) { + RectF cropRect = new RectF(); + // Get a crop rect that will fit this + if (inWidth / (float) inHeight > outWidth / (float) outHeight) { + cropRect.top = 0; + cropRect.bottom = inHeight; + cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2; + cropRect.right = inWidth - cropRect.left; + if (leftAligned) { + cropRect.right -= cropRect.left; + cropRect.left = 0; + } + } else { + cropRect.left = 0; + cropRect.right = inWidth; + cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2; + cropRect.bottom = inHeight - cropRect.top; } - } - - public static int parseIntSafely(String content, int defaultValue) { - if (content == null) return defaultValue; - try { - return Integer.parseInt(content); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - public static boolean isNullOrEmpty(String exifMake) { - return TextUtils.isEmpty(exifMake); - } - - public static void waitWithoutInterrupt(Object object) { - try { - object.wait(); - } catch (InterruptedException e) { - Log.w(TAG, "unexpected interrupt: " + object); - } - } - - public static boolean handleInterrruptedException(Throwable e) { - // A helper to deal with the interrupt exception - // If an interrupt detected, we will setup the bit again. - if (e instanceof InterruptedIOException - || e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - return true; - } - return false; - } - - /** - * @return String with special XML characters escaped. - */ - public static String escapeXml(String s) { - StringBuilder sb = new StringBuilder(); - for (int i = 0, len = s.length(); i < len; ++i) { - char c = s.charAt(i); - switch (c) { - case '<': sb.append("<"); break; - case '>': sb.append(">"); break; - case '\"': sb.append("""); break; - case '\'': sb.append("'"); break; - case '&': sb.append("&"); break; - default: sb.append(c); - } - } - return sb.toString(); - } - - public static String getUserAgent(Context context) { - PackageInfo packageInfo; - try { - packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); - } catch (NameNotFoundException e) { - throw new IllegalStateException("getPackageInfo failed"); - } - return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s", - packageInfo.packageName, - packageInfo.versionName, - Build.BRAND, - Build.DEVICE, - Build.MODEL, - Build.ID, - Build.VERSION.SDK_INT, - Build.VERSION.RELEASE, - Build.VERSION.INCREMENTAL); - } - - public static String[] copyOf(String[] source, int newSize) { - String[] result = new String[newSize]; - newSize = Math.min(source.length, newSize); - System.arraycopy(source, 0, result, 0, newSize); - return result; - } - - // Mask information for debugging only. It returns info.toString() directly - // for debugging build (i.e., 'eng' and 'userdebug') and returns a mask ("****") - // in release build to protect the information (e.g. for privacy issue). - public static String maskDebugInfo(Object info) { - if (info == null) return null; - String s = info.toString(); - int length = Math.min(s.length(), MASK_STRING.length()); - return IS_DEBUG_BUILD ? s : MASK_STRING.substring(0, length); - } - - // This method should be ONLY used for debugging. - public static void debug(String message, Object ... args) { - Log.v(DEBUG_TAG, String.format(message, args)); + return cropRect; } } diff --git a/WallpaperPicker/src/com/android/gallery3d/exif/ExifInterface.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifInterface.java index a1cf0fc852..9247e879f1 100644 --- a/WallpaperPicker/src/com/android/gallery3d/exif/ExifInterface.java +++ b/WallpaperPicker/src/com/android/gallery3d/exif/ExifInterface.java @@ -1247,7 +1247,7 @@ public class ExifInterface { if (l == null || l.length <= 0) { return null; } - return new Long(l[0]); + return Long.valueOf(l[0]); } /** @@ -1266,7 +1266,7 @@ public class ExifInterface { if (l == null || l.length <= 0) { return null; } - return new Integer(l[0]); + return Integer.valueOf(l[0]); } /** @@ -1285,7 +1285,7 @@ public class ExifInterface { if (l == null || l.length <= 0) { return null; } - return new Byte(l[0]); + return Byte.valueOf(l[0]); } /** diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java index 2e77b903f2..0f3efb7273 100644 --- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java @@ -27,7 +27,6 @@ import java.util.WeakHashMap; // If a BasicTexture is loaded into GL memory, it has a GL texture id. public abstract class BasicTexture implements Texture { - @SuppressWarnings("unused") private static final String TAG = "BasicTexture"; protected static final int UNSPECIFIED = -1; diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/BitmapTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BitmapTexture.java index 100b0b3b9c..f8b01cb428 100644 --- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/BitmapTexture.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BitmapTexture.java @@ -18,7 +18,7 @@ package com.android.gallery3d.glrenderer; import android.graphics.Bitmap; -import junit.framework.Assert; +import com.android.gallery3d.common.Utils; // BitmapTexture is a texture whose content is specified by a fixed Bitmap. // @@ -34,7 +34,7 @@ public class BitmapTexture extends UploadedTexture { public BitmapTexture(Bitmap bitmap, boolean hasBorder) { super(hasBorder); - Assert.assertTrue(bitmap != null && !bitmap.isRecycled()); + Utils.assertTrue(bitmap != null && !bitmap.isRecycled()); mContentBitmap = bitmap; } diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java index 4ead1315e0..933260b48e 100644 --- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java @@ -23,8 +23,6 @@ import android.opengl.GLUtils; import android.opengl.Matrix; import android.util.Log; -import com.android.gallery3d.util.IntArray; - import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -698,6 +696,7 @@ public class GLES20Canvas implements GLCanvas { } private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) { + deleteRecycledResources(); GLES20.glUseProgram(program); checkError(); enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA); diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLPaint.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLPaint.java index 16b220690e..b26e9ab294 100644 --- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLPaint.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLPaint.java @@ -16,7 +16,7 @@ package com.android.gallery3d.glrenderer; -import junit.framework.Assert; +import com.android.gallery3d.common.Utils; public class GLPaint { private float mLineWidth = 1f; @@ -31,7 +31,7 @@ public class GLPaint { } public void setLineWidth(float width) { - Assert.assertTrue(width >= 0); + Utils.assertTrue(width >= 0); mLineWidth = width; } diff --git a/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/IntArray.java similarity index 97% rename from WallpaperPicker/src/com/android/gallery3d/util/IntArray.java rename to WallpaperPicker/src/com/android/gallery3d/glrenderer/IntArray.java index 2c4dc2c839..f123624d6e 100644 --- a/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/IntArray.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.gallery3d.util; +package com.android.gallery3d.glrenderer; public class IntArray { private static final int INIT_CAPACITY = 8; diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java index f41a979b7d..8075bf8689 100644 --- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java @@ -20,7 +20,8 @@ import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.opengl.GLUtils; -import junit.framework.Assert; +import com.android.gallery3d.common.Utils; +import com.android.launcher3.util.Thunk; import java.util.HashMap; @@ -82,7 +83,7 @@ public abstract class UploadedTexture extends BasicTexture { return mIsUploading; } - private static class BorderKey implements Cloneable { + @Thunk static class BorderKey implements Cloneable { public boolean vertical; public Config config; public int length; @@ -144,7 +145,7 @@ public abstract class UploadedTexture extends BasicTexture { } private void freeBitmap() { - Assert.assertTrue(mBitmap != null); + Utils.assertTrue(mBitmap != null); onFreeBitmap(mBitmap); mBitmap = null; } @@ -219,7 +220,7 @@ public abstract class UploadedTexture extends BasicTexture { int texWidth = getTextureWidth(); int texHeight = getTextureHeight(); - Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight); + Utils.assertTrue(bWidth <= texWidth && bHeight <= texHeight); // Upload the bitmap to a new texture. mId = canvas.getGLId().generateTexture(); diff --git a/WallpaperPicker/src/com/android/launcher3/CropView.java b/WallpaperPicker/src/com/android/launcher3/CropView.java index 578b8eafd0..50f779add0 100644 --- a/WallpaperPicker/src/com/android/launcher3/CropView.java +++ b/WallpaperPicker/src/com/android/launcher3/CropView.java @@ -21,7 +21,6 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.RectF; import android.util.AttributeSet; -import android.util.FloatMath; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; @@ -300,12 +299,12 @@ public class CropView extends TiledImageView implements OnScaleGestureListener { adjustment[0] = (edges.right - getWidth()) / scale; } if (edges.top > 0) { - adjustment[1] = FloatMath.ceil(edges.top / scale); + adjustment[1] = (float) Math.ceil(edges.top / scale); } else if (edges.bottom < getHeight()) { adjustment[1] = (edges.bottom - getHeight()) / scale; } for (int dim = 0; dim <= 1; dim++) { - if (coef[dim] > 0) adjustment[dim] = FloatMath.ceil(adjustment[dim]); + if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]); } mInverseRotateMatrix.mapPoints(adjustment); diff --git a/src/com/android/launcher3/LauncherWallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/LauncherWallpaperPickerActivity.java similarity index 69% rename from src/com/android/launcher3/LauncherWallpaperPickerActivity.java rename to WallpaperPicker/src/com/android/launcher3/LauncherWallpaperPickerActivity.java index 10fe013eee..091c05462b 100644 --- a/src/com/android/launcher3/LauncherWallpaperPickerActivity.java +++ b/WallpaperPicker/src/com/android/launcher3/LauncherWallpaperPickerActivity.java @@ -16,15 +16,6 @@ package com.android.launcher3; -import android.content.Intent; - +// TODO: Remove this class public class LauncherWallpaperPickerActivity extends WallpaperPickerActivity { - @Override - public void startActivityForResultSafely(Intent intent, int requestCode) { - Utilities.startActivityForResultSafely(this, intent, requestCode); - } - @Override - public boolean enableRotation() { - return Utilities.isRotationEnabled(this); - } -} +} \ No newline at end of file diff --git a/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java index 88f4461bfa..b53fce1195 100644 --- a/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java +++ b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java @@ -30,11 +30,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.TextView; +import com.android.launcher3.util.Thunk; + import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; @@ -50,7 +51,7 @@ public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter private final LayoutInflater mInflater; private final PackageManager mPackageManager; - private List mWallpapers; + @Thunk List mWallpapers; @SuppressWarnings("unchecked") public LiveWallpaperListAdapter(Context context) { @@ -90,8 +91,6 @@ public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter view = convertView; } - WallpaperPickerActivity.setWallpaperItemPaddingToZero((FrameLayout) view); - LiveWallpaperTile wallpaperInfo = mWallpapers.get(position); wallpaperInfo.setView(view); ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); @@ -111,8 +110,8 @@ public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter } public static class LiveWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo { - private Drawable mThumbnail; - private WallpaperInfo mInfo; + @Thunk Drawable mThumbnail; + @Thunk WallpaperInfo mInfo; public LiveWallpaperTile(Drawable thumbnail, WallpaperInfo info, Intent intent) { mThumbnail = thumbnail; mInfo = info; @@ -122,8 +121,8 @@ public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter Intent preview = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER); preview.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, mInfo.getComponent()); - a.onLiveWallpaperPickerLaunch(mInfo); - a.startActivityForResultSafely(preview, WallpaperPickerActivity.PICK_LIVE_WALLPAPER); + a.startActivityForResultSafely(preview, + WallpaperPickerActivity.PICK_WALLPAPER_THIRD_PARTY_ACTIVITY); } } diff --git a/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java b/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java index 9f92bc105e..64b0ac4662 100644 --- a/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java +++ b/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java @@ -16,7 +16,6 @@ package com.android.launcher3; -import android.app.Activity; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -60,13 +59,13 @@ public class SavedWallpaperImages extends BaseAdapter implements ListAdapter { } } - public SavedWallpaperImages(Activity context) { + public SavedWallpaperImages(Context context) { // We used to store the saved images in the cache directory, but that meant they'd get // deleted sometimes-- move them to the data directory ImageDb.moveFromCacheDirectoryIfNecessary(context); mDb = new ImageDb(context); mContext = context; - mLayoutInflater = context.getLayoutInflater(); + mLayoutInflater = LayoutInflater.from(context); } public void loadThumbnailsAndImageIdList() { diff --git a/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java index 7a4d48ca93..f46da53ec5 100644 --- a/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java +++ b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java @@ -28,16 +28,15 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import android.widget.FrameLayout; import android.widget.ListAdapter; import android.widget.TextView; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.List; public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements ListAdapter { - private static final String LOG_TAG = "LiveWallpaperListAdapter"; - private final LayoutInflater mInflater; private final PackageManager mPackageManager; private final int mIconSize; @@ -46,7 +45,7 @@ public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements new ArrayList(); public static class ThirdPartyWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo { - private ResolveInfo mResolveInfo; + @Thunk ResolveInfo mResolveInfo; public ThirdPartyWallpaperTile(ResolveInfo resolveInfo) { mResolveInfo = resolveInfo; } @@ -62,7 +61,7 @@ public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements } public ThirdPartyWallpaperPickerListAdapter(Context context) { - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mInflater = LayoutInflater.from(context); mPackageManager = context.getPackageManager(); mIconSize = context.getResources().getDimensionPixelSize(R.dimen.wallpaperItemIconSize); final PackageManager pm = mPackageManager; @@ -126,8 +125,6 @@ public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements view = convertView; } - WallpaperPickerActivity.setWallpaperItemPaddingToZero((FrameLayout) view); - ResolveInfo info = mThirdPartyWallpaperPickers.get(position).mResolveInfo; TextView label = (TextView) view.findViewById(R.id.wallpaper_item_label); label.setText(info.loadLabel(mPackageManager)); diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java index fa8ec64c2b..f2bb509440 100644 --- a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java @@ -16,6 +16,7 @@ package com.android.launcher3; +import android.annotation.TargetApi; import android.app.ActionBar; import android.app.Activity; import android.app.WallpaperManager; @@ -25,42 +26,41 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; -import android.graphics.BitmapFactory; -import android.graphics.BitmapRegionDecoder; -import android.graphics.Canvas; import android.graphics.Matrix; -import android.graphics.Paint; import android.graphics.Point; -import android.graphics.Rect; import android.graphics.RectF; import android.net.Uri; -import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; import android.util.Log; import android.view.Display; import android.view.View; -import android.view.WindowManager; import android.widget.Toast; +import com.android.gallery3d.common.BitmapCropTask; +import com.android.gallery3d.common.BitmapUtils; import com.android.gallery3d.common.Utils; -import com.android.gallery3d.exif.ExifInterface; +import com.android.launcher3.base.BaseActivity; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.WallpaperUtils; import com.android.photos.BitmapRegionTileSource; import com.android.photos.BitmapRegionTileSource.BitmapSource; +import com.android.photos.BitmapRegionTileSource.BitmapSource.InBitmapProvider; +import com.android.photos.views.TiledImageRenderer.TileSource; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; -public class WallpaperCropActivity extends Activity { +public class WallpaperCropActivity extends BaseActivity implements Handler.Callback { private static final String LOGTAG = "Launcher3.CropActivity"; - protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; - protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; - private static final int DEFAULT_COMPRESS_QUALITY = 90; + protected static final String WALLPAPER_WIDTH_KEY = WallpaperUtils.WALLPAPER_WIDTH_KEY; + protected static final String WALLPAPER_HEIGHT_KEY = WallpaperUtils.WALLPAPER_HEIGHT_KEY; + /** * The maximum bitmap size we allow to be returned through the intent. * Intents have a maximum of 1MB in total size. However, the Bitmap seems to @@ -69,17 +69,31 @@ public class WallpaperCropActivity extends Activity { * array instead of a Bitmap instance to avoid overhead. */ public static final int MAX_BMAP_IN_INTENT = 750000; - private static final float WALLPAPER_SCREENS_SPAN = 2f; + public static final float WALLPAPER_SCREENS_SPAN = WallpaperUtils.WALLPAPER_SCREENS_SPAN; - protected static Point sDefaultWallpaperSize; + private static final int MSG_LOAD_IMAGE = 1; protected CropView mCropView; + protected View mProgressView; protected Uri mUri; protected View mSetWallpaperButton; + private HandlerThread mLoaderThread; + private Handler mLoaderHandler; + @Thunk LoadRequest mCurrentLoadRequest; + private byte[] mTempStorageForDecoding = new byte[16 * 1024]; + // A weak-set of reusable bitmaps + @Thunk Set mReusableBitmaps = + Collections.newSetFromMap(new WeakHashMap()); + @Override - protected void onCreate(Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + mLoaderThread = new HandlerThread("wallpaper_loader"); + mLoaderThread.start(); + mLoaderHandler = new Handler(mLoaderThread.getLooper(), this); + init(); if (!enableRotation()) { setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT); @@ -90,6 +104,7 @@ public class WallpaperCropActivity extends Activity { setContentView(R.layout.wallpaper_cropper); mCropView = (CropView) findViewById(R.id.cropView); + mProgressView = findViewById(R.id.loading); Intent cropIntent = getIntent(); final Uri imageUri = cropIntent.getData(); @@ -116,13 +131,12 @@ public class WallpaperCropActivity extends Activity { // Load image in background final BitmapRegionTileSource.UriBitmapSource bitmapSource = - new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024); + new BitmapRegionTileSource.UriBitmapSource(getContext(), imageUri); mSetWallpaperButton.setEnabled(false); Runnable onLoad = new Runnable() { public void run() { if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) { - Toast.makeText(WallpaperCropActivity.this, - getString(R.string.wallpaper_load_fail), + Toast.makeText(getContext(), R.string.wallpaper_load_fail, Toast.LENGTH_LONG).show(); finish(); } else { @@ -130,188 +144,163 @@ public class WallpaperCropActivity extends Activity { } } }; - setCropViewTileSource(bitmapSource, true, false, onLoad); + setCropViewTileSource(bitmapSource, true, false, null, onLoad); } @Override - protected void onDestroy() { + public void onDestroy() { if (mCropView != null) { mCropView.destroy(); } + if (mLoaderThread != null) { + mLoaderThread.quit(); + } super.onDestroy(); } - public void setCropViewTileSource( - final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled, - final boolean moveToLeft, final Runnable postExecute) { - final Context context = WallpaperCropActivity.this; - final View progressView = findViewById(R.id.loading); - final AsyncTask loadBitmapTask = new AsyncTask() { - protected Void doInBackground(Void...args) { - if (!isCancelled()) { - try { - bitmapSource.loadInBackground(); - } catch (SecurityException securityException) { - if (isDestroyed()) { - // Temporarily granted permissions are revoked when the activity - // finishes, potentially resulting in a SecurityException here. - // Even though {@link #isDestroyed} might also return true in different - // situations where the configuration changes, we are fine with - // catching these cases here as well. - cancel(false); - } else { - // otherwise it had a different cause and we throw it further - throw securityException; + /** + * This is called on {@link #mLoaderThread} + */ + @Override + public boolean handleMessage(Message msg) { + if (msg.what == MSG_LOAD_IMAGE) { + final LoadRequest req = (LoadRequest) msg.obj; + try { + req.src.loadInBackground(new InBitmapProvider() { + + @Override + public Bitmap forPixelCount(int count) { + Bitmap bitmapToReuse = null; + // Find the smallest bitmap that satisfies the pixel count limit + synchronized (mReusableBitmaps) { + int currentBitmapSize = Integer.MAX_VALUE; + for (Bitmap b : mReusableBitmaps) { + int bitmapSize = b.getWidth() * b.getHeight(); + if ((bitmapSize >= count) && (bitmapSize < currentBitmapSize)) { + bitmapToReuse = b; + currentBitmapSize = bitmapSize; + } + } + + if (bitmapToReuse != null) { + mReusableBitmaps.remove(bitmapToReuse); + } } + return bitmapToReuse; } - } - return null; - } - protected void onPostExecute(Void arg) { - if (!isCancelled()) { - progressView.setVisibility(View.INVISIBLE); - if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { - mCropView.setTileSource( - new BitmapRegionTileSource(context, bitmapSource), null); - mCropView.setTouchEnabled(touchEnabled); - if (moveToLeft) { - mCropView.moveToLeft(); - } - } - } - if (postExecute != null) { - postExecute.run(); + }); + } catch (SecurityException securityException) { + if (isActivityDestroyed()) { + // Temporarily granted permissions are revoked when the activity + // finishes, potentially resulting in a SecurityException here. + // Even though {@link #isDestroyed} might also return true in different + // situations where the configuration changes, we are fine with + // catching these cases here as well. + return true; + } else { + // otherwise it had a different cause and we throw it further + throw securityException; } } - }; + + req.result = new BitmapRegionTileSource(getContext(), req.src, mTempStorageForDecoding); + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (req == mCurrentLoadRequest) { + onLoadRequestComplete(req, + req.src.getLoadingState() == BitmapSource.State.LOADED); + } else { + addReusableBitmap(req.result); + } + } + }); + return true; + } + return false; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + protected boolean isActivityDestroyed() { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + && isDestroyed(); + } + + @Thunk void addReusableBitmap(TileSource src) { + synchronized (mReusableBitmaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + && src instanceof BitmapRegionTileSource) { + Bitmap preview = ((BitmapRegionTileSource) src).getBitmap(); + if (preview != null && preview.isMutable()) { + mReusableBitmaps.add(preview); + } + } + } + } + + protected void onLoadRequestComplete(LoadRequest req, boolean success) { + mCurrentLoadRequest = null; + if (success) { + TileSource oldSrc = mCropView.getTileSource(); + mCropView.setTileSource(req.result, null); + mCropView.setTouchEnabled(req.touchEnabled); + if (req.moveToLeft) { + mCropView.moveToLeft(); + } + if (req.scaleProvider != null) { + mCropView.setScale(req.scaleProvider.getScale(req.result)); + } + + // Free last image + if (oldSrc != null) { + // Call yield instead of recycle, as we only want to free GL resource. + // We can still reuse the bitmap for decoding any other image. + oldSrc.getPreview().yield(); + } + addReusableBitmap(oldSrc); + } + if (req.postExecute != null) { + req.postExecute.run(); + } + mProgressView.setVisibility(View.GONE); + } + + public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled, + boolean moveToLeft, CropViewScaleProvider scaleProvider, Runnable postExecute) { + final LoadRequest req = new LoadRequest(); + req.moveToLeft = moveToLeft; + req.src = bitmapSource; + req.touchEnabled = touchEnabled; + req.postExecute = postExecute; + req.scaleProvider = scaleProvider; + mCurrentLoadRequest = req; + + // Remove any pending requests + mLoaderHandler.removeMessages(MSG_LOAD_IMAGE); + Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget(); + // We don't want to show the spinner every time we load an image, because that would be // annoying; instead, only start showing the spinner if loading the image has taken // longer than 1 sec (ie 1000 ms) - progressView.postDelayed(new Runnable() { + mProgressView.postDelayed(new Runnable() { public void run() { - if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) { - progressView.setVisibility(View.VISIBLE); + if (mCurrentLoadRequest == req) { + mProgressView.setVisibility(View.VISIBLE); } } }, 1000); - loadBitmapTask.execute(); } + public boolean enableRotation() { return getResources().getBoolean(R.bool.allow_rotation); } - public static String getSharedPreferencesKey() { - return LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; - } - - // As a ratio of screen height, the total distance we want the parallax effect to span - // horizontally - private static float wallpaperTravelToScreenWidthRatio(int width, int height) { - float aspectRatio = width / (float) height; - - // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width - // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width - // We will use these two data points to extrapolate how much the wallpaper parallax effect - // to span (ie travel) at any aspect ratio: - - final float ASPECT_RATIO_LANDSCAPE = 16/10f; - final float ASPECT_RATIO_PORTRAIT = 10/16f; - final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; - final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; - - // To find out the desired width at different aspect ratios, we use the following two - // formulas, where the coefficient on x is the aspect ratio (width/height): - // (16/10)x + y = 1.5 - // (10/16)x + y = 1.2 - // We solve for x and y and end up with a final formula: - final float x = - (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / - (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); - final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; - return x * aspectRatio + y; - } - - static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) { - if (sDefaultWallpaperSize == null) { - Point minDims = new Point(); - Point maxDims = new Point(); - windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); - - int maxDim = Math.max(maxDims.x, maxDims.y); - int minDim = Math.max(minDims.x, minDims.y); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { - Point realSize = new Point(); - windowManager.getDefaultDisplay().getRealSize(realSize); - maxDim = Math.max(realSize.x, realSize.y); - minDim = Math.min(realSize.x, realSize.y); - } - - // We need to ensure that there is enough extra space in the wallpaper - // for the intended parallax effects - final int defaultWidth, defaultHeight; - if (isScreenLarge(res)) { - defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); - defaultHeight = maxDim; - } else { - defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); - defaultHeight = maxDim; - } - sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); - } - return sDefaultWallpaperSize; - } - - public static int getRotationFromExif(String path) { - return getRotationFromExifHelper(path, null, 0, null, null); - } - - public static int getRotationFromExif(Context context, Uri uri) { - return getRotationFromExifHelper(null, null, 0, context, uri); - } - - public static int getRotationFromExif(Resources res, int resId) { - return getRotationFromExifHelper(null, res, resId, null, null); - } - - private static int getRotationFromExifHelper( - String path, Resources res, int resId, Context context, Uri uri) { - ExifInterface ei = new ExifInterface(); - InputStream is = null; - BufferedInputStream bis = null; - try { - if (path != null) { - ei.readExif(path); - } else if (uri != null) { - is = context.getContentResolver().openInputStream(uri); - bis = new BufferedInputStream(is); - ei.readExif(bis); - } else { - is = res.openRawResource(resId); - bis = new BufferedInputStream(is); - ei.readExif(bis); - } - Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); - if (ori != null) { - return ExifInterface.getRotationForOrientationValue(ori.shortValue()); - } - } catch (IOException e) { - Log.w(LOGTAG, "Getting exif data failed", e); - } catch (NullPointerException e) { - // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid - Log.w(LOGTAG, "Getting exif data failed", e); - } finally { - Utils.closeSilently(bis); - Utils.closeSilently(is); - } - return 0; - } - protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) { - int rotation = getRotationFromExif(this, uri); + int rotation = BitmapUtils.getRotationFromExif(getContext(), uri); BitmapCropTask cropTask = new BitmapCropTask( - this, uri, null, rotation, 0, 0, true, false, null); + getContext(), uri, null, rotation, 0, 0, true, false, null); final Point bounds = cropTask.getImageBounds(); Runnable onEndCrop = new Runnable() { public void run() { @@ -331,11 +320,11 @@ public class WallpaperCropActivity extends Activity { Resources res, int resId, final boolean finishActivityWhenDone) { // crop this image and scale it down to the default wallpaper size for // this device - int rotation = getRotationFromExif(res, resId); + int rotation = BitmapUtils.getRotationFromExif(res, resId); Point inSize = mCropView.getSourceDimensions(); - Point outSize = getDefaultWallpaperSize(getResources(), + Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), getWindowManager()); - RectF crop = getMaxCropRect( + RectF crop = Utils.getMaxCropRect( inSize.x, inSize.y, outSize.x, outSize.y, false); Runnable onEndCrop = new Runnable() { public void run() { @@ -348,18 +337,14 @@ public class WallpaperCropActivity extends Activity { } } }; - BitmapCropTask cropTask = new BitmapCropTask(this, res, resId, + BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, resId, crop, rotation, outSize.x, outSize.y, true, false, onEndCrop); cropTask.execute(); } - private static boolean isScreenLarge(Resources res) { - Configuration config = res.getConfiguration(); - return config.smallestScreenWidthDp >= 720; - } - + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) protected void cropImageAndSetWallpaper(Uri uri, - OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { + BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { boolean centerCrop = getResources().getBoolean(R.bool.center_crop); // Get the crop boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; @@ -370,7 +355,7 @@ public class WallpaperCropActivity extends Activity { d.getSize(displaySize); boolean isPortrait = displaySize.x < displaySize.y; - Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(), + Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(getResources(), getWindowManager()); // Get the crop RectF cropRect = mCropView.getCrop(); @@ -444,7 +429,7 @@ public class WallpaperCropActivity extends Activity { } } }; - BitmapCropTask cropTask = new BitmapCropTask(this, uri, + BitmapCropTask cropTask = new BitmapCropTask(getContext(), uri, cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop); if (onBitmapCroppedHandler != null) { cropTask.setOnBitmapCropped(onBitmapCroppedHandler); @@ -452,375 +437,9 @@ public class WallpaperCropActivity extends Activity { cropTask.execute(); } - public interface OnBitmapCroppedHandler { - public void onBitmapCropped(byte[] imageBytes); - } - - protected static class BitmapCropTask extends AsyncTask { - Uri mInUri = null; - Context mContext; - String mInFilePath; - byte[] mInImageBytes; - int mInResId = 0; - RectF mCropBounds = null; - int mOutWidth, mOutHeight; - int mRotation; - String mOutputFormat = "jpg"; // for now - boolean mSetWallpaper; - boolean mSaveCroppedBitmap; - Bitmap mCroppedBitmap; - Runnable mOnEndRunnable; - Resources mResources; - OnBitmapCroppedHandler mOnBitmapCroppedHandler; - boolean mNoCrop; - - public BitmapCropTask(Context c, String filePath, - RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mContext = c; - mInFilePath = filePath; - init(cropBounds, rotation, - outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); - } - - public BitmapCropTask(byte[] imageBytes, - RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mInImageBytes = imageBytes; - init(cropBounds, rotation, - outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); - } - - public BitmapCropTask(Context c, Uri inUri, - RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mContext = c; - mInUri = inUri; - init(cropBounds, rotation, - outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); - } - - public BitmapCropTask(Context c, Resources res, int inResId, - RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mContext = c; - mInResId = inResId; - mResources = res; - init(cropBounds, rotation, - outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); - } - - private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, - boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { - mCropBounds = cropBounds; - mRotation = rotation; - mOutWidth = outWidth; - mOutHeight = outHeight; - mSetWallpaper = setWallpaper; - mSaveCroppedBitmap = saveCroppedBitmap; - mOnEndRunnable = onEndRunnable; - } - - public void setOnBitmapCropped(OnBitmapCroppedHandler handler) { - mOnBitmapCroppedHandler = handler; - } - - public void setNoCrop(boolean value) { - mNoCrop = value; - } - - public void setOnEndRunnable(Runnable onEndRunnable) { - mOnEndRunnable = onEndRunnable; - } - - // Helper to setup input stream - private InputStream regenerateInputStream() { - if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { - Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + - "image byte array given"); - } else { - try { - if (mInUri != null) { - return new BufferedInputStream( - mContext.getContentResolver().openInputStream(mInUri)); - } else if (mInFilePath != null) { - return mContext.openFileInput(mInFilePath); - } else if (mInImageBytes != null) { - return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); - } else { - return new BufferedInputStream(mResources.openRawResource(mInResId)); - } - } catch (FileNotFoundException e) { - Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); - } - } - return null; - } - - public Point getImageBounds() { - InputStream is = regenerateInputStream(); - if (is != null) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(is, null, options); - Utils.closeSilently(is); - if (options.outWidth != 0 && options.outHeight != 0) { - return new Point(options.outWidth, options.outHeight); - } - } - return null; - } - - public void setCropBounds(RectF cropBounds) { - mCropBounds = cropBounds; - } - - public Bitmap getCroppedBitmap() { - return mCroppedBitmap; - } - public boolean cropBitmap() { - boolean failure = false; - - - WallpaperManager wallpaperManager = null; - if (mSetWallpaper) { - wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext()); - } - - - if (mSetWallpaper && mNoCrop) { - try { - InputStream is = regenerateInputStream(); - if (is != null) { - wallpaperManager.setStream(is); - Utils.closeSilently(is); - } - } catch (IOException e) { - Log.w(LOGTAG, "cannot write stream to wallpaper", e); - failure = true; - } - return !failure; - } else { - // Find crop bounds (scaled to original image size) - Rect roundedTrueCrop = new Rect(); - Matrix rotateMatrix = new Matrix(); - Matrix inverseRotateMatrix = new Matrix(); - - Point bounds = getImageBounds(); - if (mRotation > 0) { - rotateMatrix.setRotate(mRotation); - inverseRotateMatrix.setRotate(-mRotation); - - mCropBounds.roundOut(roundedTrueCrop); - mCropBounds = new RectF(roundedTrueCrop); - - if (bounds == null) { - Log.w(LOGTAG, "cannot get bounds for image"); - failure = true; - return false; - } - - float[] rotatedBounds = new float[] { bounds.x, bounds.y }; - rotateMatrix.mapPoints(rotatedBounds); - rotatedBounds[0] = Math.abs(rotatedBounds[0]); - rotatedBounds[1] = Math.abs(rotatedBounds[1]); - - mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2); - inverseRotateMatrix.mapRect(mCropBounds); - mCropBounds.offset(bounds.x/2, bounds.y/2); - - } - - mCropBounds.roundOut(roundedTrueCrop); - - if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { - Log.w(LOGTAG, "crop has bad values for full size image"); - failure = true; - return false; - } - - // See how much we're reducing the size of the image - int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth, - roundedTrueCrop.height() / mOutHeight)); - // Attempt to open a region decoder - BitmapRegionDecoder decoder = null; - InputStream is = null; - try { - is = regenerateInputStream(); - if (is == null) { - Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); - failure = true; - return false; - } - decoder = BitmapRegionDecoder.newInstance(is, false); - Utils.closeSilently(is); - } catch (IOException e) { - Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); - } finally { - Utils.closeSilently(is); - is = null; - } - - Bitmap crop = null; - if (decoder != null) { - // Do region decoding to get crop bitmap - BitmapFactory.Options options = new BitmapFactory.Options(); - if (scaleDownSampleSize > 1) { - options.inSampleSize = scaleDownSampleSize; - } - crop = decoder.decodeRegion(roundedTrueCrop, options); - decoder.recycle(); - } - - if (crop == null) { - // BitmapRegionDecoder has failed, try to crop in-memory - is = regenerateInputStream(); - Bitmap fullSize = null; - if (is != null) { - BitmapFactory.Options options = new BitmapFactory.Options(); - if (scaleDownSampleSize > 1) { - options.inSampleSize = scaleDownSampleSize; - } - fullSize = BitmapFactory.decodeStream(is, null, options); - Utils.closeSilently(is); - } - if (fullSize != null) { - // Find out the true sample size that was used by the decoder - scaleDownSampleSize = bounds.x / fullSize.getWidth(); - mCropBounds.left /= scaleDownSampleSize; - mCropBounds.top /= scaleDownSampleSize; - mCropBounds.bottom /= scaleDownSampleSize; - mCropBounds.right /= scaleDownSampleSize; - mCropBounds.roundOut(roundedTrueCrop); - - // Adjust values to account for issues related to rounding - if (roundedTrueCrop.width() > fullSize.getWidth()) { - // Adjust the width - roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth(); - } - if (roundedTrueCrop.right > fullSize.getWidth()) { - // Adjust the left value - int adjustment = roundedTrueCrop.left - - Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width()); - roundedTrueCrop.left -= adjustment; - roundedTrueCrop.right -= adjustment; - } - if (roundedTrueCrop.height() > fullSize.getHeight()) { - // Adjust the height - roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight(); - } - if (roundedTrueCrop.bottom > fullSize.getHeight()) { - // Adjust the top value - int adjustment = roundedTrueCrop.top - - Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height()); - roundedTrueCrop.top -= adjustment; - roundedTrueCrop.bottom -= adjustment; - } - - crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, - roundedTrueCrop.top, roundedTrueCrop.width(), - roundedTrueCrop.height()); - } - } - - if (crop == null) { - Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); - failure = true; - return false; - } - if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) { - float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() }; - rotateMatrix.mapPoints(dimsAfter); - dimsAfter[0] = Math.abs(dimsAfter[0]); - dimsAfter[1] = Math.abs(dimsAfter[1]); - - if (!(mOutWidth > 0 && mOutHeight > 0)) { - mOutWidth = Math.round(dimsAfter[0]); - mOutHeight = Math.round(dimsAfter[1]); - } - - RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]); - RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight); - - Matrix m = new Matrix(); - if (mRotation == 0) { - m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); - } else { - Matrix m1 = new Matrix(); - m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f); - Matrix m2 = new Matrix(); - m2.setRotate(mRotation); - Matrix m3 = new Matrix(); - m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f); - Matrix m4 = new Matrix(); - m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); - - Matrix c1 = new Matrix(); - c1.setConcat(m2, m1); - Matrix c2 = new Matrix(); - c2.setConcat(m4, m3); - m.setConcat(c2, c1); - } - - Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), - (int) returnRect.height(), Bitmap.Config.ARGB_8888); - if (tmp != null) { - Canvas c = new Canvas(tmp); - Paint p = new Paint(); - p.setFilterBitmap(true); - c.drawBitmap(crop, m, p); - crop = tmp; - } - } - - if (mSaveCroppedBitmap) { - mCroppedBitmap = crop; - } - - // Get output compression format - CompressFormat cf = - convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); - - // Compress to byte array - ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); - if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) { - // If we need to set to the wallpaper, set it - if (mSetWallpaper && wallpaperManager != null) { - try { - byte[] outByteArray = tmpOut.toByteArray(); - wallpaperManager.setStream(new ByteArrayInputStream(outByteArray)); - if (mOnBitmapCroppedHandler != null) { - mOnBitmapCroppedHandler.onBitmapCropped(outByteArray); - } - } catch (IOException e) { - Log.w(LOGTAG, "cannot write stream to wallpaper", e); - failure = true; - } - } - } else { - Log.w(LOGTAG, "cannot compress bitmap"); - failure = true; - } - } - return !failure; // True if any of the operations failed - } - - @Override - protected Boolean doInBackground(Void... params) { - return cropBitmap(); - } - - @Override - protected void onPostExecute(Boolean result) { - if (mOnEndRunnable != null) { - mOnEndRunnable.run(); - } - } - } - protected void updateWallpaperDimensions(int width, int height) { - String spKey = getSharedPreferencesKey(); - SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); + String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; + SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); SharedPreferences.Editor editor = sp.edit(); if (width != 0 && height != 0) { editor.putInt(WALLPAPER_WIDTH_KEY, width); @@ -830,69 +449,21 @@ public class WallpaperCropActivity extends Activity { editor.remove(WALLPAPER_HEIGHT_KEY); } editor.commit(); - - suggestWallpaperDimension(getResources(), - sp, getWindowManager(), WallpaperManager.getInstance(this), true); + WallpaperUtils.suggestWallpaperDimension(getResources(), + sp, getWindowManager(), WallpaperManager.getInstance(getContext()), true); } - static public void suggestWallpaperDimension(Resources res, - final SharedPreferences sharedPrefs, - WindowManager windowManager, - final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { - final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager); - // If we have saved a wallpaper width/height, use that instead + static class LoadRequest { + BitmapSource src; + boolean touchEnabled; + boolean moveToLeft; + Runnable postExecute; + CropViewScaleProvider scaleProvider; - int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); - int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1); - - if (savedWidth == -1 || savedHeight == -1) { - if (!fallBackToDefaults) { - return; - } else { - savedWidth = defaultWallpaperSize.x; - savedHeight = defaultWallpaperSize.y; - } - } - - if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || - savedHeight != wallpaperManager.getDesiredMinimumHeight()) { - wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); - } + TileSource result; } - protected static RectF getMaxCropRect( - int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) { - RectF cropRect = new RectF(); - // Get a crop rect that will fit this - if (inWidth / (float) inHeight > outWidth / (float) outHeight) { - cropRect.top = 0; - cropRect.bottom = inHeight; - cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2; - cropRect.right = inWidth - cropRect.left; - if (leftAligned) { - cropRect.right -= cropRect.left; - cropRect.left = 0; - } - } else { - cropRect.left = 0; - cropRect.right = inWidth; - cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2; - cropRect.bottom = inHeight - cropRect.top; - } - return cropRect; - } - - protected static CompressFormat convertExtensionToCompressFormat(String extension) { - return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; - } - - protected static String getFileExtension(String requestFormat) { - String outputFormat = (requestFormat == null) - ? "jpg" - : requestFormat; - outputFormat = outputFormat.toLowerCase(); - return (outputFormat.equals("png") || outputFormat.equals("gif")) - ? "png" // We don't support gif compression. - : "jpg"; + interface CropViewScaleProvider { + float getScale(TileSource src); } } diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java index 09e0963967..88dc3e22b7 100644 --- a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java @@ -16,11 +16,11 @@ package com.android.launcher3; +import android.Manifest.permission; import android.animation.LayoutTransition; import android.annotation.TargetApi; import android.app.ActionBar; import android.app.Activity; -import android.app.WallpaperInfo; import android.app.WallpaperManager; import android.content.Context; import android.content.Intent; @@ -35,15 +35,15 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PorterDuff; -import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LevelListDrawable; +import android.Manifest; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.os.Process; import android.provider.MediaStore; import android.util.Log; import android.util.Pair; @@ -52,6 +52,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLayoutChangeListener; @@ -70,8 +71,14 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; +import com.android.gallery3d.common.BitmapCropTask; +import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.WallpaperUtils; import com.android.photos.BitmapRegionTileSource; import com.android.photos.BitmapRegionTileSource.BitmapSource; +import com.android.photos.views.TiledImageRenderer.TileSource; import java.io.File; import java.io.FileOutputStream; @@ -83,28 +90,25 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { public static final int IMAGE_PICK = 5; public static final int PICK_WALLPAPER_THIRD_PARTY_ACTIVITY = 6; - public static final int PICK_LIVE_WALLPAPER = 7; private static final String TEMP_WALLPAPER_TILES = "TEMP_WALLPAPER_TILES"; private static final String SELECTED_INDEX = "SELECTED_INDEX"; private static final int FLAG_POST_DELAY_MILLIS = 200; - private View mSelectedTile; - private boolean mIgnoreNextTap; - private OnClickListener mThumbnailOnClickListener; + @Thunk View mSelectedTile; + @Thunk boolean mIgnoreNextTap; + @Thunk OnClickListener mThumbnailOnClickListener; - private LinearLayout mWallpapersView; - private View mWallpaperStrip; + @Thunk LinearLayout mWallpapersView; + @Thunk HorizontalScrollView mWallpaperScrollContainer; - private ActionMode.Callback mActionModeCallback; - private ActionMode mActionMode; + @Thunk ActionMode.Callback mActionModeCallback; + @Thunk ActionMode mActionMode; - private View.OnLongClickListener mLongClickListener; + @Thunk View.OnLongClickListener mLongClickListener; ArrayList mTempWallpaperTiles = new ArrayList(); private SavedWallpaperImages mSavedImages; - private WallpaperInfo mLiveWallpaperInfoOnPickerLaunch; - private int mSelectedIndex = -1; - private WallpaperInfo mLastClickedLiveWallpaperInfo; + @Thunk int mSelectedIndex = -1; public static abstract class WallpaperTileInfo { protected View mView; @@ -136,45 +140,36 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { public static class UriWallpaperInfo extends WallpaperTileInfo { private Uri mUri; - private boolean mFirstClick = true; - private BitmapRegionTileSource.UriBitmapSource mBitmapSource; public UriWallpaperInfo(Uri uri) { mUri = uri; } @Override public void onClick(final WallpaperPickerActivity a) { - final Runnable onLoad; - if (!mFirstClick) { - onLoad = null; - } else { - mFirstClick = false; - a.mSetWallpaperButton.setEnabled(false); - onLoad = new Runnable() { - public void run() { - if (mBitmapSource != null && - mBitmapSource.getLoadingState() == BitmapSource.State.LOADED) { - a.selectTile(mView); - a.mSetWallpaperButton.setEnabled(true); - } else { - ViewGroup parent = (ViewGroup) mView.getParent(); - if (parent != null) { - parent.removeView(mView); - Toast.makeText(a, - a.getString(R.string.image_load_fail), - Toast.LENGTH_SHORT).show(); - } + a.setWallpaperButtonEnabled(false); + final BitmapRegionTileSource.UriBitmapSource bitmapSource = + new BitmapRegionTileSource.UriBitmapSource(a.getContext(), mUri); + a.setCropViewTileSource(bitmapSource, true, false, null, new Runnable() { + + @Override + public void run() { + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + a.selectTile(mView); + a.setWallpaperButtonEnabled(true); + } else { + ViewGroup parent = (ViewGroup) mView.getParent(); + if (parent != null) { + parent.removeView(mView); + Toast.makeText(a.getContext(), R.string.image_load_fail, + Toast.LENGTH_SHORT).show(); } } - }; - } - mBitmapSource = new BitmapRegionTileSource.UriBitmapSource( - a, mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE); - a.setCropViewTileSource(mBitmapSource, true, false, onLoad); + } + }); } @Override public void onSave(final WallpaperPickerActivity a) { boolean finishActivityWhenDone = true; - OnBitmapCroppedHandler h = new OnBitmapCroppedHandler() { + BitmapCropTask.OnBitmapCroppedHandler h = new BitmapCropTask.OnBitmapCroppedHandler() { public void onBitmapCropped(byte[] imageBytes) { Point thumbSize = getDefaultThumbnailSize(a.getResources()); // rotation is set to 0 since imageBytes has already been correctly rotated @@ -203,10 +198,19 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mThumb = thumb; } @Override - public void onClick(WallpaperPickerActivity a) { - BitmapRegionTileSource.UriBitmapSource bitmapSource = - new BitmapRegionTileSource.UriBitmapSource(a, Uri.fromFile(mFile), 1024); - a.setCropViewTileSource(bitmapSource, false, true, null); + public void onClick(final WallpaperPickerActivity a) { + a.setWallpaperButtonEnabled(false); + final BitmapRegionTileSource.UriBitmapSource bitmapSource = + new BitmapRegionTileSource.UriBitmapSource(a.getContext(), Uri.fromFile(mFile)); + a.setCropViewTileSource(bitmapSource, false, true, null, new Runnable() { + + @Override + public void run() { + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + a.setWallpaperButtonEnabled(true); + } + } + }); } @Override public void onSave(WallpaperPickerActivity a) { @@ -232,22 +236,30 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mThumb = thumb; } @Override - public void onClick(WallpaperPickerActivity a) { - BitmapRegionTileSource.ResourceBitmapSource bitmapSource = - new BitmapRegionTileSource.ResourceBitmapSource( - mResources, mResId, BitmapRegionTileSource.MAX_PREVIEW_SIZE); - bitmapSource.loadInBackground(); - BitmapRegionTileSource source = new BitmapRegionTileSource(a, bitmapSource); - CropView v = a.getCropView(); - v.setTileSource(source, null); - Point wallpaperSize = WallpaperCropActivity.getDefaultWallpaperSize( - a.getResources(), a.getWindowManager()); - RectF crop = WallpaperCropActivity.getMaxCropRect( - source.getImageWidth(), source.getImageHeight(), - wallpaperSize.x, wallpaperSize.y, false); - v.setScale(wallpaperSize.x / crop.width()); - v.setTouchEnabled(false); - a.setSystemWallpaperVisiblity(false); + public void onClick(final WallpaperPickerActivity a) { + a.setWallpaperButtonEnabled(false); + final BitmapRegionTileSource.ResourceBitmapSource bitmapSource = + new BitmapRegionTileSource.ResourceBitmapSource(mResources, mResId); + a.setCropViewTileSource(bitmapSource, false, false, new CropViewScaleProvider() { + + @Override + public float getScale(TileSource src) { + Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize( + a.getResources(), a.getWindowManager()); + RectF crop = Utils.getMaxCropRect( + src.getImageWidth(), src.getImageHeight(), + wallpaperSize.x, wallpaperSize.y, false); + return wallpaperSize.x / crop.width(); + } + }, new Runnable() { + + @Override + public void run() { + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + a.setWallpaperButtonEnabled(true); + } + } + }); } @Override public void onSave(WallpaperPickerActivity a) { @@ -272,27 +284,33 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { @Override public void onClick(WallpaperPickerActivity a) { CropView c = a.getCropView(); - - Drawable defaultWallpaper = WallpaperManager.getInstance(a).getBuiltInDrawable( - c.getWidth(), c.getHeight(), false, 0.5f, 0.5f); - + Drawable defaultWallpaper = WallpaperManager.getInstance(a.getContext()) + .getBuiltInDrawable(c.getWidth(), c.getHeight(), false, 0.5f, 0.5f); if (defaultWallpaper == null) { Log.w(TAG, "Null default wallpaper encountered."); c.setTileSource(null, null); return; } - c.setTileSource( - new DrawableTileSource(a, defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE), null); - c.setScale(1f); - c.setTouchEnabled(false); - a.setSystemWallpaperVisiblity(false); + LoadRequest req = new LoadRequest(); + req.moveToLeft = false; + req.touchEnabled = false; + req.scaleProvider = new CropViewScaleProvider() { + + @Override + public float getScale(TileSource src) { + return 1f; + } + }; + req.result = new DrawableTileSource(a.getContext(), + defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE); + a.onLoadRequestComplete(req, true); } @Override public void onSave(WallpaperPickerActivity a) { try { - WallpaperManager.getInstance(a).clear(); - a.setResult(RESULT_OK); + WallpaperManager.getInstance(a.getContext()).clear(); + a.setResult(Activity.RESULT_OK); } catch (IOException e) { Log.w("Setting wallpaper to default threw exception", e); } @@ -308,10 +326,6 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } } - public void setWallpaperStripYOffset(float offset) { - mWallpaperStrip.setPadding(0, 0, 0, (int) offset); - } - /** * shows the system wallpaper behind the window and hides the {@link * #mCropView} if visible @@ -338,7 +352,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { }, FLAG_POST_DELAY_MILLIS); } - private void changeWallpaperFlags(boolean visible) { + @Thunk void changeWallpaperFlags(boolean visible) { int desiredWallpaperFlag = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0; int currentWallpaperFlag = getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; @@ -349,24 +363,11 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } @Override - public void setCropViewTileSource(BitmapSource bitmapSource, - boolean touchEnabled, - boolean moveToLeft, - final Runnable postExecute) { - // we also want to show our own wallpaper instead of the one in the background - Runnable showPostExecuteRunnable = new Runnable() { - @Override - public void run() { - if(postExecute != null) { - postExecute.run(); - } - setSystemWallpaperVisiblity(false); - } - }; - super.setCropViewTileSource(bitmapSource, - touchEnabled, - moveToLeft, - showPostExecuteRunnable); + protected void onLoadRequestComplete(LoadRequest req, boolean success) { + super.onLoadRequestComplete(req, success); + if (success) { + setSystemWallpaperVisiblity(false); + } } // called by onCreate; this is subclassed to overwrite WallpaperCropActivity @@ -376,7 +377,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mCropView = (CropView) findViewById(R.id.cropView); mCropView.setVisibility(View.INVISIBLE); - mWallpaperStrip = findViewById(R.id.wallpaper_strip); + mProgressView = findViewById(R.id.loading); + mWallpaperScrollContainer = (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container); mCropView.setTouchCallback(new CropView.TouchCallback() { ViewPropertyAnimator mAnim; @Override @@ -384,15 +386,15 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { if (mAnim != null) { mAnim.cancel(); } - if (mWallpaperStrip.getAlpha() == 1f) { + if (mWallpaperScrollContainer.getAlpha() == 1f) { mIgnoreNextTap = true; } - mAnim = mWallpaperStrip.animate(); + mAnim = mWallpaperScrollContainer.animate(); mAnim.alpha(0f) .setDuration(150) .withEndAction(new Runnable() { public void run() { - mWallpaperStrip.setVisibility(View.INVISIBLE); + mWallpaperScrollContainer.setVisibility(View.INVISIBLE); } }); mAnim.setInterpolator(new AccelerateInterpolator(0.75f)); @@ -410,8 +412,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { if (mAnim != null) { mAnim.cancel(); } - mWallpaperStrip.setVisibility(View.VISIBLE); - mAnim = mWallpaperStrip.animate(); + mWallpaperScrollContainer.setVisibility(View.VISIBLE); + mAnim = mWallpaperScrollContainer.animate(); mAnim.alpha(1f) .setDuration(150) .setInterpolator(new DecelerateInterpolator(0.75f)); @@ -429,7 +431,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } return; } - mSetWallpaperButton.setEnabled(true); + setWallpaperButtonEnabled(true); WallpaperTileInfo info = (WallpaperTileInfo) v.getTag(); if (info.isSelectable() && v.getVisibility() == View.VISIBLE) { selectTile(v); @@ -460,18 +462,18 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { // Populate the built-in wallpapers ArrayList wallpapers = findBundledWallpapers(); mWallpapersView = (LinearLayout) findViewById(R.id.wallpaper_list); - SimpleWallpapersAdapter ia = new SimpleWallpapersAdapter(this, wallpapers); + SimpleWallpapersAdapter ia = new SimpleWallpapersAdapter(getContext(), wallpapers); populateWallpapersFromAdapter(mWallpapersView, ia, false); // Populate the saved wallpapers - mSavedImages = new SavedWallpaperImages(this); + mSavedImages = new SavedWallpaperImages(getContext()); mSavedImages.loadThumbnailsAndImageIdList(); populateWallpapersFromAdapter(mWallpapersView, mSavedImages, true); // Populate the live wallpapers final LinearLayout liveWallpapersView = (LinearLayout) findViewById(R.id.live_wallpaper_list); - final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(this); + final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(getContext()); a.registerDataSetObserver(new DataSetObserver() { public void onChanged() { liveWallpapersView.removeAllViews(); @@ -485,14 +487,13 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { final LinearLayout thirdPartyWallpapersView = (LinearLayout) findViewById(R.id.third_party_wallpaper_list); final ThirdPartyWallpaperPickerListAdapter ta = - new ThirdPartyWallpaperPickerListAdapter(this); + new ThirdPartyWallpaperPickerListAdapter(getContext()); populateWallpapersFromAdapter(thirdPartyWallpapersView, ta, false); // Add a tile for the Gallery LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list); FrameLayout pickImageTile = (FrameLayout) getLayoutInflater(). inflate(R.layout.wallpaper_picker_image_picker_item, masterWallpaperList, false); - setWallpaperItemPaddingToZero(pickImageTile); masterWallpaperList.addView(pickImageTile, 0); // Make its background the last photo taken on external storage @@ -500,10 +501,9 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { if (lastPhoto != null) { ImageView galleryThumbnailBg = (ImageView) pickImageTile.findViewById(R.id.wallpaper_image); - galleryThumbnailBg.setImageBitmap(getThumbnailOfLastPhoto()); + galleryThumbnailBg.setImageBitmap(lastPhoto); int colorOverlay = getResources().getColor(R.color.wallpaper_picker_translucent_gray); galleryThumbnailBg.setColorFilter(colorOverlay, PorterDuff.Mode.SRC_ATOP); - } PickImageInfo pickImageInfo = new PickImageInfo(); @@ -650,7 +650,11 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { }; } - private void selectTile(View v) { + public void setWallpaperButtonEnabled(boolean enabled) { + mSetWallpaperButton.setEnabled(enabled); + } + + @Thunk void selectTile(View v) { if (mSelectedTile != null) { mSelectedTile.setSelected(false); mSelectedTile = null; @@ -661,28 +665,35 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { // TODO: Remove this once the accessibility framework and // services have better support for selection state. v.announceForAccessibility( - getString(R.string.announce_selection, v.getContentDescription())); + getContext().getString(R.string.announce_selection, v.getContentDescription())); } - private void initializeScrollForRtl() { - final HorizontalScrollView scroll = - (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container); - - if (scroll.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - final ViewTreeObserver observer = scroll.getViewTreeObserver(); + @Thunk void initializeScrollForRtl() { + if (Utilities.isRtl(getResources())) { + final ViewTreeObserver observer = mWallpaperScrollContainer.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { public void onGlobalLayout() { LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list); - scroll.scrollTo(masterWallpaperList.getWidth(), 0); - scroll.getViewTreeObserver().removeOnGlobalLayoutListener(this); + mWallpaperScrollContainer.scrollTo(masterWallpaperList.getWidth(), 0); + mWallpaperScrollContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); } } protected Bitmap getThumbnailOfLastPhoto() { - Cursor cursor = MediaStore.Images.Media.query(getContentResolver(), + boolean canReadExternalStorage = getActivity().checkPermission( + Manifest.permission.READ_EXTERNAL_STORAGE, Process.myPid(), Process.myUid()) == + PackageManager.PERMISSION_GRANTED; + + if (!canReadExternalStorage) { + // MediaStore.Images.Media.EXTERNAL_CONTENT_URI requires + // the READ_EXTERNAL_STORAGE permission + return null; + } + + Cursor cursor = MediaStore.Images.Media.query(getContext().getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[] { MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATE_TAKEN}, @@ -692,7 +703,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { if (cursor != null) { if (cursor.moveToNext()) { int id = cursor.getInt(0); - thumb = MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(), + thumb = MediaStore.Images.Thumbnails.getThumbnail(getContext().getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null); } cursor.close(); @@ -700,16 +711,16 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { return thumb; } - protected void onStop() { + public void onStop() { super.onStop(); - mWallpaperStrip = findViewById(R.id.wallpaper_strip); - if (mWallpaperStrip.getAlpha() < 1f) { - mWallpaperStrip.setAlpha(1f); - mWallpaperStrip.setVisibility(View.VISIBLE); + mWallpaperScrollContainer = (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container); + if (mWallpaperScrollContainer.getAlpha() < 1f) { + mWallpaperScrollContainer.setAlpha(1f); + mWallpaperScrollContainer.setVisibility(View.VISIBLE); } } - protected void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(Bundle outState) { outState.putParcelableArrayList(TEMP_WALLPAPER_TILES, mTempWallpaperTiles); outState.putInt(SELECTED_INDEX, mSelectedIndex); } @@ -722,7 +733,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { mSelectedIndex = savedInstanceState.getInt(SELECTED_INDEX, -1); } - private void populateWallpapersFromAdapter(ViewGroup parent, BaseAdapter adapter, + @Thunk void populateWallpapersFromAdapter(ViewGroup parent, BaseAdapter adapter, boolean addLongPressHandler) { for (int i = 0; i < adapter.getCount(); i++) { FrameLayout thumbnail = (FrameLayout) adapter.getView(i, null, parent); @@ -737,7 +748,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } } - private void updateTileIndices() { + @Thunk void updateTileIndices() { LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list); final int childCount = masterWallpaperList.getChildCount(); final Resources res = getResources(); @@ -778,13 +789,13 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } } - private static Point getDefaultThumbnailSize(Resources res) { + @Thunk static Point getDefaultThumbnailSize(Resources res) { return new Point(res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth), res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight)); } - private static Bitmap createThumbnail(Point size, Context context, Uri uri, byte[] imageBytes, + @Thunk static Bitmap createThumbnail(Point size, Context context, Uri uri, byte[] imageBytes, Resources res, int resId, int rotation, boolean leftAligned) { int width = size.x; int height = size.y; @@ -812,7 +823,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { rotatedBounds[0] = Math.abs(rotatedBounds[0]); rotatedBounds[1] = Math.abs(rotatedBounds[1]); - RectF cropRect = WallpaperCropActivity.getMaxCropRect( + RectF cropRect = Utils.getMaxCropRect( (int) rotatedBounds[0], (int) rotatedBounds[1], width, height, leftAligned); cropTask.setCropBounds(cropRect); @@ -829,20 +840,19 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { final FrameLayout pickedImageThumbnail = (FrameLayout) getLayoutInflater(). inflate(R.layout.wallpaper_picker_item, mWallpapersView, false); pickedImageThumbnail.setVisibility(View.GONE); - setWallpaperItemPaddingToZero(pickedImageThumbnail); mWallpapersView.addView(pickedImageThumbnail, 0); // Load the thumbnail final ImageView image = (ImageView) pickedImageThumbnail.findViewById(R.id.wallpaper_image); final Point defaultSize = getDefaultThumbnailSize(this.getResources()); - final Context context = this; + final Context context = getContext(); new AsyncTask() { protected Bitmap doInBackground(Void...args) { try { - int rotation = WallpaperCropActivity.getRotationFromExif(context, uri); + int rotation = BitmapUtils.getRotationFromExif(context, uri); return createThumbnail(defaultSize, context, uri, null, null, 0, rotation, false); } catch (SecurityException securityException) { - if (isDestroyed()) { + if (isActivityDestroyed()) { // Temporarily granted permissions are revoked when the activity // finishes, potentially resulting in a SecurityException here. // Even though {@link #isDestroyed} might also return true in different @@ -879,45 +889,35 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } } - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == IMAGE_PICK && resultCode == RESULT_OK) { + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == IMAGE_PICK && resultCode == Activity.RESULT_OK) { if (data != null && data.getData() != null) { Uri uri = data.getData(); addTemporaryWallpaperTile(uri, false); } - } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY) { - setResult(RESULT_OK); + } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY + && resultCode == Activity.RESULT_OK) { + // Something was set on the third-party activity. + setResult(Activity.RESULT_OK); finish(); - } else if (requestCode == PICK_LIVE_WALLPAPER) { - WallpaperManager wm = WallpaperManager.getInstance(this); - final WallpaperInfo oldLiveWallpaper = mLiveWallpaperInfoOnPickerLaunch; - final WallpaperInfo clickedWallpaper = mLastClickedLiveWallpaperInfo; - WallpaperInfo newLiveWallpaper = wm.getWallpaperInfo(); - // Try to figure out if a live wallpaper was set; - if (newLiveWallpaper != null && - (oldLiveWallpaper == null - || !oldLiveWallpaper.getComponent() - .equals(newLiveWallpaper.getComponent()) - || clickedWallpaper.getComponent() - .equals(oldLiveWallpaper.getComponent()))) { - // Return if a live wallpaper was set - setResult(RESULT_OK); - finish(); - } } } - static void setWallpaperItemPaddingToZero(FrameLayout frameLayout) { - frameLayout.setPadding(0, 0, 0, 0); - frameLayout.setForeground(new ZeroPaddingDrawable(frameLayout.getForeground())); - } - private void addLongPressHandler(View v) { v.setOnLongClickListener(mLongClickListener); + + // Enable stylus button to also trigger long click. + final StylusEventHelper stylusEventHelper = new StylusEventHelper(v); + v.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent event) { + return stylusEventHelper.checkAndPerformStylusEvent(event); + } + }); } private ArrayList findBundledWallpapers() { - final PackageManager pm = getPackageManager(); + final PackageManager pm = getContext().getPackageManager(); final ArrayList bundled = new ArrayList(24); Partner partner = Partner.get(pm); @@ -961,7 +961,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { Pair r = getWallpaperArrayResourceId(); if (r != null) { try { - Resources wallpaperRes = getPackageManager().getResourcesForApplication(r.first); + Resources wallpaperRes = getContext().getPackageManager() + .getResourcesForApplication(r.first); addWallpapers(bundled, wallpaperRes, r.first.packageName, r.second); } catch (PackageManager.NameNotFoundException e) { } @@ -984,7 +985,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { try { f.createNewFile(); FileOutputStream thumbFileStream = - openFileOutput(f.getName(), Context.MODE_PRIVATE); + getContext().openFileOutput(f.getName(), Context.MODE_PRIVATE); b.compress(Bitmap.CompressFormat.JPEG, 95, thumbFileStream); thumbFileStream.close(); return true; @@ -996,17 +997,18 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } private File getDefaultThumbFile() { - return new File(getFilesDir(), Build.VERSION.SDK_INT + return new File(getContext().getFilesDir(), Build.VERSION.SDK_INT + "_" + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL); } private boolean saveDefaultWallpaperThumb(Bitmap b) { // Delete old thumbnails. - new File(getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL_OLD).delete(); - new File(getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); + new File(getContext().getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL_OLD).delete(); + new File(getContext().getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); for (int i = Build.VERSION_CODES.JELLY_BEAN; i < Build.VERSION.SDK_INT; i++) { - new File(getFilesDir(), i + "_" + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); + new File(getContext().getFilesDir(), i + "_" + + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete(); } return writeImageToFileAsJpeg(getDefaultThumbFile(), b); } @@ -1024,9 +1026,9 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } else { Resources res = getResources(); Point defaultThumbSize = getDefaultThumbnailSize(res); - int rotation = WallpaperCropActivity.getRotationFromExif(res, resId); + int rotation = BitmapUtils.getRotationFromExif(res, resId); thumb = createThumbnail( - defaultThumbSize, this, null, null, sysRes, resId, rotation, false); + defaultThumbSize, getContext(), null, null, sysRes, resId, rotation, false); if (thumb != null) { defaultWallpaperExists = saveDefaultWallpaperThumb(thumb); } @@ -1048,7 +1050,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { } else { Resources res = getResources(); Point defaultThumbSize = getDefaultThumbnailSize(res); - Drawable wallpaperDrawable = WallpaperManager.getInstance(this).getBuiltInDrawable( + Drawable wallpaperDrawable = WallpaperManager.getInstance(getContext()).getBuiltInDrawable( defaultThumbSize.x, defaultThumbSize.y, true, 0.5f, 0.5f); if (wallpaperDrawable != null) { thumb = Bitmap.createBitmap( @@ -1075,7 +1077,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { // package name should be. final String packageName = getResources().getResourcePackageName(R.array.wallpapers); try { - ApplicationInfo info = getPackageManager().getApplicationInfo(packageName, 0); + ApplicationInfo info = getContext().getPackageManager().getApplicationInfo(packageName, 0); return new Pair(info, R.array.wallpapers); } catch (PackageManager.NameNotFoundException e) { return null; @@ -1110,31 +1112,12 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { return mSavedImages; } - public void onLiveWallpaperPickerLaunch(WallpaperInfo info) { - mLastClickedLiveWallpaperInfo = info; - mLiveWallpaperInfoOnPickerLaunch = WallpaperManager.getInstance(this).getWallpaperInfo(); - } - - static class ZeroPaddingDrawable extends LevelListDrawable { - public ZeroPaddingDrawable(Drawable d) { - super(); - addLevel(0, 0, d); - setLevel(0); - } - - @Override - public boolean getPadding(Rect padding) { - padding.set(0, 0, 0, 0); - return true; - } - } - private static class SimpleWallpapersAdapter extends ArrayAdapter { private final LayoutInflater mLayoutInflater; - SimpleWallpapersAdapter(Activity activity, ArrayList wallpapers) { - super(activity, R.layout.wallpaper_picker_item, wallpapers); - mLayoutInflater = activity.getLayoutInflater(); + SimpleWallpapersAdapter(Context context, ArrayList wallpapers) { + super(context, R.layout.wallpaper_picker_item, wallpapers); + mLayoutInflater = LayoutInflater.from(context); } public View getView(int position, View convertView, ViewGroup parent) { @@ -1156,8 +1139,6 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { view = convertView; } - setWallpaperItemPaddingToZero((FrameLayout) view); - ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); if (thumb != null) { @@ -1168,9 +1149,17 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { return view; } - // In Launcher3, we override this with a method that catches exceptions - // from starting activities; didn't want to copy and paste code into here public void startActivityForResultSafely(Intent intent, int requestCode) { - startActivityForResult(intent, requestCode); + Utilities.startActivityForResultSafely(getActivity(), intent, requestCode); + } + + @Override + public boolean enableRotation() { + // Check if rotation is enabled for this device. + if (Utilities.isRotationAllowedForDevice(getContext())) + return true; + + // Check if the user has specifically enabled rotation via preferences. + return Utilities.isAllowRotationPrefEnabled(getApplicationContext(), true); } } diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java b/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java deleted file mode 100644 index ceaa043a78..0000000000 --- a/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.widget.RelativeLayout; - -public class WallpaperRootView extends RelativeLayout { - private final WallpaperPickerActivity a; - public WallpaperRootView(Context context, AttributeSet attrs) { - super(context, attrs); - a = (WallpaperPickerActivity) context; - } - public WallpaperRootView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - a = (WallpaperPickerActivity) context; - } - - protected boolean fitSystemWindows(Rect insets) { - a.setWallpaperStripYOffset(insets.bottom); - return true; - } -} diff --git a/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java b/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java new file mode 100644 index 0000000000..f8541188f5 --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java @@ -0,0 +1,21 @@ +package com.android.launcher3.base; + +import android.app.Activity; +import android.content.Context; + +/** + * A wrapper over {@link Activity} which allows to override some methods. + * The base implementation can change from an Activity to a Fragment (or any other custom + * implementation), Callers should not assume that the base class extends Context, instead use + * either {@link #getContext} or {@link #getActivity} + */ +public class BaseActivity extends Activity { + + public Context getContext() { + return this; + } + + public Activity getActivity() { + return this; + } +} diff --git a/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java b/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java index 66ece4ff67..2d496a5a66 100644 --- a/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java +++ b/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java @@ -20,15 +20,14 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.net.Uri; +import android.opengl.GLUtils; import android.os.Build; -import android.os.Build.VERSION_CODES; import android.util.Log; import com.android.gallery3d.common.BitmapUtils; @@ -148,24 +147,19 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { private static final String TAG = "BitmapRegionTileSource"; - private static final boolean REUSE_BITMAP = - Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; private static final int GL_SIZE_LIMIT = 2048; // This must be no larger than half the size of the GL_SIZE_LIMIT // due to decodePreview being allowed to be up to 2x the size of the target - public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; + private static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; public static abstract class BitmapSource { private SimpleBitmapRegionDecoder mDecoder; private Bitmap mPreview; - private int mPreviewSize; private int mRotation; public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; private State mState = State.NOT_LOADED; - public BitmapSource(int previewSize) { - mPreviewSize = previewSize; - } - public boolean loadInBackground() { + + public boolean loadInBackground(InBitmapProvider bitmapProvider) { ExifInterface ei = new ExifInterface(); if (readExif(ei)) { Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); @@ -180,18 +174,44 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { } else { int width = mDecoder.getWidth(); int height = mDecoder.getHeight(); - if (mPreviewSize != 0) { - int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE); - BitmapFactory.Options opts = new BitmapFactory.Options(); - opts.inPreferredConfig = Bitmap.Config.ARGB_8888; - opts.inPreferQualityOverSpeed = true; - float scale = (float) previewSize / Math.max(width, height); - opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); - opts.inJustDecodeBounds = false; + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inPreferredConfig = Bitmap.Config.ARGB_8888; + opts.inPreferQualityOverSpeed = true; + + float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height); + opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); + opts.inJustDecodeBounds = false; + opts.inMutable = true; + + if (bitmapProvider != null) { + int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize); + Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles); + if (reusableBitmap != null) { + // Try loading with reusable bitmap + opts.inBitmap = reusableBitmap; + try { + mPreview = loadPreviewBitmap(opts); + } catch (IllegalArgumentException e) { + Log.d(TAG, "Unable to reusage bitmap", e); + opts.inBitmap = null; + mPreview = null; + } + } + } + if (mPreview == null) { mPreview = loadPreviewBitmap(opts); } - mState = State.LOADED; + + // Verify that the bitmap can be used on GL surface + try { + GLUtils.getInternalFormat(mPreview); + GLUtils.getType(mPreview); + mState = State.LOADED; + } catch (IllegalArgumentException e) { + Log.d(TAG, "Image cannot be rendered on a GL surface", e); + mState = State.ERROR_LOADING; + } return true; } } @@ -208,10 +228,6 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { return mPreview; } - public int getPreviewSize() { - return mPreviewSize; - } - public int getRotation() { return mRotation; } @@ -219,12 +235,15 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { public abstract boolean readExif(ExifInterface ei); public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); + + public interface InBitmapProvider { + Bitmap forPixelCount(int count); + } } public static class FilePathBitmapSource extends BitmapSource { private String mPath; - public FilePathBitmapSource(String path, int previewSize) { - super(previewSize); + public FilePathBitmapSource(String path) { mPath = path; } @Override @@ -258,8 +277,7 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { public static class UriBitmapSource extends BitmapSource { private Context mContext; private Uri mUri; - public UriBitmapSource(Context context, Uri uri, int previewSize) { - super(previewSize); + public UriBitmapSource(Context context, Uri uri) { mContext = context; mUri = uri; } @@ -306,13 +324,13 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { Utils.closeSilently(is); return true; } catch (FileNotFoundException e) { - Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); + Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return false; } catch (IOException e) { - Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); + Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return false; } catch (NullPointerException e) { - Log.e("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e); + Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e); return false; } finally { Utils.closeSilently(is); @@ -323,8 +341,7 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { public static class ResourceBitmapSource extends BitmapSource { private Resources mRes; private int mResId; - public ResourceBitmapSource(Resources res, int resId, int previewSize) { - super(previewSize); + public ResourceBitmapSource(Resources res, int resId) { mRes = res; mResId = resId; } @@ -372,11 +389,9 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { // For use only by getTile private Rect mWantRegion = new Rect(); - private Rect mOverlapRegion = new Rect(); private BitmapFactory.Options mOptions; - private Canvas mCanvas; - public BitmapRegionTileSource(Context context, BitmapSource source) { + public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) { mTileSize = TiledImageRenderer.suggestedTileSize(context); mRotation = source.getRotation(); mDecoder = source.getBitmapRegionDecoder(); @@ -386,27 +401,26 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { mOptions = new BitmapFactory.Options(); mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; mOptions.inPreferQualityOverSpeed = true; - mOptions.inTempStorage = new byte[16 * 1024]; - int previewSize = source.getPreviewSize(); - if (previewSize != 0) { - previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); - // Although this is the same size as the Bitmap that is likely already - // loaded, the lifecycle is different and interactions are on a different - // thread. Thus to simplify, this source will decode its own bitmap. - Bitmap preview = decodePreview(source, previewSize); - if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { + mOptions.inTempStorage = tempStorage; + + Bitmap preview = source.getPreviewBitmap(); + if (preview != null && + preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { mPreview = new BitmapTexture(preview); - } else { - Log.w(TAG, String.format( - "Failed to create preview of apropriate size! " - + " in: %dx%d, out: %dx%d", - mWidth, mHeight, - preview.getWidth(), preview.getHeight())); - } + } else { + Log.w(TAG, String.format( + "Failed to create preview of apropriate size! " + + " in: %dx%d, out: %dx%d", + mWidth, mHeight, + preview.getWidth(), preview.getHeight())); } } } + public Bitmap getBitmap() { + return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null; + } + @Override public int getTileSize() { return mTileSize; @@ -435,10 +449,6 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { @Override public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { int tileSize = getTileSize(); - if (!REUSE_BITMAP) { - return getTileWithoutReusingBitmap(level, x, y, tileSize); - } - int t = tileSize << level; mWantRegion.set(x, y, x + t, y + t); @@ -462,64 +472,4 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { } return bitmap; } - - private Bitmap getTileWithoutReusingBitmap( - int level, int x, int y, int tileSize) { - - int t = tileSize << level; - mWantRegion.set(x, y, x + t, y + t); - - mOverlapRegion.set(0, 0, mWidth, mHeight); - - mOptions.inSampleSize = (1 << level); - Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions); - - if (bitmap == null) { - Log.w(TAG, "fail in decoding region"); - } - - if (mWantRegion.equals(mOverlapRegion)) { - return bitmap; - } - - Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); - if (mCanvas == null) { - mCanvas = new Canvas(); - } - mCanvas.setBitmap(result); - mCanvas.drawBitmap(bitmap, - (mOverlapRegion.left - mWantRegion.left) >> level, - (mOverlapRegion.top - mWantRegion.top) >> level, null); - mCanvas.setBitmap(null); - return result; - } - - /** - * Note that the returned bitmap may have a long edge that's longer - * than the targetSize, but it will always be less than 2x the targetSize - */ - private Bitmap decodePreview(BitmapSource source, int targetSize) { - Bitmap result = source.getPreviewBitmap(); - if (result == null) { - return null; - } - - // We need to resize down if the decoder does not support inSampleSize - // or didn't support the specified inSampleSize (some decoders only do powers of 2) - float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight())); - - if (scale <= 0.5) { - result = BitmapUtils.resizeBitmapByScale(result, scale, true); - } - return ensureGLCompatibleBitmap(result); - } - - private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { - if (bitmap == null || bitmap.getConfig() != null) { - return bitmap; - } - Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); - bitmap.recycle(); - return newBitmap; - } } diff --git a/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java b/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java deleted file mode 100644 index 8a05051850..0000000000 --- a/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java +++ /dev/null @@ -1,438 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.photos.views; - -import android.content.Context; -import android.graphics.SurfaceTexture; -import android.opengl.GLSurfaceView.Renderer; -import android.opengl.GLUtils; -import android.util.Log; -import android.view.TextureView; -import android.view.TextureView.SurfaceTextureListener; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import javax.microedition.khronos.egl.EGLSurface; -import javax.microedition.khronos.opengles.GL10; - -/** - * A TextureView that supports blocking rendering for synchronous drawing - */ -public class BlockingGLTextureView extends TextureView - implements SurfaceTextureListener { - - private RenderThread mRenderThread; - - public BlockingGLTextureView(Context context) { - super(context); - setSurfaceTextureListener(this); - } - - public void setRenderer(Renderer renderer) { - if (mRenderThread != null) { - throw new IllegalArgumentException("Renderer already set"); - } - mRenderThread = new RenderThread(renderer); - } - - public void render() { - mRenderThread.render(); - } - - public void destroy() { - if (mRenderThread != null) { - mRenderThread.finish(); - mRenderThread = null; - } - } - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, - int height) { - mRenderThread.setSurface(surface); - mRenderThread.setSize(width, height); - } - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, - int height) { - mRenderThread.setSize(width, height); - } - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - if (mRenderThread != null) { - mRenderThread.setSurface(null); - } - return false; - } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) { - } - - @Override - protected void finalize() throws Throwable { - try { - destroy(); - } catch (Throwable t) { - // Ignore - } - super.finalize(); - } - - /** - * An EGL helper class. - */ - - private static class EglHelper { - private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; - private static final int EGL_OPENGL_ES2_BIT = 4; - - EGL10 mEgl; - EGLDisplay mEglDisplay; - EGLSurface mEglSurface; - EGLConfig mEglConfig; - EGLContext mEglContext; - - private EGLConfig chooseEglConfig() { - int[] configsCount = new int[1]; - EGLConfig[] configs = new EGLConfig[1]; - int[] configSpec = getConfig(); - if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) { - throw new IllegalArgumentException("eglChooseConfig failed " + - GLUtils.getEGLErrorString(mEgl.eglGetError())); - } else if (configsCount[0] > 0) { - return configs[0]; - } - return null; - } - - private static int[] getConfig() { - return new int[] { - EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL10.EGL_RED_SIZE, 8, - EGL10.EGL_GREEN_SIZE, 8, - EGL10.EGL_BLUE_SIZE, 8, - EGL10.EGL_ALPHA_SIZE, 8, - EGL10.EGL_DEPTH_SIZE, 0, - EGL10.EGL_STENCIL_SIZE, 0, - EGL10.EGL_NONE - }; - } - - EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { - int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; - return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList); - } - - /** - * Initialize EGL for a given configuration spec. - */ - public void start() { - /* - * Get an EGL instance - */ - mEgl = (EGL10) EGLContext.getEGL(); - - /* - * Get to the default display. - */ - mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); - - if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { - throw new RuntimeException("eglGetDisplay failed"); - } - - /* - * We can now initialize EGL for that display - */ - int[] version = new int[2]; - if (!mEgl.eglInitialize(mEglDisplay, version)) { - throw new RuntimeException("eglInitialize failed"); - } - mEglConfig = chooseEglConfig(); - - /* - * Create an EGL context. We want to do this as rarely as we can, because an - * EGL context is a somewhat heavy object. - */ - mEglContext = createContext(mEgl, mEglDisplay, mEglConfig); - - if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { - mEglContext = null; - throwEglException("createContext"); - } - - mEglSurface = null; - } - - /** - * Create an egl surface for the current SurfaceTexture surface. If a surface - * already exists, destroy it before creating the new surface. - * - * @return true if the surface was created successfully. - */ - public boolean createSurface(SurfaceTexture surface) { - /* - * Check preconditions. - */ - if (mEgl == null) { - throw new RuntimeException("egl not initialized"); - } - if (mEglDisplay == null) { - throw new RuntimeException("eglDisplay not initialized"); - } - if (mEglConfig == null) { - throw new RuntimeException("mEglConfig not initialized"); - } - - /* - * The window size has changed, so we need to create a new - * surface. - */ - destroySurfaceImp(); - - /* - * Create an EGL surface we can render into. - */ - if (surface != null) { - mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null); - } else { - mEglSurface = null; - } - - if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { - int error = mEgl.eglGetError(); - if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { - Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); - } - return false; - } - - /* - * Before we can issue GL commands, we need to make sure - * the context is current and bound to a surface. - */ - if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { - /* - * Could not make the context current, probably because the underlying - * SurfaceView surface has been destroyed. - */ - logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); - return false; - } - - return true; - } - - /** - * Create a GL object for the current EGL context. - */ - public GL10 createGL() { - return (GL10) mEglContext.getGL(); - } - - /** - * Display the current render surface. - * @return the EGL error code from eglSwapBuffers. - */ - public int swap() { - if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { - return mEgl.eglGetError(); - } - return EGL10.EGL_SUCCESS; - } - - public void destroySurface() { - destroySurfaceImp(); - } - - private void destroySurfaceImp() { - if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { - mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, - EGL10.EGL_NO_SURFACE, - EGL10.EGL_NO_CONTEXT); - mEgl.eglDestroySurface(mEglDisplay, mEglSurface); - mEglSurface = null; - } - } - - public void finish() { - if (mEglContext != null) { - mEgl.eglDestroyContext(mEglDisplay, mEglContext); - mEglContext = null; - } - if (mEglDisplay != null) { - mEgl.eglTerminate(mEglDisplay); - mEglDisplay = null; - } - } - - private void throwEglException(String function) { - throwEglException(function, mEgl.eglGetError()); - } - - public static void throwEglException(String function, int error) { - String message = formatEglError(function, error); - throw new RuntimeException(message); - } - - public static void logEglErrorAsWarning(String tag, String function, int error) { - Log.w(tag, formatEglError(function, error)); - } - - public static String formatEglError(String function, int error) { - return function + " failed: " + error; - } - - } - - private static class RenderThread extends Thread { - private static final int INVALID = -1; - private static final int RENDER = 1; - private static final int CHANGE_SURFACE = 2; - private static final int RESIZE_SURFACE = 3; - private static final int FINISH = 4; - - private EglHelper mEglHelper = new EglHelper(); - - private Object mLock = new Object(); - private int mExecMsgId = INVALID; - private SurfaceTexture mSurface; - private Renderer mRenderer; - private int mWidth, mHeight; - - private boolean mFinished = false; - private GL10 mGL; - - public RenderThread(Renderer renderer) { - super("RenderThread"); - mRenderer = renderer; - start(); - } - - private void checkRenderer() { - if (mRenderer == null) { - throw new IllegalArgumentException("Renderer is null!"); - } - } - - private void checkSurface() { - if (mSurface == null) { - throw new IllegalArgumentException("surface is null!"); - } - } - - public void setSurface(SurfaceTexture surface) { - // If the surface is null we're being torn down, don't need a - // renderer then - if (surface != null) { - checkRenderer(); - } - mSurface = surface; - exec(CHANGE_SURFACE); - } - - public void setSize(int width, int height) { - checkRenderer(); - checkSurface(); - mWidth = width; - mHeight = height; - exec(RESIZE_SURFACE); - } - - public void render() { - checkRenderer(); - if (mSurface != null) { - exec(RENDER); - mSurface.updateTexImage(); - } - } - - public void finish() { - mSurface = null; - exec(FINISH); - try { - join(); - } catch (InterruptedException e) { - // Ignore - } - } - - private void exec(int msgid) { - synchronized (mLock) { - if (mExecMsgId != INVALID) { - throw new IllegalArgumentException( - "Message already set - multithreaded access?"); - } - mExecMsgId = msgid; - mLock.notify(); - try { - mLock.wait(); - } catch (InterruptedException e) { - // Ignore - } - } - } - - private void handleMessageLocked(int what) { - switch (what) { - case CHANGE_SURFACE: - if (mEglHelper.createSurface(mSurface)) { - mGL = mEglHelper.createGL(); - mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig); - } - break; - case RESIZE_SURFACE: - mRenderer.onSurfaceChanged(mGL, mWidth, mHeight); - break; - case RENDER: - mRenderer.onDrawFrame(mGL); - mEglHelper.swap(); - break; - case FINISH: - mEglHelper.destroySurface(); - mEglHelper.finish(); - mFinished = true; - break; - } - } - - @Override - public void run() { - synchronized (mLock) { - mEglHelper.start(); - while (!mFinished) { - while (mExecMsgId == INVALID) { - try { - mLock.wait(); - } catch (InterruptedException e) { - // Ignore - } - } - handleMessageLocked(mExecMsgId); - mExecMsgId = INVALID; - mLock.notify(); - } - mExecMsgId = FINISH; - } - } - } -} diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java index c4e493b347..e57ce70b94 100644 --- a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java +++ b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java @@ -20,11 +20,11 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.RectF; -import android.support.v4.util.LongSparseArray; +import android.support.v4.util.Pools.Pool; +import android.support.v4.util.Pools.SynchronizedPool; import android.util.DisplayMetrics; import android.util.Log; -import android.util.Pools.Pool; -import android.util.Pools.SynchronizedPool; +import android.util.LongSparseArray; import android.view.View; import android.view.WindowManager; @@ -32,6 +32,7 @@ import com.android.gallery3d.common.Utils; import com.android.gallery3d.glrenderer.BasicTexture; import com.android.gallery3d.glrenderer.GLCanvas; import com.android.gallery3d.glrenderer.UploadedTexture; +import com.android.launcher3.util.Thunk; /** * Handles laying out, decoding, and drawing of tiles in GL @@ -67,12 +68,12 @@ public class TiledImageRenderer { private static final int STATE_RECYCLING = 0x20; private static final int STATE_RECYCLED = 0x40; - private static Pool sTilePool = new SynchronizedPool(64); + @Thunk static Pool sTilePool = new SynchronizedPool(64); // TILE_SIZE must be 2^N - private int mTileSize; + @Thunk int mTileSize; - private TileSource mModel; + @Thunk TileSource mModel; private BasicTexture mPreview; protected int mLevelCount; // cache the value of mScaledBitmaps.length @@ -82,7 +83,7 @@ public class TiledImageRenderer { // half size of the previous one). If the value is in [0, mLevelCount), we // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value // is mLevelCount - private int mLevel = 0; + @Thunk int mLevel = 0; private int mOffsetX; private int mOffsetY; @@ -96,10 +97,10 @@ public class TiledImageRenderer { private final LongSparseArray mActiveTiles = new LongSparseArray(); // The following three queue are guarded by mQueueLock - private final Object mQueueLock = new Object(); + @Thunk final Object mQueueLock = new Object(); private final TileQueue mRecycledQueue = new TileQueue(); private final TileQueue mUploadQueue = new TileQueue(); - private final TileQueue mDecodeQueue = new TileQueue(); + @Thunk final TileQueue mDecodeQueue = new TileQueue(); // The width and height of the full-sized bitmap protected int mImageWidth = SIZE_UNKNOWN; @@ -489,7 +490,7 @@ public class TiledImageRenderer { } } - private void decodeTile(Tile tile) { + @Thunk void decodeTile(Tile tile) { synchronized (mQueueLock) { if (tile.mTileState != STATE_IN_QUEUE) { return; @@ -556,7 +557,7 @@ public class TiledImageRenderer { mActiveTiles.put(key, tile); } - private Tile getTile(int x, int y, int level) { + @Thunk Tile getTile(int x, int y, int level) { return mActiveTiles.get(makeTileKey(x, y, level)); } @@ -748,7 +749,7 @@ public class TiledImageRenderer { } } - private static class TileQueue { + @Thunk static class TileQueue { private Tile mHead; public Tile pop() { @@ -786,7 +787,7 @@ public class TiledImageRenderer { } } - private class TileDecoder extends Thread { + @Thunk class TileDecoder extends Thread { public void finishAndWait() { interrupt(); diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageView.java b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java index 94063b0277..7e3e1a936d 100644 --- a/WallpaperPicker/src/com/android/photos/views/TiledImageView.java +++ b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java @@ -16,8 +16,6 @@ package com.android.photos.views; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -28,35 +26,26 @@ import android.graphics.Paint.Align; import android.graphics.RectF; import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView.Renderer; -import android.os.Build; import android.util.AttributeSet; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; -import android.view.View; import android.widget.FrameLayout; import com.android.gallery3d.glrenderer.BasicTexture; import com.android.gallery3d.glrenderer.GLES20Canvas; +import com.android.launcher3.util.Thunk; import com.android.photos.views.TiledImageRenderer.TileSource; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** - * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView} - * or {@link BlockingGLTextureView}. + * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}. */ public class TiledImageView extends FrameLayout { - private static final boolean USE_TEXTURE_VIEW = false; - private static final boolean IS_SUPPORTED = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; - private static final boolean USE_CHOREOGRAPHER = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; - - private BlockingGLTextureView mTextureView; - private GLSurfaceView mGLSurfaceView; - private boolean mInvalPending = false; + @Thunk GLSurfaceView mGLSurfaceView; + @Thunk boolean mInvalPending = false; private FrameCallback mFrameCallback; protected static class ImageRendererWrapper { @@ -79,35 +68,19 @@ public class TiledImageView extends FrameLayout { protected Object mLock = new Object(); protected ImageRendererWrapper mRenderer; - public static boolean isTilingSupported() { - return IS_SUPPORTED; - } - public TiledImageView(Context context) { this(context, null); } public TiledImageView(Context context, AttributeSet attrs) { super(context, attrs); - if (!IS_SUPPORTED) { - return; - } - mRenderer = new ImageRendererWrapper(); mRenderer.image = new TiledImageRenderer(this); - View view; - if (USE_TEXTURE_VIEW) { - mTextureView = new BlockingGLTextureView(context); - mTextureView.setRenderer(new TileRenderer()); - view = mTextureView; - } else { - mGLSurfaceView = new GLSurfaceView(context); - mGLSurfaceView.setEGLContextClientVersion(2); - mGLSurfaceView.setRenderer(new TileRenderer()); - mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); - view = mGLSurfaceView; - } - addView(view, new LayoutParams( + mGLSurfaceView = new GLSurfaceView(context); + mGLSurfaceView.setEGLContextClientVersion(2); + mGLSurfaceView.setRenderer(new TileRenderer()); + mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + addView(mGLSurfaceView, new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); //setTileSource(new ColoredTiles()); } @@ -117,22 +90,11 @@ public class TiledImageView extends FrameLayout { super.setVisibility(visibility); // need to update inner view's visibility because it seems like we're causing it to draw // from {@link #dispatchDraw} or {@link #invalidate} even if we are invisible. - if (USE_TEXTURE_VIEW) { - mTextureView.setVisibility(visibility); - } else { - mGLSurfaceView.setVisibility(visibility); - } + mGLSurfaceView.setVisibility(visibility); } public void destroy() { - if (!IS_SUPPORTED) { - return; - } - if (USE_TEXTURE_VIEW) { - mTextureView.destroy(); - } else { - mGLSurfaceView.queueEvent(mFreeTextures); - } + mGLSurfaceView.queueEvent(mFreeTextures); } private Runnable mFreeTextures = new Runnable() { @@ -144,27 +106,14 @@ public class TiledImageView extends FrameLayout { }; public void onPause() { - if (!IS_SUPPORTED) { - return; - } - if (!USE_TEXTURE_VIEW) { - mGLSurfaceView.onPause(); - } + mGLSurfaceView.onPause(); } public void onResume() { - if (!IS_SUPPORTED) { - return; - } - if (!USE_TEXTURE_VIEW) { - mGLSurfaceView.onResume(); - } + mGLSurfaceView.onResume(); } public void setTileSource(TileSource source, Runnable isReadyCallback) { - if (!IS_SUPPORTED) { - return; - } synchronized (mLock) { mRenderer.source = source; mRenderer.isReadyCallback = isReadyCallback; @@ -177,13 +126,14 @@ public class TiledImageView extends FrameLayout { invalidate(); } + public TileSource getTileSource() { + return mRenderer.source; + } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (!IS_SUPPORTED) { - return; - } synchronized (mLock) { updateScaleIfNecessaryLocked(mRenderer); } @@ -199,44 +149,11 @@ public class TiledImageView extends FrameLayout { (float) getHeight() / (float) renderer.source.getImageHeight()); } - @Override - protected void dispatchDraw(Canvas canvas) { - if (!IS_SUPPORTED) { - return; - } - if (USE_TEXTURE_VIEW) { - mTextureView.render(); - } - super.dispatchDraw(canvas); - } - - @SuppressLint("NewApi") - @Override - public void setTranslationX(float translationX) { - if (!IS_SUPPORTED) { - return; - } - super.setTranslationX(translationX); - } - @Override public void invalidate() { - if (!IS_SUPPORTED) { - return; - } - if (USE_TEXTURE_VIEW) { - super.invalidate(); - mTextureView.invalidate(); - } else { - if (USE_CHOREOGRAPHER) { - invalOnVsync(); - } else { - mGLSurfaceView.requestRender(); - } - } + invalOnVsync(); } - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void invalOnVsync() { if (!mInvalPending) { mInvalPending = true; @@ -255,9 +172,6 @@ public class TiledImageView extends FrameLayout { private RectF mTempRectF = new RectF(); public void positionFromMatrix(Matrix matrix) { - if (!IS_SUPPORTED) { - return; - } if (mRenderer.source != null) { final int rotation = mRenderer.source.getRotation(); final boolean swap = !(rotation % 180 == 0); @@ -290,7 +204,7 @@ public class TiledImageView extends FrameLayout { } } - private class TileRenderer implements Renderer { + @Thunk class TileRenderer implements Renderer { private GLES20Canvas mCanvas; diff --git a/proguard.flags b/proguard.flags index 83a491dfd8..6a9d6f3457 100644 --- a/proguard.flags +++ b/proguard.flags @@ -1,18 +1,22 @@ --keep class com.android.launcher3.Launcher { - public void previousScreen(android.view.View); - public void nextScreen(android.view.View); - public void launchHotSeat(android.view.View); - public void onClickSearchButton(android.view.View); - public void onClickVoiceButton(android.view.View); - public void onClickConfigureButton(android.view.View); - public void onClickAllAppsButton(android.view.View); - public void onClickAppMarketButton(android.view.View); - public void dismissFirstRunCling(android.view.View); - public void dismissMigrationClingCopyApps(android.view.View); - public void dismissMigrationClingUseDefault(android.view.View); - public void dismissMigrationWorkspaceCling(android.view.View); - public void dismissWorkspaceCling(android.view.View); - public void dismissAllAppsCling(android.view.View); +-keep class com.android.launcher3.BaseRecyclerViewFastScrollBar { + public void setWidth(int); + public int getWidth(); + public void setTrackAlpha(int); + public int getTrackAlpha(); +} + +-keep class com.android.launcher3.BaseRecyclerViewFastScrollPopup { + public void setAlpha(float); + public float getAlpha(); +} + +-keep class com.android.launcher3.BubbleTextView { + public void setFastScrollFocus(float); + public float getFastScrollFocus(); +} + +-keep class com.android.launcher3.ButtonDropTarget { + public int getTextColor(); } -keep class com.android.launcher3.CellLayout { @@ -20,17 +24,6 @@ public void setBackgroundAlpha(float); } --keep class com.android.launcher3.DragLayer$LayoutParams { - public void setWidth(int); - public int getWidth(); - public void setHeight(int); - public int getHeight(); - public void setX(int); - public int getX(); - public void setY(int); - public int getY(); -} - -keep class com.android.launcher3.CellLayout$LayoutParams { public void setWidth(int); public int getWidth(); @@ -42,11 +35,20 @@ public int getY(); } --keep class com.android.launcher3.Workspace { - public float getBackgroundAlpha(); - public void setBackgroundAlpha(float); - public float getChildrenOutlineAlpha(); - public void setChildrenOutlineAlpha(float); +-keep class com.android.launcher3.DragLayer$LayoutParams { + public void setWidth(int); + public int getWidth(); + public void setHeight(int); + public int getHeight(); + public void setX(int); + public int getX(); + public void setY(int); + public int getY(); +} + +-keep class com.android.launcher3.FastBitmapDrawable { + public int getBrightness(); + public void setBrightness(int); } -keep class com.android.launcher3.MemoryDumpActivity { @@ -58,7 +60,7 @@ public void setAnimationProgress(float); } --keep class com.android.launcher3.FastBitmapDrawable { - public int getBrightness(); - public void setBrightness(int); -} +-keep class com.android.launcher3.Workspace { + public float getBackgroundAlpha(); + public void setBackgroundAlpha(float); +} \ No newline at end of file diff --git a/protos/backup.proto b/protos/backup.proto index 8ae1752342..d8d94e8cb5 100644 --- a/protos/backup.proto +++ b/protos/backup.proto @@ -14,7 +14,9 @@ * limitations under the License. */ - package launcher_backup; +syntax = "proto2"; + +package launcher_backup; option java_package = "com.android.launcher3.backup"; option java_outer_classname = "BackupProtos"; @@ -70,6 +72,17 @@ message Journal { } message Favorite { + // Type of the app, this target represents + enum TargetType { + TARGET_NONE = 0; + TARGET_PHONE = 1; + TARGET_MESSENGER = 2; + TARGET_EMAIL = 3; + TARGET_BROWSER = 4; + TARGET_GALLERY = 5; + TARGET_CAMERA = 6; + } + required int64 id = 1; required int32 itemType = 2; optional string title = 3; @@ -88,17 +101,18 @@ message Favorite { optional string iconPackage = 16; optional string iconResource = 17; optional bytes icon = 18; - } + optional TargetType targetType = 19 [default = TARGET_NONE]; +} message Screen { required int64 id = 1; optional int32 rank = 2; - } +} message Resource { required int32 dpi = 1; required bytes data = 2; - } +} message Widget { required string provider = 1; @@ -106,4 +120,8 @@ message Widget { optional bool configure = 3; optional Resource icon = 4; optional Resource preview = 5; - } + + // Assume that a widget is resizable upto 2x2 if no data is available + optional int32 minSpanX = 6 [default = 2]; + optional int32 minSpanY = 7 [default = 2]; +} diff --git a/res/drawable-hdpi/apps_customize_bg.png b/res/drawable-hdpi/apps_customize_bg.png deleted file mode 100644 index 284746796c8cee00049c6169c44330c9f3f87a3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Asp9}A3S&f1p>EaktaqI0|e{bV(k@oYx zNj^&3zMjAEB70%SZKuvwy*KU!H+!2{Ti0#6y!U~)z!C)!me`-`c70anc*nFfn)PM( zRkJKtpB}@RFBg?xdTgL(yik9}lgbAM$6m1H$<;l-BkiaxAgcf2#s(Fjr3@FeOKkU4 z%}cks(|&MAy_u!HoOJS@ezloG-tncLCq%2ljcTu6tztu)Tbl2Q z_)K4YD)@TeqM0+AGB-|YztsLzHaQ?=G+d4w8GNXucWD!y@D^BMQooYi^W`K;I-Y#{{{8;i_3Qr^l$V|p1yv3hsRZ}?Q@n>V)`t-=FXr0-^9qMC|~&P#dDc;v-VEV z5loab&iXzhE;90`*g>}0uV26ZbL-YEbF-SDS1F1nTZ9BzE*}?rIeGGArqevfj~{2f z{j|wFT&*vqD*Yw@|CL2M=cIQ&{PykIM!y$_f86jC^Vxjl<;%=%H*=OnB(1obwR86D z*@=~Zb4g7eHclG{C+nzsv{{JbV!-ozzo%FgE;3zUHZQ0p?^ffKcN|VGU_H^`2IsNo}T6(&< zoSd9p>Pq&>7cXAifBxLLZL-IA3Qt_samD4yN%3kN1jr~_EY)F}&)7nZWY0O)oNQW@`UecMm3F6v zA8rF(voW{V^r}+)nbgKG$w;I9lft%4nR~H>_vU-`sZ(Sx3*D265->N=vc3In#p=Y> zSM{7#CWq_}H#zpgx$eJ&^Lq>FZ%1nmPV(}4@+$XclR7uA>g;K!bJZiBOyN|Ca^jpC z@^Wg(`N;`?CuXb@@?AblI&?#X5{t{{)Jw+EN)r-1uKk|0_s0=;&!y*&+~N|8X3?I- z;$ozzD|GOe|CR^s59ZkimF^0>Zlv+>(7Wc8zgsygK3a4|fJ&4P+g_wOq`zp6`1gl7 WQvBYQri9y1LE@gSelF{r5}E)UHCl53 diff --git a/res/drawable-hdpi/home_press.9.png b/res/drawable-hdpi/home_press.9.png deleted file mode 100644 index ef07011f824de2c14944861c65b524a505b2748d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^EI=I2!3HFiUq4X?QVE_ejv*GOlYjjGZ_muke7IgC zEg>-_<AF zf=_wPCUpbu9Sb@%CJOpr+#C16W82w|jt-4auR^*=ajkr)F+pz5Wpn+gIgETV z76lIuG%_#G_IuP(7}v1x=@*7c(K8j*{jSL(y$KKnF2+y85}Sb4q9e0K8L08vp7aT zfU(=jY&!-97EMnV$B>MBZzB%o-7yoWJ-kq3?H;#Ho06BiR`Xn7;;eeFd9MD;?4k_6 zz27e^nlGf_(6~sm>cdU8S63q5>v>L`Bd4Mrw>~WIx?$DD#~%w%NEVrVc1bwWA*k%u zBVm{XWPE3QYv67t`CDFA$<E?jHWskb9>tsOo_HBb_Pw^ZfY5Q%x2zbiMgL zUHtj}`A;9cvMHXtzK^f(d8)~w>u=)I-=8o3xb@sCx!Y}}FW;?x?YMG&_>VV#`#tYW zjovwRb6)CmrT6z1RGB*0899IbRw`s(zPjwpqsq z0KtUM9YD9pu5n`KE7Z!0dAW1Jwv0*PDo=uqk1rBgVw8}m$NF_uv}#Ilai^=M$n_k~ zPZQ^Ilx52<*}+i_4D|UrYSL4yUIN4V=`^psU$&Z+_U9h9YTwfA{NG=H_vYK-R*!eR ze(tp?<88Aif{f{^8>wBv8msn-P= zWZymc55xXd0eh>Jlb+{2%~7_W^ye2d-~P2-k0M+3|NYtNpD^c)&%Zr~p4b0Tc@!&q zE7A2{U4QfRt9_rCw_G{#*Dir8>$#`OmESYgO*^D_IBx%eh+9X@4qutx@T`yZ!M6at z@~QF)>jQr9Oz=zUV6bz37pY;ywf^?rs;BN}&7(4p|C;-h-IZ0g`l)-6lc%G%aP9s- z8B4skOL2I7pDe%j#*H%vtn22rZQs-WC*$a`XaCA;ln?){DgUWBM_@;I-3pgQLQF?Z z+2zCP9DJHgOM^>qzu+)1oztlzljs}4{VVY)`w||GG`PP&=R2hoT`jKP5A*5?zCVm5~8N!?FL~cYqosK{f>E rrbP0l+XkKqB%b& diff --git a/res/drawable-hdpi/ic_info_launcher.png b/res/drawable-hdpi/ic_info_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ec0cdd18ce167380983b39661aa29b74f3f81cc6 GIT binary patch literal 2727 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 diff --git a/res/drawable-hdpi/ic_launcher_clear_normal_holo.png b/res/drawable-hdpi/ic_launcher_clear_normal_holo.png deleted file mode 100644 index 84549ff946a7d4b467343faf19281f09d030a988..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1003 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 diff --git a/res/drawable-hdpi/ic_launcher_info_normal_holo.png b/res/drawable-hdpi/ic_launcher_info_normal_holo.png deleted file mode 100644 index c9bcd7f374db9241a3421bf174da99e665e37485..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2692 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 Dl7gVw diff --git a/res/drawable-hdpi/ic_launcher_trashcan_normal_holo.png b/res/drawable-hdpi/ic_launcher_trashcan_normal_holo.png deleted file mode 100644 index 3fc2e8347b5f71454f45311377d95b0e90fd4049..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1750 zcmV;{1}XW8P))V|NWdZlQXw4(cBu#5Ze@u2q}d|3l&j($V;J6q)$EwBDPQvQ6l!C z^u4W61o1&o5E}47soIL9r8HRYY07lcF(H{u&)m-CzwF!c;hZGR%$_rUD86*<^K|y! z>)Y#F*B|&l3yt4?3BSiHbT)WBc%MS>txF20+TW2BVJ- z*3pL>uOS$}eJ_33y0P%1$Im?Crdm_Yb_20xz@msZX1_g8vzUA$ z{f;~U;Oib5w#oB&h7M-eTei)gK6&)fh1sJ=7QS&jqHBY0(GHmbkfLTh6>q6mFRd@H zEWf?-1s997G$Z+ns4 z4zMQN^j}hw9^5w>B@~hz>IZ`v7?L$4Oh6!{kU}Er=vOX3{fEuJMN3PAJ;D?c^YnQ> z9v%70;nN2`(>^k}Kg+^^0!T0f2o?kf1H;O$mCN0w&38UL_rZ;2af$!RmycENj|WIm zguzU5Y;c5Ukf9Z}8rfWSxG|HoV~1FTqNf8Im`-^uzcT0-MGrmX*hsI?Vtzd>L+*}u zasg=5>lbb6uG#`pC!6tR^j?S%5@Pfq2MYrYg$fmEmQ!h77F*5oQ+#O$Av@$Oy*`DL zzx)2)Nh0fvR;QOvXF;OHmGxKhKCCo4Vu;20+k zx>Tb#5)Ta!I~IYMhM7@XZD3_RfS1vm`1tH~^)#3O1~30US)O>H;tbrG^fjuh;BqL9Q%#kC_L!S!*f-9ox8` zlL5!D1GUFYcyuLb_G#liHI!|ik$zkotPXZF6jK41siuj=~%C^dA${j9bH z4BJ$6$_8!&zh1jmg+UTUK|m2Dlu|+oB@|IWL>4`B zas5}9p8DfkZ-jp@mZH+fskdHA#FVGWL37+3!hB=)ftjO|b6GpiLIyz4ZnNwZS9||h zTUo<(x|+OKEQu}W_1UfkIUo~;6BBd&xoQd%$dDks^%dfvQKCR!ZcKF4jlsGoC2QOj zz|j9Lh$ERssZ1!Mh;9<T%D`CHz}(8f;L|pBWd;TY)e_f;l9a@fRIA+l dl+3hB1|tJQBn`%vR>n{bH@udaPQJ@*2mk{k7x(}G delta 87 zcmeC<=;PQhiP_sk*U&)M$SB0n)XLb*%FtBTz}(8f;Ns&?lNlHoR7+eVN>UO_Qmu0H dQ!>*k8H@}Jku{iFnOH(J9634FX7XKTLjXz28J7S6 diff --git a/res/drawable-hdpi/ic_pageindicator_current_folder.png b/res/drawable-hdpi/ic_pageindicator_current_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..43fbb0e79dfd46484db196953a2c71eaa295bdf6 GIT binary patch literal 610 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|mUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG z?e4a)wgnKtcz>HW605}3csDg}6ie&Qo|yOj z_cGlE_6I@xH6QT-J4iWHRdn5`g5OItihZg8~-vpT}bC$&++*vFc?)! zTq88glbfGSe#2 zG#Fc28ACPP@LFaH)F276Aviy+q&%@GmBBG3KPgqgGdD3kH7GSPrLyp3Dk!cQJYD@< J);T3K0RV!{;@$uN literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_pageindicator_default.png b/res/drawable-hdpi/ic_pageindicator_default.png index 47b9989677e9b5c49da0e74971fdb65808f82860..83fa73fc6380784dbbffec1b8488fdef7e981eea 100644 GIT binary patch delta 69 zcmey*_MdG7GqaSbu7QcJfn|uHnU#rwm4UIYfw`4|!KZEN%9CZ8#gV0rEv<~9(l@-8 KnNId&HUt25lo8GV delta 69 zcmey*_MdG7GqaS5uA!l>p=F4nnU#^Lm8r3=frXWUf%+-M%*nFM;y9$krq8@O*^k)} E0AxH7xBvhE diff --git a/res/drawable-hdpi/ic_pageindicator_default_folder.png b/res/drawable-hdpi/ic_pageindicator_default_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..55cab1c657e03eee66e832ca80d8facacceec2bc GIT binary patch literal 529 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|mUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG z?e4}X8s7}?`e=VFh9g|gE{*_TGsuw%aW8& z#O||LD}KOCb;AND^AI76(j4OhT35w113qMD*}mxTd{gM>pXj)-`85A7vv;?e=0w>t zvOn%T8RdP`(kYX@0Ff`RQFwr%z3^6pbGBL0+FxE9N zw=yvJv`t+ZMMG|WN@iLmng(M_D`TjJ8(zyyff^)1HU#IVm6RtIr7}3Cx|jd} literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_remove_launcher.png b/res/drawable-hdpi/ic_remove_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c0814579a46a157e8da580743187f637b35cbdb1 GIT binary patch literal 1610 zcmZ8hdsq_o8vc=azce?Gm3EP8h>D7$geXgm1g{y|%3FYnmX2ZKWrR(-oEZnw877*S zN|Q7o{oeO`-sk)8%A#3I+i)a1g4RR|3TPL z_7?z{I)&py;)!yxus{2=B!@?`KvZ?jpZ;xw@qR9^8XO$5qOA&`&)v6dq!Y4ID%yEx zB~HiDBIh6A|AwrYbbG2Co89tO>)(W(|N3E_S9>}7lkc%8KV~3IeqixNTAj$P$NCI4 zL$UU{h`CU>U^;!_r&JqIQ&kn4nVIPiA`p&ge4pAp-RCT*ZD86&&Mv8K6 z=CKx|sJVIYDhzT7b+gZ{?=&oj^F7(I9pHkc0f@~5Ij$;yCk zq0Q~0g74DbZWbbnO*vQ$W{Wu`>fpheO4?d^*C!3o)xklcGqB~rS`*riMp;86`nQbS z8y*`QgMs}D@m#mW?~B8~2WqZ(Zoi|V88$m zh0r&qlM@poOkRTbPo9wCSo=}7Nj&Pkor+|OLy1R^zDY3xhiIj%ufN+d1z8;sYD!9m zj)}jN#kV~377wx!$gk)VohNrgt}(N+hX16wHjwf0LKzvtiUxJQB3jED&s)f=jA+7w zJ#x8xF`;q^2bQl-y24-!5Ta;o)(%|Z<1RHz-);Jo`_LQgvmLl=VB??0mRxYTy2h7J zxgPX^=leO)z*F;0I}9eltUq2IAXA3?Zd59#yBB6ggx;$F?i+S7tWg}feWtgSEQ_&* zqPZ8cq*n;|_WSqaRO91rri+$PG`aWGGg8_LPv?E}+VY23*{SVvx3Y^DFY=ARlrryh z>`jTGR_}P-8y6P`IZ;LSByWn(u01tV#x9EJgi(g-8tzPJ;vYAcT)UIM?WPgNTF_e`#t|0?g&iq zE7wA&(;YDhED?#L9j&vQnz|)PiDa@Y0za7Uiv{0xbp>3$aJRGbn{j>5{Qhe6^wM?e zg!8NUka1J{axF{ED;bWz^~cMc%V`_-5c4mTR5Vb z)M(b)q|zm5b=0v9qh1N-Y{|sL1e{a)6qK5Xw)iMnGZWp!QpT{$zHq5 ziEw_KO8fZgGDf@SJf`aBeeJaSC{XbiAGIlogAWl{OKV4Knz|IN;&%%IZEB+ literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_search_grey.png b/res/drawable-hdpi/ic_search_grey.png new file mode 100755 index 0000000000000000000000000000000000000000..f4c5e27d26bf76ca0a3675fe504a8c268a428d57 GIT binary patch literal 743 zcmV?P)8LQEIh_B)d(GsBl;3iCg^VSZnJ%sJ;fGchJim@r|&_z&%5m?h6C1xge+ zAml=uITteb zIx>biRql*3%6Y}aX|JV< zrTmfj6L%+Aa@?QO-stx3nQ*~mxwDuaO(DDQOt_@77PY0>m95G0K-v@6rzNyzE##eND2N}L#QAMHv8b?g1^=fDv5#-AACgcr64?^gy3&9gieCChU6~G_~3^H zzo8*CBMQ#@;1>lyr6H6S1uuPc>R*B%)DUX3&azRD^tg4FwHS@CDvF+y@IbckL$E7G zC-hryHJ?0?pDpsO(F$Lz!daKpdyCpMdZE|xew=lsBbgGfjb`}Z_=;G1YAw;?n5;6B zMmNORb^OEyW7WR4h%s(0r?1ZUnxe;+C5K=5|cz}vaTJdD;Yx%TP}1q|7>)}^@E!HV?-az z6dzG3vLd@OM|Dv$hE@icW}RcMDRILw8%#0iJVJR{Hzn8V-5ffU_JgR@*=VJMP{~DO zbt>&OUTMEVsUuW!D0Ti1O5Yo=v==BLhf?Epj4_6e2c=->{8b8$&Vy2rbi#xQ6DEx3 Z{037>p9cHt>oouX002ovPDHLkV1nM|U8ev5 literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_uninstall_launcher.png b/res/drawable-hdpi/ic_uninstall_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8f7264415795339c0bf0ae63baba18868ba416 GIT binary patch literal 1194 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 literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/on_boarding_welcome.png b/res/drawable-hdpi/on_boarding_welcome.png deleted file mode 100644 index 852a0cb132f2101d704d781bba988880c25700c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22423 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 i1KqvEOak1zy==VPJR?w_yv_uv=6?V@;67Xc diff --git a/res/drawable-hdpi/overscroll_glow_left.9.png b/res/drawable-hdpi/overscroll_glow_left.9.png deleted file mode 100644 index aaf43c73ab38f09244325767109396fb496435fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 858 zcmV-g1Eu_lP)#?0okANO#EJaD|#N*h1(#lB@5lSfpB#~gHCq-_6MPw=v z5r9oJ=@E2KVEU)Flv3`+7jhM35!nDQfY-otjzH*E7PKQ?G(OWKjWJ%^(=kWy9R0W0 zlJPxRkGvOT7XX}g{u|&k@D=#L5dfJ1+L>b@8%^Jq(a{shdt}l0u`xcIf2(Yc{a!!v zRpw7CJNKQRdfBPY26zX22Y%RrjkX7f)EI6HysmA#RR*UXjC?TigCncFgTwdwhlFQ! z9RmH@j?5;z7QNn4L`IH4ILA-}pa8FcPp5XYvuis-qQ|o`Z`Wi&j{u-$gR1qofvIz} zlf>ag?H144q}N@2T)3R9Tjgsl8%}lo&YtVQd$qSlUCK$vFJ;da0ATv(&pU!}78o71 zt+p4yp2c{9^U$Bkyj{`)Kz3U6==E2+j~DO=;5{Wf9i!89kD)pL@}CN2f1>P>JZDZccaB|mNB<)L zR+>R802WRw02WRw02WRw02WRw02WRw02WR=rq{mNHE=PtzAW-mnsbsEQ3}^4cf|Cw zq>nhfEW1||J%XR`$r4N6H(Pe;#FDpmn=pz}-ChEy@+)tTv*B$j*d{A<-WpSTd4H4v ztt?_Ivl!+6vs7}+X+4?6p7d}F1m1;BPc|_Fs0K>3o~+cnfGg4f5a2iPQ}OCaTGXPvfIGzsbB9_YN=E=mG*Bf8YEV?2ObT$=#U!G}Dkt(^~kU~dV028fK;bGl@w&fM?Ju|M;P=97%IE=x*7W>7T<3`Hh!BGKIwYCh(8a9lZwx kkaBtzd{0KL^!KPd0J!!_DNJge7ytkO07*qoM6N<$f`5{T6#xJL diff --git a/res/drawable-hdpi/overscroll_glow_right.9.png b/res/drawable-hdpi/overscroll_glow_right.9.png deleted file mode 100644 index d03486441a51305a6e1a185dd4c929a7174a1306..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 856 zcmV-e1E>6nP)vZk?YkF*56gX4?zs%T)b0p~Z$YlY))xSjU?fcleEL|IoTk|tI06&3mz=Mb! z$N-?KWrO-a(b$vik~y9{8@Kwly;NrB^zOSv|1u_#TYK)zxaJecY7!;@d;&fIAAx%j z(b3L4MtN<39WYewU}R&(9}LgR$BH*PyuoQBUNr8}$;#~6Y}@l#(DZ_YKn~|Xuzbs- z(Fb@3+yUP?8pWeGN0~YY^^6i3Zglw8`1XA^1#9&jj8Xm=dC~Xa?6Y_DD1B+@RL0`0 zvBBa2U`fU(Ek@qTz%@^+aj{7mzXe{GH4y*?rr(R-$)Rckcn0#K#51;+Sv){Ryd_cH z^S(@M~yYrV|V<6tj4 zWnk&mCz_i9qxUJcR_4Bdc@}`Vk7s}Fcib-hOAW&QEM(6mYo86>G5|Ju0@?tua@qi} za@qi}a@qi}a@qi}a@qi}avJe34|c!{&MQp=ejz9;l}1IMx@a^8>!M1ZHz%v&)deEq zyhPnoG|Kd{@3LpNb8N-5x0u#b3ACkjOEXzi3f=>+fR~9fQAMlnkA~mlG+V{9bF)jG z-b!A}fwrH4xmC)w!4kFJy+HJo%pKhY5bz3}idhi=zB&i>qN{d@=fr&_FPoOjiBeUw z2P^wrdK>5grw14^rYaLC4SLieFz|kLP01y!cR8{MnH>*&DN;}u4 zE6)&1A|y(MSsHCc&UB ztSp+_nist{YkPHQrPkg(y|o!HlSlJB+5{@BI=idY# izY2aG~7^00004*UgF*R}=-{Z&%grZZ*c4 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 literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/portal_ring_inner_holo.png b/res/drawable-hdpi/portal_ring_inner_holo.png deleted file mode 100644 index 857a01ec4702c4cfdce728b072ef3789bfb58a16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4429 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 K{XNVf@_zvD3$L;O literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/portal_ring_inner_nolip_holo.png b/res/drawable-hdpi/portal_ring_inner_nolip_holo.png deleted file mode 100644 index 53df36a43d141696d7f55d59793253c411902cd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4118 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 H?h*b2cfy(O diff --git a/res/drawable-hdpi/portal_ring_outer.png b/res/drawable-hdpi/portal_ring_outer.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b436b7ba3dac3611b09e288a515befca8181fe GIT binary patch literal 7887 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 literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/portal_ring_outer_holo.png b/res/drawable-hdpi/portal_ring_outer_holo.png deleted file mode 100644 index b711cf35021d024e8b9b61c3a05c6ec3bf1a62bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7658 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 diff --git a/res/drawable-hdpi/portal_ring_rest.png b/res/drawable-hdpi/portal_ring_rest.png index 2979b736bcf71c5b5bccda9e45e8c9b97281187b..e3b13393e8b3aa818ae59a8d0a5e3ff26f004104 100644 GIT binary patch delta 2724 zcmZWrc{tST7ysI_Wr<043Z)v`#8_tRSz{vmh{z0L7+f-lv0kAoHN|AfQr0nK58cX@ z?G6c{v1Thq##U*p_j~`ifBc@`=Xsy^oadbLdCqyB_nh}RV-Uv4EUVLJ0U$mZ0CoXT zE(1V>Ish=d06>on0H;Fo+w74K0;Gw6CFr&U%=-cj0D!m6>awvTVPY+R0557K*VRrhV1~OlD9l4`D{Z{R%$XhA5lWC# zjImWqj0k!DPV4QN7tVkRSC}auC;R@7)p4o3yu6*ZY33lOaR^&{RTVv4?*J|>_{Jb= z3lr|`ZR}&jAd-~4S{ejKlNML@XgO*zsC=Exl%54$`Sf`VFFQrOy z00T2rBJ~v>N1F+tBsZVPF`tLI-m;B!A#6(BOt!=Y1bqd}m0i3oYMYklC zdxZe5p3u)xvdBS>_*~l3A<0rvHXjkm?0bCCB@7@szQQ+Qx!i+`qRH$4^L+s#CR18> z%gI6qnk=nXxf=f)-hm4I7UZ1WWu2#CY7BBPrVv zLcrt6(*acEVBjWUKh;QnLtzZsgtHqs*2Y#wTM{W5TJCFWYg2V{vW-~3uWDB;A@L=)QOP z%$pqo(u~%K{(kib0qL>5y?rAoEotD@eK#YzIH@o zY5Iw06!O9st;}u2b1Z4*%a<>PA|?dN^s7gF8fvpsQ~Mt3;@?YzrSgcp9ruvScJ*f; zSmxJIsRtZQcdZ>yqq-OjhDzaJq(FS)PHL4qZSpxR3`p+i=+J+t7~51(Jmh?tcFt

#;fG9Bp15zy+b*wRS$u9!~MA`8NO|L;nC12oz6O_bSS^BUP*%O)b)MSNC5{qkC?X?XkpP~iVHtb>ARZ}Ozxa&3LM9?ISSY`SEtmke#N#ZUb_Bt zeQk{z**?up;XQluG-q(jo);dE*B_gXqrplA)|kvg_CSG{ChKJ;lNrw&RB>L6@gsVN zV|0n>*x2t`y8}QQKL{u&*h6;Q1-dKvBGi##FkG=`t&1>`v$#%@N=g@I2fO`G&)PY{ zM}M`!o$uc{buxc*73it}MZO`?07weP>)-c z2@`p%VL?}!)Gg$C)ne#gkM)S=268>I%Ug7KMYCKveH3~8iaq4{$gNIMMf1+Hz4iG{ zkkSi2M7s+nJY2aWt0Q?t0G`$upy5?AFMmXt>oQmr5v3>+&=i4cykxB?!uZ!D6V7c0HQ zD=JE2F60*rwDXC%E4TkZ$Nv0AcZG_X>J`-RT~tbzFF+}XWz5kV{Cr%Av#S_hyJ%B1 zpdxeh_m*;zUM6q9EU zEIH}8<})6J!ihL|h9yxk(N0-&0(5ptT8=HXKo!K$j08@DCpPAjlaoY;8#mS;De@-T zxn&fy?#K@LLr5eNrNe6@mBosZ2s9FKKj$?wjNfd1#$vHvjjzTogtZEejg5uL-cL37 zd9xK_*0DNXs{x0X2?z*ytgWvL`Am;iK9*!W>i0KY3DnRh#_bbyg?TVj!I7!6>#54= z*``iTCuuaAxSU+ipD{a!)8pgXp6q3F#hz~Q?4M-~?&!S1&l;qTtl+4%wW+kF`r}n4 z%C2biU!-$a)%EoB!Z+u;Pl&`f+r$xXd?3 zPHn)qfm}c==<4m|>F(}kUVjqv*RPh&Fx@>6M1QBjJ5g2H&s_0}yinNBOiZ%WoMup?JHY~C`sq|`^!0XJGUauO@qS0Q4m8@Q+ND# znA0+GZ*FYt9r*kE$M-BGD5r<$6-ZU*2(vpmJ)kXR^9X?SqbeZ@|l^0Tn8C^s#(GsIk z#qB6Y%{QA9drwWU9#f6Nw-5=#ujqbc*TB+R-0(VWF(PV(?S-J;Jl)UG4m54~0pzT4q8V|!w@F+AciL#Hq_Vw(W|t5|X`$Wu zQHj`Z_Cck3=MM1DO7h7GTv5yqXop3i;leW1|ff*5fX0vZ*Kw@sDOi0`>C7r zQE#DNzzt6++{`r`;~kFG^9jR(0)VM&XsM`c5maCrjvCr}>e_mm8YCl>|4+bsy`BdN691-f!25y3#D?ZYC$Ai&BD Kb-BjGEAbz|ry6Yl delta 2575 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 diff --git a/res/drawable-hdpi/quantum_panel.9.png b/res/drawable-hdpi/quantum_panel.9.png deleted file mode 100644 index b4ac9c0a064c602dedf25a76f2fa7758eb9a8cbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 744 zcmVP)gf&z&bKzRg;AklE;T}yEtaxNT*AlthX_9{_?xE;rP zR-*kR%TBSG`FGZ{_KcM=2Bj1YVs;qYm?OsQ;zl442y9U|J2jeK9TBI$ZVe-9<6!H$ zNepX2+h->vPFWEV(4m}|s5*S1t0M=B>UvZ3f00J61N4BS`F#H3`1ts#F-8H{a}y|~ zj6E-x%kQJn=o7F4GD@LNxQeq8>j6h;n!fAz`>%vP*CtKVkAuPBZS~Ve^WNx5BBy}P zY&JWM<9J*Y1zPJGuF8m_h&Ya4&StaI@p$|N!L1|}*BcIpXU3RLo;$COozPm-?RLAv z;qVOjO1V6pkR*c$=w(^fd+3K!V+>iA^(b!(XP+hF6wt}@JZeK+p6518J1K;eG(|Q%eXPcmLK;bjhw!^y1EpOX@u+R>q~?a>-t8ZD67+tfhRXNHy@HDc`o#?Hn+F8 zpHEIsUIPo@jo~dAkOhQq&#r1NaI2#&LN{8X=}Kv|?S>w&HwsR;{>Stow7$2}LTd0RB*l z(X6|xzOY@@Rb|&MODCi=Zd=EjZDZd(@y)iesOr4S*9^WyZDXj>CkO-rfj}Uz$EZX8 aI{hy?yX2Z#C>`|x00001PrM5GiEO*x99*sj;>5A*r_Eij^LQrIA^cgqB_ zH=E7oQ=aGVMC9Dg9mE)ewU*s(_hmdDzX$#R1;LVtErSfO*4kSUp)AYfvL0*gEmc5_Wq~gYK>j(hFh$&7RWCt3VT_I$hLLXFN0iFxsq_sgpqhkc{*GJzbQpW$@oI$fjbDO~oLaia|CNgKR1W*;MpSwl^VJ?>gHX z+GNA5>6$b=Vx3i>6pYZPN#Q7QpC1x%TT3wFM-#DxBNXmK*qq4{l-x zyddqK>@(SW;J2&YNCRUbhX6a^mwP>;CC+(9!UjFK+6~!Q@i3>&-7$4%ORAXDkE3P7 u&qK-74?ybA6#9!f@PzSNSJ)6zRsI6pd?2iI@5JZ;0000 literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/quantum_panel_dark.9.png b/res/drawable-hdpi/quantum_panel_dark.9.png deleted file mode 100644 index abaf23026df0007d804d7b840bc7777dce82b956..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 646 zcmV;10(t$3P)@3U zr-ZAppz%-~*%H8bk-u8uz;E2jgDG5x1ea4EYUE2g!4HFl8zSP zlx!?H@WW&j4N^hY0hXnBt1Mn_@bdNHtNrowPl&w9EDJDG=T4{BS08?S`}*CB3Q?A; zvMX5~A52bxx7NALo**KlqA1>v4<<*zJC<2q(DWD>U?9dETWe8$NJ-rcRmB*C7;{Yd z=*?Np>|)_m8302O_On~Ap!k?W5^gD3!OLbgYvW$B1{kVS*uf(ZPz7}grEFZ^f#zxe zAWro`e*o@_s)iMntUPT$3OMI>vbX}O8po>%7cA>pa`G-&7q6)r{Fn5yx#(qc(aYwd zm(4{ln~PpH7oC&sOp~m0ne7ZI*|^vAl+-+$GF$eYR4=nD)gtR9i9Q#!XV%T_{Bvft z8b0PmvO&}W_WbABiJjklp={;kL{;_o?cbY^=fBQQXy_uQ%9a2lU=Mf!O#J&CJ=(Yr zYk?{72e<&PfV=qORNo*>ocW0cct+lu9D1@-;Ks+UwJw{Q92U3*uKnu~iFVE-5+`Wp zW7lMxf(NmJvhP%oKeLW4nJ zaCd?*qxs3xYZ(|A%{^TlLn`LHy?vIm*+IbJp`@pzS(4&P^}k1|CY7m8=ZLfU@ws5? z_eOC?N%_~Vch;;raV8@|cTRp)hV1HHo7HzHFJ|iDGSE7(f`NQS#T?rMOIEdYyePl- zZ1T+e%FlMTuBy6Tw|?_{Dy{`IW;w-2_xDN6dKGwl^ d=2y-6!&p}r&7I^BB?t^x22WQ%mvv4FO#pTxlcWFu literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/widget_resize_frame_holo.9.png b/res/drawable-hdpi/widget_resize_frame_holo.9.png deleted file mode 100644 index 2d6fcf51f4b4760a246faec90e2111e22d28b1d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 880 zcmeAS@N?(olHy`uVBq!ia0vp^Z6M6S1|%gaKTQNuEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYZ(}rg*;sxLn`LHy_=YK%R$8Tp|zi|(9+j0n0Ea)O<|06Upyti=C7gL zp9SWtX1Up1x0|1GQqe=9SfYPo&gyGgbFOU25%UEajR_toPi5U($9Y<8r|_!F=k)$f z`FhBTKas=x|8D#Av%lEw{azg)8ciO&6oSb!3qVIAM$Lr*Zd!ecQ z61E-Ip1U{5J&xEcaddT*$nVTpuRUSzX%e+tc1$kOe;IhGIZDB>&$K)6jq2VEIn~E` zPhT08t2W=_*f{x1ME2ilU*g1;PWj}TJLSdh$%|h1OPRiwmV7S0JxiH&7W>)HYqwN} zeS5O`y;s@t`YE46Z|`OcTe;QF_jO_Ttg;{TY>#(*?pf#cYu(!;fhx6p_4-Ml=1R7e z$hn88+G=M$Ds^qXwZz)1-|3WnOs{=jtkl+b7BM1!KiX~g_${kmedNdFz_$W^V8Yk!~Zo3-7)>y6;z{FcW@AG^Np zU$on1<)0PX_v?gDmUQcRWNK4ax&E_mN8z3<#=?}=i|r@nj{f`7A2ol{y~{ywOpj08 zW_@xN!`Gch11~wg-s}6lde4g7$#z*wS|*lWoU+I)>aN$W`P1CDDo>cZ#M;w5>c^SC z%tvBh#_4vYd)hC1s_=5T(4!Ocl8^j!4ZUq)*rnSdxc#u>^WBrqo;6%j*{66``nlWl zz%_r30zMY}T~T@c*7eJL;l}1QoTnFjmT3Dh$Mm&)&3x~kUw_Mce9s!*d3`)!+lkf# zrV)&oiIjoi!+q_oVN*ACeD1Jp-0u0`AZzU})_?al&2^n?^c1At)78&qol`;+0D+Kw AzW@LL diff --git a/res/drawable-hdpi/widget_resize_handle_bottom.png b/res/drawable-hdpi/widget_resize_handle_bottom.png deleted file mode 100644 index f0afd6172bb988a05aa30b1917e1fc64b906dce5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1080 zcmeAS@N?(olHy`uVBq!ia0vp^ia>14!3HEHeI(3R2di=ni&{=0yQ)M#Y+ts7+xhXFj&oCU=Yur6o1qWsFp3s z+ueoXKL{?^yL>WGgtNdSvY3H^TNs2H8D`CqU|?X@^>lFzi8%cB`o`>(P!YBd_hDrSIRmHAC?S`;8kJ8|OA@Oe-^6E!LzE6|uJI=%O`T z(r0Jw=$!5IEaFbFvHs+oNxnvRYYJw5PrEn!^WFFJs?V28EM8!F##=D(zHFWStYw`O zCw6qO$@*+@Q~$4Al(WrE{_nDz_DA;lr+nZnG3;G=E#15`gh@K$_*oh07jNB{-BjKY z_Ca{YQP~~B=O0PlX%CjH5ZwPD`%hbaA#?W6>i1#ZSMM}A%>66!%;*D?-h_%Xm`PFKguju$d52V8_K zga!TGj{Fw9XwTBvr7c)!rzF?c@l$1!Z&za2+%t`lbB<+K+*RCOl*IV{;lY?=nLa7! za@8lsmrYTb6F%wvT4kXX6(aAS9C^l5c9l<6%4Zo@`D0#H!+vi4N2;nP{TBpZ&6xZm z<-+6&Ie!`D#xCxzQ^qTgHMqR?nGtrbDcsG0;oQSQ*S9C7_OEqOHEdt*=CbHu@ya79 z_wpyMFiqt8e*3q9m6qJ;lVz!KJkMRk)7?E7-g$~asF4t_!I9bEa zt6^(oRepI{?wfUe@u5)W{ToZ2!bE?0{f$33RdCXdH5I*UyKbp%TzTbhXpOZJllceR zf2TR~gPkVrSb622{-<9%KC1o;+HQ`Tm&)jO-4Vy1t!vX79rzG5#n2 z9aBH`OP2lD4~LUmpN1?7m%X-O{)?x~-almDKk8Q&;;89aU*H>9m94pLqDNS5v+B;a z`5(>dtW+9#V~#AhocX#)^VYKR_C(Lup|wY2Et=UyrkC37x98A%to*0JUetWwoXK}` zX5Wq8`8LWuZAwyEl;vmf4@UEjDVOOz-}Gq9{)6Q!pDk*9yzW~4ZSU%@vb~OngLD7a z^i8g;&#%kVh|Ul6e3SZTa`Mpx@4RicO zNnTgTe~DWM4fhqTuA diff --git a/res/drawable-hdpi/widget_resize_handle_left.png b/res/drawable-hdpi/widget_resize_handle_left.png deleted file mode 100644 index 47613b29f90cbce60dc65ae27fff29bcedccbcda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1097 zcmeAS@N?(olHy`uVBq!ia0vp^#z3sd!3HEt_D|jgq*&4&eH|GXHuiJ>Nn{1`8H#_*)4Mw+RN%nJ`)^;q zoYy`xP}tLzWaR6|CLsyt5)r7T=HwZT3<)j{GZH$q3+Sw z4kaitRYgvhigIR6yOH@~&8k&D?jJBw_!V)?;I+d3^^IvW9!VYRy{=it#}q1}wmZmP zzee=+4$krig?HM8e}2CIQQ7YB@*BtGB_6J4yONX`buHp7!}bT7Htp^IQs3#$IUK!X z%E5nA)|4t}mvUb2?r?OuBO_SueneQX(2h@ky7Hr^9Tu{J%axLJm3~!j7GnxZop82e zhq}_d&W>((hLx#eg842-ngt7un04Q>UwJRlQnH}^#*w)dvgNVIH;Bd^$*;I>bLr~E z{O~GS>1B56uPyq*3-j3@hgGnqe^Oh!;QBVdbc0^!{Y|_!Gq-9>K2W{WWvaR6J*QG^ z^s>IV*%kX9Z+y5#yK>TPu0sbrGaq-&omsW#mXm7gPQ!4+ z87Hi?c#N~ogt9D)?5_zINSSv0exMTbiOkB_{+_~x3TISf4yv5rnLT%%uJOdI{_{(x z>}{!0+IiFTv&>qFGogRFKC?O;`w^gZ>zHI(_^y_fcXL!id*6GyhE6ZyyYG=_c5Ri7 zb@Rz)Kh@JYkCpCSv08FnQM@2bi}ls2%^92lyKjm9>FNKtEsVpq=}*J;54L*Gd1r6i zf92)Vu+UH!m&;e#D%AJaJMMbJryZX*Vf`nzt9oYH=H^!ZY|&SrwOy&TY<2&jBWLj- zK)I&j`$yfnNo$M_rR-nkcJiIi-K164#_CJvyycObY-J+7cK*2!ruz=KUz+==TSRty z%J)^PvhEj0tjWK&;lien4YHGs?w#e?IFGUa`THF2ye+yrFNIy(b=KsZ!vBhYUgnH5 z)#ovv{Cdsk&ZTq8Nn{1`8H#_*V}Ex{sK|kj=g*uw zmsZ{@-8)GmDa1uFX|Yn6Qdatg&HlN%Q5^pm&CH^1Xm%uJrVDQsV7Yimi%ZP0lPmJB zMRwWQb8lq2tMxzMn9DQ!?r*k}ji>jR+rOLp{^!?omUo;kS-#;{Ui7E#z1+o@C6DU) zg#~3xHeQIhyT|{@*4q)uJ=SxbU$3$AX^U{+_kLj>S1q}sfoEg$>q=h3S?pKN+}*4# z+L0V_Fn5Qn|6|QN&BBrivi6U)|8&(C@ZSFUIdHaKh4YLy(_ay18PW^n^pkU^$$en9 zJHVQgvLUy4Q;@KW%NM02RV6?7BR4xLY?aI&J$vfpVk0id?{cKG6ey>L4Q@=fGstr+}Y~m(NI%!yv9kty0 z>AwEQ6=MCNESg1en|^IdnfuG2H~rM4FFKVgSp_V*-Bn-fuX>SHZLVdox?+*)48D02 zbvCc?vw7F7X&Ns7eb(Oqn^iVSnroLQ-SO$3Dd)GHgQ?szOtUpB+u&|Z!0ez)MXOit zoboYR_i&Esxh&o^ov9zEe=dzKy1C^3O13L4oqK)?%-(TD>5S{j>no!i-?!eJ`m0a3 z=CaTU)0*~r)uUUJBqg8ix>8*;Z+-S@@gKpja&I-BT&8C*`v>d%P&Ua2!7)?U*asP! zEmv)jJt_L{(D6z2*C!a>o19drP(Ihx`dnsZ-4y;?2l6YV_sf5rbHM$FP`JwJ@Y0R7 z29e&$!r{A{=6_JDvw0%vyrpC|%$hVYD|B~x_M4E4{qW=`r_9sR_?fOo+&lVZ#DQ)}X- z);x1Aw_dM%a%tI_#JI8xA9H@@HNHyiD2h!x`}ogtceUi36D@f!=STVWa{5Uw@?ZV_ z*k`64sZZ@i+n3mwi-pz!vy^IyYeY#(Vo9o1a#1RfVlXl=G}bk+)HN~*F*LO@FtRc= p*EKM=GBCIwe2WD|LvDUbW?Cg~4eR)lb^tXnc)I$ztaD0e0sw(I&9MLg diff --git a/res/drawable-hdpi/widget_resize_handle_top.png b/res/drawable-hdpi/widget_resize_handle_top.png deleted file mode 100644 index 2c60be00c8c6110d0c73b48ce34f2e15c4b84d83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1085 zcmeAS@N?(olHy`uVBq!ia0vp^ia>14!3HEHeI(3R2di=ni&{=0yQ)M#Y+ts7+xhXFj&oCU=Yur6o1qWsFp3s z+ueoXKL{?^yL>WGgtNdSvY3H^TNs2H8D`CqU|?W2@^ofotkP8$T*9Ql7BOkj7(qNQ_NTeoR-v`%fEb*>>pz$7c> z^F8-(>4xcdZu)NGzkB6Dr!iz@&{D6dTA`~#?gm~td3=)6Y~kA+ z&%<*$R=5TJw!i!7^nrq7Ze~AjH-|LpE?}DSHgebs6gG%ku*Yn89&vb!?gQozToqh84BBkl zGT-0ko~)vhs`6Cz?c@@tOQxR1S@EwY$$8D1W}=xH5ww2Ej@Sk{<{1-ARA0_plElui z@R>^NloAt$&M3ztlYX6H4xW_u|6;`oR!`$eeBQT`?5lAijoWv;)8O2D{HvsDbcyMgmNVSa z2W&nVDt0{I@o$mGgz7|#Q#<-rFxdDpB*;}Tuc_j=|DdHa($HU|;kwzvXf5XNjjAzm zQ#hRDm{}?}a_TU5AJF-*lbeA_?m0vVt=8wnA<-&806|Vi8 z`GBin&a>}RO&_p+II?GFhc!d3UWk+?HcEaw9PS*r+P*F%TJk+d()0k+ zbE3I1b-|{W<=NhUT#;9=&;!g;swJ)wB`Jv|saDBFsX&Us$iUE8*T7QO$Rxzj)XKov r%E(aHz}(8fAhM-b5=BF9eoAIqC2kEGH%*rUH86O(`njxgN@xNAwim$< diff --git a/res/drawable-hdpi/widget_resize_shadow.9.png b/res/drawable-hdpi/widget_resize_shadow.9.png new file mode 100644 index 0000000000000000000000000000000000000000..6e2932dcd0c4aaec6f97a8f9c09828316cc9f1c0 GIT binary patch literal 527 zcmeAS@N?(olHy`uVBq!ia0vp^p&-n`1|%)Mu80CsEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYZ(|AH+s4_hE&XXdnexeP=HMP$M+jqMN2RLuJ66e@|UqRc21`tOY*x+ z`!_!i1h_n_xHI#%WA0;%Wm~5mIu zQ_8j1K357|r1m`dW$>iuUlMJOPk5qwGP~;!--4hklWVJ%e4lxK|M@@W5Bx)VW_|G} zcAm;Lt72Z4Mq$E!vv(^Q7Bls58E73?(Gc4e(;72*mHL0@Un-~XJfF6UVTHuk8K2Kw zmC=1(c|~ZIi)`^ej-PX;Eq#46W-9-x%PgOzUf--)zxI=&sY<0!(R91*yN=kMyuBsI z($hAqbJmx@;+H2ZQ+_)ixVFB2>K}g5CUV0C9<0mXFaI_7)YmQgl}C&^4zioo0b`WG M)78&qol`;+0D$?^w*UYD literal 0 HcmV?d00001 diff --git a/res/drawable-ldrtl/container_fastscroll_popup_bg.xml b/res/drawable-ldrtl/container_fastscroll_popup_bg.xml new file mode 100644 index 0000000000..d79096807f --- /dev/null +++ b/res/drawable-ldrtl/container_fastscroll_popup_bg.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/res/drawable-mdpi/apps_customize_bg.png b/res/drawable-mdpi/apps_customize_bg.png deleted file mode 100644 index 284746796c8cee00049c6169c44330c9f3f87a3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Asp9}A3S&f#&|`R1OvvpBz4?Bdj10);}MQ2*l~pU(vfp)zX{ zI`ECxGmZPGFrmdLk`RMfClbUakt9+?8k|Xr`(<(eOcthv62w2!SjqU;u?`B zc7bnph#av+WcUF$`N3Cx3zANZ!KSk;e`jhV zLu2hFni#2^ht%gn7&*6UwHo9^$Ye5k1UZlcAO~_F2XY_>av%pl4&*=%>e2T^5E6yRv z`pqGb6FkId+ikboN8$*gq056Fc(>QldE0%zYy?;S5S%92u}_K7>38f7CjDo}Eiml}1zqs&W<+6Wu#CFiZB`4nvN@1v^!W0Yfk_rteQZg2*{*v*D+|E^Ie4TO~ Y2kk(}FbycKvj6}907*qoM6N<$f_;Z?f&c&j diff --git a/res/drawable-mdpi/home_press.9.png b/res/drawable-mdpi/home_press.9.png deleted file mode 100644 index 679a1f6269e6c85e493f3338ced255ecad5576d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^%s_0#!3HEtV;|oFQXZZzjv*T7k6zl%+u*>%e4+g1 zje7R71gpy|oGhvZno2*TBZWHlt%(*}Vi3@Gz2()J9oqg2>NeFz7{t%a|GnV>)1Fx` d1LwQ#V_thxfX_=Mcs9^X22WQ%mvv4FO#sA^EU^Fp diff --git a/res/drawable-mdpi/ic_arrow_back_grey.png b/res/drawable-mdpi/ic_arrow_back_grey.png new file mode 100755 index 0000000000000000000000000000000000000000..11996efe3f710c98d035a90ab0927641f843ccc7 GIT binary patch literal 151 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DI8PVHkch)?CvN0zaNs$Tr=76D z!=$5!NBBVS#b*=WxhBTvdv~%h%yn_A`WhZx_4Rz%`hOjqwpCwV#Q$xd`L&+ohQG;! ylaGbfJF}A10=;!qv^pou>GZXWN)l%%DPrcRnNmLGu{Y2n1_n=8KbLh*2~7YQWHkZ+ literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_home_all_apps_holo_dark.png b/res/drawable-mdpi/ic_home_all_apps_holo_dark.png deleted file mode 100644 index 84fa5947da5df94148418fd12d75c0d9c8dfada6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 889 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}trX+877l!}s{b%+Ad7K3vk;OpT z1B~5HX4?T(O!IVc42d}WcIw8w%K;*7`;+5$_s&*Zc)(|-M1;U9tAI8xK`}9h4;&>d z9~n9tw>PLeC{%Dws9<1QrZjEQszrW}cO3ctOOEZjqH3#)g4!qkX@6gzcz&<=+;ZmP zv!>T${x24(Xz3ExkK_7yiS>b&*3pT}H!&V)-e@s(wfaAQzYk1KPCSec3a3mlSp2;H z-^`ykZ#kbCZ$!kbv5+V7_vnISOt$tF-aU<*>U~U?3@?& z*N*UhJ9u2|6{E0EWBUnCg`mobhYxG`DNHyud)5P{8&Bs+U-%%`bRpH-Rz@Xrrsza{ zMUK-to`o+q?x>seNKM$MP*fC24{~J208VmRvf+j zr7~LV5@-4?WYk467oGHWXb5p;xuUPCb-Zt}yT9!>+r8}O6~bm#;bEWq)_Ps3-d{W6 z(9Nmd$*HToO5UA0+s(dKsfqRWo%)XtUteGT@8gz@L5Ul8&wG3AN?P^y^%E{s+&Ft# zaBF;@$G*B5H;Xdbul>r&FRoRa#Auu`JI!~k*=94rstbl^HEfi2Moz4`z$NC>z>p-) zzILVu8UO_QmvAUQh^kM zk%6J1E)W@6h8UV!nHX6aS?U^CSQ!|oolomS(U6;;l9^VCuED^{$N-|@*njUkKn;>0 u8-nxGO3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%F?hQAxvX#a~MoYm>K)%OiYt4>qJcrViu)jo#9ckjxE}#UY67oWkjhg z8D&&-CPwO=n$VQ8FBuYE%d7YE&im)R@AusA=iGZgpYuKUe82bR`g*&nsjO2005x|v z3Kc=^WkV?QDXhJ#;1F%f(S$$xzp`VFGasm~K=rfFvwHY9_!} zB$PT1z{LSfh5!(M16Uhf+TynrNhmUQx>Dfh^1gikS|Jiqj&|D{hoIX(<{~;w0$91) zo#O0o-xXA}|KP3zx}D{+E{89IB&wxxJEPWWTtSmcC7ifDabw=4rU*~*J50}g)aG~V zx}APHtuJ!L5z2|i&sEK7+8z>oy;ekElt;Vg)lF493)K3Jy?R6muZCL%w0$hu%cFC1 zN8ibke?==oXP5g1KFGES==}4>|won-WLyn8P^QN~~=;-LEKR-XejnCh=ihQ~%+=Wzr z?HI|YxJr+Vq6ReJwrzXIU6>4TWipMnamG)3QfRg3&+90|dVPJaxrGI3E!o$$#0yzc zbskx1ADuo$M{N6*-n8oK>VdupTa(Po9UUEGMn*;!aN{BL4-7cKrd23=!o}|9Rq0x} zk8dJkI?xUsI&=ecVfTdjvX8_q$*89+!q9=9(ik|6)N9`&5Qr(6JRYx3Wg4Gq6MR#$ z4y6$BZAsRIEfmSliiVfOVsYcilj`*3{t zEk)w6wJXwuSQvf#Hdqg8YHJ(Ts_+~F0>%<#Z~xK*9rbKBj5-5VV zVeRNR;7l2W!D#zs^2=5u1S7ZN1}dZSQ1DONWbhE2hTY^E_vl)4}k`MX6^q zH*Q|8G9NQgLGi>t0}_^@Xm2+Wl>p!y@@92P$gjA1d?`A>t}ju+%P+CxT;YCwPkv?k6g^2lkLnu{~isnOEyl7+{UYE~D! zcXsM-ML(0k?A(xRB8Q{>)9m*=edHI;zJH{o2S{p9KAuImc%f!L8jb#O-+yy;yx^~h zl$4(>T^B4ZWv}bE*P$q-ajVDPTs2jJ-dDBskq}(-_wnZyT!ffSt-h+Rp>Yb|nHp^$ zfyH!pcfZjj`x4903POv(W`m#>j;JO1+ag~i5($Tghx?Fs<3n0!fq!C_G$7CxOb+q1RWe=~oIxJHGC zl-W8r-Dj2J)0{UO=ZLDv@K_=!n2UxtEU+oDvZo0a*^D|o06$`f%N>NY>k~}8y3Tbmxx>s zq#<4uZuH4_GMMd{8+y_fBao76Uhb1mRW|%_d}&5nE-x>i@_BLb2X}VRIXEaNDCy7H z&~TFmzcc8JjEtNoL(M5aNrL!~#T^m0C1&2Y9mt#i**P zhRlzOAvHzLzxVbUN@bHSGAB(qJ7^qMD2GjC#`+!>jEKEF%)xz!H>HjolKwZNImB-O literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_launcher_clear_active_holo.png b/res/drawable-mdpi/ic_launcher_clear_active_holo.png deleted file mode 100644 index 2683beaa3b73f463f4e4d5c22b33fcaa69ec8d45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 949 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0V3Z2*32_B-V`OAXR8%a5gggWU z_!t;O7#LVJG%R>|{~tfzp`ehjqLQbg@*fD|;&N3~GNh$*3=AqIC5>cdvw`Bzo>l4V zcUxFgDJU>9G5z=VPghX59uo3@%9JfmP8^z=OloTL+};28_O92_Id5VTCojLk($bli z7ijMe1%)%Zy1`0H%PcH_Tp*BUVc9A!4iZ#U1ajrs*tUs_0~vFSje&xma&p@wBu?n) z07XQYn6`+EcW7(tN=fZjRSi&7Jg=v>L0nvdm36kZ_9kOvZ$(9a0Rf=Oqw4A+%phOq z^YgD37guFtV*~>(CMF3MmSA4q*UHMT6ct}9Dn62vdm=0QR!QllqT)LxCC0-Ien4$u zB|(0{KpGbS8OFrH$e4aglbKQO`c(~PMy`|>*EHBuUUD#BSABJvGvNud(L;@EFVdJ8 zH5tXuFpA81ks|BJ&B&;*qjwh2SjHr8cNgAoTN1wlIh+L^k;OpT1B~5HX4?T3cze1y zhFF|FJMnnlAqR<;`c*3zieIiiV3uI8x16Kp-tFJ}-FUWbRDRp)yyQ&ov)Qqk>5Tun zQ;J%@mp=+Uk-8{&)fKyXhf~aT^P`rE*tW4c3y9T9I63kN?^RpWd+gD!mIuPSDs)!$ zX2iAndo1H)S-IT8`=#fj30|8vE-*>i^u=(xc|d{*Q|gK5eI;gxk2Tpb%y62RU9cx2 zVcy0keU~;TYEAd`kDD~7{=^w`kM7ndJ0$~py|zX<7VQhPzWRE()FkGJ%NH`XoqHa( znK4uQll!f0e&35;mgVGr=@0xC_uon)=C`eN>ytOX^N&8pn|j}-^*;VEt*rV($2sQTcdMWO53%2_VXs>LP;n_Rd{s+aBT7;d zOH!?pi&B9UgOP!up)Lp+gczDx8JJrcS?U^CSQ!{Ni#)%Bq9HdwB{QuOU4xmGsR2ZT z)3u5zKn;>08-nxGO3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%F?hQAxvXk4UOiAAEE)4(M`_JqL@;D1TB8!2v z2N=7Z%(ernu=jLv4DmSrcJf~R>_moR_3x|CU1Xh@rJ^FX_}MiDvDy7Dg~~_#nohd8 zxJVpT@F+2m*7+mol=R;HNce3N!CeLtk~ZZT`R_BUWBxImubB7z`<(vomLHVov-A{m zePQ`!dqDH?(-ekzE+uaWurHZExujMh5ndXm;s- z(cnvmS=SnEcRc?gEu-t$=kg#!+5W_QnT_Y|9xZEGzVuJ5iQnbu3*IgbiTxfWM$^BB z|M(bVX62c3Lwo6KlkOW|I!bE1th*Mx`f*ilz=S9_`5)qqmh zW$U-E-#+8xW9OQM^`bxLgm{Qw3bOm2E0Z7)@ipW1jD07)F8OXtp0zD>{gW%FuEqai zJyZYT)`y_DHBxaEbN_2t#=l}He4F5&bhKxRW(rr3p|Q~6xbS6P7(tQnu6dhm+#qz+OGB2UMH|~)=i!2)25eCSw7O@c<(L~c_!m*&Z_06 zQio+CFUp4~Hd_m5FZ)wr|M#ls%0`KGO?AsujJDX!^W6D+|Br-J@uywYFF*E(%}l91 zC}U(Izdm(xt+0#PsxWhwoC=p;YMV|zHl3CF%Hb5phaG$U)@EYTzEOj6AG7df6D;j;hqn*U~|I}2|>`7B_X zQY~?fC`m~yNwrEYN(E93Mh1q4x*%i_VrXV%U~Xk(scT?iWnkbe^85~phTQy=%(O~$ z4Q5uR1`rKS*D9s}HAsSN2+mI{DNig)WpGT%PfAtr%uP&B4N6T+sVqF1Y6Dcn;OXk; Jvd$@?2>=fjQ;+}v diff --git a/res/drawable-mdpi/ic_launcher_info_active_holo.png b/res/drawable-mdpi/ic_launcher_info_active_holo.png deleted file mode 100644 index f84b4a6ba6c0fd0a8a00b7381b53bf1421d430df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2736 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 VBZE3AXZTCN+{D_r?40|J{{X<&G{^t| diff --git a/res/drawable-mdpi/ic_launcher_trashcan_normal_holo.png b/res/drawable-mdpi/ic_launcher_trashcan_normal_holo.png deleted file mode 100644 index 799b62f8bcee219dbdb3ab630dbc43b1c34da449..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1109 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UOiAAEE)4(M`_JqL@;D1TB8!2v z2N=7Z%(epwmK8Xr18D^?ZvQoBE&~H|tfz}(h{y4zll?R9xQZO#pI=vh?QGuW=r_v_ z9%?9*pOn>SvH*!zK#FZ)sTwJGx}Ywt$KhnEJM^B%q9aY|qLnPAu9 zvx?0hIBa_(9Td)8Ebo0>ywjBT-q+ust&h8yc(z39T>Slq$xeb*fuVa(hw#Iimwqe~ z?cbPvTc;i{=#2aK?Qih*dsh;8mhgMd>WH3vyx*?69EAX(h*wZ-u>XWC?qtG}JexoKp)`8#L0`0{yCD}7NhntB7ddxHbu=k3Siawz&d-1ToHaj( zAnU?_2;aLg8!T)1xUK87r+NRZ5qYAq%-|`b=B}q=w>Dqpw_h9MduG8GU+s&E^EBbT?_g+HdK)|-;2mWS7OLa0FP`TA?EOWu_^hS|*o-l^PIV;wb z@H70pfe-G;;!LAjSQxY6nO@QE8$yHWBHT> zX6!A$SNVMALB(UP;-5`RE^|6oRUP@8qaRe5o~Xm6d@8UnsjK7Uh4^pBXTPuc`7!3{ zau@!zB`tOx?H_9s|2Ejh+%Y${4b>Ar#PFdm_p#ga>(9=r7N#E8pL^TAK$DwcPupe& zS?#dX8VehKD=-*{2$g)hYq@>hT}hpX{(qLszTVDvx75Hd{mqixMSF6etozGb@BiO^ za$fa2V2)EQag8WRNi0dVN-jzTQVd20hK9NZ=DJ4aA%@0QCT3QK7C^3*fq{DR;cF-w za`RI%(<;$57+V<|Kr|e@`_lxdK@wy`aDG}zd16s2gJVj5QmTSyZen_BP-NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG z?e4nOcv6+zv3+GhA*ne|81+Evt!-s36>9!-r-W<{J|;7nw_xr zv#){Qr)Lb_4>)qx&Mr!Sz+l4=?&G#yi9JE2DJgedKJ%MKjwzE~J#~MYrEs&Q>wVbG zmi=eA+T?dMi&`vT_q!y!#QN0tQ?Z|QO%J$=$8s#ac1z-9wl`k|lXY(L(m%0t)A^4t zIq~G{O2OC7#SFv>Kd5n8d!!Hnpv3` zSQ!}W8kk!d7<}5Mu8g7~H$NpatrAUxv89zURKpFgWu`z4k{}y`^V3So6N^$A98>a> fQWZRN6Vp?JQWH}u3s0tkLYu+U)z4*}Q$iB}P_m<7 literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_pageindicator_default.png b/res/drawable-mdpi/ic_pageindicator_default.png index e36c25cf1dda2cab6d766ed2b001e74e9eba7b7a..34493b1551958e96f8baf0559775e7eecc88ad6e 100644 GIT binary patch delta 87 zcmdnSx{Y;%6_dBAu7QcJfn|uHnU#rwm4UIYfw`4|!KZEN$_xw)swJ)wB`Jv|saCo9 dDVb@N3`PcqNE(bSt&E`>Zg?#-o!rV~2mtp=F4nnU#^Lm8r3=frXWUf%+-MOa=x9)e_f;l9a@fRIA+l Xl+3hB1|tJQ>>9$R&%8RhmB|nQNS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG z?e47Iv{_}#KlLpC(%*f@zda{Z*(TwD;`N47(e94oc2TuGk5&Jk;;+#4J^xDB?8dIG zxqTK3OaE`#x1jLIzrM8o;EUQRu8#z*ZI=IF&i`@7`~3dIyD2rzUmpb;y$heR@0Ekg zoS*-HX4fz~6k08r@p8K~(DkY%t`Q|Ei6yC4$wjF^iowXh&{WsJMAyJF#L&#j#K6kH zSl7VZ%D~{$Hg#nb4Y~O#nQ4`18jLNijG-ECcr7ypYLEok5S*V@Ql40p%HWuipOmWL cnVXoN8kCxtQdxL16%^JCp00i_>zopr01_dnD*ylh literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_remove_launcher.png b/res/drawable-mdpi/ic_remove_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4a2c3197ef6313903bd182721790423124c17379 GIT binary patch literal 1087 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5lweEp zc6VX;4}uH!E}sk(;VkfoEC$jZVC;4>+m3;OncvgJF~s6@?G*cLrC5<;^I!XvL`thG zwMFPgM=XiJd@J!$h_6+E9O|NP!>8)l`ZMH)WWOLyMQV>j`g%xJ)HFjb7uMD3F>?SF zwdI&;Gbc13d~l@n?(^Ndcbf<<3|iTecA>(iufoQzJydI|fZ)S_|LorGdiOcO;0HUy z>9oy89UMPv{wY8I!!9`S^{(@O>*_gQwSWEncMFi?;xhT<6L-7$|2>OgKmYky-HaEBO&{HF-@d(mL0h8C?QLg|F0qfj{x|%A!FHh_avjcK)#nT(xT5+D|hR zB_tUffew2l*wmJ|=F7@etFjpyf>x>s3s&uoyH$Eu-Tuv^M@kmm^H%)5usm-4bTy?Z zUQ6%Wwxw7c={WdM^sB(BT7;dOH!?pi&B9UgOP!usjh*kuAya!p{bRr zsg;p|u7SCgfq{1r=Oh#jx%nxXX_e?246O`|AsQSsdA0yGNP=t#&QB{TPb^Aha7@Wh fN>%X8O-xS>N=;0uEIgSC%E%0!u6{1-oD!M(4}cZTXIavHlJWd({i?CfszwG!Jejt zY{>{Ew|oMXrnziMHzf~Zg1bKpj$&IBwk4yK-0=w%nwGaEiK|$;MfkDfls1X@~rhG-ips31G)D4P?97WBbD9cxr nLQs}ko)AJ%l3LygC*;L9+X5DqusM8900000NkvXXu0mjfMZ?}n literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_setting_icn.png b/res/drawable-mdpi/ic_setting_icn.png deleted file mode 100644 index 5c32c54da9d6ceaff4315e749c42a5f894d34aa7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1718 zcmZ8ie>~IqAAg(ukWn2Evc)1&Ilq>zF-v|;MDk;5?Oe?mmSVBka_&A;59jA0VPk%D z34OV4E9(xEiA4D=>xXi#gj%snb?FDU?)%sG`*=N`ujk|Sd_A78*MF~9PDt<`Lw&41 z0KhQ7-#1Llc3N!LLu#|n!S=UWg2#F7^8!Fti2f9X(AGLBVSBv6UcrGcpay?JeS{9v3G(tnq6wgXw+jxC??RR zU4dRT)R4LiV-trbr)z(v_Mq(;vfOMBT3*<(?9Gf(KSt@@S=G+t6@$1<*H<67i62iy z25GENSykdHx4G5NovUeXx+&3#pE`f69O$0<$XGlzXFioKsl0n`b=QcUWrkcWhE>!^ zsjMJtlzGFF5tr{b!WNm4b_{I!%4{n{Wg1HvKrOr_+|TCYE7~a!c;etEd3Kh8#At$O$?EU*Oj| zz0Rr$mKLN39zYc}K@A8dnrq0dUfFH0dqadG0F*Ed`mFp+{)2Y-7-I!(*(Uo zi`M05-^35dm*-bU^|%)n4D*)n3HLOhclJmAcqsA4n>3p8gPglI9$`Aak`0{B;;GD# zfikEQau*I^)%*A{M8?=+2^}I7A8kH7x2RLYY}N+fV5EaIJT-$@kP5pa_s>N187q_MYc29bxKjK@y*{+^ zU7z&@XU&v`I+ zm0g#lB;JUy-2a~{7va^JSa>W>T0+<&z;Hb@M$(X=KYrr4<|hu0+xJ@7o)4Eb%W7`- z#@@K>q@CzY(ZrR`xyz?<&1W2k2i$9|hvd{CB%3>xKG(4|{nN$~t5$_oG^e>~{jnoC zrQ=e@FTRfJDh`_x!YT|y47k8k|m!=96dP}lrr|YXuorkDLH)=d*Dr}Mu z#jnDAsLfB?OcEl^%QR_4NE;=Uo;IQX$l#HVeQ(Ru-dO%(fFwmYE9LJ#V5^potmj%E zfcN&j5sxN)<+_HipZ6}*KH-y&;!>--&94&`2_)&hW?ZrC9&GC!Yc?ikQsLT=nA4II z`5v#KXu{!Rd2*uChh=+i#7`zzSGHXh)!`W$^D=jqo`11*=i^C3g?anekinJ~XyOgI z)dSkK&AAC$gpSB~sw|>)gPQz1rORk#@ADItHgZn`WRZ@KljeL3qzIjQNKXo2mijO;w3KN7mT_sw=wgkfDq5~ zKSi+|EMRgjQjb}xkUWDIVwkG@CvYS%IF>={X8akZbd+#ra(gD6;bVq<$74$^9b5G9{flW+CFf_LNmHMd!b8XFQ+ioP;e)8y?gg?P=4_%rJpA8Q&`<5j|k1tWC?PUCHyJ%JC zEH&j!);3l0{p~a?1yehYA=<1je`3T5qCr)}S|VuhjGa$;_DHnxDscsQ<;vfJl0n@~ z+S9y+N2T*(qIhg_bTV5@V23l_#lhLl0q;h`yO4=H$Sx!YXA;@j`Rw7=uKzI{Pm1Bh z3jWVP*g+-||7%Fe=rz_FFkd!=C&lv8qLSIbD<+A>whTBP6~_)^N5u$E^s~SG4-h~N K_Py-Q%=`!Yw)jK< diff --git a/res/drawable-mdpi/ic_uninstall_launcher.png b/res/drawable-mdpi/ic_uninstall_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..af456690c964a90017afa47e4b05df5845d3d1d3 GIT binary patch literal 932 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5lweEp zc6VX;4}uH!E}sk(;VkfoEC$jZVC;4>+m3;Osl?O8F~s8Z+Ym=@=|F*h&-UKUUEQ0b zux{h2d8;HQawyqye7oSxw{XI@E6wT)>z%9hIz*~Aia4&9Yif5|cQ44}!VKpX*BnH1 zlp|IYG8dNL_v62rpkuiA%Y|lnp7*wC&#H|--{V=xd^0D^&&TIP)Y`Vghn;WTxKZ%? z_wSn~z9QYOGV=1w3}InmJtt3kN=ZsSbXzQV=ImKb1{+gT)>jXNA4(Y*Gd|e6ckh`O zFJ2hm|2~_+!z=U3>#ysRlau8cempIbJee}d={^I4FvAotRZCmjwOUh8ai88auXmA`MzJt3uRT3e^7-@Uk=IN2OSTDSn#~ryz3u+`b?ZLeycyYW!6P|s&YU@W z9*f!hv#v>dR<-w^WJ6s+fr7Z0*aF4_@80DZ7#lY`Eu64AY_+|j)Whrwx znjAEjhQ!C;uT}cR=#Z12zk2cF#nTGaCOg{5nP0nh?M~3jFI5>CFO~+qn$*FvOIg6l zra{ocNivEg~;hTPw6Mt}BfpU5^h zSF-=OrBrWRHt*pBFJG=ac1C0RY45q0mt0@6@@m$6h74d_-FoxpO=-)z*GZL?k!#nl z?@lrLS(u&u`r?BJ4<>Ke`mbkFtH7p<88!Fs+^J!9cv@tsCDt7qyzuAG%9XEou^nI$ zc&qsR(#tI}5)wN;ZrIQGe{%7==Z2f~fC*8x#5JNMC9x#cD!C{XNHG{07@F!DnCco@ zh8UVz85md@nClvt0~ya!tXHCF$jwj5OshoEU}|M*Xk}mw(cqxTvjwO@5@bVgep*R+ nVo@rCV@iHfs)A>3VtQ&&YGO)d;mK4`Zej3r^>bP0l+XkK(;b9z literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_widget_resize_handle.png b/res/drawable-mdpi/ic_widget_resize_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..c3b287ff086ced4d80d3744aba0f620c808437fa GIT binary patch literal 896 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 diff --git a/res/drawable-mdpi/overscroll_glow_left.9.png b/res/drawable-mdpi/overscroll_glow_left.9.png deleted file mode 100644 index b79cdcdc2d0a969acd66e7caf65efdc3688bb91e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 552 zcmV+@0@wYCP)Kn#YzNdpy&Zny`m z*l-ih!KvA@LTVmnk&NmZ&#SZuL4V2ejK@hlf8E5MAR>eiFc53FB8wveMdV-*5r7TQ zTStRh(o23VKVHf=caL*EpAbS*yuF%ifk)s0xN(9IGfv4|7|)JPup1M0Ah*P#NGq1z>6UKPM<;T74fd)cu3v*Vt(z*{73V*0im5J|)&m%}A@9(k|ny9f@ga#U8@+z%BVhm=szMN?Po-zAc9` qrykOXa~k;7XFWFjk)rt@A$$U8qC1!j8W+I;0000v1&> z!isw!aR6XeGi}Xc(viot9e1E;1bvbf$8|n^N!6DSGlPhrz&fxY#07!Oj2WXDF!Pdj znOrX?YMY%yE$^rL83FV}Gy<3t#LQDCS!-))PL$H$1?9D7^&?}R$$3WB1g00jBN-(Q6_e#7fG3nmX*>Yl$I$;Dbl#aR7 zdjLm=gdHvyEMq#~T5xMaWC?Fosr~4ykp6vXhpvzzxAr@LJ?1N`2d_<9zpQ>?kEm#u zMtPaOwZCKmbjgC73Fn`_46co=(%`@{c{hRIB=%onBX%Wh#IA&m*p<*>E_Ux=OO8&( zBB>MU;Eeogop{ubS2nQ!{*B6sq?f0E^InOdAg)fCCW?0GP_P;mru^KLYv!4e(H!|10LU P00000NkvXXu0mjfB2(n9 diff --git a/res/drawable-mdpi/paged_view_indicator.9.png b/res/drawable-mdpi/paged_view_indicator.9.png deleted file mode 100644 index 647b60f376b99193b6763756c661a3bd5a578e55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 264 zcmV+j0r&oiP)1Wh0_E?sQetjtLkJbLkOBnzG#v2Z|XwwG;7zL}9M zh+fk;W$BFMoao)3lbq8yBYI16Ol3}K)|T9*cusOmv?99ckb9z+RGy(>AEIbr?U2R^ z(btY8U#Pud>F`7HK$3OGk`LdK>{Tagr_^2@?qpxCdu9Ifb@%;G_MI(4@`4*re)p*W O0000dRTQ9v)25+90DAxdr9HI`seVNHM1wILPgj{>?z-J>YciR9@jUO7>$=Sp z5D8>h6h-URYBlNi``>4?*`pW`Pqic4>V?iqRRGig)B)7z^ZD0yyZ!kL(5q~*Sp4X8 zI^O_1Q`+I6UI;OzCDl&CRRP=qXnLOaF%KvJAYV=LU8TN^B_Y8W>SP)L+$TxW$P0{o z-77F-z1kGxH72JsZ>y8<3Qo757k;f6Luu9WHm#NcrxjyB4VSmspiV4pD)r`?x7qeW zWAXFeVf!v}*zfl3lDIH!LX8^63gn9{sec#{aMdbVb4yWO@RlT$| zCd1)iJRbiFg5bZmjvxq}(P;D&htOKa{l=8Y@1Q)*yg&ii4hDmV<#Ji=^?Dx~jYjP) zQ0w)2H=Ry@4~N4C0NWHeYiO+_*9L$VfOi1iQ)*Fpo$6(MOS!`UhY)%L;1R$dN`EQ8 zQJ^tkDkj%fKn*IQUWh}CUK8bM|LhpYlhF>q6M!|PCyQz)M>rkQFdT!)Sc#!FFhk-r zFg?g;0jO0+H6jym6R4Q0EE(t}9_OM7?Ki@TdO=Tq<%002ovPDHLkV1mY?c=rGR diff --git a/res/drawable-mdpi/portal_ring_inner.png b/res/drawable-mdpi/portal_ring_inner.png new file mode 100644 index 0000000000000000000000000000000000000000..99aaa60a97079f1d9defc7e3c8048b08d3739c03 GIT binary patch literal 2641 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 literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/portal_ring_inner_holo.png b/res/drawable-mdpi/portal_ring_inner_holo.png deleted file mode 100644 index 72e0af8f8d39bb79626890993c007ec87e44f5c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2779 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 cIk}}GO{ppoU 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 Nprx*ls!+p*{SOVn=uZFu diff --git a/res/drawable-mdpi/portal_ring_rest.png b/res/drawable-mdpi/portal_ring_rest.png index d0a976e55d9bb05b2132dbd9caf268acaa530077..5c33b424d0d3f1e917e10ae33fecacaae44c7499 100644 GIT binary patch delta 1597 zcmZ`(dpOg382?Udlttp?8cm2qBeFAOOqgTHG1ponbJx~bEtMY;xfDq%x#kj1hA^zA zQt4UaiO_Ni59ZdilBe9-adyr>=dbh6=kvbL`+nZfAMf*gCXm_N@(3s+0Fu)HSOYSi z1AvJJ;1dM^>}>$l>BZcmcGshIAmAn&y8#z|%wYgF2NRqfeIv&{7ZGWyt{Sg4Y+GXX zo^f-E+lw;LKjLnXXSnQ(*}mm;g}V9Gj+#CBObx}cn z&j`_Z>C(;u^_*#(M-y*B;Gw+uHdDJ+Nd8d!?Sxie;)Bt1NlU#8kL?tp)?vTkxHcJV z1QUoXPJ+9p`s)=F>ItiI0|WhIWkzIAkB&3H3=wfES-pLICA;+Wa#y5(Sp*hlD4<}3 zd@L+Eo*bI9>vm0Nu(8^qsYOk&$z@frZbDV83p;GyT`H4*-G zQ-P1ngTv)wThg_#$SOj}gB%N%T`67GBQuTR61;+o15zy*U-ALAVuhFvuQZO&oN-`%w-K)V5x>X}B$lv*__QRvW`~-VeelCC@TC{nJBQ zk^N|-NLJFD;uw>T?D|hjK6OKuPkrj(BvP5b;w(IG6Gc%`Q9Ke`2(?9<74?OWba!`S zbrvMMU@nydcoNAb>t*O5Ec>OtFKP6OOJ&BkO>j!j)Z$GZ&)(|%qt^EJ43=HjrK!s7 z^ac4}K|F)OI9TFI6D_UQz-E@tRSnKx<@5POjNRP|CGkCQr6y{9&z?QwN=T%4V4?Qb z`%zk@hcDK9d3kL>k9j=l&R4dHl`6?UUbu7G+a1!JDk>>x4W-LYCx;GQzwjtiT+x~>K6|1melI}a()&!{)J zdp7LAhx+JW_g-IHs}+iQ8c0Niah56lWvB{2Y)5u>b}l3ts$a@r7Q-iD}p#{-e8=kV@i^LyIzm+^M{h=O=qzoTuRV6CS4`TQ`W zTLNjFIMHpe6&De)7mLN#%O0gV61!98Vrpu7S6SC?g_@L<^c!fI(9s(gs@^RJYM_*1 zz6r{sjd$5=Wjgn`y14Y1AM3y&5D5BAucmR9-1^m+A|vGUrvn2jmUockt_ytfV^;GW z_v*=(mX=_QZbS`;9Gc`Mf)qv=0kok^fb32p`&cKVuCeipcx-GesqBqVyvV`=oRHZN zuzyBbS^3~dtl!0NB7TeSzMBW|0gzlVs$jVFVXe(!H+a8layK+7d1UEAYs_2l-4eNF zW4J0YV*d4eE(x~UiMdF-FS|3|;y*zgEi5Yh-2V$`l&(pcK=U7m s-)Z3qXG0iN;21^=rS2xgg+x$&s3Bo0=T7sfcCsx%IO6HdcA{MR8`T`wPXGV_ delta 1554 zcmV+t2JQKa4dD!sNg*Ww0Dyx40Qvs_0D$QL0Cg|`0P0`>06Lfe02gnPU&Tg|Y8-zH z6951U69E94oEQKA1y)H!K~!ko?V3?c(|8=mzin5>)}FG}V|eaTjvQv;LO=tYFD6P% z^b%fuH1S1!@W~h-m-tMgz8Ri);ekXGfn+{lOn~K-U?PZ+%rj%*blh#=2uyLbYw10# z9laiR?!R@L$@QE3vwxGe`+fTR_1}N~e!l{a96562$kG2-RJ{!-(Jt+41f(p#*12Yx zmkm*CK*}^|%eECz1F&bBo5xoNXog|{(1IW^ckbLd*4fz^6a?Y0rfCfTD2k#~Ns@A# zo0}g-M@Qe?zkh#IQ4|B#7`A}g0cZtdhKGlb_4oIm5Cq|Sp6B-fF1_c102+u`JlH^%kU5yP449v#k zaS4D@DP$#p2AqqgY1iD`TwhmL*9n$oNx`gQwzs$O;K75*t5>h40Oav^Z`K5mf*y`d zO-=4suU`EkiefVWsY1A-BuRhr%*@P0&!0d4R);jgmI$&Oz@q1Po_EJ$v4LPPco+xY zV--nN)yAh!pZ-NCkx9%9H^|0dN6uFD@>gX=`f>R09qGXqr|;QT*ZP(WB|d zk00-#3=h&}05SmB@$vEYP$<-j!d0!BSeBJ8UAokN^5jV`3ahU1Y_)$Ku()D&cXxZv zojdn4Y`o|GOFYkGWMrflg~(w@07*jBcpNlDB9R_Z6#b~u>(xQ>`~64O*4BPsSXhui z5UQnm(75-QU@+(og+f2-e^N6s43occ-)0ZX!-2+-wnF@C>as0W-bwYRs|3}~+h+uPgS zKA+EBM?f?;Hy-BndyKVO%#q&H{SGt1OQozaqge=Q-ui>a_ zaecEjL;(Qn>+4m0Eh|NmBoWzADxd}cv$M0ixm>PhZ!ZUFV`G0~*KCuWfLc18mQ$(J zZi&ZKMJp>SSrDX5B!GLGrfKnbe6wC~k|enzk;pm-${0j|0Mtk%@`s`*q-mBdN+c5b ziHV6!H2{s8h%z)Zl#a*a`C7winnp#V(KT6?73`NUz9?c3%CelFn3zb^+Ka2JtIXiw z;9C?DY})~$pHP1RAm6@ydu3r^L9P{CKA)!_K75!+Bod!YfF*?}Ny1w!@G6QzXEK@3 zXV0D$IgYD(UN}8Htz5Zs<%O!MITWf0C3}#x=v0J_#bPqYaZFE7&yj|PhN?j>FE6{U zU%&n=nM`i$a54xo`f{pj#re$4%(fs1d{jn)yQ+ZyfSsC}$|RG? zoG6Ol1p)z$rpsSov)L>=K0f~W+O=!X=I7@>pwPH6!x5g3&$Yq9!B{vPUfkN++Cjl{681~?#7!7GnlB9WO1C~5T3TB8 z%a<>=oj!fKB@_yI1A&0*@pu%PrZu1tf{LQh*=&}5|NgzUw6wGpiA3H#dh}>bQ4~ps zGQfx)3w8n;#}^whC?un-4$1;x1VQlh_4WCVA3uNo&7nhw+$_sdxm-?7r_;IDuU~I3 zEiG+Hl4OA7u^*1shD*)cXV!BR8=wXp_AAlWB9Xa$t?END4Qm6mV&m+Pxespu8NcRU zy&-GMTD5E&``B{z?UecJihuh6!7yI4%+1@596562$dRK8^%p6gEKipYe<1(>03~!q zSafD&VRU6WV{&C-bY%cCFflVNFgYzUHB>S%Ix#dlGcheNH##sdd0I|q0000bbVXQn zWMOn=I&E)cX=ZrS-R1`)&uN2z;z0j9uwx7+2;xp~|3MMOxFWSgex4X_3F9GSIRYyWu)w3dRegLCe4 z){Y$#6JXlqN$hh?39Qu^G3h}pxVuAQ3b-D`9Q8X=Lt<5ut_kLh5$mJW2XiJp4cwZT z8T%8e`ZqWRJ!6Ap6g!onipfyLqH1Md)aH(=mCm{Mj%0Ao6|}6CRpVgS>-9rdVtJlF zQr`D#Zy2Fde?ocVyk0Jsw~NK%N>v+G3K7xGX7jRIt)75q%B!_)DkhZO(Is#JoJBly z3%AdPx--1`-fY_ez_N{iE#SS6|(NyebjEs!@D`cpD4u1lS3d_eG S7wtU&0000 diff --git a/res/drawable-mdpi/quantum_panel_bitmap.9.png b/res/drawable-mdpi/quantum_panel_bitmap.9.png new file mode 100644 index 0000000000000000000000000000000000000000..86be568fd06a34645f0db78c83333c48ae110c60 GIT binary patch literal 436 zcmV;l0ZaagP)KmqAX$Fc3wbleS@jDzRb1 z;ovyER(ltS#G+Kdf@W}rMQo9}PGjMqBGfN=lQ{p`V>|gVs){j&1ZheqAj#}(IyE{o zaz91NK&4{F9$22}lL`f}u>pjp1}K1;YsC+tUM=wHS{uUqDu6{@*Y_f_v({=(6OmnA z*AKuVn1uFc?8|$#-|ru-wM*61jU1OThO#VQtE##KKIoM_ETsV|Rn3M1s4A+u1S*1M zje+NF+h#+(oY#B|JajErN1g?doi;_8$Gm=Hc1gN`FW?LKe}K=Dk^WSvr+ZO<`yF|P zUSTmHXXLPg8Ic|RG#0}tNu8s{NM}sHC4E(fB eeWVTms>&CIH^W?%n`$Qj0000=XWz8WZ@Cq zyYVh_3(qinU3msK-o%Z2k07GXL{yxl%SC!b226F*8OT6?P>@AY^{7;JR~yDj2Kt+k zd7`2YCf}w^oNs6oi!-Y^`Xox7Wpsd{Pz}&J`&XF~ECJH9^NVGgM50rI>kubZY>hzvcx_JSF6k{9I_aEDSc?bjHfFor4=3P40Kh6*T9+DHRS zfMNosU%6O?mfNS9Q)|^B@|KI0YJOf**0#OvFi5_!LdO4~nl;#jtv#J)U9nj%akH-2 zEEU0qDuNBw(SiwJMdW#^gf5mvxp6cw3p5|~dL<&8a3*>AtT!vVy}x&t(oX$^ZqGjj zD!Mnqn49A7L*x~B0^a-Jw4L7qU%;FD&PRgnVr_xi#SE$#bXCiNl5X=10c(M82N(>t r^9#N3WzV9bqK-^_4b}F00000NkvXXu0mjfah$)N diff --git a/res/drawable-mdpi/quantum_panel_dark_bitmap.9.png b/res/drawable-mdpi/quantum_panel_dark_bitmap.9.png new file mode 100644 index 0000000000000000000000000000000000000000..70429b9fc5a05049b8a577fd4ac4ffcc6ff605e4 GIT binary patch literal 445 zcmV;u0Yd(XP)Kmpe|wKoEw%@gr_fA{vfB zPs3R_2FFMn4nf69Xy`crNEAWA2$1aTPQc&cPRxZ5$znR?~?Qc<4M1%pM zLnZ-(#EuqS*-6j)A)&OaluNh-h8=T;LIJj}1nQy!1VCg*a3Hh{1LSs?eR$*mhnH8^ zH(?a7fQ2V`v(2;H=cmVes#WVgxU6;-&yu@5&*!Q`53)N%WD(Dj74X2WVuj{X3Xq84 z4FxD#l{t`5+g2AiR^t!-gBMzH9~dCm8Z#Q$YBe|kjy^LZg4?uw+vVVxz!P`^p9c6S z9_e2#^>|m)POqU^2P>ee`Z5|-_0oS`_r>}M; n;S;SLA}v2{W4BZVpsM@=dKZ3e^SC&m00000NkvXXu0mjfN!YMR literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/widget_resize_frame.9.png b/res/drawable-mdpi/widget_resize_frame.9.png new file mode 100644 index 0000000000000000000000000000000000000000..856cec619ae8999a45a29b1cb64868dbd64746ae GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^Rv^s51|%Qu_R;`SEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYk`87JY5_^G8*6BJjmM=AkgwKGKq0Y66dxQ)oCrN0jFLk?0Iptxu8JY z$U6Se+txSo9Q_O)iv;%b{WkV<_YXL_FiU3DF%05XOPrCv-zTnwdpm1fj?A+3mzu== zJ=)0KbMK;j?S|jSX1RY85M8LOG;hDy1eu%8TPh`dGd^W6xGI{vFf>EP)}^)UZK_)3 z?I>o2+HTL{W#)^o-2UeBewXgHfCKe=SS>?(ov&J;6TY)AoPX~3^?c(;raRl44&|P( R>$zhv3!z~GNWjWqS0$P<@N_PIX8bx#3n!G zQuyZ~GKZ&IvOA#V%F!Bqxi5c~WZcqu^JU`Guvv9s=OSfKeZAW1C(R7B1`Pfrul)RY zyV$npB7#a+ue)8^w(fLw_LevH^Hk@)x>e)@DD}^s^s_%?F%}ejH&RyLasrn(@bN;KUb?eij{(ta%TDEa=$>id?`#*jx4DwnZ znkQcz8y$Pq@>5rxp6Bt$AK#kzmNP!jOp|E4{kQJF+f(EEzyrB@pX6$FJujQwmE~(M z*9t!&m3zD}gtvC>r<=3?KkDP!TUqdC@{<0j_%~~pOc%eezkkbaj=$#Wp7VR3SNDjo z6JGn(f8Bq+g}xlQP8q^ZvjuLoTu^H@u=JbsX3tB(Q#r4M&+XHw512Xc@47bSQ-anHPG8A6@BIDr)Vn?|FH*C%eLwDD zQoDDYpI;Ts?%gtOpM?rDu>FVdQ I&MBb@091VwcmMzZ diff --git a/res/drawable-mdpi/widget_resize_handle_bottom.png b/res/drawable-mdpi/widget_resize_handle_bottom.png deleted file mode 100644 index c838bf405af66e4b87b00bb5098894f30daad50e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 745 zcmeAS@N?(olHy`uVBq!ia0vp^VnD3K!3HFw<24(A6id3JuOkD)#(wTUiL5|AV{wqX z6T`Z5GB1G~g=CK)Uj~LMRR)HJW(J0z|A916ywre!;Z*_ygVhWM2J!q!@kiZ&YT1&! z-CY>|gW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pz-%ST^vI^j=!C{ze_k!q<#NvA0boi z-Tw}zif$LkaoMslC}QoJFT6Ql1Z&u=wX?Z(b!(Y73LMN@>mtY^J@IO6SnrOTK6c%f zot`K1Cdt0})SQ^`?40@YdB3fnAE*zSl{Rq}+cmBkm;TLPX6wKpx`69Sqtt|>zhrK( z-g01nv24m5j=mR04-7VN=rCVzxLUD~aSx*%(`^UtjO9~y{mzTbU2*!=tY0yA3-?^R zy8G3tTOrz$7ihnd{W^Q{)Ybi0ijyB|Wi6W*lD{%K*n$14f|9++N|~;0E8{wJ&#kh5 zmHV|N>h0W`_xl*`FzB6s6}f=%4)e4IpSR7c8TVB(WcXiaj$vQXl5YHfqhKG)4~YV` z15*!%Tv2*u6xDV2dvIdR+DQ-M558>PeSq)7N|qAoA6|ja!WPXeQMxr<^}zbZ`69MU z;x#LC9*cf>$8f>y1J8{`FQlDUW-!J#v>)h~JlNm()x+KHi`&{&-LG7~?)z1HD(<7p z-c|F0vJ>yu=N2D0@2>f$qI1!#YZ_}TR((GZ{DAd?80xNI7kiC^~SpNEpg&Pv*E|L;|{bCh~KYs`=JU{5Zuh z#%Hd@l8wok>vleyl^yk$b<&<4UJi>iq=3<{TH+c}l9E`GYL#4+3Zxi}3=EBR4J>ty wOhODztqhE;KwNVx1B3NpAKs#9$jwj5OsmAL;U2d~AW#E?r>mdKI;Vst0FcfykpKVy diff --git a/res/drawable-mdpi/widget_resize_handle_left.png b/res/drawable-mdpi/widget_resize_handle_left.png deleted file mode 100644 index ff0b0d3570bee5dd4b5de6f559613e2a8cc425d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 762 zcmeAS@N?(olHy`uVBq!ia0vp^NI(3R2di=ni&{={s+=P@lpc@hF1v;3|2E37{v1@#UFJ8s%1;^ zc6VX;4}uH!E}sk(;VkfoEM{Qf76xHPhFNnYfX2V~ba4#vIDU4TZFY#GNZb6Ad1etVY+P_nOo?&zgiG>I^f!nd2@_xmw7y;9_IrVnhr`_!JLJD_oU!?%lHt3( z<*e}%JI?Z)uT4K!&G$#$>$8OmYXn1-gY`Z}mSYE4G=jYT2~QAmV7K6sVeVtjKA`kL z@CVBq#`s4015Xqd+)~KmVh?KwKH!@myn{_=x=bar4Wk@Gej}rS&6&Agek*dnF1Zyx z|7ZEz6|=MaW4q)tc`gO*ekEF(&DOD!?N!^ZWqQZ1niVBS?pF1zpj zziV}d;=QA)35$>YD?T8*z`HJat7FuYS37+5u3AO0`!URY`89;8)Awz`lOkV^2JQoq zMK_o4>eFHHJ}{F_f%C;t|Ep}C8;|iiC@gQ2;Xm5 z%ciitL7y??N@=j*`xK!r-g&Lk06Bh4s=UXZ5 zFkdF@?S}uRADC{i-DBK+;QN#yL-7ehKGGIK4zUSSZTeM;X7V?j-}tTJ;(LK!M<%wq z-cH$1DQ3QBJP+`kO#Oenuu#wVHKU&IdC`Jtr+#o=dANJkh5b7Y1Ji?QiEBhjN@7W> zRdP`(kYX@0Ff`URu+%j&2{AOaGBC0-w$L>&w=ytzU$Qt0MMG|WN@iLmZVmG^rLO@s OFnGH9xvXI(3R2di=ni&{={s+=P@lpc@hF1v;3|2E37{v1@#UFJ8s%1;^ zc6VX;4}uH!E}sk(;VkfoEM{Qf76xHPhFNnYfX4syba4#vIDU58?rf1jk@o#F=awoP z<^?QfkzX*kD?_nk>r}Sg3Ccg%e~L%k?wFgPnw73R!!KJ=!A@bqbhS6hcHPzD?HOg| z?{XeCHYTRMoB4iT@w@jjKm2v1zXzRdkjY?lefnESg28#gnN1t>E;%YWaKB*qe84b4 zFoNmVf${|Q8?1GVdzU%Xuk8_VZ(w~O^Fd$-d(66;{rr2_=P|Eu;C-NOaAwn)V-Ew3 zw=ULO&3~m-{dV@(WmQhPTNK;7ZiSs+vHI0H5dr1s;QSS_Td#_!g!do6>nq&Z{LN?Y zl57fs#7Vh}GtNU|c6bI`rjx{Q5TN&O7`$fF{(sROH{i#WT zp&-kHsveKGi`F)Y9551g$!pRT+@h{)Dmo*Pnd8wfomAtWjOzC}v}R_-bDWA^)-**h zLHfrHhLm1~pUu09;(I;#Z?MFvIOPaEshi#+@|;%ksU!2Uq@gX9mc9LBv@3?FPbsKI`VW#_wN*@||P4f=DA&(CM9 zZkar3&SdAq5t72(O@Fp?+8&XZYWHx-*_4<$S*1GrRwY?Y_FR*+pYf~Bo>i9@O*jWk z8LB0&5hW>!C8<`)MX5lF!N|bSSl7T(*T^Kq(A3Jn$ja1Q*TCG$z~FxHEfy3Fx%nxX YX_dG&tm8}C0o1_Y>FVdQ&MBb@01jR+#sB~S diff --git a/res/drawable-mdpi/widget_resize_handle_top.png b/res/drawable-mdpi/widget_resize_handle_top.png deleted file mode 100644 index 3b1df01702f89a7ec5f586f24431b7312b881895..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 750 zcmeAS@N?(olHy`uVBq!ia0vp^VnD3K!3HFw<24(A6id3JuOkD)#(wTUiL5|AV{wqX z6T`Z5GB1G~g=CK)Uj~LMRR)HJW(J0z|A916ywre!;Z*_ygVhWM2J!q!@kiZ&YT1&! z-CY>|gW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pz)7AT^vI^j=!C1ogLyR(Kg?5_w3#y zI~KWd#yMDvUUU-3*5)lc==OpAC-<7%BYqo%wr&>mS-w?3Kwsd*S?+4f{-ZV0{U`Iv z?lxTAS1`-?u<`vfHSetru7(8rnnq>bXqeT&w1V;LsXIb1m~0pLX!f%6CGci26(2}^ zps|6Wq+$I5u?@U`7;ZI-c`z?9K44S8wuAo;qh8pve@yq7{2Rp&NPUofv2=>ej?%3w z^;Uhq66*eXX;rN4vTIju1Fp1HdBuipKi2SV>!P^e=&W4x9RdAEUOf)k743h8`&G{V zS4uZb7qHqeob$U7d-}lC9dDQxbcm)sdNy6HLFjFTxz&nm>v!DeRB!w&+{C!f?PzH! zdknkS&Ep2i8I5;-yMFBvzUj#xsBf{7>m6hK3B6e%^G}@9`TfACVfKOF%njiu#NIEP zQl^#1{QAIV&V{d2s`HP((^RtAW%z@u;&XpMqx*rZhST=Smg;F&i(gIpmFRPP>Z|0h zZBvnKW(xOYRR!TCVp1Cs*D9jq}*)|4nJ zaCd?*qxs3xYZ(|A1wCCHLoyoQ&OGaN#6ZN&-}vpBL#^-s&uD5|H23u)-i`^k9^KKn zU&Fr1Yu1w|&-$lyEKvTaJ5yTt>H6vm+}*1Z15I^SwYgs1utG{K^zfprh!EcHRfcNU z?PYZ4e@mFA8FlTKzxC^} + + diff --git a/res/drawable-v21/quantum_panel_dark.xml b/res/drawable-v21/quantum_panel_dark.xml new file mode 100644 index 0000000000..405ad51735 --- /dev/null +++ b/res/drawable-v21/quantum_panel_dark.xml @@ -0,0 +1,22 @@ + + + diff --git a/res/drawable-xhdpi/apps_customize_bg.png b/res/drawable-xhdpi/apps_customize_bg.png deleted file mode 100644 index a51cc112b6511eb8e2b8fd56225f76c1b2bac7eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_bP&H|6fVj%4S#%?FG?LdNM1&--J z8myY(_|*8fKn|Cui(?4K_2dU2HUnqY|9)>EOSQx`q9i4;B-JXpC>2OC7#SEE>Kd5q z8d-!GnphbbSQ%RYxmE@S#U=7*Q8eV{r(~v8;?}^Vxx5glK@wy`aDG}zd16s2gJVj5 gQmTSyZen_BP-KA2Vb5TP=6_^dzU`@CMy^M1Ww&o9sW^}L?<>q&KSa@Z-SEe8Mqb~+*vZX)aw zWrU2hsQNMeX(Eseg*#$pL=h|FOBQk2TS$B;03e_GH^r`DGE0t#lh~--xl!k< zYIuF%PeIw1pvu}QC)hGA^&o|9)3$$a*Sx+s-1}|}&b2A}_cwbb=GzxWG6|Xo2u-(D zl$YwWNMv{ftXeyamV~#)bfTx6&59Za&MhrJj9#AQo#VI8+_2^MtVpCY=1~mL&z8*? zCc`Fy?vBh~LGZZZqQL<4y0AbrxKQv=hh8Vs7oAmS6QvKxuc2PnaB=*!<@+RV9HbbA z>V0^+`9SR0hB>-WmVYfL5h2;+pA7UNIsKY_*zbR#*Txp|+@@SPgYG@e8P!;)0d}1H z+M6TbCdpw~9Zv?@Vc5J4+}MVO6cDH`ZL`N*ccvx)%$ZwfJ-3MPA7tF~nyM3js-?TQ zA~swY8UMOL3=?EYZ1oAhHhe8Enc#Q!Ejfzg$vgGJK$S#tg0vT;yzQe>em%nnL+AIJ zLwu5b{K6rpR0fAQz0+^|SAe|=tv)ouN#VoKW9-TDvX2GsyTT8C)U*aaNKn~RK&wi- zK2o6OdC~mK5ty-G%1{b{(id2MuIcnxK$1GGQuPm={;s)jcfmwW0I$>E-@nxZhg&_A z&m~q>S6An-V60G0^*1lM#j2@@VD7UMdrzG@)s>>4x4t&o5Ne0PU?xk3?Uoed9p>O8 zd1tnK+X(^anWfZ5#x9U~u+crQ*7&&XCuYu+sgMSywUt#w7OIdG(vVvlWNTkDD8cQ7 z71P|^>mGlY@H2Y!8C~e6^H1RNl(7TlPt@YS9HPPTs7e^(0=68Y&QRbAKeP%)lk_x> zr1htq2bq7kR7GQl&kk*EtSoG=adNE90!Juk=T?=!-nr3N=8!?o4YX=JdhFO5KS^Gb z;Ecg+!beCJXUE@sN=mZav6xd zJ;D(PTfe`yr)+;79Zl0addqwH=hyZ!Y>e`og)J{WFqQ_i*i7~j{+{gW77B%eEGqSM z%@e6<^~dD@)y}PUcXxX+JsW;IFwX0R8-8p`oxiW65$9lVV5c2%D|V$hW<_ED{{8bz zEXYFcnXcFm6Gud~i}?;NmrD#?ndiqDKx2_Eo8^?`0Tm~lkoDcY?NxSmcC+Xpj(U`0 zzYRNz35~w9D>AuL#EsQo~Ki}UoZj_XJGS(c%gV3o5+VaVZUsRW}-gfJ-_&c|l9c#5;LLy6;mf z%F8=Smm4~Y>Q!UoE;1oeH>B1nN(ok0R-dk1Ze>y=d+_z457;va{!Mf*JazEur-_`@ z+O0N4y;}?atRt7W*@^8w^0p$OBZ{ z#NQ-V0w9L}-}I9c?q!Bs#1AxpT_RGC!YL`OdLrfe@(=Ei@RlwbtVFg!cC6#d+97ZG zc7xP?zEbPdS#~thD(9x@(M)^o*~es0rs2Nc$d?%zNE2PgTzfx`IRFMf5cw7LO5N=- zz(s#NHRppb2r=P8c8q^rSrRg#Nktvk7Z@2yO0OsFHZs#s&jJF0ds+O*C0DohFNxV- zW=}y<$hjmE$qke_tOS>&ECQm*|IGljFak0&)Y|9u_|I52n)mB~Gh#x^FDpj&Rq%pHY z+1r-fk2G;BS~tX9XoqyX0xJZ0?JPs1(G$-3&`s{e#)y5YuAp~NG4@F% z*F^FqhjjhTtv_pvf|!$4%|%}D8@s6ILH7w4U-k9%alU{5&TH`8Z)t74vJnfl3ml1n zZ`{Uef0hNA+_1?bjMUu}5PP1T*kE>L9oSB<-C(lAMyKP?@D(I(M@}aVffpnT2=`T8 z+b$G|u5@*7pxAl4n#B@k293z_!j0WfSnX-6Yk2Bq(K4Khd7Yh|@%6#uosq>(c^*au zD#}6EFz2$^a>fYEaT7BzqQ>W!JUW;BZ9&lL+}+dTcT>=^nIak?{&ju*YIrGDTj(TC z*8v+bd|&^wVr*<|he4y8TEf2f=ZLg~9Lff(x@OmwpMd-#hz+{#4>BFtSA3=!lo{*hX}?__XAr7;+Jg zb+KF+j{I|-_Ra0calbpBjB?Xx`@X$uuwuz1`YY!GGXra5}>L=?c zUo~SU!S%h zFjlJ^cpBNSRJUd`^!yihFWrC@s;J8i68rmNzzzgNCZyv}2+MlowzqNZA(?pIVc6e} MqrDTN0`8sgAGL*ILI3~& diff --git a/res/drawable-xhdpi/home_press.9.png b/res/drawable-xhdpi/home_press.9.png deleted file mode 100644 index d9abfd358dab5d524d8854bd186a5cd5fc523e3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^tUz4M!3HD)L{3-&DVAa<&kznEsNqQI0P;BtJR*x3 z7`Qt@n9=;?>9s(?0#6sm5RcB}AOHW`Gcz+Eu2(s8;LwQ^Kk9Fu`yay-An^3ukNKU9 z+zCku2?;Hi49+wJtFk3DUg}|CeVL~A@mA(bKM`5yTkj8>+qj>VnVA+e!*1pVPQIxo y_91`#ij6Y^8Tg#`3$L&Jv2(JMvE+uWYZ#ig3C(_Sfr$rbH-o3EpUXO@geCx}&qqN3 diff --git a/res/drawable-xhdpi/ic_arrow_back_grey.png b/res/drawable-xhdpi/ic_arrow_back_grey.png new file mode 100755 index 0000000000000000000000000000000000000000..79b9b486c9bfee950fd4e9a9c4a8e78491cc137a GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgHhQ`^hE&{od-WvmAqN50i+l`4 zEeg+$8SP{&Y~0za(`UA9cDQ|`R^~JgpkjuI)@#w{*Z!Y#t@w4iuf!K)wKHEftJYko ze?KGo|FyC&FE20mpKn+D3(nwvS)X|Ft2pP4p!&v!+^?)67EY{sg^lT34!Em1Hnr4y z?C_qTu+Z;NToVhYsgRtIipPWx#+>aPXE>LiII8nd=BI>^(JOw==_gK4Dd2@TKwNu< UnT5E9m=lQW>FVdQ&MBb@00SypIsgCw literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_home_all_apps_holo_dark.png b/res/drawable-xhdpi/ic_home_all_apps_holo_dark.png deleted file mode 100644 index 81228d4028b89a837587615eeb5f2c63895d154f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1569 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 bM{Sb?N}^)Z5*sC}0SOYX4e-3`{)6-%xq>!? diff --git a/res/drawable-xhdpi/ic_launcher_clear_normal_holo.png b/res/drawable-xhdpi/ic_launcher_clear_normal_holo.png deleted file mode 100644 index d4965d9f804e230e6f11acaaf2f26324bc083cfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1392 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 diff --git a/res/drawable-xhdpi/ic_launcher_market_holo.png b/res/drawable-xhdpi/ic_launcher_market_holo.png deleted file mode 100644 index 958f0de3c1e9d77aaad20ddf22667ee7c195c9a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2187 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 diff --git a/res/drawable-xhdpi/ic_launcher_trashcan_normal_holo.png b/res/drawable-xhdpi/ic_launcher_trashcan_normal_holo.png deleted file mode 100644 index 2ec7ad9b4e76dc359c0eb296cb002c952cd9094a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2240 zcmV;x2tW6UP)ht(u000NbNkl)V=W*YDJBe-VxDTaG3Ti~Ch{RJM5J+?cQiT}t0~mmqB0vZjFhw1s z4mtxNL@Jm7gQkQ6B?THNO`O_wY{&8AzRqj!y%vLOJFk0>?>#|6NbEB^kF~!2t><0` z_%AOS{gabK1js}8eHhu4QKfmqzpCI<>YXm>@V56baDj4*)+O87m}`yzV-yAy0veYI zjG+Pj&{KiIKH9V^Eiy7Rgb^5ku14>y&*a4<=@~-)iZv16k3M{phGix@`*Fc%#Qm!7wd$dJ*yYH<0=^5v?G{6 zd{q3ZbG`=N>#nr*eM-(vGy(#HDk$*?Qip$)L_xDAVo~(sKfnKXk8K74px`6?`Nij# z01z}hC=iBp!%>{D#{&RhKf)cq>6%~wo*uK)yU~3w{U@WqCqwh99`02-CEx2wb{X2d zDRk$A@VWaKD8bqdn&U@GpT2h6XtEEJNn6+TdVu)H#aVCBpYZWDz{IxZboEk+#U%4NuU}$ zgPHpB7tekE{ZB1V%~q;oKG0)Y_xZ*h-H)Z+X*V~we*ce`-}uwpSg&kHJ1Xf~Sw;@C`I+6H7oqA(k;o<4t0I2DE{ zQXr6ExC;a#1tsK=@pSbQ7kDJcgh3`QVUC`Wr?K$S z#ZS$Da{lS5s^^h^@VbD(paU2RfPkQm=(I4%c(Z%?*00wut+n1&*ZEDXj0QkcRZi*C zdMTU67{=<=^OMg_J<(XGG{S07^+T`h0V2NkRb+?=h9i@@PS(k`;QlDE5ea|&`?!^~&_=i_I?Wh|?02DMdJa`8tS4aNnz_5u; z92g7_WmWsg`@Z?ycYpeNyRsjp1pttb2IfET;fqUO`OM13t*slI8?DT4#=`4K;z-?N z!d_TlE-ZO9f2vdomS-NDo10x;2=tGy;T`Wb+Jm$V20*MRK>1E0a$;&>NlVxY!Vgh(G`%U)UjcM08*u>)HlR1~){x42WjfZU%_R-7ODM zo$g{xo$vuMqXQ7u4lXt$HM#e5NU^aF4mk{{yze~++sRN&Jp_cN= zAkJV_E(5tH0D}{2gbFbL5E2Wka(y5-6SBa?#;~VFMjvMh1d)N$j&q_QmjPjLY>X?| z84DrGU=!ay8ZN+q z05F_baB&|1SR6RATcm z-lV{ZhmHNU-7t$yaRzMM@!BuF^~&`c1;jGkq(sCCp`kX@AN&Ek)G}#aCe+6WQxtIZ zXRS(bdTh!+9MFrP&#rrO5e1*hWboRpumA4VtKRkO2AX;QPx6gR2r;Y9@L9~CUH;tC z#}=NPXoQM-M`MkeSaefwW^{L9a%BKPWN%_+AW3auXJt}lVPtu6$z?nM O0000lU>Ra)W@TbvWnippU~Xk#@M)X6G6MsHYKdz^NlIc#s#R`& dN@iLmgOPzDk_KZ-D`TjJ8(zyyCx@~c0szx67UBQ^ delta 87 zcmZqUZsXp-#p-RMYiOWrWE5g(YGrI@WoW8vU~Xk#aPje{$qWn(swJ)wB`Jv|saCo9 dDVb@N3`Pcq$Qn$oOe`T9j+~roGdYyi5C9qG7=Zu) diff --git a/res/drawable-xhdpi/ic_pageindicator_current_folder.png b/res/drawable-xhdpi/ic_pageindicator_current_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..cd92e9f21bce7611ab1f8322ada44e9e78b4e0ec GIT binary patch literal 782 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ueoXKL{?^yL>WGgtNdSvKUBvfU(=jY&)Rw_dH!3Lp+YJownaA#!=*0{pNDjGO?A{ z!Y?RkWF#_mIxC1Z`5rjOzTs194SV1PKf^mZEo;S?INunj>}Cnpyg4^}5r zy!JCLPCxg3ZoTpI827U$KHK~&Ia!#<{r>M}nRQvliswxFg*VRptL3_`ZHKD%*-K^{ z)_k=!=z7B7G*v0wSuW+N`UB$)oFyf8Ec=+BAL!ka5vrLP?xG=_AZx*T?_~Fl+t&|B z6|mGW_cc1U)V^{|*&F#W@Y-6AfEQ<`h)gP0+Bciq+n6Y9k6}yvt~>o+b=0Q= z>mD^eyU;L)C0WLG-t*KL<_#=4?msiEcs8r9W7sj{h3Mt#cTxhA6RaN1x_xQq9j1ze zPfm(jDZFioZI84>Zz%s_|FZS}B$e++uQGA%UM9Bxw0O%ogYa2%zwi6Bu(a{J&X0vh z#pW>7vq+dK{MXGnIrW;-YDN17=LhQ>k|(k(`>U9duQSs+v2N0ow?V;6RG9M`Wfv@e z-*Yo5{kQD{-N|0F1Ve-7b#656Z!Fq)e2GXt!|F*tk1lx_!*E?tEq66{=FDfIwWrqv zZrQQyzr1Vl-)%NbCtgl1_{-|=1dMXk64!{5l*E!$tK_0oAjM#0U}&mqV4`ba8DeN= zWny4uV61CkZe?KbX`8w-iiX_$l+3hBG!4dn{bH@uda0yRj2YzWRzD=AMbN@Z|N h$xljE@XSq2PYp^mm44$rjF6*2UngD{iH>Usq literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_pageindicator_default.png b/res/drawable-xhdpi/ic_pageindicator_default.png index 8eb5eb08d4ebb5d56ed9c06dd5deda98098a9a91..38538dcf02948fa20da4adb7c92e89edd9e1cf9d 100644 GIT binary patch delta 69 zcmbQhIe~M72#b`du7QcJfn|uHnU#rwm4UIYfw`4|!KZEN%9Hh2#F3?qEv<~9(l@-8 KnNE&pF$4fZMG-6j delta 69 zcmbQhIe~M72#b`7uA!l>p=F4nnU#^Lm8r3=frXWUf%+-M%*lEz;y9$krq8@OIiAH3 E05R!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YPV z+ueoXKL{?^yL>WGgtNdSvKUBvfU(=jY&)RwA)YRdAs)xyPPO%8HWX;JuXbF}VSVAk z61NLMZv-zCwOlwD|a;TCNPRKmKRwn1v6}C`p|X#>3r6_FFR#Ab>G}!RDK|_ zgXvvE^-=dIZkLsF?h30|mmg3oV9R-3ej}h`eIr{%N7cSLOOyj|Z*!ZphEe>0o`JA~ z&x3~0iEkM78&#XooXG4lt%+s2xxtE!Fk7i!ccaccd{rdL) zp(joA86MB+u`Ra<^FRKk;aPlS-vhxJTHm;T9ljM}>&kR~`MTww@)#X=tS|mKwZwU8 zdKKrJ5691&^t6UQVAvrz!E+zOJ&rp}cD2iD)jO`+$gfXUTA;i3uGb;q2b!zio>}~! zZ}-Gc&Fy#p_q^r_jIsa0pPuPj&0bVrr6HX9^&T*wRZCnWN>UO_QmvAUQh^kMk%6J9 zu7QcJfn|uHnU#rwm4UIYfw`4|!KZEN$|xFg^HVa@D$z6;TUr@IHQex8W(w3G39=zL rKdq!Zu_%?nF(p4KRlzeiF+DXXH8G{K@MJ0|H86O(`njxgN@xNAJ4x>y literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_remove_launcher.png b/res/drawable-xhdpi/ic_remove_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..de058893abb8ea1d9b6d5c668593ed0d1d2c16d4 GIT binary patch literal 1724 zcmZ8ieK^x=AHSD1Z>N$va@dp6sEFn*vxAzv%sWe(+G^N1I(dtcab|gWme}!{5vD>e z#EDvz7?CxvmCD&UQwj&UVmsw{*;$^S^ZfNZ_kDf8pX>YiUZ3m!91;epLiLqIwAYkL#~@CdvQ9)OlS&A&soLNzR&=%~^a{w?f0DPnZfGY)HPi)1b00*dn2=#Jz0UMk9MrU0S#Hhy-g5x11{1a~C zADsZ$n(N_$4J;p|EVe(Wjtgl+sU&v<-emoC5E%C zReFBOA)g2zFRmo3h;Ef|t{k`u2a?}@)7HpJ2kbvn-V7k$U#h(Ce?YCgygc3h!J2(f zY6j6Wfl`-~lk-xsvLr}ZS=@=&>N7aXViP@e6z~X*C zJ{JGiQA(_=2dAd^W6;M3T(}Um5 zjtB_}d4gzF6)nh+NTkcRuJyWcHa?BK!I@b=Fe#E>4`oM3M|%Q)3mz|la%$~+T$s0Y zzCxj})PL61u2d>Z)$8OW?{S8JY;hO{>?kl^NeLP(PE1U2mYGV;uv`Z2U))pA_i9B} z;t)N8;g*lf>%7e_kV+G_!cp*y9(B<5`t|FCrXa;f5UF{p@pV^oQ&TN$wY^r6osIU` z&ECD8v#~{MVD|IqzwsLBR97M^hDvs-N<*OfwrSfjgc`NQSAq0>?5{ge4u@X8Dm>nX z6lEn}uxs-_!JA)0AKz!-$h3iVyQ&qa8ES()!U)x*Pm}kuq`&{31Cu#jl2NtUs%#Fg zGbJTuUoUsdLD7`FTz=_q6OfF=x<3nT(N&36>E?#EV2@Pnnig@n+`0aKx4?knys173 z{(P?Gn;&ePZ1lK0{z#d6vV5HBd^6wR7@9X&R`BSctyR2a6|z!seQ<^kyCTCmRCfm# z7=h8DA#c!)bPrOXn%Yf8!^0yZ2<%AdtXm6M^q^=1$UCN9cdAv{&^aNH`9{(qw{KnT1-a9#jG7Mq zbkV~Coyuqfmgu|eswq?CbL-K{s;V^mqeqv5{q#n$q%YflRtWp5JZpo9^{=jGEKje- zN-jJAlNRg_2r(8l=E$nqTbGZabk*SL;8uw^6-_vU<#!pSYL~#CJx0deoincP%i?~a zpT4UX`9eJKSXJ`0b}aIHY*Ll6FolH9i*h?F;K#M*`_|Dx^x$o#ZUw{L4pV5bz5U_M z6&j4Kl_#12b1z;5)JQJidL+(A%7+J_3c5li=k{whHa2Pl`RXe6W#f5~%|tJIbe=@$qpAm=lY|r#mLTM%FYM#%2tClji&p@-(_FjbBl52X;gH z%F#i)NIZ>ZpNVC|k(?Yn9DdIpyeFWo=qlrT#0we`_axGp8T!ew`$6@A%&k6{|AA?E62;3G*4_y%lIgXxpme6CGgGTT>*c3$xX?G)6}+Wl zu*PcXqqCpal&lSNa?^Xa77k_~g(ci5fk@5tNliq?2@`3I$}c%KXGdsO7mW&JAm4k~ zvEg_^^%;+BmnTJH8m)Tgo%RLRcFfnkB0nwVhu|>Alv1@7rI>&7By1FVpp6q;NeQ%& z1UfG4A{_!?g|W0X$Jm-%+6P+N;%sekRv2@P9S(!>BB~euk02_B78#!W{{l-K#`?d4 zV+X&v4+*~8JmEx4ctTRhMLNLKVnXRCkEoCcI*}eiOO6xMH-8b};p*$s>`YDn4Q6U5| zpjV|r@F0+4LLvw$Sr5`6Gqbhb-I;c9-rTy~_j_-5K7R9?B}79*LqkJDLqo$N;713h zv4kX2ILA3sNMaGw=zy=-rB`DN-(X~Edx#=XjM7Wcg0-yD|8A>jE-v}C*vMIYrmdr< zIK-DChKs!A8!&Kz36vIEz7HQQiO;kxcnc~20e;w$Z@|D2LWPq52&cB?8!&K!hlP3i z=JLNFhA;vsg9|Q{A%HNZu${%ieffQ#-+9uQMV;wg^_WB2q$9~33%G?(Tp2G>o`dz* z+C^}$x)?^rH z;dGuVOH4JKd8|pi0XAi8x6Bn`xK>2VuVQB&TYL$IuTrdmF=pN`x5ejY)`d~A2I%d| ztZl}5ig8!$W|4m3b+$P}45zp?868xp&yKJrDb|QM1JhKfm?Nw+6zilo14~q>up_Lk z6l+490s3V-QeQxjV%-*JAVq~LbA;7Hu^x&ua88ACIl}6uSkF{9kXF459jiWwlj004 z3YkXRDAr|p3)z$-tZ|ApE-#gEV8%uXzRF7_d`#k1Z=17$;q;5lNB5{;bG8{5D8?PJ z`++F4p}A{|uY##?M8z5iFfaU-qv@f6d!l#6t4zozmbp3^t~cVV63v{_Y|vtX>=(n; zD5Em8&V=31|NIet92VgDUlyd;(&9mWCtj%K$WL1JP4uR1@OxfXiVu`r;%g?+rGZr5%tVn>w6ipVIl zh-Eb%$t!sbk%@}(N}h>X#2VRR$L?R9d+z=2_uccId(Qpirk!$kR*=<{1pp{uT`+hN z8tkX^K~YWLeRy6362bOv_5jpo%6%qCinv%5-q`_E4{9xmh<1<*-VJ~_H~tj-6<;y=pXC9B|ip_FqF!59ZIrueOesxvVOIHrm$;s(fsi(4Q^suZ_%0x^t3c6a$huq zeVnnjb271F<>CQ)GJLdOCUJ`R!CP!lVR543b-%6w==D7cN?$DLF&K=?;u->#nj`X% z+P2kz(8lpIpsu~0f6*hqPj5>sy>t!+CJEWDY42%#wS)seCLsuWH%d(mWM*W*1u?hP z)z#ZCe%@ZE53HDNJnwZpU3xV4>2h~>_emTlE41GCYQBfsny2;sD;padk{~6|G~iO_ z09hJF4ouSk>i^rgkcz+&OGPD*jbarq>1Fi@pfuZ@)~$@vU<`H6Y??53{E$XI6I zl{k0dYZ>MP*7GQTju=-bSk2+FC+ds#pX+v=Fv!dS~h);f-+2M)Z{QX^uWL zB7sIi1x1oX0FEJ$7RCq*W2B82(h`M0pb%EZ2rE&uKs;XYmEk;<7)p-)%3y)AK%x+4 z{~OST@^vD^H~Skrsbm_R5J>{|MCv(`HugLrn1m-0h_M&?Nc*1?U>)5t)eeD4{{mc3 BiT(fp literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_widget_resize_handle.png b/res/drawable-xhdpi/ic_widget_resize_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..f445a1c089691eff69eed563faff0361f741ca94 GIT binary patch literal 2144 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 RCwKi6YD!v)l?s*-{|Bkm`r`lq diff --git a/res/drawable-xhdpi/overscroll_glow_left.9.png b/res/drawable-xhdpi/overscroll_glow_left.9.png deleted file mode 100644 index 4f248f70b32ee7b24942abe127fb55ca44b55372..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1249 zcmV<71Rnc|P)NENYExv?F`N&ay=)3j4ciD{ZB zP%0S=8uS2#08A;_1}UXn`S1ckx$!))t*vP9mq8AU(2x^h^QmTL!a{ zO>g~P-&^~9$r!!4+ycNax(Mz9Pbp2n3HTQH3i$3;UIAKBQe4YDyA`{4-EaIM> zciy6Z)NdryI?uLNUeZa|R>gjQ_#xDVLTJAm5 zG);Fnvoh*y7O;NKa%Ql;LVG!5+Y@ z4qF4}WyY)WZ2uSYC*WTU$}2l*6O<#MO8!Bd-_bMw(BBi4 z7N$P7@>BBH+mGGT0P3J!&;aV7UC;pPpk2@a>Y!cF0P3J!&;aV7UC;pPpk2@a>Y!cF z0P3J!&;aV7UC@dCS4}44Jnyp6kM43g@6yS3z0ZzIyPK?M_S#>|=8>A`{ob>FOMX%M znq5}jrWXSJ#qf*cS3i@l*^DA>_59%YMd>9!3*8-gyQd~>Aj)!5TsD{kJE*cgaz|i2 zAXZ0mJ-uB`53KfY0X3QcYeKBeANjm?Q^+GgZCPMmJ62rL0x$Ka2B@>L)$@ShU*had z&_JDUo%?WWOaoDFq|T%>XDd&gW!cfF9G!c>_A-z)q25GG0G2jJOM6*Q@6@;SS}ys~ z@FT$?6A9wnYtJf69_{*y7G6I8UB&W|ETFyz{$#x`u;ms(1H2Znmdw(5nbtcrAU$B} zm7WLmsPwfNuokG^L?5^Z*g@G%$}aOsl@az*e`S5ulecehKZn-b$hZzlJ}Dm-*H>qg z^joNB;0xd#@B{Fc<<*qPiImSu)|)xKOsV~Q0QBmW09y;}s4Uy*(YN@oiMO@?+8)Oq zRmYnH92+eM@Luj)y@h%O{sw*p{sI1Cc{Sx|)s~JNUefChSoUGZ3bw7fo`n_JSPQGB z{?>8m_G`x^vi0mNdrObjM<1?OX>K2&IP>X6KmEh1hID|B)@kwA>!+p-&!$ygvMbf= zjkPzPUi;8b!xvT3_@vJ^w1?QA65|Ve&Ice(w~wUlVW0>7e`)#;WIB^P9k)*Y00000 LNkvXXu0mjfh5lzzT!} zthL4lYf$UOxP5PZI5W1=PK8Uhy#hE=@T`XmRer9H_WUfZ$1C@*`&;94PWej_wF0;S za9wLXmQsF4yHo>C(UEP7qx16B?{i31XSQ7AFUr3!zge9(&e`#S(0lftZ6@EdQ8)s) z0`LvM8vtJbd^wRdj;QAL?G41vX%Odo<%+o2PEB%DzO1m@C2ZL~Q;z$QttYB4(=Q&^ zo%!v}Q#2phh3e&bD43$@cL1LN`~>h0z_(iKbtwgssI^w^Sd)8^-U+*d(eF-nFnX0% z)i`~O3&lP($-NHNVNbWLT8BBBPb}(%ZFpbzg4>71%Ouo z-eZciny3NzA5t|}A<383A2AJ{&4g3zH zy>U~rp`}%1j3QQpC3MA~<*K7+aGR`e=7-X$JRYNG0G{A>VRwo89KdHxp*UK_ZN=^> z&qfeHM`SO;c(*Ag@A2fvE`UbN>r?tC`Q*r{u1q9U`D^q%rfVXu`W;1F6`56}_3R+h znFwvmHQi$Xk8peE?Gp79Zl4(xNx8v$(s`uz23VD!LkzY*IQkw@djva|>^)NPIC6Q; z=Q6!VheiZtr}R;v3LoHya$q9p6{g6PSy`D5t&u#Tig+0UN0Cwyb}a#W z&$hig%iIVb=`-WGM8c#irI{z{am#X067_$9edar-&n0q=zx(LN=CSLW3=I5z45AKf z0)wano4_FIz$P$=IC2H)YqcHJP~Rj)k%3}&nNFINi{%B6qeR+bi&`$M}DG+ zsQTqCkRBQB!nZO@SdUnlh{q)3d?ckMTaS<|+o`>F=6Lq$DYfdnmB<(1Pmr2$kBE_E z;#s22_1h!1YHu$xM?_`9Mug;usR*bds5gg7k26Rl^^DJ$Uh+HQbh1nmwY{cVYrP2? zl39IG$2k&2WxYti6VGM^uphJAjV>UIKUv z;0H4AOtp^yn72{wc}w+Pkk_zYC+6n2fn%F&xo84haQZ>*(2fX*fF7uDSDQ z;n{j(U0KFh1*Z2U}?;B=9!AMkm>k9O0(qNJ1#+CB-S7`+tBygJ~sfj!m2w zHu0OVFMPgYQwf}$*sPNj$z93s8WYJ(<4h&3N0MagMPQ3fLrzYFBJ2uTIFN1QAQd&vj zpCq>=544IYb|kOiWnsq<;H~kj9#_$49Z^+2szCjTd$_B0M|h}HWW~pIy;Ti7zzvN< zZH>HF@d4hIoHJy9u|BnT#HvR_RyMdV{E8nX-K;&9ATT4V!ysv6p;tmHIR5HWJM9^Mjl22w{cFfD?2d9 z9ejgp!j5E9T4`fqjT)Fm`(pW;aftia6aJ9Rbu|L7;lt7N|3-Rjq!-V8 z{;iRQGk8U@r`HL+Ea^-f)dJou+st)S!bRdX0!1k1dc7>`wX%1Xw{lAi1g11ZVI#bW z>#|3RESpI6K6o4^bZa_H*bo|HO6N-CBgMAxk#U&tkJFf$&1&K0bYHp!%J0pt?ZBy0Gx{5fT0Z XQXy)|n;9Td00000NkvXXu0mjf-CvIY diff --git a/res/drawable-xhdpi/portal_container_holo.9.png b/res/drawable-xhdpi/portal_container_holo.9.png deleted file mode 100644 index cedbdc401e981a926f8767e8526266d2701c3a44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2130 zcmV-Y2(9;tP)*Yx6Jh|HCAb z4ZYXauIRIEeh}&(h5V?9v95a}%jao4m%}yK#PT^$A7hx7Dzs7~ZQ4OM5hZ<=^;$cv zA1kT>#%P1H5fe((v=kLWltzNAi!ALj4f0&K5kg1Bat+F7-!ntpMk9rv4@AAKt?YLU zE=7fqRDn(uwUO6DVwjI8O+9oBM7?Z7#6_YH{%rNRHlkc4aYCq!mHLWkFZAp#qnpjpt%SiFej#h5m~$~Csli%Ge-G{ zlH|lnSuy=o*tsMuHvPF=F$6YOJ4aScblx}TeO6y#*Uy#yRcJ)XIAT+hMog8J8nLq= zDlz>^7vu4W$0e>l`FtX3%>8`9Tn1&0kQhJ>K>Xy%ldo>yzP)kd#*Oc4wOYMWsVuQR zYys4U+-(MfL8sU2wGR&ucV4`B@!P$7_nOG6hzKNmh;X`Q4d$^px2Z|0h!YkAsNK19 zXZ_*Bhd*Awe*N28tyYhsi2llhf`?R(_lTk>u2!p8*4NiRZ!{X8?(OaU`s&rI9vi%E zeWk6(GHSC0RY}koF{WGqP<{OP@z;0n-u?dS)vMp2M0o%H{qNh`+dtpGfBz4pajJ_{ zJv~$M*d_l!VC8T&vcVRgNL5t;)y>V#jkUG4TT>a`6tT9pc58EUa|6asWW6C*ytEOe z&2~d#kSb~cz#@R9Mx(J6$MN!H!Sb)RgCN`sf|$$=9;2hL7Xe9hV-wr*zfn3rZldp!`QZH4zimoijUE%Dwpd~ zslB$yI8}Do;*&;BA=jf)s*3(H3+5<0$i^Nk$pz&%9_rUJKG%~APWhrsf2Fd6t4Z?^ zKp*!IA@6#c%gI<(cGQq6rHWBd=P%F{+^vv`(iWg0?|Ws3D`?wPlsR{OA7zVC{2*nO z9aW^OZ*Ds&N&8lI452G?t&Xd!?C`C^r?xVe?k3e$c5qweID9S$AY2W;SXFl9iA~lH zw4ZLM)E1tcpx_7n$FSM0jS*1Bx7*!Lr&D+ovqGoS zLA%}VgV~C(WVJEmS($5YH~=ts_wLC->=_xJmSMl(Yk931p_c6KP+j+Dg@P9Ts&QL<5x3-0dj9yFWH!{g)Q(x~eA z_&91dn}@r*y9X%da+!1Ohd?bIddbu+o4OUG86uyU^2)Vq*VZ3Ac<{r`n>Rn(+S;nr z>-A)Hb@ii0`ID2A=;-JudiLzu;N{Dge?5Bi=%-ezbp+r9Y5wNlru=|ZVgDc{v4in^ z^o-K9pTjklc|rqM)lkN*R_kPYd;6FD{rxZAym|9wykDefs?Q z^S#%vU+=eCtrLv98n>S>n4JOL_R})uHP(<4L|JN!rd&34q^wm_u9$jK9w~7Fqg(~* zAISHeJs%X`nQ)F#wr>X`rSy<0rwiZ|BPI3B^*(?zQ}#^Xls85F4+*m+SR`fuGA?O> zodRWKO6oU3Hrs`g;$3Ngk~D2K&21o8;c3x*rR0HJ+f?LzrSZ8Q-;@LMzyu@a&oIh2 zqJ1SbQ&%x`jI#I%K;KmlsDcrbsbYj<(Vm6#A&1r;Y!)D&2|w+TOvpyq$Hko zOY-F@HhlynmIl}(7rmS)P8bp6S{4MY(p|2{jpH$ah~T6FBUMQyQEcL~Y0+PqwaJ8B zkncz!qRx1MLP{77!7V5IOzZ zwo>?9&S!^BS=ypl2L9Yj10XE*nJb3Z$r+FaOK!*s)OH8(dqN@IM8+&migZocE)o4X z;WE_r9>@o&ZJFS0TuQP%+vfIA>TRwll_t;Sy|(_n#@eoHWO2o4$?fvo_go)Q{F+pS zJa;K5m?WRUt~PSOE>djmCxLVsO=bMEtG(<~MX13(H}R=vS_Dbhh@vutAXohmMGOy( zYkA^dUWB-1o_1bLV(_%om=}drk+a<;E-^zym;bQTk0eR(FB%Rhpd(J~kpKVy07*qo IM6N<$g1Gn|&j0`b diff --git a/res/drawable-xhdpi/portal_ring_inner.png b/res/drawable-xhdpi/portal_ring_inner.png new file mode 100644 index 0000000000000000000000000000000000000000..7b6a8a04e8f0c6fb699543baa8e7872d7aacfd03 GIT binary patch literal 5983 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 CwunIh diff --git a/res/drawable-xhdpi/portal_ring_inner_nolip.png b/res/drawable-xhdpi/portal_ring_inner_nolip.png new file mode 100644 index 0000000000000000000000000000000000000000..4b84436ee50870821002db8a5bb7f380a88866cf GIT binary patch literal 5569 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 TH;Vc%aSv#y=qlGE?c@F*n&5(G diff --git a/res/drawable-xhdpi/portal_ring_outer.png b/res/drawable-xhdpi/portal_ring_outer.png new file mode 100644 index 0000000000000000000000000000000000000000..79c188873fbc6a6e72bddb9b4002df1dbaa96e55 GIT binary patch literal 10394 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 a?R4y{Z2jIM?f#1qL{oXK38|BZhW{T~m#(t_ literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/portal_ring_outer_holo.png b/res/drawable-xhdpi/portal_ring_outer_holo.png deleted file mode 100644 index f32051dcf63cf9fd9db1c1b29ea27e276247a673..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11037 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�FzX!ath1i|5+QEPK3ZZhWFCdt{j40kKKu4enYQ|w9^wt@wx`vwZ|gy{6Hbgoy>Jl&(}lcX9^KWe7}qN^)sx`TlS+5A;uRX$uCA^| z(fljTrlmyE<-6v_9#L#j7QR@*388gHMiWJE7E=xw+u7L-d)0UEucZj9zSs-vR%7jJ zrwD>r`w);P(1G+4D27%-vDDDT-M`IGNZO%-_Ow~+4nOeWU}Kv}>33dX6CtWLyCd`E z8XqRX?#k&I#G?i8YdlhA=ioIhatNhldUpkH{vb|Hw)QOt`n)Euuk$AL8;-YVbueBL zm5@$!enLHz81TQws2D`C1jY|qn{8@Z=7)ueo$n~L#V_3G$OpE$hS>)vr!4oje% zRC~w1c~Rw!m363gh!GKpd$k=*n61hCWx3=go)*UwKyO^NIT05C6+7z|C^F~Pl8Hpg zH5oAvk8Ggs}_f9w5+c&tIWu0uKVBvuF@YS#x6#A+wca#TaqOIUfrmrBBJz52mV z@h_M6x`F8l!`nVObK5BH>J4yL2Y-b1vbOxJtbbjzWl}dj-G6kBfMQ!*Xg86&*m>c= zBgD^|&6V4G(aQP8Dmh;8Ue{={(8`(e)2HADYtm%Qo&@k({W`qeGucJ^-YOZAX`bHF z%YCnot;`kp{c6Wl@V605bW7AGYrQM|?{>5UM-j%_kbd1?u$cc|t-ay7!T;m)bKw`J z`0S8=4x6(;`>9C=A}s@5oC49X`vGVg0sd9Ec0*3#hTOl`%&sZJ6qI3#*W_ruf&xU1 z$^L&Rc>B6|xQ6~O1;l^T@OA8&r)lv2-2&z78W`*pfCe;Ne4WuE`rb}%Xj8P4OQ`=K R8cv%8=p&7^>ogss{|lE9@yGxG literal 3939 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 Z>tyf$-vU&Whclo6pstKoszF)4_#YBjSxNu^ diff --git a/res/drawable-xhdpi/quantum_panel.9.png b/res/drawable-xhdpi/quantum_panel.9.png deleted file mode 100644 index 1797ad500af021be256feead9fb50128fc8af05c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 928 zcmeAS@N?(olHy`uVBq!ia0vp^o*>M@1|$oMx$giemSQK*5Dp-y;YjHK@;M7UB8wRq zxI00Z(fs7;wG0f*PM$7~Ar*7p&N-iT+d<&i{;6*`cB@VZzIRaIh42rd7aO;%zg+w- z_g!84J?6$EOY8ggvi#!NdDEp(D!r_Fg4!*KTkF!6dMx_fd{FS%>Wj-~Ud<{uJ7&qA z%rlSkdO`JC>vu7i^|ZSdX>=J0+>%PSxqaX@zlqh3%vVWg&fTvSIQdqlTk`17Pa18E z6BY<{^zS~fNa*C$<2@d0W>}xw@5``cp6Uvg*Gyu^m=w;h`ChBLWa1NU6Rq5ssQE6p zrlm31Jje{$%D4QoT!T0RpPs({{U?=mt}fT2Uhgtd+Eh6&UU&NGat1yx`R@vEm{iJd zzb|%U*!21H=btrp|5JKARsFWK7WYj%{q*+Z$B)l5)Gh4Ldbhc0V%HJPt?UWA;?|#6 zn|x9`TeoAy=a;O>JeN#dC#l@XF|+^n{d+v~4p&8yh08n)YbQBg4D8>eWBsbz!NHK- z#f70k=X75{OgdMhz2xT^X%mkRik*Vfk!(zgJl)vF$51M!!iAa@hY%Y5uZ5liV7eRYUAWtGv?ncl7H|WbRmU zH?lG6?2&KkyYp|>9-S|_!neocUx~~8&QF^=J%T2Rh-!AQ9QK&*chQ5(!If*l>76eQ zE|@6N(1izm$?pM6I|wWBCSKAzb$rIrlmFXps4iXj@y+YJnxO2pdwX-1o37bvE-;1R z*O4W+7q1OltslgGD{X63t@{(FtTi5D4Y4|6@|_vFIjc=OGWJI3*e%eC6zf!3JMHPt zk1Pj%JbI*bKW}?{`sSMtORQ{7j`g&@HDeNddj0zK`+olZ_vg=<^M|FNebI`Lkkh)c zEDsi~;5i_2;PfT66mdKI;Vst0OeAnA^-pY diff --git a/res/drawable-xhdpi/quantum_panel_bitmap.9.png b/res/drawable-xhdpi/quantum_panel_bitmap.9.png new file mode 100644 index 0000000000000000000000000000000000000000..13bdf09ec6e6e8120d0347eaf8b71c25d8b8682d GIT binary patch literal 843 zcmV-R1GM~!P)jWUjDbD3ssLes`P z-Rd-&HYjydssAM?6G<}1ku+InBu$e7NwcLPa~wbqKtBkAcVQSl3nBJWN>yhQLhQpZ zd=7%(5kOySG=q@1DOu4lQ;`GcEEbEm!)6?_b8==5CTe7z2=_>V+@>gc%FydZuf0C9DV>0AvQ@y3FYPWV;mXh{6Q%N zDP`>j0l?J4Ie$RGWSD1?p6mhWFvjkw2&p*&tH66rSFN!1sz?(1*voLg#!Hd)NR!pYfjUBK}JjlAB!Q^T!GHJcI zL0C1GnN-*fq^s4Whw8twXdGqI_2y|!x7%5 zCKF{OzVCmQQf{i3_%fuF8{hZ8;Ix(}0k`yV*lC{6=f9&U`aT+s?goRw9b>Fpz2x!; zA=b;~(x1&{AE(pl4*(kgA>s;uC>mZQId$a&o=;8 zy8QvLMQk*IZC6X_3nBnJ#CMA67pU`11dTt^w9^{*$Vpe5Nh;-va5Sj&W7^rR11dEm zU7M@1|$oMx$giemSQK*5Dp-y;YjHK@;M7UB8wRq zxI00Z(fs7;wG0f*3Z5>GAr*7p&bH6L>>$xTf3xhPkT)JZb9Xa1HO-x>&%zzVeyvVE zZG{zsxA-(a4v~-US(*zobL5+APafrcRFi(3Z%=j!wN&pp-Fpsb2Dgi+4Q=UZfc@ zx$Tlz<~ePhAmi`tW@eLDNnD6YNq)&Si{HtYDZqKwhTJc^JnwBuytE+9boREAPEWos zKYO+scX;wakPA=wZW{@eNiqVHWnTRL{bH_z(n&@`!l=>A=T@u!+a?n)6-$AnOL{H= zvifK3)UIapb0<9e{4n~_q}i@Zd(V98RoQ(g^m*jBTbt*fJNfy|j9K@V)wV2)W)yiI zzuY~eR`5c&`-|?+`j?fNyr(>7U|qJM#N>D3$Kb=W3$1nvAN_duK%KK@L*%vynfm#4 zRo~uzKF)tYKfy0j=*E@}pIuMbGYXxmI%4~v?5f%UU9-r?Un4tpYH85Pd~z3W3kWneLkyR&kDc(tS;F} zS#v#$g!7s6wK9h``m4<>*`1ae)R(T(%6Di-66En1oX>iIojdWE2t`Z?R=+}i&MNSd6xzxX!OL(%GGYk(Pu!PC{xWt~$( F695)PkzW7+ diff --git a/res/drawable-xhdpi/quantum_panel_dark_bitmap.9.png b/res/drawable-xhdpi/quantum_panel_dark_bitmap.9.png new file mode 100644 index 0000000000000000000000000000000000000000..ac2e423f26f29a35a1c7b223e2da117be7836db0 GIT binary patch literal 842 zcmV-Q1GW5#P)`6pHRA_Xb`tmY zd_LFrI`)aEDk4HnXk86iY74BV3smh6lC#|;nzNl@P|}cWSf}5+Yt&n(W_CX7S~soFTb5lzXG?!Sxuj-TGU-v z-E=&joQTLsjUh4y)w<{W?GO>dAV7j(I37<m+T6?WLM}VyFxG76?)08&`WlOUa~9nl3k&f z>aXZS(YUE^)&t`Y0gDOV7x1?oy&T&1L6 zY0*?B-NAPil6yi_@% literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/widget_resize_frame.9.png b/res/drawable-xhdpi/widget_resize_frame.9.png new file mode 100644 index 0000000000000000000000000000000000000000..bf7cc6b82e233deb9b6146a4b880a8ec636d206f GIT binary patch literal 494 zcmeAS@N?(olHy`uVBq!ia0vp^g&@qq1|%QV`CI@}EX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYZ(|ACwRIzhE&XXd)LtKumOWZ;O<9pU(#A5cR$HFJCZ8d{c zPG6k9SEcA#RQ4H5_cK)m_TLZ4+_tiaqB>SKtWf{} literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/widget_resize_frame_holo.9.png b/res/drawable-xhdpi/widget_resize_frame_holo.9.png deleted file mode 100644 index 76cec606df66e660e2a9a2d3cd3048e8d168ae31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1323 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 SQm$u21>ol7fv|10E57V(^DOqb`UbdGZ zylh3U5ngjFErgK?y-3B2M0nox=lkQGd%pK`?)Tj9x&Pg3PL8&c@FQ>lfFzNCcM%XT zkp2M?LFcurWCiNw)X`T z`0*$JK2iX{l>s0hS?oJtBiI$WWN(WH`#+2MsIEldh(!@D#sVOI=%*l{qDnzR)q#`cVK_3f}p@uOX9w3(My zA)+=BhR^m!a)x`%<1Kjk?mV;3^;!G)m7WFciTVZz8mI&ELpByk1r`_XZis+c*|K>H z1b!eQo+)C?d)zqq1Y#%?arSNWj@cOb&>jtT4`YoAy|U1D(<<^g!_p*pLeHop5p$L;yejnn!soO zvC@CMU)-;vQ#e&RwVu*!in_{7DYMhF{ci585ZG3O3QQN~L-@-6NqI9B)1yt(t5wqt z4YR0l2S0;KD9^NopaQSSu zW_2v2^~x{??Qf<*i+aRNIQ?3hM36px$Tfg0KzKY6)uLc>@6 z8vhnO5*pK&{KF&0lQuIp*GDGU z6Wk)cNd1_Dn%UZwhfP1c@0du9O2;wJN65RRv9cGj7mx$2WKG`~c#>GBbdnWY5!1WV zA345(2h9w+D|FvY=A>KA?F~Wt^N|lDPj|bfk&-?X!UeD+C{P*N~9dngKtDyk^aul*N zRYGTp3}uj#KHIqGk_3ow2M-4T>VCj%1?`Z`uz0GgBjAr{h$I7nPNsSQkfsYj=0yNL zZ%Zig6CnT=X#iN40H6}f34G!r*+mAsyE=pIZ^iAnS0r&zOtN1*02rljf`MD5s*-a8 zinD`H#N5g)k5~;7Hm7`g=i|Z^Y!Ms5fVt^N>A-Ru+L%H>p0(0^zlGqJiecm{-tYDD z*jfl*1qZa^pr(}z6GH9f(3zZ1F)wV)hVRsvca#U5OMY~fTX}q=Fs%K|Tx#v3_L?^r zMAs?{9B#*7%;QjfhW&ZJnfc%(J


g7<)`GhH*%Y}?YM8zS4N zoyyQ=y(6n?Vf5DeJPr|6z5wqN-7t`8pJ{1cV%_LR@|r!N0tIC*f4h9Lvk4>6Vp1Iz zfsxQtSQk(|=bk7Y`ZCg1#a*s$+bKhL#QTo@W5AS8!KA=iA*lt~DI_1RE>NZ^rBrcO zyr6!CT%#hWH`w93^77|VsGx3X0ZG_o3;0s~tPNcDz098k$<{G7+;ALhO2N19yj1Ar zCwbcF-m^R2(uIk_sq2UC!vo=gnlX)6ABLzr<>;W$gXEmGHaEEl!r8Z0LJyi8iu-&} zjfrlRn{^77m7$l$8)HP)+C-~)!?2+Ps{2{tEE9I?!KgOrbX2;$Ijc*rMA5L!C+F1~ zZxY>;g?wueC2*rO-v_a^W!g29vqV)=jzQdrF0T6{f{7DfpKcql^piH$Y&foz0xBt- zO7E-IcVwa7d^|BEEgE=g!x|Zomu+m-oK;hWDdPeTfla%(AXVQiW{bj$D1WG~b}nX6 zR~}Bp;_Mk=)wP-?n)$!5$3Otj_v;Ck4#GVLM!oBsX9D{G9w3ki9YJSR($taagdYpy zcGJoDo56I3@d?(v{`%23-@AmX(8+LL(t1N$sH++Lls@`R12oW?UcuBPxtMW@u^BZ%Bc)3>4X+yV( z!*57WG&m{PW!1s^CPNM7pv%kY#n&{J&{PAl%s?gn_E^qd)(3^K!_K^!=Jj?Nn=F_q zzj>(vK~&5hv10BPE2pSbS1g}A_4L#`)7AXKmp-;hjs!g$TK!^I;KLg2f)ZCZB@8+8 zqb#){)n&6H?7==JX1XFBGwaGbd778Jc5@t{GNfpn#ja7@<@9dyJDqb&4}W48Bq}Pp4ft-OupW_;~=P^3JliSFNLjTCu?$(&{NAi-A$^1-c=0HBbLIrAN9S^om6igii= diff --git a/res/drawable-xhdpi/widget_resize_handle_top.png b/res/drawable-xhdpi/widget_resize_handle_top.png deleted file mode 100644 index e866c008c2bce289d64dcae2cbba9977c9d03ed6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1463 zcmZ`(dpOez82*ihrZuJJ9=RV`%}QbjZ8mnyC6{#^F>}vla*aCXG%+2UAxTBVahc|F zl3b?cBuZg&&16y@Mm5#pG|^dqoIlPV=lP!Zd%yR6zW4e5`xqWX92BAn0RRB%g2#Bt z*jxsTl7g&D4JMAtXj{0Wnd>@WMF(+(GRnnb4!)z1#pG%?JQ!7XjeQ zHyO+PBqRWcg8;y$1OU|H3IcnbWxEQ&1RMtV_PuWLY6@k}b}If<0sttheg_CBy`e5U z*WiM2^p1G-?iwLZ8@ng#L7C#dzT_MiJdH$ic)giVx(-!_(;k)W4IXk28pze9(Mip? zUAcQIXjw{L`6|jSm?q?Q*b|Fm!~AX(DmmHhM_jL$_{h?2fpjhB#Z_y?(UF z)Uy)9*lpzc@33WpH?}4z?T^#DR?E1(iXQ#~kQrsN?As{L*c5!gg^n=S@KCrjTj;LhEX3%#SVH2ypFkJGOPR zYn!=zxFrMLr1%vi6bJ-7=A{p5XgU(Ulxc^;1U#{6s$A2Q7)s0}rV=3l>frCBf+x4x z%ip6-myX6(oma13LuSpWal~!6C|I*@s4c9kpE^-5SCeUl{b(;r7ZLKN*`@&_U`++6 zZv(j?+)fH{xLa#Kot_wf6GW8LMOs^_UxJEMqpc$4+GxZA z!<+YCWcIK>j5odGfP^eWQ)D4F`E>CN*w0$4+#3}W7($qQCEeLK3w_-Cg9tIj^RxZB zdg&1qr=Xz*!u_74 zXyxnCNc6sO8V*k>?jl5_P_wl7W|F_P`}I2yJ9TP*V4Cx znsy?q)trk`ZSqh^C*Qufjr_n=uQhERQA?pl7Cu+a5GURfI-J1PTu2Jq=6XU>Da9tmyX^$(WiER1HE zK6fvm2eDiHf|5TED@Y6^_7b2FW}TsUa9|KIHKWQJ9B#0_eO5Ug@jPO9`J~4hN|Lv- zQXq{Z2AFaQsU__+Y%GVwOxZ{1o$W=j3;PT=5R%p+US3S-MpRpMER*UPgYeaW%B^*z z@xdZB5ww9xC9ROt-+@KpdDBHYjZ}EOe=W5t@n?@Zf&gp&S748;AB${xS#j(jM4GPu zSsre19wdBp@6fj$ZKE~wbVVOzw$C9-^~F7lj4oV)e@R9oXedgbBYsgFY}I^6jBag9 z%D_?H7sA`q9n^heb=qGWlye1v{ilni165P^u7=G+x(ZjX%#NP3DlNi=k-a_BL%`h* zks+oS*KusE>(XJa=iP*m<65h%WA7kGwkWD;$tpZB<)6e|D>WGcdc{j-#>h z?Qw+?u4=PA<3nJTg|P~!d>LNO+PJ-XeBI>kqr0oHTn2}07N=w^duB$7SW;pLIWg3R zayC>(019bw7=g4#ppJN3SlS>D+n_8ENGls8@)SK!<3E9zxRA)ObN?SOowBT!1ppT; K5yN!~`t=`e-iq}A diff --git a/res/drawable-xhdpi/widget_resize_shadow.9.png b/res/drawable-xhdpi/widget_resize_shadow.9.png new file mode 100644 index 0000000000000000000000000000000000000000..96dee7f0120a3e840c780c4e4ba0c54b40f8d8ff GIT binary patch literal 669 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 a%z$(D6;S04^M8M!0F3m^b<3}#BK`-HEQn?R diff --git a/res/drawable-xxhdpi/default_widget_preview_holo.9.png b/res/drawable-xxhdpi/default_widget_preview_holo.9.png deleted file mode 100644 index 0f62097a81c907d2211b047e12a0588afa3ffdff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1342 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%xRe+5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8$ZOie9eW?zP+vl9-pA3bQv8WG_UoBVN5$&PAz-CHX}m`T04p6cCV+Uy@&( zkzb(T9BiuKo0y!L2jYXG2jW|o)S}F?)D*X({9FZa_*!LRvES6##K^_m(8bac;%^HV zCnpy}Qzru_Gb0yAM++sW-V}1eOdseNeNf^+N-QuTU z3c=2&C#Pga&6wB`q1^rc@Q$w9pT!|hPrZBmHTbj5`u&0H{1@iN`>g%@syAY@x3$@h z1rCnwJ*E8qMrRguZuez1IxUbQ#xyfaN$AwYZI`yp3s}5X$w|_!Ws5?ya03I2!he<{ zxv60{F5T%)KX?4o_fy~FZEtU1Clk2sPhrw-J^#5|CqywYp5So&l`&6IiGh*BVUcHW zeD>w1_21M4f5qv0=zPoQ?46$WUkYp2 z2)|x0_$#XPXx`qqz00Ou>bs=3m~rREdAHx>$u`c}>zg&fs3x!R3UAk?jnlUY?woCN xtYF(BEdlE#+5ua4%J1@+rlVtOyThK5je+ZO(b>fkvT>ji&C}J-Wt~$(69ChM+WPc-e)t9@GtUD6e~fP)yhd$fT|JgACBkk<%|g zD?zN!S=k=clR7FJ&<^W?O}JpY7)=nZZ3S011_2`bK)>b0sIL#7P^4wXsN;N!Nx5+_ zV09Ed&|U1z8pT0FGO4!1WNSgj1U5iAS+Iv3UoOO`60eN+!8J{hB@;9lqn4fONu|j+ z^Z?0+nUKK<3@NZ-Hqs)B;z5#QScYaen&m>QC`UM%VaW1BVKvXp$r&|K*21S4HGq&S z({w(c59cFc==IaABuN1W$Az#($RBZ#UI;mUV}(HlzTsIevY-&pTPQHZ z-pE~^g^QvOsn8g!sPY`g3Q?AoM1^mUDx4r_QGw^B3dd^L zL{t(5iRV;RiLk83C{kQfR5c>9Tttj3S_PYQe55-DsOVZ)HxZ3$k#@CZO{^UEfR3P- zg>b0S0qFsVpg#azGTzOTDc!J~;2e}^Ia(EX))`(&@*KGd6{nl7Ih{c!){-j2Dc^rNd+;*W`?&+lfk zv!|+u_Ae!?Mw_z2s{_f=Yjxko8%~|QX}{hJ=NEp|zN^1c8yo-mU{CSepmo8)2%|XVCz$`1a))a<{OwMh_0Gd zuAoYBOfp=X)*~A_v^|U_h;%mebYli0vI?gxCqw;s@roiXGeceDi=gP`;j}fn=)=jy z@v^ZvV~8e|9VXMEgb8ejbTYJOoj?jRRFhZ2`{;u9)w_f#bSrp30jH5 zDPuz2V^|t6wjIUQv<^@S{&nL}>!7^gL8b%)cg{C(J!*q(FwWi4LXjc%M!M=-xG4Ip z;u>={bWlObQ1}mRTBf9Ga#9nL3eRz@kYZUeDf79M%n5>;5_n#0a~y`%a-5m~skp++ zBFm~k&Wkxs6BWR537M5U*n$%v-7#QW*TT9jEO#VU%KK19u3vWD*>(p^OuNVprd^NB zPx53@H!LSQN98$)R)M}X4^7Q?ZL*nP$vQ+|;B!2iimPm#1I@TuAd0HW^8lpsiIhgQ zu;%~djKR(@QFHv$EbS}Yfzh_p`uNZp9_ZkS@$qOpJwKx3vGNLvTn=}td;9y@F1oS3 zeecTJi6_ISy7!hJ-c}l0BXIfi-QATt4n45fy;A*^T{n;4+WoYo&)51lHnaQOv*#y` ztw-;FYHz=MTxx`g()SDR%x}Fz8)IFkORwQz`tkLit(~QTuNOb84Ui4u!~IJyna$tF OqM0tJyWMeO{yh diff --git a/res/drawable-xxhdpi/hotseat_track_holo.9.png b/res/drawable-xxhdpi/hotseat_track_holo.9.png deleted file mode 100644 index dd2216fb71935f88b30b8c3acb08f8578d973394..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1213 zcmbVMe`wrP98Xsmy3$c)S}Y!fRK!1emzP|VyM$gx?vh)B-nCxOc??UN=HB(wg0oI46@$(A8}+wpCaKnUwuvSX6}lB`9jA4#o!c4}H1bRsSga!$9)h z_r34u^ZlJeBiUV@+j_Q96xFE@sX4Ol2;PUblJ7f*LY-uJ0&4|4ib}XS8?EW-hcYA;#&0xo2plTBntj6f$+w_Sp!sDYhz*Dxy()01$@auUoRixdwfM+>A%*Es=F9Sd~ABI@-iX}Ox zrkl2ilwisjyE4nxYPCo$8bRI^%Sn#MAW{+BJ# zMJ+B0g4E*J44Y2!T0e*-RY8$BP6J9xN~Y723V6O>;gk)m?)cbnOxUt(5xY&S@<6Pd z@}Pl{mq%#2)d3@AgpprHE}a?`=!{`nP9O*6S&vqQp0yVi(;l+vMt)^$6Z@i&6u5X) z<7!k^7S9Sc;U+S{h(`g&x*U$e(9&$TIKT6u{&+mgQ-vS+Lzr&uC|p=ES~*F zo1DvbJaTjJ?Q;w6-h;13cmMviQ7SKUN4hRP{A}pV{jUmZZgSV%ccbIO;T^ZWUE2E0 zzVOkv&TpBbsqVqyM^6o%Yk&Cz=5+7#9Z&9idikzCR5>pG`P9if&cgS5}*G?K%*Ws6X-?(qggWS=H)s73>D+fZK-t0LL{to{-7FwLkKDJVx zx3}E;XzcphXD32EhpYVDt>O7&XyN_Wc>%KBk@HNZ zVx{@aYVHNIny)53`L5lp&TAlkY=VmCBnY~4XOo_G>yghj{DDs#Lp~{JRSHbC>F|G@{nd0Uj_Y$`rHIM~e2Pk?de@a`O_ALvu7?dn3(e`xSw&p zm3^;WNgfTB|ihv1@k;kgi&5>TkN+zluOVgV&Hq%X3<*c=z{|JSQzzF4Xu z@UYXx+^CboUuBoDr%Q#>#by_!L&`93sBHbtbZyUZH`mp%?|{L;;OXk;vd$@?2>|`B BbD#hK literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_home_all_apps_holo_dark.png b/res/drawable-xxhdpi/ic_home_all_apps_holo_dark.png deleted file mode 100644 index 0b8e88c169ab2a81f99f380214c3f4d5b46c04f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3300 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 Y?eAAbA9T!8m;HCRIJuLqIqo_1UtW84z5oCK diff --git a/res/drawable-xxhdpi/ic_home_google_logo_normal_holo.png b/res/drawable-xxhdpi/ic_home_google_logo_normal_holo.png deleted file mode 100644 index a47b2ba9902d90bd26425f1258891988cdba9e2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13630 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 TnFjj?00000NkvXXu0mjfye(IS diff --git a/res/drawable-xxhdpi/ic_home_search_pressed_holo.png b/res/drawable-xxhdpi/ic_home_search_pressed_holo.png deleted file mode 100644 index 800d99430c959de073a6e62cfa2a2740070c407a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7909 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 P{qd}aFof4?I>r4T#FseZ diff --git a/res/drawable-xxhdpi/ic_home_voice_search_pressed_holo.png b/res/drawable-xxhdpi/ic_home_voice_search_pressed_holo.png deleted file mode 100644 index 27a58978234f747008b5b310c028c31db6f0a1c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5445 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 ZU^W)kzOMZ?|2`-HN($=oRZ#Q){s#whJ=FjJ literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_launcher_clear_active_holo.png b/res/drawable-xxhdpi/ic_launcher_clear_active_holo.png deleted file mode 100644 index 95cf84115a7046473bb05f21b16081fe46c98d77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2357 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 j2QXu3HZR0bFoBV5!ga@c diff --git a/res/drawable-xxhdpi/ic_launcher_info_normal_holo.png b/res/drawable-xxhdpi/ic_launcher_info_normal_holo.png deleted file mode 100644 index 94f0955d64e2f00e14b39d693ec032c2414a3f87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7833 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 b5&)v7cXs(dbpHMG8KA19rC2R*5&C}sz1n$Z diff --git a/res/drawable-xxhdpi/ic_launcher_trashcan_active_holo.png b/res/drawable-xxhdpi/ic_launcher_trashcan_active_holo.png deleted file mode 100644 index 3bb098c2beb873772c8437b03e3b73298a9af93f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4493 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 diff --git a/res/drawable-xxhdpi/ic_launcher_trashcan_normal_holo.png b/res/drawable-xxhdpi/ic_launcher_trashcan_normal_holo.png deleted file mode 100644 index 550cc5bb826dda10cdf7ce551bbbc71f3d8b9da8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4522 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 we`t^oSk<2}Z5#BLh#>}$c1IrLYGbUO_Qmu0H dQ!>*k8H@}Jku(@vS{Xw%-0)gvI+=sR5CAdk7rg)g delta 87 zcmdlfuv1{eUUqL2T|)z1Bcl*QQ!8ULD??LV19K|_gNu(pO=e(VP%UwdC`m~yNwvz& dPsvQHWH2%?MAl$xWnu}@aOC7vo5>s;h5&ez8C(DW diff --git a/res/drawable-xxhdpi/ic_pageindicator_current_folder.png b/res/drawable-xxhdpi/ic_pageindicator_current_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..602b89a40cd86a8b4e5cf65f0e9f21ebf4e45b21 GIT binary patch literal 1023 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9>jA5L~c#`DCC7XMsm#F_88EW4Dvpb_@(m=RI8$|aEOG``3uO6Cr#rwjt;z=q;3VkN2Ff6obyg2#fWTuthdLQqw zSKxPYWJed5eojSFG-m`@+L?YV%< zL5|1hhuH^)8|k8x_%nR!_y3hIkgs_2QrDsNiZ`1LV_0ftP+&%R#7&jlYOUmO*YL1( z19p{roD%OR>^?9_$J^{6%c`7|MaiBeN@h&?kEcjo(5>a$p?;rvS@XuSkoQIf*IwvK zY`+&clldOw@*_`U6V|L)*Uw~ng{@ch{)BT+79H@}?Nd7Ml*@NkFPr!@9p-gOemCc- ze$5f-c>7(%f=MS}-_qX#S9deW9OLGc=SUE=NMJk9#IbSXi{gj{4ouk%+^?3M-rU_& z(EjGY{0cq~yRVCUX3lLlF?)WLbE(5_C!Gmyf2L)gzO{0;$D6=Ss?YED&6Sy8lJ|&j zO@_bh_A1+$AJG5R{D9R@-CON10ypWIab3U6H{s+RrqfvxA9c5%=xy8U}fi7AzZCsRQgk-^i|&t;ucLK6V2E1&ED literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_pageindicator_default.png b/res/drawable-xxhdpi/ic_pageindicator_default.png index e608cae30aa7b7098ecccca67234845a2fbb4cb9..3fa9e5fd75f438136bac0c67278ca8c46fa1d479 100644 GIT binary patch delta 68 zcmaFF^N44|Ggc{6T>}$c1IrLYGbNmD delta 68 zcmaFF^N44|Ggc`RT|+}%L(33DGbEpLun%A)6rp DgC!C= diff --git a/res/drawable-xxhdpi/ic_pageindicator_default_folder.png b/res/drawable-xxhdpi/ic_pageindicator_default_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..bbcd7f91e012be9331c78ae953a8c6833f5a41bb GIT binary patch literal 832 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+eS5K)hhiu0R{01Y44~ zy9>jA5L~c#`DCC7XMsm#F_88EW4Dvpb_@(m0-i38ArXh)UO(UG<0x|cqyKjw;f)(6 z&UnD9({W=%LSg#`=Qo{4q$4`EMpSSLWgkitc-Pb#+1U{;!I|W(^!NOtRm-${)m)YDX(qltOoXq;U! z`Tdo=-B)xP{QCSJ7Pe#_=bUZrVETBwS>FRrKR5a0=hEpD)T|oXBf5pRa*18)+Lm3> z(ySV&$@uCalOAWHR^!QqyAH8h$k(1_nUZ&$w|TYg${>mL{6FV#brcAnPd&Gsv1v0` zUa97Vjg}0u^TjXBTg9@XJO5*OV3Bl?)aKT6HVsxzmNP^%isc+uX4!XJoAL(e_f^Lk zJ-1j;x23XwJDcQ=klcp~*MCgVEl^YV(fr#f*GqB-%d*z@JG$>X+bw5G3TU=_Y*u%) z(~9lk3GoZYCtlbWa>w&*RSafK4YgeQhj-zl{bEiNyRw7gV&Wez3V6AF_vy-S&M$3h zg`_r@#)xjJ@R}2SD7Q*+&d&c*TMB2nbKmZIf1_KhjOEIqS${gc+>2sDL#_5KzS3}$c1IrLYGb3VtQ&&YGO)d;mK4`N@nnM^>bP0l+XkK%kD}p literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_qs_remote_display.png b/res/drawable-xxhdpi/ic_qs_remote_display.png deleted file mode 100644 index 25ea9fab22ff6046f4b09e4f63ebaff96ac6ef8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1715 zcmbVLYfuwc6uvwZONS~9B3dPDm=>ib`v`eLq9Gd)lvhBZV-=QUfmD)>o5chK%(Pks z-dYDlEXOtMp5tCKrnCP$H5LiG&1KYCOgfdvSO}T~N*$F;LJ0;UpVW|q zu6V;GG6n#jJbgwMla;hmrlkyg!mY!%8ceJ;05MCgCPKTOWT0F!UvHFiN6(()LVBH? zn<-2}l1wVHK)<|%CeurjGqfe^wNf2-sRD|z%2)sc$qppBuXOoU`%C;$3>|{2r5KGA}NZhF{Kp4B}z=F!ljr1 z!-XE(1f!WDj9Sv;SI_#Lw2gYDtxQFe1Vhmo6jkV9KxzTSQ04;41gX-6P!gfl8{KDj zdtPLVleB&#sf(j212j=znSKiYQZ{S>iVIN{hPd;Vs<9{>L&Q;R+OaCBh&ySk`#&{< z*C!?BV6`kVbZ5Xm&I{oPKZ@0N%eQ;K~f^t$T-*TdIiAi+u)X zP~fIp-3KeHf;ev-PT#h@@Zw0-h$ivo=|pGm^5G0!HUIdT;K1R%*PCuEI*M033J$y+ zv~T$NdgjKlSFc zh{2Sxw9Rc*otON;Pj&Nhx~~g4ft%(z?zcG-p2fC|EN-tIj2K&6d?Cp0>U_K9@8*Vu zuH!L$dw6@yo-NTC8;WhDPb9ZP;qi>0f)n(yg zeB9OP)A9+y+ZEqbxB);PKRVWD7+nVj(!Lpy``a9!0jR#M!|654ULWRIFu)AD%-KJ5 zl}Fa^&n|ixxya8pcjmLOv$iZ?9#gv#BlqyrAlqUWbLYHb{`u3T?VPm!$RFE=w+$ZJ z!io02x4&wI!r2u_Edgt8?7YOO5Qf+c4ZB@u!lw1usIYNS`zaM+cPdz`Ix3cpbvs9AfYET``=uW7?*US_1d^yI^JZ~U?bAitR% zdd=;cj@wu1>Vuq=fcsn3SDm)#F`xS6MJEqbBGJ98KA*V^G|WC#^HEv#&cp!RCp5x* ztl>oM2Vq~7yr0-T;*AW)tyFp#lG?B6r^vqoQkZ4s@tPf(mLcyP%ZiW%pkZ*c^Fnad nyYlhFB6a8#oU)y`2765h)r3L*T5sKMmS!M9os6G|&H3OTB#M4k diff --git a/res/drawable-xxhdpi/ic_qs_remote_display_connected.png b/res/drawable-xxhdpi/ic_qs_remote_display_connected.png deleted file mode 100644 index 33a8d2d5ad82db05182f6560c12c010ff40422f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1718 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 KF{GeeNdE?F=TmP0 literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_search_grey.png b/res/drawable-xxhdpi/ic_search_grey.png new file mode 100755 index 0000000000000000000000000000000000000000..1d5c91361c1cd6ca4c4cd0f8e55653e94858bf44 GIT binary patch literal 1473 zcmV;y1wQ(TP)TOVFn)54Nfv%JriIp)8Z8t87Ht(*q9GE6f~81| zVyRj`aI5g7_1lezpkPc96SYvQXu(}qB1Ei8s}@Y7F^IUA1cl;PG|8N1(M2iq&gAya zJNL{@^7{{-_s*SjW-hVwM>Wf6;WVA}($4_HBVN%*FP)qQ8hHz`OYmhP%Q?n<0yIWI z53MvZaqI+I$Vr}}d3r)S3raoE1lDjHEx;|B$(3fHGFI~dO+YVAr59*EzoQAd&b-nI zRK;P2(1Z3brCx23x^+0QRiF)*$=IjP4;{ck1{n-qZ;|DY)9pthWD91T8 zMQ6xmDNtD+pPqm_9Az_$nMpMjBdVFnVz$sq7Xdm-?O6(RV3cV(S#)FPuaixa#?W39?UpgFuiWBkZ_NS3Q(t&76VP-0qV3JNmvW&)I-i*pf!)Xn7IOu>*Bz zuoLK*bNZ=8Je%#Dh>q9^bRP}XiKO5v8mik$py|#tT#uw+BN}R$X*L2ab6#N8NDAI@ z9?()7fm}oX9V8`pT?>wlK&R1AN0F4Yp`ki#1nNXXZAMbE6%BRSMxb6a)M6wh^{%bn zMxcH))J!BLwP>ivHUbTxp{kLT)S#hW*a$R?hN?hPQi+Bd%0!^?K(EYRVlQn3>Kp%( z9>2|d85Fk^eNen)d(~(y`l``xBM@k)E+i%Q&`{fK1ZqS>1x!!rafdoI*a%cjfQH(D zq+pZtfTr3AB=n%6t{^G+)%`rmN}yKf8O}mH`^fqEJ!~gXBkFJh@$3}pu*A}f!za!c zhcgh5KHv=+s?XAkL!ljY_zCgoJnGPfSPHZNMN?ln8c?TCYy}c-p$-q3jCe4WN2tSf z#9E+c)ag9p!9~>R3wwcb^rB863EP1>-J#50AfX9$3iujH&^Ks=&#nGW`UdJW$T}n; z8yG^JE+bY0&0`RC3TW|uaVZ0QY&Vc_2#s`sDFxoSE~1fkB9;SH(v3!XSilOw$zix=>GUGaEsuLW`3~NJ1qC z8AKEIn*Ed`|Ii4#O$Bn8%QZAXSExh&MT?V2NYYAr&;)d|0{IV(u`3IKgd9!WL|&w>7KpU!R=gn}rB-jdmzt$9)b^L{`I8Ot3U$r6Qo_hBElT?iym zxPNcjeBhYaI2_%p}0tqCL1rtahfdmprAb|ujfdmpr bpdafC?m9)~Vz%P)00000NkvXXu0mjf^4^xC literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_uninstall_launcher.png b/res/drawable-xxhdpi/ic_uninstall_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..872e829338bd00f9ac96e540275461f16a0f40d2 GIT binary patch literal 2045 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 literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/overscroll_glow_left.9.png b/res/drawable-xxhdpi/overscroll_glow_left.9.png deleted file mode 100644 index 1a895cdc883c37566240308eed2ab1c9b4e47539..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4562 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 diff --git a/res/drawable-xxhdpi/overscroll_glow_right.9.png b/res/drawable-xxhdpi/overscroll_glow_right.9.png deleted file mode 100644 index 5766761452aef49c2ef1526521925101d3998187..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2738 zcmbVOd00~E8fT0gvmBKc8(Tyy%dYH#!bmEhX$E@HF_$a}2*?0s2}H|KN*!BmvY8fb z8MjQlQ{rSnR%2$S*DWzMv!*fBEX6H!9J$zb?;rEbANM@xobUVI^ZtJC{yZlul)A~r z+R+*S0N4Ztl4<63*8I3zS(@Meaq}hS)m1SR4Y4 z#C?n$;f2Pb5m>SZ3Pncyp&%cz%>O5AaI-V;x%T*4tmU1r`V~Qr`Wn-ZN&Y3 zHzr9}a|Uw{TJ;*x2@R&wh?EC|{T;^KH;TtSHCcTtxJ_G!#&q3p+V#N|`TjG6`437G z245bEsd{{i@@LDhV@f&2qzPNmI8~e)vIDK^x%UHF-@mp)At@Urzr7GWyJbYL_Ez=f zt7o*iMMpvri=FnJ=(mU)k(WaMG2J%Kz)jYkZJ$(cJKLiZk)LVz#wO{?YiyMMggQoZTlEy*AmoCcpbwS=c?_lfR(9k2W5M>UlL zzN$$JWK1;scIIW7LIX`NqjQNy>5A;Qz?1=helTdTa$!lk>$g0bdO59Ve~Bu1AM8l{ z<>X%&<8N0@RE0PB61%tedUIUV9giovz{6)0(nd=^=GuyHe?FXfn3J46nXf%k*Jq4v z8MiGMIZU|S;nY0c3Jt0|P~oqxsXO!h?xoH5aGpS2rHxHy0q*q1#m%k?i&*)xfm>1E z>1ZQ%skM`Sr2W~$j6-9FvuaK3qqFLis@!)D@alucDnn?Mb7JM{`oVKuMb(&(uOy}qo=?+U^%~_iPhtjR4ZuQJ|&B4cr7TTtpt|JAT zyFA=}anM5NO9n&R9NLQ){m?<6RSqV1uUoc^tOD{ZSMI3-30pOj&+Hd8Q3A@!4T~e) zffn;KYke@n!Hf5tJ(s`-f!^Pb9?USZTTKz(hX`qx3uP9{d5S`~jf$J)HsRTtTU@1T zsV&oAvtDIc?SPwxTpqr#I^XhM2+gr9QFC-_cDjA15S&&l+1S;a-E3zX^}PFFGdfhy zvh!t%f7AZ4Y2gZv^)ysds81R+Xl?H0a!}RHsM8r8?I^vPy^j_CuW}-}4x25RSVO}o>+OUC8{e-gu z)uzZ}b5d*{3KADJ%Bq|XfSQl#HM1!wxw@cf!EzI`|MZI6>4aB-G14;3UXiIgf4!a6 z(^Y9k#T_jub+r7Ag~FFJ;|sQ{u?1TA{rv_E%Ey6T0xd2Y<|%ybeC(`%hT)_gck-jA zw!Ql1LJhqr>$7~@bV?$VbdU}g`E*XE24fkeZ_udHeEB{&XNnRk-t!9&N}g9qVH&p zscTYvJT4`c!XdcA>LqpJe>N=mLgXbMcA&_{&I+9eBb#l{zZ+|^s z)e=oF2Kl}%x_k5b8lK<7Cy+wd@;E^@uhFGMXY390Iai52;9{TmtmKzsjN4DgE26ah zl_eMZ3LZyvkFj?bwcR}DG*G<&Bb1a;7tyL47QyzrQBCWEk76SALrTXpPnC|N@*Keh zWm|3Qx;}MYZT0VAUgad|nLQEim7P^%@^{ffeQcfXyTocM;)Qi<3N4ex}sK6MH%Wi|A&KbJqGbeNJQ_{woH-8U2e9H2uDvHsRce-UJ2HhK} zTk1C58okmb6)?)43a`v$UovTOophsZ(X$&SIxiVs6AHiImi|pLO>F!7()DQIu|B+; z!`ELgd)07Tg?x3jY8=!Q9NM(~*)^zdqN@@2vK`mg(`{qOuUESjZp?{dR~$18C0{Mc zEXxZ!Hl^pie)C$FFDS&lX$%?e$!@9_37r2d85(f1e$l&h;q%U051P(rG}KQntG=_E zC(B5ls(;t8pMP6{d-6IqwzRQ9o02`_RIh(zxRD`(hk#b+ZQRj?OR{}AcE0J4_+(Wy zAe3V4tf@XUlF42OtN@kH08OFPo%Am)q&240`O5+5iGE7vqt@MXf004{RI-W~m-Ziv C9=Uh` diff --git a/res/drawable-xxhdpi/portal_container_holo.9.png b/res/drawable-xxhdpi/portal_container_holo.9.png deleted file mode 100644 index 599a076d8b47c4145e051a21e4a7670bc64b988c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 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 E0Mv#YrT_o{ diff --git a/res/drawable-xxhdpi/portal_ring_inner.png b/res/drawable-xxhdpi/portal_ring_inner.png new file mode 100644 index 0000000000000000000000000000000000000000..d088a172d9082b70c2e90c07fa4369e0b8f02438 GIT binary patch literal 9918 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 literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/portal_ring_inner_holo.png b/res/drawable-xxhdpi/portal_ring_inner_holo.png deleted file mode 100644 index 8cd6a59309a0fb8575e7c62517f6913453a189b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10046 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 diff --git a/res/drawable-xxhdpi/portal_ring_inner_nolip_holo.png b/res/drawable-xxhdpi/portal_ring_inner_nolip.png similarity index 100% rename from res/drawable-xxhdpi/portal_ring_inner_nolip_holo.png rename to res/drawable-xxhdpi/portal_ring_inner_nolip.png diff --git a/res/drawable-xxhdpi/portal_ring_outer.png b/res/drawable-xxhdpi/portal_ring_outer.png new file mode 100644 index 0000000000000000000000000000000000000000..45ac0406c35d3a8be2c8bb16de59242c3a11c0d9 GIT binary patch literal 19200 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

X|O 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 diff --git a/res/drawable-xxhdpi/quantum_panel.9.png b/res/drawable-xxhdpi/quantum_panel.9.png deleted file mode 100644 index d7ba87416edac0ee70f1755216def1d24f224ccf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1439 zcma)+`#Tc~9LBfOa(R^Nwp<^~tP4s*$TE*?J4G(ZrR6rt^vKEWnB*SjGKEOUZ7%Vw zi%W!sBcjC^MdjAaorXm=%Z|=D&vW|me4o$feV^|?@RIFpG18Jsk^lfe8i%!V*zJIy zii38uW{Z1ix5R=iaTmpRFH+nmdH0w29qSql07#L43N-Fz&;WpaXq*-LVtBzyp?To6 zwM?Q%6t*@7YSMHf^OKQ7%Q>QGk5=fB?|g}o+5J>(3;-Pg*-w?4GK!6@s=KFPDPz1K z1d5}!L*#mG(ET^-bJE7f5A?~-0m0cmt~KLHbuF;2@g9>TM9P4}*vXmC6OuQXPTGlH z(EnI-N0KPChB@9IRu37ykb>XjFTn%z5cVXS-%{x#LK=8X{&FMdb_ebbDp#sRp>Bl0 z<$gj`(+H2-2{&EVg=P1uj_ajyULWJ{X~6NcYAkf$O$tQvV!hbuAP0a1cxLT8F&^Va zal?1-%%)0NV~J5BMdu>*GKCL>48nYNR|%Hknlef#217K8Ey!5jb{(|*8Am7-F45`q z+#gXQ$Jj_^9VU;xxw-j%S7QVNXG6dTJl2b|#`n+*4;sj2n^_&ShzJV{Yier~IX6-X_4W0c zuV23&L~Oq}1m14(WK%(!3IV--NWl?YCgj`E(o)1--#oh)eR6M3u(fV?&2Vhz>MskkF`l)J0 zpVlM@Q%zcqC;|)p4KaeWv(DvYR@cz^#S=uye<)FRQA&A+bxg+nOONST(6GrdmV)>H z<(q6C^3H?5!A>p?I}J}1T^09YDQFsMpUus;reJT6a`X+7pCo8R=~M;Z&?~@PuXr<9=3~G&w^O5CGs^#wT3Oi^@IIueeO;L|9)5a9QI%+*uBNur9b`}A4guiY zsi~>l?d|O|ch-rDWrOpIN)N`--rstzEguFbKp+sESRPR|tJfzaGE%6l#_Uwb<9inT zwe;!v;7tyPQ&d=3xYpg>z0~3P?BmjgQHiko;2b{3_ajFj5M&Gu4RI+H%B!}QFY`(Z zKA2-n`fh=Mq!Q=$;*S@J?mJ!^;V#;4Zxfy_Qx#L-ZY$oPDVzIO0Torw9mO3u8B1GL zYz+h$Dvv&@GDwii_vSwu`7nREApxe_*2S3DcdA_!oq>|cl_W2NY|V*M_lgurYUrSu z=ktQ#Y+1vna`(-W?AqYk>8X1c7y3zJ0-n%6t`vcB?KT0IlO^{0T)i~I!uG3Vl#}1N zPRg|Mwe0cGT(HVv;_$*cX+ae{;w4)Mx*|S-;g4Go*wh?0dmsDFF{D3?7^CK;qL=P= zU~$qa-`c`fX*E2M!wQG_XU!H_OvN7^B&m_%U>%Q!{tZ*BhefXw`33MJ?>t7|Go5Sd zoi49sP1{_05N2N^L!To4vQfY6)rwY7zebANtqsvjy~)N#%Yx46jyJ&h&r-|vy9*1z Mowc=kV(A(87ok_YTmS$7 diff --git a/res/drawable-xxhdpi/quantum_panel_bitmap.9.png b/res/drawable-xxhdpi/quantum_panel_bitmap.9.png new file mode 100644 index 0000000000000000000000000000000000000000..b44269ea95da4a78603c76a37cdb66e7a6423583 GIT binary patch literal 1323 zcmeAS@N?(olHy`uVBq!ia0vp^p&-n`1|%)Mu80CsEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYk_Lcdb&7zqQ$Cob@qT=Md=OQ6z<1y(tor)AAe6f#%A9#MaYT6M)=cCxfRDYmrh7Llo*-0QSar}DGy(28eMZu5nju>yX)fI zczdx~Z+6+t^-(|g_uKhYJ^zfp)|LZ8E&lzcm&`nO{gKeh($24L z=eav~T~W(?RS}$We&f_tlW!^?ubx%x+{kt6#73U09kCk0iIcK~G#Okn`mPB~Jn`Fo z@z#Cox49k6y;QIF{nMvUtM~5RJD;uj;PMFOm4!0#vGMWs_ENod?viUALLV*an5rgd zvUTbSxrTd3j~<==`}gnsMH;`Bl!}MVF!8+~9~)b1E!F$aK`+dd^Ro(DgvU}%`ITm~ z&;ILLl#%aQ8{v7$WUm}w`+R1PZnvYCHg50R5*u{(_{EPEf1)@(26=I6s$Md2otDb2 zC&;!pjz3Vuk%Lk3$Z4Q-ReAaM^d~}7n_DM`o-deP#<_Lt)~}`vic4LlrDiLO9N1$Y z*NSZ1nGnThX^K1pHm@B@EQ~$;GhNy~d5Xd%O7Itr5nL*G?m91AiT2H%P=XuWG6EiKup?vA9 zZ_KN|o(Ps~V_|>eA;&3dyXLOIRsaF!9z?d%m() z*6yp=#XNI=Ei*_ypcWLF(K(gduf9AgeAxxj1Ctmwsd(m1|8S&9q3^MwocNN38LZal z|CyVc@84G0q-ZqLr%t=f_iC+N;-po&C8c$B|GG~zv-H?lTUWE!mPq~HzoI3GC86QZ z>_%w@JGKu=Ml<)^y>sW!n!lf8Yu~+l_wU}#n?KnOurvH;tY9#3j&oOSU3g&8*-2i# znyc)BtBQ&~9WJr@yZqszh04n>zudok`EvWFFOt8S;-kb|S7|9+VE@2WxrJ|{`mku=HI@3d%c`I5Xj2LGVO4eTevyt|H2H(Qyy0nLmhoX z_ed&D{}s6Lu0|h!z_c~olA@i4pG5Y%YkuBm@vp%sD_S%lq&#T7PmqaL)KW+Jhu2sZ zOfGba>IvE)<(~P=JZbi(tenuGWQO;|{`rQhl6sR@9#Z`qI%U;- z8Cf{rWd#bDZ5RkEP;^#lmdKI;Vst04{xN%>V!Z literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/quantum_panel_dark.9.png b/res/drawable-xxhdpi/quantum_panel_dark.9.png deleted file mode 100644 index 17ba0f1163406cad4cad568e9602cecc5d1278ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1366 zcmaJ>c~H^`82w>dtYv~xm`5AlSU|C)d7_4jt|V(pN@=c|Y3iDHqNWy-x5<=g9_^~A zDW-Ytcw3g{ec)Z<&KigZnR(pYRnR-L9T;v1ikYU}}FeN0@7r zrsx1;%ouDu6$(wvq^+bx~}J731jx?4E_ zIhVqDVoNv-B>L!b!~LVNTYC|g`);&xjytdx;kk;3ga%eJxyTOPQ(C6~A@ z?ZU1lCrX$<^S!{T=5S<}r?<8u0QKwPOl-&<-WO(KG@g&=jK0KZR8OMl5Qv1E^&RM5 zz#%yv2|@q}dY5nL{=N>l>!6?;N%AV}Aj5Ru!5tjRKdV*vURb}9udS40Oh0;?UW5r=c`3uXRF`0JRDF02Q^hop4z{GFtNbohP<>*!Z=0S@`dKofM*9oC0Br}B$9-D^-n6y(C1zH93?F~t!z)zLud)goi zIJW~VJ^HYcnfJlpaCga%&S~`_KFT=#3AXsw-seNCgNWmB?XHY=Vi5R0)^GR!UArOu z9=1yQP(#HJ1Ap|fZD{JroZgc}ET}xJH-~m{%RP1|rhY}QF5$x3he4|ORA4}?V+LL! z$NfZt!`2}*n*D2+3a+@OKTa~w*mz5+F&)d{SZZEz{j86htW2(vJgz*0*H1ppJ#%(| z0(EbSi2Nde=mf3kzE&sU9heH*lX0e!J7rrUMSl#O0|tK5CG^89 z2FSK1%FmdYyxw}2)Cjht|c|kpnNo7`v_qC*s=DPNigwrRsA= zz!Mt}f~P5NwG7W#J~o-Q3_=y4`U-0&I<`Iis8Qnur%dtp$Q2}28^J6igcSb1Hci7< z0jBeP>EbFQ96o9uF>U*=p!p|qJoX7@XLcJm{BN#CY|E}JvThh-*NVl#2w0h)Cp4nJ zaCd?*qxs3xYk_L6dAc};RLprhcYpsK7m?%pli%Nacd@ZWWC|Or;w%=<2?D&jvj1J8 zye=*C@>-$D`rlC_bHU<8oFW1O>a4L7HSF~)7rA&9eeCy4-2e6Sog4E$zKhA*^Io~P z?ELes<#u((pRbjF_sd+naLOUhh5oNx*wn8%I(D@C@^-J}S>Nm9|N6v?t`mB0+}}i# zZ{NQoHajPHp6{~h4{rYWzG>2Oo!d!HiOfotdO^9DM6zRlA8FcVbA8R%xA{lagM2&F z9{)btqPf*uD))WY8_{(^xkkdg~qWI^dpQIVyCrJf;-@I6S`oG_=`{OUTXkVCSA+m9oKa2b= z6(hz8d&(b9K3^`@Ex+K(&5gDeXEWBmPjhz{&;MKbI6Ne2W#$+CB!&g;N>_W%i8$=q z91|BTEXu{;wX(cy+UfU

~P?msHdOFzQrsx{v_nBg%zKf@B@@F0A-FNAv*HSP-(Y5lqD#lzZFzh_vx>0WEUUNqyn zVf#DsE?0>2J~kt<$sT@BaycIISjn7n3?w@GDAX5~dL@s$qRYiFI4x#hdM-)5;w zRq@q*tzGgB=}foVex588n1BACWHoo0_>V0{RNsuG^ zU8beaQeM^`KB2ypiDBt>g`i%$s{7jCdP6?9Og?hSeZ9>k>xuV+R>qyVaHr66qVTZ; zyJ|~L2DAFRWHC@3<(!o&CH z7u?WWxs55}>im@KzZuN=I(%F9^*?6KjHeR@4GGEP<7wMdy&A$En_XlC! zt6IIiT#LON-<~|o9)4=kkK12WCN4a^>BsYvi`l~u{^F@VsITo@x^hLs0iFXw2lN_P z8_XHr>??it=6C+S+Gndv?EZ_qzM53~H{-9;`Onh*{O@YZE8d@zYk1BukI`b){QYa9 zJH3k1Sxu)W#fN-MYxH{M_jgBh{`|$7k>bW8ostbQQ#~gA+;i`rknfc~tpy(Xi?k(vNX$5KaHO?ruq! zL4NV2$1mnrtx;A;n;3h8h5L|8pz^=t_qXwK+gnCG7M0CCaHVTw#sgrn$KdJe=d#Wz Gp$P!%8*N+w literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/widget_resize_frame.9.png b/res/drawable-xxhdpi/widget_resize_frame.9.png new file mode 100644 index 0000000000000000000000000000000000000000..7e189d44249d052501f315cd385336e2e5ca0d78 GIT binary patch literal 626 zcmeAS@N?(olHy`uVBq!ia0vp^D?ylp4M+xDmDLAQEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYZ(}rL_A#_Ln`LHy)h!Jclh~(FI)Fp9SGse2{ps;$US4@e-|gFO~&Ibpb|YLg|lk#5O`kH zAs%zxxIsRT1m}nS0UHx3vIVCg!0Qw*S AB>(^b literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/widget_resize_frame_holo.9.png b/res/drawable-xxhdpi/widget_resize_frame_holo.9.png deleted file mode 100644 index 1681387a139c91bd6947ab94fa134844d8379dae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4396 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 U$7dXXT|GyDHrxPK32_MjAMfU;ZU6uP diff --git a/res/drawable-xxhdpi/widget_resize_handle_bottom.png b/res/drawable-xxhdpi/widget_resize_handle_bottom.png deleted file mode 100644 index d549fcd91805c3e408fcee31ca2ffeaa8f6c0500..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4470 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 diff --git a/res/drawable-xxhdpi/widget_resize_handle_left.png b/res/drawable-xxhdpi/widget_resize_handle_left.png deleted file mode 100644 index dd56dad15eb79fadd78f3ba29bd7a2f832bac60e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4428 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 diff --git a/res/drawable-xxhdpi/widget_resize_shadow.9.png b/res/drawable-xxhdpi/widget_resize_shadow.9.png new file mode 100644 index 0000000000000000000000000000000000000000..41c448bbb302f0f5552f6de7aa67939e50ab9209 GIT binary patch literal 975 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 literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/ic_remove_launcher.png b/res/drawable-xxxhdpi/ic_remove_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..7043be0157db1a3fa5e30f3206ab20ec449caaac GIT binary patch literal 3210 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 literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/ic_search_grey.png b/res/drawable-xxxhdpi/ic_search_grey.png new file mode 100755 index 0000000000000000000000000000000000000000..28519fda60fe834dc60760ad9486107c9996ca59 GIT binary patch literal 1996 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 kiRM+yFIS@8Z%x&J)mJNeH4q$*W93J9ox^xi>0 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 P-=5c0)k9U^v5ojY_Rl3B literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/portal_ring_outer.png b/res/drawable-xxxhdpi/portal_ring_outer.png new file mode 100644 index 0000000000000000000000000000000000000000..d2df32267004200e4d6ac08d238e2e96cc8fdab4 GIT binary patch literal 23360 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 literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/portal_ring_rest.png b/res/drawable-xxxhdpi/portal_ring_rest.png new file mode 100644 index 0000000000000000000000000000000000000000..11e92eeb604b57a155f2545d3277e566598df78b GIT binary patch literal 8628 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 VD;OZ7d-*>KJ*}IXmFo7<{|88{vts}N literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/quantum_panel_bitmap.9.png b/res/drawable-xxxhdpi/quantum_panel_bitmap.9.png new file mode 100644 index 0000000000000000000000000000000000000000..bc887fe916fe04ce9a4fddeb60f1cef32d46c291 GIT binary patch literal 2546 zcmaJ@3p7;w8Xu7#GjPaNmGn*ORoJJ@qhOW@?s$v69oxHVmIu|YfV^;i0_6C z27QUXQWg{~^4czgHf{Ig@V3YD=zQ$j?=Y@P20{=I!CZ_oK29uWDBZ9#ybMIvstH)k z3NMsy|&Hw-gk%;-S zu!uJqU%&`rdw%hSyt!ecVOYu_5EKdpUO~o7WI_UoPNx%yE(8}B9D=~flf^Jsi4)6L z&N8qeIZq~%!Xk+nqh;hqNfKcP~CN(m%9kq{rRz1Iv{ z4hKR14da_=IVV{P5rQDOBvHmg>LFM;YmDUX?;UA@NHh$8nFuKgH;yggCB{Qy*qiN! zMZVzqB0hsi<_btr01-!~@dP-43{Y`gzJQD4Lp%x{0(jIY$Yqx2TlRDookC)hNuETI zN+PWTm~;;!lS=iV5J5Jb!lKREdW+>SSImQEeTxv^Uu=ngYs+BCATBJCaU_zs*)9l( zmcSBuv_y(wv1Y1{aq#8xL}IPdQCq0b_hv&fQ4+-Wlu6<-Gnr5HC{Z{bNQ#2!5SIiBAS?k1k)Um!zckaV3h7X-_;oapm#@PGiIIttA){lY(6pdX z^K`x0Opdbqd0~@}bFi8EW#gI{v!HZ1AuWULRI$Fx-ZrFQ^}2eqX|yQUgBUr%;LiVK zWc$h{(92xkBx0d5x$rRMwDZ|Ow(k44>btFX1@`nlg)r&g5054)hCXDpRX(dHpU~{k z&=pA?PJ6xBxj|V53-`9>pD0NiH7y>__s~a~9vd*rfzo%gSS)q>q zpsLLC$_25!5l}Kid&`HUDo?3UDBMOp{_DvnLqpWOpnV044GnkopXz*82r~-bc6WD6 zj5z(YD$M47XL7w*C>*-AXV0ELf`fy%;hmi?mE`%bSDiUn5%V42Y@dUDU|h3BV@zGo z3QL!K=iHwLGc{=6TL0l5{l4kx>EoVfMji2&_vgR-P}tbms4-cvVERC~*Y2wFf2L?s zDX7FwyE6$pcYca3aA}%zY|3!c$g|SYQVF(*bV+q{CF=#CJH9;VscC;}YiqDvF5kB5 zAI-1AZ>a}D?VBH(1O?{gE#f9^=jS>I0-sYTln7|!$oi%_JD{^=W|MU!TDpyCSoGb9b91t zVRJS(g@naD>KgstqdQ_;TIdP1@cS%4-0MdcON%_OY`uFRsj{C&HMcKppp}FWFKkABkq2~t?dA9=rC!!G z%|p9dKTcOmyOX|&G{)1_{Mu zi5mfuzZF5U>w1{+lFHNz_wvx`C{tZ`eU#43Zi||O$hIg%`FYoM*UJA3_`fir4QBd* z20(AzvM4pj17W&tV47U#x^TI^o=1&QP7Xs|w4vNkPq#0@S|>Vg8J0^ z%$=mUOzF#*IgkiS$$&y2TkdBF zsziQjwVH4X$g-<=5$i97;c;x~SV=0v3dMxb zjBLY7`>?_L6u;{=nRn&Swzf8LMf>}Ksi~<9WkUg>1S~Q)H*bm)i>EDq-v9Ha64i2g zsl{QBL%`}jFZ=OvOu?>l@fTVH-`$lhq9FhzTZ^}YS` z=1#wVrSJYFqFdM@eq6kND7kWK|GUsm56z?gcV0<-nRO|n$`{uL6Q_!-97p7zvdag2 zB8?v@-&neC^ErCe%2G4XpKD!VF{rxA(F^IcDjN?mz9iZuQ3>tN-?*7$3$4cC7Rk@S z4I|`VYY(cM=Oh%D23(vwyu|fP=FLM13`|nL@sm6C^*JY>m$cvbE^Vj>z*R386z1<& z#4*NlxiVoPD$7iTW~q-VM*6p+lf%4j7hF7)*SPITTO6mp zb1%W0WgfO(FPCC&)OCXB(4CS$;NBQA0d_jF-sgeoW!vl@tT5LdVr1vc%iM9AP#dVw zdt!NdM8sao-k`IktoumW(k!EBo; literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/quantum_panel_dark_bitmap.9.png b/res/drawable-xxxhdpi/quantum_panel_dark_bitmap.9.png new file mode 100644 index 0000000000000000000000000000000000000000..7cfd6e477a44f0188a9f637fbcc321cf32d5f9f0 GIT binary patch literal 2531 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 fnC0b>2(SpT>)-+tfs?(;tH zJzJ0vx5&#QzykmPFXduIBD32a%iWE62SBTDGn+phokAy38rp)JNI<5hR3xa>;~8Wk ziK}xzyFf+&fXjMqatfUiyHr9@dM@t3aIJbHqYVHNQC1^PWRWzeA~UoGDW|`(h68HV zQqHHsSR~dcCo{E+^Gsw?UR*Mfmqm!xoTx}J!YW}1^dyagR=v((mRO~nab5{?c8FmP zI1Zt+q?`$-QeqQ8Ib|Y2Ay)_y2oD7@F&7mIF^m@q@(@%2BPfjWAyh082qXvsPCgvQ znn|sZBq|n8+G1`}P9{woB`|EUShyBGmojC*s8}q95gyFrK@0*i=Nf3-3K`77P6h>O zCQMo*t)&d0gArFz*|e0ycsdb+-WVJEg4kf5Ocawc*oqrrl#9T6y(6x1w3$vM|8wJ& zXmfI|k%SXTGnH*3n0jb}oytt^UT??&WW15Yo3u<(aGio8vh}2aRw|?%<_lM?RZ9>) zu0d4-1mcSb4J6P|kK3;Y3 zqgb5K8XUq9N1-O-R*)ub4yj&fqV(W+rX|`}ctP?>l~|w_KtgpG9}?gQ4-(-50z&zy zkQ54q1cHk>lltnHGC0wxjD(5A=yXUTMtOVprQpB;^wW-0!vF=a)|+H+Mp_6GqCM{D9YF4t~25;rE+UY>hk3tPVtZJP1CGJjuo zTW^KeBKf7U3ZARw60_r^h;^-io8zaNxgY2cs(xB`x##fqWzDTIzIy@NCS%%xtAx%vH*{U4Z=q&BE63j+ z*d7EoK3o&j9qQ51BY*3J0gc~Q7u zDaP&{dV1^$@Kb%^iYez;Wz?nZc;A!X>L20C-OWoOUl2De?4pT`~Kn-fKBsQq)!7nLqWPkooeGN(@_aXkZvb zPuh0pu}(Aw%^%TUEvxpoJ+t}Pzbowws#+5>?Az0^>rT3#ul?TiTi5pwvn(5-V2iV}*pT_px z2UNCMw}p2;?g?y*wj^CD+k*xE-r9J6>$~j_?|;+qU}(#nIcIPEoUnXXU$?z`^y-&2 z+~$$N4;!A^h~w>T>J`d3xvo;y{C;0Z!`7koZ9~754;2r?BUIE*R(Wu3AJ(1ZL-?=S z#JA_Q-rycgy~wr5+kZ$JFeeefKH>#!V|Z96OJ_~?y44?lrR_g?PJ?}+|( zU~`MNeYSUfgqttun29%8?6(3PpUBrSOFS+vu3eM3vUs-V!6=_G?{r}3jPlfP1u@-@ Ozq2wrPH}WWdj7wIv!h-B literal 0 HcmV?d00001 diff --git a/res/drawable/all_apps_search_bg.xml b/res/drawable/all_apps_search_bg.xml new file mode 100644 index 0000000000..a09f88fd4a --- /dev/null +++ b/res/drawable/all_apps_search_bg.xml @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/res/drawable/info_target_selector.xml b/res/drawable/bg_screenpanel.xml similarity index 75% rename from res/drawable/info_target_selector.xml rename to res/drawable/bg_screenpanel.xml index f3a7016c38..cdb71dfa18 100644 --- a/res/drawable/info_target_selector.xml +++ b/res/drawable/bg_screenpanel.xml @@ -2,7 +2,7 @@ - - - - + + + + + + \ No newline at end of file diff --git a/res/drawable/container_fastscroll_popup_bg.xml b/res/drawable/container_fastscroll_popup_bg.xml new file mode 100644 index 0000000000..2ef07ab96a --- /dev/null +++ b/res/drawable/container_fastscroll_popup_bg.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/WallpaperPicker/res/values-sw600dp/config.xml b/res/drawable/quantum_panel.xml similarity index 76% rename from WallpaperPicker/res/values-sw600dp/config.xml rename to res/drawable/quantum_panel.xml index 62342dcc2f..1f4fb711bb 100644 --- a/WallpaperPicker/res/values-sw600dp/config.xml +++ b/res/drawable/quantum_panel.xml @@ -1,5 +1,6 @@ - - - true - + diff --git a/res/values-sw340dp-port/config.xml b/res/drawable/quantum_panel_dark.xml similarity index 76% rename from res/values-sw340dp-port/config.xml rename to res/drawable/quantum_panel_dark.xml index 5f71077c72..6642e78ac7 100644 --- a/res/values-sw340dp-port/config.xml +++ b/res/drawable/quantum_panel_dark.xml @@ -1,5 +1,6 @@ - - - - + diff --git a/res/drawable/quantum_panel_shape.xml b/res/drawable/quantum_panel_shape.xml new file mode 100644 index 0000000000..1083615768 --- /dev/null +++ b/res/drawable/quantum_panel_shape.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/apps_customize_application.xml b/res/drawable/quantum_panel_shape_dark.xml similarity index 68% rename from res/layout/apps_customize_application.xml rename to res/drawable/quantum_panel_shape_dark.xml index c56cdf3d29..c3821c416c 100644 --- a/res/layout/apps_customize_application.xml +++ b/res/drawable/quantum_panel_shape_dark.xml @@ -1,5 +1,6 @@ - - - + + + + \ No newline at end of file diff --git a/res/drawable/remove_target_selector.xml b/res/drawable/remove_target_selector.xml deleted file mode 100644 index 5e071fbfaf..0000000000 --- a/res/drawable/remove_target_selector.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/res/drawable/uninstall_target_selector.xml b/res/drawable/uninstall_target_selector.xml deleted file mode 100644 index 229942e3ad..0000000000 --- a/res/drawable/uninstall_target_selector.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/res/values-land/config.xml b/res/drawable/widgets_row_divider.xml similarity index 70% rename from res/values-land/config.xml rename to res/drawable/widgets_row_divider.xml index 31115c9cb6..bb5b6b55bf 100644 --- a/res/values-land/config.xml +++ b/res/drawable/widgets_row_divider.xml @@ -1,5 +1,5 @@ - - - - - - false - + + + + diff --git a/res/layout-land/launcher.xml b/res/layout-land/launcher.xml index 6f95bd506e..6500ebcd2e 100644 --- a/res/layout-land/launcher.xml +++ b/res/layout-land/launcher.xml @@ -18,11 +18,9 @@ + + + android:layout_gravity="right" /> - + + diff --git a/res/layout-land/migration_cling.xml b/res/layout-land/migration_cling.xml index db93da87af..269c1aee6d 100644 --- a/res/layout-land/migration_cling.xml +++ b/res/layout-land/migration_cling.xml @@ -46,6 +46,7 @@ android:layout_width="@dimen/cling_migration_content_width" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/cling_migration_content_margin" + android:layout_marginRight="@dimen/cling_migration_content_margin" android:orientation="vertical" android:paddingLeft="24dp" android:paddingRight="24dp" > diff --git a/res/layout-port/launcher.xml b/res/layout-port/launcher.xml index af30a32e50..d0772ee70b 100644 --- a/res/layout-port/launcher.xml +++ b/res/layout-port/launcher.xml @@ -22,7 +22,6 @@ android:id="@+id/launcher" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@drawable/workspace_bg" android:fitsSystemWindows="true"> + + - + + diff --git a/res/layout-port/migration_cling.xml b/res/layout-port/migration_cling.xml index 81689d38b3..3f696a216c 100644 --- a/res/layout-port/migration_cling.xml +++ b/res/layout-port/migration_cling.xml @@ -48,6 +48,7 @@ android:layout_height="wrap_content" android:layout_below="@+id/ic_cling_migration" android:layout_marginStart="@dimen/cling_migration_content_margin" + android:layout_marginLeft="@dimen/cling_migration_content_margin" android:orientation="vertical" android:paddingLeft="24dp" android:paddingRight="24dp" > diff --git a/res/layout-sw720dp/launcher.xml b/res/layout-sw720dp/launcher.xml index 960ccf330d..802922ec10 100644 --- a/res/layout-sw720dp/launcher.xml +++ b/res/layout-sw720dp/launcher.xml @@ -18,11 +18,9 @@ + + - - + + diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml new file mode 100644 index 0000000000..1bf54eefba --- /dev/null +++ b/res/layout/all_apps.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/all_apps_button.xml b/res/layout/all_apps_button.xml index 9d6d82bb21..68cc10932c 100644 --- a/res/layout/all_apps_button.xml +++ b/res/layout/all_apps_button.xml @@ -15,5 +15,5 @@ --> diff --git a/res/layout/all_apps_container.xml b/res/layout/all_apps_container.xml new file mode 100644 index 0000000000..626edafab7 --- /dev/null +++ b/res/layout/all_apps_container.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/layout/add_list_item.xml b/res/layout/all_apps_empty_search.xml similarity index 58% rename from res/layout/add_list_item.xml rename to res/layout/all_apps_empty_search.xml index e937d7bc1d..f60c4a09ad 100644 --- a/res/layout/add_list_item.xml +++ b/res/layout/all_apps_empty_search.xml @@ -1,25 +1,28 @@ - - - + android:gravity="center" + android:paddingTop="24dp" + android:paddingBottom="24dp" + android:paddingRight="@dimen/all_apps_grid_view_start_margin" + android:textSize="16sp" + android:textColor="#4c4c4c" + android:focusable="false" /> + diff --git a/res/layout/all_apps_icon.xml b/res/layout/all_apps_icon.xml new file mode 100644 index 0000000000..0985e95c00 --- /dev/null +++ b/res/layout/all_apps_icon.xml @@ -0,0 +1,29 @@ + + + + diff --git a/res/layout/all_apps_prediction_bar_icon.xml b/res/layout/all_apps_prediction_bar_icon.xml new file mode 100644 index 0000000000..341d8ef4f8 --- /dev/null +++ b/res/layout/all_apps_prediction_bar_icon.xml @@ -0,0 +1,29 @@ + + + + diff --git a/res/layout/all_apps_search_bar.xml b/res/layout/all_apps_search_bar.xml new file mode 100644 index 0000000000..cf30eac364 --- /dev/null +++ b/res/layout/all_apps_search_bar.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/application.xml b/res/layout/app_icon.xml similarity index 96% rename from res/layout/application.xml rename to res/layout/app_icon.xml index c21dea070f..831cee5b00 100644 --- a/res/layout/application.xml +++ b/res/layout/app_icon.xml @@ -15,5 +15,5 @@ --> diff --git a/res/layout/apps_customize_pane.xml b/res/layout/apps_customize_pane.xml deleted file mode 100644 index e42576ffe4..0000000000 --- a/res/layout/apps_customize_pane.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/res/layout/apps_customize_progressbar.xml b/res/layout/apps_customize_progressbar.xml deleted file mode 100644 index 6aa90999cb..0000000000 --- a/res/layout/apps_customize_progressbar.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/res/layout/apps_customize_widget.xml b/res/layout/apps_customize_widget.xml deleted file mode 100644 index a8344e3ff9..0000000000 --- a/res/layout/apps_customize_widget.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout/appwidget_error.xml b/res/layout/appwidget_error.xml index 03d4ae424c..708ece4e2c 100644 --- a/res/layout/appwidget_error.xml +++ b/res/layout/appwidget_error.xml @@ -15,15 +15,12 @@ --> diff --git a/res/layout/dummy_widget.xml b/res/layout/dummy_widget.xml new file mode 100644 index 0000000000..a0fa8fc3e4 --- /dev/null +++ b/res/layout/dummy_widget.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/folder_application.xml b/res/layout/folder_application.xml index b48b61331d..4d003313ef 100644 --- a/res/layout/folder_application.xml +++ b/res/layout/folder_application.xml @@ -15,5 +15,5 @@ --> diff --git a/res/layout/folder_icon.xml b/res/layout/folder_icon.xml index fd45d76859..237af68903 100644 --- a/res/layout/folder_icon.xml +++ b/res/layout/folder_icon.xml @@ -26,9 +26,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:antialias="true" - android:src="@drawable/portal_ring_inner_holo"/> + android:src="@drawable/portal_ring_inner"/> - - - - - - - - - diff --git a/res/layout/search_drop_target_bar.xml b/res/layout/search_drop_target_bar.xml index af2d016349..4737ee1bc4 100644 --- a/res/layout/search_drop_target_bar.xml +++ b/res/layout/search_drop_target_bar.xml @@ -17,8 +17,7 @@ + android:focusable="false" > @@ -37,8 +36,7 @@ + android:text="@string/delete_target_label" /> + + + + + + + \ No newline at end of file diff --git a/res/layout/user_folder.xml b/res/layout/user_folder.xml index ed8d43e46d..ecf7def48f 100644 --- a/res/layout/user_folder.xml +++ b/res/layout/user_folder.xml @@ -1,5 +1,6 @@ - - - + android:background="@drawable/quantum_panel" + android:elevation="5dp" + android:orientation="vertical" > - - - + android:layout_height="match_parent" > - + + + + + + + - + android:clipChildren="false" + android:orientation="horizontal" + android:paddingLeft="8dp" + android:paddingRight="8dp" > + + + + + + + + \ No newline at end of file diff --git a/res/layout/widget_cell.xml b/res/layout/widget_cell.xml new file mode 100644 index 0000000000..7fefebaec5 --- /dev/null +++ b/res/layout/widget_cell.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/widgets_list_row_view.xml b/res/layout/widgets_list_row_view.xml new file mode 100644 index 0000000000..ced564801d --- /dev/null +++ b/res/layout/widgets_list_row_view.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + diff --git a/res/layout/widgets_view.xml b/res/layout/widgets_view.xml new file mode 100644 index 0000000000..755634f82f --- /dev/null +++ b/res/layout/widgets_view.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/mipmap-xxhdpi/on_boarding_welcome.png b/res/mipmap-xxhdpi/on_boarding_welcome.png deleted file mode 100644 index 7b11dea0e0947a5262232690c98b765d7fcfb366..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50862 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 k>+1jORZYRUP%EC76zNq{dt=V!Z diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml index f37f08f3b0..7004524634 100644 --- a/res/values-af/strings.xml +++ b/res/values-af/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Tuis" - "Android-kernprogramme" + "Werk" "Program is nie geïnstalleer nie." "Program is nie beskikbaar nie" "Afgelaaide program in veiligmodus gedeaktiveer" "Legstukke gedeaktiveer in Veiligmodus" - "Legstukke" - "Legstukke" "Wys Mem" "Raak en hou om \'n legstuk op te tel." "%1$d × %2$d" - "Kan nie item op hierdie Tuisskerm laat los nie." - "Kies legstuk om te skep" - "Vouernaam" - "Hernoem vouer" - "OK" - "Kanselleer" - "Voeg by Tuisskerm" - "Programme" - "Kortpaaie" - "Legstukke" - "Niks meer spasie op jou Tuisskerms nie." + "Deursoek programme" + "Laai tans programme …" + "Geen programme gevind wat met \"%1$s\" ooreenstem nie" "Niks meer spasie op die tuisskerm nie." "Geen plek meer in die Gunstelinge-laai nie" - "Hierdie legstuk is te groot vir die Gunstelinge-laai" - "Kortpad \"%s\" is geskep." - "Kortpad \"%s\" is verwyder." - "Kortpad \"%s\" bestaan reeds." - "Kies kortpad" - "Kies program" "Programme" "Tuis" - "Verwyder" - "Deïnstalleer" "Verwyder" "Deïnstalleer" "Programinligting" - "Programme" - "Verwyder" - "Deïnstalleer opdatering" - "Deïnstalleer program" - "Programbesonderhede" - "1 program gekies" - "1 legstuk gekies" - "1 vouer gekies" - "1 kortpad gekies" "installeer kortpaaie" "Laat \'n program toe om kortpaaie by te voeg sonder gebruikerinmenging." - "deïnstalleer kortpaaie" - "Laat die program toe om kortpaaie te verwyder sonder gebruikerinmenging." "lees Tuis-instellings en -kortpaaie" "Laat die program toe om die instellings en kortpaaie in Tuis te lees." "skryf Tuis-instellings en -kortpaaie" @@ -78,30 +48,17 @@ "Kon nie legstuk laai nie" "Stel op" "Dit is \'n stelselprogram en kan nie gedeïnstalleer word nie." - "Vuurpyllanseerder" "Naamlose vouer" - "Tuisskerm %1$d" "Bladsy %1$d van %2$d" "Tuisskerm %1$d van %2$d" - "Programme-bladsy %1$d van %2$d" - "Legstukke-bladsy %1$d van %2$d" "Welkom" - "Maak jouself tuis." - - - "Skep meer skerms vir programme en vouers" "Kopieer jou program-ikone" "Voer ikone en vouers vanaf jou ou tuisskerms in?" "KOPIEER IKONE" "BEGIN VAN NUUTS AF" - "Organiseer jou spasie" - "Raak en hou agtergrond om muurpapier, legstukke en instellings te bestuur." "Muurpapiere, legstukke en instellings" "Raak en hou agtergrond om te pasmaak" "HET DIT" - "Hier\'s \'n vouer" - "Om een soos dié te skep, raak en hou \'n program en skuif dit dan oor \'n ander een." - "OK" "Vouer oopgemaak, %1$d by %2$d" "Raak om vouer toe te maak" "Raak om hernoem te stoor" @@ -111,14 +68,34 @@ "Legstukke" "Muurpapiere" "Instellings" - "Wag tans…" - "Laai tans af…" - "Installeer tans…" + "Laat draai toe" "Onbekend" - "Nie teruggestel nie" - "Verwyder almal" "Verwyder" "Soek" "Hierdie program is nie geïnstalleer nie" "Die program vir hierdie ikoon is nie geïnstalleer nie. Jy kan dit verwyder of die program soek en dit self installeer." + "Voeg by tuisskerm" + "Skuif item hierheen" + "Item is by tuisskerm gevoeg" + "Item is verwyder" + "Skuif item" + "Skuif na ry %1$s kolom %2$s" + "Skuif na posisie %1$s" + "Skuif na gunstelingposisie %1$s" + "Item geskuif" + "Voeg by vouer: %1$s" + "Voeg by vouer met %1$s" + "Item by vouer gevoeg" + "Skep vouer met: %1$s" + "Vouer geskep" + "Skuif na tuisskerm" + "Skuif skerm na links" + "Skuif skerm na regs" + "Skerm is geskuif" + "Verander grootte" + "Vermeerder breedte" + "Vermeerder hoogte" + "Verminder breedte" + "Verminder hoogte" + "Legstukgrootte is verander na breedte %1$s hoogte %2$s" diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml index 1a25f66cd2..d7fed9de4e 100644 --- a/res/values-am/strings.xml +++ b/res/values-am/strings.xml @@ -20,57 +20,27 @@ "ማስጀመሪያ3" - "መነሻ" - "Android ዋና መተግበሪያዎች" + "ስራ" "መተግበሪያ አልተጫነም።" "መተግበሪያ አይገኝም" "የወረደው መተግበሪያ ደህንነቱ በተጠበቀ ሁኔታ ውስጥ ተሰናክሏል" "ምግብሮች በደህንነቱ የተጠበቀ ሁኔታ ተሰናክለዋል" - "ፍርግሞች" - "ፍርግሞች" "ማህደረ ማስታወሻ አሳይ" "ፍርግም ለማንሳት ይንኩ እና ይያዙት" "%1$d × %2$d" - "ንጥሉን እዚህ የመነሻ ማያ ገጽ ላይ ማኖር አልተቻለም።" - "ለመፍጠር መግብር ይምረጡ" - "አቃፊ ስም" - "አቃፊ ዳግም ሰይም" - "እሺ" - "ይቅር" - "ወደ መነሻ ማያ ገጽ ያክሉ" - "መተግበሪያዎች" - "አቋራጮች" - "ፍርግሞች" - "የመነሻ ማያ ገጾችዎ ላይ ተጨማሪ ቦታ የለም።" + "መተግበሪያዎችን ይፈልጉ" + "መተግበሪያዎችን በመጫን ላይ..." + "ከ«%1$s» ጋር የሚዛመዱ ምንም መተግበሪያዎች አልተገኙም" "በዚህ መነሻ ማያ ገጽ ላይ ምንም ቦታ የለም።" "በተወዳጆች መሣቢያ ውስጥ ተጨማሪ ቦታ የለም" - "ይህ መግብር ወደ የተወዳጆች መሣቢያ ላይ እንዳይገባ በጣም ትልቅ ነው" - "አቋራጭ «%s» ተፈጥሯል።" - "አቋራጭ «%s» ተወግዶ ነበር።" - "አቋራጭ «%s» አስቀድሞ አለ።" - "አቋራጭ ይምረጡ" - "መተግበሪያ ይምረጡ" "መተግበሪያዎች" "መነሻ" - "አስወግድ" - "አራግፍ" "አስወግድ" "አራግፍ" "የመተግበሪያ መረጃ" - "መተግበሪያዎች" - "አስወግድ" - "ዝማኔ አራግፍ" - "መተግበሪያ አራግፍ" - "የመተግበሪያ ዝርዝሮች" - "1 መተግበሪያ ተመርጧል" - "1 ፍርግም ተመርጧል" - "1 አቃፊ ተመርጧል" - "1 አቋራጭ ተመርጧል" "አቋራጮችን ይጭናል" "መተግበሪያው ያለተጠቃሚ ጣልቃ ገብነት አቋራጭ እንዲያክል ያስችለዋል።" - "አቋራጮችን ያራግፋል" - "መተግበሪያው አቋራጮችን ያለተጠቃሚ ጣልቃ ገብነት እንዲያስወግድ ያስችለዋል።" "የመነሻ ቅንብሮች እና አቋራጮችን ያነባል" "መተግበሪያው በመነሻ ውስጥ ያሉ ቅንብሮችን እና አቋራጮችን እንዲያነብ ያስችለዋል።" "የመነሻ ቅንብሮችን እና አቋራጮችን ይጽፋል" @@ -78,30 +48,17 @@ "ፍርግም የመጫን ችግር" "ማዋቀሪያ" "ይህ የስርዓት መተግበሪያ ነው እና ማራገፍ አይቻልም።" - "የሮኬት ማስጀመሪያ" "ስም-አልባ አቃፊ" - "መነሻ ማያ ገጽ %1$d" "ገጽ %1$d ከ%2$d" "መነሻ ማያ ገጽ %1$d ከ%2$d" - "የመተግበሪያዎች ገጽ %1$d ከ%2$d" - "የመግብሮች ገጽ %1$d ከ%2$d" "እንኳን በደህና መጡ" - "ልክ እቤትዎ እንዳሉ ሆነው ዘና ይበሉ።" - - - "ለመተግበሪያዎች እና አቃፊዎች ተጨማሪ ማያ ገጾችን ይፍጠሩ" "የመተግበሪያ አዶዎችዎን ይቅዱ" "አዶዎች እና አቃፊዎች ከድሮው የመነሻ ማያ ገጾችዎ ይምጡ?" "አዶዎችን ይቅዱ" "እንደ አዲስ ይጀምሩ" - "ቦታዎን ያደራጁ" - "ልጣፍ ፣ ምግብሮችን እና ቅንብሮችን ለማቀናበር ጀርባውን ይንኩ እና ይያዙት።" "የግድግዳ ወረቀቶች፣ ንዑስ ፕሮግራሞች እና ቅንብሮች" "ለማበጀት ጀርባውን ነክተው ይያዙት" "ገባኝ" - "አንድ አቃፊ እነሆ" - "አንድ እንደዚህ አይነት ለመፍጠር መተግበሪያውን ነክተው ይያዙት እና ወደ ሌላ ያንቀሳቅሱት።" - "እሺ" "አቃፊ ተከፍቷል፣ %1$d%2$d" "አቃፊን ለመዝጋት ይንኩ" "ዳግም የተሰየመውን ለማስቀመጥ ይንኩ" @@ -111,14 +68,34 @@ "ፍርግሞች" "የግድግዳ ወረቀቶች" "ቅንብሮች" - "በመጠበቅ ላይ" - "በማውረድ ላይ" - "በመጫን ላይ" + "ማሽከርከርን ይፍቀዱ" "የማይታወቅ" - "ወደነበረበት አልተመለሰም" - "ሁሉንም አስወግድ" "አስወግድ" "ፈልግ" "ይህ መተግበሪያ አልተጫነም" "የዚህ አዶ መተግበሪያ አልተጫነም። ማስወገድ ወይም መተግበሪያውን መፈለግና ራስዎ መጫን ይችላሉ።" + "ወደ መነሻ ማያ ገጽ ያክሉ" + "ንጥልን ወደዚህ ውሰድ" + "ወደ መነሻ ማያ ገጽ ንጥል ታክሏል" + "ንጥል ነገር ተንቀሳቅሷል" + "ንጥልን አንቀሳቅስ" + "ወደ ረድፍ %1$s ዓምድ %2$s አንቀሳቅስ" + "ወደ አቀማመጥ %1$s አንቀሳቅስ" + "ወደ ተወዳጆች አቀማመጥ %1$s አንቀሳቅስ" + "ንጥል ተንቀሳቅሷል" + "ወደ አቃፊ አክል፦ %1$s" + "ወደ አቃፊ አክል ከ%1$s" + "ንጥል ወደ አቃፊ ታክሏል" + "አቃፊ ፍጠር ከዚህ ጋር፦ %1$s" + "አቃፊ ተፈጥሮዋል" + "ወደ መነሻ ማያ ገጽ አንቀሳቅስ" + "ማያ ገጽን ወደ ግራ አንቀሳቅስ" + "ማያ ገጽን ወደ ቀኝ አንቀሳቅስ" + "ማያ ገጽ ተንቀሳቅሷል" + "መጠን ቀይር" + "ስፋት ጨምር" + "ቁመት ጨምር" + "ስፋት ይቀንሱ" + "ቁመት ይቀንሱ" + "የመግብር መጠን ወደ ስፋት %1$s ቁመት %2$s ተለውጧል" diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index 592be5841b..b7cd901de3 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "الرئيسية" - "‏تطبيقات Android الأساسية" + "العمل" "لم يتم تثبيت التطبيق." "التطبيق ليس متاحًا" "تم تعطيل التطبيق الذي تم تنزيله في الوضع الآمن" "الأدوات معطلة في الوضع الآمن" - "الأدوات" - "الأدوات" "عرض الذاكرة" "المس مع الاستمرار لاختيار إحدى الأدوات." "%1$d × %2$d" - "تعذر إسقاط العنصر على هذه الشاشة الرئيسية." - "اختيار أداة لإنشائها" - "اسم المجلد" - "إعادة تسمية المجلد" - "موافق" - "إلغاء" - "إضافة إلى الشاشة الرئيسية" - "التطبيقات" - "الاختصارات" - "الأدوات" - "ليس هناك مساحة أخرى في الشاشات الرئيسية." + "البحث في التطبيقات" + "جارٍ تحميل التطبيقات…" + "لم يتم العثور على أية تطبيقات تتطابق مع \"%1$s\"" "ليس هناك مساحة أخرى في هذه الشاشة الرئيسية." "لا يوجد المزيد من الحقول في علبة المفضلة" - "هذه الأداة كبيرة جدًا مما يحول دون قبولها في علبة المفضّلة" - "تم إنشاء الاختصار \"%s\"." - "تمت إزالة الاختصار \"%s\"." - "الاختصار \"%s\" موجود من قبل." - "اختيار اختصار" - "اختيار تطبيق" "التطبيقات" "الرئيسية" - "إزالة" - "إزالة" "إزالة" "إزالة" "معلومات عن التطبيق" - "التطبيقات" - "إزالة" - "إزالة التحديث" - "إزالة التطبيق" - "تفاصيل التطبيق" - "تم تحديد تطبيق واحد" - "تم تحديد أداة واحدة" - "تم تحديد مجلد واحد" - "تم تحديد اختصار واحد" "تثبيت اختصارات" "للسماح لتطبيق ما بإضافة اختصارات بدون تدخل المستخدم." - "إزالة الاختصارات" - "للسماح للتطبيق بإزالة الاختصارات بدون تدخل المستخدم." "قراءة إعدادات واختصارات الشاشة الرئيسية" "للسماح للتطبيق بقراءة الإعدادات والاختصارات في الشاشة الرئيسية." "كتابة إعدادات واختصارات الشاشة الرئيسية" @@ -78,30 +48,17 @@ "حدثت مشكلة أثناء تحميل الأداة" "الإعداد" "هذا تطبيق نظام وتتعذر إزالته." - "قاذفة صواريخ" "مجلد بدون اسم" - "‏الشاشة الرئيسية %1$d" "‏الصفحة %1$d من %2$d" "‏الشاشة الرئيسية %1$d من %2$d" - "‏صفحة التطبيقات %1$d من %2$d" - "‏صفحة الأدوات %1$d من %2$d" "مرحبًا" - "تصرف على راحتك." - - - "إنشاء المزيد من الشاشات للتطبيقات والمجلدات" "نسخ رموز التطبيقات" "هل تريد استيراد رموز ومجلدات من الشاشات الرئيسية القديمة؟" "نسخ الرموز" "بداية جديدة" - "تنظيم مساحتك" - "المس مع الاستمرار الجزء الخلفي من صورة الشاشة لإدارة الخلفية والأدوات والإعدادات." "الخلفيات والأدوات والإعدادات" "المس مع الاستمرار الخلفية لتخصيصها" "حسنًا" - "إليك المجلد" - "لإنشاء مجلد مثل هذا، المس أحد التطبيقات مع استمرار اللمس، ثم حركه فوق آخر." - "موافق" "تم فتح المجلد، بمقاس %1$d في %2$d" "المس لإغلاق المجلد" "المس لحفظ إعادة التسمية" @@ -111,14 +68,34 @@ "الأدوات" "الخلفيات" "الإعدادات" - "انتظار" - "جارٍ التنزيل" - "جارٍ التثبيت" + "السماح بالتدوير" "غير معروفة" - "استعادة مخفقة" - "إزالة الكل" "إزالة" "بحث" "لم يتم تثبيت هذا التطبيق" "لم يتم تثبيت تطبيق لهذا الرمز. يمكنك إزالته أو البحث عن التطبيق وتثبيته يدويًا." + "إضافة إلى الشاشة الرئيسية" + "نقل العنصر إلى هنا" + "تمت إضافة العنصر إلى الشاشة الرئيسية" + "تم حذف العنصر" + "نقل العنصر" + "نقل إلى الصف %1$s العمود %2$s" + "نقل إلى الموضع %1$s" + "نقل إلى الموضع المفضل %1$s" + "تم نقل العنصر" + "إضافة إلى المجلد: %1$s" + "إضافة إلى المجلد الذي به %1$s" + "تمت إضافة العنصر إلى المجلد" + "إنشاء مجلد يتضمن: %1$s" + "تم إنشاء المجلد" + "نقل إلى الشاشة الرئيسية" + "نقل الشاشة إلى اليسار" + "نقل الشاشة إلى اليمين" + "تم نقل الشاشة" + "تغيير حجم" + "زيادة العرض" + "زيادة الارتفاع" + "تقليل العرض" + "تقليل الارتفاع" + "تم تغيير حجم الأداة إلى العرض %1$s والارتفاع %2$s" diff --git a/res/values-az-rAZ/strings.xml b/res/values-az-rAZ/strings.xml index 0da8364ab6..099a2208d8 100644 --- a/res/values-az-rAZ/strings.xml +++ b/res/values-az-rAZ/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Əsas səhifə" - "Əsas Android Tətbiqləri" + "İş" "Tətbiq quraşdırılmayıb." "Tətbiq əlçatmazdır" "Güvənli rejimdə icazə verilməyən tətbiq endirildi" "Vidcetlər Güvənli rejimdə deaktiv edilib" - "Vidcetlər" - "Vidcetlər" "Suvenirləri göstər" "Vidceti götürmək üçün toxunub saxlayın." "%1$d × %2$d" - "Bu Əsas ekrana element atmaq mümkün olmadı." - "Yaratmaq üçün vidcet seçin" - "Qovluq adı" - "Qovluq adını dəyiş" - "OK" - "Ləğv et" - "Əsas ekrana əlavə et" - "Tətbiqlər" - "Qısa yollar" - "Vidcet" - "Əsas ekranlarınızda boş yer yoxdur." + "Tətbiq Axtarın" + "Tətbiqlər endirilir..." + "\"%1$s\" sorğusuna uyğun Tətbiqlər tapılmadı" "Bu Əsas ekranda boş yer yoxdur." "Favoritlər-də yer yoxdur" - "Bu vidcet Favorit-ə yerləşdirmək üçün çox böyükdür" - "\"%s\" qısayolu yaradılıb." - "\"%s\" qısayolu kənarlaşdırıldı." - "\"%s\" qısayolu artıq mövcuddur." - "Qısayol seçin" - "Tətbiq seçin" "Tətbiqlər" "Əsas səhifə" - "Yığışdır" - "Sistemdən sil" "Kənarlaşdır" "Sistemdən sil" "Tətbiq məlumatı" - "Tətbiqlər" - "Kənarlaşdır" - "Güncəlləməni sistemdən sil" - "Tətbiqi sistemdən sil" - "Tətbiq məlumatları" - "1 tətbiq seçildi" - "1 vidcet seçilib" - "1 qovluq seçildi" - "1 qısayol seçilib" "qısayolları quraşdır" "Tətbiqə istifadəçi müdaxiləsi olmadan qısayolları əlavə etməyə icazə verir." - "qısayolları sistemdən sil" - "İstifadəçi müdaxiləsi olmadan tətbiqə qısayolları silməyə icazə verir." "Əsas Səhifə ayarlarını və qısayolları oxuyun" "Tətbiqə Əsas Səhifədə parametrləri və qısayolları oxumağa icazə verir." "Əsas Səhifə ayarlarını və qısayolları yazın" @@ -78,30 +48,17 @@ "Vidcet yükləmə problemi" "Quraşdırma" "Bu sistem tətbiqi olduğu üçün sistemdən silinə bilməz." - "Raket Başladıcı" "Adsız Qovluq" - "Əsas Səhifə ekranı %1$d" "Səhifə %1$d of %2$d" "Əsas Səhifə ekranı %1$d of %2$d" - "%2$d tətbiq səhifəsi %1$d" - "%2$d vidcet səhifəsi %1$d" "Xoş gəlmisiniz" - "Özünüzü evinizdəki kimi hiss edin." - - - "Tətbiqlər və qovluqlar üçün daha çox ekran yaradın" "Tətbiq ikonalarınızı kopyalayın" "Köhnə Əsas ekranınızda olan ikonalar və qovluqlar import edilsin?" "İKONALARI KOPYALA" "YENİDƏN BAŞLA" - "Sahənizi təşkil edin" - "Divar kağızını idarə etmək üçün arxa fona toxunun və saxlayın, vidcetlər və ayarlar." "Divar kağızları, vidcetlər və ayarlar" "Fərdiləşdirmək üçün arxa fona toxunaraq saxlayın" "ANLADIM" - "Burada qovluq var" - "Bunun kimi birini yaradın, tətbiqə toxunun və saxlayın, sonra da bunu digərinin üstünə daşıyın." - "OK" "Qovluq açıldı, %2$d hündürlük ilə %1$d enində" "Qovluğu bağlamaq üçün toxunun" "Ad dəyişikliyini yadda saxlamaq üçün toxunun" @@ -111,14 +68,34 @@ "Vidcet" "Divar kağızları" "Ayarlar" - "Gözlənilir" - "Endirilir" - "Quraşdırılır" + "Fırlatmağa icazə verin" "Naməlum" - "Bərpa edilməyib" - "Hamısını Silin" "Yığışdır" "Axtarış" "Bu tətbiq quraşdırılmayıb" "Bu ikona üçün tətbiq quraşdırılmayıb. Onu silə bilərsiniz, və ya tətbiqi taparaq manual yol ilə quraşdıra bilərsiniz." + "Əsas ekrana əlavə edin" + "Elementi bura köçürün" + "Element əsas ekrana əlavə edildi" + "Element silindi" + "Elementi köçürün" + "Sıra %1$s sütun %2$s köçürün" + "%1$s mövqeyinə köçürün" + "%1$s sevimlilər mövqeyinə köçürün" + "Elementin yeri dəyişildi" + "Qovluğa əlavə edin: %1$s" + "%1$s adlı qovluğa əlavə edin" + "Element qovluğa əlavə edildi" + "Qovluq yaradın: %1$s" + "Qovluq yaradıldı" + "Əsas ekrana köçürün" + "Ekranı sola köçürün" + "Ekranı sağa köçürün" + "Ekran köçürülüb" + "Ölçüsünü dəyişin" + "Eni artırın" + "Hündürlüyü artırın" + "Eni azaldın" + "Hündürlüyü azaldın" + "Vidcetin eni %1$s hündürlüyü %2$s kimi ölçüləndirildi" diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml index 2015158d9b..fc5390b806 100644 --- a/res/values-be/strings.xml +++ b/res/values-be/strings.xml @@ -173,8 +173,6 @@ - - @@ -199,10 +197,6 @@ - - - - diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index 3f710db307..eeaa1ede47 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Начало" - "Основни приложения на Android" + "Работа" "Приложението не е инсталирано." "Приложението не е налично" "Изтегленото приложение е деактивирано в безопасния режим" "Приспособленията са деактивирани в безопасния режим" - "Приспособления" - "Приспособления" "Показване на паметта" "Докоснете и задръжте за избор на приспособление." "%1$d × %2$d" - "Не можа да се премести на този начален екран." - "Избор на приспособл. за създаване" - "Име на папката" - "Преименуване на папка" - "ОK" - "Отказ" - "Добавяне към началния екран" - "Приложения" - "Преки пътища" - "Приспособления" - "На началните ви екрани няма повече място." + "Търсене в приложенията" + "Приложенията се зареждат…" + "Няма намерени приложения, съответстващи на „%1$s“" "На този начален екран няма повече място." "Няма повече място в областта с любимите" - "Това приспособление е твърде голямо за областта с любимите" - "Прекият път към %s е създаден." - "Прекият път към %s бе премахнат." - "Прекият път към %s вече съществува." - "Избор на пряк път" - "Избор на приложение" "Приложения" "Начало" - "Премахване" - "Деинсталиране" "Премахване" "Деинсталиране" "Информация за приложението" - "Приложения" - "Премахване" - "Деинст. на актуализацията" - "Деинсталиране на приложението" - "Подробности за приложението" - "Избрано е 1 приложение" - "Избрано е 1 приспособление" - "Избрана е 1 папка" - "Избран е 1 пряк път" "инсталиране на преки пътища" "Разрешава на приложението да добавя преки пътища без намеса на потребителя." - "деинсталиране на преки пътища" - "Разрешава на приложението да премахва преки пътища без намеса на потребителя." "четене на настройките и преките пътища в Начало" "Разрешава на приложението да чете настройките и преките пътища в Начало." "запис на настройките и преките пътища в Начало" @@ -78,30 +48,17 @@ "Проблем при зареждане на приспособлението" "Настройване" "Това е системно приложение и не може да се деинсталира." - "Ракетна площадка" "Папка без име" - "Начален екран %1$d" "Страница %1$d от %2$d" "Начален екран %1$d от %2$d" - "Страница с приложения %1$d от %2$d" - "Страница с приспособления %1$d от %2$d" "Добре дошли" - "Персонализиране и приспособяване." - - - "Създаване на още екрани за приложения и папки" "Икони на прилож. ви: Копиране" "Да се импортират ли иконите и папките от старите ви начални екрани?" "КОПИРАНЕ НА ИКОНИТЕ" "СТАРТИРАНЕ ОТНАЧАЛО" - "Организиране на мястото ви" - "Докоснете и задръжте фона, за да управлявате тапета, приспособленията и настройките." "Тапети, приспособления и настройки" "Докоснете и задръжте фона за персонализиране" "РАЗБРАХ" - "Ето една папка" - "За да създадете подобна, докоснете и задръжте приложение, след което го преместете върху друго." - "ОK" "Папката е отворена – %1$d на %2$d" "Докоснете, за да затворите папката" "Докоснете, за да запазите новото име" @@ -111,14 +68,34 @@ "Приспособления" "Тапети" "Настройки" - "Изчаква" - "Изтегля се" - "Инсталира се" + "Разрешаване на завъртането" "Няма информация" - "Не е възстановено" - "Премахване на всички" "Премахване" "Търсене" "Това приложение не е инсталирано" "Приложението за тази икона не е инсталирано. Можете да я премахнете или да потърсите приложението и да го инсталирате ръчно." + "Добавяне към началния екран" + "Преместване на елемента тук" + "Елементът е добавен към началния екран" + "Елементът е премахнат" + "Преместване на елемента" + "Преместване към ред %1$s, колона %2$s" + "Преместване към позиция %1$s" + "Преместване към позиция %1$s в любимите" + "Елементът е преместен" + "Добавяне към папката „%1$s“" + "Добавяне към папката, съдържаща %1$s" + "Елементът е добавен към папката" + "Създаване на папка с елемента „%1$s“" + "Папката е създадена" + "Преместване към началния екран" + "Преместване на екрана наляво" + "Преместване на екрана надясно" + "Екранът е преместен" + "Преоразмеряване" + "Увеличаване на ширината" + "Увеличаване на височината" + "Намаляване на ширината" + "Намаляване на височината" + "Приспособлението е преоразмерено към ширина %1$s и височина %2$s" diff --git a/res/values-bn-rBD/strings.xml b/res/values-bn-rBD/strings.xml index 143420103c..57f7b511dd 100644 --- a/res/values-bn-rBD/strings.xml +++ b/res/values-bn-rBD/strings.xml @@ -20,57 +20,27 @@ "লঞ্চার৩" - "হোম" - "Android প্রাথমিক অ্যাপ্লিকেশানগুলি" + "কাজ" "অ্যাপ্লিকেশান ইনস্টল করা নেই৷" "অ্যাপ্লিকেশান অনুপলব্ধ" "ডাউনলোড করা অ্যাপ্লিকেশান নিরাপদ মোডে অক্ষম রয়েছে" "সুরক্ষিত মোডে উইজেট নিষ্ক্রিয় থাকে" - "উইজেটগুলি" - "উইজেটগুলি" "মেম দেখান" "একটি উইজেট তুলতে তা স্পর্শ করে ধরে রাখুন৷" "%1$d × %2$d" - "এই হোম স্ক্রীনে আইটেম রাখা যায়নি৷" - "তৈরি করেতে উইজেট চয়ন করুন" - "ফোল্ডারের নাম" - "ফোল্ডার পুনঃনামকরণ করুন" - "ঠিক আছে" - "বাতিল করুন" - "হোম স্ক্রীনে যোগ করুন" - "অ্যাপ্লিকেশানগুলি" - "শর্টকাটগুলি" - "উইজেটগুলি" - "আপনার হোম স্ক্রীনগুলিতে আর কোনো জায়গা নেই৷" + "অ্যাপ্লিকেশানগুলি অনুসন্ধান করুন" + "অ্যাপ্লিকেশানগুলি লোড হচ্ছে..." + "\"%1$s\" এর সাথে মেলে এমন কোনো অ্যাপ্লিকেশান পাওয়া যায়নি" "এই হোম স্ক্রীনে আর কোনো জায়গা নেই৷" "পছন্দসই ট্রে-তে আর কোনো জায়গা নেই" - "পছন্দসই ট্রে\'র জন্য এই উইজেটটি খুবই বড়" - "শর্টকাট \"%s\" তৈরি করা হয়েছে৷" - "শর্টকাট \"%s\" সরানো হয়েছে৷" - "শর্টকাট %s আগে থেকেই আছে৷" - "শর্টকাট চয়ন করুন" - "অ্যাপ্লিকেশান চয়ন করুন" "অ্যাপ্লিকেশানগুলি" "হোম" - "সরান" - "আনইনস্টল করুন" "সরান" "আনইনস্টল করুন" "অ্যাপ্লিকেশানের তথ্য" - "অ্যাপ্লিকেশানগুলি" - "সরান" - "আপডেট আনইনস্টল করুন" - "অ্যাপ্লিকেশান আনইনস্টল করুন" - "অ্যাপ্লিকেশানের বিশদ বিবরণ" - "১টি অ্যাপ্লিকেশান নির্বাচন করা হয়েছে" - "১টি উইজেট নির্বাচন করা হয়েছে" - "১টি ফোল্ডার নির্বাচন করা হয়েছে" - "১টি শর্টকাট নির্বাচন করা হয়েছে" "শর্টকাটগুলি ইনস্টল করে" "একটি অ্যাপ্লিকেশানকে ব্যবহারকারীর হস্তক্ষেপ ছাড়াই শর্টকাটগুলি যোগ করার অনুমতি দেয়৷" - "শর্টকাটগুলি আনইনস্টল করে" - "অ্যাপ্লিকেশানটিকে ব্যবহারকারীর হস্তক্ষেপ ছাড়াই শর্টকাটগুলি সরানোর অনুমতি দেয়৷" "হোম সেটিংস এবং শর্টকাটগুলি পড়ে" "হোমে অ্যাপ্লিকেশানটিকে সেটিংস এবং শর্টকাটগুলি পড়তে দেয়৷" "হোম সেটিংস এবং শর্টকাটগুলি লেখে" @@ -78,30 +48,17 @@ "উইজেট লোড হতে সমস্যা হয়েছে" "সেটআপ" "এটি একটি সিস্টেম অ্যাপ্লিকেশান এবং আনইনস্টল করা যাবে না৷" - "রকেট লঞ্চার" "নামবিহীন ফোল্ডার" - "%1$d নম্বর হোম স্ক্রীন" "%2$dটির মধ্যে %1$dটি পৃষ্ঠা" "%2$dটির %1$d নম্বর হোম স্ক্রীন" - "%2$dটির মধ্যে %1$dটি অ্যাপ্লিকেশান পৃষ্ঠা" - "%2$dটির মধ্যে %1$dটি উইজেট পৃষ্ঠা" "স্বাগতম" - "নিজের বাড়ির মতো স্বাচ্ছন্দ্য বোধ করুন৷" - - - "অ্যাপ্লিকেশান এবং ফোল্ডারগুলির জন্য আরো স্ক্রীn তৈরি করুন" "আপনার অ্যাপ্লিকেশান আইকনগুলি অনুলিপি করুন" "আপনার পুরানো হোম স্ক্রীন থেকে আইকন এবং ফোল্ডারগুলি আমদানি করবেন?" "আইকনগুলি অনুলিপি করুন" "নতুন করে শুরু করুন" - "আপনার স্থান সংগঠিত করুন" - "ওয়ালপেপার, উইজেট এবং সেটিংস পরিচালনা করতে পটভূমি স্পর্শ করে ধরে রাখুন৷" "ওয়ালপেপার, উইজেট এবং সেটিংস" "কাস্টমাইজ করার জন্য পটভূমি স্পর্শ করে ধরে থাকুন" "বুঝেছি" - "এখানে একটি ফোল্ডার আছে" - "এটির মতো একটি তৈরি করতে, একটি অ্যাপ্লিকেশান স্পর্শ করে ধরে রাখুন, এবং তারপরে এটিকে অন্য একটির উপরে সরিয়ে নিয়ে যান৷" - "ঠিক আছে" "ফোল্ডার খোলা হয়েছে, %1$d বাই %2$d" "ফোল্ডার বন্ধ করতে স্পর্শ করুন" "পুনঃনামকরণ সংরক্ষণ করতে স্পর্শ করুন" @@ -111,14 +68,34 @@ "উইজেটগুলি" "ওয়ালপেপারগুলি" "সেটিংস" - "প্রতীক্ষা" - "ডাউনলোড হচ্ছে" - "ইনস্টল করা হচ্ছে" + "ঘূর্ণনের অনুমতি দিন" "অজানা" - "পুনঃস্থাপন করা যায়নি" - "সবগুলি সরান" "সরান" "অনুসন্ধান" "এই অ্যাপ্লিকেশানটি ইন্সটল করা নাই" "এই আইকনের অ্যাপ্লিকেশানটি ইন্সটল করা নাই। আপনি এটি সরাতে পারেন বা অ্যাপ্লিকেশানটি অনুসন্ধান করে এটি নিজে ইন্সটল করতে পারেন।" + "হোম স্ক্রীনে যোগ করুন" + "এখানে আইটেম সরান" + "হোম স্ক্রীনে আইটেম যোগ করা হয়েছে" + "আইটেম সরানো হয়েছে" + "আইটেম সরান" + "সারি %1$s কলাম %2$s এ সরান" + "অবস্থানে সরান %1$s" + "পছন্দসই অবস্থানে সরান %1$s" + "আইটেম সরানো হয়েছে" + "ফোল্ডারে যোগ করুন: %1$s" + "%1$s সহ ফোল্ডারে যোগ করা হয়েছে" + "আইটেম ফোল্ডারে যোগ করা হয়েছে" + "এর সাথে ফোল্ডার তৈরি করুন: %1$s" + "ফোল্ডার তৈরি করা হয়েছে" + "হোম স্ক্রীনে সরান" + "স্ক্রীন বাম দিকে সরান" + "স্ক্রীন ডান দিকে সরান" + "স্ক্রীন সরানো হয়েছে" + "পুনরায় আকার দিন" + "প্রস্থ বাড়ান" + "উচ্চতা বাড়ান" + "প্রস্থ কমান" + "উচ্চতা কমান" + "উইজেটের আকার প্রস্থ %1$s উচ্চতা %2$s তে পরিবর্তন করা হয়েছে" diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 89e582a814..83f284a9d4 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Inici" - "Aplicacions principals d\'Android" + "Feina" "L\'aplicació no s\'ha instal·lat." "L\'aplicació no està disponible." "L\'aplicació que has baixat està desactivada al mode segur." "En Mode segur, els widgets estan desactivats." - "Widgets" - "Widgets" "Mostra la memòria" "Mantén premut un widget per triar-lo." "%1$d × %2$d" - "No s\'ha pogut deixar anar l\'element a Inici." - "Tria el widget que vulguis crear" - "Nom de la carpeta" - "Canvi de nom de la carpeta" - "D\'acord" - "Cancel·la" - "Afegir a la pantalla d\'inici" - "Aplicacions" - "Dreceres" - "Widgets" - "No queda espai a les pantalles d\'inici." + "Cerca a les aplicacions" + "S\'estan carregant les aplicacions..." + "No s\'ha trobat cap aplicació que coincideixi amb %1$s" "Ja no queda espai en aquesta pantalla d\'inici." "No hi ha més espai a la safata Preferits." - "Aquest widget és massa gran per a la safata Preferits." - "S\'ha creat la drecera \"%s\"." - "S\'ha suprimit la drecera \"%s\"." - "La drecera \"%s\" ja existeix." - "Tria d\'una drecera" - "Tria d\'una aplicació" "Aplicacions" "Inici" - "Suprimeix" - "Desinstal·la" "Suprimeix" "Desinstal·la" "Informació de l\'aplicació" - "Aplicacions" - "Suprimeix" - "Desinstal·la l\'actualització" - "Desinstal·la l\'aplicació" - "Detalls de l\'aplicació" - "1 aplicació seleccionada" - "1 widget seleccionat" - "1 carpeta seleccionada" - "1 drecera seleccionada" "instal·la dreceres" "Permet que una aplicació afegeixi dreceres sense la intervenció de l\'usuari." - "desinstal·la dreceres" - "Permet que l\'aplicació suprimeixi dreceres sense la intervenció de l\'usuari." "llegeix la configuració i les dreceres de la pantalla d\'inici" "Permet que l\'aplicació llegeixi la configuració i les dreceres de la pantalla d\'inici." "escriu la configuració i les dreceres de la pantalla d\'inici" @@ -78,30 +48,17 @@ "S\'ha produït un problema en carregar el widget" "Configuració" "Aquesta aplicació és una aplicació del sistema i no es pot desinstal·lar." - "Rocket Launcher" "Carpeta sense nom" - "Pantalla d\'inici %1$d" "Pàgina %1$d de %2$d" "Pantalla d\'inici %1$d de %2$d" - "Pàgina d\'aplicacions %1$d de %2$d" - "Pàgina de widgets %1$d de %2$d" "Us donem la benvinguda" - "Personalitza la pantalla d\'inici" - - - "Crea més pantalles per a aplicacions i carpetes" "Copiar les icones d\'aplicació" "Importar icones i carpetes de pantalles d\'inici anteriors?" "COPIA LES ICONES." "NOU COMENÇAMENT" - "Organitza el teu espai" - "Toca i mantén premut el fons per gestionar el fons de pantalla, els widgets i la configuració." "Fons de pantalla, widgets i configuració" "Mantén premut el fons per fer personalitzacions." "D\'ACORD" - "Aquí hi ha una carpeta" - "Per crear-ne una com aquesta, mantén premuda una aplicació i, a continuació, mou-la sobre una altra." - "D\'acord" "S\'ha obert la carpeta, %1$d per %2$d" "Toca per tancar la carpeta" "Toca per desar el canvi de nom" @@ -111,14 +68,34 @@ "Widgets" "Fons de pantalla" "Configuració" - "En espera" - "S\'està baixant" - "Instal·lant" + "Permet la rotació" "Desconegut" - "No restaurat" - "Suprimeix-ho tot" "Suprimeix" "Cerca" "Aquesta aplicació no està instal·lada" "L\'aplicació d\'aquesta icona no està instal·lada. Pots suprimir-la o cercar l\'aplicació i instal·lar-la manualment." + "Afegeix a la pantalla d\'inici" + "Mou l\'element aquí" + "S\'ha afegit l\'element a la pantalla d\'inici" + "S\'ha suprimit l\'element" + "Desplaça l\'element" + "Desplaça l\'element a la fila %1$s i la columna %2$s" + "Desplaça l\'element a la posició %1$s" + "Desplaça l\'element a la posició de preferits %1$s" + "Element desplaçat" + "Afegeix l\'element a la carpeta: %1$s" + "Afegeix l\'element a la carpeta amb %1$s" + "Element afegit a la carpeta" + "Crea una carpeta amb: %1$s" + "Carpeta creada" + "Desplaça a la pantalla d\'inici" + "Desplaça pantalla a l\'esquerra" + "Desplaça pantalla a la dreta" + "S\'ha desplaçat la pantalla" + "Canvia la mida" + "Augmenta l\'amplada" + "Augmenta l\'alçada" + "Redueix l\'amplada" + "Redueix l\'alçada" + "S\'ha canviat la mida del widget a l\'amplada %1$s i l\'alçada %2$s" diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml index 0288806f46..c99a7ed840 100644 --- a/res/values-cs/strings.xml +++ b/res/values-cs/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Plocha" - "Android Core Apps" + "Práce" "Aplikace není nainstalována." "Aplikace není k dispozici." "Stažená aplikace je v nouzovém režimu zakázána" "V nouzovém režimu jsou widgety zakázány." - "Widgety" - "Widgety" "Zobrazit Mem" "Widget vyberete dotykem a podržením." "%1$d × %2$d" - "Na tuto plochu položku nelze přesunout." - "Vyberte widget k vytvoření" - "Název složky" - "Přejmenovat složku" - "OK" - "Zrušit" - "Přidat na plochu" - "Aplikace" - "Klávesové zkratky" - "Widgety" - "Na plochách již není místo." + "Hledat aplikace" + "Načítání aplikací…" + "Dotazu „%1$s“ neodpovídají žádné aplikace" "Na této ploše již není místo." "Na panelu Oblíbené položky již není místo." - "Tento widget je pro panel Oblíbené položky příliš velký." - "Zástupce aplikace %s byl vytvořen." - "Zástupce aplikace %s byl odebrán." - "Zástupce aplikace %s již existuje." - "Výběr zástupce" - "Výběr aplikace" "Aplikace" "Plocha" - "Odstranit" - "Odinstalovat" "Odstranit" "Odinstalovat" "Informace o aplikaci" - "Aplikace" - "Odstranit" - "Odinstalovat aktualizaci" - "Odinstalovat aplikaci" - "Podrobnosti o aplikaci" - "Vybrána 1 aplikace" - "Vybrán 1 widget" - "Vybrána 1 složka" - "Vybrán 1 zástupce" "instalace zástupce" "Umožňuje aplikaci přidat zástupce bez zásahu uživatele." - "odinstalovat zástupce" - "Umožňuje aplikaci odstranit zástupce bez zásahu uživatele." "čtení nastavení a odkazů plochy" "Umožňuje aplikaci číst nastavení a odkazy na ploše." "zápis nastavení a odkazů plochy" @@ -78,30 +48,17 @@ "Problém s načtením widgetu" "Nastavení" "Toto je systémová aplikace a nelze ji odinstalovat." - "Rocket Launcher" "Složka bez názvu" - "Plocha %1$d" "Strana %1$d z %2$d" "Plocha %1$d z %2$d" - "Stránka aplikací %1$d z %2$d" - "Stránka widgetů %1$d z %2$d" "Vítejte" - "Udělejte si pohodlí." - - - "Vytvořte několik obrazovek pro aplikace a složky" "Zkopírování ikon aplikací" "Chcete importovat ikony a složky ze svých starých ploch?" "ZKOPÍROVAT IKONY" "ZAČÍT S VÝCHOZÍM ROZVRŽENÍM" - "Organizace prostoru" - "Chcete-li spravovat tapetu, widgety a nastavení, dotkněte se pozadí a přidržte je." "Tapety, widgety a nastavení" "Pozadí můžete přizpůsobit klepnutím a podržením" "ROZUMÍM" - "Toto je složka" - "Chcete-li vytvořit složku, přetáhněte aplikaci na jinou aplikaci." - "OK" "Složka otevřena, rozměry %1$d x %2$d" "Dotykem složku zavřete" "Dotykem uložíte změnu názvu" @@ -111,14 +68,34 @@ "Widgety" "Tapety" "Nastavení" - "Čekání" - "Stahování" - "Instalace" + "Povolit otáčení" "Neznámé" - "Nebylo obnoveno" - "Odstranit vše" "Odstranit" "Hledat" "Tato aplikace není nainstalována" "Aplikace pro tuto ikonu není nainstalována. Můžete ikonu odstranit nebo zkusit aplikaci vyhledat a nainstalovat ručně." + "Přidat na plochu" + "Přesunout položku sem" + "Položka byla přidána na plochu" + "Položka byla odstraněna" + "Přesunout položku" + "Přesunout na řádek %1$s do sloupce %2$s" + "Přesunout na pozici %1$s" + "Přesunout do oblíbených položek na pozici %1$s" + "Položka byla přesunuta" + "Přidat do složky: %1$s" + "Přidat do složky s aplikací %1$s" + "Položka byla přidána do složky" + "Vytvořit složku s položkou %1$s" + "Složka byla vytvořena" + "Přesunout na plochu" + "Přesunout obrazovku doleva" + "Přesunout obrazovku doprava" + "Obrazovka byla přesunuta" + "Změnit velikost" + "Zvýšit šířku" + "Zvýšit výšku" + "Snížit šířku" + "Snížit výšku" + "Velikost widgetu upravena: šířka %1$s, výška %2$s" diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index 5fa6aa6b62..eeab92e6ec 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Startskærm" - "Kerneapps i Android" + "Arbejde" "Appen er ikke installeret." "Appen er ikke tilgængelig" "Downloadet app er deaktiveret i sikker tilstand" "Widgets er deaktiveret i Beskyttet tilstand" - "Widgets" - "Widgets" "Vis Mem" "Tryk på en widget, og hold den nede for at vælge." "%1$d × %2$d" - "Elementet kunne ikke trækkes til startskærmen." - "Vælg den widget, du vil oprette" - "Mappenavn" - "Omdøb mappe" - "OK" - "Annuller" - "Føj til startskærm" - "Apps" - "Genveje" - "Widgets" - "Der er ikke mere plads på dine startskærme." + "Søg i Apps" + "Indlæser apps…" + "Der blev ikke fundet nogen apps, som matcher \"%1$s\"" "Der er ikke mere plads på denne startskærm." "Der er ikke mere plads i bakken Foretrukne" - "Denne widget er for stor til bakken Foretrukne" - "Genvejen \"%s\" blev oprettet." - "Genvejen \"%s\" blev fjernet." - "Genvejen \"%s\" findes allerede." - "Vælg genvej" - "Vælg app" "Apps" "Startskærm" - "Fjern" - "Afinstaller" "Fjern" "Afinstaller" "Oplysninger om appen" - "Apps" - "Fjern" - "Afinstaller opdatering" - "Afinstaller appen" - "Oplysninger om appen" - "1 app er valgt" - "1 widget er valgt" - "1 mappe er valgt" - "1 genvej er valgt" "installere genveje" "Tillader, at en app tilføjer genveje uden brugerens indgriben." - "afinstaller genveje" - "Tillader, at appen fjerner genveje uden brugerens indgriben." "læs indstillinger og genveje for startskærmen" "Tillader, at appen læser indstillingerne og genvejene på startskærmen." "skrive indstillinger og genveje for startskærmen" @@ -78,30 +48,17 @@ "Der er problemer med indlæsning af widgetten" "Konfigurer" "Dette er en systemapp, som ikke kan afinstalleres." - "Rocket Launcher" "Unavngiven mappe" - "Startskærm %1$d" "Side %1$d ud af %2$d" "Startskærm %1$d ud af %2$d" - "Apps-side %1$d ud af %2$d" - "Widgets-side %1$d ud af %2$d" "Velkommen" - "Føl dig hjemme." - - - "Opret flere skærme til apps og mapper" "Kopiér dine appikoner" "Vil du importere ikoner og mapper fra gamle startskærme?" "KOPIÉR IKONER" "START PÅ EN FRISK" - "Organiser din arbejdsplads" - "Tryk på en baggrund, og hold fingeren nede for at administrere baggrunde, widgets og indstillinger." "Baggrunde, widgets og indstillinger" "Tryk på baggrunden, og hold fingeren nede for at tilpasse den" "OK, FORSTÅET" - "Her kan du se en mappe" - "Du kan oprette en mappe magen til denne ved at trykke på en app og holde fingeren nede, mens du flytter appen til en anden mappe." - "OK" "Mappen er åben, %1$d gange %2$d" "Tryk for at lukke mappen" "Tryk for at gemme det nye navn" @@ -111,14 +68,34 @@ "Widgets" "Baggrunde" "Indstillinger" - "Afventer" - "Downloader" - "Installerer" + "Tillad rotation" "Ukendt" - "Ikke gendannet" - "Slet alle" "Fjern" "Søg" "Denne app er ikke installeret" "Appen, der hører til dette ikon, er ikke installeret. Du kan fjerne den eller prøve at søge efter appen og installere den manuelt." + "Føj til startskærm" + "Flyt elementet hertil" + "Elementet er føjet til startskærmen" + "Elementet er fjernet" + "Flyt element" + "Flyt til række %1$s, kolonne %2$s" + "Flyt til position %1$s" + "Flyt til foretrukne position %1$s" + "Elementet blev flyttet" + "Føj til mappen: %1$s" + "Føj til mappen, der indeholder %1$s" + "Elementet blev føjet til mappen" + "Opret mappe med: %1$s" + "Mappen blev oprettet" + "Flyt til startskærmen" + "Flyt skærmen til venstre" + "Flyt skærmen til højre" + "Skærmen er flyttet" + "Tilpas størrelse" + "Øg bredden" + "Øg højden" + "Reducer bredden" + "Reducer højden" + "Størrelsen for widgetten er ændret til bredde %1$s og højde %2$s" diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 059983ecf1..ac6f76b0c7 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Startseite" - "Android Core Apps" + "Arbeit" "App ist nicht installiert." "App nicht verfügbar" "Heruntergeladene App im abgesicherten Modus deaktiviert" "Widgets im abgesicherten Modus deaktiviert" - "Widgets" - "Widgets" "Speicher anzeigen" "Zum Hinzufügen Widget berühren und halten" "%1$d × %2$d" - "Element wurde nicht auf diesem Startbildschirm abgelegt." - "Widget zum Erstellen auswählen" - "Ordnername" - "Ordner umbenennen" - "OK" - "Abbrechen" - "Zum Startbildschirm hinzufügen" - "Apps" - "Verknüpfungen" - "Widgets" - "Auf Ihren Startbildschirmen ist kein Platz mehr vorhanden." + "In Apps suchen" + "Apps werden geladen..." + "Keine Apps für \"%1$s\" gefunden" "Auf diesem Startbildschirm ist kein Platz mehr vorhanden." "Ablage \"Favoriten\" ist voll." - "Dieses Widget ist zu groß für die Ablage \"Favoriten\"." - "Verknüpfung \"%s\" wurde erstellt." - "Verknüpfung \"%s\" wurde entfernt." - "Verknüpfung \"%s\" ist bereits vorhanden." - "Verknüpfung auswählen" - "App auswählen" "Apps" "Startseite" - "Entfernen" - "Deinstallieren" "Entfernen" "Deinstallieren" "App-Info" - "Apps" - "Entfernen" - "Update deinstallieren" - "App deinstallieren" - "App-Details" - "1 App ausgewählt" - "1 Widget ausgewählt" - "1 Ordner ausgewählt" - "1 Verknüpfung ausgewählt" "Verknüpfungen installieren" "Ermöglicht einer App das Hinzufügen von Verknüpfungen ohne Eingreifen des Nutzers" - "Verknüpfungen deinstallieren" - "Ermöglicht einer App das Entfernen von Verknüpfungen ohne Eingreifen des Nutzers" "Einstellungen und Verknüpfungen auf dem Startbildschirm lesen" "Ermöglicht der App, die Einstellungen und Verknüpfungen auf dem Startbildschirm zu lesen" "Einstellungen und Verknüpfungen für den Startbildschirm schreiben" @@ -78,30 +48,17 @@ "Problem beim Laden des Widgets" "Einrichten" "Dies ist eine Systemanwendung, die nicht deinstalliert werden kann." - "Rocket Launcher" "Unbenannter Ordner" - "Startbildschirm %1$d" "Seite %1$d von %2$d" "Startbildschirm %1$d von %2$d" - "Apps-Seite %1$d von %2$d" - "Widgets-Seite %1$d von %2$d" "Hallo!" - "Gerät personalisieren" - - - "Mehr Bildschirme für Apps und Ordner erstellen" "App-Symbole kopieren" "Symbole und Ordner alter Startbildschirme importieren?" "Symbole kopieren" "Standardübersicht verwenden" - "Arbeitsbereich organisieren" - "Hintergrund berühren und halten, um Hintergrund, Widgets und Einstellungen zu verwalten" "Hintergründe, Widgets & Einstellungen" "Berühren und halten Sie den Hintergrund, um ihn anzupassen." "OK" - "Hier ist ein Ordner" - "Um einen Ordner zu erstellen, berühren und halten Sie eine App und verschieben Sie sie auf eine andere." - "OK" "Ordner geöffnet, %1$d x %2$d" "Ordner durch Berühren schließen" "Umbenennung durch Berühren speichern" @@ -111,14 +68,34 @@ "Widgets" "Hintergründe" "Einstellungen" - "Warten" - "Download läuft" - "Installation" + "Drehung zulassen" "Unbekannt" - "Nicht wiederhergestellt" - "Alle entfernen" "Entfernen" "Suchen" "Diese App ist nicht installiert" "Die App für dieses Symbol ist nicht installiert. Sie können das Symbol entfernen oder nach der App suchen und sie manuell installieren." + "Zum Startbildschirm hinzufügen" + "Element hierhin verschieben" + "Element zum Startbildschirm hinzugefügt" + "Element entfernt" + "Element verschieben" + "In Zeile %1$s, Spalte %2$s verschoben" + "Auf Position %1$s verschoben" + "Auf Favoritenposition %1$s verschoben" + "Element verschoben" + "Zum Ordner \"%1$s\" hinzufügen" + "Zum Ordner hinzufügen, in dem sich %1$s befindet" + "Element zum Ordner hinzugefügt" + "Anhand von %1$s Ordner erstellen" + "Ordner erstellt" + "Auf Startbildschirm verschieben" + "Bildschirm nach links" + "Bildschirm nach rechts" + "Bildschirm verschoben" + "Größe anpassen" + "Breite vergrößern" + "Höhe vergrößern" + "Breite verringern" + "Höhe verringern" + "Größe des Widgets zu Breite %1$s und Höhe %2$s geändert" diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 7e5989d3e4..a60458a973 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Αρχική οθόνη" - "Βασικές εφαρμογές Android" + "Εργασία" "Η εφαρμογή δεν έχει εγκατασταθεί." "Η εφαρμογή δεν είναι διαθέσιμη" "Η λήψη εφαρμογών απενεργοποήθηκε στην Ασφαλή λειτουργία" "Τα γραφικά στοιχεία απενεργοποιήθηκαν στην ασφαλή λειτουργία" - "Γραφικά στοιχεία" - "Γραφικά στοιχεία" "Εμφάνιση Mem" "Αγγίξτε παρατεταμένα για να πάρετε ένα γραφ.στοιχ." "%1$d × %2$d" - "Αδυναμία τοποθέτησης στοιχείου στην Αρχική οθόνη." - "Επιλ. γραφ. στοιχείο για δημιουργία" - "Όνομα φακέλου" - "Μετονομασία φακέλου" - "OK" - "Ακύρωση" - "Προσθήκη στην αρχική οθόνη" - "Εφαρμογές" - "Συντομεύσεις" - "Γραφικά στοιχεία" - "Δεν υπάρχει άλλος χώρος στις Αρχικές οθόνες σας." + "Αναζήτηση εφαρμογών" + "Φόρτωση εφαρμογών…" + "Δεν βρέθηκαν εφαρμογές για το ερώτημα \"%1$s\"" "Δεν υπάρχει χώρος σε αυτήν την αρχική οθόνη." "Δεν υπάρχει επιπλέον χώρος στην περιοχή Αγαπημένα" - "Αυτό το γραφικό στοιχείο είναι πολύ μεγάλο για την περιοχή Αγαπημένα." - "Δημιουργήθηκε η συντόμευση \"%s\"." - "Η συντόμευση \"%s\" καταργήθηκε." - "Η συντόμευση \"%s\" υπάρχει ήδη." - "Επιλογή συντόμευσης" - "Επιλογή εφαρμογής" "Εφαρμογές" "Αρχική οθόνη" - "Κατάργηση" - "Κατάργηση εγκατάστασης" "Κατάργηση" "Κατάργηση εγκατάστασης" "Πληροφορίες εφαρμογής" - "Εφαρμογές" - "Κατάργηση" - "Κατάργηση εγκατάστασης ενημέρωσης" - "Κατάργηση εγκατάστασης εφαρμογής" - "Λεπτομέρειες εφαρμογής" - "Επιλέχτηκε 1 εφαρμογή" - "Επιλέχτηκε 1 γραφικό στοιχείο" - "Επιλέχτηκε 1 φάκελος" - "Επιλέχτηκε 1 συντόμευση" "εγκατάσταση συντομεύσεων" "Επιτρέπει σε μια εφαρμογή την προσθήκη συντομεύσεων χωρίς την παρέμβαση του χρήστη." - "κατάργηση εγκατάστασης συντομεύσεων" - "Επιτρέπει στην εφαρμογή την κατάργηση συντομεύσεων χωρίς την παρέμβαση του χρήστη." "ανάγνωση ρυθμίσεων και συντομεύσεων αρχικής οθόνης" "Επιτρέπει στην εφαρμογή την ανάγνωση των ρυθμίσεων και των συντομεύσεων στην Αρχική οθόνη." "εγγραφή ρυθμίσεων και συντομεύσεων αρχικής οθόνης" @@ -78,30 +48,17 @@ "Παρουσιάστηκε πρόβλημα στη φόρτωση του γραφικού στοιχείου" "Ρύθμιση" "Αυτή είναι μια εφαρμογή συστήματος και δεν είναι δυνατή η κατάργηση της εγκατάστασής της." - "Rocket Launcher" "Φάκελος χωρίς όνομα" - "Αρχική οθόνη %1$d" "Σελίδα %1$d από %2$d" "Αρχική οθόνη %1$d από %2$d" - "Σελίδα εφαρμογών %1$d από %2$d" - "Σελίδα γραφικών στοιχείων %1$d από %2$d" "Καλώς ορίσατε" - "Νιώστε σαν στο σπίτι σας." - - - "Δημιουργία περισσότερων οθονών για εφαρμογές και φακέλους" "Αντιγραφή εικονιδίων εφαρμογών" "Εισαγωγή εικονιδίων και φακέλων από παλιές αρχικές οθόνες;" "ΑΝΤΙΓΡΑΦΗ ΕΙΚΟΝΙΔΙΩΝ" "ΝΕΑ ΕΝΑΡΞΗ" - "Οργανώστε το χώρο σας" - "Αγγίξτε παρατεταμένα το φόντο για να διαχειριστείτε την ταπετσαρία, τα γραφικά στοιχεία και τις ρυθμίσεις." "Ταπετσαρίες, γραφικά στοιχεία και ρυθμίσεις" "Αγγίξτε παρατεταμένα το παρασκήνιο για προσαρμογή" "ΕΓΙΝΕ" - "Ορίστε ένας φάκελος" - "Για να δημιουργήσετε έναν φάκελο σαν κι αυτόν, πατήστε παρατεταμένα μια εφαρμογή και στη συνέχεια, μετακινήστε τη πάνω σε μια άλλη." - "OK" "Άνοιγμα φακέλου, %1$d επί %2$d" "Αγγίξτε για να κλείσετε τον φάκελο" "Αγγίξτε για να αποθηκεύσετε το νέο όνομα" @@ -111,14 +68,34 @@ "Γραφικά στοιχεία" "Ταπετσαρίες" "Ρυθμίσεις" - "Αναμονή" - "Λήψη " - "Εγκατάσταση" + "Να επιτρέπεται η περιστροφή" "Άγνωστο" - "Δεν ανακτήθηκε" - "Κατάργηση όλων" "Κατάργηση" "Αναζήτηση" "Αυτή η εφαρμογή δεν είναι εγκατεστημένη" "Η εφαρμογή γι\' αυτό το εικονίδιο δεν είναι εγκατεστημένη. Μπορείτε να το καταργήσετε ή να αναζητήσετε την εφαρμογή και να την εγκαταστήσετε με μη αυτόματο τρόπο." + "Προσθήκη στην αρχική οθόνη" + "Μετακίνηση στοιχείου εδώ" + "Το στοιχείο προστέθηκε στην αρχική οθόνη" + "Το στοιχείο καταργήθηκε" + "Μετακίνηση στοιχείου" + "Μετακίνηση στη σειρά %1$s, στήλη %2$s" + "Μετακίνηση στη θέση %1$s" + "Μετακίνηση στη θέση %1$s στα αγαπημένα" + "Το στοιχείο καταργήθηκε" + "Προσθήκη στο φάκελο: %1$s" + "Προσθήκη στο φάκελο με την εφαρμογή %1$s" + "Το στοιχείο προστέθηκε στο φάκελο" + "Δημιουργία φακέλου με: %1$s" + "Δημιουργήθηκε φάκελος" + "Μετακίνηση Αρχικής οθόνης" + "Μετακίνηση οθόνης αριστερά" + "Μετακίνηση οθόνης δεξιά" + "Η οθόνη μετακινήθηκε" + "Προσαρμογή μεγέθους" + "Αύξηση του πλάτους" + "Αύξηση του ύψους" + "μείωση του πλάτους" + "Μείωση του ύψους" + "Έγινε προσαρμογή του μεγέθους του γραφικού στοιχείου σε %1$s πλάτος και %2$s ύψος" diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml index 0d004fef56..09963e422d 100644 --- a/res/values-en-rAU/strings.xml +++ b/res/values-en-rAU/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Home" - "Android Core Apps" + "Work" "App isn\'t installed." "App isn\'t available" "Downloaded app disabled in Safe mode" "Widgets disabled in Safe mode" - "Widgets" - "Widgets" "Show Mem" "Touch & hold to pick up a widget." "%1$d × %2$d" - "Couldn\'t drop item on this Home screen." - "Choose widget to create" - "Folder name" - "Rename folder" - "OK" - "Cancel" - "Add to Home screen" - "Apps" - "Shortcuts" - "Widgets" - "No more room on your Home screens." + "Search Apps" + "Loading Apps…" + "No Apps found matching \"%1$s\"" "No more room on this Home screen." "No more room in the Favourites tray" - "This widget is too large for the Favourites tray" - "Shortcut \"%s\" created." - "Shortcut \"%s\" was removed." - "Shortcut \"%s\" already exists." - "Choose shortcut" - "Choose app" "Apps" "Home" - "Remove" - "Uninstall" "Remove" "Uninstall" "App info" - "Apps" - "Remove" - "Uninstall update" - "Uninstall app" - "App details" - "1 app selected" - "1 widget selected" - "1 folder selected" - "1 shortcut selected" "install shortcuts" "Allows an app to add shortcuts without user intervention." - "uninstall shortcuts" - "Allows the app to remove shortcuts without user intervention." "read Home settings and shortcuts" "Allows the app to read the settings and shortcuts in Home." "write Home settings and shortcuts" @@ -78,30 +48,17 @@ "Problem loading widget" "Setup" "This is a system app and can\'t be uninstalled." - "Rocket Launcher" "Unnamed Folder" - "Home screen %1$d" "Page %1$d of %2$d" "Home screen %1$d of %2$d" - "Apps page %1$d of %2$d" - "Widgets page %1$d of %2$d" "Welcome" - "Make yourself at home." - - - "Create more screens for apps and folders" "Copy your app icons" "Import icons and folders from your old Home screens?" "COPY ICONS" "START AFRESH" - "Organise your space" - "Touch & hold background to manage wallpaper, widgets and settings." "Wallpapers, widgets, & settings" "Touch & hold background to customise" "GOT IT" - "Here\'s a folder" - "To create one like this, touch & hold an app, then move it over another." - "OK" "Folder opened, %1$d by %2$d" "Touch to close folder" "Touch to save rename" @@ -111,14 +68,34 @@ "Widgets" "Wallpapers" "Settings" - "Waiting" - "Downloading" - "Installing" + "Allow rotation" "Unknown" - "Not restored" - "Remove All" "Remove" "Search" "This app is not installed" "The app for this icon isn\'t installed. You can remove it, or search for the app and install it manually." + "Add to Home screen" + "Move item here" + "Item added to home screen" + "Item removed" + "Move item" + "Move to row %1$s column %2$s" + "Move to position %1$s" + "Move to favourites position %1$s" + "Item moved" + "Add to folder: %1$s" + "Add to folder with %1$s" + "Item added to folder" + "Create folder with: %1$s" + "Folder created" + "Move to Home screen" + "Move screen to left" + "Move screen to right" + "Screen moved" + "Re-size" + "Increase width" + "Increase height" + "Decrease width" + "Decrease height" + "Widget re-sized to width %1$s height %2$s" diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml index 0d004fef56..09963e422d 100644 --- a/res/values-en-rGB/strings.xml +++ b/res/values-en-rGB/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Home" - "Android Core Apps" + "Work" "App isn\'t installed." "App isn\'t available" "Downloaded app disabled in Safe mode" "Widgets disabled in Safe mode" - "Widgets" - "Widgets" "Show Mem" "Touch & hold to pick up a widget." "%1$d × %2$d" - "Couldn\'t drop item on this Home screen." - "Choose widget to create" - "Folder name" - "Rename folder" - "OK" - "Cancel" - "Add to Home screen" - "Apps" - "Shortcuts" - "Widgets" - "No more room on your Home screens." + "Search Apps" + "Loading Apps…" + "No Apps found matching \"%1$s\"" "No more room on this Home screen." "No more room in the Favourites tray" - "This widget is too large for the Favourites tray" - "Shortcut \"%s\" created." - "Shortcut \"%s\" was removed." - "Shortcut \"%s\" already exists." - "Choose shortcut" - "Choose app" "Apps" "Home" - "Remove" - "Uninstall" "Remove" "Uninstall" "App info" - "Apps" - "Remove" - "Uninstall update" - "Uninstall app" - "App details" - "1 app selected" - "1 widget selected" - "1 folder selected" - "1 shortcut selected" "install shortcuts" "Allows an app to add shortcuts without user intervention." - "uninstall shortcuts" - "Allows the app to remove shortcuts without user intervention." "read Home settings and shortcuts" "Allows the app to read the settings and shortcuts in Home." "write Home settings and shortcuts" @@ -78,30 +48,17 @@ "Problem loading widget" "Setup" "This is a system app and can\'t be uninstalled." - "Rocket Launcher" "Unnamed Folder" - "Home screen %1$d" "Page %1$d of %2$d" "Home screen %1$d of %2$d" - "Apps page %1$d of %2$d" - "Widgets page %1$d of %2$d" "Welcome" - "Make yourself at home." - - - "Create more screens for apps and folders" "Copy your app icons" "Import icons and folders from your old Home screens?" "COPY ICONS" "START AFRESH" - "Organise your space" - "Touch & hold background to manage wallpaper, widgets and settings." "Wallpapers, widgets, & settings" "Touch & hold background to customise" "GOT IT" - "Here\'s a folder" - "To create one like this, touch & hold an app, then move it over another." - "OK" "Folder opened, %1$d by %2$d" "Touch to close folder" "Touch to save rename" @@ -111,14 +68,34 @@ "Widgets" "Wallpapers" "Settings" - "Waiting" - "Downloading" - "Installing" + "Allow rotation" "Unknown" - "Not restored" - "Remove All" "Remove" "Search" "This app is not installed" "The app for this icon isn\'t installed. You can remove it, or search for the app and install it manually." + "Add to Home screen" + "Move item here" + "Item added to home screen" + "Item removed" + "Move item" + "Move to row %1$s column %2$s" + "Move to position %1$s" + "Move to favourites position %1$s" + "Item moved" + "Add to folder: %1$s" + "Add to folder with %1$s" + "Item added to folder" + "Create folder with: %1$s" + "Folder created" + "Move to Home screen" + "Move screen to left" + "Move screen to right" + "Screen moved" + "Re-size" + "Increase width" + "Increase height" + "Decrease width" + "Decrease height" + "Widget re-sized to width %1$s height %2$s" diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml index 0d004fef56..09963e422d 100644 --- a/res/values-en-rIN/strings.xml +++ b/res/values-en-rIN/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Home" - "Android Core Apps" + "Work" "App isn\'t installed." "App isn\'t available" "Downloaded app disabled in Safe mode" "Widgets disabled in Safe mode" - "Widgets" - "Widgets" "Show Mem" "Touch & hold to pick up a widget." "%1$d × %2$d" - "Couldn\'t drop item on this Home screen." - "Choose widget to create" - "Folder name" - "Rename folder" - "OK" - "Cancel" - "Add to Home screen" - "Apps" - "Shortcuts" - "Widgets" - "No more room on your Home screens." + "Search Apps" + "Loading Apps…" + "No Apps found matching \"%1$s\"" "No more room on this Home screen." "No more room in the Favourites tray" - "This widget is too large for the Favourites tray" - "Shortcut \"%s\" created." - "Shortcut \"%s\" was removed." - "Shortcut \"%s\" already exists." - "Choose shortcut" - "Choose app" "Apps" "Home" - "Remove" - "Uninstall" "Remove" "Uninstall" "App info" - "Apps" - "Remove" - "Uninstall update" - "Uninstall app" - "App details" - "1 app selected" - "1 widget selected" - "1 folder selected" - "1 shortcut selected" "install shortcuts" "Allows an app to add shortcuts without user intervention." - "uninstall shortcuts" - "Allows the app to remove shortcuts without user intervention." "read Home settings and shortcuts" "Allows the app to read the settings and shortcuts in Home." "write Home settings and shortcuts" @@ -78,30 +48,17 @@ "Problem loading widget" "Setup" "This is a system app and can\'t be uninstalled." - "Rocket Launcher" "Unnamed Folder" - "Home screen %1$d" "Page %1$d of %2$d" "Home screen %1$d of %2$d" - "Apps page %1$d of %2$d" - "Widgets page %1$d of %2$d" "Welcome" - "Make yourself at home." - - - "Create more screens for apps and folders" "Copy your app icons" "Import icons and folders from your old Home screens?" "COPY ICONS" "START AFRESH" - "Organise your space" - "Touch & hold background to manage wallpaper, widgets and settings." "Wallpapers, widgets, & settings" "Touch & hold background to customise" "GOT IT" - "Here\'s a folder" - "To create one like this, touch & hold an app, then move it over another." - "OK" "Folder opened, %1$d by %2$d" "Touch to close folder" "Touch to save rename" @@ -111,14 +68,34 @@ "Widgets" "Wallpapers" "Settings" - "Waiting" - "Downloading" - "Installing" + "Allow rotation" "Unknown" - "Not restored" - "Remove All" "Remove" "Search" "This app is not installed" "The app for this icon isn\'t installed. You can remove it, or search for the app and install it manually." + "Add to Home screen" + "Move item here" + "Item added to home screen" + "Item removed" + "Move item" + "Move to row %1$s column %2$s" + "Move to position %1$s" + "Move to favourites position %1$s" + "Item moved" + "Add to folder: %1$s" + "Add to folder with %1$s" + "Item added to folder" + "Create folder with: %1$s" + "Folder created" + "Move to Home screen" + "Move screen to left" + "Move screen to right" + "Screen moved" + "Re-size" + "Increase width" + "Increase height" + "Decrease width" + "Decrease height" + "Widget re-sized to width %1$s height %2$s" diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml index 4408080e20..636674293f 100644 --- a/res/values-es-rUS/strings.xml +++ b/res/values-es-rUS/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Pantalla principal" - "Aplicaciones básicas de Android" + "Trabajo" "No se instaló la aplicación." "La aplicación no está disponible." "Aplicación descargada inhabilitada en modo seguro" "Widgets inhabilitados en modo seguro" - "Widgets" - "Widgets" "Mostrar memoria" "Mantén presionado el widget que desees elegir." "%1$d × %2$d" - "Error al soltar elemento en la pantalla principal" - "Elegir los widgets para crear" - "Nombre de carpeta" - "Cambiar nombre de carpeta" - "Aceptar" - "Cancelar" - "Agregar a la pantalla principal" - "Aplicaciones" - "Accesos directos" - "Widgets" - "No hay más espacio en tus pantallas principales." + "Buscar aplicaciones" + "Cargando aplicaciones…" + "No hay aplicaciones que coincidan con %1$s." "No hay más espacio en esta pantalla principal." "La bandeja de favoritos está llena." - "Este widget es demasiado grande para la bandeja de favoritos." - "Se creó el acceso directo \"%s\"." - "Se eliminó el acceso directo \"%s\"." - "El acceso directo \"%s\" ya existe." - "Elegir acceso directo" - "Elegir aplicación" "Aplicaciones" "Pantalla principal" - "Eliminar" - "Desinstalar" "Eliminar" "Desinstalar" "Información de la aplicación" - "Aplicaciones" - "Eliminar" - "Desinstalar actualización" - "Desinstalar aplicación" - "Detalles de la aplicación" - "Se seleccionó 1 aplicación." - "Se seleccionó 1 widget." - "Se seleccionó 1 carpeta." - "Se seleccionó 1 acceso directo." "instalar accesos directos" "Permite que una aplicación agregue accesos directos sin que el usuario intervenga." - "desinstalar accesos directos" - "Permite que la aplicación elimine accesos directos sin que el usuario intervenga." "leer configuración y accesos directos de la pantalla principal" "Permite que la aplicación lea la configuración y los accesos directos de la pantalla principal." "escribir configuración y accesos directos de la pantalla principal" @@ -78,30 +48,17 @@ "Problema al cargar el widget" "Configuración" "Esta es una aplicación del sistema y no se puede desinstalar." - "Lanzacohetes" "Carpeta sin nombre" - "Pantalla principal %1$d" "Página %1$d de %2$d" "Pantalla principal %1$d de %2$d" - "Página de aplicaciones %1$d de %2$d" - "Página de widgets %1$d de %2$d" "Bienvenido" - "Siéntete como en casa." - - - "Crea más pantallas para aplicaciones y carpetas." "Copiar íconos de aplicaciones" "¿Importar íconos y carpetas de pant. principales antiguas?" "COPIAR ÍCONOS" "EMPEZAR DE CERO" - "Organiza tu espacio" - "Mantén presionado el fondo para administrar el fondo de pantalla, los widgets y la configuración." "Fondos, widgets y configuración" "Mantén presionado el fondo para personalizarlo" "ENTENDIDO" - "Aquí tienes una carpeta" - "Para crear una carpeta como esta, mantén presionada una aplicación y luego muévela sobre otra." - "Aceptar" "Carpeta abierta, %1$d por %2$d" "Toca para cerrar la carpeta." "Toca para guardar el nuevo nombre." @@ -111,14 +68,34 @@ "Widgets" "Fondos de pantalla" "Configuración" - "Pendiente" - "Descargando" - "Instalando" + "Permitir la rotación" "Desconocido" - "No restaurado" - "Eliminar todo" "Eliminar" "Buscar" "Esta aplicación no está instalada" "La aplicación para este ícono no está instalada. Puedes eliminar el ícono o buscar la aplicación e instarla manualmente." + "Agregar a la pantalla principal" + "Mover elemento aquí" + "Se agregó el elemento a la pantalla principal." + "Se eliminó el elemento." + "Mover elemento" + "Mover a fila %1$s, columna %2$s" + "Mover a la posición número %1$s" + "Mover a la posición de favoritos número %1$s" + "Elemento movido" + "Agregar a carpeta: %1$s" + "Agregar a la carpeta que contiene %1$s" + "Elemento agregado a la carpeta" + "Crear carpeta con: %1$s" + "Carpeta creada" + "Mover a la pantalla principal" + "Mover pantalla a la izquierda" + "Mover pantalla a la derecha" + "Se movió la pantalla." + "Ajustar tamaño" + "Aumentar el ancho" + "Aumentar la altura" + "Reducir el ancho" + "Reducir la altura" + "Se cambió la dimensión del widget a %1$s de ancho y %2$s de alto." diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 898d0eaa53..34331fce93 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Home" - "Aplicaciones básicas de Android" + "Trabajo" "La aplicación no está instalada." "La aplicación no está disponible" "Aplicación descargada inhabilitada en modo seguro" "Widgets inhabilitados en modo seguro" - "Widgets" - "Widgets" "Mostrar memoria" "Mantén pulsado el widget que quieras seleccionar." "%1$d × %2$d" - "Error al arrastrar elemento a pantalla de inicio." - "Selecciona widget a añadir" - "Nombre de carpeta" - "Cambiar nombre de carpeta" - "Aceptar" - "Cancelar" - "Añadir a la pantalla de inicio" - "Aplicaciones" - "Accesos directos" - "Widgets" - "No queda espacio en las pantallas de inicio." + "Busca aplicaciones" + "Cargando aplicaciones…" + "No se han encontrado aplicaciones que contengan \"%1$s\"" "No queda espacio en la pantalla de inicio." "La bandeja de favoritos está completa" - "Este widget es demasiado grande para la bandeja de favoritos" - "Se ha creado el acceso directo \"%s\"." - "Se ha eliminado el acceso directo \"%s\"." - "El acceso directo \"%s\" ya existe." - "Selecciona un acceso directo" - "Selecciona una aplicación" "Aplicaciones" "Inicio" - "Eliminar" - "Desinstalar" "Eliminar" "Desinstalar" "Información de la aplicación" - "Aplicaciones" - "Eliminar" - "Desinstalar actualización" - "Desinstalar aplicación" - "Información de la aplicación" - "1 aplicación seleccionada" - "1 widget seleccionado" - "1 carpeta seleccionada" - "1 acceso directo seleccionado" "instalar accesos directos" "Permite que una aplicación añada accesos directos sin intervención del usuario." - "desinstalar accesos directos" - "Permite que la aplicación elimine accesos directos sin intervención del usuario." "leer información de accesos directos y de ajustes de la pantalla de inicio" "Permite que la aplicación consulte los ajustes y los accesos directos de la pantalla de inicio." "escribir información de accesos directos y de ajustes de la pantalla de inicio" @@ -78,30 +48,17 @@ "Problema al cargar el widget" "Configuración" "Esta aplicación es del sistema y no se puede desinstalar." - "Rocket Launcher" "Carpeta sin nombre" - "Pantalla de inicio %1$d" "Página %1$d de %2$d" "Pantalla de inicio %1$d de %2$d" - "Página de aplicaciones %1$d de %2$d" - "Página de widgets %1$d de %2$d" "Te damos la bienvenida" - "Personaliza tu pantalla de inicio." - - - "Crea más pantallas para aplicaciones y carpetas" "Copiar iconos de aplicaciones" "¿Importar iconos y carpetas de pantallas de inicio antiguas?" "COPIAR ICONOS" "AJUSTES PREDETERMINADOS" - "Organiza tu espacio" - "Mantén pulsado el fondo para gestionar el fondo de pantalla, los widgets y los ajustes." "Fondos de pantalla, widgets y ajustes" "Mantén pulsado el fondo para personalizarlo" "ENTENDIDO" - "Esto es una carpeta" - "Para crear una carpeta como esta, mantén pulsada una aplicación y muévela sobre otra." - "Aceptar" "Carpeta abierta, %1$d por %2$d" "Toca para cerrar la carpeta" "Toca para cambiar el nuevo nombre" @@ -111,14 +68,34 @@ "Widgets" "Fondos de pantalla" "Ajustes" - "Esperando" - "Descargando" - "Instalando" + "Permitir rotación" "Desconocido" - "No restaurado" - "Eliminar todo" "Eliminar" "Buscar" "Esta aplicación no está instalada" "La aplicación de este icono no está instalada. Puedes eliminar el icono o buscar la aplicación e instalarla manualmente." + "Añadir a la pantalla de inicio" + "Mover elemento aquí" + "Elemento añadido a la pantalla de inicio" + "Elemento eliminado" + "Mover elemento" + "Mover a la fila %1$s, columna %2$s" + "Mover a la posición número %1$s" + "Mover a la posición número %1$s de favoritos" + "Elemento movido" + "Añadir a carpeta: %1$s" + "Añadir a la carpeta con %1$s" + "Elemento añadido a carpeta" + "Crear carpeta con: %1$s" + "Carpeta creada" + "Mover a la pantalla de inicio" + "Mover pantalla a la izquierda" + "Mover pantalla a la derecha" + "Pantalla movida" + "Modificar tamaño" + "Aumentar ancho" + "Aumentar altura" + "Reducir ancho" + "Reducir altura" + "Se ha modificado el tamaño del widget a %1$s de ancho y %2$s de alto" diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml index 76b20aba72..c4ba823cc1 100644 --- a/res/values-et-rEE/strings.xml +++ b/res/values-et-rEE/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Avaekraan" - "Androidi tuumrakendused" + "Töö" "Rakendus pole installitud." "Rakendus ei ole saadaval" "Allalaetud rakendus on turvarežiimis keelatud" "Turvarežiimis on vidinad keelatud" - "Vidinad" - "Vidinad" "Mälu kuvamine" "Vidina valimiseks vajutage ja hoidke seda all." "%1$d × %2$d" - "Üksust ei saanud sellele avaekraanile tuua." - "Valige loomiseks vidin" - "Kausta nimi" - "Kausta ümbernimetamine" - "OK" - "Tühista" - "Avaekraanile lisamine" - "Rakendused" - "Otseteed" - "Vidinad" - "Teie avaekraanidel ei ole enam ruumi." + "Otsige rakendustest" + "Rakenduste laadimine ..." + "Päringule „%1$s” ei vastanud ükski rakendus" "Sellel avaekraanil pole enam ruumi." "Salves Lemmikud pole rohkem ruumi" - "See vidin on salve Lemmikud jaoks liiga suur" - "Otsetee „%s” on loodud." - "Otsetee „%s” on eemaldatud." - "Otsetee „%s” on juba olemas." - "Otsetee valimine" - "Rakenduse valimine" "Rakendused" "Avaekraan" - "Eemalda" - "Desinstalli" "Eemalda" "Desinstalli" "Rakenduse teave" - "Rakendused" - "Eemalda" - "Desinstalli värskendus" - "Rakenduse desinstallimine" - "Rakenduse üksikasjad" - "Valitud on 1 rakendus" - "Valitud on 1 vidin" - "Valitud on 1 kaust" - "Valitud on 1 otsetee" "installi otseteed" "Võimaldab rakendusel lisada otseteid kasutaja sekkumiseta." - "desinstalli otseteed" - "Võimaldab rakendusel eemaldada otseteid kasutaja sekkumiseta." "loe avaekraani seadeid ja otseteid" "Võimaldab rakendusel lugeda avaekraanil seadeid ja otseteid." "kirjuta avaekraani seaded ja otseteed" @@ -78,30 +48,17 @@ "Probleem vidina laadimisel" "Seadistamine" "See on süsteemirakendus ja seda ei saa desinstallida." - "Rocket Launcher" "Nimetu kaust" - "Avaekraan %1$d" "Leht %1$d/%2$d" "Avaekraan %1$d/%2$d" - "Rakenduste leht %1$d/%2$d" - "Vidinate leht %1$d/%2$d" "Tere tulemast" - "Tundke end nagu kodus." - - - "Looge rakenduste ja kaustade jaoks rohkem ekraanikuvasid" "Kopeerige rakenduste ikoonid" "Kas importida vanade avaekraanide ikoonid ja kaustad?" "KOPEERI IKOONID" "ALUSTA ALGUSEST" - "Korraldage oma ruumi" - "Taustapildi, vidinate ja seadete haldamiseks puudutage tausta ning hoidke seda all." "Taustapildid, vidinad ja seaded" "Kohandamiseks puudutage ja hoidke tausta all" "SELGE" - "Siin on kaust" - "Sarnase loomiseks vajutage ja hoidke rakendust all, seejärel viige see teise peale." - "OK" "Kaust on avatud, %1$d x %2$d" "Puudutage kausta sulgemiseks" "Puudutage uue nime salvestamiseks" @@ -111,14 +68,34 @@ "Vidinad" "Taustapildid" "Seaded" - "Ootamine" - "Allalaadimine" - "Installimine" + "Luba pööramine" "Teadmata" - "Ei taastatud" - "Eemalda kõik" "Eemalda" "Otsing" "See rakendus ei ole installitud" "Selle ikooni rakendust pole installitud. Saate selle eemaldada või rakendust otsida ja käsitsi installida." + "Lisa avaekraanile" + "Teisalda üksus siia" + "Üksus lisati avaekraanile" + "Üksus eemaldati" + "Teisalda üksus" + "Teisaldamine %1$s. rea %2$s. veergu" + "Teisaldamine %1$s. positsioonile" + "Teisaldamine lemmikute %1$s. positsioonile" + "Üksus teisaldati" + "Lisamine kausta: %1$s" + "Lisamine kausta nimega %1$s" + "Üksus lisati kausta" + "Kausta loomine nimega %1$s" + "Kaust on loodud" + "Teisalda avaekraanile" + "Teisalda ekraan vasakule" + "Teisalda ekraan paremale" + "Ekraan teisaldati" + "Muuda suurust" + "Suurenda laiust" + "Suurenda kõrgust" + "Vähenda laiust" + "Vähenda kõrgust" + "Vidina suurust muudeti. Laius: %1$s. Kõrgus: %2$s" diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml index 3b0deb85b3..afed3b5480 100644 --- a/res/values-et/strings.xml +++ b/res/values-et/strings.xml @@ -20,95 +20,34 @@ "Käivitaja" - "Kodu" - "Androidi tuumrakendused" - "Taustapildi valimiskoht:" "Määra taustapilt" "Taustapildid" "Rakendus pole installitud." - "Vidinad" "Vidina valimiseks puudutage seda pikalt." - "Pood" "%1$d × %2$d" - "Üksust ei saa sellele avaekraanile tuua." - "Valige loomiseks vidin" - "Kausta nimi" - "Nimeta kaust ümber" - "OK" - "Tühista" - "Lisa avamenüüsse" - "Rakendused" - "Otseteed" - "Vidinad" - "Teie avakuvadel ei ole enam ruumi." "Sellel avalehel pole enam ruumi." "Kohandataval dokialal pole rohkem ruumi." - "See vidin on tööpunkti jaoks liiga suur." - "Otsetee „%s” loodud." - "Otsetee „%s” eemaldatud." - "Otsetee „%s” on juba olemas." - "Otsetee valimine" - "Rakenduse valimine" "Rakendused" "Kodu" - "Eemalda" - "Desinstalli" "Eemalda" "Desinstalli" "Rakenduse teave" - "Otsing" - "Häälotsing" - "Rakendused" - "Eemalda" - "Desinstalli värskendus" - "Lisa" - "Rakenduste haldamine" - "Taustapilt" - "Otsing" - "Teadistused" - "Süsteemiseaded" - "Abi" - "Rakenduse desinstallimine" - "Rakenduse üksikasjad" - "Valitud on 1 rakendus" - "1 vidin on valitud" - "1 kaust on valitud" - "1 otsetee on valitud" "otseteede installimine" "Võimaldab rakendusel lisada otseteid kasutaja sekkumiseta." - "otseteede desinstallimine" - "Võimaldab rakendusel eemaldada otseteid kasutaja sekkumiseta." "avalehe seadete ja otseteede lugemine" "Võimaldab rakendusel lugeda avalehe seadeid ja otseteid." "avalehe seadete ja otseteede kirjutamine" "Võimaldab rakendusel muuta avalehel seadeid ja otseteid." "Probleem vidina laadimisel" "See on süsteemirakendus ja seda ei saa desinstallida." - "Rocket Launcher" "Nimeta kaust" - "Avakuva %1$d" "Leht %1$d/%2$d" "Avakuva %1$d/%2$d" - "Rakenduste leht %1$d/%2$d" - "Vidinate leht %1$d/%2$d" - "Tunne end nagu kodus" - "Võite panna oma lemmikrakendused siia." - "Kõikide oma rakenduste nägemiseks puudutage ringi." - "Valige mõned rakendused" - "Rakenduse lisamiseks avakuvale puudutage seda pikalt." - "Korraldage oma rakendused kaustadesse" - "Rakenduse liigutamiseks pange sõrm rakendusele ja hoidke seda." - "Avakuval uue kausta tegemiseks virnastage üks rakendus teisele." - "OK" "Kaust on avatud, %1$d x %2$d" "Puudutage kausta sulgemiseks" "Puudutage uue nime salvestamiseks" "Kaust suletud" "Kausta uus nimi: %1$s" "%1$s" - - - - diff --git a/res/values-eu-rES/strings.xml b/res/values-eu-rES/strings.xml index e02479c0eb..8bfca6cd1a 100644 --- a/res/values-eu-rES/strings.xml +++ b/res/values-eu-rES/strings.xml @@ -20,57 +20,27 @@ "Abiarazlea3" - "Hasiera" - "Android-en nukleoko aplikazioak" + "Lana" "Aplikazioa instalatu gabe dago." "Ez dago erabilgarri aplikazioa" "Deskargatutako aplikazioa modu seguruan desgaitu da" "Widgetak desgaitu egin dira modu seguruan" - "Widgetak" - "Widgetak" "Erakutsi memoria" "Eduki ukituta widgeta aukeratzeko." "%1$d × %2$d" - "Ezin izan da elementua hasierako pantailan jaregin." - "Aukeratu sortu beharreko widgeta" - "Karpetaren izena" - "Aldatu karpetaren izena" - "Ados" - "Utzi" - "Gehitu hasierako pantailan" - "Aplikazioak" - "Lasterbideak" - "Widgetak" - "Hasierako pantailetan ez dago toki gehiago." + "Bilatu aplikazioetan" + "Aplikazioak kargatzen…" + "Ez da aurkitu \"%1$s\" bilaketarekin bat datorren aplikaziorik" "Hasierako pantaila honetan ez dago toki gehiago." "Ez dago toki gehiago Gogokoak erretiluan" - "Widgeta handiegia da Gogokoak erretiluan ezartzeko" - "\"%s\" lasterbidea sortu da." - "\"%s\" lasterbidea kendu da." - "\"%s\" lasterbidea lehendik dago." - "Aukeratu lasterbidea" - "Aukeratu aplikazioa" "Aplikazioak" "Hasiera" - "Kendu" - "Desinstalatu" "Kendu" "Desinstalatu" "Aplikazioaren informazioa" - "Aplikazioak" - "Kendu" - "Desinstalatu eguneratzea" - "Desinstalatu aplikazioa" - "Aplikazioaren xehetasunak" - "Aplikazio bat hautatu da" - "Widget bat hautatu da" - "Karpeta bat hautatu da" - "Lasterbide bat hautatu da" "Instalatu lasterbideak" "Erabiltzaileak ezer egin gabe lasterbideak gehitzea baimentzen die aplikazioei." - "Desinstalatu lasterbideak" - "Erabiltzaileak ezer egin gabe lasterbideak kentzea baimentzen die aplikazioei." "Irakurri hasierako ezarpenak eta lasterbideak" "Hasierako pantailako ezarpenak eta lasterbideak irakurtzea baimentzen die aplikazioei." "Idatzi hasierako ezarpenak eta lasterbideak" @@ -78,30 +48,17 @@ "Arazo bat izan da widgeta kargatzean" "Konfigurazioa" "Sistema-aplikazioa da hau eta ezin da desinstalatu." - "Rocket Launcher" "Izenik gabeko karpeta" - "%1$d hasierako pantaila" "%1$d/%2$d orria" "%1$d/%2$d hasierako pantaila" - "%1$d/%2$d aplikazio-orria" - "%1$d/%2$d widget-orria" "Ongi etorri" - "Pertsonalizatu hasierako pantaila nahieran." - - - "Sortu pantaila gehiago aplikazioak eta karpetak ezartzeko" "Kopiatu aplikazioen ikonoak" "Ikonoak eta karpetak aurreko hasierako pantailatik inportatu?" "KOPIATU IKONOAK" "HASI HUTSETIK" - "Antolatu zure txokoa" - "Eduki ukituta atzeko planoa horma-paperak, widgetak eta ezarpenak kudeatzeko." "Horma-paperak, widgetak eta ezarpenak" "Pertsonalizatzeko, eduki ukituta atzeko planoa" "ADOS" - "Hortxe duzu karpeta" - "Horrelako bat sortzeko, eduki ukituta aplikazio bat eta eraman beste baten gainera." - "Ados" "Karpeta ireki da: %1$d x %2$d" "Karpeta ixteko, uki ezazu" "Karpetaren izen berria gordetzeko, uki ezazu" @@ -111,14 +68,34 @@ "Widgetak" "Horma-paperak" "Ezarpenak" - "Zain" - "Deskargatzen" - "Instalatzen" + "Baimendu biratzeko aukera" "Ezezaguna" - "Ez da leheneratu" - "Kendu guztiak" "Kendu" "Bilatu" "Aplikazio hau ez dago instalatuta" "Ikono honen aplikazioa ez dago instalatuta. Ikonoa ken dezakezu, edo aplikazioa bilatu eta eskuz instalatu." + "Gehitu hasierako pantailan" + "Ekarri elementua hona" + "Gehitu da elementua hasierako pantailan" + "Kendu da elementua" + "Mugitu elementua" + "Eraman %1$s. errenkadara, %2$s. zutabera" + "Eraman %1$s. postura" + "Eraman gogokoen %1$s. postura" + "Elementua mugitu da" + "Gehitu %1$s karpetan" + "Gehitu %1$s duen karpetan" + "Elementua karpetan gehitu da" + "Sortu karpeta %1$s elementuarekin" + "Karpeta sortu da" + "Eraman hasierako pantailara" + "Eraman pantaila ezkerrera" + "Eraman pantaila eskuinera" + "Mugitu da pantaila" + "Aldatu tamaina" + "Handitu zabalera" + "Handitu altuera" + "Txikitu zabalera" + "Txikitu altuera" + "Aldatu da widgetaren tamaina. Zabalera: %1$s. Altuera: %2$s." diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index 924cccf759..701d59d50d 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "صفحه اصلی" - "‏برنامه‌های Android Core" + "کاری" "برنامه نصب نشده است." "برنامه در دسترس نیست" "برنامه دانلود شده در حالت ایمن غیرفعال شد" "ابزارک‌ها در حالت ایمن غیرفعال هستند" - "ابزارک‌ها" - "ابزارک‌ها" "‏نمایش Mem" "برای انتخاب ابزارک لمس کنید و نگه دارید." "%1$d × %2$d" - "این مورد را نمی‌توان در این صفحه اصلی رها کرد." - "انتخاب ابزارکی که باید ایجاد شود" - "نام پوشه" - "تغییر نام پوشه" - "تأیید" - "لغو" - "افزودن به صفحه اصلی" - "برنامه‌ها" - "میان‌برها" - "ابزارک‌ها" - "فضای بیشتری در صفحات نمایش اصلی شما موجود نیست." + "جستجوی برنامه‌ها" + "در حال بارگیری برنامه‌ها..." + "هیچ برنامه‌ای مطابق با «%1$s» پیدا نشد" "فضای بیشتری در این صفحه اصلی موجود نیست." "فضای بیشتری در سینی موارد دلخواه وجود ندارد" - "این ابزارک برای سینی موارد دلخواه بسیار بزرگ است" - "میان‌بر «%s» ایجاد شد." - "میان‌بر «%s» حذف شد." - "میان‌بر «%s» در حال حاضر وجود دارد." - "انتخاب میان‌بر" - "انتخاب برنامه" "برنامه‌ها" "صفحه اصلی" - "حذف" - "حذف نصب" "حذف" "حذف نصب" "اطلاعات برنامه" - "برنامه‌ها" - "حذف" - "حذف نصب به‌روزرسانی" - "حذف نصب برنامه" - "جزئیات برنامه" - "۱ برنامه انتخاب شد" - "۱ ابزارک انتخاب شد" - "۱ پوشه انتخاب شد" - "۱ میان‌بر انتخاب شد" "نصب میان‌برها" "به برنامه اجازه می‌دهد میان‌برها را بدون دخالت کاربر اضافه کند." - "حذف نصب میان‌برها" - "به برنامه اجازه می‌دهد میان‌برها را بدون دخالت کاربر حذف کند." "خواندن تنظیمات و میان‌برهای صفحه اصلی" "به برنامه اجازه می‌دهد تنظیمات و میان‌برها را در صفحه اصلی بخواند." "نوشتن تنظیمات و میان‌برهای صفحه اصلی" @@ -78,30 +48,17 @@ "مشکل در بارگیری ابزارک" "تنظیم" "این برنامه سیستمی است و حذف نصب نمی‌شود." - "Rocket Launcher" "پوشه بی‌نام" - "‏صفحه اصلی %1$d" "‏صفحه %1$d از %2$d" "‏صفحه اصلی %1$d از %2$d" - "‏صفحه برنامه‌ها %1$d از %2$d" - "‏صفحه ابزارک‌ها %1$d از %2$d" "خوش آمدید" - "راحت باشید." - - - "صفحات بیشتری را برای برنامه‌ها و پوشه‌ها ایجاد کنید" "کپی کردن نمادهای برنامه شما" "نمادها و پوشه‌ها از صفحه‌های اصلی قدیمی شما وارد شوند؟" "کپی نمادها" "شروع تازه" - "فضای خود را سازماندهی کنید" - "برای مدیریت کاغذدیواری، ابزارک‌ها و تنظیمات، پس‌زمینه را لمس کرده و نگه‌دارید." "کاغذدیواری‌ها، ابزارک‌ها و تنظیمات" "برای سفارشی کردن، پس‌زمینه را لمس کنید و نگه‌دارید" "متوجه شدم" - "اینجا یک پوشه است" - "برای ایجاد پوشه‌ای مثل این، یک برنامه را لمس کرده و نگه‌دارید، سپس آن را روی برنامه دیگر بیاندازید." - "تأیید" "پوشه باز شده، %1$d در %2$d" "برای بستن پوشه لمس کنید" "برای ذخیره تغییر نام لمس کنید" @@ -111,14 +68,34 @@ "ابزارک‌ها" "کاغذدیواری‌ها" "تنظیمات" - "در حال انتظار" - "در حال دانلود" - "در حال نصب" + "چرخش مجاز است" "نامشخص" - "بازیابی نشد" - "حذف همه" "حذف" "جستجو" "این برنامه نصب نشده است." "برنامه برای این نماد نصب نشده است. می‌توانید آن را حذف کنید یا سعی کنید برنامه را جستجو کنید و آن را به صورت دستی نصب کنید." + "افزودن به صفحه اصلی" + "انتقال مورد به اینجا" + "مورد به صفحه اصلی اضافه شد" + "مورد حذف شد" + "انتقال مورد" + "انتقال به سطر %1$s ستون %2$s" + "انتقال به موقعیت %1$s" + "انتقال به موقعیت دلخواه %1$s" + "مورد منتقل شد" + "افزودن به پوشه: %1$s" + "افزودن به پوشه حاوی %1$s" + "مورد به پوشه اضافه شد" + "ایجاد پوشه با: %1$s" + "پوشه ایجاد شد" + "انتقال به صفحه اصلی" + "انتقال صفحه به چپ" + "انتقال صفحه به راست" + "صفحه منتقل شد" + "تغییر اندازه" + "افزایش عرض" + "افزایش ارتفاع" + "کاهش عرض" + "کاهش ارتفاع" + "اندازه ابزارک به عرض %1$s ارتفاع %2$s تغییر کرد" diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index a5da03f436..f0766d68ab 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Aloitusruutu" - "Androidin ydinsovellukset" + "Työ" "Sovellusta ei ole asennettu." "Sovellus ei ole käytettävissä" "Ladattu sovellus poistettiin käytöstä suojatussa tilassa" "Widgetit poistettu käytöstä vikasietotilassa" - "Widgetit" - "Widgetit" "Näytä muisti" "Valitse widget painamalla sitä pitkään." "%1$d × %2$d" - "Kohteen lisääminen tähän aloitusruutuun epäonnistui." - "Valitse luotava widget" - "Kansion nimi" - "Nimeä kansio uudelleen" - "OK" - "Peruuta" - "Lisää aloitusruutuun" - "Sovellukset" - "Pikakuvakkeet" - "Widgetit" - "Aloitusruuduilla ei ole enää tilaa." + "Sovellushaku" + "Ladataan sovelluksia…" + "”%1$s” ei palauttanut sovelluksia." "Tässä aloitusruudussa ei ole enää tilaa." "Suosikit-valikossa ei ole enää tilaa" - "Tämä widget on liian suuri Suosikit-valikkoon lisättäväksi" - "Pikakuvake %s luotiin." - "Pikakuvake %s poistettiin." - "Pikakuvake %s on jo olemassa." - "Valitse pikakuvake" - "Valitse sovellus" "Sovellukset" "Aloitusruutu" - "Poista" - "Poista" "Poista" "Poista" "Sovelluksen tiedot" - "Sovellukset" - "Poista" - "Poista päivitys" - "Poista sovellus" - "Sovelluksen tiedot" - "1 sovellus valittu" - "1 widget valittu" - "1 kansio valittu" - "1 pikakuvake valittu" "asenna pikakuvakkeita" "Antaa sovelluksen lisätä pikakuvakkeita itsenäisesti ilman käyttäjän valintaa." - "poista pikakuvakkeita" - "Antaa sovelluksen poistaa pikakuvakkeita ilman käyttäjän valintaa." "lue aloitusruudun asetuksia ja pikakuvakkeita" "Antaa sovelluksen lukea aloitusruudun asetukset ja pikakuvakkeet." "kirjoita aloitusruudun asetuksia ja pikakuvakkeita" @@ -78,30 +48,17 @@ "Ongelma ladattaessa widgetiä" "Asetus" "Tämä on järjestelmäsovellus, eikä sitä voi poistaa." - "Sinko" "Nimetön kansio" - "Aloitusruutu %1$d" "Sivu %1$d / %2$d" "Aloitusruutu %1$d/%2$d" - "Apps-sivu %1$d / %2$d" - "Widgetit-sivu %1$d / %2$d" "Tervetuloa" - "Ole kuin kotonasi." - - - "Luo lisää ruutuja sovelluksille ja kansioille" "Kopioi sovelluskuvakkeet" "Tuodaanko kuvakkeet ja kansiot vanhoista aloitusruuduista?" "KOPIOI KUVAKKEET" "ALOITA ALUSTA" - "Järjestä tilasi" - "Hallitse taustakuvaa, widgetejä ja asetuksia koskettamalla taustaa pitkään." "Taustakuvat, widgetit ja asetukset" "Muokkaa taustaa koskettamalla ja painamalla pitkään" "SELVÄ" - "Tässä on kansio" - "Luo se seuraavasti: kosketa sovellusta pitkään ja siirrä se sitten toisen päälle." - "OK" "Kansio avattu, koko %1$d x %2$d" "Sulje kansio koskettamalla" "Tallenna uudella nimellä koskettamalla" @@ -111,14 +68,34 @@ "Widgetit" "Taustakuvat" "Asetukset" - "Odottaa" - "Ladataan" - "Asennetaan" + "Salli kierto" "Tuntematon" - "Ei palautettu" - "Poista kaikki" "Poista" "Haku" "Sovellusta ei ole asennettu" "Kuvakkeen sovellusta ei ole asennettu. Voit poistaa kuvakkeen tai etsiä sovelluksen ja asentaa sen manuaalisesti." + "Lisää aloitusnäytölle" + "Siirrä kohde tänne" + "Kohde lisättiin aloitusnäytölle." + "Kohde poistettiin." + "Siirrä kohde" + "Siirrä rivin %1$s sarakkeeseen %2$s." + "Siirrä kohtaan %1$s." + "Siirrä suosikkien kohtaan %1$s." + "Kohde on siirretty." + "Lisää kansioon %1$s." + "Lisää samaan kansioon sovelluksen %1$s kanssa." + "Kohde on lisätty kansioon." + "Luo kansio, jossa on %1$s." + "Kansio on luotu." + "Siirrä aloitusnäytölle" + "Siirrä näyttöä vasemmalle" + "Siirrä näyttöä oikealle" + "Näyttö siirrettiin." + "Muuta kokoa" + "Lisää leveyttä" + "Lisää korkeutta" + "Vähennä leveyttä" + "Vähennä korkeutta" + "Widgetin kokoa muutettiin. Sen leveys on nyt %1$s ja korkeus %2$s." diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml index 62a2b894e8..0c19ef2687 100644 --- a/res/values-fr-rCA/strings.xml +++ b/res/values-fr-rCA/strings.xml @@ -20,57 +20,27 @@ "Lanceur3" - "Accueil" - "Applications de base Android" + "Travail" "L\'application n\'est pas installée." "Application indisponible" "L\'application téléchargée est désactivée en mode sécurisé." "Widgets désactivés en mode sans échec" - "Widgets" - "Widgets" "Afficher la mémoire" "Maintenez un doigt sur le widget pour l\'ajouter." "%1$d × %2$d" - "Imposs. de déposer l\'élément sur l\'écran d\'accueil" - "Sélectionnez le widget à créer" - "Nom du dossier" - "Renommer le dossier" - "OK" - "Annuler" - "Ajouter à l\'écran d\'accueil" - "Applications" - "Raccourcis" - "Widgets" - "Vous n\'avez plus d\'espace libre sur vos écrans d\'accueil." + "Rechercher des applications" + "Chargement des applications en cours..." + "Aucune application trouvée correspondant à « %1$s »" "Pas d\'espace libre sur l\'écran d\'accueil." "Il n\'y a plus d\'espace dans la zone des favoris" - "Ce widget est trop volumineux pour la zone des favoris" - "Le raccourci « %s » a été créé." - "Le raccourci « %s » a été supprimé." - "Le raccourci « %s » existe déjà." - "Sélectionner un raccourci" - "Sélectionner une application" "Applications" "Accueil" - "Supprimer" - "Désinstaller" "Supprimer" "Désinstaller" "Détails de l\'application" - "Applications" - "Supprimer" - "Désinstaller la mise à jour" - "Désinstaller l\'application" - "Détails de l\'application" - "1 application sélectionnée" - "1 widget sélectionné" - "1 dossier sélectionné" - "1 raccourci sélectionné" "installer des raccourcis" "Permet à une application d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur." - "désinstaller des raccourcis" - "Permet à l\'application de supprimer des raccourcis sans l\'intervention de l\'utilisateur." "lire les paramètres et les raccourcis de la page d\'accueil" "Permet à l\'application de lire les paramètres et les raccourcis de l\'écran d\'accueil." "enregistrer les paramètres de la page d\'accueil et des raccourcis" @@ -78,30 +48,17 @@ "Problème lors du chargement du widget" "Configuration" "Impossible de désinstaller cette application, car il s\'agit d\'une application système." - "Lance-missile" "Dossier sans nom" - "Écran d\'accueil %1$d" "Page %1$d sur %2$d" "Écran d\'accueil %1$d sur %2$d" - "Page des applications : %1$d sur %2$d" - "Page des widgets : %1$d sur %2$d" "Bienvenue" - "Faites comme chez vous." - - - "Créer plus d\'écrans pour les applications et les dossiers" "Copier les icônes de vos applis" "Importer les icônes et dossiers des anciens écrans d\'accueil?" "COPIER LES ICÔNES" "DISPOSITION PAR DÉFAUT" - "Organiser son espace personnel" - "Maintenez votre doigt sur l\'arrière-plan pour gérer les fonds d\'écran, les widgets et les paramètres." "Fonds d\'écran, widgets et paramètres" "Maintenez le doigt sur le fond d\'écran pour personnaliser" "J\'ai compris" - "Voici un dossier" - "Pour créer un dossier comme ça, maintenez votre doigt sur une application, puis déplacez-la sur une autre." - "OK" "Dossier ouvert, %1$d par %2$d" "Toucher pour fermer le dossier" "Toucher pour enregistrer le nouveau nom" @@ -111,14 +68,34 @@ "Widgets" "Fonds d\'écran" "Paramètres" - "En attente" - "Téléchargement..." - "Installation…" + "Permettre la rotation" "Inconnu" - "Non restauré" - "Tout supprimer" "Supprimer" "Rechercher" "Cette application n\'est pas installée" "L\'application liée à cette icône n\'est pas installée. Vous pouvez la supprimer ou rechercher l\'application et l\'installer manuellement." + "Ajouter à l\'écran d\'accueil" + "Déplacer l\'élément ici" + "Élément ajouté à l\'écran d\'accueil" + "Élément supprimé" + "Déplacer l\'élément" + "Déplacer vers rangée %1$s colonne %2$s" + "Déplacer vers la position %1$s" + "Déplacer vers la position %1$s dans les favoris" + "Élément déplacé" + "Ajouter au dossier : %1$s" + "Ajouter au dossier contenant %1$s" + "Élément ajouté au dossier" + "Créer un dossier avec : %1$s" + "Dossier créé" + "Déplacer sur l\'écran d\'accueil" + "Déplacer l\'écran à gauche" + "Déplacer l\'écran à droite" + "Écran déplacé" + "Redimensionner" + "Augmenter la largeur" + "Augmenter la hauteur" + "Diminuer la largeur" + "Diminuer la hauteur" + "Le widget a été redimensionné (largeur : %1$s, hauteur : %2$s)" diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index d23b6a492d..662bcf14e9 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Accueil" - "Applications de base Android" + "Android Work" "L\'application n\'est pas installée." "Application indisponible" "L\'application téléchargée est désactivée en mode sécurisé." "Les widgets sont désactivés en mode sécurisé." - "Widgets" - "Widgets" "Afficher la mémoire" "App. de manière prolongée pour sélectionner widget." "%1$d x %2$d" - "Impossible de déposer élément sur écran d\'accueil." - "Sélectionner le widget à créer" - "Nom du dossier" - "Renommer le dossier" - "OK" - "Annuler" - "Ajouter à l\'écran d\'accueil" - "Applications" - "Raccourcis" - "Widgets" - "Vous n\'avez plus d\'espace libre sur vos écrans d\'accueil." + "Rechercher dans les applications" + "Chargement des applications en cours…" + "Aucune application ne correspond à la requête \"%1$s\"." "Pas d\'espace libre sur cet écran d\'accueil." "Plus d\'espace disponible dans la zone de favoris." - "Ce widget est trop volumineux pour la zone de favoris." - "Le raccourci \"%s\" a été créé." - "Le raccourci \"%s\" a été supprimé." - "Le raccourci \"%s\" existe déjà." - "Sélectionner un raccourci" - "Sélectionner une application" "Applications" "Accueil" - "Supprimer" - "Désinstaller" "Supprimer" "Désinstaller" "Informations sur l\'application" - "Applications" - "Supprimer" - "Désinstaller la mise à jour" - "Désinstaller l\'application" - "Informations sur l\'application" - "1 application sélectionnée" - "1 widget sélectionné" - "1 dossier sélectionné" - "1 raccourci sélectionné" "installer des raccourcis" "Permettre à une application d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur" - "désinstaller des raccourcis" - "Permettre à l\'application de supprimer des raccourcis sans l\'intervention de l\'utilisateur" "lire les paramètres et les raccourcis de l\'écran d\'accueil" "Permettre à l\'application de lire les paramètres et les raccourcis de l\'écran d\'accueil" "modifier les paramètres et les raccourcis de l\'écran d\'accueil" @@ -78,30 +48,17 @@ "Problème lors du chargement du widget." "Configuration" "Impossible de désinstaller cette application, car il s\'agit d\'une application système." - "Rocket Launcher" "Dossier sans nom" - "Écran d\'accueil %1$d" "Page %1$d sur %2$d" "Écran d\'accueil %1$d sur %2$d" - "Page des applications %1$d sur %2$d" - "Page des widgets %1$d sur %2$d" "Bienvenue" - "Familiarisez-vous avec l\'écran d\'accueil." - - - "Créez des écrans personnalisés pour vos applis et dossiers" "Copier les icônes de vos applis" "Importer les icônes et dossiers des anciens écrans d\'accueil ?" "COPIER LES ICÔNES" "DISPOSITION PAR DÉFAUT" - "Organisez votre espace" - "Appuyez de manière prolongée sur l\'arrière-plan pour gérer les fonds d\'écran, les widgets et les paramètres." "Fonds d\'écran, widgets et paramètres" "Appuyez de manière prolongée sur l\'arrière-plan pour le personnaliser." "OK" - "Voici un dossier" - "Pour en créer un, appuyez de manière prolongée sur une application, puis déplacez-la vers une autre." - "OK" "Dossier ouvert, %1$d par %2$d" "Appuyez pour fermer le dossier." "Appuyez pour enregistrer le nouveau nom." @@ -111,14 +68,34 @@ "Widgets" "Fonds d\'écran" "Paramètres" - "En attente" - "Téléchargement…" - "Installation…" + "Autoriser la rotation" "Inconnu" - "Non restauré" - "Tout supprimer" "Supprimer" "Rechercher" "Cette application n\'est pas installée" "L\'application correspondant à cette icône n\'est pas installée. Vous pouvez supprimer cette dernière, ou essayer de rechercher l\'application et de l\'installer manuellement." + "Ajouter à l\'écran d\'accueil" + "Déplacer l\'élément ici" + "L\'élément a bien été ajouté à l\'écran d\'accueil." + "L\'élément a bien été supprimé." + "Déplacer l\'élément" + "Déplacer vers la ligne %1$s, colonne %2$s" + "Déplacer vers la position %1$s" + "Déplacer vers la position %1$s dans les favoris" + "Élément déplacé" + "Ajouter au dossier \"%1$s\"" + "Ajouter au dossier contenant \"%1$s\"" + "Élément ajouté au dossier" + "Créer un dossier avec \"%1$s\"" + "Dossier créé" + "Déplacer vers l\'écran d\'accueil" + "Déplacer l\'écran vers la gauche" + "Déplacer l\'écran vers la droite" + "L\'écran a bien été déplacé." + "Redimensionner" + "Augmenter la largeur" + "Augmenter la hauteur" + "Diminuer la largeur" + "Diminuer la hauteur" + "Le widget a bien été redimensionné (largeur : %1$s, hauteur : %2$s)." diff --git a/res/values-gl-rES/strings.xml b/res/values-gl-rES/strings.xml index 96a53dc090..6cf76a7c0b 100644 --- a/res/values-gl-rES/strings.xml +++ b/res/values-gl-rES/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Inicio" - "Aplicacións básicas de Android" + "Traballo" "A aplicación non está instalada" "A aplicación non está dispoñible" "A aplicación que descargaches está desactivada no modo seguro" "Os widgets están desactivados no modo seguro" - "Widgets" - "Widgets" "Mostrar memoria" "Mantén premido un widget para seleccionalo." "%1$d × %2$d" - "Non se puido engadir á pantalla de inicio." - "Escolle o widget que queiras crear" - "Nome do cartafol" - "Cambiar o nome do cartafol" - "Aceptar" - "Cancelar" - "Engadir á pantalla de inicio" - "Aplicacións" - "Atallos" - "Widgets" - "Non hai máis espazo nas pantallas de inicio." + "Aplicacións de busca" + "Cargando aplicacións..." + "Non se atoparon aplicacións que coincidan con \"%1$s\"" "Non hai máis espazo nesta pantalla de inicio." "Non hai máis espazo na bandexa de favoritos" - "Este widget é demasiado grande para a bandexa de favoritos" - "Creouse o atallo \"%s\"." - "Eliminouse o atallo \"%s\"." - "O atallo \"%s\" xa existe." - "Escoller un atallo" - "Escoller unha aplicación" "Aplicacións" "Inicio" - "Eliminar" - "Desinstalar" "Eliminar" "Desinstalar" "Información da aplicación" - "Aplicacións" - "Eliminar" - "Desinstalar actualización" - "Desinstalar aplicación" - "Detalles da aplicación" - "1 aplicación seleccionada" - "1 widget seleccionado" - "1 cartafol seleccionado" - "1 atallo seleccionado" "instalar atallos" "Permite a unha aplicación engadir atallos sen intervención do usuario." - "desinstalar atallos" - "Permite a unha aplicación eliminar atallos sen intervención do usuario." "ler a configuración e os atallos da pantalla de inicio" "Permite a unha aplicación ler a configuración e os atallos da páxina de inicio." "modificar a configuración e os atallos da pantalla de inicio" @@ -78,30 +48,17 @@ "Produciuse un problema ao cargar o widget" "Configuración" "Esta aplicación é do sistema e non se pode desinstalar." - "Lanzacohetes" "Cartafol sen nome" - "Pantalla de inicio %1$d" "Páxina %1$d de %2$d" "Pantalla de inicio %1$d de %2$d" - "Páxina de aplicacións %1$d de %2$d" - "Páxina de widgets %1$d de %2$d" "Dámosche a benvida" - "Síntete como na túa casa." - - - "Crea máis pantallas para aplicacións e cartafoles" "Copiar iconas das aplicacións" "Queres importar as iconas e os cartafoles doutras pantallas de inicio?" "COPIAR ICONAS" "COMEZAR DE CERO" - "Organiza o espazo" - "Mantén premido o fondo para xestionar o fondo de pantalla e máis." "Fondos pantalla, widgets e configuración" "Mantén tocado o segundo plano para personalizar" "DE ACORDO" - "Isto é un cartafol" - "Para crear un igual, mantén premida a aplicación e móvea sobre outra." - "Aceptar" "Abriuse o cartafol, %1$d por %2$d" "Toca para pechar o cartafol" "Toca para gardar o cambio de nome" @@ -111,14 +68,34 @@ "Widgets" "Fondos de pantalla" "Configuración" - "En espera" - "Descargando" - "Instalando" + "Permitir xiro" "Descoñecido" - "Non restaurado" - "Eliminar todas" "Eliminar" "Buscar" "Esta aplicación non está instalada" "A aplicación para esta icona non está instalada. Podes eliminala ou buscar a aplicación e instalala manualmente." + "Engadir á pantalla de inicio" + "Mover elemento aquí" + "Engadiuse o elemento á pantalla de inicio" + "Eliminouse o elemento" + "Mover elemento" + "Mover á fila %1$s columna %2$s" + "Mover á posición %1$s" + "Mover á posición dos favoritos %1$s" + "Moveuse o elemento" + "Engadir ao cartafol: %1$s" + "Engadir ao cartafol con %1$s" + "Engadiuse o elemento ao cartafol" + "Crear cartafol con: %1$s" + "Creouse o cartafol" + "Mover á pantalla de inicio" + "Mover pantalla á esquerda" + "Mover pantalla á dereita" + "Moveuse a pantalla" + "Cambiar tamaño" + "Aumentar ancho" + "Aumentar altura" + "Reducir ancho" + "Reducir altura" + "Cambiouse o tamaño do widget polo ancho %1$s e a altura %2$s" diff --git a/res/values-gu-rIN/strings.xml b/res/values-gu-rIN/strings.xml index a2798d6b97..4a2274f1b6 100644 --- a/res/values-gu-rIN/strings.xml +++ b/res/values-gu-rIN/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "હોમ" - "Android Core એપ્લિકેશન્સ" + "કાર્યાલય" "એપ્લિકેશન ઇન્સ્ટોલ થઈ નથી." "એપ્લિકેશન ઉપલબ્ધ નથી" "સુરક્ષિત મોડમાં ડાઉનલોડ કરેલ એપ્લિકેશન અક્ષમ કરી" "સુરક્ષિત મોડમાં વિજેટ્સ અક્ષમ કર્યા" - "વિજેટ્સ" - "વિજેટ્સ" - "Mem દર્શાવો" + "Mem બતાવો" "વિજેટ ચૂંટવા માટે ટચ કરો અને પકડી રાખો." "%1$d × %2$d" - "આ હોમ સ્ક્રીન પર આઇટમ છોડી શકાઈ નથી." - "બનાવવા માટે વિજેટ પસંદ કરો" - "ફોલ્ડરનું નામ" - "ફોલ્ડરનું નામ બદલો" - "ઑકે" - "રદ કરો" - "હોમ સ્ક્રીન પર ઉમેરો" - "એપ્લિકેશન્સ" - "શોર્ટકટ્સ" - "વિજેટ્સ" - "તમારી હોમ સ્ક્રીન પર વધુ જગ્યા નથી." + "શોધ એપ્લિકેશનો" + "એપ્લિકેશનો લોડ કરી રહ્યું છે…" + "\"%1$s\" થી મેળ ખાતી કોઈ એપ્લિકેશનો મળી નથી" "આ હોમ સ્ક્રીન પર વધુ જગ્યા નથી." "મનપસંદ ટ્રે પર વધુ જગ્યા નથી" - "મનપસંદ ટ્રે માટે આ વિજેટ ખૂબ મોટું છે" - "\"%s\" શોર્ટકટ બનાવ્યું." - "\"%s\" શોર્ટકટ દૂર કર્યું હતું." - "\"%s\" શોર્ટકટ પહેલાંથી અસ્તિત્વમાં છે." - "શોર્ટકટ પસંદ કરો" - "એપ્લિકેશન પસંદ કરો" - "એપ્લિકેશન્સ" + "એપ્લિકેશનો" "હોમ" - "દૂર કરો" - "અનઇન્સ્ટોલ કરો" "દૂર કરો" "અનઇન્સ્ટોલ કરો" "એપ્લિકેશન માહિતી" - "એપ્લિકેશન્સ" - "દૂર કરો" - "અપડેટ અનઇન્સ્ટોલ કરો" - "એપ્લિકેશન અનઇન્સ્ટોલ કરો" - "એપ્લિકેશનની વિગતો" - "1 એપ્લિકેશન પસંદ કરી" - "1 વિજેટ પસંદ કર્યું" - "1 ફોલ્ડર પસંદ કર્યું" - "1 શોર્ટકટ પસંદ કર્યું" "શોર્ટકટ્સ ઇન્સ્ટોલ કરો" "એપ્લિકેશનને વપરાશકર્તા હસ્તક્ષેપ વગર શોર્ટકટ્સ ઉમેરવાની મંજૂરી આપે છે." - "શોર્ટકટ્સ અનઇન્સ્ટોલ કરો" - "એપ્લિકેશનને વપરાશકર્તા હસ્તક્ષેપ વગર શોર્ટકટ્સ દૂર કરવાની મંજૂરી આપે છે." "હોમ સેટિંગ્સ અને શોર્ટકટ્સ વાંચો" "એપ્લિકેશનને હોમમાં સેટિંગ્સ અને શોર્ટકટ્સ વાંચવાની મંજૂરી આપે છે." "હોમ સેટિંગ્સ અને શોર્ટકટ્સ લખો" @@ -78,30 +48,17 @@ "વિજેટ લોડ કરવામાં સમસ્યા" "સેટઅપ" "આ એક સિસ્ટમ એપ્લિકેશન છે અને અનઇન્સ્ટોલ કરી શકાતી નથી." - "રોકેટ લોન્ચર" "અનામી ફોલ્ડર" - "%1$d હોમ સ્ક્રીન" "%2$d માંથી %1$d પૃષ્ઠ" "%2$d માંથી %1$d હોમ સ્ક્રીન" - "%2$d માંથી %1$d એપ્લિકેશન્સ પૃષ્ઠ" - "%2$d માંથી %1$d વિજેટ્સ પૃષ્ઠ" "સ્વાગત છે" - "ઘરે જાતે બનાવે છે." - - - "એપ્લિકેશન્સ અને ફોલ્ડર્સ માટે વધુ સ્ક્રીન બનાવો" "તમારા એપ્લિકેશન આયકન્સને કૉપિ કરો" "તમારી જૂની હોમ સ્ક્રીન્સથી આયકન્સ અને ફોલ્ડર્સને આયાત કરીએ?" "આયકન્સને કૉપિ કરો" "નવેસરથી પ્રારંભ કરો" - "તમારા સ્થાનને ગોઠવો" - "વૉલપેપર, વિજેટ્સ અને સેટિંગ્સને સંચાલિત કરવા માટે પૃષ્ઠભૂમિને ટચ કરો અને પકડી રાખો." "વૉલપેપર્સ, વિજેટ્સ અને સેટિંગ્સ" "પૃષ્ઠભૂમિને કસ્ટમાઇઝ કરવા માટે ટચ કરો અને પકડી રાખો" "સમજાઈ ગયું" - "અહીં ફોલ્ડર છે" - "આના જેવું એક બનાવવા માટે, એપ્લિકેશનને ટચ કરો અને પકડી રાખો, પછી તેને બીજીમાં ખસેડો." - "ઑકે" "%1$d બાય %2$d નું ફોલ્ડર ખોલ્યું" "ફોલ્ડર બંધ કરવા માટે ટચ કરો" "નામ બદલવાનું સાચવવા માટે ટચ કરો" @@ -111,14 +68,34 @@ "વિજેટ્સ" "વૉલપેપર્સ" "સેટિંગ્સ" - "રાહ જુએ છે" - "ડાઉનલોડ કરે છે" - "ઇન્સ્ટોલ કરી રહ્યું છે" + "પરિભ્રમણને મંજૂરી આપો" "અજાણ્યો" - "પુનર્સ્થાપિત કર્યું નથી" - "બધું દૂર કરો" "દૂર કરો" "શોધો" "આ એપ્લિકેશન ઇન્સ્ટોલ થયેલ નથી" "આ આયકન માટેની એપ્લિકેશન ઇન્સ્ટોલ થયેલ નથી. તમે તેને દૂર કરી શકો છો અથવા એપ્લિકેશન માટે શોધ કરી અને તેને મેન્યુઅલી ઇન્સ્ટોલ કરી શકો છો." + "હોમ સ્ક્રીન પર ઉમેરો" + "આઇટમ અહીં ખસેડો" + "હોમ સ્ક્રીનમાં આઇટમ ઉમેરી" + "આઇટમ દૂર કરી" + "આઇટમ ખસેડો" + "%1$s પંક્તિ %2$s કૉલમ પર ખસેડો" + "%1$s સ્થિતિ પર ખસેડો" + "મનપસંદ સ્થિતિ %1$s પર ખસેડો" + "આઇટમ ખસેડી" + "ફોલ્ડરમાં ઉમેરો: %1$s" + "%1$s સાથે ફોલ્ડરમાં ઉમેરો" + "ફોલ્ડરમાં આઇટમ ઉમેરી" + "આની સાથે ફોલ્ડર બનાવો: %1$s" + "ફોલ્ડર બનાવ્યું" + "હોમ સ્ક્રીન પર ખસેડો" + "સ્ક્રીનને ડાબી બાજુ ખસેડો" + "સ્ક્રીનને જમણી બાજુ ખસેડો" + "સ્ક્રીન ખસેડી" + "આકાર બદલો" + "પહોળાઈ વધારો" + "ઊંચાઈ વધારો" + "પહોળાઈ ઘટાડો" + "ઊંચાઈ ઘટાડો" + "વિજેટનો આકાર બદલીને %1$s પહોળાઈ %2$s ઊંચાઈ કર્યો" diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml index 0599da0e84..6fdbf24411 100644 --- a/res/values-hi/strings.xml +++ b/res/values-hi/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "होम" - "Android के मुख्य ऐप्लिकेशन" + "कार्यस्‍थल" "एप्‍लिकेशन इंस्‍टॉल नहीं है." "ऐप्स उपलब्ध नहीं है" "डाउनलोड किए गए ऐप्स सुरक्षित मोड में अक्षम है" "विजेट सुरक्षित मोड में अक्षम हैं" - "शॉर्टकट" - "शॉर्टकट" "मेमोरी दिखाएं" "विजेट को चुनने के लिए स्‍पर्श करके रखें." "%1$d × %2$d" - "आइटम को इस होम स्‍क्रीन पर नहीं छोड़ा जा सका." - "बनाने के लिए विजेट चुनें" - "फ़ोल्‍डर का नाम" - "फ़ोल्‍डर का नाम बदलें" - "ठीक" - "अभी नहीं" - "होम स्‍क्रीन में जोड़ें" - "ऐप्लिकेशन" - "शॉर्टकट" - "शॉर्टकट" - "आपकी होम स्‍क्रीन पर स्थान शेष नहीं है." + "ऐप्‍स खोजें" + "ऐप्स लोड हो रहे हैं..." + "\"%1$s\" से मिलान करने वाला कोई ऐप नहीं मिला" "इस होम स्‍क्रीन पर स्थान शेष नहीं है." "पसंदीदा ट्रे में और स्थान नहीं है" - "पसंदीदा ट्रे के लिए यह विजेट बहुत ही बड़ा है" - "शॉर्टकट \"%s\" बनाया गया." - "शॉर्टकट \"%s\" निकाल दिया गया था." - "शॉर्टकट \"%s\" पहले से मौजूद है." - "शॉर्टकट चुनें" - "एप्‍लिकेशन चुनें" "ऐप्लिकेशन" "होम" - "निकालें" - "अनइंस्टॉल करें" "निकालें" "अनइंस्टॉल करें" "ऐप्लिकेशन की जानकारी" - "ऐप्लिकेशन" - "निकालें" - "अपडेट अनइंस्‍टॉल करें" - "ऐप्लिकेशन अनइंस्‍टॉल करें" - "ऐप्लिकेशन का विवरण" - "1 एप्‍लिकेशन चयनित" - "1 विजेट चयनित" - "1 फ़ोल्‍डर चयनित" - "1 शॉर्टकट चयनित" "शॉर्टकट इंस्‍टॉल करें" "ऐप्लिकेशन को उपयोगकर्ता के हस्‍तक्षेप के बिना शॉर्टकट जोड़ने देती है." - "शॉर्टकट अनइंस्टॉल करें" - "ऐप्लिकेशन को उपयोगकर्ता के हस्‍तक्षेप के बिना शॉर्टकट निकालने देती है." "होम सेटिंग और शॉर्टकट पढ़ें" "ऐप्लिकेशन को होम में सेटिंग और शॉर्टकट पढ़ने देती है." "होम सेटिंग और शॉर्टकट लिखें" @@ -78,30 +48,17 @@ "विजेट लोड करने में समस्‍या" "सेटअप" "यह एक सिस्टम ऐप्लिकेशन है और इसे अनइंस्टॉल नहीं किया जा सकता." - "रॉकेट लॉन्‍चर" "अनामित फ़ोल्डर" - "होम स्‍क्रीन %1$d" "पृष्ठ %2$d में से %1$d" "होम स्क्रीन %2$d में से %1$d" - "ऐप्लिकेशन पृष्ठ %2$d में से %1$d" - "शॉर्टकट %2$d में से %1$d" "स्वागत है" - "जैसा चाहें वैसा उपयोग करें." - - - "ऐप्लिकेशन और फ़ोल्डर के लिए और अधिक स्क्रीन बनाएं" "ऐप्स आइकन की प्रतिलिपि बनाएं" "अपनी पुरानी होम स्क्रीन से आइकन और फ़ोल्डर आयात करें?" "आइकन की प्रतिलिपि बनाएं" "फिर से शुरू करें" - "अपने स्थान को व्यवस्थित करें" - "वॉलपेपर, शॉर्टकट और सेटिंग प्रबंधित करने के लिए पृष्ठभूमि को स्पर्श करके रखें." "वॉलपेपर, शॉर्टकट और सेटिंग" "पृष्ठभूमि कस्टमाइज़ करने के लिए स्पर्श करके रखें" "समझ लिया" - "यहां एक फ़ोल्डर है" - "इसके जैसा कोई एक बनाने के लिए, किसी ऐप्लिकेशन को स्पर्श करके रखें, फिर इसे किसी दूसरे पर ले जाएं." - "ठीक" "फ़ोल्डर खोला गया, %1$d गुणा %2$d" "फ़ोल्‍डर बंद करने के लिए स्‍पर्श करें" "बदला गया नाम सहेजने के लिए स्पर्श करें" @@ -111,14 +68,34 @@ "शॉर्टकट" "वॉलपेपर" "सेटिंग" - "प्रतीक्षा में" - "डाउनलोड हो रहा है" - "इंस्टॉल हो रहा है" + "रोटेशन की अनुमति दें" "अज्ञात" - "पुन:स्थापित नहीं हुआ" - "सभी निकालें" "निकालें" "खोजें" "यह ऐप्स इंस्टॉल नहीं है" "इस आइकन का ऐप्स इंस्टॉल नहीं है. आप उसे निकाल सकते हैं या ऐप्स की खोज करके उसे मैन्युअल रूप से इंस्टॉल कर सकते हैं." + "होम स्‍क्रीन में जोड़ें" + "आइटम यहां ले जाएं" + "होम स्क्रीन में आइटम जोड़ा गया" + "आइटम निकाला गया" + "आइटम ले जाएं" + "पंक्ति %1$s स्तंभ %2$s पर ले जाएं" + "%1$s स्थिति पर ले जाएं" + "%1$s की पसंदीदा स्थिति पर ले जाएं" + "आइटम ले जाया गया" + "फ़ोल्डर में जोड़ें: %1$s" + "%1$s के साथ फ़ोल्डर में जोड़ें" + "आइटम फ़ोल्डर में जोड़ा गया" + "इसके साथ फ़ोल्डर बनाएं: %1$s" + "फ़ोल्डर बनाया गया" + "होम स्क्रीन पर ले जाएं" + "स्क्रीन को बाएं ले जाएं" + "स्क्रीन को दाएं ले जाएं" + "स्क्रीन ले जाई गई" + "आकार बदलें" + "चौड़ाई बढ़ाएं" + "ऊंचाई बढ़ाएं" + "चौड़ाई घटाएं" + "ऊंचाई घटाएं" + "विजेट का आकार बदलकर उसकी चौड़ाई %1$s और ऊंचाई %2$s कर दी गई" diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml index 00810a76ca..a54a7e4b02 100644 --- a/res/values-hr/strings.xml +++ b/res/values-hr/strings.xml @@ -20,57 +20,27 @@ "Pokretač3" - "Početna" - "Matične aplikacije za Android" + "Posao" "Aplikacija nije instalirana." "Aplikacija nije dostupna" "Preuzeta aplikacija onemogućena je u Sigurnom načinu rada" "Widgeti su onemogućeni u Sigurnom načinu rada" - "Widgeti" - "Widgeti" "Prikaži mem" "Dodirnite i držite kako biste podigli widget." "%1$d × %2$d" - "Stavka nije ispuštena na ovaj početni zaslon." - "Odabir widgeta za stvaranje" - "Naziv mape" - "Preimenovanje mape" - "U redu" - "Odustani" - "Dodavanje na početni zaslon" - "Aplikacije" - "Prečaci" - "Widgeti" - "Na vašim početnim zaslonima više nema mjesta." + "Pretraži aplikacije" + "Učitavanje aplikacija…" + "Nema aplikacija podudarnih s upitom \"%1$s\"" "Na ovom početnom zaslonu više nema mjesta." "Nema više prostora na traci Favoriti" - "Widget je prevelik za traku Favoriti" - "Izrađen je prečac za \"%s\"." - "Uklonjen je prečac za \"%s\"." - "Prečac za \"%s\" već postoji." - "Odabir prečaca" - "Odabir aplikacije" "Aplikacije" "Početna" - "Ukloni" - "Deinstaliraj" "Ukloni" "Deinstaliraj" "Informacije o aplikaciji" - "Aplikacije" - "Ukloni" - "Deinstalacija ažuriranja" - "Deinstaliranje aplikacije" - "Pojedinosti o aplikaciji" - "Odabrana je 1 aplikacija" - "Odabran je 1 widget" - "Odabrana je 1 mapa" - "Odabran je 1 prečac" "instaliranje prečaca" "Aplikaciji omogućuje dodavanje prečaca bez intervencije korisnika." - "deinstaliranje prečaca" - "Aplikaciji omogućuje uklanjanje prečaca bez intervencije korisnika." "čitanje postavki početnog zaslona i prečaca" "Aplikaciji omogućuje čitanje postavki i prečaca na početnom zaslonu." "pisanje postavki početnog zaslona i prečaca" @@ -78,30 +48,17 @@ "Problem pri učitavanju widgeta" "Postavljanje" "Ovo je aplikacija sustava i ne može se ukloniti." - "Lansirna rampa" "Neimenovana mapa" - "Početni zaslon %1$d" "Stranica %1$d od %2$d" "Početni zaslon %1$d od %2$d" - "Stranica aplikacija %1$d od %2$d" - "Stranica widgeta %1$d od %2$d" "Dobro došli" - "Osjećajte se kao kod kuće." - - - "Izradite više zaslona za aplikacije i mape" "Kopiranje ikona aplikacija" "Želite li uvesti ikone i mape sa starih početnih zaslona?" "KOPIRAJ IKONE" "POKRENI NOVO" - "Organizirajte svoj prostor" - "Dodirnite i držite pozadinu da biste upravljali pozadinskom slikom, widgetima i postavkama." "Pozadine, widgeti i postavke" "Dodirnite i zadržite pozadinu radi prilagodbe" "SHVAĆAM" - "Evo mape" - "Da biste izradili ovakvu mapu, dodirnite i držite aplikaciju pa je pomaknite preko druge aplikacije." - "U redu" "Mapa je otvorena, %1$d x %2$d" "Dodirnite da biste zatvorili mapu" "Dodirnite da biste spremili preimenovanje" @@ -111,14 +68,34 @@ "Widgeti" "Pozadine" "Postavke" - "Čekanje" - "Preuzimanje" - "Instaliranje" + "Dopusti rotaciju" "Nepoznato" - "Nije vraćeno" - "Ukloni sve" "Ukloni" "Traži" "Ta aplikacija nije instalirana" "Aplikacija ove ikone nije instalirana. Možete je ukloniti ili potražiti aplikaciju i instalirati je ručno." + "Dodavanje na početni zaslon" + "Premjesti stavku ovdje" + "Stavka je dodana na početni zaslon" + "Stavka je uklonjena" + "Premještanje stavke" + "Premještanje u redak %1$s, stupac %2$s" + "Premještanje na položaj %1$s" + "Premještanje na položaj favorita %1$s" + "Stavka premještena" + "Dodavanje u mapu: %1$s" + "Dodavanje u mapu s aplikacijom %1$s" + "Stavka dodana u mapu" + "Izrada mape pomoću stavke: %1$s" + "Mapa izrađena" + "Premještanje na početni zaslon" + "Premještanje zaslona ulijevo" + "Premještanje zaslona udesno" + "Zaslon je premješten" + "Promjena veličine" + "Povećanje širine" + "Povećanje visine" + "Smanjenje širine" + "Smanjenje visine" + "Širina widgeta promijenjena je na %1$s, a visina na %2$s" diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml index 513548a2aa..e9fc5747fd 100644 --- a/res/values-hu/strings.xml +++ b/res/values-hu/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Főoldal" - "Alap Android-alkalmazások" + "Munka" "Az alkalmazás nincs telepítve." "Az alkalmazás nem érhető el" "A letöltött alkalmazás Csökkentett módban ki van kapcsolva" "A modulok ki vannak kapcsolva Csökkentett módban" - "Modulok" - "Modulok" "Mem. megjelenítése" "Modul felvételéhez érintse meg, és tartsa lenyomva" "%1$d × %2$d" - "Nem lehet elemeket dobni erre a kezdőképernyőre." - "A létrehozáshoz válasszon modult" - "Mappa neve" - "Mappa átnevezése" - "OK" - "Mégse" - "Hozzáadás a kezdőképernyőhöz" - "Alkalmazások" - "Parancsikonok" - "Modulok" - "Nincs több hely a kezdőképernyőkön." + "Alkalmazások keresése" + "Alkalmazások betöltése…" + "Egy alkalmazás sem található a(z) „%1$s” lekérdezésre." "Nincs több hely ezen a kezdőképernyőn." "Nincs több hely a Kedvencek tálcán" - "Ez a modul túl nagy a Kedvencek tálcán való elhelyezéshez" - "A(z) „%s” parancsikon létrehozva." - "A(z) „%s” parancsikon eltávolítva." - "A(z) „%s” parancsikon már létezik." - "Parancsikon választása" - "Válasszon alkalmazást" "Alkalmazások" "Főoldal" - "Eltávolítás" - "Eltávolítás" "Eltávolítás" "Eltávolítás" "Alkalmazásinformáció" - "Alkalmazások" - "Eltávolítás" - "Eltávolítja a frissítést" - "Alkalmazás eltávolítása" - "Az alkalmazás adatai" - "1 alkalmazás kiválasztva" - "1 modul kiválasztva" - "1 mappa kiválasztva" - "1 parancsikon kiválasztva" "parancsikonok telepítése" "Lehetővé teszi egy alkalmazás számára, hogy felhasználói beavatkozás nélkül adjon hozzá parancsikonokat." - "parancsikonok eltávolítása" - "Lehetővé teszi egy alkalmazás számára, hogy felhasználói beavatkozás nélkül távolítson el parancsikonokat." "Főoldal beállításainak és parancsikonjainak beolvasása" "Lehetővé teszi az alkalmazás számára, hogy beolvassa a kezdőképernyő beállításait és parancsikonjait." "Főoldal beállításainak és parancsikonjainak írása" @@ -78,30 +48,17 @@ "Probléma történt a modul betöltésekor" "Beállítás" "Ez egy rendszeralkalmazás, és nem lehet eltávolítani." - "Aknavető" "Névtelen mappa" - "%1$d. kezdőképernyő" "%2$d/%1$d. oldal" "%2$d/%1$d. kezdőképernyő" - "%2$d/%1$d. alkalmazásoldal" - "%2$d/%1$d. moduloldal" "Üdvözöljük!" - "Varázsolja egyedivé készülékét." - - - "Hozzon létre további képernyőket az alkalmazásoknak és mappáknak" "Alkalmazásikonok másolása" "Importálja ikonjait és mappáit régi kezdőképernyőiről?" "IKONOK MÁSOLÁSA" "TELJESEN ÚJ" - "Munkaterület rendezése" - "Érintse meg és tartsa lenyomva a hátteret a háttérkép, modulok és beállítások kezeléséhez." "Háttérképek, modulok és beállítások" "Érintse meg és tartsa lenyomva a személyre szabáshoz" "MEGÉRTETTEM" - "Itt egy mappa" - "Mappa létrehozásához érintse meg és tartsa lenyomva az alkalmazást, majd húzza egy másik fölé." - "OK" "Mappa megnyitva – szélesség: %1$d; magasság: %2$d" "Érintse meg a mappa bezárásához" "Érintse meg az átnevezés mentéséhez" @@ -111,14 +68,34 @@ "Modulok" "Háttérképek" "Beállítások" - "Várakozik" - "Letöltés alatt" - "Települ" + "Elforgatás engedélyezése" "Ismeretlen" - "Nincs visszaállítva" - "Az összes eltávolítása" "Eltávolítás" "Keresés" "Az alkalmazás nincs telepítve" "Az ikonhoz tartozó alkalmazás nincs telepítve. Törölheti az ikont, vagy az alkalmazás megkeresése után manuálisan telepítheti azt." + "Hozzáadás a kezdőképernyőhöz" + "Elem áthelyezése ide" + "Elem hozzáadva a kezdőképernyőhöz" + "Elem eltávolítva" + "Elem mozgatása" + "Áthelyezés ide: %1$s. sor, %2$s. oszlop" + "Áthelyezés a(z) %1$s. pozícióba" + "Áthelyezés a kedvencek %1$s. pozíciójába" + "Elem áthelyezve" + "Hozzáadás a(z) %1$s mappához" + "Hozzáadás a mappához a következővel: %1$s" + "Elem hozzáadva a mappához" + "Mappa létrehozása a következővel: %1$s" + "Mappa létrehozva" + "Áthelyezés a kezdőképernyőre" + "Képernyő mozgatása balra" + "Képernyő mozgatása jobbra" + "Képernyő áthelyezve" + "Átméretezés" + "Szélesség növelése" + "Magasság növelése" + "Szélesség csökkentése" + "Magasság csökkentése" + "Modul átméretezve %1$s szélességre és %2$s magasságra" diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy-rAM/strings.xml index 4af794e6f5..de4aed1583 100644 --- a/res/values-hy-rAM/strings.xml +++ b/res/values-hy-rAM/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Հիմնական" - "Android Core Apps" + "Աշխատանքային" "Ծրագիրը տեղադրված չէ:" "Հավելվածը հասանելի չէ" "Ներբեռնված ծրագիրն անջատված է Անվտանգ ռեժիմում" "Վիջեթներն անջատված են անվտանգ ռեժիմում" - "Վիջեթներ" - "Վիջեթներ" "Ցուցադրել մեմը" "Հպեք և պահեք՝ վիջեթն ընտրելու համար:" "%1$d × %2$d" - "Հնարավոր չէ տեղադրել տարրն այս հիմնական էկրանին:" - "Ստեղծելու համար ընտրեք վիջեթը" - "Պանակի անունը" - "Վերանվանել պանակը" - "Լավ" - "Չեղարկել" - "Ավելացնել հիմնական էկրանին" - "Ծրագրեր" - "Դյուրանցումներ" - "Վիջեթներ" - "Այլևս տեղ չկա ձեր հիմնական էկրաններին:" + "Հավելվածների որոնում" + "Հավելվածների բեռնում…" + %1$s» հարցմանը համապատասխանող հավելվածներ չեն գտնվել" "Այլևս տեղ չկա այս հիմնական էկրանին:" "Ընտրյալների ցուցակում այլևս ազատ տեղ չկա" - "Այս վիջեթը շատ մեծ է Ընտրյալների ցուցակի համար" - %s» դյուրանցումը ստեղծված է:" - %s» դյուրանցումը հեռացվեց:" - %s» դյուրանցումն արդեն գոյություն ունի:" - "Ընտրել դյուրանցումը" - "Ընտրել ծրագիրը" "Ծրագրեր" "Հիմնական" - "Հեռացնել" - "Ապատեղադրել" "Հեռացնել" "Ապատեղադրել" "Ծրագրի տեղեկություններ" - "Ծրագրեր" - "Հեռացնել" - "Ապատեղադրել թարմացումը" - "Ապատեղադրել ծրագիրը" - "Ծրագրի մանրամասներ" - "1 ընտրված ծրագիր" - "1 ընտրված վիջեթ" - "1 ընտրված պանակ" - "1 ընտրված դյուրանցում" "տեղադրել դյուրանցումներ" "Ծրագրին թույլ է տալիս ավելացնել դյուրանցումներ՝ առանց օգտագործողի միջամտության:" - "ապատեղադրել դյուրանցումները" - "Ծրագրին թույլ է տալիս հեռացնել դյուրանցումներ՝ առանց օգտագործողի միջամտության:" "կարդալ հիմնաէջի կարգավորումներն ու դյուրանցումները" "Ծրագրին թույլ է տալիս կարդալ հիմնաէջի կարգավորումներն ու դյուրանցումները:" "ստեղծել հիմնաէջի կարգավորումներ ու դյուրանցումներ" @@ -78,30 +48,17 @@ "Վիջեթի բեռնման խնդիր կա" "Կարգավորում" "Սա համակարգային ծրագիր է և չի կարող ապատեղադրվել:" - "Հրթիռային թողարկիչ" "Անանուն պանակ" - "Հիմնական էկրան %1$d" "Էջ %1$d՝ %2$d-ից" "Հիմնական էկրան %1$d` %2$d-ից" - "Ծրագրերի էջերը՝ %1$d %2$d-ից" - "Վիջեթների էջերը՝ %1$d %2$d-ից" "Բարի գալուստ" - "Զգացեք ձեզ ինչպես տանը:" - - - "Ստեղծեք նոր էկրաններ ծրագրերի և պանակների համար" "Պատճենել ձեր ծրագրի պատկերակները" "Ներմուծե՞լ պատկերակները և պանակները ձեր նախկին Հիմնական էկրանից" "ՊԱՏՃԵՆԵԼ ՊԱՏԿԵՐԱԿՆԵՐԸ" "ՄԵԿՆԱՐԿԵԼ ԸՍՏ ԿԱՆԽԱԴՐՎԱԾԻ" - "Կառավարեք ձեր տարածությունը" - "Հպեք և պահեք հետնաշերտի վրա՝ պաստառները, վիջեթներն ու կարգավորումները կառավարելու համար:" "Պաստառներ, վիջեթներ և կարգավորումներ" "Հարմարեցնելու համար հպեք և պահեք հետնաշերտի վրա" "ՀԱՍԿԱՆԱԼԻ Է" - "Ահա մի պանակ" - "Նման պանակ ստեղծելու համար հպեք և պահեք որևէ ծրագրի վրա, ապա տեղաշարժեք այն մեկ ուրիշ ծրագրի վրա:" - "Լավ" "Թղթապանակը բաց է, %1$d-ից %2$d" "Հպեք՝ պանակը փակելու համար" "Հպեք՝ վերանվանումը պահելու համար" @@ -111,14 +68,34 @@ "Վիջեթներ" "Պաստառներ" "Կարգավորումներ" - "Առկախ է" - "Ներբեռնվում է" - "Տեղադրվում է" + "Թույլ տալ պարբերական կրկնությունը" "Անհայտ է" - "Չի վերականգնվել" - "Հեռացնել բոլորը" "Հեռացնել" "Գտնել" "Այս ծրագիրը տեղադրված չէ:" "Այս պատկերակի ծրագիրը տեղադրված չէ: Դուք կարող եք հեռացնել այն կամ գտնել ծրագիրը և տեղադրել այն ձեռքով:" + "Ավելացնել Հիմնական էկրանին" + "Տեղափոխել տարրն այստեղ" + "Տարրն ավելացվեց հիմնական էկրանին" + "Տարրը հեռացվեց" + "Տեղափոխել տարրը" + "Տեղափոխել տող %1$s սյունակ %2$s" + "Տեղափոխել դիրք %1$s" + "Տեղափոխել նախընտրած դիրք՝ %1$s" + "Տարրը տեղափոխվեց" + "Ավելացնել թղթապանակում՝ %1$s" + "Ավելացնել «%1$s» պանակին" + "Տարրն ավելացվեց թղթապանակում" + "Ստեղծել թղթապանակ, օգտագործելով՝ %1$s" + "Թղթապանակը ստեղծվեց" + "Տեղափոխել Հիմնական էկրան" + "Տեղափոխել էկրանը ձախ" + "Տեղափոխել էկրանը աջ" + "Էկրանը տեղափոխվեց" + "Չափափոխել" + "Ավելացնել լայնությունը" + "Ավելացնել բարձրությունը" + "Նվազեցնել լայնությունը" + "Նվազեցնել բարձրությունը" + "Վիջեթի լայնությունը փոխվել է %1$s-ի, իսկ բարձրությունը՝ %2$s-ի" diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml index b0c5d139d7..c65254a2a0 100644 --- a/res/values-in/strings.xml +++ b/res/values-in/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Layar Utama" - "Aplikasi Inti Android" + "Kantor" "Aplikasi tidak dipasang." "Aplikasi tidak tersedia" "Aplikasi yang diunduh dinonaktifkan dalam mode Aman" "Widget dinonaktifkan dalam mode Aman" - "Widget" - "Widget" "Tampilkan Memori" "Sentuh lama untuk memilih widget." "%1$d × %2$d" - "Tidak dapat melepas item ke layar Utama ini." - "Pilih widget untuk membuat" - "Nama folder" - "Ganti nama folder" - "Oke" - "Batal" - "Tambahkan ke layar Utama" - "Aplikasi" - "Pintasan" - "Widget" - "Tidak ada ruang lagi di layar Utama Anda." + "Telusuri Apps" + "Memuat Aplikasi..." + "Tidak ditemukan Aplikasi yang cocok dengan \"%1$s\"" "Tidak ada ruang lagi pada layar Utama ini." "Tidak ada ruang tersisa di baki Favorit" - "Widget ini terlalu besar untuk baki Favorit" - "Pintasan \"%s\" sudah dibuat." - "Pintasan \"%s\" telah dihapus." - "Pintasan \"%s\" sudah ada." - "Pilih pintasan" - "Pilih aplikasi" "Aplikasi" "Layar Utama" - "Hapus" - "Copot pemasangan" "Hapus" "Copot pemasangan" "Info aplikasi" - "Aplikasi" - "Hapus" - "Copot pemasangan pembaruan" - "Copot aplikasi" - "Detail aplikasi" - "1 aplikasi dipilih" - "1 widget dipilih" - "1 folder dipilih" - "1 pintasan dipilih" "memasang pintasan" "Mengizinkan aplikasi menambahkan pintasan tanpa campur tangan pengguna." - "mencopot pemasangan pintasan" - "Mengizinkan aplikasi menghapus pintasan tanpa campur tangan pengguna." "membaca setelan dan pintasan layar Utama" "Mengizinkan aplikasi membaca setelan dan pintasan di layar Utama." "menulis setelan dan pintasan layar Utama" @@ -78,30 +48,17 @@ "Masalah memuat widget" "Siapkan" "Ini adalah aplikasi sistem dan tidak dapat dicopot pemasangannya." - "Rocket Launcher" "Folder Tanpa Nama" - "Layar utama %1$d" "Laman %1$d dari %2$d" "Layar utama %1$d dari %2$d" - "Laman aplikasi %1$d dari %2$d" - "Laman widget %1$d dari %2$d" "Selamat Datang" - "Serasa di rumah sendiri." - - - "Buat lebih banyak layar untuk aplikasi dan folder" "Salin ikon aplikasi Anda" "Impor ikon dan folder dari layar Utama lama Anda?" "IKON SALIN" "MULAI DARI AWAL" - "Kelola ruang Anda" - "Sentuh lama latar belakang untuk mengelola wallpaper, widget, dan setelan." "Wallpaper, widget, & setelan" "Sentuh & tahan latar belakang untuk menyesuaikan" "MENGERTI" - "Ini adalah folder" - "Untuk membuat seperti yang ini, sentuh lama aplikasi, lalu pindahkan ke atas aplikasi lain." - "Oke" "Folder dibuka, %1$d x %2$d" "Sentuh untuk menutup folder" "Sentuh untuk menyimpan ganti nama" @@ -111,14 +68,34 @@ "Widget" "Wallpaper" "Setelan" - "Menunggu" - "Mengunduh" - "Memasang" + "Izinkan putaran" "Tidak dikenal" - "Tak dipulihkan" - "Buang Semua" "Buang" "Telusuri" "Aplikasi ini belum terpasang" "Aplikasi untuk ikon ini belum dipasang. Anda dapat membuangnya, atau menelusuri aplikasi dan memasangnya secara manual." + "Tambahkan ke layar Utama" + "Pindahkan item ke sini" + "Item ditambahkan ke layar utama" + "Item dihapus" + "Pindahkan item" + "Pindahkan ke baris %1$s kolom %2$s" + "PIndahkan ke posisi %1$s" + "Pindahkan ke posisi favorit %1$s" + "Item dipindahkan" + "Tambahkan ke folder: %1$s" + "Tambahkan ke folder dengan %1$s" + "Item ditambahkan ke folder" + "Buat folder dengan: %1$s" + "Folder dibuat" + "Pindahkan ke layar Utama" + "Pindahkan layar ke kiri" + "Pindahkan layar ke kanan" + "Layar dipindahkan" + "Ubah ukuran" + "Tambahi lebar" + "Tambahi tinggi" + "Kurangi lebar" + "Kurangi tinggi" + "Widget diubah ukurannya menjadi lebar %1$s tinggi %2$s" diff --git a/res/values-is-rIS/strings.xml b/res/values-is-rIS/strings.xml index a3fa0bc523..1d46cdc93e 100644 --- a/res/values-is-rIS/strings.xml +++ b/res/values-is-rIS/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Heim" - "Kjarnaforrit Android" + "Vinna" "Forritið er ekki uppsett." "Forritið er ekki í boði" "Sótt forrit er óvirkt í öryggisstillingu" "Græjur eru óvirkar í öruggri stillingu" - "Græjur" - "Græjur" "Sýna minni" "Haltu fingri á græju til að grípa hana." "%1$d × %2$d" - "Ekki er hægt að sleppa atriði á þennan heimaskjá." - "Veldu græju til að búa til" - "Möppuheiti" - "Endurnefna möppu" - "Í lagi" - "Hætta við" - "Bæta á heimaskjá" - "Forrit" - "Flýtileiðir" - "Græjur" - "Heimaskjáirnir þínir eru fullskipaðir." + "Leita í forritum" + "Hleður forrit…" + "Ekki fundust forrit sem samsvara „%1$s“" "Ekki meira pláss á þessum heimaskjá." "Ekki meira pláss í bakka fyrir uppáhald" - "Þessi græja er of stór fyrir bakkann fyrir uppáhald" - "Flýtileiðin „%s“ var búin til." - "Flýtileiðin „%s“ var fjarlægð." - "Flýtileiðin „%s“ er þegar til." - "Veldu flýtileið" - "Veldu forrit" "Forrit" "Heim" - "Fjarlægja" - "Eyða" "Fjarlægja" "Eyða" "Upplýsingar um forrit" - "Forrit" - "Fjarlægja" - "Fjarlægja uppfærslu" - "Fjarlægja forrit" - "Upplýsingar um forrit" - "1 forrit valið" - "1 græja valin" - "1 mappa valin" - "1 flýtileið valin" "setja upp flýtileiðir" "Leyfir forriti að bæta við flýtileiðum án íhlutunar notanda." - "fjarlægja flýtileiðir" - "Leyfir forriti að fjarlægja flýtileiðir án íhlutunar notanda." "lesa stillingar og flýtileiðir heimaskjás" "Leyfir forriti að lesa stillingar og flýtileiðir heimaskjás." "skrifa stillingar og flýtileiðir heimaskjás" @@ -78,30 +48,17 @@ "Vandamál við að hlaða græju" "Uppsetning" "Þetta er kerfisforrit sem ekki er hægt að fjarlægja." - "Eldflaugapallur" "Ónefnd mappa" - "Heimaskjár %1$d" "Síða %1$d af %2$d" "Heimaskjár %1$d af %2$d" - "Forritasíða %1$d af %2$d" - "Græjusíða %1$d af %2$d" "Komdu fagnandi" - "Komdu þér vel fyrir." - - - "Búðu til fleiri skjái fyrir forrit og möppur" "Afritaðu forritatáknin þín" "Flytja inn tákn og möppur af eldri heimaskjáum?" "AFRITA TÁKN" "BYRJA UPP Á NÝTT" - "Settu hlutina á sína staði" - "Haltu inni á bakgrunni til að stjórna veggfóðri, græjum og stillingum." "Veggfóður, græjur og stillingar" "Haltu fingri á bakgrunninum til að sérsníða hann" "ÉG SKIL" - "Hér er mappa" - "Til að búa til svona skaltu draga forrit yfir á annað forrit." - "Í lagi" "Mappa opnuð, %1$d sinnum %2$d" "Snertu til að loka möppunni" "Snertu til að staðfesta nýtt heiti" @@ -111,14 +68,34 @@ "Græjur" "Veggfóður" "Stillingar" - "Bíður" - "Sækir" - "Setur upp" + "Leyfa snúning" "Óþekkt" - "Ekki endurheimt" - "Fjarlægja öll" "Fjarlægja" "Leita" "Þetta forrit er ekki uppsett" "Forritið fyrir þetta tákn er ekki uppsett. Þú getur fjarlægt það eða leitað að forritinu og sett það upp handvirkt." + "Bæta á heimaskjá" + "Færa atriði hingað" + "Atriði bætt á heimaskjáinn" + "Atriði fjarlægt" + "Færa atriði" + "Færa í línu %1$s, dálk %2$s" + "Færa í stöðu %1$s" + "Færa í stöðu %1$s á festisvæði" + "Atriði fært" + "Setja í möppu: %1$s" + "Setja í möppu með %1$s" + "Atriði sett í möppu" + "Búa til möppu með: %1$s" + "Mappa búin til" + "Færa á heimaskjá" + "Færa skjá til vinstri" + "Færa skjá til hægri" + "Skjár færður" + "Breyta stærð" + "Auka breidd" + "Auka hæð" + "Minnka breidd" + "Minnka hæð" + "Stærð græju breytt í %1$s á breidd og %2$s á hæð" diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index d78d0326df..02057a19c5 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Home page" - "Applicazioni di base Android" + "Lavoro" "App non installata." "App non disponibile" "L\'app scaricata è stata disattivata in modalità provvisoria" "Widget disabilitati in modalità provvisoria" - "Widget" - "Widget" "Mostra Mem" "Tocca e tieni premuto per scegliere un widget." "%1$d × %2$d" - "Rilascio elemento in schermata Home non riuscito." - "Scegli il widget da creare" - "Nome cartella" - "Rinomina cartella" - "OK" - "Annulla" - "Aggiungi a schermata Home" - "App" - "Scorciatoie" - "Widget" - "Spazio nelle schermate Home esaurito." + "Cerca app" + "Caricamento di app…" + "Nessuna app trovata corrispondente a \"%1$s\"" "Spazio nella schermata Home esaurito." "Spazio esaurito nella barra dei Preferiti" - "Questo widget è troppo grande per la barra dei Preferiti" - "Scorciatoia \"%s\" creata." - "La scorciatoia \"%s\" è stata rimossa." - "Scorciatoia \"%s\" già presente." - "Scegli scorciatoia" - "Scegli app" "App" "Home page" - "Rimuovi" - "Disinstalla" "Rimuovi" "Disinstalla" "Informazioni app" - "App" - "Rimuovi" - "Disinstalla aggiornamento" - "Disinstalla app" - "Dettagli sull\'app" - "1 app selezionata" - "1 widget selezionato" - "1 cartella selezionata" - "1 scorciatoia selezionata" "aggiunta di scorciatoie" "Consente a un\'app di aggiungere scorciatoie automaticamente." - "eliminazione di scorciatoie" - "Consente all\'app di rimuovere scorciatoie automaticamente." "lettura di impostazioni e scorciatoie in Home" "Consente all\'app di leggere le impostazioni e le scorciatoie in Home." "creazione di impostazioni e scorciatoie in Home" @@ -78,30 +48,17 @@ "Errore durante il caricamento del widget" "Configurazione" "Questa è un\'app di sistema e non può essere disinstallata." - "Lanciamissili" "Cartella senza nome" - "Schermata Home %1$d" "Pagina %1$d di %2$d" "Schermata Home %1$d di %2$d" - "Pagina di applicazioni %1$d di %2$d" - "Pagina di widget %1$d di %2$d" "Benvenuto" - "Personalizza la schermata Home." - - - "Creare più schermate per app e cartelle" "Copia le icone delle tue app" "Importare icone e cartelle dalle schermate Home precedenti?" "COPIA ICONE" "RICOMINCIA" - "Organizza il tuo spazio" - "Tocca e tieni premuto lo sfondo per gestire sfondi, widget e impostazioni." "Sfondi, widget e impostazioni" "Tocca lo sfondo e tieni premuto per personalizzare" "OK" - "Ecco una cartella" - "Per crearne una simile, tocca un\'app e tieni premuto, dopodiché spostala sopra un\'altra." - "OK" "Cartella aperta, %1$d per %2$d" "Tocca per chiudere la cartella" "Tocca per salvare nuovo nome" @@ -111,14 +68,34 @@ "Widget" "Sfondi" "Impostazioni" - "In attesa" - "Download..." - "Installazione..." + "Consenti rotazione" "Sconosciuto" - "Non ripristinato" - "Rimuovi tutto" "Rimuovi" "Cerca" "L\'app non è installata" "L\'app per questa icona non è installata. Puoi rimuoverla o cercare l\'app e installarla manualmente." + "Aggiungi a schermata Home" + "Sposta elemento qui" + "Elemento aggiunto alla schermata Home" + "Elemento rimosso" + "Sposta elemento" + "Sposta a riga %1$s, colonna %2$s" + "Sposta nella posizione %1$s" + "Sposta nella posizione %1$s dei preferiti" + "Elemento spostato" + "Aggiungi alla cartella: %1$s" + "Aggiungi a cartella con %1$s" + "Elemento aggiunto alla cartella" + "Crea cartella con: %1$s" + "Cartella creata" + "Sposta nella schermata Home" + "Sposta schermata a sinistra" + "Sposta schermata a destra" + "Schermata spostata" + "Ridimensiona" + "Aumenta larghezza" + "Aumenta altezza" + "Riduci larghezza" + "Riduci altezza" + "Widget ridimensionato a larghezza %1$s, altezza %2$s" diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml index 61fb53efa1..a8039f7c18 100644 --- a/res/values-iw/strings.xml +++ b/res/values-iw/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "דף הבית" - "‏אפליקציות הליבה של Android" + "עבודה" "האפליקציה לא מותקנת." "האפליקציה אינה זמינה" "אפליקציה שהורדת הושבתה במצב בטוח" "ווידג\'טים מושבתים במצב בטוח" - "רכיבי ווידג\'ט" - "רכיבי ווידג\'ט" "הצג זכרון" "גע נגיעה רציפה בווידג\'ט כדי לבחור בו." "%1$d × %2$d" - "לא ניתן היה לשחרר את הפריט במסך דף הבית הזה." - "בחר ווידג\'ט ליצירה" - "שם תיקיה" - "שנה את שם התיקיה" - "אישור" - "בטל" - "הוסף למסך דף הבית" - "אפליקציות" - "קיצורי דרך" - "רכיבי ווידג\'ט" - "אין יותר מקום במסכי דף הבית." + "חפש אפליקציות" + "טוען אפליקציות…" + "לא נמצאו אפליקציות התואמות ל-\"%1$s\"" "אין עוד מקום במסך דף הבית הזה." "אין עוד מקום במגש המועדפים" - "הווידג\'ט הזה גדול מדי עבור מגש המועדפים." - "קיצור הדרך \'%s\' נוצר." - "קיצור הדרך \'%s\' הוסר." - "קיצור הדרך \'%s\' כבר קיים." - "בחר קיצור דרך" - "בחר אפליקציה" "אפליקציות" "דף הבית" - "הסר" - "הסר התקנה" "הסר" "הסר התקנה" "פרטי אפליקציה" - "אפליקציות" - "הסר" - "הסר את התקנת העדכון" - "הסר את התקנת האפליקציה" - "פרטי האפליקציה" - "נבחרה אפליקציה אחת" - "נבחר ווידג\'ט אחד" - "נבחרה תיקיה אחת" - "נבחר קיצור דרך אחד" "התקן קיצורי דרך" "מאפשר לאפליקציה להוסיף קיצורי דרך ללא התערבות המשתמש." - "הסר התקנה של קיצורי דרך" - "מאפשר לאפליקציה להסיר קיצורי דרך ללא התערבות המשתמש." "קרא הגדרות וקיצורי דרך של דף הבית" "מאפשר לאפליקציה לקרוא את ההגדרות וקיצורי הדרך בדף הבית." "כתוב הגדרות וקיצורי דרך של דף הבית" @@ -78,30 +48,17 @@ "בעיה בטעינת ווידג\'ט" "הגדר" "זוהי אפליקציית מערכת ולא ניתן להסיר את התקנתה." - "Rocket Launcher" "תיקיה ללא שם" - "‏מסך דף הבית %1$d" "‏דף %1$d מתוך %2$d" "‏מסך דף הבית %1$d מתוך %2$d" - "‏דף אפליקציות %1$d מתוך %2$d" - "‏דף רכיבי ווידג\'ט ‏%1$d מתוך %2$d" "ברוכים הבאים" - "להרגיש בבית." - - - "צור מסכים נוספים עבור אפליקציות ותיקיות" "העתקת סמלי האפליקציות שלך" "האם לייבא סמלים ותיקיות ממסכי דף הבית הישנים שלך?" "העתק סמלים" "התחל דף חדש" - "ארגן את אזור העבודה שלך" - "גע נגיעה רציפה ברקע כדי לנהל את הטפט, רכיבי הווידג\'ט וההגדרות." "טפטים, ווידג\'טים והגדרות" "גע והחזק ברקע לביצוע התאמה אישית" "הבנתי" - "הנה תיקייה" - "כדי ליצור תיקייה כזו, גע נגיעה רציפה באפליקציה, ולאחר מכן גרור ושחרר אותו על-גבי אפליקציה אחרת." - "אישור" "תיקיה פתוחה, %1$d על %2$d" "גע כדי לסגור את התיקיה" "גע כדי לשמור את שינוי השם" @@ -111,14 +68,34 @@ "רכיבי ווידג\'ט" "טפטים" "הגדרות" - "ממתין" - "מוריד" - "מתקין" + "אפשרות סיבוב" "לא ידוע" - "לא שוחזרה" - "הסר את הכל" "הסר" "חפש" "אפליקציה זו אינה מותקנת" "האפליקציה של סמל זה אינה מותקנת. ניתן להסיר אותו, או לחפש את האפליקציה ולהתקין אותה ידנית." + "הוסף למסך דף הבית" + "העבר את הפריט לכאן" + "הפריט הועבר אל מסך דף הבית" + "הפריט הוסר" + "העבר את הפריט" + "העבר אל שורה %1$s עמודה %2$s" + "העבר אל מיקום %1$s" + "העבר אל מיקום %1$s במועדפים" + "הפריט הועבר" + "הוסף לתיקייה: %1$s" + "העבר אל התיקייה עם %1$s" + "הפריט נוסף לתיקייה" + "צור תיקייה עם: %1$s" + "התיקייה נוצרה" + "העבר אל מסך דף הבית" + "הזז את המסך שמאלה" + "הזז את המסך ימינה" + "המסך הועבר" + "שנה גודל" + "הגדל רוחב" + "הגדל גובה" + "הקטן רוחב" + "הקטן גובה" + "גודל הווידג\'ט שונה - רוחב %1$s גובה %2$s" diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index ec872b8a90..ba58d88ef6 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "ホーム" - "Android Core Apps" + "仕事用" "このアプリはインストールされていません。" "このアプリは使用できません" "ダウンロードしたアプリは、セーフモードでは無効です" "セーフモードではウィジェットは無効です" - "ウィジェット" - "ウィジェット" "メモリーを表示" "ウィジェットを追加するには押し続けます。" "%1$dx%2$d" - "このホーム画面にアイテムをドロップできませんでした" - "作成するウィジェットの選択" - "フォルダ名" - "フォルダ名を変更" - "OK" - "キャンセル" - "ホーム画面に追加" - "アプリ" - "ショートカット" - "ウィジェット" - "ホーム画面に空きスペースがありません。" + "アプリを検索" + "アプリを読み込んでいます…" + "「%1$s」に一致するアプリは見つかりませんでした" "このホーム画面に空きスペースがありません。" "お気に入りトレイに空きスペースがありません" - "このウィジェットはお気に入りトレイには大きすぎます" - "ショートカット「%s」を作成しました。" - "ショートカット「%s」を削除しました。" - "ショートカット「%s」は既に存在します。" - "ショートカットを選択" - "アプリを選択" "アプリ" "ホーム" - "削除" - "アンインストール" "削除" "アンインストール" "アプリ情報" - "アプリ" - "削除" - "更新をアンインストール" - "アプリをアンインストール" - "アプリの詳細" - "1つのアプリが選択されています" - "1つのウィジェットが選択されています" - "1つのフォルダが選択されています" - "1つのショートカットが選択されています" "ショートカットのインストール" "ユーザー操作なしでショートカットを追加することをアプリに許可します。" - "ショートカットのアンインストール" - "ユーザー操作なしでショートカットを削除することをアプリに許可します。" "ホームの設定とショートカットの読み取り" "ホームの設定とショートカットの読み取りをアプリに許可します。" "ホームの設定とショートカットの書き込み" @@ -78,30 +48,17 @@ "ウィジェットを表示できません" "セットアップ" "このシステムアプリはアンインストールできません。" - "Rocket Launcher" "名前のないフォルダ" - "ホーム画面: %1$d" "%1$d/%2$dページ" "ホーム画面: %1$d/%2$d" - "アプリの%1$d/%2$dページ" - "ウィジェットの%1$d/%2$dページ" "ようこそ" - "ホームをカスタマイズします。" - - - "アプリとフォルダの画面をもっと作成します" "アプリのアイコンをコピー" "古いホーム画面からアイコンとフォルダをインポートしますか?" "アイコンをコピー" "初期状態にリセットする" - "スペースを整理" - "壁紙、ウィジェット、設定を管理するには、背景を押し続けます。" "壁紙、ウィジェット、設定" "カスタマイズするにはバックグラウンドを押し続けます" "OK" - "これがフォルダです" - "これと同じフォルダを作成するには、アプリを押し続けてから別のアプリの上に移動します。" - "OK" "フォルダが開いています。%1$dx%2$dの大きさです" "タップしてフォルダを閉じます" "タップして名前の変更を保存します" @@ -111,14 +68,34 @@ "ウィジェット" "壁紙" "設定" - "待機中" - "ダウンロード中" - "インストール中" + "回転を許可" "不明" - "復元失敗" - "すべて削除" "削除" "検索" "このアプリはインストールされていません" "このアイコンのアプリはインストールされていません。このアイコンは削除できます。または、手動でアプリを検索してインストールしください。" + "ホーム画面に追加" + "アイテムをここに移動" + "アイテムをホーム画面に追加しました" + "アイテムを削除しました" + "アイテムを移動" + "行%1$s、列%2$sに移動" + "位置%1$sに移動" + "お気に入りの位置%1$sに移動" + "アイテムを移動しました" + "フォルダ「%1$s」に追加" + "%1$sのあるフォルダに追加" + "アイテムをフォルダに追加しました" + "「%1$s」フォルダを作成" + "フォルダを作成しました" + "ホーム画面に移動" + "画面を左に移動" + "画面を右に移動" + "画面を移動しました" + "サイズを変更" + "幅を広くする" + "高さを高くする" + "幅を狭くする" + "高さを低くする" + "ウィジェットのサイズを幅%1$s、高さ%2$sに変更しました" diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml index b96d3d2672..07030d866e 100644 --- a/res/values-ka-rGE/strings.xml +++ b/res/values-ka-rGE/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "მთავარი" - "Android-ის ბირთვის აპები" + "სამუშაო" "აპი არ არის დაყენებული." "აპი მიუწვდომელია" "უსაფრთხო რეჟიმში ჩამოტვირთული აპი გაუქმებულია" "უსაფრთხო რეჟიმში ვიჯეტი გამორთულია" - "ვიჯეტები" - "ვიჯეტები" "Mem-ის ჩვენება" "შეეხეთ და დააყოვნეთ ვიჯეტის ასარჩევად." "%1$d × %2$d" - "ერთეულის მთავარ ეკრანზე ჩაგდება ვერ მოხერხდა." - "აირჩიეთ ვიჯეტი შესაქმნელად" - "საქაღალდის სახელი" - "საქაღალდის გადარქმევა" - "კარგი" - "გაუქმება" - "მთავარ ეკრანზე დამატება" - "აპები" - "მალსახმობები" - "ვიჯეტები" - "მთავარ ეკრანებზე ადგილი აღარ არის." + "აპების ძიება" + "აპები იტვირთება..." + "„%1$s“-ის თანხვედრი აპები არ მოიძებნა" "ამ მთავარ ეკრანზე ადგილი აღარ არის." "რჩეულების თაროზე ადგილი არ არის" - "ეს ვიჯეტი ძალიან დიდია რჩეულების თაროსთვის" - "შეიქმნა მალსახმობი „%s“." - "მასლახმობი „%s“ წაშლილია." - "მალსახმობი „%s“ უკვე არსებობს." - "აირჩიეთ მალსახმობი" - "აირჩიეთ აპი" "აპები" "მთავარი" - "წაშლა" - "დეინსტალაცია" "წაშლა" "დეინსტალაცია" "აპის შესახებ" - "აპები" - "წაშლა" - "განახლების დეინსტალაცია" - "აპის დეინსტალაცია" - "აპის შესახებ" - "არჩეულია 1 აპი" - "არჩეულია 1 ვიჯეტი" - "არჩეულია 1 საქაღალდე" - "არჩეულია 1 მალსახმობი" "მალსახმობების დაყენება" "აპისთვის მალსახმობების დამოუკიდებლად დამატების უფლების მიცემა." - "მალსახმობების წაშლა" - "აპისთვის მალსახმობების დამოუკიდებლად წაშლის უფლების მიცემა." "მთავარი ეკრანის პარამეტრებისა და მალსახმობების წაკითხვა" "აპისთვის მთავარი ეკრანის პარამეტრებისა და მალსახმობების წაკითხვის უფლების მიცემა." "მთავარი ეკრანის პარამეტრებისა და მალსახმობების ჩაწერა" @@ -78,30 +48,17 @@ "პრობლემა ვიჯეტის ჩატვირთვისას" "დაყენება" "ეს სისტემური აპია და მისი წაშლა შეუძლებელია." - "ფეიერვერკი" "უსახელო საქაღალდე" - "მთავარი ეკრანი %1$d" "გვერდი %1$d %2$d-დან" "მთავარი ეკრანი %1$d, %2$d-დან" - "აპების გვერდი %1$d, %2$d-დან" - "ვიჯეტების გვერდი %1$d, %2$d-დან" "მოგესალმებით" - "იგრძენით თავი საკუთარ სახლში" - - - "აპებისა და საქაღალდეებისთვის კიდევ ერთი ეკრანის შექმნა" "თქვენი აპის ხატულების კოპირება" "გსურთ, ძველი მთავარი ეკრანიდან ხატულების და საქაღ. იმპორტი?" "ხატულების კოპირება" "სტანდარტული განლაგება" - "თქვენი სივრცის ორგანიზება" - "თუ გსურთ ფონების, ვიჯეტების და პარამეტრების მართვა, შეეხეთ და არ აუშვათ ფონს." "ფონები, ვიჯეტები, & პარამეტრები" "მოსარგებად შეეხეთ & დააყოვნეთ ფონზე" "გასაგებია" - "აი, საქაღალდე" - "ასეთის შესაქმნელად, შეეხეთ და დააყოვნეთ აპზე, ხოლო შემდეგ გადააჩოჩეთ შემდეგზე." - "კარგი" "საქაღალდე გახსნილია, %1$d x %2$d" "შეეხეთ საქაღალდის დასახურად" "შეეხეთ ახალი სახელის შესანახად" @@ -111,14 +68,34 @@ "ვიჯეტები" "ფონები" "პარამეტრები" - "მოცდა..." - "ჩამოტვირთვა..." - "ინსტალაცია..." + "როტაციის დაშვება" "უცნობი" - "არ აღდგა" - "ყველას ამოშლა" "ამოშლა" "ძიება" "ეს აპი დაყენებული არ არის" "ამ ხატულის აპი დაყენებული არ არის. შეგიძლიათ ამოშალოთ, ან მოიძიოთ აპი და ხელით მოახდინოთ მისი ინსტალაცია." + "მთავარ ეკრანზე დამატება" + "ერთეულის გადაადგილება აქ" + "ერთეული დაემატა მთავარ ეკრანს" + "ერთეული წაიშალა" + "ერთეულის გადაადგილება" + "გადატანა რიგში %1$s სვეტში %2$s" + "გადატანა %1$s პოზიციაზე" + "გადატანა რჩეულთა პოზიციაზე %1$s" + "ერთეული გადაადგილდა" + "საქაღალდეში დამატება: %1$s" + "საქაღალდეში დამატება %1$s-ით" + "ერთეული დაემატა საქაღალდეს" + "საქაღალდის შექმნა ერთეულით: %1$s" + "საქაღალდე შექმნილია" + "მთავარ ეკრანზე გადატანა" + "ეკრანის გადატანა მარცხნივ" + "ეკრანის გადატანა მარჯვნით" + "ეკრანი გადაადგილდა" + "ზომის შეცვლა" + "სიგანის გაზრდა" + "სიმაღლის გაზრდა" + "სიგანის შემცირება" + "სიმაღლის შემცირება" + "ვიჯეტის ზომები შეიცვალა: სიგანე %1$s სიმაღლე %2$s" diff --git a/res/values-kk-rKZ/strings.xml b/res/values-kk-rKZ/strings.xml index c5b5a62f69..e79c9ce987 100644 --- a/res/values-kk-rKZ/strings.xml +++ b/res/values-kk-rKZ/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Негізгі" - "Android Core қолданбалары" + "Жұмыс" "Қолданба орнатылмаған." "Қолданба қол жетімді емес" "Жүктелген қолданба қауіпсіз режимде өшірілген" "Қауіпсіз режимде виджеттер өшіріледі" - "Виджеттер" - "Виджеттер" "Жадты көрсету" "Виджетті таңдау үшін түртіп, мықтап ұстаңыз." "%1$d × %2$d" - "Элементті осы Негізгі Экранға тастау орындалмады." - "Жасақтау үшін виджет таңдау" - "Қалта атауы" - "Қалтаның атауын өзгерту" - "Жарайды" - "Өшіру" - "Негізгі экранға қосу" - "Қолданбалар" - "Төте пернелер" - "Виджеттер" - "Негізгі экранда орын жоқ." + "Қолданбаларды іздеу" + "Қолданбалар жүктелуде…" + %1$s» сұрауына сәйкес келетін қолданбалар жоқ" "Бұл Негізгі экранда орын қалмады." "Қалаулылар науасында орын қалмады" - "Бұл виджет Қалаулылар науасы үшін тым үлкен" - "\"%s\" төте пернесі жасақталды." - "\"%s\" төте пернесі алынды." - "\"%s\" төте пернесі бұрыннан бар." - "Төте перне таңдау" - "Қолданба таңдау" "Қолданбалар" "Негізгі" - "Алып тастау" - "Алмау" "Алып тастау" "Алмау" "Қолданба ақпары" - "Қолданбалар" - "Алып тастау" - "Жаңартуды алмау" - "Қолданбаны алып тастау" - "Қолданба туралы толығырақ" - "1 қолданба таңдалған" - "1 виджет таңдалған" - "1 қалта таңдалған" - "1 төте перне таңдалған" "төте пернелерді орнату" "Қолданбаға пайдаланушының қатысуынсыз төте пернелерді қосу мүмкіндігін береді." - "төте пернелерді алып тастау" - "Қолданбаға пайдаланушының қатысуынсыз төте пернелерді алу мүмкіндігін береді." "Негізгі экрандағы параметрлер мен төте пернелерді оқу" "Қолданбаға Негізгі экрандағы параметрлер мен төте пернелерді оқу мүмкіндігін береді." "Негізгі экран параметрлері мен төте пернелерін жазу" @@ -78,30 +48,17 @@ "Виджетті жүктеу барысында мәселе орын алды" "Орнату" "Бұл жүйе қолданбасы, сондықтан оны алу мүмкін емес." - "Rocket Launcher" "Атауы жоқ қалта" - "%1$d негізгі экран" "%1$d бет, барлығы %2$d" "%1$d негізгі экран, барлығы %2$d" - "%1$d қолданба беті, барлығы %2$d" - "%1$d виджет беті, барлығы %2$d" "Қош келдіңіз" - "Өзіңізді ыңғайлы сезініңіз." - - - "Қолданбалар мен қалталар үшін көбірек экрандар жасау" "Қолданба таңбаларын көшіру" "Бұрынғы негізгі экрандарыңыздағы таңбалар мен қалталар импортталсын ба?" "ТАҢБАЛАРДЫ КӨШІРУ" "ЖАҢАДАН БАСТАУ" - "Кеңістікті реттеу" - "Артқы фонды, виджеттерді және параметрлерді басқару үшін артқы шебін түртіп, мықтап ұстаңыз." "Тұсқағаздар, виджеттер және параметрлер" "Теңшеу үшін фонды түртіп, ұстап тұрыңыз" "ТҮСІНДІМ" - "Міне, қалта." - "Осы сияқты қалта жасау үшін, қолданбаны түртіп, мықтап ұстаңыз, одан кейін екіншісінің үстінен жылжытыңыз." - "Жарайды" "Қалта ашылды, %1$d және %2$d" "Қалтаны жабу үшін түртіңіз" "Өзгертілген атауын сақтау үшін түртіңіз" @@ -111,14 +68,34 @@ "Виджеттер" "Артқы фондар" "Параметрлер" - "Күтілуде" - "Жүктелуде" - "Орнатылуда" + "Айналуға рұқсат ету" "Белгісіз" - "Қалп. кел-меді" - "Барлығын алып тастау" "Алып тастау" "Іздеу" "Бұл қолданба орнатылмаған" "Осы белгіше үшін қолданба орнатылмаған. Оны жоюға болады немесе қолданбаны іздеп, қолмен орнатуға болады." + "Негізгі экранға қосу" + "Элементті мұнда жылжыту" + "Элемент негізгі экранға қосылды" + "Элемент жойылды" + "Элементті жылжыту" + "%1$s-жол, %2$s-бағанға жылжыту" + "%1$s-орынға жылжыту" + "%1$s нөмірлі таңдаулы орынға жылжыту" + "Элемент жылжытылды" + "Қалтаға қосу: %1$s" + "%1$s бар қалтаға қосу" + "Элемент қалтаға қосылды" + "Мына бар қалтаны жасау: %1$s" + "Қалта жасалды" + "Негізгі экранға жылжыту" + "Экранды солға жылжыту" + "Экранды оңға жылжыту" + "Экран жылжытылды" + "Өлшемін өзгерту" + "Енін арттыру" + "Биіктігін арттыру" + "Енін азайту" + "Биіктігін азайту" + "Виджет өлшемінің ені %1$s, биіктігі %2$s болып өзгертілді" diff --git a/res/values-km-rKH/strings.xml b/res/values-km-rKH/strings.xml index 670ac70b5a..9f7f53717b 100644 --- a/res/values-km-rKH/strings.xml +++ b/res/values-km-rKH/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "ដើម" - "កម្មវិធី​​សំខាន់​ៗ​របស់ Android" + "ការងារ" "មិន​បាន​ដំឡើង​កម្មវិធី។" "មិន​មាន​កម្មវិធី" "បាន​បិទ​កម្មវិធី​ដែល​បាន​ទាញ​យក​ក្នុង​របៀប​សុវត្ថិភាព" "បាន​បិទ​ធាតុ​ក្រាហ្វិក​ក្នុង​របៀប​សុវត្ថិភាព" - "ធាតុ​ក្រាហ្វិក" - "ធាតុ​ក្រាហ្វិក" "បង្ហាញ​ Mem" "ប៉ះ & សង្កត់ ដើម្បី​ជ្រើស​ធាតុ​ក្រាហ្វិក។" "%1$d × %2$d" - "មិន​អាច​ទម្លាក់​ធាតុ​លើ​អេក្រង់​ដើម​នេះ​ទេ" - "ជ្រើស​ធាតុ​ក្រាហ្វិក ដើម្បី​​​បង្កើត" - "ឈ្មោះ​ថត" - "ប្ដូរ​ឈ្មោះ​ថត" - "យល់ព្រម" - "បោះបង់" - "បន្ថែម​ទៅ​អេក្រង់​ដើម" - "កម្មវិធី" - "ផ្លូវកាត់" - "ធាតុ​ក្រាហ្វិក" - "គ្មាន​បន្ទប់​នៅ​លើ​អេក្រង់​ដើម​រស់​អ្នក​ទៀត​ទេ។" + "ស្វែងរកកម្មវិធី" + "កំពុងដំណើរការកម្មវិធី..." + "គ្មានកម្មវិធីដែលត្រូវជាមួយ \"%1$s\" ទេ" "គ្មាន​បន្ទប់​នៅ​លើ​អេក្រង់​ដើម​នេះ​ទៀត​ទេ។" "គ្មាន​បន្ទប់​​ក្នុង​ថាស​និយម​ប្រើ" - "ធាតុ​ក្រាហ្វិក​នេះ​ធំ​ពេក​សម្រាប់​ថាស​និយម​ប្រើ" - "បាន​បង្កើត​ផ្លូវកាត់ \"%s\" ។" - "បាន​លុប​ផ្លូវកាត់ \"%s\" ។" - "មាន​ផ្លូវកាត់ \"%s\" រួច​ហើយ។" - "ជ្រើស​ផ្លូវកាត់" - "ជ្រើស​កម្មវិធី" "កម្មវិធី" "ដើម" - "លុប​ចេញ" - "លុប" "លុប​ចេញ" "លុប" "ព័ត៌មាន​កម្មវិធី" - "កម្មវិធី" - "លុប​ចេញ" - "លុប​បច្ចុប្បន្នភាព" - "លុប​កម្មវិធី" - "ព័ត៌មាន​លម្អិត​កម្មវិធី" - "បាន​ជ្រើស​កម្មវិធី ១" - "បាន​ជ្រើស​ធាតុ ១" - "បាន​ជ្រើស​ថត ១" - "បាន​ជ្រើស​ផ្លូវកាត់ ១" "ដំឡើង​ផ្លូវកាត់" "អនុញ្ញាត​ឲ្យ​កម្មវិធី​បន្ថែម​ផ្លូវកាត់​ ដោយ​មិន​ចាំបាច់​​អំពើ​ពី​អ្នក​ប្រើ។" - "លុប​ផ្លូវកាត់" - "អនុញ្ញាត​ឲ្យ​កម្មវិធី​លុប​ផ្លូវកាត់​ដោយ​មិន​ចាំបាច់​អំពើ​ពី​អ្នក​ប្រើ។" "អាន​ការ​កំណត់​ និង​ផ្លូវកាត់​​អេក្រង់​ដើម" "អនុញ្ញាត​ឲ្យ​កម្មវិធី​អាន​ការ​កំណត់ និង​ផ្លូវកាត់​ក្នុង​អេក្រង់​ដើម។" "សរសេរ​ការ​កំណត់ ​និង​ផ្លូវកាត់​​លើ​អេក្រង់​ដើម" @@ -78,30 +48,17 @@ "បញ្ហា​ក្នុង​ការ​ផ្ទុក​ធាតុ​​ក្រាហ្វិក" "រៀបចំ" "នេះ​​​ជា​កម្មវិធី​ប្រព័ន្ធ មិន​អាច​លុប​បាន​ទេ។" - "កម្មវិធី​ចាប់ផ្ដើម​រ៉ូកែត" "ថត​គ្មាន​ឈ្មោះ" - "អេក្រង់​ដើម %1$d" "ទំព័រ %1$d នៃ %2$d" "អេក្រង់​ដើម %1$d នៃ %2$d" - "ទំព័រ​កម្មវិធី %1$d នៃ %2$d" - "ទំព័រ​ធាតុ​ក្រាហ្វិក ​%1$d នៃ %2$d" "សូម​ស្វាគមន៍​" - "ធ្វើ​ដោយ​ខ្លួន​ឯង​នៅ​លើ​អេក្រង់​ដើម។" - - - "បង្កើត​អេក្រង់​ច្រើន​សម្រាប់​កម្មវិធី​ ​និង​ថតឯកសារ" "ចម្លង​រូបតំណាង​កម្មវិធី​របស់​អ្នក" "នាំចូល​រូបតំណាង និង​ថត​ពី​អេក្រង់​ដើម​ចាស់​របស់​អ្នក?" "រូប​តំណាង​ច្បាប់​ចម្លង" "ចាប់ផ្ដើម​ធ្វើ​ឲ្យ​ស្រស់" - "រៀបចំ​ចន្លោះ​របស់​អ្នក" - "ប៉ះ & សង្កត់​លើ​ផ្ទៃ​ខាង​ក្រោម ដើម្បី​គ្រប់គ្រង​ផ្ទាំង​រូបភាព, ធាតុ​ក្រាហ្វិក និង​ការ​កំណត់។" "ផ្ទាំងរូបភាព,ធាតុក្រាហ្វិក & ការកំណត់" "ប៉ះ & សង្កត់​ផ្ទៃ​ខាង​ក្រោយ​ដើម្បី​ប្ដូរ​តាម​​តម្រូវ​ការ" "យល់​ហើយ" - "នេះ​ជា​ថត" - "ដើម្បី​បង្កើត​មួយ​ដូច​នេះ ប៉ះ & សង្កត់​​លើ​កម្មវិធី បន្ទាប់​មក​ផ្លាស់ទី​វា​ទៅ​លើ​ធាតុ​មួយ​ផ្សេង​ទៀត។" - "យល់ព្រម" "បាន​បើក​ថត %1$d ដោយ %2$d" "ប៉ះ ដើម្បី​បិទ​ថត" "ប៉ះ ដើម្បី​រក្សាទុក​ការ​ប្ដូរ​ឈ្មោះ" @@ -111,14 +68,34 @@ "ធាតុ​ក្រាហ្វិក" "ផ្ទាំង​រូបភាព" "ការកំណត់" - "រង់ចាំ" - "​ទាញ​យក" - "ដំឡើង" + "អនុញ្ញាតឲ្យបង្វិល" "មិន​ស្គាល់" - "មិនបាន​​ស្តា​រ" - "លុបចេញ​​​ទាំងអស់" "លុបចេញ" "ស្វែងរក" "មិន​បាន​ដំឡើង​កម្មវិធី​នេះ" "មិន​បាន​ដំឡើង​កម្មវិធី​សម្រាប់​រូបតំណាង​នេះ។ អ្នក​អាច​លុប​វា ឬ​ស្វែងរក​កម្មវិធី និង​ដំឡើង​វា​ដោយ​ដៃ។" + "បន្ថែមទៅអេក្រង់ដើម" + "ផ្លាស់ធាតុមកទីនេះ" + "ធាតុដែលត្រូវបានបន្ថែមទៅអេក្រង់ដើម" + "ធាតុដែលបានដកចេញ" + "ផ្លាស់ទីធាតុ" + "ផ្លាស់ទីទៅជួរដេកទី %1$s ជួរឈរទី %2$s" + "ផ្លាស់ទីទៅទីតាំង %1$s" + "ផ្លាស់ទីទៅការចូលចិត្តទីតាំងទី %1$s" + "បានផ្លាស់ទីធាតុ" + "បន្ថែមទៅថតឯកសារ៖ %1$s" + "បន្ថែមទៅថតឯកសារដែលមានឈ្មោះ %1$s" + "បានបន្ថែមធាតុទៅថតឯកសារ" + "បង្កើតថតឯកសារជាមួយ៖ %1$s" + "បានបង្កើតថតឯកសារ" + "ផ្លាស់ទៅអេក្រង់ដើម" + "រំកិលអេក្រង់ទៅខាងឆ្វេង" + "រំកិលអេក្រង់ទៅខាងស្តាំ" + "អេក្រង់ដែលបានផ្លាស់ទី" + "ប្ដូរទំហំ" + "បង្កើនទទឹង" + "បង្កើនកម្ពស់" + "បន្ថយទទឹង" + "បន្ថយកម្ពស់" + "ធាតុក្រាហ្វិកដែលបានប្តូរទំហំទៅទទឹងប្រវែង %1$s កម្ពស់ប្រវែង %2$s" diff --git a/res/values-kn-rIN/strings.xml b/res/values-kn-rIN/strings.xml index 7a070511df..5cd8792dd4 100644 --- a/res/values-kn-rIN/strings.xml +++ b/res/values-kn-rIN/strings.xml @@ -20,57 +20,27 @@ "ಲಾಂಚರ್3" - "ಮುಖಪುಟ" - "Android Core ಅಪ್ಲಿಕೇಶನ್‌ಗಳು" + "ಕೆಲಸ" "ಅಪ್ಲಿಕೇಶನ್‌ ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ" "ಅಪ್ಲಿಕೇಶನ್ ಲಭ್ಯವಿಲ್ಲ" "ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾದ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಸುರಕ್ಷಿತ ಮೋಡ್‌ನಲ್ಲಿ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ" "ಸುರಕ್ಷಿತ ಮೋಡ್‌ನಲ್ಲಿ ವಿಜೆಟ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ" - "ವಿಜೆಟ್‌ಗಳು" - "ವಿಜೆಟ್‌ಗಳು" "ಸ್ಮರಣೆ ತೋರಿಸು" "ವಿಜೆಟ್ ಅನ್ನು ಆರಿಸಿಕೊಳ್ಳಲು ಸ್ಪರ್ಶಿಸಿ & ಹಿಡಿದುಕೊಳ್ಳಿ." "%1$d × %2$d" - "ಈ ಮುಖಪುಟದ ಪರದೆಯಲ್ಲಿ ಐಟಂ ಅನ್ನು ಬಿಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ." - "ರಚಿಸಲು ವಿಜೆಟ್‌ ಆಯ್ಕೆಮಾಡಿ" - "ಫೋಲ್ಡರ್ ಹೆಸರು" - "ಫೋಲ್ಡರ್ ಅನ್ನು ಮರುಹೆಸರಿಸಿ" - "ಸರಿ" - "ರದ್ದುಮಾಡು" - "ಮುಖಪುಟಕ್ಕೆ ಸೇರಿಸು" - "ಅಪ್ಲಿಕೇಶನ್‌ಗಳು" - "ಶಾರ್ಟ್‌ಕಟ್‌ಗಳು" - "ವಿಜೆಟ್‌ಗಳು" - "ನಿಮ್ಮ ಮುಖಪುಟದ ಪರದೆಗಳಲ್ಲಿ ಯಾವುದೇ ಸ್ಥಳಾವಕಾಶವಿಲ್ಲ" + "ಅಪ್ಲಿಕೇಷನ್‌ಗಳನ್ನು ಹುಡುಕಿ" + "ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ..." + "\"%1$s\" ಹೊಂದಿಕೆಯ ಯಾವುದೇ ಅಪ್ಲಿಕೇಶನ್‌ಗಳು ಕಂಡುಬಂದಿಲ್ಲ" "ಈ ಮುಖಪುಟದ ಪರದೆಯಲ್ಲಿ ಹೆಚ್ಚು ಸ್ಥಳಾವಕಾಶವಿಲ್ಲ." "ಮೆಚ್ಚಿನವುಗಳ ಟ್ರೇನಲ್ಲಿ ಹೆಚ್ಚಿನ ಸ್ಥಳಾವಕಾಶವಿಲ್ಲ" - "ಮೆಚ್ಚಿನವುಗಳ ಟ್ರೇಗೆ ಈ ವಿಜೆಟ್‌ ತುಂಬಾ ದೊಡ್ಡದಾಗಿದೆ" - "ಶಾರ್ಟ್‌ಕಟ್‌ \"%s\" ಅನ್ನು ರಚಿಸಲಾಗಿದೆ." - "ಶಾರ್ಟ್‌ಕಟ್‌ \"%s\" ಅನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ." - "ಶಾರ್ಟ್‌ಕಟ್‌ \"%s\" ಈಗಾಗಲೇ ಅಸ್ತಿತ್ವದಲ್ಲಿದೆ." - "ಶಾರ್ಟ್‌ಕಟ್‌ ಆರಿಸಿ" - "ಅಪ್ಲಿಕೇಶನ್ ಆಯ್ಕೆಮಾಡಿ" "ಅಪ್ಲಿಕೇಶನ್‌ಗಳು" "ಮುಖಪುಟ" - "ತೆಗೆದುಹಾಕು" - "ಅಸ್ಥಾಪಿಸು" "ತೆಗೆದುಹಾಕು" "ಅಸ್ಥಾಪಿಸು" "ಅಪ್ಲಿಕೇಶನ್ ಮಾಹಿತಿ" - "ಅಪ್ಲಿಕೇಶನ್‌ಗಳು" - "ತೆಗೆದುಹಾಕು" - "ನವೀಕರಣವನ್ನು ಅಸ್ಥಾಪಿಸು" - "ಅಪ್ಲಿಕೇಶನ್ ಅಸ್ಥಾಪಿಸು" - "ಅಪ್ಲಿಕೇಶನ್ ವಿವರಗಳು" - "1 ಅಪ್ಲಿಕೇಶನ್‌ ಅನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ" - "1 ವಿಜೆಟ್‌ ಆಯ್ಕೆಮಾಡಲಾಗಿದೆ" - "1 ಫೋಲ್ಡರ್‌ ಅನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ" - "1 ಶಾರ್ಟ್‌ಕಟ್‌ ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ" "ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಸ್ಥಾಪಿಸಿ" "ಬಳಕೆದಾರರ ಹಸ್ತಕ್ಷೇಪವಿಲ್ಲದೆ ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಸೇರಿಸಲು ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಅನುಮತಿಸುತ್ತದೆ." - "ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಅಸ್ಥಾಪಿಸಿ" - "ಬಳಕೆದಾರರ ಹಸ್ತಕ್ಷೇಪವಿಲ್ಲದೆ ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ತೆಗೆದುಹಾಕಲು ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಅನುಮತಿ ನೀಡುತ್ತದೆ." "ಮುಖಪುಟದ ಸೆಟ್ಟಿಂಗ್‌ಗಳು ಮತ್ತು ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಓದಿ" "ಮುಖಪುಟದಲ್ಲಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳು ಮತ್ತು ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಓದಲು ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಅನುಮತಿ ನೀಡುತ್ತದೆ." "ಮುಖಪುಟದ ಸೆಟ್ಟಿಂಗ್‌ಗಳು ಮತ್ತು ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ಬರೆಯಿರಿ" @@ -78,30 +48,17 @@ "ವಿಜೆಟ್ ಲೋಡ್‌ ಮಾಡುವಲ್ಲಿ ಸಮಸ್ಯೆ" "ಸೆಟಪ್" "ಇದೊಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಗಿದೆ ಮತ್ತು ಅಸ್ಥಾಪಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ರಾಕೆಟ್ ಲಾಂಚರ್" "ಹೆಸರಿಲ್ಲದ ಫೋಲ್ಡರ್" - "ಮುಖಪುಟದ ಪರದೆ %1$d" "%2$d ರಲ್ಲಿ %1$d ಪುಟ" "%2$d ರಲ್ಲಿ %1$d ಮುಖಪುಟದ ಪರದೆ" - "%2$d ರಲ್ಲಿ %1$d ಅಪ್ಲಿಕೇಶನ್ ಪುಟ" - "%2$d ರಲ್ಲಿ %1$d ವಿಜೆಟ್‌ಗಳ ಪುಟ" "ಸುಸ್ವಾಗತ" - "ನಿಮ್ಮ ಮನೆಯಂತೆ ಭಾವಿಸಿ." - - - "ಅಪ್ಲಿಕೇಶನ್‌ಗಳು ಮತ್ತು ಫೋಲ್ಡರ್‌ಗಳಿಗಾಗಿ ಇನ್ನಷ್ಟು ಪರದೆಗಳನ್ನು ರಚಿಸಿ" "ಅಪ್ಲಿಕೇಶನ್‌ ಐಕಾನ್‌ ನಕಲಿಸು" "ನಿಮ್ಮ ಹಳೆಯ ಮುಖಪುಟದ ಪರದೆಗಳಿಂದ ಐಕಾನ್‌ಗಳು ಮತ್ತು ಫೋಲ್ಡರ್‌ಗಳನ್ನು ಆಮದು ಮಾಡಿಕೊಳ್ಳುವುದೇ?" "ಐಕಾನ್‌ಗಳನ್ನು ನಕಲಿಸು" "ಹೊಸದಾಗಿ ಪ್ರಾರಂಭಿಸು" - "ನಿಮ್ಮ ಸ್ಥಳವನ್ನು ವ್ಯವಸ್ಥಿತಗೊಳಿಸಿ" - "ವಾಲ್‌ಪೇಪರ್‌, ವಿಜೆಟ್‌ಗಳು ಮತ್ತು ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಲು ಹಿನ್ನೆಲೆಯನ್ನು ಸ್ಪರ್ಶಿಸಿ & ಹಿಡಿದುಕೊಳ್ಳಿ." "ವಾಲ್‌ಪೇಪರ್‌ಗಳು, ವಿಜೆಟ್‌ಗಳು, & ಸೆಟ್ಟಿಂಗ್‌ಗಳು" "ಕಸ್ಟಮೈಸ್ ಮಾಡಲು ಹಿನ್ನೆಲೆಯನ್ನು ಸ್ಪರ್ಶಿಸಿ & ಒತ್ತಿ ಹಿಡಿಯಿರಿ" "ಅರ್ಥವಾಯಿತು" - "ಇಲ್ಲೊಂದು ಫೋಲ್ಡರ್ ಇದೆ" - "ಈ ರೀತಿ ರಚಿಸಲು, ಸ್ಪರ್ಶಿಸಿ & ಆಪ್‌ ಹಿಡಿದುಕೊಂಡು ಮತ್ತೊಂದರ ಮೇಲೆ ಸರಿಸಿ." - "ಸರಿ" "ಫೋಲ್ಡರ್ ತೆರೆಯಲಾಗಿದೆ, %1$d ಬೈ %2$d" "ಫೋಲ್ಡರ್‌ ಮುಚ್ಚಲು ಸ್ಪರ್ಶಿಸಿ" "ಮರುಹೆಸರನ್ನು ಉಳಿಸಲು ಸ್ಪರ್ಶಿಸಿ" @@ -111,14 +68,34 @@ "ವಿಜೆಟ್‌ಗಳು" "ವಾಲ್‌ಪೇಪರ್‌ಗಳು" "ಸೆಟ್ಟಿಂಗ್‌ಗಳು" - "ನಿರೀಕ್ಷಿಸಲಾಗುತ್ತಿದೆ" - "ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಸ್ಥಾಪಿಸಲಾಗುತ್ತಿದೆ" + "ತಿರುಗಿಸುವಿಕೆಯನ್ನು ಅನುಮತಿಸಿ" "ಅಜ್ಞಾತ" - "ಇನ್ನೂ ಪುನಃಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ" - "ಎಲ್ಲವನ್ನೂ ತೆಗೆದುಹಾಕಿ" "ತೆಗೆದುಹಾಕಿ" "ಹುಡುಕು" "ಈ ಅಪ್ಲಿಕೇಶನ್ ಸ್ಥಾಪನೆಗೊಂಡಿಲ್ಲ" "ಈ ಐಕಾನ್ ಅಪ್ಲಿಕೇಶನ್ ಸ್ಥಾಪನೆಗೊಂಡಿಲ್ಲ. ನೀವು ಅದನ್ನು ತೆಗೆದುಹಾಕಬಹುದು ಅಥವಾ ಅಪ್ಲಿಕೇಶನ್ ಹುಡುಕಬಹುದು ಮತ್ತು ಹಸ್ತಚಾಲಿತವಾಗಿ ಅದನ್ನು ಸ್ಥಾಪಿಸಬಹುದು." + "ಮುಖಪುಟಕ್ಕೆ ಸೇರಿಸು" + "ಐಟಂ ಇಲ್ಲಿಗೆ ಸರಿಸಿ" + "ಮುಖಪುಟ ಪರದೆಗೆ ಐಟಂ ಸೇರಿಸಲಾಗಿದೆ" + "ಐಟಂ ತೆಗೆದುಹಾಕಲಾಗಿದೆ" + "ಐಟಂ ಸರಿಸಿ" + "%1$s ಸಾಲು %2$s ಕಾಲಮ್‌ಗೆ ಸರಿಸಿ" + "%1$s ಸ್ಥಾನಕ್ಕೆ ಸರಿಸಿ" + "ಮೆಚ್ಚಿನ %1$s ಸ್ಥಾನಕ್ಕೆ ಸರಿಸಿ" + "ಐಟಂ ಸರಿಸಲಾಗಿದೆ" + "ಫೋಲ್ಡರ್‌ಗೆ ಸೇರಿಸಿ: %1$s" + "%1$s ಮೂಲಕ ಫೋಲ್ಡರ್‌ಗೆ ಸೇರಿಸಿ" + "ಐಟಂ ಅನ್ನು ಫೋಲ್ಡರ್‌ಗೆ ಸೇರಿಸಲಾಗಿದೆ" + "ಇದನ್ನು ಬಳಸಿಕೊಂಡು ಫೋಲ್ಡರ್ ರಚಿಸಿ: %1$s" + "ಫೋಲ್ಡರ್ ರಚಿಸಲಾಗಿದೆ" + "ಮುಖಪುಟಕ್ಕೆ ಸರಿಸಿ" + "ಪರದೆಯನ್ನು ಎಡಕ್ಕೆ ಸರಿಸಿ" + "ಪರದೆಯನ್ನು ಬಲಕ್ಕೆ ಸರಿಸಿ" + "ಪರದೆ ಸರಿಸಲಾಗಿದೆ" + "ಮರುಗಾತ್ರ" + "ಅಗಲವನ್ನು ಹೆಚ್ಚು ಮಾಡಿ" + "ಎತ್ತರವನ್ನು ಹೆಚ್ಚು ಮಾಡಿ" + "ಅಗಲವನ್ನು ಕಡಿಮೆ ಮಾಡಿ" + "ಎತ್ತರವನ್ನು ಕಡಿಮೆ ಮಾಡಿ" + "ವಿಜೆಟ್ ಅನ್ನು %1$s ಅಗಲ %2$s ಎತ್ತರಕ್ಕೆ ಮರುಗಾತ್ರಗೊಳಿಸಲಾಗಿದೆ" diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index c73b431bda..fa445acca2 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "홈" - "Android 핵심 앱" + "업무" "앱이 설치되지 않았습니다." "앱을 사용할 수 없음" "다운로드한 앱은 안전 모드에서 사용할 수 없습니다." "안전 모드에서 위젯 사용 중지됨" - "위젯" - "위젯" "메모리 표시" "위젯을 선택하려면 길게 터치하세요." "%1$d×%2$d" - "홈 화면에 항목을 놓을 수 없습니다." - "만들 위젯 선택" - "폴더 이름" - "폴더 이름 바꾸기" - "확인" - "취소" - "홈 화면에 추가" - "앱" - "바로가기" - "위젯" - "홈 화면에 더 이상 공간이 없습니다." + "앱 검색" + "앱 로드 중..." + "\'%1$s\'와(과) 일치하는 앱이 없습니다." "홈 화면에 더 이상 공간이 없습니다." "즐겨찾기 트레이에 더 이상 공간이 없습니다." - "위젯이 너무 커서 즐겨찾기 트레이에 들어갈 수 없습니다." - "바로가기(\'%s\')가 생성되었습니다." - "바로가기(\'%s\')가 삭제되었습니다." - "바로가기(\'%s\')가 이미 있습니다." - "바로가기 선택" - "앱 선택" "앱" "홈" - "삭제" - "제거" "삭제" "제거" "앱 정보" - "앱" - "삭제" - "업데이트 제거" - "앱 제거" - "앱 세부정보" - "앱 1개 선택됨" - "위젯 1개 선택됨" - "폴더 1개 선택됨" - "바로가기 1개 선택됨" "바로가기 설치" "앱이 사용자의 작업 없이 바로가기를 추가할 수 있도록 합니다." - "바로가기 제거" - "앱이 사용자의 작업 없이 바로가기를 삭제할 수 있도록 합니다." "홈 설정 및 바로가기 읽기" "앱이 홈에 있는 설정 및 바로가기를 읽을 수 있도록 합니다." "홈 설정 및 바로가기 쓰기" @@ -78,30 +48,17 @@ "위젯을 로드하는 중 문제가 발생했습니다." "설정" "시스템 앱은 제거할 수 없습니다." - "로켓 실행기" "이름이 없는 폴더" - "홈 화면 %1$d" "페이지 %1$d/%2$d" "홈 화면 %1$d/%2$d" - "앱 페이지 %1$d/%2$d" - "위젯 페이지 %1$d/%2$d" "환영합니다." - "편리한 사용 환경을 만드세요." - - - "앱 및 폴더를 표시할 화면 더 만들기" "앱 아이콘 복사" "이전 메인 스크린에서 아이콘과 폴더를 가져오시겠습니까?" "아이콘 복사" "새로 시작" - "공간 관리하기" - "배경화면, 위젯, 설정을 관리하려면 백그라운드를 길게 터치합니다." "배경화면, 위젯, 설정" "백그라운드를 길게 터치하여 맞춤설정합니다." "확인" - "폴더" - "폴더를 만들려면 앱을 길게 터치한 다음 다른 앱 위에 올려 놓으세요." - "확인" "폴더 열림(%1$dX%2$d)" "터치하여 폴더를 닫음" "터치하여 바꾼 이름을 저장" @@ -111,14 +68,34 @@ "위젯" "배경화면" "설정" - "대기 중" - "다운로드 중" - "설치 중" + "회전 허용" "알 수 없음" - "복원되지 않음" - "모두 삭제" "삭제" "검색" "이 앱이 설치되어 있지 않음" "이 아이콘의 앱이 설치되어 있지 않습니다. 아이콘을 삭제하거나 앱을 검색하여 수동으로 설치하세요." + "메인 스크린에 추가" + "여기에 항목을 이동" + "메인 스크린에 항목 추가됨" + "항목 삭제됨" + "항목 이동" + "%1$s%2$s열로 이동" + "%1$s번 위치로 이동" + "즐겨찾는 %1$s번 위치로 이동" + "항목을 이동했습니다." + "폴더에 추가: %1$s" + "%1$s이(가) 포함된 폴더에 추가" + "항목이 폴더에 추가되었습니다." + "다음이 포함된 폴더 만들기: %1$s" + "폴더를 만들었습니다." + "메인 스크린으로 이동" + "화면을 왼쪽으로 이동" + "화면을 오른쪽으로 이동" + "화면 이동됨" + "크기 조정" + "폭 늘리기" + "높이 늘리기" + "폭 줄이기" + "높이 줄이기" + "폭 %1$s, 높이 %2$s로 위젯 크기 조정됨" diff --git a/res/values-ky-rKG/strings.xml b/res/values-ky-rKG/strings.xml index a8a605b26b..20787489e5 100644 --- a/res/values-ky-rKG/strings.xml +++ b/res/values-ky-rKG/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Үйгө" - "Android Core колдонмолору" + "Жумуш" "Колдонмо орнотулган эмес." "Колдонмо жеткиликтүү эмес" "Жүктөп алынган колдонмо Коопсуз режиминде иштен чыгарылды" "Виджеттер Коопсуз режимде өчүрүлгөн" - "Виджеттер" - "Виджеттер" "Мемди көргөзүү" "Виджетти тандаш үчүн, басып туруңуз" "%1$d × %2$d" - "Муну бул Үй экранына ыргытуу мүмкүн эмес." - "Түзүлүүчү виджетти тандаңыз" - "Фолдердин аты" - "Фолдердин атын өзгөртүү" - "OK" - "Жокко чыгаруу" - "Үй экранына кошуу" - "Колдонмолор" - "Тез чакырмалар" - "Виджеттер" - "Үй экрандарыңызда бош орун калган жок." + "Колдонмолорду издөө" + "Колдонмолор жүктөлүүдө…" + "\"%1$s\" дал келген колдонмолор табылган жок" "Бул Үй экранында бош орун жок." "Тандамалдар тайпасында орун калган жок" - "Бул виджет Тандамалдар үчүн өтө чоң" - "\"%s\" тез чакырмасы түзүлдү." - "\"%s\" тез чакырмасы алынып салынды." - "\"%s\" тез чакырмасы бар." - "Тез чакырма тандоо" - "Колдонмо тандоо" "Колдонмолор" "Үйгө" - "Алып салуу" - "Чечип салуу" "Алып салуу" "Чечип салуу" "Колдонмо тууралуу" - "Колдонмолор" - "Алып салуу" - "Жаңыртууну чечип салуу" - "Колдонмону чечип салуу" - "Колдонмонун кеңири маалыматтары" - "1 колдонмо тандалды" - "1 виджет тандалды" - "1 фолдер тандалды" - "1 тез чакырма тандалды" "тез чакырмаларды орнотуу" "Колдонмого колдонуучуга кайрылбастан тез чакырма кошууга уруксат берет." - "тез чакырмаларды жок кылуу" - "Колдонмого колдонуучуга кайрылбастан тез чакырмаларды жок кылууга уруксат берет." "Үйдүн тууралоолорун жана тез чакырмаларын окуу" "Колдонмого Үйдүн тууралоолорун жана тез чакырмаларын окууга уруксат берет." "Үйдүн тууралоолорун жана тез чакырмаларын жазуу" @@ -78,30 +48,17 @@ "Виджетти жүктөөдө маселе бар" "Орнотуу" "Бул системдик колдонмо жана аны чечкенге болбойт." - "Rocket Launcher" "Аты жок фолдер" - "Үй экраны %1$d" "%2$d ичинен %1$d барак" "Үй экраны %2$d ичинен %1$d" - "Колдонмолор барагы %2$d ичинен %1$d" - "Виджеттер барагы %2$d ичинен %1$d" "Кош келиңиз" - "Өзүңүздү үйүңүздөгүдөй эркин сезиңиз." - - - "Колдонмолор жана фолдерлер үчүн кошумча экрандарды түзүңүз" "Колдонмоңуздун сүрөтчөлөрүн көчүрүү" "Эски үй экрандарыңыздан сүрөтчөлөр жана фолдерлер импорттолсунбу?" "СҮРӨТЧӨЛӨРДҮ КӨЧҮРҮҮ" "ТАЗАСЫН БАШТОО" - "Өз мейкиндигиңизди уюштуруңуз" - "Тушкагаздарды, виджеттерди жана тууралоолорду башкаруу үчүн фонду басып туруңуз." "Тушкагаздар, виджеттер & жөндөөлөр" "Өзгөчөлөштүрүү үчүн фонго тийип & коё бербей туруңуз" "ТҮШҮНДҮМ" - "Мынакей фолдер" - "Башкасын түзүш үчүн колдонмону басып туруп, башканын жанына жылдырыңыз." - "OK" "Фолдер ачылды, туурасы %1$d, бийиктиги %2$d" "Фолдерди жабыш үчүн тийиңиз" "Тийип, аттын өзгөртүлүшүн сактаңыз" @@ -111,14 +68,34 @@ "Виджеттер" "Тушкагаздар" "Тууралоолор" - "Күтүүдө" - "Жүктөлп алнууда" - "Орнотулууда" + "Айлантууга уруксат берүү" "Белгисиз" - "Калыбн келт. жок" - "Баарын алып салуу" "Алып салуу" "Издөө" "Бул колдонмо орнотулган эмес" "Бул сүрөтчөнүн колдонмосу орнотулган эмес. Аны алып салсаңыз же колдонмону издеп, кол менен орнотсоңуз болот." + "Башкы экранга кошуу" + "Бул нерсени бул жерге жылдыруу" + "Башкы экранга кошулду" + "Жоюлду" + "Муну жылдыруу" + "%1$s катарга %2$s тилкеге жылдыруу" + "%1$s орунга жылдыруу" + "Сүйүктүүлөргө %1$s жылдыруу" + "Нерсе жылдырылды" + "Куржунга кошуу: %1$s" + "%1$s куржунуна кошуу" + "Нерсе куржунга кошулду" + "Төмөнкү менен куржун түзүү: %1$s" + "Куржун түзүлдү" + "Башкы экранга жылдыруу" + "Экранды солго жылдыруу" + "Экранды оңго жылдыруу" + "Экран жылдырылды" + "Өлчөмүн өзгөртүү" + "Кеңейтүү" + "Бийиктетүү" + "Ичкертүү" + "Жапыздатуу" + "Виджеттин кеңдиги %1$s бийиктиги %2$s болду" diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml index 1b3418154f..06a99842ef 100644 --- a/res/values-land/dimens.xml +++ b/res/values-land/dimens.xml @@ -18,9 +18,4 @@ 8dip 0dip - - - 42dp - 3 - 2 diff --git a/res/values-land/styles.xml b/res/values-land/styles.xml index 8a255c91bc..c5a76d595b 100644 --- a/res/values-land/styles.xml +++ b/res/values-land/styles.xml @@ -28,7 +28,7 @@ - - - diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml index 5bc16275e0..f57f491ae6 100644 --- a/res/values-vi/strings.xml +++ b/res/values-vi/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "Màn hình chính" - "Ứng dụng lõi Android" + "Work" "Ứng dụng chưa được cài đặt." "Ứng dụng không có sẵn" "Ứng dụng đã tải xuống bị tắt ở chế độ An toàn" "Tiện ích con bị vô hiệu hóa ở chế độ an toàn" - "Tiện ích con" - "Tiện ích con" "Hiển thị bộ nhớ" "Chạm và giữ để chọn tiện ích con." "%1$d × %2$d" - "Không thể thả mục vào Màn hình chính này." - "Chọn tiện ích con để tạo" - "Tên thư mục" - "Đổi tên thư mục" - "OK" - "Hủy" - "Thêm vào Màn hình chính" - "Ứng dụng" - "Lối tắt" - "Tiện ích con" - "Không còn chỗ trên Màn hình chính của bạn." + "Tìm kiếm ứng dụng" + "Đang tải ứng dụng..." + "Không tìm thấy ứng dụng nào phù hợp với \"%1$s\"" "Không còn chỗ trên Màn hình chính này." "Không còn chỗ trong khay Mục yêu thích" - "Tiện ích con này có kích thước quá lớn để đặt vào khay Mục yêu thích" - "Lối tắt \"%s\" đã được tạo." - "Lối tắt \"%s\" đã bị xóa." - "Lối tắt \"%s\" đã tồn tại." - "Chọn lối tắt" - "Chọn ứng dụng" "Ứng dụng" "Màn hình chính" - "Xóa" - "Gỡ cài đặt" "Xóa" "Gỡ cài đặt" "Thông tin ứng dụng" - "Ứng dụng" - "Xóa" - "Gỡ cài đặt cập nhật" - "Gỡ cài đặt ứng dụng" - "Thông tin chi tiết về ứng dụng" - "Đã chọn 1 ứng dụng" - "Đã chọn 1 tiện ích con" - "Đã chọn 1 thư mục" - "Đã chọn 1 lối tắt" "cài đặt lối tắt" "Cho phép ứng dụng thêm lối tắt mà không cần sự can thiệp của người dùng." - "gỡ cài đặt lối tắt" - "Cho phép ứng dụng xóa lối tắt mà không cần sự can thiệp của người dùng." "đọc cài đặt và lối tắt trên Màn hình chính" "Cho phép ứng dụng đọc cài đặt và lối tắt trên Màn hình chính." "ghi cài đặt và lối tắt trên Màn hình chính" @@ -78,30 +48,17 @@ "Sự cố khi tải tiện ích con" "Thiết lập" "Đây là ứng dụng hệ thống và không thể gỡ cài đặt." - "Rocket Launcher" "Thư mục chưa đặt tên" - "Màn hình chính %1$d" "Trang %1$d / %2$d" "Màn hình chính %1$d / %2$d" - "Trang ứng dụng %1$d / %2$d" - "Trang tiện ích con %1$d / %2$d" "Chào mừng" - "Tự nhiên như ở nhà." - - - "Tạo thêm màn hình cho ứng dụng và thư mục" "Sao chép biểu tượng ứng dụng của bạn" "Nhập biểu tượng và thư mục từ Màn hình chính cũ của bạn?" "BIỂU TƯỢNG SAO CHÉP" "BẮT ĐẦU LÀM MỚI" - "Sắp xếp không gian của bạn" - "Chạm và giữ nền để quản lý hình nền, tiện ích con và cài đặt." "Hình nền, tiện ích và cài đặt" "Chạm và giữ nền để tùy chỉnh" "OK" - "Đây là một thư mục" - "Để tạo thư mục như thế này, hãy chạm và giữ một ứng dụng, sau đó di chuyển ứng dụng đó lên trên một ứng dụng khác." - "OK" "Đã mở thư mục, %1$d x %2$d" "Chạm để đóng thư mục" "Chạm để lưu tên mới" @@ -111,14 +68,34 @@ "Tiện ích con" "Hình nền" "Cài đặt" - "Đang đợi" - "Đang tải xuống" - "Đang cài đặt" + "Cho phép xoay" "Không xác định" - "Không được khôi phục" - "Xóa tất cả" "Xóa" "Tìm kiếm" "Ứng dụng này chưa được cài đặt" "Ứng dụng cho biểu tượng này chưa được cài đặt. Bạn có thể xóa ứng dụng hoặc tìm kiếm và cài đặt ứng dụng theo cách thủ công." + "Thêm vào màn hình chính" + "Di chuyển mục vào đây" + "Đã thêm mục vào màn hình chính" + "Đã xóa mục" + "Di chuyển mục" + "Di chuyển đến hàng %1$s cột %2$s" + "Di chuyển tới vị trí %1$s" + "Di chuyển tới vị trí mục yêu thích %1$s" + "Đã di chuyển mục" + "Thêm vào thư mục: %1$s" + "Thêm vào thư mục có %1$s" + "Đã thêm mục vào thư mục" + "Tạo thư mục bằng: %1$s" + "Đã tạo thư mục" + "Di chuyển đến màn hình chính" + "Di chuyển màn hình sang trái" + "Di chuyển màn hình sang phải" + "Đã di chuyển màn hình" + "Đổi kích thước" + "Tăng chiều rộng" + "Tăng chiều cao" + "Giảm chiều rộng" + "Giảm chiều cao" + "Đã đổi kích thước tiện ích thành chiều rộng %1$s chiều cao %2$s" diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index b7c42fe5c8..82274ba2c1 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "主屏" - "Android 核心应用" + "Work" "未安装该应用。" "应用不可用" "安全模式下不允许使用下载的此应用" "安全模式下不允许使用小部件" - "小部件" - "小部件" "显示内存空间" "触摸并按住小部件即可选择。" "%1$d × %2$d" - "无法将相关内容拖放到此主屏幕上。" - "选择要创建的小部件" - "文件夹名称" - "重命名文件夹" - "确定" - "取消" - "添加到主屏幕" - "应用" - "快捷方式" - "小部件" - "您的主屏幕上没有空间了。" + "搜索应用" + "正在加载应用…" + "未找到与“%1$s”相符的应用" "此主屏幕上已没有空间。" "收藏栏已满" - "该小部件太大,收藏栏中放不下" - "已创建“%s”快捷方式。" - "已删除“%s”快捷方式。" - "“%s”快捷方式已存在。" - "选择快捷方式" - "选择应用" "应用" "主屏幕" - "删除" - "卸载" "删除" "卸载" "应用信息" - "应用" - "删除" - "卸载更新内容" - "卸载应用" - "应用详情" - "已选择1个应用" - "已选择1个小部件" - "已选择1个文件夹" - "已选择1个快捷方式" "安装快捷方式" "允许应用自行添加快捷方式。" - "卸载快捷方式" - "允许应用自行删除快捷方式。" "读取主屏幕设置和快捷方式" "允许应用读取主屏幕中的设置和快捷方式。" "写入主屏幕设置和快捷方式" @@ -78,30 +48,17 @@ "加载小部件时出现问题" "设置" "这是系统应用,无法卸载。" - "火箭发射器" "未命名文件夹" - "主屏幕%1$d" "第%1$d页,共%2$d页" "主屏幕:第%1$d屏,共%2$d屏" - "应用:第%1$d页,共%2$d页" - "小部件:第%1$d页,共%2$d页" "欢迎使用" - "您的主屏幕您做主。" - - - "添加更多屏幕来容纳应用和文件夹" "复制应用图标" "要导入旧的主屏幕中的图标和文件夹吗?" "复制图标" "使用全新配置" - "整理您的空间" - "触摸并按住背景,即可管理壁纸、小部件和设置。" "壁纸、小部件和设置" "触摸并按住背景,即可进行个性化设置" "知道了" - "这是一个文件夹" - "要创建一个类似的文件夹,请触摸并按住某个应用,然后将其移至另一个应用上。" - "确定" "文件夹已打开,大小为%1$d×%2$d" "触摸可关闭文件夹" "触摸可保存新名称" @@ -111,14 +68,34 @@ "小部件" "壁纸" "设置" - "正在等待" - "正在下载" - "正在安装" + "允许旋转" "未知" - "无法还原" - "全部移除" "移除" "搜索" "未安装此应用" "未安装此图标对应的应用。您可以移除此图标,也可以尝试搜索相应的应用并手动安装。" + "添加到主屏幕" + "将项目移至此处" + "已将项目添加到主屏幕" + "项目已移除" + "移动项目" + "移至第 %1$s 行第 %2$s 列" + "移至第 %1$s 个位置" + "移至收藏夹第 %1$s 个位置" + "已移动项目" + "添加到“%1$s”文件夹" + "添加到“%1$s”所在文件夹" + "项目已添加到文件夹" + "创建“%1$s”文件夹" + "文件夹已创建" + "移至主屏幕" + "将屏幕向左移动" + "将屏幕向右移动" + "屏幕已移动" + "调整大小" + "增加宽度" + "增加高度" + "减小宽度" + "减小高度" + "小部件尺寸已调整为:宽度 %1$s,高度 %2$s" diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml index 2eb7008906..f7d240e053 100644 --- a/res/values-zh-rHK/strings.xml +++ b/res/values-zh-rHK/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "主畫面" - "Android 核心應用程式" + "工作" "尚未安裝應用程式。" "目前無法使用這個應用程式" "在安全模式中無法使用「已下載的應用程式」功能" "在安全模式中無法使用小工具" - "小工具" - "小工具" "顯示記憶體" "輕觸並按住小工具即可選取。" "%1$d × %2$d" - "無法將項目拖放至主畫面。" - "選擇要建立的小工具" - "資料夾名稱" - "重新命名資料夾" - "確定" - "取消" - "新增至主畫面" - "應用程式" - "捷徑" - "小工具" - "主畫面已無空間。" + "搜尋應用程式" + "正在載入應用程式…" + "無法找到與「%1$s」相符的應用程式" "主畫面已無空間。" "我的收藏寄存區沒有足夠空間" - "這個小工具過大,我的收藏寄存區沒有足夠空間" - "已建立「%s」捷徑。" - "已移除「%s」捷徑。" - "「%s」捷徑已存在。" - "選擇捷徑" - "選擇應用程式" "應用程式" "主畫面" - "移除" - "解除安裝" "移除" "解除安裝" "應用程式資料" - "應用程式" - "移除" - "解除安裝更新" - "解除安裝應用程式" - "應用程式詳細資料" - "已選取 1 個應用程式" - "已選取 1 個小工具" - "已選取 1 個資料夾" - "已選取 1 個捷徑" "安裝捷徑" "允許應用程式無需使用者許可也可新增捷徑。" - "解除安裝捷徑" - "允許應用程式無需使用者許可也可移除捷徑。" "讀取主畫面的設定和捷徑" "允許應用程式讀取主畫面中的設定和捷徑。" "寫入主畫面的設定和捷徑" @@ -78,30 +48,17 @@ "載入小工具時發生問題" "設定" "這是系統應用程式,無法將其解除安裝。" - "Rocket Launcher" "未命名的資料夾" - "主畫面 %1$d" "第 %1$d 頁,共 %2$d 頁" "主畫面 %1$d,共 %2$d 個" - "第 %1$d 個應用程式頁面,共 %2$d 頁" - "第 %1$d 個小工具頁面,共 %2$d 頁" "歡迎" - "自訂主畫面。" - - - "建立更多應用程式和資料夾的畫面" "複製您的應用程式圖示" "要從舊主畫面匯入圖示和資料夾嗎?" "複製圖示" "重新開始" - "管理您的空間" - "輕觸並按住背景,即可管理桌布、小工具和設定。" "桌布、小工具和設定" "輕觸並按住背景即可自訂" "知道了" - "資料夾顯示如下" - "如要建立類似的資料夾,請輕觸並按住某個應用程式,然後疊到另一個應用程式之上。" - "確定" "資料夾已開啟 (%1$d x %2$d)" "輕觸即可關閉資料夾" "輕觸即可儲存新改的名稱" @@ -111,14 +68,34 @@ "小工具" "桌布" "設定" - "等候中" - "下載中" - "安裝中" + "允許旋轉" "不明" - "無法還原" - "全部移除" "移除" "搜尋" "尚未安裝這個應用程式" "您尚未安裝這個圖示代表的應用程式。您可以移除這個圖示,也可以搜尋該應用程式並手動安裝。" + "新增至主畫面" + "移動項目至這裡" + "已將項目加入至主畫面" + "項目已移除" + "移動項目" + "移動至第 %1$s 行第 %2$s 列" + "移動至位置 %1$s" + "移動至喜愛的位置 %1$s" + "已移動項目" + "加入資料夾:%1$s" + "加入至資料夾:%1$s" + "項目已加入資料夾" + "使用以下項目建立資料夾:%1$s" + "已建立資料夾" + "移動至主畫面" + "向左移動螢幕" + "向右移動螢幕" + "已移動螢幕" + "重新調整大小" + "增加闊度" + "增加高度" + "減少闊度" + "減少高度" + "已調整小工具的大小至闊 %1$s%2$s" diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index 52e7583960..39a0ec77ae 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -20,57 +20,27 @@ "Launcher3" - "主螢幕" - "Android 核心應用程式" + "公司" "應用程式未安裝。" "應用程式目前無法使用" "在安全模式中無法使用「已下載的應用程式」功能" "在安全模式下無法使用小工具" - "小工具" - "小工具" "顯示記憶體" "輕觸並按住小工具即可選取。" "%1$d × %2$d" - "無法將項目拖放至這個主螢幕上。" - "選擇要建立的小工具" - "資料夾名稱" - "重新命名資料夾" - "確定" - "取消" - "新增至主螢幕" - "應用程式" - "捷徑" - "小工具" - "主螢幕已無空間。" + "搜尋應用程式" + "正在載入應用程式…" + "找不到符合「%1$s」的應用程式" "這個主螢幕已無空間。" "「我的最愛」匣已無可用空間" - "這個小工具過大,「我的最愛」匣無法容納" - "已建立「%s」捷徑。" - "已移除「%s」捷徑。" - "「%s」捷徑已存在。" - "選擇捷徑" - "選擇應用程式" "應用程式" "主螢幕" - "移除" - "解除安裝" "移除" "解除安裝" "應用程式資訊" - "應用程式" - "移除" - "解除安裝更新" - "解除安裝應用程式" - "應用程式詳細資料" - "已選取 1 個應用程式" - "已選取 1 個小工具" - "已選取 1 個資料夾" - "已選取 1 個捷徑" "安裝捷徑" "允許應用程式自動新增捷徑。" - "解除安裝捷徑" - "允許應用程式自動移除捷徑。" "讀取主螢幕的設定和捷徑" "允許應用程式讀取主螢幕中的設定和捷徑。" "寫入主螢幕設定和捷徑" @@ -78,30 +48,17 @@ "載入小工具時發生問題" "設定" "這是系統應用程式,不可解除安裝。" - "Rocket Launcher" "未命名的資料夾" - "主螢幕 %1$d" "第 %1$d 頁,共 %2$d 頁" "主螢幕:第 %1$d 頁,共 %2$d 頁" - "應用程式:第 %1$d 頁,共 %2$d 頁" - "小工具:第 %1$d 頁,共 %2$d 頁" "歡迎使用" - "主螢幕由您作主。" - - - "建立更多畫面容納應用程式和資料夾" "複製您的應用程式圖示" "要從舊的主螢幕匯入圖示和資料夾嗎?" "複製圖示" "重新開始" - "管理您的空間" - "輕觸並按住背景,即可管理桌布、小工具和設定。" "桌布、小工具和設定" "輕觸並按住背景即可自訂" "知道了" - "資料夾顯示如下" - "如要建立類似的資料夾,請輕觸並按住應用程式,然後將應用程式疊放在另一個應用程式上。" - "確定" "資料夾已開啟 (%1$d x %2$d)" "輕觸即可關閉資料夾" "輕觸即可儲存新名稱" @@ -111,14 +68,34 @@ "小工具" "桌布" "設定" - "等待中" - "下載中…" - "安裝中" + "允許輪播" "不明" - "無法還原" - "全部移除" "移除" "搜尋" "尚未安裝這個應用程式" "您尚未安裝這個圖示代表的應用程式。您可以移除這個圖示,也可以搜尋該應用程式並手動安裝。" + "新增至主畫面" + "將項目移至這裡" + "已將項目新增到主畫面" + "已移除項目" + "移動項目" + "移至第 %1$s 列第 %2$s 欄" + "移至位置 %1$s" + "移至收藏位置 %1$s" + "已移動項目" + "新增到「%1$s」資料夾" + "新增到「%1$s」所在資料夾" + "已將項目新增到資料夾" + "建立「%1$s」資料夾" + "已建立資料夾" + "移至主畫面" + "將畫面往左移" + "將畫面往右移" + "已移動畫面" + "調整大小" + "增加寬度" + "增加高度" + "減少寬度" + "減少高度" + "已將小工具的寬度和高度分別調整為 %1$s%2$s" diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml index aea09e8454..83ba089df5 100644 --- a/res/values-zu/strings.xml +++ b/res/values-zu/strings.xml @@ -20,57 +20,27 @@ "Isiqalisi3" - "Ikhaya" - "Izinhlelo zokusebenza ze-Android Core" + "Umsebenzi" "Uhlelo lokusebenza alufakiwe." "Uhlelo lokusebenza alutholakali" "Uhlelo lokusebenza olulandiwe lukhutshaziwe kumodi ephephile" "Amawijethi akhutshaziwe kwimodi yokuphepha" - "Amawijethi" - "Amawijethi" "Bonisa i-Mem" "Thinta uphinde ubambe ukuze uphakamise iwijethi." "%1$d × %2$d" - "Ayikwazanga ukwehlisela into kulesi sikrini se-Ikhaya." - "Khetha iwijethi ongayidala" - "Igama lefolda" - "Qamba kabusha ifolda" - "KULUNGILE" - "Khansela" - "Faka kwisikrini saseKhaya" - "Izinhlelo zokusebenza" - "Izinqamuleli" - "Amawijethi" - "Akusenagumbi ezikrinini zakho Zekhaya." + "Sesha Izinhlelo Zokusebenza" + "Ilayisha izinhlelo zokusebenza..." + "Azikho izinhlelo zokusebenza ezitholakele ezifana ne-\"%1$s\"" "Asisekho isikhala kulesi sikrini Sasekhaya." "Asisekho isikhala kwitreyi lezintandokazi" - "Le wijethi inkulu kakhulu ukuba kwitreyi lezintandokazi." - "Isinqamuleli esithi \"%s\" sidaliwe." - "Isinqamuleli esithi \"%s\" sisusiwe." - "Isinqamuleli esithi \"%s\" sesivele sikhona." - "Khetha isinqamulelo" - "Khetha uhlelo lokusebenza" "Izinhlelo zokusebenza" "Ikhaya" - "Susa" - "Khipha" "Susa" "Khipha" "Ulwazi lohlelo lokusebenza" - "Izinhlelo zokusebenza" - "Susa" - "Khipha isibuyekezo" - "Khipha uhlelo lokusebenza" - "Imininingwane yohlelo lokusebenza" - "1 uhlelo lokusebenza olukhethiwe" - "1 iwijethi ekhethiwe" - "1 ifolda ekhethiwe" - "1 isinqamuleli esikhethiwe" "faka izinqamuleli" "Ivumela uhlelo lokusebenza ukufaka izinqamuleli ngaphandle kokungenelela komsebenzisi." - "khipha izinqamuleli" - "Ivumela uhlelo lokusebenza ukuthi lisuse izinqamuleli ngaphandle kokungenelela komsebenzisi." "funda izilungiselelo zokuthi Ikhaya nezinqamuleli" "Ivumela uhlelo lokusebenza ukuthi lifunde izilungiselelo nezinqamuleli ekhaya." "bhala izilungiselelo zokuthi Ikhaya nezinqamuleli" @@ -78,30 +48,17 @@ "Inkinga yokulayisha iwijethi" "Ukumisa" "Lolu uhlelo lokusebenza lwesistimu futhi alikwazi ukukhishwa." - "Isiqalisi se-Rocket" "Ifolda engenagama" - "Isikrini sasekhaya esingu-%1$d" "Ikhasi elingu-%1$d kwangu-%2$d" "Isikrini sasekhaya esingu-%1$d se-%2$d" - "Ikhasi lezinhlelo zokusebenza elingu-%1$d le-%2$d" - "Ikhasi lamawijethi elingu-%1$d le-%2$d" "Siyakwamukela" - "Zizwe usekhaya." - - - "Dala izikrini eziningi zezinhlelo zokusebenza namafolda" "Kopisha izithonjana zakho zohlelo lokusebenza" "Ngenisa izithonjana namafolda kusukela kuzikrini zakho ezindala zasekhaya?" "KOPISHA IZITHONJANA" "QALISA KABUSHA" - "Hlela isikhala sakho" - "Thinta uphinde ubambe okungemuva ukuze uphathe isithombe sangemuva, amawijethi nezilungiselelo." "Izithombe zangemuva, amawijethi, nezilungiselelo" "Thinta uphinde ubambe ingemuva ukuze wenze ngokwezifiso" "NGIYITHOLILE" - "Nayi ifolda" - "Ukuze udale eyodwa efana nale, thinta uphinde ubambe uhlelo lokusebenza, bese ulidlulisa ngaphezulu kwelinye." - "KULUNGILE" "Ifolda ivuliwe, %1$d nge-%2$d" "Thinta ukuze uvale ifolda" "Thinta ukuze ulondoloze ukuqamba kabusha" @@ -111,14 +68,34 @@ "Amawijethi" "Izithombe zangemuva" "Izilungiselelo" - "Ilindile" - "Iyalanda" - "Iyafaka" + "Vumela ukuphenduka" "Akwaziwa" - "Ayibuyiselwe" - "Susa konke" "Susa" "Sesha" "Lolu hlelo lokusebenza alifakiwe" "Uhlelo lokusebenza lalesi sithonjana alufakiwe. Ungalisusa, noma sesha uhlelo lokusebenza bese uzifakela lona ngokuzenzela." + "Faka kusikrini sasekhaya" + "Hambisa into lapha" + "Into ingezwe kusikrini sasekhaya" + "Into isusiwe" + "Hambisa into" + "Hambisa kurowu engu-%1$s ikholomu engu-%2$s" + "Hambisa kusimo esingu-%1$s" + "Hambisa kusimo sezintandokazi esingu-%1$s" + "Into ihanjisiwe" + "Engeza kufolda: %1$s" + "Engeza kufolda nge-%1$s" + "Into ingeziwe kufolda" + "Dala ifolda nge-: %1$s" + "Ifolda idaliwe" + "Hambisa kusikrini sasekhaya" + "Hambisa isikrini kwesokunxele" + "Hambisa isikrini kwesokudla" + "Isikrini sihanjisiwe" + "Shintsha usayizi" + "Khuphula ububanzi" + "Khuphula ubude" + "Nciphisa ububanzi" + "Nciphisa ubude" + "Iwijethi inikezwe usayizi omusha ngobubanzi obungu-%1$s ubude obungu-%2$s" diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 3331cdec4d..827332ad7b 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -18,6 +18,19 @@ + + + + + + + + + + + + + @@ -59,16 +72,6 @@ - - - - - - - - - - @@ -80,28 +83,6 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/res/values/colors.xml b/res/values/colors.xml index 2daf9fe123..51e4d40a5b 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -20,20 +20,34 @@ - #DAFF0000 - #DA0099CC + #FFC1C1C1 + #FFF0592B + #FF009688 #80000000 - #20000000 #80c6c5c5 - #FCCC - #FFF - #FF666666 - #FFC4C4C4 - #FFFFFFFF - #FF374248 + #FFFFFFFF + #FF757575 + #FF666666 + #FFF5F5F5 + #FF374248 + + #FFFFFFFF + + + #42000000 + #009688 + + + #009688 + + + #42FFFFFF + #FFFFFF + #C4C4C4 + #263238 diff --git a/res/values/config.xml b/res/values/config.xml index 96bd13bbb2..73de794179 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -1,8 +1,5 @@ - 6 - 5 - 3 20 @@ -23,28 +20,22 @@ -1500 + - + 55 - 300 + 100 250 80 - - 220 - 350 - 600 - 7 - 250 - 200 - 300 - - 250 - 60 + + 220 + 300 + 60 @@ -52,20 +43,7 @@ 0 - - 250 - - - 150 - - - false - - - 0 - 900 @@ -86,13 +64,6 @@ 800 - - 80 - 250 - - - 8000 - true @@ -106,4 +77,15 @@ + + + + + + + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index f12cb57ba9..589d696666 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -15,19 +15,25 @@ --> + 64dp + 6dp 500dp 56dp 20dp 4dp - 18dp 8dp 80dp 120dp 80dp 20dp + + 8dp + 13dp + 24dp + 240dp 165dp @@ -38,60 +44,89 @@ 16dp - 0dp 4dip 12dip - - - 52dp - 0dp - 48dp - 0dp + + + 8dp + + 4dp - - 12dp - 4dp - 16dp + 4dp + 8dp + 64dp + -24dp + 72dp + 48dp - - 14dp - 14sp + + 0dp + 8dp + 24sp + 48dp + 8dp + 8dp + 24dp + + 8dp + 18dp + 8dp - - - 20dp + + 8dp + 8dp + 8dp + 8dp - - 0dp - 0dp - - 12dp + 56dp + 40dp + 8dp + 16dp - - 16dp - 16dp - 32dp - 8dp - 8dp + 8dp + 2dp 0dp 0dp 0dp + + + 14dp + 14sp + + + 20dp + + + 12dp + + + 30dp + + + 4dp + 4dp - 10dp 24dp - 4dp + 5dp + 2dp + + + 1dp + 2dp + 4dp + 2dp + diff --git a/res/values/strings.xml b/res/values/strings.xml index b6f42379df..88f149bd0e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -32,12 +32,10 @@ Launcher3 - - Home - - Android Core Apps + + Work App isn\'t installed. @@ -46,84 +44,36 @@ Downloaded app disabled in Safe mode Widgets disabled in Safe mode - - Widgets - - Widgets Show Mem - + Touch & hold to pick up a widget. %1$d \u00d7 %2$d - - %1$s (%2$d \u00d7 %3$d) - - Couldn\'t drop item on this Home screen. - - Choose widget to create + + + Search Apps + + Loading Apps… + + No Apps found matching \"%1$s\" - + - - Folder name - - Rename folder - - OK - - Cancel - - - - - Add to Home screen - - Apps - - Shortcuts - - Widgets - - No more room on your Home screens. No more room on this Home screen. No more room in the Favorites tray - - This widget is too large for the Favorites tray - - Shortcut \"%s\" created. - - Shortcut \"%s\" was removed. - - Shortcut \"%s\" already exists. - - - Choose shortcut - - Choose app Apps Home - - Remove - - Uninstall Remove @@ -132,35 +82,6 @@ s --> App info - - Apps - - Remove - - - Uninstall update - - - - - - - - Uninstall app - - App details - - 1 app selected - - 1 widget selected - - 1 folder selected - - 1 shortcut selected - @@ -169,11 +90,6 @@ s --> Allows an app to add shortcuts without user intervention. - uninstall shortcuts - - Allows the app to remove - shortcuts without user intervention. - read Home settings and shortcuts Allows the app to read the settings and @@ -196,38 +112,20 @@ s --> This is a system app and can\'t be uninstalled. - - Rocket Launcher - Unnamed Folder - - Home screen %1$d - Page %1$d of %2$d Home screen %1$d of %2$d - - Apps page %1$d of %2$d - - Widgets page %1$d of %2$d Welcome - - Make yourself at home. - - - - - - Create more screens for apps and folders Copy your app icons @@ -236,25 +134,12 @@ s --> COPY ICONS START FRESH - - Organize your space - - Touch & hold background to manage wallpaper, widgets and settings. Wallpapers, widgets, & settings Touch & hold background to customize GOT IT - - Here\'s a folder - - To create one like this, touch & hold an app, then move it over another. - - OK - - Error: custom workspace layout passed in but custom cling was not overwritten - @@ -281,19 +166,13 @@ s --> Settings - - Waiting - - Downloading - - Installing + + + Allow rotation + Unknown - - Not restored - - Remove All Remove @@ -305,4 +184,78 @@ s --> The app for this icon isn\'t installed. You can remove it, or search for the app and install it manually. + + + + Add to Home screen + + + Move item here + + + Item added to home screen + + + Item removed + + + Move item + + + Move to row %1$s column %2$s + + + Move to position %1$s + + + Move to favorites position %1$s + + + Item moved + + + Add to folder: %1$s + + + Add to folder with %1$s + + + Item added to folder + + + Create folder with: %1$s + + + Folder created + + + Move to Home screen + + + Move screen to left + + + Move screen to right + + + Screen moved + + + Resize + + + Increase width + + + Increase height + + + Decrease width + + + Decrease height + + + Widget resized to width %1$s height %2$s + diff --git a/res/values/styles.xml b/res/values/styles.xml index 77798f1749..7d60cbe0ac 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -18,8 +18,15 @@ --> + - + + - - - - - - - - + - - - diff --git a/res/xml/app_target_browser.xml b/res/xml/app_target_browser.xml new file mode 100644 index 0000000000..d7c3ed5fd5 --- /dev/null +++ b/res/xml/app_target_browser.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/anim/fade_out_fast.xml b/res/xml/app_target_camera.xml similarity index 64% rename from res/anim/fade_out_fast.xml rename to res/xml/app_target_camera.xml index a061a6ca9a..f65a2b1689 100644 --- a/res/anim/fade_out_fast.xml +++ b/res/xml/app_target_camera.xml @@ -1,5 +1,6 @@ - - - android:fromAlpha="1.0" - android:toAlpha="0.0" + + - android:duration="@android:integer/config_mediumAnimTime" /> + \ No newline at end of file diff --git a/res/anim/fade_in_fast.xml b/res/xml/app_target_email.xml similarity index 65% rename from res/anim/fade_in_fast.xml rename to res/xml/app_target_email.xml index 4fa9847aa8..44f0a407d2 100644 --- a/res/anim/fade_in_fast.xml +++ b/res/xml/app_target_email.xml @@ -1,5 +1,6 @@ - - - android:fromAlpha="0.0" - android:toAlpha="1.0" + + - android:duration="@android:integer/config_mediumAnimTime" /> + \ No newline at end of file diff --git a/res/xml/app_target_gallery.xml b/res/xml/app_target_gallery.xml new file mode 100644 index 0000000000..c9d34924af --- /dev/null +++ b/res/xml/app_target_gallery.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/xml/app_target_messenger.xml b/res/xml/app_target_messenger.xml new file mode 100644 index 0000000000..278eb5ca82 --- /dev/null +++ b/res/xml/app_target_messenger.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/app_target_phone.xml b/res/xml/app_target_phone.xml new file mode 100644 index 0000000000..5d6ca316e7 --- /dev/null +++ b/res/xml/app_target_phone.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml new file mode 100644 index 0000000000..624d9eb2c3 --- /dev/null +++ b/res/xml/launcher_preferences.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/res/xml/update_workspace.xml b/res/xml/update_workspace.xml deleted file mode 100644 index 38442b91cd..0000000000 --- a/res/xml/update_workspace.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java index 72c6693b31..3b25dca34c 100644 --- a/src/com/android/launcher3/AllAppsList.java +++ b/src/com/android/launcher3/AllAppsList.java @@ -24,6 +24,7 @@ import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; @@ -98,14 +99,14 @@ class AllAppsList { user); for (LauncherActivityInfoCompat info : matches) { - add(new AppInfo(context, info, user, mIconCache, null)); + add(new AppInfo(context, info, user, mIconCache)); } } /** * Remove the apps for the given apk identified by packageName. */ - public void removePackage(String packageName, UserHandleCompat user, boolean clearCache) { + public void removePackage(String packageName, UserHandleCompat user) { final List data = this.data; for (int i = data.size() - 1; i >= 0; i--) { AppInfo info = data.get(i); @@ -115,8 +116,15 @@ class AllAppsList { data.remove(i); } } - if (clearCache) { - mIconCache.remove(packageName, user); + } + + public void updateIconsAndLabels(HashSet packages, UserHandleCompat user, + ArrayList outUpdates) { + for (AppInfo info : data) { + if (info.user.equals(user) && packages.contains(info.componentName.getPackageName())) { + mIconCache.updateTitleAndIcon(info); + outUpdates.add(info); + } } } @@ -137,7 +145,6 @@ class AllAppsList { && packageName.equals(component.getPackageName())) { if (!findActivity(matches, component)) { removed.add(applicationInfo); - mIconCache.remove(component, user); data.remove(i); } } @@ -150,10 +157,9 @@ class AllAppsList { info.getComponentName().getPackageName(), user, info.getComponentName().getClassName()); if (applicationInfo == null) { - add(new AppInfo(context, info, user, mIconCache, null)); + add(new AppInfo(context, info, user, mIconCache)); } else { - mIconCache.remove(applicationInfo.componentName, user); - mIconCache.getTitleAndIcon(applicationInfo, info, null); + mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */); modified.add(applicationInfo); } } diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index a66bac08ab..c95d5585a7 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -19,19 +19,16 @@ package com.android.launcher3; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.util.Log; import com.android.launcher3.compat.LauncherActivityInfoCompat; -import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.ComponentKey; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; /** * Represents an app in AllAppsView. @@ -42,19 +39,24 @@ public class AppInfo extends ItemInfo { /** * The intent used to start the application. */ - Intent intent; + public Intent intent; /** * A bitmap version of the application icon. */ - Bitmap iconBitmap; + public Bitmap iconBitmap; + + /** + * Indicates whether we're using a low res icon + */ + boolean usingLowResIcon; /** * The time at which the app was first installed. */ long firstInstallTime; - ComponentName componentName; + public ComponentName componentName; static final int DOWNLOADED_FLAG = 1; static final int UPDATED_SYSTEM_APP_FLAG = 2; @@ -77,13 +79,13 @@ public class AppInfo extends ItemInfo { * Must not hold the Context. */ public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user, - IconCache iconCache, HashMap labelCache) { + IconCache iconCache) { this.componentName = info.getComponentName(); this.container = ItemInfo.NO_ID; flags = initFlags(info); firstInstallTime = info.getFirstInstallTime(); - iconCache.getTitleAndIcon(this, info, labelCache); + iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */); intent = makeLaunchIntent(context, info, user); this.user = user; } @@ -104,7 +106,7 @@ public class AppInfo extends ItemInfo { public AppInfo(AppInfo info) { super(info); componentName = info.componentName; - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); flags = info.flags; firstInstallTime = info.firstInstallTime; @@ -113,19 +115,22 @@ public class AppInfo extends ItemInfo { @Override public String toString() { - return "ApplicationInfo(title=" + title.toString() + " id=" + this.id + return "ApplicationInfo(title=" + title + " id=" + this.id + " type=" + this.itemType + " container=" + this.container + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + " user=" + user + ")"; } + /** + * Helper method used for debugging. + */ public static void dumpApplicationInfoList(String tag, String label, ArrayList list) { Log.d(tag, label + " size=" + list.size()); for (AppInfo info: list) { - Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" - + info.iconBitmap + " firstInstallTime=" - + info.firstInstallTime); + Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap + + " firstInstallTime=" + info.firstInstallTime + + " componentName=" + info.componentName.getPackageName()); } } @@ -133,6 +138,10 @@ public class AppInfo extends ItemInfo { return new ShortcutInfo(this); } + public ComponentKey toComponentKey() { + return new ComponentKey(componentName, user); + } + public static Intent makeLaunchIntent(Context context, LauncherActivityInfoCompat info, UserHandleCompat user) { long serialNumber = UserManagerCompat.getInstance(context).getSerialNumberForUser(user); diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index f57f4d0361..ea7c22189e 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -8,30 +8,43 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; +import android.content.res.Resources; import android.graphics.Rect; import android.view.Gravity; import android.widget.FrameLayout; import android.widget.ImageView; public class AppWidgetResizeFrame extends FrameLayout { - private LauncherAppWidgetHostView mWidgetView; - private CellLayout mCellLayout; - private DragLayer mDragLayer; - private ImageView mLeftHandle; - private ImageView mRightHandle; - private ImageView mTopHandle; - private ImageView mBottomHandle; + private static final int SNAP_DURATION = 150; + private static final float DIMMED_HANDLE_ALPHA = 0f; + private static final float RESIZE_THRESHOLD = 0.66f; + + private static Rect sTmpRect = new Rect(); + + private final Launcher mLauncher; + private final LauncherAppWidgetHostView mWidgetView; + private final CellLayout mCellLayout; + private final DragLayer mDragLayer; + + private final ImageView mLeftHandle; + private final ImageView mRightHandle; + private final ImageView mTopHandle; + private final ImageView mBottomHandle; + + private final Rect mWidgetPadding; + + private final int mBackgroundPadding; + private final int mTouchTargetWidth; + + private final int[] mDirectionVector = new int[2]; + private final int[] mLastDirectionVector = new int[2]; + private final int[] mTmpPt = new int[2]; private boolean mLeftBorderActive; private boolean mRightBorderActive; private boolean mTopBorderActive; private boolean mBottomBorderActive; - private int mWidgetPaddingLeft; - private int mWidgetPaddingRight; - private int mWidgetPaddingTop; - private int mWidgetPaddingBottom; - private int mBaselineWidth; private int mBaselineHeight; private int mBaselineX; @@ -47,30 +60,9 @@ public class AppWidgetResizeFrame extends FrameLayout { private int mDeltaXAddOn; private int mDeltaYAddOn; - private int mBackgroundPadding; - private int mTouchTargetWidth; - private int mTopTouchRegionAdjustment = 0; private int mBottomTouchRegionAdjustment = 0; - int[] mDirectionVector = new int[2]; - int[] mLastDirectionVector = new int[2]; - int[] mTmpPt = new int[2]; - - final int SNAP_DURATION = 150; - final int BACKGROUND_PADDING = 24; - final float DIMMED_HANDLE_ALPHA = 0f; - final float RESIZE_THRESHOLD = 0.66f; - - private static Rect mTmpRect = new Rect(); - - public static final int LEFT = 0; - public static final int TOP = 1; - public static final int RIGHT = 2; - public static final int BOTTOM = 3; - - private Launcher mLauncher; - public AppWidgetResizeFrame(Context context, LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer) { @@ -78,48 +70,56 @@ public class AppWidgetResizeFrame extends FrameLayout { mLauncher = (Launcher) context; mCellLayout = cellLayout; mWidgetView = widgetView; - mResizeMode = widgetView.getAppWidgetInfo().resizeMode; + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) + widgetView.getAppWidgetInfo(); + mResizeMode = info.resizeMode; mDragLayer = dragLayer; - final AppWidgetProviderInfo info = widgetView.getAppWidgetInfo(); - int[] result = Launcher.getMinSpanForWidget(mLauncher, info); - mMinHSpan = result[0]; - mMinVSpan = result[1]; + mMinHSpan = info.getMinSpanX(mLauncher); + mMinVSpan = info.getMinSpanY(mLauncher); - setBackgroundResource(R.drawable.widget_resize_frame_holo); + setBackgroundResource(R.drawable.widget_resize_shadow); + setForeground(getResources().getDrawable(R.drawable.widget_resize_frame)); setPadding(0, 0, 0, 0); + final int handleMargin = getResources().getDimensionPixelSize(R.dimen.widget_handle_margin); LayoutParams lp; mLeftHandle = new ImageView(context); - mLeftHandle.setImageResource(R.drawable.widget_resize_handle_left); - lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + mLeftHandle.setImageResource(R.drawable.ic_widget_resize_handle); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL); + lp.leftMargin = handleMargin; addView(mLeftHandle, lp); mRightHandle = new ImageView(context); - mRightHandle.setImageResource(R.drawable.widget_resize_handle_right); - lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + mRightHandle.setImageResource(R.drawable.ic_widget_resize_handle); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.CENTER_VERTICAL); + lp.rightMargin = handleMargin; addView(mRightHandle, lp); mTopHandle = new ImageView(context); - mTopHandle.setImageResource(R.drawable.widget_resize_handle_top); - lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + mTopHandle.setImageResource(R.drawable.ic_widget_resize_handle); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.TOP); + lp.topMargin = handleMargin; addView(mTopHandle, lp); mBottomHandle = new ImageView(context); - mBottomHandle.setImageResource(R.drawable.widget_resize_handle_bottom); - lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + mBottomHandle.setImageResource(R.drawable.ic_widget_resize_handle); + lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); + lp.bottomMargin = handleMargin; addView(mBottomHandle, lp); - Rect p = AppWidgetHostView.getDefaultPaddingForWidget(context, - widgetView.getAppWidgetInfo().provider, null); - mWidgetPaddingLeft = p.left; - mWidgetPaddingTop = p.top; - mWidgetPaddingRight = p.right; - mWidgetPaddingBottom = p.bottom; + if (!info.isCustomWidget) { + mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, + widgetView.getAppWidgetInfo().provider, null); + } else { + Resources r = context.getResources(); + int padding = r.getDimensionPixelSize(R.dimen.default_widget_padding); + mWidgetPadding = new Rect(padding, padding, padding, padding); + } if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { mTopHandle.setVisibility(GONE); @@ -129,8 +129,8 @@ public class AppWidgetResizeFrame extends FrameLayout { mRightHandle.setVisibility(GONE); } - final float density = mLauncher.getResources().getDisplayMetrics().density; - mBackgroundPadding = (int) Math.ceil(density * BACKGROUND_PADDING); + mBackgroundPadding = getResources() + .getDimensionPixelSize(R.dimen.resize_frame_background_padding); mTouchTargetWidth = 2 * mBackgroundPadding; // When we create the resize frame, we first mark all cells as unoccupied. The appropriate @@ -335,13 +335,12 @@ public class AppWidgetResizeFrame extends FrameLayout { static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher, int spanX, int spanY) { - - getWidgetSizeRanges(launcher, spanX, spanY, mTmpRect); - widgetView.updateAppWidgetSize(null, mTmpRect.left, mTmpRect.top, - mTmpRect.right, mTmpRect.bottom); + getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect); + widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top, + sTmpRect.right, sTmpRect.bottom); } - static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) { + public static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) { if (rect == null) { rect = new Rect(); } @@ -396,19 +395,19 @@ public class AppWidgetResizeFrame extends FrameLayout { public void snapToWidget(boolean animate) { final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding - mWidgetPaddingLeft - - mWidgetPaddingRight; - int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding - mWidgetPaddingTop - - mWidgetPaddingBottom; + int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding + - mWidgetPadding.left - mWidgetPadding.right; + int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding + - mWidgetPadding.top - mWidgetPadding.bottom; mTmpPt[0] = mWidgetView.getLeft(); mTmpPt[1] = mWidgetView.getTop(); mDragLayer.getDescendantCoordRelativeToSelf(mCellLayout.getShortcutsAndWidgets(), mTmpPt); - int newX = mTmpPt[0] - mBackgroundPadding + mWidgetPaddingLeft; - int newY = mTmpPt[1] - mBackgroundPadding + mWidgetPaddingTop; + int newX = mTmpPt[0] - mBackgroundPadding + mWidgetPadding.left; + int newY = mTmpPt[1] - mBackgroundPadding + mWidgetPadding.top; - // We need to make sure the frame's touchable regions lie fully within the bounds of the + // We need to make sure the frame's touchable regions lie fully within the bounds of the // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions // down accordingly to provide a proper touch target. if (newY < 0) { diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java index 880aaf1ecf..5e7a012d21 100644 --- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -90,5 +90,10 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } + + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app != null) { + app.reloadWorkspace(); + } } } diff --git a/src/com/android/launcher3/AppsCustomizeCellLayout.java b/src/com/android/launcher3/AppsCustomizeCellLayout.java deleted file mode 100644 index a50fb68214..0000000000 --- a/src/com/android/launcher3/AppsCustomizeCellLayout.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.view.View; - -public class AppsCustomizeCellLayout extends CellLayout implements Page { - - final FocusIndicatorView mFocusHandlerView; - - public AppsCustomizeCellLayout(Context context) { - super(context); - - mFocusHandlerView = new FocusIndicatorView(context); - addView(mFocusHandlerView, 0); - mFocusHandlerView.getLayoutParams().width = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; - mFocusHandlerView.getLayoutParams().height = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; - } - - @Override - public void removeAllViewsOnPage() { - removeAllViews(); - setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void removeViewOnPageAt(int index) { - removeViewAt(index); - } - - @Override - public int getPageChildCount() { - return getChildCount(); - } - - @Override - public View getChildOnPageAt(int i) { - return getChildAt(i); - } - - @Override - public int indexOfChildOnPage(View v) { - return indexOfChild(v); - } - - /** - * Clears all the key listeners for the individual icons. - */ - public void resetChildrenOnKeyListeners() { - ShortcutAndWidgetContainer children = getShortcutsAndWidgets(); - int childCount = children.getChildCount(); - for (int j = 0; j < childCount; ++j) { - children.getChildAt(j).setOnKeyListener(null); - } - } -} diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java deleted file mode 100644 index c8187f0684..0000000000 --- a/src/com/android/launcher3/AppsCustomizePagedView.java +++ /dev/null @@ -1,1567 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; -import android.appwidget.AppWidgetHostView; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Process; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; -import android.widget.GridLayout; -import android.widget.ImageView; -import android.widget.Toast; - -import com.android.launcher3.DropTarget.DragObject; -import com.android.launcher3.compat.AppWidgetManagerCompat; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -/** - * A simple callback interface which also provides the results of the task. - */ -interface AsyncTaskCallback { - void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data); -} - -/** - * The data needed to perform either of the custom AsyncTasks. - */ -class AsyncTaskPageData { - enum Type { - LoadWidgetPreviewData - } - - AsyncTaskPageData(int p, ArrayList l, int cw, int ch, AsyncTaskCallback bgR, - AsyncTaskCallback postR, WidgetPreviewLoader w) { - page = p; - items = l; - generatedImages = new ArrayList(); - maxImageWidth = cw; - maxImageHeight = ch; - doInBackgroundCallback = bgR; - postExecuteCallback = postR; - widgetPreviewLoader = w; - } - void cleanup(boolean cancelled) { - // Clean up any references to source/generated bitmaps - if (generatedImages != null) { - if (cancelled) { - for (int i = 0; i < generatedImages.size(); i++) { - widgetPreviewLoader.recycleBitmap(items.get(i), generatedImages.get(i)); - } - } - generatedImages.clear(); - } - } - int page; - ArrayList items; - ArrayList sourceImages; - ArrayList generatedImages; - int maxImageWidth; - int maxImageHeight; - AsyncTaskCallback doInBackgroundCallback; - AsyncTaskCallback postExecuteCallback; - WidgetPreviewLoader widgetPreviewLoader; -} - -/** - * A generic template for an async task used in AppsCustomize. - */ -class AppsCustomizeAsyncTask extends AsyncTask { - AppsCustomizeAsyncTask(int p, AsyncTaskPageData.Type ty) { - page = p; - threadPriority = Process.THREAD_PRIORITY_DEFAULT; - dataType = ty; - } - @Override - protected AsyncTaskPageData doInBackground(AsyncTaskPageData... params) { - if (params.length != 1) return null; - // Load each of the widget previews in the background - params[0].doInBackgroundCallback.run(this, params[0]); - return params[0]; - } - @Override - protected void onPostExecute(AsyncTaskPageData result) { - // All the widget previews are loaded, so we can just callback to inflate the page - result.postExecuteCallback.run(this, result); - } - - void setThreadPriority(int p) { - threadPriority = p; - } - void syncThreadPriority() { - Process.setThreadPriority(threadPriority); - } - - // The page that this async task is associated with - AsyncTaskPageData.Type dataType; - int page; - int threadPriority; -} - -/** - * The Apps/Customize page that displays all the applications, widgets, and shortcuts. - */ -public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements - View.OnClickListener, View.OnKeyListener, DragSource, - PagedViewWidget.ShortPressListener, LauncherTransitionable { - static final String TAG = "AppsCustomizePagedView"; - - private static Rect sTmpRect = new Rect(); - - /** - * The different content types that this paged view can show. - */ - public enum ContentType { - Applications, - Widgets - } - private ContentType mContentType = ContentType.Applications; - - // Refs - private Launcher mLauncher; - private DragController mDragController; - private final LayoutInflater mLayoutInflater; - private final PackageManager mPackageManager; - - // Save and Restore - private int mSaveInstanceStateItemIndex = -1; - - // Content - private ArrayList mApps; - private ArrayList mWidgets; - - // Caching - private IconCache mIconCache; - - // Dimens - private int mContentWidth, mContentHeight; - private int mWidgetCountX, mWidgetCountY; - private PagedViewCellLayout mWidgetSpacingLayout; - private int mNumAppsPages; - private int mNumWidgetPages; - private Rect mAllAppsPadding = new Rect(); - - // Previews & outlines - ArrayList mRunningTasks; - private static final int sPageSleepDelay = 200; - - private Runnable mInflateWidgetRunnable = null; - private Runnable mBindWidgetRunnable = null; - static final int WIDGET_NO_CLEANUP_REQUIRED = -1; - static final int WIDGET_PRELOAD_PENDING = 0; - static final int WIDGET_BOUND = 1; - static final int WIDGET_INFLATED = 2; - int mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; - int mWidgetLoadingId = -1; - PendingAddWidgetInfo mCreateWidgetInfo = null; - private boolean mDraggingWidget = false; - boolean mPageBackgroundsVisible = true; - - private Toast mWidgetInstructionToast; - - // Deferral of loading widget previews during launcher transitions - private boolean mInTransition; - private ArrayList mDeferredSyncWidgetPageItems = - new ArrayList(); - private ArrayList mDeferredPrepareLoadWidgetPreviewsTasks = - new ArrayList(); - - WidgetPreviewLoader mWidgetPreviewLoader; - - private boolean mInBulkBind; - private boolean mNeedToUpdatePageCountsAndInvalidateData; - - public AppsCustomizePagedView(Context context, AttributeSet attrs) { - super(context, attrs); - mLayoutInflater = LayoutInflater.from(context); - mPackageManager = context.getPackageManager(); - mApps = new ArrayList(); - mWidgets = new ArrayList(); - mIconCache = (LauncherAppState.getInstance()).getIconCache(); - mRunningTasks = new ArrayList(); - - // Save the default widget preview background - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0); - mWidgetCountX = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountX, 2); - mWidgetCountY = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountY, 2); - a.recycle(); - mWidgetSpacingLayout = new PagedViewCellLayout(getContext()); - - // The padding on the non-matched dimension for the default widget preview icons - // (top + bottom) - mFadeInAdjacentScreens = false; - - // Unless otherwise specified this view is important for accessibility. - if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - setSinglePageInViewport(); - } - - @Override - protected void init() { - super.init(); - mCenterPagesVertically = false; - - Context context = getContext(); - Resources r = context.getResources(); - setDragSlopeThreshold(r.getInteger(R.integer.config_appsCustomizeDragSlopeThreshold)/100f); - } - - public void onFinishInflate() { - super.onFinishInflate(); - - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setPadding(grid.edgeMarginPx, 2 * grid.edgeMarginPx, - grid.edgeMarginPx, 2 * grid.edgeMarginPx); - } - - void setAllAppsPadding(Rect r) { - mAllAppsPadding.set(r); - } - - void setWidgetsPageIndicatorPadding(int pageIndicatorHeight) { - setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), pageIndicatorHeight); - } - - WidgetPreviewLoader getWidgetPreviewLoader() { - if (mWidgetPreviewLoader == null) { - mWidgetPreviewLoader = new WidgetPreviewLoader(mLauncher); - } - return mWidgetPreviewLoader; - } - - /** Returns the item index of the center item on this page so that we can restore to this - * item index when we rotate. */ - private int getMiddleComponentIndexOnCurrentPage() { - int i = -1; - if (getPageCount() > 0) { - int currentPage = getCurrentPage(); - if (mContentType == ContentType.Applications) { - AppsCustomizeCellLayout layout = (AppsCustomizeCellLayout) getPageAt(currentPage); - ShortcutAndWidgetContainer childrenLayout = layout.getShortcutsAndWidgets(); - int numItemsPerPage = mCellCountX * mCellCountY; - int childCount = childrenLayout.getChildCount(); - if (childCount > 0) { - i = (currentPage * numItemsPerPage) + (childCount / 2); - } - } else if (mContentType == ContentType.Widgets) { - int numApps = mApps.size(); - PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(currentPage); - int numItemsPerPage = mWidgetCountX * mWidgetCountY; - int childCount = layout.getChildCount(); - if (childCount > 0) { - i = numApps + - (currentPage * numItemsPerPage) + (childCount / 2); - } - } else { - throw new RuntimeException("Invalid ContentType"); - } - } - return i; - } - - /** Get the index of the item to restore to if we need to restore the current page. */ - int getSaveInstanceStateIndex() { - if (mSaveInstanceStateItemIndex == -1) { - mSaveInstanceStateItemIndex = getMiddleComponentIndexOnCurrentPage(); - } - return mSaveInstanceStateItemIndex; - } - - /** Returns the page in the current orientation which is expected to contain the specified - * item index. */ - int getPageForComponent(int index) { - if (index < 0) return 0; - - if (index < mApps.size()) { - int numItemsPerPage = mCellCountX * mCellCountY; - return (index / numItemsPerPage); - } else { - int numItemsPerPage = mWidgetCountX * mWidgetCountY; - return (index - mApps.size()) / numItemsPerPage; - } - } - - /** Restores the page for an item at the specified index */ - void restorePageForIndex(int index) { - if (index < 0) return; - mSaveInstanceStateItemIndex = index; - } - - private void updatePageCounts() { - mNumWidgetPages = (int) Math.ceil(mWidgets.size() / - (float) (mWidgetCountX * mWidgetCountY)); - mNumAppsPages = (int) Math.ceil((float) mApps.size() / (mCellCountX * mCellCountY)); - } - - protected void onDataReady(int width, int height) { - // Now that the data is ready, we can calculate the content width, the number of cells to - // use for each page - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mCellCountX = (int) grid.allAppsNumCols; - mCellCountY = (int) grid.allAppsNumRows; - updatePageCounts(); - - // Force a measure to update recalculate the gaps - mContentWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); - mContentHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - mWidgetSpacingLayout.measure(widthSpec, heightSpec); - - final boolean hostIsTransitioning = getTabHost().isInTransition(); - int page = getPageForComponent(mSaveInstanceStateItemIndex); - invalidatePageData(Math.max(0, page), hostIsTransitioning); - } - - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - - if (!isDataReady()) { - if ((LauncherAppState.isDisableAllApps() || !mApps.isEmpty()) && !mWidgets.isEmpty()) { - post(new Runnable() { - // This code triggers requestLayout so must be posted outside of the - // layout pass. - public void run() { - if (Utilities.isViewAttachedToWindow(AppsCustomizePagedView.this)) { - setDataIsReady(); - onDataReady(getMeasuredWidth(), getMeasuredHeight()); - } - } - }); - } - } - } - - public void onPackagesUpdated(ArrayList widgetsAndShortcuts) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - // Get the list of widgets and shortcuts - mWidgets.clear(); - for (Object o : widgetsAndShortcuts) { - if (o instanceof AppWidgetProviderInfo) { - AppWidgetProviderInfo widget = (AppWidgetProviderInfo) o; - if (!app.shouldShowAppOrWidgetProvider(widget.provider)) { - continue; - } - if (widget.minWidth > 0 && widget.minHeight > 0) { - // Ensure that all widgets we show can be added on a workspace of this size - int[] spanXY = Launcher.getSpanForWidget(mLauncher, widget); - int[] minSpanXY = Launcher.getMinSpanForWidget(mLauncher, widget); - int minSpanX = Math.min(spanXY[0], minSpanXY[0]); - int minSpanY = Math.min(spanXY[1], minSpanXY[1]); - if (minSpanX <= (int) grid.numColumns && - minSpanY <= (int) grid.numRows) { - mWidgets.add(widget); - } else { - Log.e(TAG, "Widget " + widget.provider + " can not fit on this device (" + - widget.minWidth + ", " + widget.minHeight + ")"); - } - } else { - Log.e(TAG, "Widget " + widget.provider + " has invalid dimensions (" + - widget.minWidth + ", " + widget.minHeight + ")"); - } - } else { - // just add shortcuts - mWidgets.add(o); - } - } - updatePageCountsAndInvalidateData(); - } - - public void setBulkBind(boolean bulkBind) { - if (bulkBind) { - mInBulkBind = true; - } else { - mInBulkBind = false; - if (mNeedToUpdatePageCountsAndInvalidateData) { - updatePageCountsAndInvalidateData(); - } - } - } - - private void updatePageCountsAndInvalidateData() { - if (mInBulkBind) { - mNeedToUpdatePageCountsAndInvalidateData = true; - } else { - updatePageCounts(); - invalidateOnDataChange(); - mNeedToUpdatePageCountsAndInvalidateData = false; - } - } - - @Override - public void onClick(View v) { - // When we have exited all apps or are in transition, disregard clicks - if (!mLauncher.isAllAppsVisible() - || mLauncher.getWorkspace().isSwitchingState() - || !(v instanceof PagedViewWidget)) return; - - // Let the user know that they have to long press to add a widget - if (mWidgetInstructionToast != null) { - mWidgetInstructionToast.cancel(); - } - mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add, - Toast.LENGTH_SHORT); - mWidgetInstructionToast.show(); - - // Create a little animation to show that the widget can move - float offsetY = getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY); - final ImageView p = (ImageView) v.findViewById(R.id.widget_preview); - AnimatorSet bounce = LauncherAnimUtils.createAnimatorSet(); - ValueAnimator tyuAnim = LauncherAnimUtils.ofFloat(p, "translationY", offsetY); - tyuAnim.setDuration(125); - ValueAnimator tydAnim = LauncherAnimUtils.ofFloat(p, "translationY", 0f); - tydAnim.setDuration(100); - bounce.play(tyuAnim).before(tydAnim); - bounce.setInterpolator(new AccelerateInterpolator()); - bounce.start(); - } - - public boolean onKey(View v, int keyCode, KeyEvent event) { - return FocusHelper.handleAppsCustomizeKeyEvent(v, keyCode, event); - } - - /* - * PagedViewWithDraggableItems implementation - */ - @Override - protected void determineDraggingStart(android.view.MotionEvent ev) { - // Disable dragging by pulling an app down for now. - } - - private void beginDraggingApplication(View v) { - mLauncher.getWorkspace().beginDragShared(v, this); - } - - static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { - Bundle options = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, sTmpRect); - Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher, - info.componentName, null); - - float density = launcher.getResources().getDisplayMetrics().density; - int xPaddingDips = (int) ((padding.left + padding.right) / density); - int yPaddingDips = (int) ((padding.top + padding.bottom) / density); - - options = new Bundle(); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, - sTmpRect.left - xPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, - sTmpRect.top - yPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, - sTmpRect.right - xPaddingDips); - options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, - sTmpRect.bottom - yPaddingDips); - } - return options; - } - - private void preloadWidget(final PendingAddWidgetInfo info) { - final AppWidgetProviderInfo pInfo = info.info; - final Bundle options = getDefaultOptionsForWidget(mLauncher, info); - - if (pInfo.configure != null) { - info.bindOptions = options; - return; - } - - mWidgetCleanupState = WIDGET_PRELOAD_PENDING; - mBindWidgetRunnable = new Runnable() { - @Override - public void run() { - mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); - if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed( - mWidgetLoadingId, pInfo, options)) { - mWidgetCleanupState = WIDGET_BOUND; - } - } - }; - post(mBindWidgetRunnable); - - mInflateWidgetRunnable = new Runnable() { - @Override - public void run() { - if (mWidgetCleanupState != WIDGET_BOUND) { - return; - } - AppWidgetHostView hostView = mLauncher. - getAppWidgetHost().createView(getContext(), mWidgetLoadingId, pInfo); - info.boundWidget = hostView; - mWidgetCleanupState = WIDGET_INFLATED; - hostView.setVisibility(INVISIBLE); - int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(info.spanX, - info.spanY, info, false); - - // We want the first widget layout to be the correct size. This will be important - // for width size reporting to the AppWidgetManager. - DragLayer.LayoutParams lp = new DragLayer.LayoutParams(unScaledSize[0], - unScaledSize[1]); - lp.x = lp.y = 0; - lp.customPosition = true; - hostView.setLayoutParams(lp); - mLauncher.getDragLayer().addView(hostView); - } - }; - post(mInflateWidgetRunnable); - } - - @Override - public void onShortPress(View v) { - // We are anticipating a long press, and we use this time to load bind and instantiate - // the widget. This will need to be cleaned up if it turns out no long press occurs. - if (mCreateWidgetInfo != null) { - // Just in case the cleanup process wasn't properly executed. This shouldn't happen. - cleanupWidgetPreloading(false); - } - mCreateWidgetInfo = new PendingAddWidgetInfo((PendingAddWidgetInfo) v.getTag()); - preloadWidget(mCreateWidgetInfo); - } - - private void cleanupWidgetPreloading(boolean widgetWasAdded) { - if (!widgetWasAdded) { - // If the widget was not added, we may need to do further cleanup. - PendingAddWidgetInfo info = mCreateWidgetInfo; - mCreateWidgetInfo = null; - - if (mWidgetCleanupState == WIDGET_PRELOAD_PENDING) { - // We never did any preloading, so just remove pending callbacks to do so - removeCallbacks(mBindWidgetRunnable); - removeCallbacks(mInflateWidgetRunnable); - } else if (mWidgetCleanupState == WIDGET_BOUND) { - // Delete the widget id which was allocated - if (mWidgetLoadingId != -1) { - mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); - } - - // We never got around to inflating the widget, so remove the callback to do so. - removeCallbacks(mInflateWidgetRunnable); - } else if (mWidgetCleanupState == WIDGET_INFLATED) { - // Delete the widget id which was allocated - if (mWidgetLoadingId != -1) { - mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); - } - - // The widget was inflated and added to the DragLayer -- remove it. - AppWidgetHostView widget = info.boundWidget; - mLauncher.getDragLayer().removeView(widget); - } - } - mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED; - mWidgetLoadingId = -1; - mCreateWidgetInfo = null; - PagedViewWidget.resetShortPressTarget(); - } - - @Override - public void cleanUpShortPress(View v) { - if (!mDraggingWidget) { - cleanupWidgetPreloading(false); - } - } - - private boolean beginDraggingWidget(View v) { - mDraggingWidget = true; - // Get the widget preview as the drag representation - ImageView image = (ImageView) v.findViewById(R.id.widget_preview); - PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); - - // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and - // we abort the drag. - if (image.getDrawable() == null) { - mDraggingWidget = false; - return false; - } - - // Compose the drag image - Bitmap preview; - Bitmap outline; - float scale = 1f; - Point previewPadding = null; - - if (createItemInfo instanceof PendingAddWidgetInfo) { - // This can happen in some weird cases involving multi-touch. We can't start dragging - // the widget if this is null, so we break out. - if (mCreateWidgetInfo == null) { - return false; - } - - PendingAddWidgetInfo createWidgetInfo = mCreateWidgetInfo; - createItemInfo = createWidgetInfo; - int spanX = createItemInfo.spanX; - int spanY = createItemInfo.spanY; - int[] size = mLauncher.getWorkspace().estimateItemSize(spanX, spanY, - createWidgetInfo, true); - - FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable(); - float minScale = 1.25f; - int maxWidth, maxHeight; - maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]); - maxHeight = Math.min((int) (previewDrawable.getIntrinsicHeight() * minScale), size[1]); - - int[] previewSizeBeforeScale = new int[1]; - - preview = getWidgetPreviewLoader().generateWidgetPreview(createWidgetInfo.info, - spanX, spanY, maxWidth, maxHeight, null, previewSizeBeforeScale); - - // Compare the size of the drag preview to the preview in the AppsCustomize tray - int previewWidthInAppsCustomize = Math.min(previewSizeBeforeScale[0], - getWidgetPreviewLoader().maxWidthForWidgetPreview(spanX)); - scale = previewWidthInAppsCustomize / (float) preview.getWidth(); - - // The bitmap in the AppsCustomize tray is always the the same size, so there - // might be extra pixels around the preview itself - this accounts for that - if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) { - int padding = - (previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2; - previewPadding = new Point(padding, 0); - } - } else { - PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); - Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.shortcutActivityInfo); - preview = Utilities.createIconBitmap(icon, mLauncher); - createItemInfo.spanX = createItemInfo.spanY = 1; - } - - // Don't clip alpha values for the drag outline if we're using the default widget preview - boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo && - (((PendingAddWidgetInfo) createItemInfo).previewImage == 0)); - - // Save the preview for the outline generation, then dim the preview - outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(), - false); - - // Start the drag - mLauncher.lockScreenOrientation(); - mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha); - mDragController.startDrag(image, preview, this, createItemInfo, - DragController.DRAG_ACTION_COPY, previewPadding, scale); - outline.recycle(); - preview.recycle(); - return true; - } - - @Override - protected boolean beginDragging(final View v) { - if (!super.beginDragging(v)) return false; - - if (v instanceof BubbleTextView) { - beginDraggingApplication(v); - } else if (v instanceof PagedViewWidget) { - if (!beginDraggingWidget(v)) { - return false; - } - } - - // We delay entering spring-loaded mode slightly to make sure the UI - // thready is free of any work. - postDelayed(new Runnable() { - @Override - public void run() { - // We don't enter spring-loaded mode if the drag has been cancelled - if (mLauncher.getDragController().isDragging()) { - // Go into spring loaded mode (must happen before we startDrag()) - mLauncher.enterSpringLoadedDragMode(); - } - } - }, 150); - - return true; - } - - /** - * Clean up after dragging. - * - * @param target where the item was dragged to (can be null if the item was flung) - */ - private void endDragging(View target, boolean isFlingToDelete, boolean success) { - if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && - !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { - // Exit spring loaded mode if we have not successfully dropped or have not handled the - // drop in Workspace - mLauncher.exitSpringLoadedDragModeDelayed(true, - Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); - mLauncher.unlockScreenOrientation(false); - } else { - mLauncher.unlockScreenOrientation(false); - } - } - - @Override - public View getContent() { - if (getChildCount() > 0) { - return getChildAt(0); - } - return null; - } - - @Override - public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - mInTransition = true; - if (toWorkspace) { - cancelAllTasks(); - } - } - - @Override - public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { - } - - @Override - public void onLauncherTransitionStep(Launcher l, float t) { - } - - @Override - public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - mInTransition = false; - for (AsyncTaskPageData d : mDeferredSyncWidgetPageItems) { - onSyncWidgetPageItems(d, false); - } - mDeferredSyncWidgetPageItems.clear(); - for (Runnable r : mDeferredPrepareLoadWidgetPreviewsTasks) { - r.run(); - } - mDeferredPrepareLoadWidgetPreviewsTasks.clear(); - mForceDrawAllChildrenNextFrame = !toWorkspace; - } - - @Override - public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, - boolean success) { - // Return early and wait for onFlingToDeleteCompleted if this was the result of a fling - if (isFlingToDelete) return; - - endDragging(target, false, success); - - // Display an error message if the drag failed due to there not being enough space on the - // target layout we were dropping on. - if (!success) { - boolean showOutOfSpaceMessage = false; - if (target instanceof Workspace) { - int currentScreen = mLauncher.getCurrentWorkspaceScreen(); - Workspace workspace = (Workspace) target; - CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); - ItemInfo itemInfo = (ItemInfo) d.dragInfo; - if (layout != null) { - layout.calculateSpans(itemInfo); - showOutOfSpaceMessage = - !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); - } - } - if (showOutOfSpaceMessage) { - mLauncher.showOutOfSpaceMessage(false); - } - - d.deferDragViewCleanupPostAnimation = false; - } - cleanupWidgetPreloading(success); - mDraggingWidget = false; - } - - @Override - public void onFlingToDeleteCompleted() { - // We just dismiss the drag when we fling, so cleanup here - endDragging(null, true, true); - cleanupWidgetPreloading(false); - mDraggingWidget = false; - } - - @Override - public boolean supportsFlingToDelete() { - return true; - } - - @Override - public boolean supportsAppInfoDropTarget() { - return true; - } - - @Override - public boolean supportsDeleteDropTarget() { - return false; - } - - @Override - public float getIntrinsicIconScaleFactor() { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - return (float) grid.allAppsIconSizePx / grid.iconSizePx; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - cancelAllTasks(); - } - - @Override - public void trimMemory() { - super.trimMemory(); - clearAllWidgetPages(); - } - - public void clearAllWidgetPages() { - cancelAllTasks(); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View v = getPageAt(i); - if (v instanceof PagedViewGridLayout) { - ((PagedViewGridLayout) v).removeAllViewsOnPage(); - mDirtyPageContent.set(i, true); - } - } - } - - private void cancelAllTasks() { - // Clean up all the async tasks - Iterator iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - task.cancel(false); - iter.remove(); - mDirtyPageContent.set(task.page, true); - - // We've already preallocated the views for the data to load into, so clear them as well - View v = getPageAt(task.page); - if (v instanceof PagedViewGridLayout) { - ((PagedViewGridLayout) v).removeAllViewsOnPage(); - } - } - mDeferredSyncWidgetPageItems.clear(); - mDeferredPrepareLoadWidgetPreviewsTasks.clear(); - } - - public void setContentType(ContentType type) { - // Widgets appear to be cleared every time you leave, always force invalidate for them - if (mContentType != type || type == ContentType.Widgets) { - int page = (mContentType != type) ? 0 : getCurrentPage(); - mContentType = type; - invalidatePageData(page, true); - } - } - - public ContentType getContentType() { - return mContentType; - } - - protected void snapToPage(int whichPage, int delta, int duration) { - super.snapToPage(whichPage, delta, duration); - - // Update the thread priorities given the direction lookahead - Iterator iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int pageIndex = task.page; - if ((mNextPage > mCurrentPage && pageIndex >= mCurrentPage) || - (mNextPage < mCurrentPage && pageIndex <= mCurrentPage)) { - task.setThreadPriority(getThreadPriorityForPage(pageIndex)); - } else { - task.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); - } - } - } - - /* - * Apps PagedView implementation - */ - private void setVisibilityOnChildren(ViewGroup layout, int visibility) { - int childCount = layout.getChildCount(); - for (int i = 0; i < childCount; ++i) { - layout.getChildAt(i).setVisibility(visibility); - } - } - private void setupPage(AppsCustomizeCellLayout layout) { - layout.setGridSize(mCellCountX, mCellCountY); - - // Note: We force a measure here to get around the fact that when we do layout calculations - // immediately after syncing, we don't have a proper width. That said, we already know the - // expected page width, so we can actually optimize by hiding all the TextView-based - // children that are expensive to measure, and let that happen naturally later. - setVisibilityOnChildren(layout, View.GONE); - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - layout.measure(widthSpec, heightSpec); - - Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel); - if (bg != null) { - bg.setAlpha(mPageBackgroundsVisible ? 255: 0); - layout.setBackground(bg); - } - - setVisibilityOnChildren(layout, View.VISIBLE); - } - - public void setPageBackgroundsVisible(boolean visible) { - mPageBackgroundsVisible = visible; - int childCount = getChildCount(); - for (int i = 0; i < childCount; ++i) { - Drawable bg = getChildAt(i).getBackground(); - if (bg != null) { - bg.setAlpha(visible ? 255 : 0); - } - } - } - - public void syncAppsPageItems(int page, boolean immediate) { - // ensure that we have the right number of items on the pages - final boolean isRtl = isLayoutRtl(); - int numCells = mCellCountX * mCellCountY; - int startIndex = page * numCells; - int endIndex = Math.min(startIndex + numCells, mApps.size()); - AppsCustomizeCellLayout layout = (AppsCustomizeCellLayout) getPageAt(page); - - layout.removeAllViewsOnPage(); - ArrayList items = new ArrayList(); - ArrayList images = new ArrayList(); - for (int i = startIndex; i < endIndex; ++i) { - AppInfo info = mApps.get(i); - BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( - R.layout.apps_customize_application, layout, false); - icon.applyFromApplicationInfo(info); - icon.setOnClickListener(mLauncher); - icon.setOnLongClickListener(this); - icon.setOnTouchListener(this); - icon.setOnKeyListener(this); - icon.setOnFocusChangeListener(layout.mFocusHandlerView); - - int index = i - startIndex; - int x = index % mCellCountX; - int y = index / mCellCountX; - if (isRtl) { - x = mCellCountX - x - 1; - } - layout.addViewToCellLayout(icon, -1, i, new CellLayout.LayoutParams(x,y, 1,1), false); - - items.add(info); - images.add(info.iconBitmap); - } - - enableHwLayersOnVisiblePages(); - } - - /** - * A helper to return the priority for loading of the specified widget page. - */ - private int getWidgetPageLoadPriority(int page) { - // If we are snapping to another page, use that index as the target page index - int toPage = mCurrentPage; - if (mNextPage > -1) { - toPage = mNextPage; - } - - // We use the distance from the target page as an initial guess of priority, but if there - // are no pages of higher priority than the page specified, then bump up the priority of - // the specified page. - Iterator iter = mRunningTasks.iterator(); - int minPageDiff = Integer.MAX_VALUE; - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - minPageDiff = Math.abs(task.page - toPage); - } - - int rawPageDiff = Math.abs(page - toPage); - return rawPageDiff - Math.min(rawPageDiff, minPageDiff); - } - /** - * Return the appropriate thread priority for loading for a given page (we give the current - * page much higher priority) - */ - private int getThreadPriorityForPage(int page) { - // TODO-APPS_CUSTOMIZE: detect number of cores and set thread priorities accordingly below - int pageDiff = getWidgetPageLoadPriority(page); - if (pageDiff <= 0) { - return Process.THREAD_PRIORITY_LESS_FAVORABLE; - } else if (pageDiff <= 1) { - return Process.THREAD_PRIORITY_LOWEST; - } else { - return Process.THREAD_PRIORITY_LOWEST; - } - } - private int getSleepForPage(int page) { - int pageDiff = getWidgetPageLoadPriority(page); - return Math.max(0, pageDiff * sPageSleepDelay); - } - /** - * Creates and executes a new AsyncTask to load a page of widget previews. - */ - private void prepareLoadWidgetPreviewsTask(int page, ArrayList widgets, - int cellWidth, int cellHeight, int cellCountX) { - - // Prune all tasks that are no longer needed - Iterator iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int taskPage = task.page; - if (taskPage < getAssociatedLowerPageBound(mCurrentPage) || - taskPage > getAssociatedUpperPageBound(mCurrentPage)) { - task.cancel(false); - iter.remove(); - } else { - task.setThreadPriority(getThreadPriorityForPage(taskPage)); - } - } - - // We introduce a slight delay to order the loading of side pages so that we don't thrash - final int sleepMs = getSleepForPage(page); - AsyncTaskPageData pageData = new AsyncTaskPageData(page, widgets, cellWidth, cellHeight, - new AsyncTaskCallback() { - @Override - public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - try { - try { - Thread.sleep(sleepMs); - } catch (Exception e) {} - loadWidgetPreviewsInBackground(task, data); - } finally { - if (task.isCancelled()) { - data.cleanup(true); - } - } - } - }, - new AsyncTaskCallback() { - @Override - public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) { - mRunningTasks.remove(task); - if (task.isCancelled()) return; - // do cleanup inside onSyncWidgetPageItems - onSyncWidgetPageItems(data, false); - } - }, getWidgetPreviewLoader()); - - // Ensure that the task is appropriately prioritized and runs in parallel - AppsCustomizeAsyncTask t = new AppsCustomizeAsyncTask(page, - AsyncTaskPageData.Type.LoadWidgetPreviewData); - t.setThreadPriority(getThreadPriorityForPage(page)); - t.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pageData); - mRunningTasks.add(t); - } - - /* - * Widgets PagedView implementation - */ - private void setupPage(PagedViewGridLayout layout) { - // Note: We force a measure here to get around the fact that when we do layout calculations - // immediately after syncing, we don't have a proper width. - int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST); - - Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel_dark); - if (bg != null) { - bg.setAlpha(mPageBackgroundsVisible ? 255 : 0); - layout.setBackground(bg); - } - layout.measure(widthSpec, heightSpec); - } - - public void syncWidgetPageItems(final int page, final boolean immediate) { - int numItemsPerPage = mWidgetCountX * mWidgetCountY; - - final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); - - // Calculate the dimensions of each cell we are giving to each widget - final ArrayList items = new ArrayList(); - int contentWidth = mContentWidth - layout.getPaddingLeft() - layout.getPaddingRight(); - final int cellWidth = contentWidth / mWidgetCountX; - int contentHeight = mContentHeight - layout.getPaddingTop() - layout.getPaddingBottom(); - - final int cellHeight = contentHeight / mWidgetCountY; - - // Prepare the set of widgets to load previews for in the background - int offset = page * numItemsPerPage; - for (int i = offset; i < Math.min(offset + numItemsPerPage, mWidgets.size()); ++i) { - items.add(mWidgets.get(i)); - } - - // Prepopulate the pages with the other widget info, and fill in the previews later - layout.setColumnCount(layout.getCellCountX()); - for (int i = 0; i < items.size(); ++i) { - Object rawInfo = items.get(i); - PendingAddItemInfo createItemInfo = null; - PagedViewWidget widget = (PagedViewWidget) mLayoutInflater.inflate( - R.layout.apps_customize_widget, layout, false); - if (rawInfo instanceof AppWidgetProviderInfo) { - // Fill in the widget information - AppWidgetProviderInfo info = (AppWidgetProviderInfo) rawInfo; - createItemInfo = new PendingAddWidgetInfo(info, null, null); - - // Determine the widget spans and min resize spans. - int[] spanXY = Launcher.getSpanForWidget(mLauncher, info); - createItemInfo.spanX = spanXY[0]; - createItemInfo.spanY = spanXY[1]; - int[] minSpanXY = Launcher.getMinSpanForWidget(mLauncher, info); - createItemInfo.minSpanX = minSpanXY[0]; - createItemInfo.minSpanY = minSpanXY[1]; - - widget.applyFromAppWidgetProviderInfo(info, -1, spanXY, getWidgetPreviewLoader()); - widget.setTag(createItemInfo); - widget.setShortPressListener(this); - } else if (rawInfo instanceof ResolveInfo) { - // Fill in the shortcuts information - ResolveInfo info = (ResolveInfo) rawInfo; - createItemInfo = new PendingAddShortcutInfo(info.activityInfo); - createItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; - createItemInfo.componentName = new ComponentName(info.activityInfo.packageName, - info.activityInfo.name); - widget.applyFromResolveInfo(mPackageManager, info, getWidgetPreviewLoader()); - widget.setTag(createItemInfo); - } - widget.setOnClickListener(this); - widget.setOnLongClickListener(this); - widget.setOnTouchListener(this); - widget.setOnKeyListener(this); - - // Layout each widget - int ix = i % mWidgetCountX; - int iy = i / mWidgetCountX; - - if (ix > 0) { - View border = widget.findViewById(R.id.left_border); - border.setVisibility(View.VISIBLE); - } - if (ix < mWidgetCountX - 1) { - View border = widget.findViewById(R.id.right_border); - border.setVisibility(View.VISIBLE); - } - - GridLayout.LayoutParams lp = new GridLayout.LayoutParams( - GridLayout.spec(iy, GridLayout.START), - GridLayout.spec(ix, GridLayout.TOP)); - lp.width = cellWidth; - lp.height = cellHeight; - lp.setGravity(Gravity.TOP | Gravity.START); - layout.addView(widget, lp); - } - - // wait until a call on onLayout to start loading, because - // PagedViewWidget.getPreviewSize() will return 0 if it hasn't been laid out - // TODO: can we do a measure/layout immediately? - layout.setOnLayoutListener(new Runnable() { - public void run() { - // Load the widget previews - int maxPreviewWidth = cellWidth; - int maxPreviewHeight = cellHeight; - if (layout.getChildCount() > 0) { - PagedViewWidget w = (PagedViewWidget) layout.getChildAt(0); - int[] maxSize = w.getPreviewSize(); - maxPreviewWidth = maxSize[0]; - maxPreviewHeight = maxSize[1]; - } - - getWidgetPreviewLoader().setPreviewSize( - maxPreviewWidth, maxPreviewHeight, mWidgetSpacingLayout); - if (immediate) { - AsyncTaskPageData data = new AsyncTaskPageData(page, items, - maxPreviewWidth, maxPreviewHeight, null, null, getWidgetPreviewLoader()); - loadWidgetPreviewsInBackground(null, data); - onSyncWidgetPageItems(data, immediate); - } else { - if (mInTransition) { - mDeferredPrepareLoadWidgetPreviewsTasks.add(this); - } else { - prepareLoadWidgetPreviewsTask(page, items, - maxPreviewWidth, maxPreviewHeight, mWidgetCountX); - } - } - layout.setOnLayoutListener(null); - } - }); - } - private void loadWidgetPreviewsInBackground(AppsCustomizeAsyncTask task, - AsyncTaskPageData data) { - // loadWidgetPreviewsInBackground can be called without a task to load a set of widget - // previews synchronously - if (task != null) { - // Ensure that this task starts running at the correct priority - task.syncThreadPriority(); - } - - // Load each of the widget/shortcut previews - ArrayList items = data.items; - ArrayList images = data.generatedImages; - int count = items.size(); - for (int i = 0; i < count; ++i) { - if (task != null) { - // Ensure we haven't been cancelled yet - if (task.isCancelled()) break; - // Before work on each item, ensure that this task is running at the correct - // priority - task.syncThreadPriority(); - } - - images.add(getWidgetPreviewLoader().getPreview(items.get(i))); - } - } - - private void onSyncWidgetPageItems(AsyncTaskPageData data, boolean immediatelySyncItems) { - if (!immediatelySyncItems && mInTransition) { - mDeferredSyncWidgetPageItems.add(data); - return; - } - try { - int page = data.page; - PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); - - ArrayList items = data.items; - int count = items.size(); - for (int i = 0; i < count; ++i) { - PagedViewWidget widget = (PagedViewWidget) layout.getChildAt(i); - if (widget != null) { - Bitmap preview = data.generatedImages.get(i); - widget.applyPreview(new FastBitmapDrawable(preview), i); - } - } - - enableHwLayersOnVisiblePages(); - - // Update all thread priorities - Iterator iter = mRunningTasks.iterator(); - while (iter.hasNext()) { - AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next(); - int pageIndex = task.page; - task.setThreadPriority(getThreadPriorityForPage(pageIndex)); - } - } finally { - data.cleanup(false); - } - } - - @Override - public void syncPages() { - disablePagedViewAnimations(); - - removeAllViews(); - cancelAllTasks(); - - Context context = getContext(); - if (mContentType == ContentType.Applications) { - for (int i = 0; i < mNumAppsPages; ++i) { - AppsCustomizeCellLayout layout = new AppsCustomizeCellLayout(context); - setupPage(layout); - addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - } - } else if (mContentType == ContentType.Widgets) { - for (int j = 0; j < mNumWidgetPages; ++j) { - PagedViewGridLayout layout = new PagedViewGridLayout(context, mWidgetCountX, - mWidgetCountY); - setupPage(layout); - addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - } - } else { - throw new RuntimeException("Invalid ContentType"); - } - - enablePagedViewAnimations(); - } - - @Override - public void syncPageItems(int page, boolean immediate) { - if (mContentType == ContentType.Widgets) { - syncWidgetPageItems(page, immediate); - } else { - syncAppsPageItems(page, immediate); - } - } - - // We want our pages to be z-ordered such that the further a page is to the left, the higher - // it is in the z-order. This is important to insure touch events are handled correctly. - View getPageAt(int index) { - return getChildAt(indexToPage(index)); - } - - @Override - protected int indexToPage(int index) { - return getChildCount() - index - 1; - } - - // In apps customize, we have a scrolling effect which emulates pulling cards off of a stack. - @Override - protected void screenScrolled(int screenCenter) { - super.screenScrolled(screenCenter); - enableHwLayersOnVisiblePages(); - } - - private void enableHwLayersOnVisiblePages() { - final int screenCount = getChildCount(); - - getVisiblePages(mTempVisiblePagesRange); - int leftScreen = mTempVisiblePagesRange[0]; - int rightScreen = mTempVisiblePagesRange[1]; - int forceDrawScreen = -1; - if (leftScreen == rightScreen) { - // make sure we're caching at least two pages always - if (rightScreen < screenCount - 1) { - rightScreen++; - forceDrawScreen = rightScreen; - } else if (leftScreen > 0) { - leftScreen--; - forceDrawScreen = leftScreen; - } - } else { - forceDrawScreen = leftScreen + 1; - } - - for (int i = 0; i < screenCount; i++) { - final View layout = (View) getPageAt(i); - if (!(leftScreen <= i && i <= rightScreen && - (i == forceDrawScreen || shouldDrawChild(layout)))) { - layout.setLayerType(LAYER_TYPE_NONE, null); - } - } - - for (int i = 0; i < screenCount; i++) { - final View layout = (View) getPageAt(i); - - if (leftScreen <= i && i <= rightScreen && - (i == forceDrawScreen || shouldDrawChild(layout))) { - if (layout.getLayerType() != LAYER_TYPE_HARDWARE) { - layout.setLayerType(LAYER_TYPE_HARDWARE, null); - } - } - } - } - - protected void overScroll(float amount) { - dampedOverScroll(amount); - } - - /** - * Used by the parent to get the content width to set the tab bar to - * @return - */ - public int getPageContentWidth() { - return mContentWidth; - } - - @Override - protected void onPageEndMoving() { - super.onPageEndMoving(); - mForceDrawAllChildrenNextFrame = true; - // We reset the save index when we change pages so that it will be recalculated on next - // rotation - mSaveInstanceStateItemIndex = -1; - } - - /* - * AllAppsView implementation - */ - public void setup(Launcher launcher, DragController dragController) { - mLauncher = launcher; - mDragController = dragController; - } - - /** - * We should call thise method whenever the core data changes (mApps, mWidgets) so that we can - * appropriately determine when to invalidate the PagedView page data. In cases where the data - * has yet to be set, we can requestLayout() and wait for onDataReady() to be called in the - * next onMeasure() pass, which will trigger an invalidatePageData() itself. - */ - private void invalidateOnDataChange() { - if (!isDataReady()) { - // The next layout pass will trigger data-ready if both widgets and apps are set, so - // request a layout to trigger the page data when ready. - requestLayout(); - } else { - cancelAllTasks(); - invalidatePageData(); - } - } - - public void setApps(ArrayList list) { - if (!LauncherAppState.isDisableAllApps()) { - mApps = list; - Collections.sort(mApps, LauncherModel.getAppNameComparator()); - updatePageCountsAndInvalidateData(); - } - } - private void addAppsWithoutInvalidate(ArrayList list) { - // We add it in place, in alphabetical order - int count = list.size(); - for (int i = 0; i < count; ++i) { - AppInfo info = list.get(i); - int index = Collections.binarySearch(mApps, info, LauncherModel.getAppNameComparator()); - if (index < 0) { - mApps.add(-(index + 1), info); - } - } - } - public void addApps(ArrayList list) { - if (!LauncherAppState.isDisableAllApps()) { - addAppsWithoutInvalidate(list); - updatePageCountsAndInvalidateData(); - } - } - private int findAppByComponent(List list, AppInfo item) { - ComponentName removeComponent = item.intent.getComponent(); - int length = list.size(); - for (int i = 0; i < length; ++i) { - AppInfo info = list.get(i); - if (info.user.equals(item.user) - && info.intent.getComponent().equals(removeComponent)) { - return i; - } - } - return -1; - } - private void removeAppsWithoutInvalidate(ArrayList list) { - // loop through all the apps and remove apps that have the same component - int length = list.size(); - for (int i = 0; i < length; ++i) { - AppInfo info = list.get(i); - int removeIndex = findAppByComponent(mApps, info); - if (removeIndex > -1) { - mApps.remove(removeIndex); - } - } - } - public void removeApps(ArrayList appInfos) { - if (!LauncherAppState.isDisableAllApps()) { - removeAppsWithoutInvalidate(appInfos); - updatePageCountsAndInvalidateData(); - } - } - public void updateApps(ArrayList list) { - // We remove and re-add the updated applications list because it's properties may have - // changed (ie. the title), and this will ensure that the items will be in their proper - // place in the list. - if (!LauncherAppState.isDisableAllApps()) { - removeAppsWithoutInvalidate(list); - addAppsWithoutInvalidate(list); - updatePageCountsAndInvalidateData(); - } - } - - public void reset() { - // If we have reset, then we should not continue to restore the previous state - mSaveInstanceStateItemIndex = -1; - - if (mContentType != ContentType.Applications) { - setContentType(ContentType.Applications); - } - - if (mCurrentPage != 0) { - invalidatePageData(0); - } - } - - private AppsCustomizeTabHost getTabHost() { - return (AppsCustomizeTabHost) mLauncher.findViewById(R.id.apps_customize_pane); - } - - public void dumpState() { - // TODO: Dump information related to current list of Applications, Widgets, etc. - AppInfo.dumpApplicationInfoList(TAG, "mApps", mApps); - dumpAppWidgetProviderInfoList(TAG, "mWidgets", mWidgets); - } - - private void dumpAppWidgetProviderInfoList(String tag, String label, - ArrayList list) { - Log.d(tag, label + " size=" + list.size()); - for (Object i: list) { - if (i instanceof AppWidgetProviderInfo) { - AppWidgetProviderInfo info = (AppWidgetProviderInfo) i; - Log.d(tag, " label=\"" + info.label + "\" previewImage=" + info.previewImage - + " resizeMode=" + info.resizeMode + " configure=" + info.configure - + " initialLayout=" + info.initialLayout - + " minWidth=" + info.minWidth + " minHeight=" + info.minHeight); - } else if (i instanceof ResolveInfo) { - ResolveInfo info = (ResolveInfo) i; - Log.d(tag, " label=\"" + info.loadLabel(mPackageManager) + "\" icon=" - + info.icon); - } - } - } - - public void surrender() { - // TODO: If we are in the middle of any process (ie. for holographic outlines, etc) we - // should stop this now. - - // Stop all background tasks - cancelAllTasks(); - } - - /* - * We load an extra page on each side to prevent flashes from scrolling and loading of the - * widget previews in the background with the AsyncTasks. - */ - final static int sLookBehindPageCount = 2; - final static int sLookAheadPageCount = 2; - protected int getAssociatedLowerPageBound(int page) { - final int count = getChildCount(); - int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1); - int windowMinIndex = Math.max(Math.min(page - sLookBehindPageCount, count - windowSize), 0); - return windowMinIndex; - } - protected int getAssociatedUpperPageBound(int page) { - final int count = getChildCount(); - int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1); - int windowMaxIndex = Math.min(Math.max(page + sLookAheadPageCount, windowSize - 1), - count - 1); - return windowMaxIndex; - } - - protected String getCurrentPageDescription() { - int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; - int stringId = R.string.default_scroll_format; - int count = 0; - - if (mContentType == ContentType.Applications) { - stringId = R.string.apps_customize_apps_scroll_format; - count = mNumAppsPages; - } else if (mContentType == ContentType.Widgets) { - stringId = R.string.apps_customize_widgets_scroll_format; - count = mNumWidgetPages; - } else { - throw new RuntimeException("Invalid ContentType"); - } - - return String.format(getContext().getString(stringId), page + 1, count); - } -} diff --git a/src/com/android/launcher3/AppsCustomizeTabHost.java b/src/com/android/launcher3/AppsCustomizeTabHost.java deleted file mode 100644 index a2717126d1..0000000000 --- a/src/com/android/launcher3/AppsCustomizeTabHost.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; -import android.widget.FrameLayout; - -public class AppsCustomizeTabHost extends FrameLayout implements LauncherTransitionable, Insettable { - static final String LOG_TAG = "AppsCustomizeTabHost"; - - private static final String APPS_TAB_TAG = "APPS"; - private static final String WIDGETS_TAB_TAG = "WIDGETS"; - - private AppsCustomizePagedView mPagedView; - private View mContent; - private boolean mInTransition = false; - - private final Rect mInsets = new Rect(); - - public AppsCustomizeTabHost(Context context, AttributeSet attrs) { - super(context, attrs); - } - - /** - * Convenience methods to select specific tabs. We want to set the content type immediately - * in these cases, but we note that we still call setCurrentTabByTag() so that the tab view - * reflects the new content (but doesn't do the animation and logic associated with changing - * tabs manually). - */ - void setContentTypeImmediate(AppsCustomizePagedView.ContentType type) { - mPagedView.setContentType(type); - } - - public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) { - setContentTypeImmediate(type); - } - - @Override - public void setInsets(Rect insets) { - mInsets.set(insets); - LayoutParams flp = (LayoutParams) mContent.getLayoutParams(); - flp.topMargin = insets.top; - flp.bottomMargin = insets.bottom; - flp.leftMargin = insets.left; - flp.rightMargin = insets.right; - mContent.setLayoutParams(flp); - } - - /** - * Setup the tab host and create all necessary tabs. - */ - @Override - protected void onFinishInflate() { - mPagedView = (AppsCustomizePagedView) findViewById(R.id.apps_customize_pane_content); - mContent = findViewById(R.id.content); - } - - public String getContentTag() { - return getTabTagForContentType(mPagedView.getContentType()); - } - - /** - * Returns the content type for the specified tab tag. - */ - public AppsCustomizePagedView.ContentType getContentTypeForTabTag(String tag) { - if (tag.equals(APPS_TAB_TAG)) { - return AppsCustomizePagedView.ContentType.Applications; - } else if (tag.equals(WIDGETS_TAB_TAG)) { - return AppsCustomizePagedView.ContentType.Widgets; - } - return AppsCustomizePagedView.ContentType.Applications; - } - - /** - * Returns the tab tag for a given content type. - */ - public String getTabTagForContentType(AppsCustomizePagedView.ContentType type) { - if (type == AppsCustomizePagedView.ContentType.Applications) { - return APPS_TAB_TAG; - } else if (type == AppsCustomizePagedView.ContentType.Widgets) { - return WIDGETS_TAB_TAG; - } - return APPS_TAB_TAG; - } - - /** - * Disable focus on anything under this view in the hierarchy if we are not visible. - */ - @Override - public int getDescendantFocusability() { - if (getVisibility() != View.VISIBLE) { - return ViewGroup.FOCUS_BLOCK_DESCENDANTS; - } - return super.getDescendantFocusability(); - } - - void reset() { - // Reset immediately - mPagedView.reset(); - } - - void trimMemory() { - mPagedView.trimMemory(); - } - - public void onWindowVisible() { - if (getVisibility() == VISIBLE) { - mContent.setVisibility(VISIBLE); - // We unload the widget previews when the UI is hidden, so need to reload pages - // Load the current page synchronously, and the neighboring pages asynchronously - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage(), true); - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); - } - } - @Override - public ViewGroup getContent() { - return mPagedView; - } - - public boolean isInTransition() { - return mInTransition; - } - - /* LauncherTransitionable overrides */ - @Override - public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionPrepare(l, animated, toWorkspace); - mInTransition = true; - - if (toWorkspace) { - // Going from All Apps -> Workspace - setVisibilityOfSiblingsWithLowerZOrder(VISIBLE); - } else { - // Going from Workspace -> All Apps - mContent.setVisibility(VISIBLE); - - // Make sure the current page is loaded (we start loading the side pages after the - // transition to prevent slowing down the animation) - // TODO: revisit this - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); - } - } - - @Override - public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionStart(l, animated, toWorkspace); - } - - @Override - public void onLauncherTransitionStep(Launcher l, float t) { - mPagedView.onLauncherTransitionStep(l, t); - } - - @Override - public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - mPagedView.onLauncherTransitionEnd(l, animated, toWorkspace); - mInTransition = false; - - if (!toWorkspace) { - // Make sure adjacent pages are loaded (we wait until after the transition to - // prevent slowing down the animation) - mPagedView.loadAssociatedPages(mPagedView.getCurrentPage()); - - // Opening apps, need to announce what page we are on. - AccessibilityManager am = (AccessibilityManager) - getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - if (am.isEnabled()) { - // Notify the user when the page changes - announceForAccessibility(mPagedView.getCurrentPageDescription()); - } - - // Going from Workspace -> All Apps - // NOTE: We should do this at the end since we check visibility state in some of the - // cling initialization/dismiss code above. - setVisibilityOfSiblingsWithLowerZOrder(INVISIBLE); - } - } - - private void setVisibilityOfSiblingsWithLowerZOrder(int visibility) { - ViewGroup parent = (ViewGroup) getParent(); - if (parent == null) return; - - View overviewPanel = ((Launcher) getContext()).getOverviewPanel(); - final int count = parent.getChildCount(); - if (!isChildrenDrawingOrderEnabled()) { - for (int i = 0; i < count; i++) { - final View child = parent.getChildAt(i); - if (child == this) { - break; - } else { - if (child.getVisibility() == GONE || child == overviewPanel) { - continue; - } - child.setVisibility(visibility); - } - } - } else { - throw new RuntimeException("Failed; can't get z-order of views"); - } - } -} diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index a5d22286d4..99a98ddac2 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -37,6 +37,7 @@ import android.util.Patterns; import com.android.launcher3.LauncherProvider.SqlArguments; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -44,6 +45,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Locale; /** * Layout parsing code for auto installs layout @@ -56,6 +58,11 @@ public class AutoInstallsLayout { static final String ACTION_LAUNCHER_CUSTOMIZATION = "android.autoinstalls.config.action.PLAY_AUTO_INSTALL"; + /** + * Layout resource which also includes grid size and hotseat count, e.g., default_layout_6x6_h5 + */ + private static final String FORMATTED_LAYOUT_RES_WITH_HOSTEAT = "default_layout_%dx%d_h%s"; + private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d"; private static final String LAYOUT_RES = "default_layout"; static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost, @@ -65,19 +72,44 @@ public class AutoInstallsLayout { if (customizationApkInfo == null) { return null; } + return get(context, customizationApkInfo.first, customizationApkInfo.second, + appWidgetHost, callback); + } + + static AutoInstallsLayout get(Context context, String pkg, Resources targetRes, + AppWidgetHost appWidgetHost, LayoutParserCallback callback) { + InvariantDeviceProfile grid = LauncherAppState.getInstance().getInvariantDeviceProfile(); + + // Try with grid size and hotseat count + String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT, + (int) grid.numColumns, (int) grid.numRows, (int) grid.numHotseatIcons); + int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg); + + // Try with only grid size + if (layoutId == 0) { + Log.d(TAG, "Formatted layout: " + layoutName + + " not found. Trying layout without hosteat"); + layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES, + (int) grid.numColumns, (int) grid.numRows); + layoutId = targetRes.getIdentifier(layoutName, "xml", pkg); + } + + // Try the default layout + if (layoutId == 0) { + Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout"); + layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml", pkg); + } - String pkg = customizationApkInfo.first; - Resources res = customizationApkInfo.second; - int layoutId = res.getIdentifier(LAYOUT_RES, "xml", pkg); if (layoutId == 0) { Log.e(TAG, "Layout definition not found in package: " + pkg); return null; } - return new AutoInstallsLayout(context, appWidgetHost, callback, res, layoutId, + return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId, TAG_WORKSPACE); } // Object Tags + private static final String TAG_INCLUDE = "include"; private static final String TAG_WORKSPACE = "workspace"; private static final String TAG_APP_ICON = "appicon"; private static final String TAG_AUTO_INSTALL = "autoinstall"; @@ -100,6 +132,9 @@ public class AutoInstallsLayout { private static final String ATTR_ICON = "icon"; private static final String ATTR_URL = "url"; + // Attrs for "Include" + private static final String ATTR_WORKSPACE = "workspace"; + // Style attrs -- "Extra" private static final String ATTR_KEY = "key"; private static final String ATTR_VALUE = "value"; @@ -110,9 +145,9 @@ public class AutoInstallsLayout { private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE = "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE"; - private final Context mContext; - private final AppWidgetHost mAppWidgetHost; - private final LayoutParserCallback mCallback; + @Thunk final Context mContext; + @Thunk final AppWidgetHost mAppWidgetHost; + protected final LayoutParserCallback mCallback; protected final PackageManager mPackageManager; protected final Resources mSourceRes; @@ -121,14 +156,21 @@ public class AutoInstallsLayout { private final int mHotseatAllAppsRank; private final long[] mTemp = new long[2]; - private final ContentValues mValues; - private final String mRootTag; + @Thunk final ContentValues mValues; + protected final String mRootTag; protected SQLiteDatabase mDb; public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, LayoutParserCallback callback, Resources res, int layoutId, String rootTag) { + this(context, appWidgetHost, callback, res, layoutId, rootTag, + LauncherAppState.getInstance().getInvariantDeviceProfile().hotseatAllAppsRank); + } + + public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost, + LayoutParserCallback callback, Resources res, + int layoutId, String rootTag, int hotseatAllAppsRank) { mContext = context; mAppWidgetHost = appWidgetHost; mCallback = callback; @@ -139,8 +181,7 @@ public class AutoInstallsLayout { mSourceRes = res; mLayoutId = layoutId; - mHotseatAllAppsRank = LauncherAppState.getInstance() - .getDynamicGrid().getDeviceProfile().hotseatAllAppsRank; + mHotseatAllAppsRank = hotseatAllAppsRank; } /** @@ -202,6 +243,17 @@ public class AutoInstallsLayout { HashMap tagParserMap, ArrayList screenIds) throws XmlPullParserException, IOException { + + if (TAG_INCLUDE.equals(parser.getName())) { + final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0); + if (resId != 0) { + // recursively load some more favorites, why not? + return parseLayout(resId, screenIds); + } else { + return 0; + } + } + mValues.clear(); parseContainerAndScreen(parser, mTemp); final long container = mTemp[0]; @@ -528,6 +580,7 @@ public class AutoInstallsLayout { int type; int folderDepth = parser.getDepth(); + int rank = 0; while ((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > folderDepth) { if (type != XmlPullParser.START_TAG) { @@ -535,12 +588,14 @@ public class AutoInstallsLayout { } mValues.clear(); mValues.put(Favorites.CONTAINER, folderId); + mValues.put(Favorites.RANK, rank); TagParser tagParser = mFolderElements.get(parser.getName()); if (tagParser != null) { final long id = tagParser.parseAndAdd(parser); if (id >= 0) { folderItems.add(id); + rank++; } } else { throw new RuntimeException("Invalid folder item " + parser.getName()); @@ -554,7 +609,7 @@ public class AutoInstallsLayout { // failed to add, and less than 2 were actually added if (folderItems.size() < 2) { // Delete the folder - Uri uri = Favorites.getContentUri(folderId, false); + Uri uri = Favorites.getContentUri(folderId); SqlArguments args = new SqlArguments(uri, null, null); mDb.delete(args.table, args.where, args.args); addedId = -1; @@ -627,7 +682,7 @@ public class AutoInstallsLayout { long insertAndCheck(SQLiteDatabase db, ContentValues values); } - private static void copyInteger(ContentValues from, ContentValues to, String key) { + @Thunk static void copyInteger(ContentValues from, ContentValues to, String key) { to.put(key, from.getAsInteger(key)); } } diff --git a/src/com/android/launcher3/BaseContainerView.java b/src/com/android/launcher3/BaseContainerView.java new file mode 100644 index 0000000000..c8de9df10d --- /dev/null +++ b/src/com/android/launcher3/BaseContainerView.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.LinearLayout; + +/** + * A base container view, which supports resizing. + */ +public abstract class BaseContainerView extends LinearLayout implements Insettable { + + private final static String TAG = "BaseContainerView"; + + // The window insets + private Rect mInsets = new Rect(); + // The bounds of the search bar. Only the left, top, right are used to inset the + // search bar and the height is determined by the measurement of the layout + private Rect mFixedSearchBarBounds = new Rect(); + // The bounds of the container + protected Rect mContentBounds = new Rect(); + // The padding to apply to the container to achieve the bounds + protected Rect mContentPadding = new Rect(); + // The inset to apply to the edges and between the search bar and the container + private int mContainerBoundsInset; + private boolean mHasSearchBar; + + public BaseContainerView(Context context) { + this(context, null); + } + + public BaseContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BaseContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mContainerBoundsInset = getResources().getDimensionPixelSize(R.dimen.container_bounds_inset); + } + + @Override + final public void setInsets(Rect insets) { + mInsets.set(insets); + updateBackgroundAndPaddings(); + } + + protected void setHasSearchBar() { + mHasSearchBar = true; + } + + /** + * Sets the search bar bounds for this container view to match. + */ + final public void setSearchBarBounds(Rect bounds) { + if (LauncherAppState.isDogfoodBuild() && !isValidSearchBarBounds(bounds)) { + Log.e(TAG, "Invalid search bar bounds: " + bounds); + } + + mFixedSearchBarBounds.set(bounds); + + // Post the updates since they can trigger a relayout, and this call can be triggered from + // a layout pass itself. + post(new Runnable() { + @Override + public void run() { + updateBackgroundAndPaddings(); + } + }); + } + + /** + * Update the backgrounds and padding in response to a change in the bounds or insets. + */ + protected void updateBackgroundAndPaddings() { + Rect padding; + Rect searchBarBounds = new Rect(mFixedSearchBarBounds); + if (!isValidSearchBarBounds(mFixedSearchBarBounds)) { + // Use the default bounds + padding = new Rect(mInsets.left + mContainerBoundsInset, + (mHasSearchBar ? 0 : (mInsets.top + mContainerBoundsInset)), + mInsets.right + mContainerBoundsInset, + mInsets.bottom + mContainerBoundsInset); + + // Special case -- we have the search bar, but no specific bounds, so just give it + // the inset bounds without a height. + searchBarBounds.set(mInsets.left + mContainerBoundsInset, + mInsets.top + mContainerBoundsInset, + getMeasuredWidth() - (mInsets.right + mContainerBoundsInset), 0); + } else { + // Use the search bounds, if there is a search bar, the bounds will contain + // the offsets for the insets so we can ignore that + padding = new Rect(mFixedSearchBarBounds.left, + (mHasSearchBar ? 0 : (mInsets.top + mContainerBoundsInset)), + getMeasuredWidth() - mFixedSearchBarBounds.right, + mInsets.bottom + mContainerBoundsInset); + } + if (!padding.equals(mContentPadding) || !searchBarBounds.equals(mFixedSearchBarBounds)) { + mContentPadding.set(padding); + mContentBounds.set(padding.left, padding.top, + getMeasuredWidth() - padding.right, + getMeasuredHeight() - padding.bottom); + mFixedSearchBarBounds.set(searchBarBounds); + onUpdateBackgroundAndPaddings(mFixedSearchBarBounds, padding); + } + } + + /** + * To be implemented by container views to update themselves when the bounds changes. + */ + protected abstract void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding); + + /** + * Returns whether the search bar bounds we got are considered valid. + */ + private boolean isValidSearchBarBounds(Rect searchBarBounds) { + return !searchBarBounds.isEmpty() && + searchBarBounds.right <= getMeasuredWidth() && + searchBarBounds.bottom <= getMeasuredHeight(); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java new file mode 100644 index 0000000000..0fae427e86 --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import com.android.launcher3.util.Thunk; + + +/** + * A base {@link RecyclerView}, which does the following: + *
    + *
  • NOT intercept a touch unless the scrolling velocity is below a predefined threshold. + *
  • Enable fast scroller. + *
+ */ +public abstract class BaseRecyclerView extends RecyclerView + implements RecyclerView.OnItemTouchListener { + + private static final int SCROLL_DELTA_THRESHOLD_DP = 4; + + /** Keeps the last known scrolling delta/velocity along y-axis. */ + @Thunk int mDy = 0; + private float mDeltaThreshold; + + /** + * The current scroll state of the recycler view. We use this in onUpdateScrollbar() + * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so + * that we can calculate what the scroll bar looks like, and where to jump to from the fast + * scroller. + */ + public static class ScrollPositionState { + // The index of the first visible row + public int rowIndex; + // The offset of the first visible row + public int rowTopOffset; + // The height of a given row (they are currently all the same height) + public int rowHeight; + } + + protected BaseRecyclerViewFastScrollBar mScrollbar; + + private int mDownX; + private int mDownY; + private int mLastY; + protected Rect mBackgroundPadding = new Rect(); + + public BaseRecyclerView(Context context) { + this(context, null); + } + + public BaseRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; + mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); + + ScrollListener listener = new ScrollListener(); + setOnScrollListener(listener); + } + + private class ScrollListener extends OnScrollListener { + public ScrollListener() { + // Do nothing + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mDy = dy; + + // TODO(winsonc): If we want to animate the section heads while scrolling, we can + // initiate that here if the recycler view scroll state is not + // RecyclerView.SCROLL_STATE_IDLE. + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + /** + * We intercept the touch handling only to support fast scrolling when initiated from the + * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling. + */ + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { + return handleTouchEvent(ev); + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent ev) { + handleTouchEvent(ev); + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + private boolean handleTouchEvent(MotionEvent ev) { + int action = ev.getAction(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + // Keep track of the down positions + mDownX = x; + mDownY = mLastY = y; + if (shouldStopScroll(ev)) { + stopScroll(); + } + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + break; + case MotionEvent.ACTION_MOVE: + mLastY = y; + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onFastScrollCompleted(); + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); + break; + } + return mScrollbar.isDragging(); + } + + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS + } + + /** + * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped. + */ + protected boolean shouldStopScroll(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if ((Math.abs(mDy) < mDeltaThreshold && + getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { + // now the touch events are being passed to the {@link WidgetCell} until the + // touch sequence goes over the touch slop. + return true; + } + } + return false; + } + + public void updateBackgroundPadding(Rect padding) { + mBackgroundPadding.set(padding); + } + + public Rect getBackgroundPadding() { + return mBackgroundPadding; + } + + /** + * Returns the scroll bar width when the user is scrolling. + */ + public int getMaxScrollbarWidth() { + return mScrollbar.getThumbMaxWidth(); + } + + /** + * Returns the available scroll height: + * AvailableScrollHeight = Total height of the all items - last page height + * + * This assumes that all rows are the same height. + * + * @param yOffset the offset from the top of the recycler view to start tracking. + */ + protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) { + int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; + int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom(); + int availableScrollHeight = scrollHeight - visibleHeight; + return availableScrollHeight; + } + + /** + * Returns the available scroll bar height: + * AvailableScrollBarHeight = Total height of the visible view - thumb height + */ + protected int getAvailableScrollBarHeight() { + int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; + int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); + return availableScrollBarHeight; + } + + /** + * Returns the track color (ignoring alpha), can be overridden by each subclass. + */ + public int getFastScrollerTrackColor(int defaultTrackColor) { + return defaultTrackColor; + } + + /** + * Returns the inactive thumb color, can be overridden by each subclass. + */ + public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { + return defaultInactiveThumbColor; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + onUpdateScrollbar(); + mScrollbar.draw(canvas); + } + + /** + * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does + * this by mapping the available scroll area of the recycler view to the available space for the + * scroll bar. + * + * @param scrollPosState the current scroll position + * @param rowCount the number of rows, used to calculate the total scroll height (assumes that + * all rows are the same height) + * @param yOffset the offset to start tracking in the recycler view (only used for all apps) + */ + protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, + int rowCount, int yOffset) { + int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight, + yOffset); + int availableScrollBarHeight = getAvailableScrollBarHeight(); + + // Only show the scrollbar if there is height to be scrolled + if (availableScrollHeight <= 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + // Calculate the current scroll position, the scrollY of the recycler view accounts for the + // view padding, while the scrollBarY is drawn right up to the background padding (ignoring + // padding) + int scrollY = getPaddingTop() + yOffset + + (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int scrollBarY = mBackgroundPadding.top + + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); + + // Calculate the position and size of the scroll bar + int scrollBarX; + if (Utilities.isRtl(getResources())) { + scrollBarX = mBackgroundPadding.left; + } else { + scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getWidth(); + } + mScrollbar.setScrollbarThumbOffset(scrollBarX, scrollBarY); + } + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. + *

Override in each subclass of this base class. + */ + public abstract String scrollToPositionAtProgress(float touchFraction); + + /** + * Updates the bounds for the scrollbar. + *

Override in each subclass of this base class. + */ + public abstract void onUpdateScrollbar(); + + /** + *

Override in each subclass of this base class. + */ + public void onFastScrollCompleted() {} +} \ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java new file mode 100644 index 0000000000..2c4184dc48 --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.launcher3.util.Thunk; + +/** + * The track and scrollbar that shows when you scroll the list. + */ +public class BaseRecyclerViewFastScrollBar { + + public interface FastScrollFocusableView { + void setFastScrollFocused(boolean focused, boolean animated); + } + + private final static int MAX_TRACK_ALPHA = 30; + private final static int SCROLL_BAR_VIS_DURATION = 150; + + @Thunk BaseRecyclerView mRv; + private BaseRecyclerViewFastScrollPopup mPopup; + + private AnimatorSet mScrollbarAnimator; + + private int mThumbInactiveColor; + private int mThumbActiveColor; + @Thunk Point mThumbOffset = new Point(-1, -1); + @Thunk Paint mThumbPaint; + private Paint mTrackPaint; + private int mThumbMinWidth; + private int mThumbMaxWidth; + @Thunk int mThumbWidth; + @Thunk int mThumbHeight; + // The inset is the buffer around which a point will still register as a click on the scrollbar + private int mTouchInset; + private boolean mIsDragging; + + // This is the offset from the top of the scrollbar when the user first starts touching. To + // prevent jumping, this offset is applied as the user scrolls. + private int mTouchOffset; + + private Rect mInvalidateRect = new Rect(); + private Rect mTmpRect = new Rect(); + + public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) { + mRv = rv; + mPopup = new BaseRecyclerViewFastScrollPopup(rv, res); + mTrackPaint = new Paint(); + mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK)); + mTrackPaint.setAlpha(0); + mThumbInactiveColor = rv.getFastScrollerThumbInactiveColor( + res.getColor(R.color.container_fastscroll_thumb_inactive_color)); + mThumbActiveColor = res.getColor(R.color.container_fastscroll_thumb_active_color); + mThumbPaint = new Paint(); + mThumbPaint.setColor(mThumbInactiveColor); + mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width); + mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width); + mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height); + mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset); + } + + public void setScrollbarThumbOffset(int x, int y) { + if (mThumbOffset.x == x && mThumbOffset.y == y) { + return; + } + mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + mThumbOffset.set(x, y); + mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, + mRv.getHeight())); + mRv.invalidate(mInvalidateRect); + } + + // Setter/getter for the search bar width for animations + public void setWidth(int width) { + mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + mThumbWidth = width; + mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, + mRv.getHeight())); + mRv.invalidate(mInvalidateRect); + } + + public int getWidth() { + return mThumbWidth; + } + + // Setter/getter for the track background alpha for animations + public void setTrackAlpha(int alpha) { + mTrackPaint.setAlpha(alpha); + mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + mRv.invalidate(mInvalidateRect); + } + + public int getTrackAlpha() { + return mTrackPaint.getAlpha(); + } + + public int getThumbHeight() { + return mThumbHeight; + } + + public int getThumbMaxWidth() { + return mThumbMaxWidth; + } + + public boolean isDragging() { + return mIsDragging; + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) { + ViewConfiguration config = ViewConfiguration.get(mRv.getContext()); + + int action = ev.getAction(); + int y = (int) ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + if (isNearPoint(downX, downY)) { + mTouchOffset = downY - mThumbOffset.y; + } + break; + case MotionEvent.ACTION_MOVE: + // Check if we should start scrolling + if (!mIsDragging && isNearPoint(downX, downY) && + Math.abs(y - downY) > config.getScaledTouchSlop()) { + mRv.getParent().requestDisallowInterceptTouchEvent(true); + mIsDragging = true; + mTouchOffset += (lastY - downY); + mPopup.animateVisibility(true); + animateScrollbar(true); + } + if (mIsDragging) { + // Update the fastscroller section name at this touch position + int top = mRv.getBackgroundPadding().top; + int bottom = mRv.getHeight() - mRv.getBackgroundPadding().bottom - mThumbHeight; + float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset)); + String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) / + (bottom - top)); + mPopup.setSectionName(sectionName); + mPopup.animateVisibility(!sectionName.isEmpty()); + mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY)); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mTouchOffset = 0; + if (mIsDragging) { + mIsDragging = false; + mPopup.animateVisibility(false); + animateScrollbar(false); + } + break; + } + } + + public void draw(Canvas canvas) { + if (mThumbOffset.x < 0 || mThumbOffset.y < 0) { + return; + } + + // Draw the scroll bar track and thumb + if (mTrackPaint.getAlpha() > 0) { + canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight(), mTrackPaint); + } + canvas.drawRect(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, + mThumbOffset.y + mThumbHeight, mThumbPaint); + + // Draw the popup + mPopup.draw(canvas); + } + + /** + * Animates the width and color of the scrollbar. + */ + private void animateScrollbar(boolean isScrolling) { + if (mScrollbarAnimator != null) { + mScrollbarAnimator.cancel(); + } + ObjectAnimator trackAlphaAnim = ObjectAnimator.ofInt(this, "trackAlpha", + isScrolling ? MAX_TRACK_ALPHA : 0); + ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "width", + isScrolling ? mThumbMaxWidth : mThumbMinWidth); + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), + mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor); + colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + mThumbPaint.setColor((Integer) animator.getAnimatedValue()); + mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, + mThumbOffset.y + mThumbHeight); + } + }); + mScrollbarAnimator = new AnimatorSet(); + mScrollbarAnimator.playTogether(trackAlphaAnim, thumbWidthAnim, colorAnimation); + mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION); + mScrollbarAnimator.start(); + } + + /** + * Returns whether the specified points are near the scroll bar bounds. + */ + private boolean isNearPoint(int x, int y) { + mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, + mThumbOffset.y + mThumbHeight); + mTmpRect.inset(mTouchInset, mTouchInset); + return mTmpRect.contains(x, y); + } +} diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java new file mode 100644 index 0000000000..aeeb5156da --- /dev/null +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +/** + * The fast scroller popup that shows the section name the list will jump to. + */ +public class BaseRecyclerViewFastScrollPopup { + + private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f; + + private Resources mRes; + private BaseRecyclerView mRv; + + private Drawable mBg; + // The absolute bounds of the fast scroller bg + private Rect mBgBounds = new Rect(); + private int mBgOriginalSize; + private Rect mInvalidateRect = new Rect(); + private Rect mTmpRect = new Rect(); + + private String mSectionName; + private Paint mTextPaint; + private Rect mTextBounds = new Rect(); + private float mAlpha; + + private Animator mAlphaAnimator; + private boolean mVisible; + + public BaseRecyclerViewFastScrollPopup(BaseRecyclerView rv, Resources res) { + mRes = res; + mRv = rv; + mBgOriginalSize = res.getDimensionPixelSize(R.dimen.container_fastscroll_popup_size); + mBg = res.getDrawable(R.drawable.container_fastscroll_popup_bg); + mBg.setBounds(0, 0, mBgOriginalSize, mBgOriginalSize); + mTextPaint = new Paint(); + mTextPaint.setColor(Color.WHITE); + mTextPaint.setAntiAlias(true); + mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.container_fastscroll_popup_text_size)); + } + + /** + * Sets the section name. + */ + public void setSectionName(String sectionName) { + if (!sectionName.equals(mSectionName)) { + mSectionName = sectionName; + mTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTextBounds); + // Update the width to use measureText since that is more accurate + mTextBounds.right = (int) (mTextBounds.left + mTextPaint.measureText(sectionName)); + } + } + + /** + * Updates the bounds for the fast scroller. + * @return the invalidation rect for this update. + */ + public Rect updateFastScrollerBounds(BaseRecyclerView rv, int lastTouchY) { + mInvalidateRect.set(mBgBounds); + + if (isVisible()) { + // Calculate the dimensions and position of the fast scroller popup + int edgePadding = rv.getMaxScrollbarWidth(); + int bgPadding = (mBgOriginalSize - mTextBounds.height()) / 2; + int bgHeight = mBgOriginalSize; + int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding)); + if (Utilities.isRtl(mRes)) { + mBgBounds.left = rv.getBackgroundPadding().left + (2 * rv.getMaxScrollbarWidth()); + mBgBounds.right = mBgBounds.left + bgWidth; + } else { + mBgBounds.right = rv.getWidth() - rv.getBackgroundPadding().right - + (2 * rv.getMaxScrollbarWidth()); + mBgBounds.left = mBgBounds.right - bgWidth; + } + mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight); + mBgBounds.top = Math.max(edgePadding, + Math.min(mBgBounds.top, rv.getHeight() - edgePadding - bgHeight)); + mBgBounds.bottom = mBgBounds.top + bgHeight; + } else { + mBgBounds.setEmpty(); + } + + // Combine the old and new fast scroller bounds to create the full invalidate rect + mInvalidateRect.union(mBgBounds); + return mInvalidateRect; + } + + /** + * Animates the visibility of the fast scroller popup. + */ + public void animateVisibility(boolean visible) { + if (mVisible != visible) { + mVisible = visible; + if (mAlphaAnimator != null) { + mAlphaAnimator.cancel(); + } + mAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", visible ? 1f : 0f); + mAlphaAnimator.setDuration(visible ? 200 : 150); + mAlphaAnimator.start(); + } + } + + // Setter/getter for the popup alpha for animations + public void setAlpha(float alpha) { + mAlpha = alpha; + mRv.invalidate(mBgBounds); + } + + public float getAlpha() { + return mAlpha; + } + + public int getHeight() { + return mBgOriginalSize; + } + + public void draw(Canvas c) { + if (isVisible()) { + // Draw the fast scroller popup + int restoreCount = c.save(Canvas.MATRIX_SAVE_FLAG); + c.translate(mBgBounds.left, mBgBounds.top); + mTmpRect.set(mBgBounds); + mTmpRect.offsetTo(0, 0); + mBg.setBounds(mTmpRect); + mBg.setAlpha((int) (mAlpha * 255)); + mBg.draw(c); + mTextPaint.setAlpha((int) (mAlpha * 255)); + c.drawText(mSectionName, (mBgBounds.width() - mTextBounds.width()) / 2, + mBgBounds.height() - (mBgBounds.height() - mTextBounds.height()) / 2, + mTextPaint); + c.restoreToCount(restoreCount); + } + } + + public boolean isVisible() { + return (mAlpha > 0f) && (mSectionName != null); + } +} diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 07f3045a5b..a0be8ea2b2 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -16,6 +16,8 @@ package com.android.launcher3; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -23,22 +25,30 @@ import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Region; import android.graphics.drawable.Drawable; +import android.os.Build; import android.util.AttributeSet; import android.util.SparseArray; import android.util.TypedValue; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import android.widget.TextView; +import com.android.launcher3.IconCache.IconLoadRequest; +import com.android.launcher3.model.PackageItemInfo; /** * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan * because we want to make the bubble taller than the text and TextView's clip is * too aggressive. */ -public class BubbleTextView extends TextView { +public class BubbleTextView extends TextView + implements BaseRecyclerViewFastScrollBar.FastScrollFocusableView { private static SparseArray sPreloaderThemes = new SparseArray(2); @@ -47,25 +57,47 @@ public class BubbleTextView extends TextView { private static final float SHADOW_Y_OFFSET = 2.0f; private static final int SHADOW_LARGE_COLOUR = 0xDD000000; private static final int SHADOW_SMALL_COLOUR = 0xCC000000; - static final float PADDING_V = 3.0f; - private HolographicOutlineHelper mOutlineHelper; + private static final int DISPLAY_WORKSPACE = 0; + private static final int DISPLAY_ALL_APPS = 1; + + private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f; + private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0; + private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1; + private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2; + private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175; + private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125; + + private final Launcher mLauncher; + private Drawable mIcon; + private final Drawable mBackground; + private final CheckLongPressHelper mLongPressHelper; + private final HolographicOutlineHelper mOutlineHelper; + private final StylusEventHelper mStylusEventHelper; + + private boolean mBackgroundSizeChanged; + private Bitmap mPressedBackground; private float mSlop; - private int mTextColor; + private final boolean mDeferShadowGenerationOnTouch; private final boolean mCustomShadowsEnabled; - private boolean mIsTextVisible; - - // TODO: Remove custom background handling code, as no instance of BubbleTextView use any - // background. - private boolean mBackgroundSizeChanged; - private final Drawable mBackground; + private final boolean mLayoutHorizontal; + private final int mIconSize; + private int mTextColor; private boolean mStayPressed; private boolean mIgnorePressedStateChange; - private CheckLongPressHelper mLongPressHelper; + private boolean mDisableRelayout = false; + + private ObjectAnimator mFastScrollFocusAnimator; + private Paint mFastScrollFocusBgPaint; + private float mFastScrollFocusFraction; + private boolean mFastScrollFocused; + private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON; + + private IconLoadRequest mIconLoadRequest; public BubbleTextView(Context context) { this(context, null, 0); @@ -77,10 +109,28 @@ public class BubbleTextView extends TextView { public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mLauncher = (Launcher) context; + DeviceProfile grid = mLauncher.getDeviceProfile(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BubbleTextView, defStyle, 0); mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true); + mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false); + mDeferShadowGenerationOnTouch = + a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false); + + int display = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE); + int defaultIconSize = grid.iconSizePx; + if (display == DISPLAY_WORKSPACE) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); + } else if (display == DISPLAY_ALL_APPS) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx); + defaultIconSize = grid.allAppsIconSizePx; + } + + mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride, + defaultIconSize); + a.recycle(); if (mCustomShadowsEnabled) { @@ -90,45 +140,37 @@ public class BubbleTextView extends TextView { } else { mBackground = null; } - init(); - } - public void onFinishInflate() { - super.onFinishInflate(); - - // Ensure we are using the right text size - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); - } - - private void init() { mLongPressHelper = new CheckLongPressHelper(this); + mStylusEventHelper = new StylusEventHelper(this); mOutlineHelper = HolographicOutlineHelper.obtain(getContext()); if (mCustomShadowsEnabled) { setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); } + + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG) { + mFastScrollFocusBgPaint = new Paint(); + mFastScrollFocusBgPaint.setAntiAlias(true); + mFastScrollFocusBgPaint.setColor( + getResources().getColor(R.color.container_fastscroll_thumb_active_color)); + } + + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); + } + + public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) { + applyFromShortcutInfo(info, iconCache, false); } public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, - boolean setDefaultPadding) { - applyFromShortcutInfo(info, iconCache, setDefaultPadding, false); - } - - public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache, - boolean setDefaultPadding, boolean promiseStateChanged) { + boolean promiseStateChanged) { Bitmap b = info.getIcon(iconCache); - LauncherAppState app = LauncherAppState.getInstance(); - FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b); + FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b); iconDrawable.setGhostModeEnabled(info.isDisabled != 0); - setCompoundDrawables(null, iconDrawable, null, null); - if (setDefaultPadding) { - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - setCompoundDrawablePadding(grid.iconDrawablePaddingPx); - } + setIcon(iconDrawable, mIconSize); if (info.contentDescription != null) { setContentDescription(info.contentDescription); } @@ -141,20 +183,37 @@ public class BubbleTextView extends TextView { } public void applyFromApplicationInfo(AppInfo info) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - Drawable topDrawable = Utilities.createIconDrawable(info.iconBitmap); - topDrawable.setBounds(0, 0, grid.allAppsIconSizePx, grid.allAppsIconSizePx); - setCompoundDrawables(null, topDrawable, null, null); - setCompoundDrawablePadding(grid.iconDrawablePaddingPx); + setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize); setText(info.title); if (info.contentDescription != null) { setContentDescription(info.contentDescription); } - setTag(info); + // We don't need to check the info since it's not a ShortcutInfo + super.setTag(info); + + // Verify high res immediately + verifyHighRes(); } + public void applyFromPackageItemInfo(PackageItemInfo info) { + setIcon(mLauncher.createIconDrawable(info.iconBitmap), mIconSize); + setText(info.title); + if (info.contentDescription != null) { + setContentDescription(info.contentDescription); + } + // We don't need to check the info since it's not a ShortcutInfo + super.setTag(info); + + // Verify high res immediately + verifyHighRes(); + } + + /** + * Overrides the default long press timeout. + */ + public void setLongPressTimeout(int longPressTimeout) { + mLongPressHelper.setLongPressTimeout(longPressTimeout); + } @Override protected boolean setFrame(int left, int top, int right, int bottom) { @@ -186,10 +245,19 @@ public class BubbleTextView extends TextView { } } + /** Returns the icon for this view. */ + public Drawable getIcon() { + return mIcon; + } + + /** Returns whether the layout is horizontal. */ + public boolean isLayoutHorizontal() { + return mLayoutHorizontal; + } + private void updateIconState() { - Drawable top = getCompoundDrawables()[1]; - if (top instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) top).setPressed(isPressed() || mStayPressed); + if (mIcon instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed); } } @@ -199,16 +267,25 @@ public class BubbleTextView extends TextView { // isPressed() on an ACTION_UP boolean result = super.onTouchEvent(event); + // Check for a stylus button press, if it occurs cancel any long press checks. + if (mStylusEventHelper.checkAndPerformStylusEvent(event)) { + mLongPressHelper.cancelLongPress(); + result = true; + } + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // So that the pressed outline is visible immediately on setStayPressed(), // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time // to create it) - if (mPressedBackground == null) { + if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) { mPressedBackground = mOutlineHelper.createMediumDropShadow(this); } - mLongPressHelper.postCheckForLongPress(); + // If we're in a stylus button press, don't check for long press. + if (!mStylusEventHelper.inStylusButtonPressed()) { + mLongPressHelper.postCheckForLongPress(); + } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: @@ -233,12 +310,17 @@ public class BubbleTextView extends TextView { mStayPressed = stayPressed; if (!stayPressed) { mPressedBackground = null; + } else { + if (mPressedBackground == null) { + mPressedBackground = mOutlineHelper.createMediumDropShadow(this); + } } // Only show the shadow effect when persistent pressed state is set. - if (getParent() instanceof ShortcutAndWidgetContainer) { - CellLayout layout = (CellLayout) getParent().getParent(); - layout.setPressedIcon(this, mPressedBackground, mOutlineHelper.shadowBitmapPadding); + ViewParent parent = getParent(); + if (parent != null && parent.getParent() instanceof BubbleTextShadowHandler) { + ((BubbleTextShadowHandler) parent.getParent()).setPressedIcon( + this, mPressedBackground); } updateIconState(); @@ -278,7 +360,18 @@ public class BubbleTextView extends TextView { @Override public void draw(Canvas canvas) { if (!mCustomShadowsEnabled) { + // Draw the fast scroll focus bg if we have one + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG && + mFastScrollFocusFraction > 0f) { + DeviceProfile grid = mLauncher.getDeviceProfile(); + int iconCenterX = getScrollX() + (getWidth() / 2); + int iconCenterY = getScrollY() + getPaddingTop() + (grid.iconSizePx / 2); + canvas.drawCircle(iconCenterX, iconCenterY, + mFastScrollFocusFraction * (getWidth() / 2), mFastScrollFocusBgPaint); + } + super.draw(canvas); + return; } @@ -325,10 +418,9 @@ public class BubbleTextView extends TextView { super.onAttachedToWindow(); if (mBackground != null) mBackground.setCallback(this); - Drawable top = getCompoundDrawables()[1]; - if (top instanceof PreloadIconDrawable) { - ((PreloadIconDrawable) top).applyTheme(getPreloaderTheme()); + if (mIcon instanceof PreloadIconDrawable) { + ((PreloadIconDrawable) mIcon).applyPreloaderTheme(getPreloaderTheme()); } mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @@ -358,16 +450,6 @@ public class BubbleTextView extends TextView { } else { super.setTextColor(res.getColor(android.R.color.transparent)); } - mIsTextVisible = visible; - } - - public boolean isTextVisible() { - return mIsTextVisible; - } - - @Override - protected boolean onSetAlpha(int alpha) { - return true; } @Override @@ -385,15 +467,13 @@ public class BubbleTextView extends TextView { ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ? info.getInstallProgress() : 0)) : 100; - Drawable[] drawables = getCompoundDrawables(); - Drawable top = drawables[1]; - if (top != null) { + if (mIcon != null) { final PreloadIconDrawable preloadDrawable; - if (top instanceof PreloadIconDrawable) { - preloadDrawable = (PreloadIconDrawable) top; + if (mIcon instanceof PreloadIconDrawable) { + preloadDrawable = (PreloadIconDrawable) mIcon; } else { - preloadDrawable = new PreloadIconDrawable(top, getPreloaderTheme()); - setCompoundDrawables(drawables[0], preloadDrawable, drawables[2], drawables[3]); + preloadDrawable = new PreloadIconDrawable(mIcon, getPreloaderTheme()); + setIcon(preloadDrawable, mIconSize); } preloadDrawable.setLevel(progressLevel); @@ -417,4 +497,132 @@ public class BubbleTextView extends TextView { } return theme; } + + /** + * Sets the icon for this view based on the layout direction. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private Drawable setIcon(Drawable icon, int iconSize) { + mIcon = icon; + if (iconSize != -1) { + mIcon.setBounds(0, 0, iconSize, iconSize); + } + if (mLayoutHorizontal) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + setCompoundDrawablesRelative(mIcon, null, null, null); + } else { + setCompoundDrawables(mIcon, null, null, null); + } + } else { + setCompoundDrawables(null, mIcon, null, null); + } + return icon; + } + + @Override + public void requestLayout() { + if (!mDisableRelayout) { + super.requestLayout(); + } + } + + /** + * Applies the item info if it is same as what the view is pointing to currently. + */ + public void reapplyItemInfo(final ItemInfo info) { + if (getTag() == info) { + mIconLoadRequest = null; + mDisableRelayout = true; + if (info instanceof AppInfo) { + applyFromApplicationInfo((AppInfo) info); + } else if (info instanceof ShortcutInfo) { + applyFromShortcutInfo((ShortcutInfo) info, + LauncherAppState.getInstance().getIconCache()); + } else if (info instanceof PackageItemInfo) { + applyFromPackageItemInfo((PackageItemInfo) info); + } + mDisableRelayout = false; + } + } + + /** + * Verifies that the current icon is high-res otherwise posts a request to load the icon. + */ + public void verifyHighRes() { + if (mIconLoadRequest != null) { + mIconLoadRequest.cancel(); + mIconLoadRequest = null; + } + if (getTag() instanceof AppInfo) { + AppInfo info = (AppInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } + } else if (getTag() instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } + } else if (getTag() instanceof PackageItemInfo) { + PackageItemInfo info = (PackageItemInfo) getTag(); + if (info.usingLowResIcon) { + mIconLoadRequest = LauncherAppState.getInstance().getIconCache() + .updateIconInBackground(BubbleTextView.this, info); + } + } + } + + // Setters & getters for the animation + public void setFastScrollFocus(float fraction) { + mFastScrollFocusFraction = fraction; + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) { + setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); + setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f)); + } else { + invalidate(); + } + } + + public float getFastScrollFocus() { + return mFastScrollFocusFraction; + } + + @Override + public void setFastScrollFocused(final boolean focused, boolean animated) { + if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) { + return; + } + + if (mFastScrollFocused != focused) { + mFastScrollFocused = focused; + + if (animated) { + // Clean up the previous focus animator + if (mFastScrollFocusAnimator != null) { + mFastScrollFocusAnimator.cancel(); + } + mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus", + focused ? 1f : 0f); + if (focused) { + mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator()); + } else { + mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator()); + } + mFastScrollFocusAnimator.setDuration(focused ? + FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION); + mFastScrollFocusAnimator.start(); + } else { + mFastScrollFocusFraction = focused ? 1f : 0f; + } + } + } + + /** + * Interface to be implemented by the grand parent to allow click shadow effect. + */ + public static interface BubbleTextShadowHandler { + void setPressedIcon(BubbleTextView icon, Bitmap background); + } } diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index 019f86c217..b7f89d02a8 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -16,26 +16,41 @@ package com.android.launcher3; +import android.animation.AnimatorSet; +import android.animation.FloatArrayEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.Context; -import android.content.res.Resources; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Build; import android.util.AttributeSet; import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; import android.widget.TextView; +import com.android.launcher3.util.Thunk; /** * Implements a DropTarget. */ -public class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener { +public abstract class ButtonDropTarget extends TextView + implements DropTarget, DragController.DragListener, OnClickListener { - protected final int mTransitionDuration; + private static int DRAG_VIEW_DROP_DURATION = 285; protected Launcher mLauncher; private int mBottomDragPadding; - protected TextView mText; protected SearchDropTargetBar mSearchDropTargetBar; /** Whether this drop target is active for the current drag */ @@ -44,71 +59,196 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro /** The paint applied to the drag view on hover */ protected int mHoverColor = 0; + protected ColorStateList mOriginalTextColor; + protected Drawable mDrawable; + + private AnimatorSet mCurrentColorAnim; + @Thunk ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter; + + public ButtonDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - - Resources r = getResources(); - mTransitionDuration = r.getInteger(R.integer.config_dropTargetBgTransitionDuration); - mBottomDragPadding = r.getDimensionPixelSize(R.dimen.drop_target_drag_padding); + mBottomDragPadding = getResources().getDimensionPixelSize(R.dimen.drop_target_drag_padding); } - void setLauncher(Launcher launcher) { + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mOriginalTextColor = getTextColors(); + + // Remove the text in the Phone UI in landscape + DeviceProfile grid = ((Launcher) getContext()).getDeviceProfile(); + if (grid.isVerticalBarLayout()) { + setText(""); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + protected void setDrawable(int resId) { + // We do not set the drawable in the xml as that inflates two drawables corresponding to + // drawableLeft and drawableStart. + mDrawable = getResources().getDrawable(resId); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null); + } else { + setCompoundDrawablesWithIntrinsicBounds(mDrawable, null, null, null); + } + } + + public void setLauncher(Launcher launcher) { mLauncher = launcher; } - public boolean acceptDrop(DragObject d) { - return false; - } - public void setSearchDropTargetBar(SearchDropTargetBar searchDropTargetBar) { mSearchDropTargetBar = searchDropTargetBar; } - protected Drawable getCurrentDrawable() { - Drawable[] drawables = getCompoundDrawablesRelative(); - for (int i = 0; i < drawables.length; ++i) { - if (drawables[i] != null) { - return drawables[i]; - } - } - return null; - } + @Override + public void onFlingToDelete(DragObject d, PointF vec) { } - public void onDrop(DragObject d) { - } - - public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { - // Do nothing - } - - public void onDragEnter(DragObject d) { + @Override + public final void onDragEnter(DragObject d) { d.dragView.setColor(mHoverColor); + if (Utilities.isLmpOrAbove()) { + animateTextColor(mHoverColor); + } else { + if (mCurrentFilter == null) { + mCurrentFilter = new ColorMatrix(); + } + DragView.setColorScale(mHoverColor, mCurrentFilter); + mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter)); + setTextColor(mHoverColor); + } } + @Override public void onDragOver(DragObject d) { // Do nothing } - public void onDragExit(DragObject d) { - d.dragView.setColor(0); + protected void resetHoverColor() { + if (Utilities.isLmpOrAbove()) { + animateTextColor(mOriginalTextColor.getDefaultColor()); + } else { + mDrawable.setColorFilter(null); + setTextColor(mOriginalTextColor); + } } - public void onDragStart(DragSource source, Object info, int dragAction) { - // Do nothing + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void animateTextColor(int targetColor) { + if (mCurrentColorAnim != null) { + mCurrentColorAnim.cancel(); + } + + mCurrentColorAnim = new AnimatorSet(); + mCurrentColorAnim.setDuration(DragView.COLOR_CHANGE_DURATION); + + if (mSrcFilter == null) { + mSrcFilter = new ColorMatrix(); + mDstFilter = new ColorMatrix(); + mCurrentFilter = new ColorMatrix(); + } + + DragView.setColorScale(getTextColor(), mSrcFilter); + DragView.setColorScale(targetColor, mDstFilter); + ValueAnimator anim1 = ValueAnimator.ofObject( + new FloatArrayEvaluator(mCurrentFilter.getArray()), + mSrcFilter.getArray(), mDstFilter.getArray()); + anim1.addUpdateListener(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter)); + invalidate(); + } + }); + + mCurrentColorAnim.play(anim1); + mCurrentColorAnim.play(ObjectAnimator.ofArgb(this, "textColor", targetColor)); + mCurrentColorAnim.start(); } + @Override + public final void onDragExit(DragObject d) { + if (!d.dragComplete) { + d.dragView.setColor(0); + resetHoverColor(); + } else { + // Restore the hover color + d.dragView.setColor(mHoverColor); + } + } + + @Override + public final void onDragStart(DragSource source, Object info, int dragAction) { + mActive = supportsDrop(source, info); + mDrawable.setColorFilter(null); + if (mCurrentColorAnim != null) { + mCurrentColorAnim.cancel(); + mCurrentColorAnim = null; + } + setTextColor(mOriginalTextColor); + ((ViewGroup) getParent()).setVisibility(mActive ? View.VISIBLE : View.GONE); + } + + @Override + public final boolean acceptDrop(DragObject dragObject) { + return supportsDrop(dragObject.dragSource, dragObject.dragInfo); + } + + protected abstract boolean supportsDrop(DragSource source, Object info); + + @Override public boolean isDropEnabled() { return mActive; } + @Override public void onDragEnd() { - // Do nothing + mActive = false; } + /** + * On drop animate the dropView to the icon. + */ + @Override + public void onDrop(final DragObject d) { + final DragLayer dragLayer = mLauncher.getDragLayer(); + final Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + + int width = mDrawable.getIntrinsicWidth(); + int height = mDrawable.getIntrinsicHeight(); + final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + width, height); + final float scale = (float) to.width() / from.width(); + mSearchDropTargetBar.deferOnDragEnd(); + + Runnable onAnimationEndRunnable = new Runnable() { + @Override + public void run() { + completeDrop(d); + mSearchDropTargetBar.onDragEnd(); + mLauncher.exitSpringLoadedDragModeDelayed(true, 0, null); + } + }; + dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, + DRAG_VIEW_DROP_DURATION, new DecelerateInterpolator(2), + new LinearInterpolator(), onAnimationEndRunnable, + DragLayer.ANIMATION_END_DISAPPEAR, null); + } + + @Override + public void prepareAccessibilityDrop() { } + + @Thunk abstract void completeDrop(DragObject d); + @Override public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) { super.getHitRect(outRect); @@ -119,11 +259,7 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro outRect.offsetTo(coords[0], coords[1]); } - private boolean isRtl() { - return (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - } - - Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) { + protected Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) { DragLayer dragLayer = mLauncher.getDragLayer(); // Find the rect to animate to (the view is center aligned) @@ -136,7 +272,7 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro final int left; final int right; - if (isRtl()) { + if (Utilities.isRtl(getResources())) { right = to.right - getPaddingRight(); left = right - width; } else { @@ -157,7 +293,26 @@ public class ButtonDropTarget extends TextView implements DropTarget, DragContro return to; } + @Override public void getLocationInDragLayer(int[] loc) { mLauncher.getDragLayer().getLocationInDragLayer(this, loc); } + + public void enableAccessibleDrag(boolean enable) { + setOnClickListener(enable ? this : null); + } + + protected String getAccessibilityDropConfirmation() { + return null; + } + + @Override + public void onClick(View v) { + LauncherAppState.getInstance().getAccessibilityDelegate() + .handleAccessibleDrop(this, null, getAccessibilityDropConfirmation()); + } + + public int getTextColor() { + return getTextColors().getDefaultColor(); + } } diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 0ff1ef4adf..8096887128 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -22,6 +22,7 @@ import android.animation.AnimatorSet; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -33,7 +34,10 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.os.Build; import android.os.Parcelable; +import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -41,11 +45,16 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.animation.Animation; +import android.view.accessibility.AccessibilityEvent; import android.view.animation.DecelerateInterpolator; -import android.view.animation.LayoutAnimationController; +import com.android.launcher3.BubbleTextView.BubbleTextShadowHandler; import com.android.launcher3.FolderIcon.FolderRingAnimator; +import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; +import com.android.launcher3.accessibility.FolderAccessibilityHelper; +import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.PendingAddWidgetInfo; import java.util.ArrayList; import java.util.Arrays; @@ -54,55 +63,47 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Stack; -public class CellLayout extends ViewGroup { +public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { + public static final int WORKSPACE_ACCESSIBILITY_DRAG = 2; + public static final int FOLDER_ACCESSIBILITY_DRAG = 1; + static final String TAG = "CellLayout"; private Launcher mLauncher; - private int mCellWidth; - private int mCellHeight; + @Thunk int mCellWidth; + @Thunk int mCellHeight; private int mFixedCellWidth; private int mFixedCellHeight; - private int mCountX; - private int mCountY; + @Thunk int mCountX; + @Thunk int mCountY; private int mOriginalWidthGap; private int mOriginalHeightGap; - private int mWidthGap; - private int mHeightGap; + @Thunk int mWidthGap; + @Thunk int mHeightGap; private int mMaxGap; private boolean mDropPending = false; private boolean mIsDragTarget = true; // These are temporary variables to prevent having to allocate a new object just to // return an (x, y) value from helper functions. Do NOT use them to maintain other state. - private final int[] mTmpXY = new int[2]; - private final int[] mTmpPoint = new int[2]; - int[] mTempLocation = new int[2]; + @Thunk final int[] mTmpPoint = new int[2]; + @Thunk final int[] mTempLocation = new int[2]; boolean[][] mOccupied; boolean[][] mTmpOccupied; - private boolean mLastDownOnOccupiedCell = false; private OnTouchListener mInterceptTouchListener; + private StylusEventHelper mStylusEventHelper; private ArrayList mFolderOuterRings = new ArrayList(); private int[] mFolderLeaveBehindCell = {-1, -1}; - private float FOREGROUND_ALPHA_DAMPER = 0.65f; - private int mForegroundAlpha = 0; private float mBackgroundAlpha; - private float mBackgroundAlphaMultiplier = 1.0f; - private boolean mDrawBackground = true; - private Drawable mNormalBackground; - private Drawable mActiveGlowBackground; - private Drawable mOverScrollForegroundDrawable; - private Drawable mOverScrollLeft; - private Drawable mOverScrollRight; - private Rect mBackgroundRect; - private Rect mForegroundRect; - private int mForegroundPadding; + private static final int BACKGROUND_ACTIVATE_DURATION = 120; + private final TransitionDrawable mBackground; // These values allow a fixed measurement to be set on the CellLayout. private int mFixedWidth = -1; @@ -110,12 +111,11 @@ public class CellLayout extends ViewGroup { // If we're actively dragging something over this screen, mIsDragOverlapping is true private boolean mIsDragOverlapping = false; - boolean mUseActiveGlowBackground = false; // These arrays are used to implement the drag visualization on x-large screens. // They are used as circular arrays, indexed by mDragOutlineCurrent. - private Rect[] mDragOutlines = new Rect[4]; - private float[] mDragOutlineAlphas = new float[mDragOutlines.length]; + @Thunk Rect[] mDragOutlines = new Rect[4]; + @Thunk float[] mDragOutlineAlphas = new float[mDragOutlines.length]; private InterruptibleInOutAnimator[] mDragOutlineAnims = new InterruptibleInOutAnimator[mDragOutlines.length]; @@ -123,12 +123,10 @@ public class CellLayout extends ViewGroup { private int mDragOutlineCurrent = 0; private final Paint mDragOutlinePaint = new Paint(); - private final FastBitmapView mTouchFeedbackView; + private final ClickShadowView mTouchFeedbackView; - private HashMap mReorderAnimators = new - HashMap(); - private HashMap - mShakeAnimators = new HashMap(); + @Thunk HashMap mReorderAnimators = new HashMap<>(); + @Thunk HashMap mShakeAnimators = new HashMap<>(); private boolean mItemPlacementDirty = false; @@ -156,19 +154,22 @@ public class CellLayout extends ViewGroup { private static final float REORDER_PREVIEW_MAGNITUDE = 0.12f; private static final int REORDER_ANIMATION_DURATION = 150; - private float mReorderPreviewAnimationMagnitude; + @Thunk float mReorderPreviewAnimationMagnitude; private ArrayList mIntersectingViews = new ArrayList(); private Rect mOccupiedRect = new Rect(); private int[] mDirectionVector = new int[2]; int[] mPreviousReorderDirection = new int[2]; private static final int INVALID_DIRECTION = -100; - private DropTarget.DragEnforcer mDragEnforcer; - private Rect mTempRect = new Rect(); + private final Rect mTempRect = new Rect(); private final static Paint sPaint = new Paint(); + // Related to accessible drag and drop + private DragAndDropAccessibilityDelegate mTouchHelper; + private boolean mUseTouchHelper = false; + public CellLayout(Context context) { this(context, null); } @@ -179,7 +180,6 @@ public class CellLayout extends ViewGroup { public CellLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mDragEnforcer = new DropTarget.DragEnforcer(context); // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show // the user where a dragged item will land when dropped. @@ -187,8 +187,7 @@ public class CellLayout extends ViewGroup { setClipToPadding(false); mLauncher = (Launcher) context; - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); mCellWidth = mCellHeight = -1; @@ -196,8 +195,8 @@ public class CellLayout extends ViewGroup { mWidthGap = mOriginalWidthGap = 0; mHeightGap = mOriginalHeightGap = 0; mMaxGap = Integer.MAX_VALUE; - mCountX = (int) grid.numColumns; - mCountY = (int) grid.numRows; + mCountX = (int) grid.inv.numColumns; + mCountY = (int) grid.inv.numRows; mOccupied = new boolean[mCountX][mCountY]; mTmpOccupied = new boolean[mCountX][mCountY]; mPreviousReorderDirection[0] = INVALID_DIRECTION; @@ -210,20 +209,12 @@ public class CellLayout extends ViewGroup { final Resources res = getResources(); mHotseatScale = (float) grid.hotseatIconSizePx / grid.iconSizePx; - mNormalBackground = res.getDrawable(R.drawable.screenpanel); - mActiveGlowBackground = res.getDrawable(R.drawable.screenpanel_hover); - - mOverScrollLeft = res.getDrawable(R.drawable.overscroll_glow_left); - mOverScrollRight = res.getDrawable(R.drawable.overscroll_glow_right); - mForegroundPadding = - res.getDimensionPixelSize(R.dimen.workspace_overscroll_drawable_padding); + mBackground = (TransitionDrawable) res.getDrawable(R.drawable.bg_screenpanel); + mBackground.setCallback(this); mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * grid.iconSizePx); - mNormalBackground.setFilterBitmap(true); - mActiveGlowBackground.setFilterBitmap(true); - // Initialize the data structures used for the drag visualization. mEaseOutInterpolator = new DecelerateInterpolator(2.5f); // Quint ease out mDragCell[0] = mDragCell[1] = -1; @@ -281,19 +272,78 @@ public class CellLayout extends ViewGroup { mDragOutlineAnims[i] = anim; } - mBackgroundRect = new Rect(); - mForegroundRect = new Rect(); - mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context); mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, mCountX, mCountY); - mTouchFeedbackView = new FastBitmapView(context); - // Make the feedback view large enough to hold the blur bitmap. - addView(mTouchFeedbackView, (int) (grid.cellWidthPx * 1.5), (int) (grid.cellHeightPx * 1.5)); + mStylusEventHelper = new StylusEventHelper(this); + + mTouchFeedbackView = new ClickShadowView(context); + addView(mTouchFeedbackView); addView(mShortcutsAndWidgets); } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void enableAccessibleDrag(boolean enable, int dragType) { + mUseTouchHelper = enable; + if (!enable) { + ViewCompat.setAccessibilityDelegate(this, null); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + setOnClickListener(mLauncher); + } else { + if (dragType == WORKSPACE_ACCESSIBILITY_DRAG && + !(mTouchHelper instanceof WorkspaceAccessibilityHelper)) { + mTouchHelper = new WorkspaceAccessibilityHelper(this); + } else if (dragType == FOLDER_ACCESSIBILITY_DRAG && + !(mTouchHelper instanceof FolderAccessibilityHelper)) { + mTouchHelper = new FolderAccessibilityHelper(this); + } + ViewCompat.setAccessibilityDelegate(this, mTouchHelper); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + setOnClickListener(mTouchHelper); + } + + // Invalidate the accessibility hierarchy + if (getParent() != null) { + getParent().notifySubtreeAccessibilityStateChanged( + this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); + } + } + + @Override + public boolean dispatchHoverEvent(MotionEvent event) { + // Always attempt to dispatch hover events to accessibility first. + if (mUseTouchHelper && mTouchHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mUseTouchHelper || + (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) { + return true; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean handled = super.onTouchEvent(ev); + // Stylus button press on a home screen should not switch between overview mode and + // the home screen mode, however, once in overview mode stylus button press should be + // enabled to allow rearranging the different home screens. So check what mode + // the workspace is in, and only perform stylus button presses while in overview mode. + if (mLauncher.mWorkspace.isInOverviewMode() + && mStylusEventHelper.checkAndPerformStylusEvent(ev)) { + return true; + } + return handled; + } + public void enableHardwareLayer(boolean hasLayer) { mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint); } @@ -337,47 +387,19 @@ public class CellLayout extends ViewGroup { return mDropPending; } - void setOverScrollAmount(float r, boolean left) { - if (left && mOverScrollForegroundDrawable != mOverScrollLeft) { - mOverScrollForegroundDrawable = mOverScrollLeft; - } else if (!left && mOverScrollForegroundDrawable != mOverScrollRight) { - mOverScrollForegroundDrawable = mOverScrollRight; - } - - r *= FOREGROUND_ALPHA_DAMPER; - mForegroundAlpha = (int) Math.round((r * 255)); - mOverScrollForegroundDrawable.setAlpha(mForegroundAlpha); - invalidate(); - } - - void setPressedIcon(BubbleTextView icon, Bitmap background, int padding) { + @Override + public void setPressedIcon(BubbleTextView icon, Bitmap background) { if (icon == null || background == null) { mTouchFeedbackView.setBitmap(null); mTouchFeedbackView.animate().cancel(); } else { - int offset = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - - (mCountX * mCellWidth); - mTouchFeedbackView.setTranslationX(icon.getLeft() + (int) Math.ceil(offset / 2f) - - padding); - mTouchFeedbackView.setTranslationY(icon.getTop() - padding); if (mTouchFeedbackView.setBitmap(background)) { - mTouchFeedbackView.setAlpha(0); - mTouchFeedbackView.animate().alpha(1) - .setDuration(FastBitmapDrawable.CLICK_FEEDBACK_DURATION) - .setInterpolator(FastBitmapDrawable.CLICK_FEEDBACK_INTERPOLATOR) - .start(); + mTouchFeedbackView.alignWithIconView(icon, mShortcutsAndWidgets); + mTouchFeedbackView.animateShadow(); } } } - void setUseActiveGlowBackground(boolean use) { - mUseActiveGlowBackground = use; - } - - void disableBackground() { - mDrawBackground = false; - } - void disableDragTarget() { mIsDragTarget = false; } @@ -389,7 +411,11 @@ public class CellLayout extends ViewGroup { void setIsDragOverlapping(boolean isDragOverlapping) { if (mIsDragOverlapping != isDragOverlapping) { mIsDragOverlapping = isDragOverlapping; - setUseActiveGlowBackground(mIsDragOverlapping); + if (mIsDragOverlapping) { + mBackground.startTransition(BACKGROUND_ACTIVATE_DURATION); + } else { + mBackground.reverseTransition(BACKGROUND_ACTIVATE_DURATION); + } invalidate(); } } @@ -400,24 +426,17 @@ public class CellLayout extends ViewGroup { @Override protected void onDraw(Canvas canvas) { + if (!mIsDragTarget) { + return; + } + // When we're large, we are either drawn in a "hover" state (ie when dragging an item to // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f) // When we're small, we are either drawn normally or in the "accepts drops" state (during // a drag). However, we also drag the mini hover background *over* one of those two // backgrounds - if (mDrawBackground && mBackgroundAlpha > 0.0f) { - Drawable bg; - - if (mUseActiveGlowBackground) { - // In the mini case, we draw the active_glow bg *over* the active background - bg = mActiveGlowBackground; - } else { - bg = mNormalBackground; - } - - bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255)); - bg.setBounds(mBackgroundRect); - bg.draw(canvas); + if (mBackgroundAlpha > 0.0f) { + mBackground.draw(canvas); } final Paint paint = mDragOutlinePaint; @@ -453,8 +472,7 @@ public class CellLayout extends ViewGroup { int previewOffset = FolderRingAnimator.sPreviewSize; // The folder outer / inner ring image(s) - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); for (int i = 0; i < mFolderOuterRings.size(); i++) { FolderRingAnimator fra = mFolderOuterRings.get(i); @@ -513,15 +531,6 @@ public class CellLayout extends ViewGroup { } } - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - if (mForegroundAlpha > 0) { - mOverScrollForegroundDrawable.setBounds(mForegroundRect); - mOverScrollForegroundDrawable.draw(canvas); - } - } - public void showFolderAccept(FolderRingAnimator fra) { mFolderOuterRings.add(fra); } @@ -578,11 +587,11 @@ public class CellLayout extends ViewGroup { mInterceptTouchListener = listener; } - int getCountX() { + public int getCountX() { return mCountX; } - int getCountY() { + public int getCountY() { return mCountY; } @@ -591,6 +600,10 @@ public class CellLayout extends ViewGroup { mShortcutsAndWidgets.setIsHotseat(isHotseat); } + public boolean isHotseat() { + return mIsHotseat; + } + public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params, boolean markCells) { final LayoutParams lp = params; @@ -613,7 +626,6 @@ public class CellLayout extends ViewGroup { if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; child.setId(childId); - mShortcutsAndWidgets.addView(child, index, lp); if (markCells) markCellsAsOccupiedForView(child); @@ -637,10 +649,6 @@ public class CellLayout extends ViewGroup { } } - public void removeViewWithoutMarkingCells(View view) { - mShortcutsAndWidgets.removeView(view); - } - @Override public void removeView(View view) { markCellsAsUnoccupiedForView(view); @@ -675,25 +683,13 @@ public class CellLayout extends ViewGroup { mShortcutsAndWidgets.removeViewsInLayout(start, count); } - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - // First we clear the tag to ensure that on every touch down we start with a fresh slate, - // even in the case where we return early. Not clearing here was causing bugs whereby on - // long-press we'd end up picking up an item from a previous drag operation. - if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) { - return true; - } - - return false; - } - /** * Given a point, return the cell that strictly encloses that point * @param x X coordinate of the point * @param y Y coordinate of the point * @param result Array of 2 ints to hold the x and y coordinate of the cell */ - void pointToCellExact(int x, int y, int[] result) { + public void pointToCellExact(int x, int y, int[] result) { final int hStartPadding = getPaddingLeft(); final int vStartPadding = getPaddingTop(); @@ -782,9 +778,7 @@ public class CellLayout extends ViewGroup { public float getDistanceFromCell(float x, float y, int[] cell) { cellToCenterPoint(cell[0], cell[1], mTmpPoint); - float distance = (float) Math.sqrt( Math.pow(x - mTmpPoint[0], 2) + - Math.pow(y - mTmpPoint[1], 2)); - return distance; + return (float) Math.hypot(x - mTmpPoint[0], y - mTmpPoint[1]); } int getCellWidth() { @@ -803,28 +797,6 @@ public class CellLayout extends ViewGroup { return mHeightGap; } - Rect getContentRect(Rect r) { - if (r == null) { - r = new Rect(); - } - int left = getPaddingLeft(); - int top = getPaddingTop(); - int right = left + getWidth() - getPaddingLeft() - getPaddingRight(); - int bottom = top + getHeight() - getPaddingTop() - getPaddingBottom(); - r.set(left, top, right, bottom); - return r; - } - - /** Return a rect that has the cellWidth/cellHeight (left, top), and - * widthGap/heightGap (right, bottom) */ - static void getMetrics(Rect metrics, int paddedMeasureWidth, - int paddedMeasureHeight, int countX, int countY) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - metrics.set(grid.calculateCellWidth(paddedMeasureWidth, countX), - grid.calculateCellHeight(paddedMeasureHeight, countY), 0, 0); - } - public void setFixedSize(int width, int height) { mFixedWidth = width; mFixedHeight = height; @@ -832,9 +804,6 @@ public class CellLayout extends ViewGroup { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); @@ -842,8 +811,8 @@ public class CellLayout extends ViewGroup { int childWidthSize = widthSize - (getPaddingLeft() + getPaddingRight()); int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom()); if (mFixedCellWidth < 0 || mFixedCellHeight < 0) { - int cw = grid.calculateCellWidth(childWidthSize, mCountX); - int ch = grid.calculateCellHeight(childHeightSize, mCountY); + int cw = DeviceProfile.calculateCellWidth(childWidthSize, mCountX); + int ch = DeviceProfile.calculateCellHeight(childHeightSize, mCountY); if (cw != mCellWidth || ch != mCellHeight) { mCellWidth = cw; mCellHeight = ch; @@ -877,19 +846,20 @@ public class CellLayout extends ViewGroup { mWidthGap = mOriginalWidthGap; mHeightGap = mOriginalHeightGap; } - int count = getChildCount(); - int maxWidth = 0; - int maxHeight = 0; - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(newWidth, - MeasureSpec.EXACTLY); - int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, - MeasureSpec.EXACTLY); - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); - maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); - } + + // Make the feedback view large enough to hold the blur bitmap. + mTouchFeedbackView.measure( + MeasureSpec.makeMeasureSpec(mCellWidth + mTouchFeedbackView.getExtraSize(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mCellHeight + mTouchFeedbackView.getExtraSize(), + MeasureSpec.EXACTLY)); + + mShortcutsAndWidgets.measure( + MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); + + int maxWidth = mShortcutsAndWidgets.getMeasuredWidth(); + int maxHeight = mShortcutsAndWidgets.getMeasuredHeight(); if (mFixedWidth > 0 && mFixedHeight > 0) { setMeasuredDimension(maxWidth, maxHeight); } else { @@ -903,13 +873,13 @@ public class CellLayout extends ViewGroup { (mCountX * mCellWidth); int left = getPaddingLeft() + (int) Math.ceil(offset / 2f); int top = getPaddingTop(); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - child.layout(left, top, - left + r - l, - top + b - t); - } + + mTouchFeedbackView.layout(left, top, + left + mTouchFeedbackView.getMeasuredWidth(), + top + mTouchFeedbackView.getMeasuredHeight()); + mShortcutsAndWidgets.layout(left, top, + left + r - l, + top + b - t); } @Override @@ -917,12 +887,9 @@ public class CellLayout extends ViewGroup { super.onSizeChanged(w, h, oldw, oldh); // Expand the background drawing bounds by the padding baked into the background drawable - Rect padding = new Rect(); - mNormalBackground.getPadding(padding); - mBackgroundRect.set(-padding.left, -padding.top, w + padding.right, h + padding.bottom); - - mForegroundRect.set(mForegroundPadding, mForegroundPadding, - w - mForegroundPadding, h - mForegroundPadding); + mBackground.getPadding(mTempRect); + mBackground.setBounds(-mTempRect.left, -mTempRect.top, + w + mTempRect.right, h + mTempRect.bottom); } @Override @@ -939,25 +906,18 @@ public class CellLayout extends ViewGroup { return mBackgroundAlpha; } - public void setBackgroundAlphaMultiplier(float multiplier) { - - if (mBackgroundAlphaMultiplier != multiplier) { - mBackgroundAlphaMultiplier = multiplier; - invalidate(); - } - } - - public float getBackgroundAlphaMultiplier() { - return mBackgroundAlphaMultiplier; - } - public void setBackgroundAlpha(float alpha) { if (mBackgroundAlpha != alpha) { mBackgroundAlpha = alpha; - invalidate(); + mBackground.setAlpha((int) (mBackgroundAlpha * 255)); } } + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || (mIsDragTarget && who == mBackground); + } + public void setShortcutAndWidgetAlpha(float alpha) { mShortcutsAndWidgets.setAlpha(alpha); } @@ -1054,36 +1014,6 @@ public class CellLayout extends ViewGroup { return false; } - /** - * Estimate where the top left cell of the dragged item will land if it is dropped. - * - * @param originX The X value of the top left corner of the item - * @param originY The Y value of the top left corner of the item - * @param spanX The number of horizontal cells that the item spans - * @param spanY The number of vertical cells that the item spans - * @param result The estimated drop cell X and Y. - */ - void estimateDropCell(int originX, int originY, int spanX, int spanY, int[] result) { - final int countX = mCountX; - final int countY = mCountY; - - // pointToCellRounded takes the top left of a cell but will pad that with - // cellWidth/2 and cellHeight/2 when finding the matching cell - pointToCellRounded(originX, originY, result); - - // If the item isn't fully on this screen, snap to the edges - int rightOverhang = result[0] + spanX - countX; - if (rightOverhang > 0) { - result[0] -= rightOverhang; // Snap to right - } - result[0] = Math.max(0, result[0]); // Snap to left - int bottomOverhang = result[1] + spanY - countY; - if (bottomOverhang > 0) { - result[1] -= bottomOverhang; // Snap to bottom - } - result[1] = Math.max(0, result[1]); // Snap to top - } - void visualizeDropLocation(View v, Bitmap dragOutline, int originX, int originY, int cellX, int cellY, int spanX, int spanY, boolean resize, Point dragOffset, Rect dragRegion) { final int oldDragCellX = mDragCell[0]; @@ -1167,9 +1097,8 @@ public class CellLayout extends ViewGroup { * @return The X, Y cell of a vacant area that can contain this object, * nearest the requested location. */ - int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, - int[] result) { - return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, result); + int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, int[] result) { + return findNearestVacantArea(pixelX, pixelY, spanX, spanY, spanX, spanY, result, null); } /** @@ -1189,30 +1118,10 @@ public class CellLayout extends ViewGroup { */ int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] result, int[] resultSpan) { - return findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, + return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, true, result, resultSpan); } - /** - * Find a vacant area that will fit the given bounds nearest the requested - * cell location. Uses Euclidean distance to score multiple vacant areas. - * - * @param pixelX The X location at which you want to search for a vacant area. - * @param pixelY The Y location at which you want to search for a vacant area. - * @param spanX Horizontal span of the object. - * @param spanY Vertical span of the object. - * @param ignoreOccupied If true, the result can be an occupied cell - * @param result Array in which to place the result, or null (in which case a new array will - * be allocated) - * @return The X, Y cell of a vacant area that can contain this object, - * nearest the requested location. - */ - int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, View ignoreView, - boolean ignoreOccupied, int[] result) { - return findNearestArea(pixelX, pixelY, spanX, spanY, - spanX, spanY, ignoreView, ignoreOccupied, result, null, mOccupied); - } - private final Stack mTempRectStack = new Stack(); private void lazyInitTempRectStack() { if (mTempRectStack.isEmpty()) { @@ -1244,12 +1153,9 @@ public class CellLayout extends ViewGroup { * @return The X, Y cell of a vacant area that can contain this object, * nearest the requested location. */ - int[] findNearestArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, - View ignoreView, boolean ignoreOccupied, int[] result, int[] resultSpan, - boolean[][] occupied) { + private int[] findNearestArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, + int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan) { lazyInitTempRectStack(); - // mark space take by ignoreView as available (method checks if ignoreView is null) - markCellsAsUnoccupiedForView(ignoreView, occupied); // For items with a spanX / spanY > 1, the passed in point (pixelX, pixelY) corresponds // to the center of the item, but we are searching based on the top-left cell, so @@ -1280,7 +1186,7 @@ public class CellLayout extends ViewGroup { // First, let's see if this thing fits anywhere for (int i = 0; i < minSpanX; i++) { for (int j = 0; j < minSpanY; j++) { - if (occupied[x + i][y + j]) { + if (mOccupied[x + i][y + j]) { continue inner; } } @@ -1297,7 +1203,7 @@ public class CellLayout extends ViewGroup { while (!(hitMaxX && hitMaxY)) { if (incX && !hitMaxX) { for (int j = 0; j < ySize; j++) { - if (x + xSize > countX -1 || occupied[x + xSize][y + j]) { + if (x + xSize > countX -1 || mOccupied[x + xSize][y + j]) { // We can't move out horizontally hitMaxX = true; } @@ -1307,7 +1213,7 @@ public class CellLayout extends ViewGroup { } } else if (!hitMaxY) { for (int i = 0; i < xSize; i++) { - if (y + ySize > countY - 1 || occupied[x + i][y + ySize]) { + if (y + ySize > countY - 1 || mOccupied[x + i][y + ySize]) { // We can't move out vertically hitMaxY = true; } @@ -1324,7 +1230,7 @@ public class CellLayout extends ViewGroup { hitMaxX = xSize >= spanX; hitMaxY = ySize >= spanY; } - final int[] cellXY = mTmpXY; + final int[] cellXY = mTmpPoint; cellToCenterPoint(x, y, cellXY); // We verify that the current rect is not a sub-rect of any of our previous @@ -1340,8 +1246,7 @@ public class CellLayout extends ViewGroup { } } validRegions.push(currentRect); - double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2) - + Math.pow(cellXY[1] - pixelY, 2)); + double distance = Math.hypot(cellXY[0] - pixelX, cellXY[1] - pixelY); if ((distance <= bestDistance && !contained) || currentRect.contains(bestRect)) { @@ -1356,8 +1261,6 @@ public class CellLayout extends ViewGroup { } } } - // re-mark space taken by ignoreView as occupied - markCellsAsOccupiedForView(ignoreView, occupied); // Return -1, -1 if no suitable location found if (bestDistance == Double.MAX_VALUE) { @@ -1411,8 +1314,7 @@ public class CellLayout extends ViewGroup { } } - float distance = (float) - Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY)); + float distance = (float) Math.hypot(x - cellX, y - cellY); int[] curDirection = mTmpPoint; computeDirectionVector(x - cellX, y - cellY, curDirection); // The direction score is just the dot product of the two candidate direction @@ -2028,7 +1930,7 @@ public class CellLayout extends ViewGroup { } } - ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, + private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution) { // Copy the current state into the solution. This solution will be manipulated as necessary. @@ -2264,7 +2166,7 @@ public class CellLayout extends ViewGroup { } } - private void completeAnimationImmediately() { + @Thunk void completeAnimationImmediately() { if (a != null) { a.cancel(); } @@ -2317,7 +2219,7 @@ public class CellLayout extends ViewGroup { mLauncher.getWorkspace().updateItemLocationsInDatabase(this); } - public void setUseTempCoords(boolean useTempCoords) { + private void setUseTempCoords(boolean useTempCoords) { int childCount = mShortcutsAndWidgets.getChildCount(); for (int i = 0; i < childCount; i++) { LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams(); @@ -2325,11 +2227,11 @@ public class CellLayout extends ViewGroup { } } - ItemConfiguration findConfigurationNoShuffle(int pixelX, int pixelY, int minSpanX, int minSpanY, + private ItemConfiguration findConfigurationNoShuffle(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, View dragView, ItemConfiguration solution) { int[] result = new int[2]; int[] resultSpan = new int[2]; - findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, result, + findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, result, resultSpan); if (result[0] >= 0 && result[1] >= 0) { copyCurrentStateToSolution(solution, false); @@ -2585,7 +2487,7 @@ public class CellLayout extends ViewGroup { return mItemPlacementDirty; } - private class ItemConfiguration { + @Thunk class ItemConfiguration { HashMap map = new HashMap(); private HashMap savedMap = new HashMap(); ArrayList sortedViews = new ArrayList(); @@ -2645,45 +2547,6 @@ public class CellLayout extends ViewGroup { } - /** - * Find a vacant area that will fit the given bounds nearest the requested - * cell location. Uses Euclidean distance to score multiple vacant areas. - * - * @param pixelX The X location at which you want to search for a vacant area. - * @param pixelY The Y location at which you want to search for a vacant area. - * @param spanX Horizontal span of the object. - * @param spanY Vertical span of the object. - * @param ignoreView Considers space occupied by this view as unoccupied - * @param result Previously returned value to possibly recycle. - * @return The X, Y cell of a vacant area that can contain this object, - * nearest the requested location. - */ - int[] findNearestVacantArea( - int pixelX, int pixelY, int spanX, int spanY, View ignoreView, int[] result) { - return findNearestArea(pixelX, pixelY, spanX, spanY, ignoreView, true, result); - } - - /** - * Find a vacant area that will fit the given bounds nearest the requested - * cell location. Uses Euclidean distance to score multiple vacant areas. - * - * @param pixelX The X location at which you want to search for a vacant area. - * @param pixelY The Y location at which you want to search for a vacant area. - * @param minSpanX The minimum horizontal span required - * @param minSpanY The minimum vertical span required - * @param spanX Horizontal span of the object. - * @param spanY Vertical span of the object. - * @param ignoreView Considers space occupied by this view as unoccupied - * @param result Previously returned value to possibly recycle. - * @return The X, Y cell of a vacant area that can contain this object, - * nearest the requested location. - */ - int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, - int spanX, int spanY, View ignoreView, int[] result, int[] resultSpan) { - return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, ignoreView, true, - result, resultSpan, mOccupied); - } - /** * Find a starting cell position that will fit the given bounds nearest the requested * cell location. Uses Euclidean distance to score multiple vacant areas. @@ -2697,9 +2560,8 @@ public class CellLayout extends ViewGroup { * @return The X, Y cell of a vacant area that can contain this object, * nearest the requested location. */ - int[] findNearestArea( - int pixelX, int pixelY, int spanX, int spanY, int[] result) { - return findNearestArea(pixelX, pixelY, spanX, spanY, null, false, result); + int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, int[] result) { + return findNearestArea(pixelX, pixelY, spanX, spanY, spanX, spanY, false, result, null); } boolean existsEmptyCell() { @@ -2719,104 +2581,33 @@ public class CellLayout extends ViewGroup { * * @return True if a vacant cell of the specified dimension was found, false otherwise. */ - boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { - return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null, mOccupied); - } - - /** - * Like above, but ignores any cells occupied by the item "ignoreView" - * - * @param cellXY The array that will contain the position of a vacant cell if such a cell - * can be found. - * @param spanX The horizontal span of the cell we want to find. - * @param spanY The vertical span of the cell we want to find. - * @param ignoreView The home screen item we should treat as not occupying any space - * @return - */ - boolean findCellForSpanIgnoring(int[] cellXY, int spanX, int spanY, View ignoreView) { - return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, - ignoreView, mOccupied); - } - - /** - * Like above, but if intersectX and intersectY are not -1, then this method will try to - * return coordinates for rectangles that contain the cell [intersectX, intersectY] - * - * @param spanX The horizontal span of the cell we want to find. - * @param spanY The vertical span of the cell we want to find. - * @param ignoreView The home screen item we should treat as not occupying any space - * @param intersectX The X coordinate of the cell that we should try to overlap - * @param intersectX The Y coordinate of the cell that we should try to overlap - * - * @return True if a vacant cell of the specified dimension was found, false otherwise. - */ - boolean findCellForSpanThatIntersects(int[] cellXY, int spanX, int spanY, - int intersectX, int intersectY) { - return findCellForSpanThatIntersectsIgnoring( - cellXY, spanX, spanY, intersectX, intersectY, null, mOccupied); - } - - /** - * The superset of the above two methods - */ - boolean findCellForSpanThatIntersectsIgnoring(int[] cellXY, int spanX, int spanY, - int intersectX, int intersectY, View ignoreView, boolean occupied[][]) { - // mark space take by ignoreView as available (method checks if ignoreView is null) - markCellsAsUnoccupiedForView(ignoreView, occupied); - + public boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { boolean foundCell = false; - while (true) { - int startX = 0; - if (intersectX >= 0) { - startX = Math.max(startX, intersectX - (spanX - 1)); - } - int endX = mCountX - (spanX - 1); - if (intersectX >= 0) { - endX = Math.min(endX, intersectX + (spanX - 1) + (spanX == 1 ? 1 : 0)); - } - int startY = 0; - if (intersectY >= 0) { - startY = Math.max(startY, intersectY - (spanY - 1)); - } - int endY = mCountY - (spanY - 1); - if (intersectY >= 0) { - endY = Math.min(endY, intersectY + (spanY - 1) + (spanY == 1 ? 1 : 0)); - } + final int endX = mCountX - (spanX - 1); + final int endY = mCountY - (spanY - 1); - for (int y = startY; y < endY && !foundCell; y++) { - inner: - for (int x = startX; x < endX; x++) { - for (int i = 0; i < spanX; i++) { - for (int j = 0; j < spanY; j++) { - if (occupied[x + i][y + j]) { - // small optimization: we can skip to after the column we just found - // an occupied cell - x += i; - continue inner; - } + for (int y = 0; y < endY && !foundCell; y++) { + inner: + for (int x = 0; x < endX; x++) { + for (int i = 0; i < spanX; i++) { + for (int j = 0; j < spanY; j++) { + if (mOccupied[x + i][y + j]) { + // small optimization: we can skip to after the column we just found + // an occupied cell + x += i; + continue inner; } } - if (cellXY != null) { - cellXY[0] = x; - cellXY[1] = y; - } - foundCell = true; - break; } - } - if (intersectX == -1 && intersectY == -1) { + if (cellXY != null) { + cellXY[0] = x; + cellXY[1] = y; + } + foundCell = true; break; - } else { - // if we failed to find anything, try again but without any requirements of - // intersecting - intersectX = -1; - intersectY = -1; - continue; } } - // re-mark space taken by ignoreView as occupied - markCellsAsOccupiedForView(ignoreView, occupied); return foundCell; } @@ -2826,7 +2617,6 @@ public class CellLayout extends ViewGroup { * or it may have begun on another layout. */ void onDragEnter() { - mDragEnforcer.onDragEnter(); mDragging = true; } @@ -2834,7 +2624,6 @@ public class CellLayout extends ViewGroup { * Called when drag has left this CellLayout or has been completed (successfully or not) */ void onDragExit() { - mDragEnforcer.onDragExit(); // This can actually be called when we aren't in a drag, e.g. when adding a new // item to this layout via the customize drawer. // Guard against that case. @@ -2900,18 +2689,20 @@ public class CellLayout extends ViewGroup { * @param height Height in pixels * @param result An array of length 2 in which to store the result (may be null). */ - public static int[] rectToCell(int width, int height, int[] result) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - Rect padding = grid.getWorkspacePadding(grid.isLandscape ? - CellLayout.LANDSCAPE : CellLayout.PORTRAIT); + public static int[] rectToCell(Launcher launcher, int width, int height, int[] result) { + return rectToCell(launcher.getDeviceProfile(), launcher, width, height, result); + } + + public static int[] rectToCell(DeviceProfile grid, Context context, int width, int height, + int[] result) { + Rect padding = grid.getWorkspacePadding(Utilities.isRtl(context.getResources())); // Always assume we're working with the smallest span to make sure we // reserve enough space in both orientations. - int parentWidth = grid.calculateCellWidth(grid.widthPx - - padding.left - padding.right, (int) grid.numColumns); - int parentHeight = grid.calculateCellHeight(grid.heightPx - - padding.top - padding.bottom, (int) grid.numRows); + int parentWidth = DeviceProfile.calculateCellWidth(grid.widthPx + - padding.left - padding.right, (int) grid.inv.numColumns); + int parentHeight = DeviceProfile.calculateCellHeight(grid.heightPx + - padding.top - padding.bottom, (int) grid.inv.numRows); int smallerSize = Math.min(parentWidth, parentHeight); // Always round up to next largest cell @@ -2926,13 +2717,6 @@ public class CellLayout extends ViewGroup { return result; } - public int[] cellSpansToSize(int hSpans, int vSpans) { - int[] size = new int[2]; - size[0] = hSpans * mCellWidth + (hSpans - 1) * mWidthGap; - size[1] = vSpans * mCellHeight + (vSpans - 1) * mHeightGap; - return size; - } - /** * Calculate the grid spans needed to fit given item */ @@ -2951,49 +2735,11 @@ public class CellLayout extends ViewGroup { info.spanX = info.spanY = 1; return; } - int[] spans = rectToCell(minWidth, minHeight, null); + int[] spans = rectToCell(mLauncher, minWidth, minHeight, null); info.spanX = spans[0]; info.spanY = spans[1]; } - /** - * Find the first vacant cell, if there is one. - * - * @param vacant Holds the x and y coordinate of the vacant cell - * @param spanX Horizontal cell span. - * @param spanY Vertical cell span. - * - * @return True if a vacant cell was found - */ - public boolean getVacantCell(int[] vacant, int spanX, int spanY) { - - return findVacantCell(vacant, spanX, spanY, mCountX, mCountY, mOccupied); - } - - static boolean findVacantCell(int[] vacant, int spanX, int spanY, - int xCount, int yCount, boolean[][] occupied) { - - for (int y = 0; y < yCount; y++) { - for (int x = 0; x < xCount; x++) { - boolean available = !occupied[x][y]; -out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { - for (int j = y; j < y + spanY - 1 && y < yCount; j++) { - available = available && !occupied[i][j]; - if (!available) break out; - } - } - - if (available) { - vacant[0] = x; - vacant[1] = y; - return true; - } - } - } - - return false; - } - private void clearOccupiedCells() { for (int x = 0; x < mCountX; x++) { for (int y = 0; y < mCountY; y++) { @@ -3002,27 +2748,16 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { } } - public void onMove(View view, int newCellX, int newCellY, int newSpanX, int newSpanY) { - markCellsAsUnoccupiedForView(view); - markCellsForView(newCellX, newCellY, newSpanX, newSpanY, mOccupied, true); - } - public void markCellsAsOccupiedForView(View view) { - markCellsAsOccupiedForView(view, mOccupied); - } - public void markCellsAsOccupiedForView(View view, boolean[][] occupied) { if (view == null || view.getParent() != mShortcutsAndWidgets) return; LayoutParams lp = (LayoutParams) view.getLayoutParams(); - markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, true); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, mOccupied, true); } public void markCellsAsUnoccupiedForView(View view) { - markCellsAsUnoccupiedForView(view, mOccupied); - } - public void markCellsAsUnoccupiedForView(View view, boolean occupied[][]) { if (view == null || view.getParent() != mShortcutsAndWidgets) return; LayoutParams lp = (LayoutParams) view.getLayoutParams(); - markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, false); + markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, mOccupied, false); } private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean[][] occupied, @@ -3068,17 +2803,6 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { return new CellLayout.LayoutParams(p); } - public static class CellLayoutAnimationController extends LayoutAnimationController { - public CellLayoutAnimationController(Animation animation, float delay) { - super(animation, delay); - } - - @Override - protected long getDelayForView(View view) { - return (int) (Math.random() * 150); - } - } - public static class LayoutParams extends ViewGroup.MarginLayoutParams { /** * Horizontal location of the item in the grid. @@ -3237,7 +2961,7 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { // 2. When long clicking on an empty cell in a CellLayout, we save information about the // cellX and cellY coordinates and which page was clicked. We then set this as a tag on // the CellLayout that was long clicked - static final class CellInfo { + public static final class CellInfo { View cell; int cellX = -1; int cellY = -1; @@ -3246,7 +2970,7 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { long screenId; long container; - CellInfo(View v, ItemInfo info) { + public CellInfo(View v, ItemInfo info) { cell = v; cellX = info.cellX; cellY = info.cellY; @@ -3263,7 +2987,24 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { } } - public boolean lastDownOnOccupiedCell() { - return mLastDownOnOccupiedCell; + public boolean findVacantCell(int spanX, int spanY, int[] outXY) { + return Utilities.findVacantCell(outXY, spanX, spanY, mCountX, mCountY, mOccupied); + } + + public boolean isRegionVacant(int x, int y, int spanX, int spanY) { + int x2 = x + spanX - 1; + int y2 = y + spanY - 1; + if (x < 0 || y < 0 || x2 >= mCountX || y2 >= mCountY) { + return false; + } + for (int i = x; i <= x2; i++) { + for (int j = y; j <= y2; j++) { + if (mOccupied[i][j]) { + return false; + } + } + } + + return true; } } diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java index 81149793df..483c62249e 100644 --- a/src/com/android/launcher3/CheckLongPressHelper.java +++ b/src/com/android/launcher3/CheckLongPressHelper.java @@ -18,16 +18,27 @@ package com.android.launcher3; import android.view.View; +import com.android.launcher3.util.Thunk; + public class CheckLongPressHelper { - private View mView; - private boolean mHasPerformedLongPress; + + @Thunk View mView; + @Thunk View.OnLongClickListener mListener; + @Thunk boolean mHasPerformedLongPress; + private int mLongPressTimeout = 300; private CheckForLongPress mPendingCheckForLongPress; class CheckForLongPress implements Runnable { public void run() { if ((mView.getParent() != null) && mView.hasWindowFocus() && !mHasPerformedLongPress) { - if (mView.performLongClick()) { + boolean handled; + if (mListener != null) { + handled = mListener.onLongClick(mView); + } else { + handled = mView.performLongClick(); + } + if (handled) { mView.setPressed(false); mHasPerformedLongPress = true; } @@ -39,14 +50,25 @@ public class CheckLongPressHelper { mView = v; } + public CheckLongPressHelper(View v, View.OnLongClickListener listener) { + mView = v; + mListener = listener; + } + + /** + * Overrides the default long press timeout. + */ + public void setLongPressTimeout(int longPressTimeout) { + mLongPressTimeout = longPressTimeout; + } + public void postCheckForLongPress() { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } - mView.postDelayed(mPendingCheckForLongPress, - LauncherAppState.getInstance().getLongPressTimeout()); + mView.postDelayed(mPendingCheckForLongPress, mLongPressTimeout); } public void cancelLongPress() { diff --git a/src/com/android/launcher3/ClickShadowView.java b/src/com/android/launcher3/ClickShadowView.java new file mode 100644 index 0000000000..e31d7f7f63 --- /dev/null +++ b/src/com/android/launcher3/ClickShadowView.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.view.View; +import android.view.ViewGroup; + +public class ClickShadowView extends View { + + private static final int SHADOW_SIZE_FACTOR = 3; + private static final int SHADOW_LOW_ALPHA = 30; + private static final int SHADOW_HIGH_ALPHA = 60; + + private final Paint mPaint; + + private final float mShadowOffset; + private final float mShadowPadding; + + private Bitmap mBitmap; + + public ClickShadowView(Context context) { + super(context); + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + mPaint.setColor(Color.BLACK); + + mShadowPadding = getResources().getDimension(R.dimen.blur_size_click_shadow); + mShadowOffset = getResources().getDimension(R.dimen.click_shadow_high_shift); + } + + /** + * @return extra space required by the view to show the shadow. + */ + public int getExtraSize() { + return (int) (SHADOW_SIZE_FACTOR * mShadowPadding); + } + + /** + * Applies the new bitmap. + * @return true if the view was invalidated. + */ + public boolean setBitmap(Bitmap b) { + if (b != mBitmap){ + mBitmap = b; + invalidate(); + return true; + } + return false; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBitmap != null) { + mPaint.setAlpha(SHADOW_LOW_ALPHA); + canvas.drawBitmap(mBitmap, 0, 0, mPaint); + mPaint.setAlpha(SHADOW_HIGH_ALPHA); + canvas.drawBitmap(mBitmap, 0, mShadowOffset, mPaint); + } + } + + public void animateShadow() { + setAlpha(0); + animate().alpha(1) + .setDuration(FastBitmapDrawable.CLICK_FEEDBACK_DURATION) + .setInterpolator(FastBitmapDrawable.CLICK_FEEDBACK_INTERPOLATOR) + .start(); + } + + /** + * Aligns the shadow with {@param view} + * @param viewParent immediate parent of {@param view}. It must be a sibling of this view. + */ + public void alignWithIconView(BubbleTextView view, ViewGroup viewParent) { + float leftShift = view.getLeft() + viewParent.getLeft() - getLeft(); + float topShift = view.getTop() + viewParent.getTop() - getTop(); + int iconWidth = view.getRight() - view.getLeft(); + int iconHSpace = iconWidth - view.getCompoundPaddingRight() - view.getCompoundPaddingLeft(); + float drawableWidth = view.getIcon().getBounds().width(); + + setTranslationX(leftShift + + viewParent.getTranslationX() + + view.getCompoundPaddingLeft() * view.getScaleX() + + (iconHSpace - drawableWidth) * view.getScaleX() / 2 /* drawable gap */ + + iconWidth * (1 - view.getScaleX()) / 2 /* gap due to scale */ + - mShadowPadding /* extra shadow size */ + ); + setTranslationY(topShift + + viewParent.getTranslationY() + + view.getPaddingTop() * view.getScaleY() /* drawable gap */ + + view.getHeight() * (1 - view.getScaleY()) / 2 /* gap due to scale */ + - mShadowPadding /* extra shadow size */ + ); + } +} diff --git a/src/com/android/launcher3/CommonAppTypeParser.java b/src/com/android/launcher3/CommonAppTypeParser.java new file mode 100644 index 0000000000..5314ecff17 --- /dev/null +++ b/src/com/android/launcher3/CommonAppTypeParser.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.res.XmlResourceParser; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.backup.BackupProtos.Favorite; +import com.android.launcher3.util.Thunk; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * A class that parses content values corresponding to some common app types. + */ +public class CommonAppTypeParser implements LayoutParserCallback { + private static final String TAG = "CommonAppTypeParser"; + + // Including TARGET_NONE + public static final int SUPPORTED_TYPE_COUNT = 7; + + private static final int RESTORE_FLAG_BIT_SHIFT = 4; + + + private final long mItemId; + @Thunk final int mResId; + @Thunk final Context mContext; + + ContentValues parsedValues; + Intent parsedIntent; + String parsedTitle; + + public CommonAppTypeParser(long itemId, int itemType, Context context) { + mItemId = itemId; + mContext = context; + mResId = getResourceForItemType(itemType); + } + + @Override + public long generateNewItemId() { + return mItemId; + } + + @Override + public long insertAndCheck(SQLiteDatabase db, ContentValues values) { + parsedValues = values; + + // Remove unwanted values + values.put(Favorites.ICON_TYPE, (Integer) null); + values.put(Favorites.ICON_PACKAGE, (String) null); + values.put(Favorites.ICON_RESOURCE, (String) null); + values.put(Favorites.ICON, (byte[]) null); + return 1; + } + + /** + * Tries to find a suitable app to the provided app type. + */ + public boolean findDefaultApp() { + if (mResId == 0) { + return false; + } + + parsedIntent = null; + parsedValues = null; + new MyLayoutParser().parseValues(); + return (parsedValues != null) && (parsedIntent != null); + } + + private class MyLayoutParser extends DefaultLayoutParser { + + public MyLayoutParser() { + super(CommonAppTypeParser.this.mContext, null, CommonAppTypeParser.this, + CommonAppTypeParser.this.mContext.getResources(), mResId, TAG_RESOLVE, 0); + } + + @Override + protected long addShortcut(String title, Intent intent, int type) { + if (type == Favorites.ITEM_TYPE_APPLICATION) { + parsedIntent = intent; + parsedTitle = title; + } + return super.addShortcut(title, intent, type); + } + + public void parseValues() { + XmlResourceParser parser = mSourceRes.getXml(mLayoutId); + try { + beginDocument(parser, mRootTag); + new ResolveParser().parseAndAdd(parser); + } catch (IOException | XmlPullParserException e) { + Log.e(TAG, "Unable to parse default app info", e); + } + parser.close(); + } + } + + public static int getResourceForItemType(int type) { + switch (type) { + case Favorite.TARGET_PHONE: + return R.xml.app_target_phone; + + case Favorite.TARGET_MESSENGER: + return R.xml.app_target_messenger; + + case Favorite.TARGET_EMAIL: + return R.xml.app_target_email; + + case Favorite.TARGET_BROWSER: + return R.xml.app_target_browser; + + case Favorite.TARGET_GALLERY: + return R.xml.app_target_gallery; + + case Favorite.TARGET_CAMERA: + return R.xml.app_target_camera; + + default: + return 0; + } + } + + public static int encodeItemTypeToFlag(int itemType) { + return itemType << RESTORE_FLAG_BIT_SHIFT; + } + + public static int decodeItemTypeFromFlag(int flag) { + return (flag & ShortcutInfo.FLAG_RESTORED_APP_TYPE) >> RESTORE_FLAG_BIT_SHIFT; + } + +} diff --git a/src/com/android/launcher3/CustomAppWidget.java b/src/com/android/launcher3/CustomAppWidget.java new file mode 100644 index 0000000000..1b4ed79c0b --- /dev/null +++ b/src/com/android/launcher3/CustomAppWidget.java @@ -0,0 +1,14 @@ +package com.android.launcher3; + +public interface CustomAppWidget { + public String getLabel(); + public int getPreviewImage(); + public int getIcon(); + public int getWidgetLayout(); + + public int getSpanX(); + public int getSpanY(); + public int getMinSpanX(); + public int getMinSpanY(); + public int getResizeMode(); +} diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java index e3ea40ebb8..7b91c675be 100644 --- a/src/com/android/launcher3/DefaultLayoutParser.java +++ b/src/com/android/launcher3/DefaultLayoutParser.java @@ -13,13 +13,13 @@ import android.text.TextUtils; import android.util.Log; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.net.URISyntaxException; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -29,17 +29,15 @@ import java.util.List; public class DefaultLayoutParser extends AutoInstallsLayout { private static final String TAG = "DefaultLayoutParser"; - private static final String TAG_RESOLVE = "resolve"; + protected static final String TAG_RESOLVE = "resolve"; private static final String TAG_FAVORITES = "favorites"; - private static final String TAG_FAVORITE = "favorite"; + protected static final String TAG_FAVORITE = "favorite"; private static final String TAG_APPWIDGET = "appwidget"; private static final String TAG_SHORTCUT = "shortcut"; private static final String TAG_FOLDER = "folder"; private static final String TAG_PARTNER_FOLDER = "partner-folder"; - private static final String TAG_INCLUDE = "include"; - private static final String ATTR_URI = "uri"; - private static final String ATTR_WORKSPACE = "workspace"; + protected static final String ATTR_URI = "uri"; private static final String ATTR_CONTAINER = "container"; private static final String ATTR_SCREEN = "screen"; private static final String ATTR_FOLDER_ITEMS = "folderItems"; @@ -47,7 +45,12 @@ public class DefaultLayoutParser extends AutoInstallsLayout { public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost, LayoutParserCallback callback, Resources sourceRes, int layoutId) { super(context, appWidgetHost, callback, sourceRes, layoutId, TAG_FAVORITES); - Log.e(TAG, "Default layout parser initialized"); + } + + public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost, + LayoutParserCallback callback, Resources sourceRes, int layoutId, String rootTag, + int hotseatAllAppsRank) { + super(context, appWidgetHost, callback, sourceRes, layoutId, rootTag, hotseatAllAppsRank); } @Override @@ -55,7 +58,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { return getFolderElementsMap(mSourceRes); } - private HashMap getFolderElementsMap(Resources res) { + @Thunk HashMap getFolderElementsMap(Resources res) { HashMap parsers = new HashMap(); parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser()); parsers.put(TAG_SHORTCUT, new UriShortcutParser(res)); @@ -84,29 +87,10 @@ public class DefaultLayoutParser extends AutoInstallsLayout { out[1] = Long.parseLong(getAttributeValue(parser, ATTR_SCREEN)); } - @Override - protected int parseAndAddNode( - XmlResourceParser parser, - HashMap tagParserMap, - ArrayList screenIds) - throws XmlPullParserException, IOException { - if (TAG_INCLUDE.equals(parser.getName())) { - final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0); - if (resId != 0) { - // recursively load some more favorites, why not? - return parseLayout(resId, screenIds); - } else { - return 0; - } - } else { - return super.parseAndAddNode(parser, tagParserMap, screenIds); - } - } - /** * AppShortcutParser which also supports adding URI based intents */ - private class AppShortcutWithUriParser extends AppShortcutParser { + @Thunk class AppShortcutWithUriParser extends AppShortcutParser { @Override protected long invalidPackageOrClass(XmlResourceParser parser) { @@ -218,7 +202,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * Contains a list of nodes, and accepts the first successfully parsed node. */ - private class ResolveParser implements TagParser { + protected class ResolveParser implements TagParser { private final AppShortcutWithUriParser mChildParser = new AppShortcutWithUriParser(); @@ -248,7 +232,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * A parser which adds a folder whose contents come from partner apk. */ - private class PartnerFolderParser implements TagParser { + @Thunk class PartnerFolderParser implements TagParser { @Override public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, @@ -274,7 +258,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { /** * An extension of FolderParser which allows adding items from a different xml. */ - private class MyFolderParser extends FolderParser { + @Thunk class MyFolderParser extends FolderParser { @Override public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, diff --git a/src/com/android/launcher3/DeferredHandler.java b/src/com/android/launcher3/DeferredHandler.java index a2d121d635..a43ab67239 100644 --- a/src/com/android/launcher3/DeferredHandler.java +++ b/src/com/android/launcher3/DeferredHandler.java @@ -20,10 +20,10 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; -import android.util.Pair; + +import com.android.launcher3.util.Thunk; import java.util.LinkedList; -import java.util.ListIterator; /** * Queue of things to run on a looper thread. Items posted with {@link #post} will not @@ -33,20 +33,18 @@ import java.util.ListIterator; * This class is fifo. */ public class DeferredHandler { - private LinkedList> mQueue = new LinkedList>(); + @Thunk LinkedList mQueue = new LinkedList<>(); private MessageQueue mMessageQueue = Looper.myQueue(); private Impl mHandler = new Impl(); - private class Impl extends Handler implements MessageQueue.IdleHandler { + @Thunk class Impl extends Handler implements MessageQueue.IdleHandler { public void handleMessage(Message msg) { - Pair p; Runnable r; synchronized (mQueue) { if (mQueue.size() == 0) { return; } - p = mQueue.removeFirst(); - r = p.first; + r = mQueue.removeFirst(); } r.run(); synchronized (mQueue) { @@ -77,11 +75,8 @@ public class DeferredHandler { /** Schedule runnable to run after everything that's on the queue right now. */ public void post(Runnable runnable) { - post(runnable, 0); - } - public void post(Runnable runnable, int type) { synchronized (mQueue) { - mQueue.add(new Pair(runnable, type)); + mQueue.add(runnable); if (mQueue.size() == 1) { scheduleNextLocked(); } @@ -90,31 +85,10 @@ public class DeferredHandler { /** Schedule runnable to run when the queue goes idle. */ public void postIdle(final Runnable runnable) { - postIdle(runnable, 0); - } - public void postIdle(final Runnable runnable, int type) { - post(new IdleRunnable(runnable), type); + post(new IdleRunnable(runnable)); } - public void cancelRunnable(Runnable runnable) { - synchronized (mQueue) { - while (mQueue.remove(runnable)) { } - } - } - public void cancelAllRunnablesOfType(int type) { - synchronized (mQueue) { - ListIterator> iter = mQueue.listIterator(); - Pair p; - while (iter.hasNext()) { - p = iter.next(); - if (p.second == type) { - iter.remove(); - } - } - } - } - - public void cancel() { + public void cancelAll() { synchronized (mQueue) { mQueue.clear(); } @@ -122,20 +96,19 @@ public class DeferredHandler { /** Runs all queued Runnables from the calling thread. */ public void flush() { - LinkedList> queue = new LinkedList>(); + LinkedList queue = new LinkedList<>(); synchronized (mQueue) { queue.addAll(mQueue); mQueue.clear(); } - for (Pair p : queue) { - p.first.run(); + for (Runnable r : queue) { + r.run(); } } void scheduleNextLocked() { if (mQueue.size() > 0) { - Pair p = mQueue.getFirst(); - Runnable peek = p.first; + Runnable peek = mQueue.getFirst(); if (peek instanceof IdleRunnable) { mMessageQueue.addIdleHandler(mHandler); } else { diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index ea058ea71a..9c8659c293 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -17,45 +17,17 @@ package com.android.launcher3; import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.ComponentName; import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.drawable.TransitionDrawable; import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.UserManager; import android.util.AttributeSet; import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.LinearInterpolator; -import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.FlingAnimation; +import com.android.launcher3.util.Thunk; public class DeleteDropTarget extends ButtonDropTarget { - private static int DELETE_ANIMATION_DURATION = 285; - private static int FLING_DELETE_ANIMATION_DURATION = 350; - private static float FLING_TO_DELETE_FRICTION = 0.035f; - private static int MODE_FLING_DELETE_TO_TRASH = 0; - private static int MODE_FLING_DELETE_ALONG_VECTOR = 1; - - private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR; - - private ColorStateList mOriginalTextColor; - private TransitionDrawable mUninstallDrawable; - private TransitionDrawable mRemoveDrawable; - private TransitionDrawable mCurrentDrawable; - - private boolean mWaitingForUninstall = false; public DeleteDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -68,442 +40,86 @@ public class DeleteDropTarget extends ButtonDropTarget { @Override protected void onFinishInflate() { super.onFinishInflate(); - - // Get the drawable - mOriginalTextColor = getTextColors(); - // Get the hover color - Resources r = getResources(); - mHoverColor = r.getColor(R.color.delete_target_hover_tint); - mUninstallDrawable = (TransitionDrawable) - r.getDrawable(R.drawable.uninstall_target_selector); - mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector); + mHoverColor = getResources().getColor(R.color.delete_target_hover_tint); - mRemoveDrawable.setCrossFadeEnabled(true); - mUninstallDrawable.setCrossFadeEnabled(true); - - // The current drawable is set to either the remove drawable or the uninstall drawable - // and is initially set to the remove drawable, as set in the layout xml. - mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); - - // Remove the text in the Phone UI in landscape - int orientation = getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - if (!LauncherAppState.getInstance().isScreenLarge()) { - setText(""); - } - } + setDrawable(R.drawable.ic_remove_launcher); } - private boolean isAllAppsApplication(DragSource source, Object info) { - return source.supportsAppInfoDropTarget() && (info instanceof AppInfo); - } - private boolean isAllAppsWidget(DragSource source, Object info) { - if (source instanceof AppsCustomizePagedView) { - if (info instanceof PendingAddItemInfo) { - PendingAddItemInfo addInfo = (PendingAddItemInfo) info; - switch (addInfo.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: - return true; - } - } - } - return false; - } - private boolean isDragSourceWorkspaceOrFolder(DragObject d) { - return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder); - } - private boolean isWorkspaceOrFolderApplication(DragObject d) { - return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof ShortcutInfo); - } - private boolean isWorkspaceOrFolderWidget(DragObject d) { - return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof LauncherAppWidgetInfo); - } - private boolean isWorkspaceFolder(DragObject d) { - return (d.dragSource instanceof Workspace) && (d.dragInfo instanceof FolderInfo); - } - - private void setHoverColor() { - if (mCurrentDrawable != null) { - mCurrentDrawable.startTransition(mTransitionDuration); - } - setTextColor(mHoverColor); - } - private void resetHoverColor() { - if (mCurrentDrawable != null) { - mCurrentDrawable.resetTransition(); - } - setTextColor(mOriginalTextColor); + public static boolean supportsDrop(Object info) { + return (info instanceof ShortcutInfo) + || (info instanceof LauncherAppWidgetInfo) + || (info instanceof FolderInfo); } @Override - public boolean acceptDrop(DragObject d) { - return willAcceptDrop(d.dragInfo); - } - - public static boolean willAcceptDrop(Object info) { - if (info instanceof ItemInfo) { - ItemInfo item = (ItemInfo) info; - if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET || - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { - return true; - } - - if (!LauncherAppState.isDisableAllApps() && - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { - return true; - } - - if (!LauncherAppState.isDisableAllApps() && - item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && - item instanceof AppInfo) { - AppInfo appInfo = (AppInfo) info; - return (appInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; - } - - if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && - item instanceof ShortcutInfo) { - if (LauncherAppState.isDisableAllApps()) { - ShortcutInfo shortcutInfo = (ShortcutInfo) info; - return (shortcutInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; - } else { - return true; - } - } - } - return false; + protected boolean supportsDrop(DragSource source, Object info) { + return source.supportsDeleteDropTarget() && supportsDrop(info); } @Override - public void onDragStart(DragSource source, Object info, int dragAction) { - boolean isVisible = true; - boolean useUninstallLabel = !LauncherAppState.isDisableAllApps() && - isAllAppsApplication(source, info); - boolean useDeleteLabel = !useUninstallLabel && source.supportsDeleteDropTarget(); - - // If we are dragging an application from AppsCustomize, only show the control if we can - // delete the app (it was downloaded), and rename the string to "uninstall" in such a case. - // Hide the delete target if it is a widget from AppsCustomize. - if (!willAcceptDrop(info) || isAllAppsWidget(source, info)) { - isVisible = false; - } - if (useUninstallLabel) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - UserManager userManager = (UserManager) - getContext().getSystemService(Context.USER_SERVICE); - Bundle restrictions = userManager.getUserRestrictions(); - if (restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false) - || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false)) { - isVisible = false; - } - } - } - - if (useUninstallLabel) { - setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null); - } else if (useDeleteLabel) { - setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null); - } else { - isVisible = false; - } - mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); - - mActive = isVisible; - resetHoverColor(); - ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); - if (isVisible && getText().length() > 0) { - setText(useUninstallLabel ? R.string.delete_target_uninstall_label - : R.string.delete_target_label); - } - } - - @Override - public void onDragEnd() { - super.onDragEnd(); - mActive = false; - } - - public void onDragEnter(DragObject d) { - super.onDragEnter(d); - - setHoverColor(); - } - - public void onDragExit(DragObject d) { - super.onDragExit(d); - - if (!d.dragComplete) { - resetHoverColor(); - } else { - // Restore the hover color if we are deleting - d.dragView.setColor(mHoverColor); - } - } - - private void animateToTrashAndCompleteDrop(final DragObject d) { - final DragLayer dragLayer = mLauncher.getDragLayer(); - final Rect from = new Rect(); - dragLayer.getViewRectRelativeToSelf(d.dragView, from); - - int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth(); - int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight(); - final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), - width, height); - final float scale = (float) to.width() / from.width(); - - mSearchDropTargetBar.deferOnDragEnd(); - deferCompleteDropIfUninstalling(d); - - Runnable onAnimationEndRunnable = new Runnable() { - @Override - public void run() { - completeDrop(d); - mSearchDropTargetBar.onDragEnd(); - mLauncher.exitSpringLoadedDragModeDelayed(true, 0, null); - } - }; - dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, - DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2), - new LinearInterpolator(), onAnimationEndRunnable, - DragLayer.ANIMATION_END_DISAPPEAR, null); - } - - private void deferCompleteDropIfUninstalling(DragObject d) { - mWaitingForUninstall = false; - if (isUninstallFromWorkspace(d)) { - if (d.dragSource instanceof Folder) { - ((Folder) d.dragSource).deferCompleteDropAfterUninstallActivity(); - } else if (d.dragSource instanceof Workspace) { - ((Workspace) d.dragSource).deferCompleteDropAfterUninstallActivity(); - } - mWaitingForUninstall = true; - } - } - - private boolean isUninstallFromWorkspace(DragObject d) { - if (LauncherAppState.isDisableAllApps() && isWorkspaceOrFolderApplication(d)) { - ShortcutInfo shortcut = (ShortcutInfo) d.dragInfo; - // Only allow manifest shortcuts to initiate an un-install. - return !InstallShortcutReceiver.isValidShortcutLaunchIntent(shortcut.intent); - } - return false; - } - - private void completeDrop(DragObject d) { + @Thunk void completeDrop(DragObject d) { ItemInfo item = (ItemInfo) d.dragInfo; - boolean wasWaitingForUninstall = mWaitingForUninstall; - mWaitingForUninstall = false; - if (isAllAppsApplication(d.dragSource, item)) { - // Uninstall the application if it is being dragged from AppsCustomize - AppInfo appInfo = (AppInfo) item; - mLauncher.startApplicationUninstallActivity(appInfo.componentName, appInfo.flags, - appInfo.user); - } else if (isUninstallFromWorkspace(d)) { - ShortcutInfo shortcut = (ShortcutInfo) item; - if (shortcut.intent != null && shortcut.intent.getComponent() != null) { - final ComponentName componentName = shortcut.intent.getComponent(); - final DragSource dragSource = d.dragSource; - final UserHandleCompat user = shortcut.user; - mWaitingForUninstall = mLauncher.startApplicationUninstallActivity( - componentName, shortcut.flags, user); - if (mWaitingForUninstall) { - final Runnable checkIfUninstallWasSuccess = new Runnable() { - @Override - public void run() { - mWaitingForUninstall = false; - String packageName = componentName.getPackageName(); - boolean uninstallSuccessful = !AllAppsList.packageHasActivities( - getContext(), packageName, user); - if (dragSource instanceof Folder) { - ((Folder) dragSource). - onUninstallActivityReturned(uninstallSuccessful); - } else if (dragSource instanceof Workspace) { - ((Workspace) dragSource). - onUninstallActivityReturned(uninstallSuccessful); - } - } - }; - mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess); - } - } - } else if (isWorkspaceOrFolderApplication(d)) { - LauncherModel.deleteItemFromDatabase(mLauncher, item); - } else if (isWorkspaceFolder(d)) { - // Remove the folder from the workspace and delete the contents from launcher model - FolderInfo folderInfo = (FolderInfo) item; - mLauncher.removeFolder(folderInfo); - LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo); - } else if (isWorkspaceOrFolderWidget(d)) { - // Remove the widget from the workspace - mLauncher.removeAppWidget((LauncherAppWidgetInfo) item); - LauncherModel.deleteItemFromDatabase(mLauncher, item); + if ((d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder)) { + removeWorkspaceOrFolderItem(mLauncher, item, null); + } + } - final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; - final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); - if ((appWidgetHost != null) && launcherAppWidgetInfo.isWidgetIdValid()) { + /** + * Removes the item from the workspace. If the view is not null, it also removes the view. + * @return true if the item was removed. + */ + public static boolean removeWorkspaceOrFolderItem(Launcher launcher, ItemInfo item, View view) { + if (item instanceof ShortcutInfo) { + LauncherModel.deleteItemFromDatabase(launcher, item); + } else if (item instanceof FolderInfo) { + FolderInfo folder = (FolderInfo) item; + launcher.removeFolder(folder); + LauncherModel.deleteFolderContentsFromDatabase(launcher, folder); + } else if (item instanceof LauncherAppWidgetInfo) { + final LauncherAppWidgetInfo widget = (LauncherAppWidgetInfo) item; + + // Remove the widget from the workspace + launcher.removeAppWidget(widget); + LauncherModel.deleteItemFromDatabase(launcher, widget); + + final LauncherAppWidgetHost appWidgetHost = launcher.getAppWidgetHost(); + + if (appWidgetHost != null && !widget.isCustomWidget() + && widget.isWidgetIdValid()) { // Deleting an app widget ID is a void call but writes to disk before returning // to the caller... new AsyncTask() { public Void doInBackground(Void ... args) { - appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId); + appWidgetHost.deleteAppWidgetId(widget.appWidgetId); return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - } - if (wasWaitingForUninstall && !mWaitingForUninstall) { - if (d.dragSource instanceof Folder) { - ((Folder) d.dragSource).onUninstallActivityReturned(false); - } else if (d.dragSource instanceof Workspace) { - ((Workspace) d.dragSource).onUninstallActivityReturned(false); - } - } - } - - public void onDrop(DragObject d) { - animateToTrashAndCompleteDrop(d); - } - - /** - * Creates an animation from the current drag view to the delete trash icon. - */ - private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer, - DragObject d, PointF vel, ViewConfiguration config) { - - int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth(); - int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight(); - final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), - width, height); - final Rect from = new Rect(); - dragLayer.getViewRectRelativeToSelf(d.dragView, from); - - // Calculate how far along the velocity vector we should put the intermediate point on - // the bezier curve - float velocity = Math.abs(vel.length()); - float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f)); - int offsetY = (int) (-from.top * vp); - int offsetX = (int) (offsetY / (vel.y / vel.x)); - final float y2 = from.top + offsetY; // intermediate t/l - final float x2 = from.left + offsetX; - final float x1 = from.left; // drag view t/l - final float y1 = from.top; - final float x3 = to.left; // delete target t/l - final float y3 = to.top; - - final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() { - @Override - public float getInterpolation(float t) { - return t * t * t * t * t * t * t * t; - } - }; - return new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - final DragView dragView = (DragView) dragLayer.getAnimatedView(); - float t = ((Float) animation.getAnimatedValue()).floatValue(); - float tp = scaleAlphaInterpolator.getInterpolation(t); - float initialScale = dragView.getInitialScale(); - float finalAlpha = 0.5f; - float scale = dragView.getScaleX(); - float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f; - float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f; - float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) + - (t * t) * x3; - float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) + - (t * t) * y3; - - dragView.setTranslationX(x); - dragView.setTranslationY(y); - dragView.setScaleX(initialScale * (1f - tp)); - dragView.setScaleY(initialScale * (1f - tp)); - dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp)); - } - }; - } - - /** - * Creates an animation from the current drag view along its current velocity vector. - * For this animation, the alpha runs for a fixed duration and we update the position - * progressively. - */ - private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { - private DragLayer mDragLayer; - private PointF mVelocity; - private Rect mFrom; - private long mPrevTime; - private boolean mHasOffsetForScale; - private float mFriction; - - private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); - - public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from, - long startTime, float friction) { - mDragLayer = dragLayer; - mVelocity = vel; - mFrom = from; - mPrevTime = startTime; - mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction); + } else { + return false; } - @Override - public void onAnimationUpdate(ValueAnimator animation) { - final DragView dragView = (DragView) mDragLayer.getAnimatedView(); - float t = ((Float) animation.getAnimatedValue()).floatValue(); - long curTime = AnimationUtils.currentAnimationTimeMillis(); - - if (!mHasOffsetForScale) { - mHasOffsetForScale = true; - float scale = dragView.getScaleX(); - float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f; - float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f; - - mFrom.left += xOffset; - mFrom.top += yOffset; - } - - mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); - mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); - - dragView.setTranslationX(mFrom.left); - dragView.setTranslationY(mFrom.top); - dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); - - mVelocity.x *= mFriction; - mVelocity.y *= mFriction; - mPrevTime = curTime; + if (view != null) { + launcher.getWorkspace().removeWorkspaceItem(view); + launcher.getWorkspace().stripEmptyScreens(); } - }; - private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer, - DragObject d, PointF vel, final long startTime, final int duration, - ViewConfiguration config) { - final Rect from = new Rect(); - dragLayer.getViewRectRelativeToSelf(d.dragView, from); - - return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime, - FLING_TO_DELETE_FRICTION); + return true; } - public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) { - final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView; - + @Override + public void onFlingToDelete(final DragObject d, PointF vel) { // Don't highlight the icon as it's animating d.dragView.setColor(0); d.dragView.updateInitialScaleToCurrentScale(); - // Don't highlight the target if we are flinging from AllApps - if (isAllApps) { - resetHoverColor(); - } - if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { - // Defer animating out the drop target if we are animating to it - mSearchDropTargetBar.deferOnDragEnd(); - mSearchDropTargetBar.finishAnimations(); - } - - final ViewConfiguration config = ViewConfiguration.get(mLauncher); final DragLayer dragLayer = mLauncher.getDragLayer(); - final int duration = FLING_DELETE_ANIMATION_DURATION; + FlingAnimation fling = new FlingAnimation(d, vel, + getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()), + dragLayer); + + final int duration = fling.getDuration(); final long startTime = AnimationUtils.currentAnimationTimeMillis(); // NOTE: Because it takes time for the first frame of animation to actually be @@ -527,28 +143,22 @@ public class DeleteDropTarget extends ButtonDropTarget { return Math.min(1f, mOffset + t); } }; - AnimatorUpdateListener updateCb = null; - if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { - updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config); - } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) { - updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime, - duration, config); - } - deferCompleteDropIfUninstalling(d); Runnable onAnimationEndRunnable = new Runnable() { @Override public void run() { - // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up - // itself, otherwise, complete the drop to initiate the deletion process - if (!isAllApps) { - mLauncher.exitSpringLoadedDragMode(); - completeDrop(d); - } + mLauncher.exitSpringLoadedDragMode(); + completeDrop(d); mLauncher.getDragController().onDeferredEndFling(d); } }; - dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable, + + dragLayer.animateView(d.dragView, fling, duration, tInterpolator, onAnimationEndRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); } + + @Override + protected String getAccessibilityDropConfirmation() { + return getResources().getString(R.string.item_removed); + } } diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index d6aadcee1e..62b05b0d65 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -19,155 +19,103 @@ package com.android.launcher3; import android.appwidget.AppWidgetHostView; import android.content.ComponentName; import android.content.Context; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Paint; import android.graphics.Paint.FontMetrics; import android.graphics.Point; -import android.graphics.PointF; import android.graphics.Rect; import android.util.DisplayMetrics; -import android.view.Display; import android.view.Gravity; -import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.MarginLayoutParams; -import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; - - -class DeviceProfileQuery { - DeviceProfile profile; - float widthDps; - float heightDps; - float value; - PointF dimens; - - DeviceProfileQuery(DeviceProfile p, float v) { - widthDps = p.minWidthDps; - heightDps = p.minHeightDps; - value = v; - dimens = new PointF(widthDps, heightDps); - profile = p; - } -} - public class DeviceProfile { - public static interface DeviceProfileCallbacks { - public void onAvailableSizeChanged(DeviceProfile grid); - } - String name; - float minWidthDps; - float minHeightDps; - float numRows; - float numColumns; - float numHotseatIcons; - float iconSize; - private float iconTextSize; - private int iconDrawablePaddingOriginalPx; - private float hotseatIconSize; + public final InvariantDeviceProfile inv; - int defaultLayoutId; + // Device properties + public final boolean isTablet; + public final boolean isLargeTablet; + public final boolean isPhone; + public final boolean transposeLayoutWithOrientation; - boolean isLandscape; - boolean isTablet; - boolean isLargeTablet; - boolean isLayoutRtl; - boolean transposeLayoutWithOrientation; + // Device properties in current orientation + public final boolean isLandscape; + public final int widthPx; + public final int heightPx; + public final int availableWidthPx; + public final int availableHeightPx; - int desiredWorkspaceLeftRightMarginPx; - int edgeMarginPx; - Rect defaultWidgetPadding; + // Overview mode + private final int overviewModeMinIconZoneHeightPx; + private final int overviewModeMaxIconZoneHeightPx; + private final int overviewModeBarItemWidthPx; + private final int overviewModeBarSpacerWidthPx; + private final float overviewModeIconZoneRatio; + private final float overviewModeScaleFactor; - int widthPx; - int heightPx; - int availableWidthPx; - int availableHeightPx; - int defaultPageSpacingPx; + // Workspace + private int desiredWorkspaceLeftRightMarginPx; + public final int edgeMarginPx; + public final Rect defaultWidgetPadding; + private final int pageIndicatorHeightPx; + private final int defaultPageSpacingPx; + private float dragViewScale; - int overviewModeMinIconZoneHeightPx; - int overviewModeMaxIconZoneHeightPx; - int overviewModeBarItemWidthPx; - int overviewModeBarSpacerWidthPx; - float overviewModeIconZoneRatio; - float overviewModeScaleFactor; + // Workspace icons + public int iconSizePx; + public int iconTextSizePx; + public int iconDrawablePaddingPx; + public int iconDrawablePaddingOriginalPx; - int iconSizePx; - int iconTextSizePx; - int iconDrawablePaddingPx; - int cellWidthPx; - int cellHeightPx; - int allAppsIconSizePx; - int allAppsIconTextSizePx; - int allAppsCellWidthPx; - int allAppsCellHeightPx; - int allAppsCellPaddingPx; - int folderBackgroundOffset; - int folderIconSizePx; - int folderCellWidthPx; - int folderCellHeightPx; - int hotseatCellWidthPx; - int hotseatCellHeightPx; - int hotseatIconSizePx; - int hotseatBarHeightPx; - int hotseatAllAppsRank; - int allAppsNumRows; - int allAppsNumCols; - int searchBarSpaceWidthPx; - int searchBarSpaceHeightPx; - int pageIndicatorHeightPx; - int allAppsButtonVisualSize; + public int cellWidthPx; + public int cellHeightPx; - float dragViewScale; + // Folder + public int folderBackgroundOffset; + public int folderIconSizePx; + public int folderCellWidthPx; + public int folderCellHeightPx; - int allAppsShortEdgeCount = -1; - int allAppsLongEdgeCount = -1; + // Hotseat + public int hotseatCellWidthPx; + public int hotseatCellHeightPx; + public int hotseatIconSizePx; + private int hotseatBarHeightPx; - private ArrayList mCallbacks = new ArrayList(); + // All apps + public int allAppsNumCols; + public int allAppsNumPredictiveCols; + public int allAppsButtonVisualSize; + public final int allAppsIconSizePx; + public final int allAppsIconTextSizePx; - DeviceProfile(String n, float w, float h, float r, float c, - float is, float its, float hs, float his, int dlId) { - // Ensure that we have an odd number of hotseat items (since we need to place all apps) - if (!LauncherAppState.isDisableAllApps() && hs % 2 == 0) { - throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); - } + // QSB + private int searchBarSpaceWidthPx; + private int searchBarSpaceHeightPx; - name = n; - minWidthDps = w; - minHeightDps = h; - numRows = r; - numColumns = c; - iconSize = is; - iconTextSize = its; - numHotseatIcons = hs; - hotseatIconSize = his; - defaultLayoutId = dlId; - } + public DeviceProfile(Context context, InvariantDeviceProfile inv, + Point minSize, Point maxSize, + int width, int height, boolean isLandscape) { - DeviceProfile() { - } + this.inv = inv; + this.isLandscape = isLandscape; - DeviceProfile(Context context, - ArrayList profiles, - float minWidth, float minHeight, - int wPx, int hPx, - int awPx, int ahPx, - Resources res) { + Resources res = context.getResources(); DisplayMetrics dm = res.getDisplayMetrics(); - ArrayList points = - new ArrayList(); + + // Constants from resources + isTablet = res.getBoolean(R.bool.is_tablet); + isLargeTablet = res.getBoolean(R.bool.is_large_tablet); + isPhone = !isTablet && !isLargeTablet; + + // Some more constants transposeLayoutWithOrientation = res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); - minWidthDps = minWidth; - minHeightDps = minHeight; ComponentName cn = new ComponentName(context.getPackageName(), this.getClass().getName()); @@ -178,8 +126,6 @@ public class DeviceProfile { res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height); defaultPageSpacingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing); - allAppsCellPaddingPx = - res.getDimensionPixelSize(R.dimen.dynamic_grid_all_apps_cell_padding); overviewModeMinIconZoneHeightPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_min_icon_zone_height); overviewModeMaxIconZoneHeightPx = @@ -192,91 +138,31 @@ public class DeviceProfile { res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f; overviewModeScaleFactor = res.getInteger(R.integer.config_dynamic_grid_overview_scale_percentage) / 100f; - - // Find the closes profile given the width/height - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p, 0f)); - } - DeviceProfile closestProfile = findClosestDeviceProfile(minWidth, minHeight, points); - - // Snap to the closest row count - numRows = closestProfile.numRows; - - // Snap to the closest column count - numColumns = closestProfile.numColumns; - - // Snap to the closest hotseat size - numHotseatIcons = closestProfile.numHotseatIcons; - hotseatAllAppsRank = (int) (numHotseatIcons / 2); - - // Snap to the closest default layout id - defaultLayoutId = closestProfile.defaultLayoutId; - - // Interpolate the icon size - points.clear(); - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p, p.iconSize)); - } - iconSize = invDistWeightedInterpolate(minWidth, minHeight, points); - - // AllApps uses the original non-scaled icon size - allAppsIconSizePx = DynamicGrid.pxFromDp(iconSize, dm); - - // Interpolate the icon text size - points.clear(); - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p, p.iconTextSize)); - } - iconTextSize = invDistWeightedInterpolate(minWidth, minHeight, points); iconDrawablePaddingOriginalPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding); + // AllApps uses the original non-scaled icon text size - allAppsIconTextSizePx = DynamicGrid.pxFromDp(iconTextSize, dm); + allAppsIconTextSizePx = Utilities.pxFromDp(inv.iconTextSize, dm); - // Interpolate the hotseat icon size - points.clear(); - for (DeviceProfile p : profiles) { - points.add(new DeviceProfileQuery(p, p.hotseatIconSize)); + // AllApps uses the original non-scaled icon size + allAppsIconSizePx = Utilities.pxFromDp(inv.iconSize, dm); + + // Determine sizes. + widthPx = width; + heightPx = height; + if (isLandscape) { + availableWidthPx = maxSize.x; + availableHeightPx = minSize.y; + } else { + availableWidthPx = minSize.x; + availableHeightPx = maxSize.y; } - // Hotseat - hotseatIconSize = invDistWeightedInterpolate(minWidth, minHeight, points); - - // If the partner customization apk contains any grid overrides, apply them - applyPartnerDeviceProfileOverrides(context, dm); // Calculate the remaining vars - updateFromConfiguration(context, res, wPx, hPx, awPx, ahPx); - updateAvailableDimensions(context); + updateAvailableDimensions(dm, res); computeAllAppsButtonSize(context); } - /** - * Apply any Partner customization grid overrides. - * - * Currently we support: all apps row / column count. - */ - private void applyPartnerDeviceProfileOverrides(Context ctx, DisplayMetrics dm) { - Partner p = Partner.get(ctx.getPackageManager()); - if (p != null) { - DeviceProfile partnerDp = p.getDeviceProfileOverride(dm); - if (partnerDp != null) { - if (partnerDp.numRows > 0 && partnerDp.numColumns > 0) { - numRows = partnerDp.numRows; - numColumns = partnerDp.numColumns; - } - if (partnerDp.allAppsShortEdgeCount > 0 && partnerDp.allAppsLongEdgeCount > 0) { - allAppsShortEdgeCount = partnerDp.allAppsShortEdgeCount; - allAppsLongEdgeCount = partnerDp.allAppsLongEdgeCount; - } - if (partnerDp.iconSize > 0) { - iconSize = partnerDp.iconSize; - // AllApps uses the original non-scaled icon size - allAppsIconSizePx = DynamicGrid.pxFromDp(iconSize, dm); - } - } - } - } - /** * Determine the exact visual footprint of the all apps button, taking into account scaling * and internal padding of the drawable. @@ -284,96 +170,39 @@ public class DeviceProfile { private void computeAllAppsButtonSize(Context context) { Resources res = context.getResources(); float padding = res.getInteger(R.integer.config_allAppsButtonPaddingPercent) / 100f; - LauncherAppState app = LauncherAppState.getInstance(); allAppsButtonVisualSize = (int) (hotseatIconSizePx * (1 - padding)); } - void addCallback(DeviceProfileCallbacks cb) { - mCallbacks.add(cb); - cb.onAvailableSizeChanged(this); - } - void removeCallback(DeviceProfileCallbacks cb) { - mCallbacks.remove(cb); - } - - private int getDeviceOrientation(Context context) { - WindowManager windowManager = (WindowManager) - context.getSystemService(Context.WINDOW_SERVICE); - Resources resources = context.getResources(); - DisplayMetrics dm = resources.getDisplayMetrics(); - Configuration config = resources.getConfiguration(); - int rotation = windowManager.getDefaultDisplay().getRotation(); - - boolean isLandscape = (config.orientation == Configuration.ORIENTATION_LANDSCAPE) && - (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180); - boolean isRotatedPortrait = (config.orientation == Configuration.ORIENTATION_PORTRAIT) && - (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270); - if (isLandscape || isRotatedPortrait) { - return CellLayout.LANDSCAPE; - } else { - return CellLayout.PORTRAIT; - } - } - - private void updateAvailableDimensions(Context context) { - WindowManager windowManager = (WindowManager) - context.getSystemService(Context.WINDOW_SERVICE); - Display display = windowManager.getDefaultDisplay(); - Resources resources = context.getResources(); - DisplayMetrics dm = resources.getDisplayMetrics(); - Configuration config = resources.getConfiguration(); - - // There are three possible configurations that the dynamic grid accounts for, portrait, - // landscape with the nav bar at the bottom, and landscape with the nav bar at the side. - // To prevent waiting for fitSystemWindows(), we make the observation that in landscape, - // the height is the smallest height (either with the nav bar at the bottom or to the - // side) and otherwise, the height is simply the largest possible height for a portrait - // device. - Point size = new Point(); - Point smallestSize = new Point(); - Point largestSize = new Point(); - display.getSize(size); - display.getCurrentSizeRange(smallestSize, largestSize); - availableWidthPx = size.x; - if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { - availableHeightPx = smallestSize.y; - } else { - availableHeightPx = largestSize.y; - } - + private void updateAvailableDimensions(DisplayMetrics dm, Resources res) { // Check to see if the icons fit in the new available height. If not, then we need to // shrink the icon size. float scale = 1f; int drawablePadding = iconDrawablePaddingOriginalPx; - updateIconSize(1f, drawablePadding, resources, dm); - float usedHeight = (cellHeightPx * numRows); + updateIconSize(1f, drawablePadding, res, dm); + float usedHeight = (cellHeightPx * inv.numRows); - Rect workspacePadding = getWorkspacePadding(); + // We only care about the top and bottom workspace padding, which is not affected by RTL. + Rect workspacePadding = getWorkspacePadding(false /* isLayoutRtl */); int maxHeight = (availableHeightPx - workspacePadding.top - workspacePadding.bottom); if (usedHeight > maxHeight) { scale = maxHeight / usedHeight; drawablePadding = 0; } - updateIconSize(scale, drawablePadding, resources, dm); - - // Make the callbacks - for (DeviceProfileCallbacks cb : mCallbacks) { - cb.onAvailableSizeChanged(this); - } + updateIconSize(scale, drawablePadding, res, dm); } - private void updateIconSize(float scale, int drawablePadding, Resources resources, + private void updateIconSize(float scale, int drawablePadding, Resources res, DisplayMetrics dm) { - iconSizePx = (int) (DynamicGrid.pxFromDp(iconSize, dm) * scale); - iconTextSizePx = (int) (DynamicGrid.pxFromSp(iconTextSize, dm) * scale); + iconSizePx = (int) (Utilities.pxFromDp(inv.iconSize, dm) * scale); + iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale); iconDrawablePaddingPx = drawablePadding; - hotseatIconSizePx = (int) (DynamicGrid.pxFromDp(hotseatIconSize, dm) * scale); + hotseatIconSizePx = (int) (Utilities.pxFromDp(inv.hotseatIconSize, dm) * scale); // Search Bar searchBarSpaceWidthPx = Math.min(widthPx, - resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width)); searchBarSpaceHeightPx = getSearchBarTopOffset() - + resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); + + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height); // Calculate the actual text height Paint textPaint = new Paint(); @@ -381,7 +210,7 @@ public class DeviceProfile { FontMetrics fm = textPaint.getFontMetrics(); cellWidthPx = iconSizePx; cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top); - final float scaleDps = resources.getDimensionPixelSize(R.dimen.dragViewScale); + final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); dragViewScale = (iconSizePx + scaleDps) / iconSizePx; // Hotseat @@ -394,123 +223,27 @@ public class DeviceProfile { folderCellHeightPx = cellHeightPx + edgeMarginPx; folderBackgroundOffset = -edgeMarginPx; folderIconSizePx = iconSizePx + 2 * -folderBackgroundOffset; - - // All Apps - allAppsCellWidthPx = allAppsIconSizePx; - allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx; - int maxLongEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count); - int maxShortEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count); - int minEdgeCellCount = - resources.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count); - int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount); - int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount); - - if (allAppsShortEdgeCount > 0 && allAppsLongEdgeCount > 0) { - allAppsNumRows = isLandscape ? allAppsShortEdgeCount : allAppsLongEdgeCount; - allAppsNumCols = isLandscape ? allAppsLongEdgeCount : allAppsShortEdgeCount; - } else { - allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) / - (allAppsCellHeightPx + allAppsCellPaddingPx); - allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows)); - allAppsNumCols = (availableWidthPx) / - (allAppsCellWidthPx + allAppsCellPaddingPx); - allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols)); - } } - void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx, - int awPx, int ahPx) { - Configuration configuration = resources.getConfiguration(); - isLandscape = (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE); - isTablet = resources.getBoolean(R.bool.is_tablet); - isLargeTablet = resources.getBoolean(R.bool.is_large_tablet); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { - isLayoutRtl = (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - } else { - isLayoutRtl = false; - } - widthPx = wPx; - heightPx = hPx; - availableWidthPx = awPx; - availableHeightPx = ahPx; - - updateAvailableDimensions(context); - } - - private float dist(PointF p0, PointF p1) { - return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) + - (p1.y-p0.y)*(p1.y-p0.y)); - } - - private float weight(PointF a, PointF b, - float pow) { - float d = dist(a, b); - if (d == 0f) { - return Float.POSITIVE_INFINITY; - } - return (float) (1f / Math.pow(d, pow)); - } - - /** Returns the closest device profile given the width and height and a list of profiles */ - private DeviceProfile findClosestDeviceProfile(float width, float height, - ArrayList points) { - return findClosestDeviceProfiles(width, height, points).get(0).profile; - } - - /** Returns the closest device profiles ordered by closeness to the specified width and height */ - private ArrayList findClosestDeviceProfiles(float width, float height, - ArrayList points) { - final PointF xy = new PointF(width, height); - - // Sort the profiles by their closeness to the dimensions - ArrayList pointsByNearness = points; - Collections.sort(pointsByNearness, new Comparator() { - public int compare(DeviceProfileQuery a, DeviceProfileQuery b) { - return (int) (dist(xy, a.dimens) - dist(xy, b.dimens)); - } - }); - - return pointsByNearness; - } - - private float invDistWeightedInterpolate(float width, float height, - ArrayList points) { - float sum = 0; - float weights = 0; - float pow = 5; - float kNearestNeighbors = 3; - final PointF xy = new PointF(width, height); - - ArrayList pointsByNearness = findClosestDeviceProfiles(width, height, - points); - - for (int i = 0; i < pointsByNearness.size(); ++i) { - DeviceProfileQuery p = pointsByNearness.get(i); - if (i < kNearestNeighbors) { - float w = weight(xy, p.dimens, pow); - if (w == Float.POSITIVE_INFINITY) { - return p.value; - } - weights += w; - } - } - - for (int i = 0; i < pointsByNearness.size(); ++i) { - DeviceProfileQuery p = pointsByNearness.get(i); - if (i < kNearestNeighbors) { - float w = weight(xy, p.dimens, pow); - sum += w * p.value / weights; - } - } - - return sum; + /** + * @param recyclerViewWidth the available width of the AllAppsRecyclerView + */ + public void updateAppsViewNumCols(Resources res, int recyclerViewWidth) { + int appsViewLeftMarginPx = + res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); + int allAppsCellWidthGap = + res.getDimensionPixelSize(R.dimen.all_apps_icon_width_gap); + int availableAppsWidthPx = (recyclerViewWidth > 0) ? recyclerViewWidth : availableWidthPx; + int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) / + (allAppsIconSizePx + allAppsCellWidthGap); + int numPredictiveAppCols = Math.max(inv.minAllAppsPredictionColumns, numAppsCols); + allAppsNumCols = numAppsCols; + allAppsNumPredictiveCols = numPredictiveAppCols; } /** Returns the search bar top offset */ - int getSearchBarTopOffset() { - if (isTablet() && !isVerticalBarLayout()) { + private int getSearchBarTopOffset() { + if (isTablet && !isVerticalBarLayout()) { return 4 * edgeMarginPx; } else { return 2 * edgeMarginPx; @@ -518,14 +251,9 @@ public class DeviceProfile { } /** Returns the search bar bounds in the current orientation */ - Rect getSearchBarBounds() { - return getSearchBarBounds(isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT); - } - /** Returns the search bar bounds in the specified orientation */ - Rect getSearchBarBounds(int orientation) { + public Rect getSearchBarBounds(boolean isLayoutRtl) { Rect bounds = new Rect(); - if (orientation == CellLayout.LANDSCAPE && - transposeLayoutWithOrientation) { + if (isLandscape && transposeLayoutWithOrientation) { if (isLayoutRtl) { bounds.set(availableWidthPx - searchBarSpaceHeightPx, edgeMarginPx, availableWidthPx, availableHeightPx - edgeMarginPx); @@ -534,16 +262,14 @@ public class DeviceProfile { availableHeightPx - edgeMarginPx); } } else { - if (isTablet()) { + if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing // between all icons - int width = (orientation == CellLayout.LANDSCAPE) - ? Math.max(widthPx, heightPx) - : Math.min(widthPx, heightPx); + int width = getCurrentWidth(); // XXX: If the icon size changes across orientations, we will have to take // that into account here too. int gap = (int) ((width - 2 * edgeMarginPx - - (numColumns * cellWidthPx)) / (2 * (numColumns + 1))); + (inv.numColumns * cellWidthPx)) / (2 * (inv.numColumns + 1))); bounds.set(edgeMarginPx + gap, getSearchBarTopOffset(), availableWidthPx - (edgeMarginPx + gap), searchBarSpaceHeightPx); @@ -557,36 +283,11 @@ public class DeviceProfile { return bounds; } - /** Returns the bounds of the workspace page indicators. */ - Rect getWorkspacePageIndicatorBounds(Rect insets) { - Rect workspacePadding = getWorkspacePadding(); - if (isLandscape && transposeLayoutWithOrientation) { - if (isLayoutRtl) { - return new Rect(workspacePadding.left, workspacePadding.top, - workspacePadding.left + pageIndicatorHeightPx, - heightPx - workspacePadding.bottom - insets.bottom); - } else { - int pageIndicatorLeft = widthPx - workspacePadding.right; - return new Rect(pageIndicatorLeft, workspacePadding.top, - pageIndicatorLeft + pageIndicatorHeightPx, - heightPx - workspacePadding.bottom - insets.bottom); - } - } else { - int pageIndicatorTop = heightPx - insets.bottom - workspacePadding.bottom; - return new Rect(workspacePadding.left, pageIndicatorTop, - widthPx - workspacePadding.right, pageIndicatorTop + pageIndicatorHeightPx); - } - } - /** Returns the workspace padding in the specified orientation */ - Rect getWorkspacePadding() { - return getWorkspacePadding(isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT); - } - Rect getWorkspacePadding(int orientation) { - Rect searchBarBounds = getSearchBarBounds(orientation); + Rect getWorkspacePadding(boolean isLayoutRtl) { + Rect searchBarBounds = getSearchBarBounds(isLayoutRtl); Rect padding = new Rect(); - if (orientation == CellLayout.LANDSCAPE && - transposeLayoutWithOrientation) { + if (isLandscape && transposeLayoutWithOrientation) { // Pad the left and right of the workspace with search/hotseat bar sizes if (isLayoutRtl) { padding.set(hotseatBarHeightPx, edgeMarginPx, @@ -596,22 +297,18 @@ public class DeviceProfile { hotseatBarHeightPx, edgeMarginPx); } } else { - if (isTablet()) { + if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing // between all icons float gapScale = 1f + (dragViewScale - 1f) / 2f; - int width = (orientation == CellLayout.LANDSCAPE) - ? Math.max(widthPx, heightPx) - : Math.min(widthPx, heightPx); - int height = (orientation != CellLayout.LANDSCAPE) - ? Math.max(widthPx, heightPx) - : Math.min(widthPx, heightPx); + int width = getCurrentWidth(); + int height = getCurrentHeight(); int paddingTop = searchBarBounds.bottom; int paddingBottom = hotseatBarHeightPx + pageIndicatorHeightPx; - int availableWidth = Math.max(0, width - (int) ((numColumns * cellWidthPx) + - (numColumns * gapScale * cellWidthPx))); + int availableWidth = Math.max(0, width - (int) ((inv.numColumns * cellWidthPx) + + (inv.numColumns * gapScale * cellWidthPx))); int availableHeight = Math.max(0, height - paddingTop - paddingBottom - - (int) (2 * numRows * cellHeightPx)); + - (int) (2 * inv.numRows * cellHeightPx)); padding.set(availableWidth / 2, paddingTop + availableHeight / 2, availableWidth / 2, paddingBottom + availableHeight / 2); } else { @@ -625,16 +322,15 @@ public class DeviceProfile { return padding; } - int getWorkspacePageSpacing(int orientation) { - if ((orientation == CellLayout.LANDSCAPE && - transposeLayoutWithOrientation) || isLargeTablet()) { + private int getWorkspacePageSpacing(boolean isLayoutRtl) { + if ((isLandscape && transposeLayoutWithOrientation) || isLargeTablet) { // In landscape mode the page spacing is set to the default. return defaultPageSpacingPx; } else { // In portrait, we want the pages spaced such that there is no // overhang of the previous / next page into the current page viewport. // We assume symmetrical padding in portrait mode. - return Math.max(defaultPageSpacingPx, 2 * getWorkspacePadding().left); + return Math.max(defaultPageSpacingPx, 2 * getWorkspacePadding(isLayoutRtl).left); } } @@ -645,8 +341,8 @@ public class DeviceProfile { return new Rect(0, availableHeightPx - zoneHeight, 0, availableHeightPx); } - float getOverviewModeScale() { - Rect workspacePadding = getWorkspacePadding(); + public float getOverviewModeScale(boolean isLayoutRtl) { + Rect workspacePadding = getWorkspacePadding(isLayoutRtl); Rect overviewBar = getOverviewModeButtonBarRect(); int pageSpace = availableHeightPx - workspacePadding.top - workspacePadding.bottom; return (overviewModeScaleFactor * (pageSpace - overviewBar.height())) / pageSpace; @@ -663,32 +359,26 @@ public class DeviceProfile { } } - int calculateCellWidth(int width, int countX) { + public static int calculateCellWidth(int width, int countX) { return width / countX; } - int calculateCellHeight(int height, int countY) { + public static int calculateCellHeight(int height, int countY) { return height / countY; } - boolean isPhone() { - return !isTablet && !isLargeTablet; - } - boolean isTablet() { - return isTablet; - } - boolean isLargeTablet() { - return isLargeTablet; - } - + /** + * When {@code true}, hotseat is on the bottom row when in landscape mode. + * If {@code false}, hotseat is on the right column when in landscape mode. + */ boolean isVerticalBarLayout() { return isLandscape && transposeLayoutWithOrientation; } boolean shouldFadeAdjacentWorkspaceScreens() { - return isVerticalBarLayout() || isLargeTablet(); + return isVerticalBarLayout() || isLargeTablet; } - int getVisibleChildCount(ViewGroup parent) { + private int getVisibleChildCount(ViewGroup parent) { int visibleChildren = 0; for (int i = 0; i < parent.getChildCount(); i++) { if (parent.getChildAt(i).getVisibility() != View.GONE) { @@ -700,25 +390,31 @@ public class DeviceProfile { public void layout(Launcher launcher) { FrameLayout.LayoutParams lp; - Resources res = launcher.getResources(); boolean hasVerticalBarLayout = isVerticalBarLayout(); + final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); // Layout the search bar space View searchBar = launcher.getSearchBar(); lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); if (hasVerticalBarLayout) { - // Vertical search bar space - lp.gravity = Gravity.TOP | Gravity.LEFT; + // Vertical search bar space -- The search bar is fixed in the layout to be on the left + // of the screen regardless of RTL + lp.gravity = Gravity.LEFT; lp.width = searchBarSpaceHeightPx; - lp.height = LayoutParams.WRAP_CONTENT; LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar); targets.setOrientation(LinearLayout.VERTICAL); + FrameLayout.LayoutParams targetsLp = (FrameLayout.LayoutParams) targets.getLayoutParams(); + targetsLp.gravity = Gravity.TOP; + targetsLp.height = LayoutParams.WRAP_CONTENT; + } else { // Horizontal search bar space - lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - lp.width = searchBarSpaceWidthPx; + lp.gravity = Gravity.TOP; lp.height = searchBarSpaceHeightPx; + + LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar); + targets.getLayoutParams().width = searchBarSpaceWidthPx; } searchBar.setLayoutParams(lp); @@ -726,22 +422,22 @@ public class DeviceProfile { PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace); lp = (FrameLayout.LayoutParams) workspace.getLayoutParams(); lp.gravity = Gravity.CENTER; - int orientation = isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT; - Rect padding = getWorkspacePadding(orientation); + Rect padding = getWorkspacePadding(isLayoutRtl); workspace.setLayoutParams(lp); workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom); - workspace.setPageSpacing(getWorkspacePageSpacing(orientation)); + workspace.setPageSpacing(getWorkspacePageSpacing(isLayoutRtl)); // Layout the hotseat View hotseat = launcher.findViewById(R.id.hotseat); lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams(); if (hasVerticalBarLayout) { - // Vertical hotseat - lp.gravity = Gravity.END; + // Vertical hotseat -- The hotseat is fixed in the layout to be on the right of the + // screen regardless of RTL + lp.gravity = Gravity.RIGHT; lp.width = hotseatBarHeightPx; lp.height = LayoutParams.MATCH_PARENT; hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx); - } else if (isTablet()) { + } else if (isTablet) { // Pad the hotseat with the workspace padding calculated above lp.gravity = Gravity.BOTTOM; lp.width = LayoutParams.MATCH_PARENT; @@ -777,64 +473,6 @@ public class DeviceProfile { } } - // Layout AllApps - AppsCustomizeTabHost host = (AppsCustomizeTabHost) - launcher.findViewById(R.id.apps_customize_pane); - if (host != null) { - // Center the all apps page indicator - int pageIndicatorHeight = (int) (pageIndicatorHeightPx * Math.min(1f, - (allAppsIconSizePx / DynamicGrid.DEFAULT_ICON_SIZE_PX))); - pageIndicator = host.findViewById(R.id.apps_customize_page_indicator); - if (pageIndicator != null) { - LinearLayout.LayoutParams lllp = (LinearLayout.LayoutParams) pageIndicator.getLayoutParams(); - lllp.width = LayoutParams.WRAP_CONTENT; - lllp.height = pageIndicatorHeight; - pageIndicator.setLayoutParams(lllp); - } - - AppsCustomizePagedView pagedView = (AppsCustomizePagedView) - host.findViewById(R.id.apps_customize_pane_content); - - FrameLayout fakePageContainer = (FrameLayout) - host.findViewById(R.id.fake_page_container); - FrameLayout fakePage = (FrameLayout) host.findViewById(R.id.fake_page); - - padding = new Rect(); - if (pagedView != null) { - // Constrain the dimensions of all apps so that it does not span the full width - int paddingLR = (availableWidthPx - (allAppsCellWidthPx * allAppsNumCols)) / - (2 * (allAppsNumCols + 1)); - int paddingTB = (availableHeightPx - (allAppsCellHeightPx * allAppsNumRows)) / - (2 * (allAppsNumRows + 1)); - paddingLR = Math.min(paddingLR, (int)((paddingLR + paddingTB) * 0.75f)); - paddingTB = Math.min(paddingTB, (int)((paddingLR + paddingTB) * 0.75f)); - int maxAllAppsWidth = (allAppsNumCols * (allAppsCellWidthPx + 2 * paddingLR)); - int gridPaddingLR = (availableWidthPx - maxAllAppsWidth) / 2; - // Only adjust the side paddings on landscape phones, or tablets - if ((isTablet() || isLandscape) && gridPaddingLR > (allAppsCellWidthPx / 4)) { - padding.left = padding.right = gridPaddingLR; - } - - // The icons are centered, so we can't just offset by the page indicator height - // because the empty space will actually be pageIndicatorHeight + paddingTB - padding.bottom = Math.max(0, pageIndicatorHeight - paddingTB); - - pagedView.setWidgetsPageIndicatorPadding(pageIndicatorHeight); - fakePage.setBackground(res.getDrawable(R.drawable.quantum_panel)); - - // Horizontal padding for the whole paged view - int pagedFixedViewPadding = - res.getDimensionPixelSize(R.dimen.apps_customize_horizontal_padding); - - padding.left += pagedFixedViewPadding; - padding.right += pagedFixedViewPadding; - - pagedView.setPadding(padding.left, padding.top, padding.right, padding.bottom); - fakePageContainer.setPadding(padding.left, padding.top, padding.right, padding.bottom); - - } - } - // Layout the Overview Mode ViewGroup overviewMode = launcher.getOverviewPanel(); if (overviewMode != null) { @@ -875,4 +513,16 @@ public class DeviceProfile { } } } + + private int getCurrentWidth() { + return isLandscape + ? Math.max(widthPx, heightPx) + : Math.min(widthPx, heightPx); + } + + private int getCurrentHeight() { + return isLandscape + ? Math.min(widthPx, heightPx) + : Math.max(widthPx, heightPx); + } } diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java index 480dce9998..2191455d53 100644 --- a/src/com/android/launcher3/DragController.java +++ b/src/com/android/launcher3/DragController.java @@ -34,6 +34,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.inputmethod.InputMethodManager; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.HashSet; @@ -49,8 +51,8 @@ public class DragController { /** Indicates the drag is a copy. */ public static int DRAG_ACTION_COPY = 1; - private static final int SCROLL_DELAY = 500; - private static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; + public static final int SCROLL_DELAY = 500; + public static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; private static final boolean PROFILE_DRAWING_DURING_DRAG = false; @@ -63,16 +65,20 @@ public class DragController { private static final float MAX_FLING_DEGREES = 35f; - private Launcher mLauncher; + @Thunk Launcher mLauncher; private Handler mHandler; // temporaries to avoid gc thrash private Rect mRectTemp = new Rect(); private final int[] mCoordinatesTemp = new int[2]; + private final boolean mIsRtl; /** Whether or not we're dragging. */ private boolean mDragging; + /** Whether or not this is an accessible drag operation */ + private boolean mIsAccessibleDrag; + /** X coordinate of the down event. */ private int mMotionDownX; @@ -99,17 +105,17 @@ public class DragController { private View mMoveTarget; - private DragScroller mDragScroller; - private int mScrollState = SCROLL_OUTSIDE_ZONE; + @Thunk DragScroller mDragScroller; + @Thunk int mScrollState = SCROLL_OUTSIDE_ZONE; private ScrollRunnable mScrollRunnable = new ScrollRunnable(); private DropTarget mLastDropTarget; private InputMethodManager mInputMethodManager; - private int mLastTouch[] = new int[2]; - private long mLastTouchUpTime = -1; - private int mDistanceSinceScroll = 0; + @Thunk int mLastTouch[] = new int[2]; + @Thunk long mLastTouchUpTime = -1; + @Thunk int mDistanceSinceScroll = 0; private int mTmpPoint[] = new int[2]; private Rect mDragLayerRect = new Rect(); @@ -120,7 +126,7 @@ public class DragController { /** * Interface to receive notifications when a drag starts or stops */ - interface DragListener { + public interface DragListener { /** * A drag has begun * @@ -152,6 +158,7 @@ public class DragController { float density = r.getDisplayMetrics().density; mFlingToDeleteThresholdVelocity = (int) (r.getInteger(R.integer.config_flingToDeleteMinVelocity) * density); + mIsRtl = Utilities.isRtl(r); } public boolean dragging() { @@ -167,22 +174,21 @@ public class DragController { * @param dragInfo The data associated with the object that is being dragged * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or * {@link #DRAG_ACTION_COPY} + * @param viewImageBounds the position of the image inside the view * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. * Makes dragging feel more precise, e.g. you can clip out a transparent border */ - public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction, - Point extraPadding, float initialDragViewScale) { + public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, + Rect viewImageBounds, int dragAction, float initialDragViewScale) { int[] loc = mCoordinatesTemp; mLauncher.getDragLayer().getLocationInDragLayer(v, loc); - int viewExtraPaddingLeft = extraPadding != null ? extraPadding.x : 0; - int viewExtraPaddingTop = extraPadding != null ? extraPadding.y : 0; - int dragLayerX = loc[0] + v.getPaddingLeft() + viewExtraPaddingLeft + - (int) ((initialDragViewScale * bmp.getWidth() - bmp.getWidth()) / 2); - int dragLayerY = loc[1] + v.getPaddingTop() + viewExtraPaddingTop + - (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2); + int dragLayerX = loc[0] + viewImageBounds.left + + (int) ((initialDragViewScale * bmp.getWidth() - bmp.getWidth()) / 2); + int dragLayerY = loc[1] + viewImageBounds.top + + (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2); startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, - null, initialDragViewScale); + null, initialDragViewScale, false); if (dragAction == DRAG_ACTION_MOVE) { v.setVisibility(View.GONE); @@ -202,10 +208,11 @@ public class DragController { * {@link #DRAG_ACTION_COPY} * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. * Makes dragging feel more precise, e.g. you can clip out a transparent border + * @param accessible whether this drag should occur in accessibility mode */ public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY, DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion, - float initialDragViewScale) { + float initialDragViewScale, boolean accessible) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } @@ -228,12 +235,21 @@ public class DragController { final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; mDragging = true; + mIsAccessibleDrag = accessible; mDragObject = new DropTarget.DragObject(); mDragObject.dragComplete = false; - mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); - mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + if (mIsAccessibleDrag) { + // For an accessible drag, we assume the view is being dragged from the center. + mDragObject.xOffset = b.getWidth() / 2; + mDragObject.yOffset = b.getHeight() / 2; + mDragObject.accessibleDrag = true; + } else { + mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); + mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + } + mDragObject.dragSource = source; mDragObject.dragInfo = dragInfo; @@ -349,6 +365,7 @@ public class DragController { private void endDrag() { if (mDragging) { mDragging = false; + mIsAccessibleDrag = false; clearScrollRunnable(); boolean isDeferred = false; if (mDragObject.dragView != null) { @@ -361,7 +378,7 @@ public class DragController { // Only end the drag if we are not deferred if (!isDeferred) { - for (DragListener listener : mListeners) { + for (DragListener listener : new ArrayList<>(mListeners)) { listener.onDragEnd(); } } @@ -378,13 +395,13 @@ public class DragController { if (mDragObject.deferDragViewCleanupPostAnimation) { // If we skipped calling onDragEnd() before, do it now - for (DragListener listener : mListeners) { + for (DragListener listener : new ArrayList<>(mListeners)) { listener.onDragEnd(); } } } - void onDeferredEndFling(DropTarget.DragObject d) { + public void onDeferredEndFling(DropTarget.DragObject d) { d.dragSource.onFlingToDeleteCompleted(); } @@ -421,6 +438,10 @@ public class DragController { + mDragging); } + if (mIsAccessibleDrag) { + return false; + } + // Update the velocity tracker acquireVelocityTrackerAndAddMovement(ev); @@ -442,7 +463,7 @@ public class DragController { mLastTouchUpTime = System.currentTimeMillis(); if (mDragging) { PointF vec = isFlingingToDelete(mDragObject.dragSource); - if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) { + if (!DeleteDropTarget.supportsDrop(mDragObject.dragInfo)) { vec = null; } if (vec != null) { @@ -493,8 +514,7 @@ public class DragController { checkTouchMove(dropTarget); // Check if we are hovering over the scroll areas - mDistanceSinceScroll += - Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); + mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y); mLastTouch[0] = x; mLastTouch[1] = y; checkScrollState(x, y); @@ -525,13 +545,12 @@ public class DragController { mLastDropTarget = dropTarget; } - private void checkScrollState(int x, int y) { + @Thunk void checkScrollState(int x, int y) { final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop(); final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY; final DragLayer dragLayer = mLauncher.getDragLayer(); - final boolean isRtl = (dragLayer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); - final int forwardDirection = isRtl ? SCROLL_RIGHT : SCROLL_LEFT; - final int backwardsDirection = isRtl ? SCROLL_LEFT : SCROLL_RIGHT; + final int forwardDirection = mIsRtl ? SCROLL_RIGHT : SCROLL_LEFT; + final int backwardsDirection = mIsRtl ? SCROLL_LEFT : SCROLL_RIGHT; if (x < mScrollZone) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { @@ -560,7 +579,7 @@ public class DragController { * Call this from a drag source view. */ public boolean onTouchEvent(MotionEvent ev) { - if (!mDragging) { + if (!mDragging || mIsAccessibleDrag) { return false; } @@ -596,7 +615,7 @@ public class DragController { if (mDragging) { PointF vec = isFlingingToDelete(mDragObject.dragSource); - if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) { + if (!DeleteDropTarget.supportsDrop(mDragObject.dragInfo)) { vec = null; } if (vec != null) { @@ -616,6 +635,35 @@ public class DragController { return true; } + /** + * Since accessible drag and drop won't cause the same sequence of touch events, we manually + * inject the appropriate state. + */ + public void prepareAccessibleDrag(int x, int y) { + mMotionDownX = x; + mMotionDownY = y; + mLastDropTarget = null; + } + + /** + * As above, since accessible drag and drop won't cause the same sequence of touch events, + * we manually ensure appropriate drag and drop events get emulated for accessible drag. + */ + public void completeAccessibleDrag(int[] location) { + final int[] coordinates = mCoordinatesTemp; + + // We make sure that we prime the target for drop. + DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates); + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + checkTouchMove(dropTarget); + + dropTarget.prepareAccessibilityDrop(); + // Perform the drop + drop(location[0], location[1]); + endDrag(); + } + /** * Determines whether the user flung the current item to delete it. * @@ -662,8 +710,7 @@ public class DragController { mDragObject.dragComplete = true; mFlingToDeleteDropTarget.onDragExit(mDragObject); if (mFlingToDeleteDropTarget.acceptDrop(mDragObject)) { - mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, mDragObject.x, mDragObject.y, - vel); + mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, vel); accepted = true; } mDragObject.dragSource.onDropCompleted((View) mFlingToDeleteDropTarget, mDragObject, true, diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index a352b79146..aaa14e6a6c 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -24,6 +24,7 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; @@ -38,7 +39,8 @@ import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.TextView; -import com.android.launcher3.InsettableFrameLayout.LayoutParams; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -46,30 +48,34 @@ import java.util.ArrayList; * A ViewGroup that coordinates dragging across its descendants */ public class DragLayer extends InsettableFrameLayout { - private DragController mDragController; - private int[] mTmpXY = new int[2]; + + public static final int ANIMATION_END_DISAPPEAR = 0; + public static final int ANIMATION_END_REMAIN_VISIBLE = 2; + + // Scrim color without any alpha component. + private static final int SCRIM_COLOR = Color.BLACK & 0x00FFFFFF; + + private final int[] mTmpXY = new int[2]; + + @Thunk DragController mDragController; private int mXDown, mYDown; private Launcher mLauncher; // Variables relating to resizing widgets - private final ArrayList mResizeFrames = - new ArrayList(); + private final ArrayList mResizeFrames = new ArrayList<>(); + private final boolean mIsRtl; private AppWidgetResizeFrame mCurrentResizeFrame; // Variables relating to animation of views after drop private ValueAnimator mDropAnim = null; - private ValueAnimator mFadeOutAnim = null; - private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); - private DragView mDropView = null; - private int mAnchorViewInitialScrollX = 0; - private View mAnchorView = null; + private final TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); + @Thunk DragView mDropView = null; + @Thunk int mAnchorViewInitialScrollX = 0; + @Thunk View mAnchorView = null; private boolean mHoverPointClosesFolder = false; - private Rect mHitRect = new Rect(); - public static final int ANIMATION_END_DISAPPEAR = 0; - public static final int ANIMATION_END_FADE_OUT = 1; - public static final int ANIMATION_END_REMAIN_VISIBLE = 2; + private final Rect mHitRect = new Rect(); private TouchCompleteListener mTouchCompleteListener; @@ -78,10 +84,10 @@ public class DragLayer extends InsettableFrameLayout { private int mChildCountOnLastUpdate = -1; // Darkening scrim - private Drawable mBackground; private float mBackgroundAlpha = 0; // Related to adjacent page hints + private final Rect mScrollChildPosition = new Rect(); private boolean mInScrollArea; private boolean mShowPageHints; private Drawable mLeftHoverDrawable; @@ -109,7 +115,7 @@ public class DragLayer extends InsettableFrameLayout { mRightHoverDrawable = res.getDrawable(R.drawable.page_hover_right); mLeftHoverDrawableActive = res.getDrawable(R.drawable.page_hover_left_active); mRightHoverDrawableActive = res.getDrawable(R.drawable.page_hover_right_active); - mBackground = res.getDrawable(R.drawable.apps_customize_bg); + mIsRtl = Utilities.isRtl(res); } public void setup(Launcher launcher, DragController controller) { @@ -152,6 +158,14 @@ public class DragLayer extends InsettableFrameLayout { return false; } + private boolean isEventOverDropTargetBar(MotionEvent ev) { + getDescendantRectRelativeToSelf(mLauncher.getSearchBar(), mHitRect); + if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { + return true; + } + return false; + } + public void setBlockTouch(boolean block) { mBlockTouches = block; } @@ -187,10 +201,16 @@ public class DragLayer extends InsettableFrameLayout { } } - getDescendantRectRelativeToSelf(currentFolder, hitRect); if (!isEventOverFolder(currentFolder, ev)) { - mLauncher.closeFolder(); - return true; + if (isInAccessibleDrag()) { + // Do not close the folder if in drag and drop. + if (!isEventOverDropTargetBar(ev)) { + return true; + } + } else { + mLauncher.closeFolder(); + return true; + } } } return false; @@ -227,11 +247,12 @@ public class DragLayer extends InsettableFrameLayout { getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (accessibilityManager.isTouchExplorationEnabled()) { final int action = ev.getAction(); - boolean isOverFolder; + boolean isOverFolderOrSearchBar; switch (action) { case MotionEvent.ACTION_HOVER_ENTER: - isOverFolder = isEventOverFolder(currentFolder, ev); - if (!isOverFolder) { + isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) || + (isInAccessibleDrag() && isEventOverDropTargetBar(ev)); + if (!isOverFolderOrSearchBar) { sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); mHoverPointClosesFolder = true; return true; @@ -239,12 +260,13 @@ public class DragLayer extends InsettableFrameLayout { mHoverPointClosesFolder = false; break; case MotionEvent.ACTION_HOVER_MOVE: - isOverFolder = isEventOverFolder(currentFolder, ev); - if (!isOverFolder && !mHoverPointClosesFolder) { + isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) || + (isInAccessibleDrag() && isEventOverDropTargetBar(ev)); + if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) { sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); mHoverPointClosesFolder = true; return true; - } else if (!isOverFolder) { + } else if (!isOverFolderOrSearchBar) { return true; } mHoverPointClosesFolder = false; @@ -267,6 +289,12 @@ public class DragLayer extends InsettableFrameLayout { } } + private boolean isInAccessibleDrag() { + LauncherAccessibilityDelegate delegate = LauncherAppState + .getInstance().getAccessibilityDelegate(); + return delegate != null && delegate.isInAccessibleDrag(); + } + @Override public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); @@ -274,6 +302,10 @@ public class DragLayer extends InsettableFrameLayout { if (child == currentFolder) { return super.onRequestSendAccessibilityEvent(child, event); } + + if (isInAccessibleDrag() && child instanceof SearchDropTargetBar) { + return super.onRequestSendAccessibilityEvent(child, event); + } // Skip propagating onRequestSendAccessibilityEvent all for other children // when a folder is open return false; @@ -287,6 +319,10 @@ public class DragLayer extends InsettableFrameLayout { if (currentFolder != null) { // Only add the folder as a child for accessibility when it is open childrenForAccessibility.add(currentFolder); + + if (isInAccessibleDrag()) { + childrenForAccessibility.add(mLauncher.getSearchBar()); + } } else { super.addChildrenForAccessibility(childrenForAccessibility); } @@ -656,8 +692,7 @@ public class DragLayer extends InsettableFrameLayout { final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) { // Calculate the duration of the animation based on the object's distance - final float dist = (float) Math.sqrt(Math.pow(to.left - from.left, 2) + - Math.pow(to.top - from.top, 2)); + final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top); final Resources res = getResources(); final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); @@ -725,7 +760,6 @@ public class DragLayer extends InsettableFrameLayout { final int animationEndStyle, View anchorView) { // Clean up the previous animations if (mDropAnim != null) mDropAnim.cancel(); - if (mFadeOutAnim != null) mFadeOutAnim.cancel(); // Show the drop view if it was previously hidden mDropView = view; @@ -753,9 +787,6 @@ public class DragLayer extends InsettableFrameLayout { case ANIMATION_END_DISAPPEAR: clearAnimatedView(); break; - case ANIMATION_END_FADE_OUT: - fadeOutDragView(); - break; case ANIMATION_END_REMAIN_VISIBLE: break; } @@ -779,31 +810,6 @@ public class DragLayer extends InsettableFrameLayout { return mDropView; } - private void fadeOutDragView() { - mFadeOutAnim = new ValueAnimator(); - mFadeOutAnim.setDuration(150); - mFadeOutAnim.setFloatValues(0f, 1f); - mFadeOutAnim.removeAllUpdateListeners(); - mFadeOutAnim.addUpdateListener(new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - final float percent = (Float) animation.getAnimatedValue(); - - float alpha = 1 - percent; - mDropView.setAlpha(alpha); - } - }); - mFadeOutAnim.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - if (mDropView != null) { - mDragController.onDeferredEndDrag(mDropView); - } - mDropView = null; - invalidate(); - } - }); - mFadeOutAnim.start(); - } - @Override public void onChildViewAdded(View parent, View child) { super.onChildViewAdded(parent, child); @@ -880,6 +886,9 @@ public class DragLayer extends InsettableFrameLayout { void showPageHints() { mShowPageHints = true; + Workspace workspace = mLauncher.getWorkspace(); + getDescendantRectRelativeToSelf(workspace.getChildAt(workspace.numCustomPages()), + mScrollChildPosition); invalidate(); } @@ -888,21 +897,12 @@ public class DragLayer extends InsettableFrameLayout { invalidate(); } - /** - * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. - */ - private boolean isLayoutRtl() { - return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); - } - @Override protected void dispatchDraw(Canvas canvas) { - // Draw the background gradient below children. - if (mBackground != null && mBackgroundAlpha > 0.0f) { + // Draw the background below children. + if (mBackgroundAlpha > 0.0f) { int alpha = (int) (mBackgroundAlpha * 255); - mBackground.setAlpha(alpha); - mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); - mBackground.draw(canvas); + canvas.drawColor((alpha << 24) | SCRIM_COLOR); } super.dispatchDraw(canvas); @@ -912,27 +912,22 @@ public class DragLayer extends InsettableFrameLayout { if (mShowPageHints) { Workspace workspace = mLauncher.getWorkspace(); int width = getMeasuredWidth(); - Rect childRect = new Rect(); - getDescendantRectRelativeToSelf(workspace.getChildAt(workspace.getChildCount() - 1), - childRect); - int page = workspace.getNextPage(); - final boolean isRtl = isLayoutRtl(); - CellLayout leftPage = (CellLayout) workspace.getChildAt(isRtl ? page + 1 : page - 1); - CellLayout rightPage = (CellLayout) workspace.getChildAt(isRtl ? page - 1 : page + 1); + CellLayout leftPage = (CellLayout) workspace.getChildAt(mIsRtl ? page + 1 : page - 1); + CellLayout rightPage = (CellLayout) workspace.getChildAt(mIsRtl ? page - 1 : page + 1); if (leftPage != null && leftPage.isDragTarget()) { Drawable left = mInScrollArea && leftPage.getIsDragOverlapping() ? mLeftHoverDrawableActive : mLeftHoverDrawable; - left.setBounds(0, childRect.top, - left.getIntrinsicWidth(), childRect.bottom); + left.setBounds(0, mScrollChildPosition.top, + left.getIntrinsicWidth(), mScrollChildPosition.bottom); left.draw(canvas); } if (rightPage != null && rightPage.isDragTarget()) { Drawable right = mInScrollArea && rightPage.getIsDragOverlapping() ? mRightHoverDrawableActive : mRightHoverDrawable; right.setBounds(width - right.getIntrinsicWidth(), - childRect.top, width, childRect.bottom); + mScrollChildPosition.top, width, mScrollChildPosition.bottom); right.draw(canvas); } } diff --git a/src/com/android/launcher3/DragSource.java b/src/com/android/launcher3/DragSource.java index 7369eeac2c..2a1346ef5d 100644 --- a/src/com/android/launcher3/DragSource.java +++ b/src/com/android/launcher3/DragSource.java @@ -22,9 +22,9 @@ import com.android.launcher3.DropTarget.DragObject; /** * Interface defining an object that can originate a drag. - * */ public interface DragSource { + /** * @return whether items dragged from this source supports */ diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java index ea34e46f9e..dfa8202a73 100644 --- a/src/com/android/launcher3/DragView.java +++ b/src/com/android/launcher3/DragView.java @@ -16,25 +16,35 @@ package com.android.launcher3; +import android.animation.FloatArrayEvaluator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.Point; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; +import android.os.Build; import android.view.View; import android.view.animation.DecelerateInterpolator; +import com.android.launcher3.util.Thunk; + +import java.util.Arrays; + public class DragView extends View { - private static float sDragAlpha = 1f; + public static int COLOR_CHANGE_DURATION = 120; + + @Thunk static float sDragAlpha = 1f; private Bitmap mBitmap; private Bitmap mCrossFadeBitmap; - private Paint mPaint; + @Thunk Paint mPaint; private int mRegistrationX; private int mRegistrationY; @@ -42,16 +52,19 @@ public class DragView extends View { private Rect mDragRegion = null; private DragLayer mDragLayer = null; private boolean mHasDrawn = false; - private float mCrossFadeProgress = 0f; + @Thunk float mCrossFadeProgress = 0f; ValueAnimator mAnim; - private float mOffsetX = 0.0f; - private float mOffsetY = 0.0f; + @Thunk float mOffsetX = 0.0f; + @Thunk float mOffsetY = 0.0f; private float mInitialScale = 1f; // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace // size. This is ignored for non-icons. private float mIntrinsicIconScale = 1f; + @Thunk float[] mCurrentFilter; + private ValueAnimator mFilterAnimator; + /** * Construct the drag view. *

@@ -63,6 +76,7 @@ public class DragView extends View { * @param registrationX The x coordinate of the registration point. * @param registrationY The y coordinate of the registration point. */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, int left, int top, int width, int height, final float initialScale) { super(launcher); @@ -70,8 +84,6 @@ public class DragView extends View { mInitialScale = initialScale; final Resources res = getResources(); - final float offsetX = res.getDimensionPixelSize(R.dimen.dragViewOffsetX); - final float offsetY = res.getDimensionPixelSize(R.dimen.dragViewOffsetY); final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale); final float scale = (width + scaleDps) / width; @@ -87,8 +99,8 @@ public class DragView extends View { public void onAnimationUpdate(ValueAnimator animation) { final float value = (Float) animation.getAnimatedValue(); - final int deltaX = (int) ((value * offsetX) - mOffsetX); - final int deltaY = (int) ((value * offsetY) - mOffsetY); + final int deltaX = (int) (-mOffsetX); + final int deltaY = (int) (-mOffsetY); mOffsetX += deltaX; mOffsetY += deltaY; @@ -118,6 +130,10 @@ public class DragView extends View { int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); measure(ms, ms); mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + + if (Utilities.isLmpOrAbove()) { + setElevation(getResources().getDimension(R.dimen.drag_elevation)); + } } /** Sets the scale of the view over the normal workspace icon size. */ @@ -229,11 +245,49 @@ public class DragView extends View { mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); } if (color != 0) { - mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + ColorMatrix m1 = new ColorMatrix(); + m1.setSaturation(0); + + ColorMatrix m2 = new ColorMatrix(); + setColorScale(color, m2); + m1.postConcat(m2); + + if (Utilities.isLmpOrAbove()) { + animateFilterTo(m1.getArray()); + } else { + mPaint.setColorFilter(new ColorMatrixColorFilter(m1)); + invalidate(); + } } else { - mPaint.setColorFilter(null); + if (!Utilities.isLmpOrAbove() || mCurrentFilter == null) { + mPaint.setColorFilter(null); + invalidate(); + } else { + animateFilterTo(new ColorMatrix().getArray()); + } } - invalidate(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void animateFilterTo(float[] targetFilter) { + float[] oldFilter = mCurrentFilter == null ? new ColorMatrix().getArray() : mCurrentFilter; + mCurrentFilter = Arrays.copyOf(oldFilter, oldFilter.length); + + if (mFilterAnimator != null) { + mFilterAnimator.cancel(); + } + mFilterAnimator = ValueAnimator.ofObject(new FloatArrayEvaluator(mCurrentFilter), + oldFilter, targetFilter); + mFilterAnimator.setDuration(COLOR_CHANGE_DURATION); + mFilterAnimator.addUpdateListener(new AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mPaint.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter)); + invalidate(); + } + }); + mFilterAnimator.start(); } public boolean hasDrawn() { @@ -300,5 +354,9 @@ public class DragView extends View { mDragLayer.removeView(DragView.this); } } -} + public static void setColorScale(int color, ColorMatrix target) { + target.setScale(Color.red(color) / 255f, Color.green(color) / 255f, + Color.blue(color) / 255f, Color.alpha(color) / 255f); + } +} diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java index 64f0ac867c..c8fac54669 100644 --- a/src/com/android/launcher3/DropTarget.java +++ b/src/com/android/launcher3/DropTarget.java @@ -16,10 +16,8 @@ package com.android.launcher3; -import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; -import android.util.Log; /** * Interface defining an object that can receive a drag. @@ -29,7 +27,7 @@ public interface DropTarget { public static final String TAG = "DropTarget"; - class DragObject { + public static class DragObject { public int x = -1; public int y = -1; @@ -54,6 +52,9 @@ public interface DropTarget { /** Where the drag originated */ public DragSource dragSource = null; + /** The object is part of an accessible drag operation */ + public boolean accessibleDrag; + /** Post drag animation runnable */ public Runnable postAnimationRunnable = null; @@ -65,42 +66,28 @@ public interface DropTarget { public DragObject() { } - } - public static class DragEnforcer implements DragController.DragListener { - int dragParity = 0; + /** + * This is used to compute the visual center of the dragView. This point is then + * used to visualize drop locations and determine where to drop an item. The idea is that + * the visual center represents the user's interpretation of where the item is, and hence + * is the appropriate point to use when determining drop location. + */ + public final float[] getVisualCenter(float[] recycle) { + final float res[] = (recycle == null) ? new float[2] : recycle; - public DragEnforcer(Context context) { - Launcher launcher = (Launcher) context; - launcher.getDragController().addDragListener(this); - } + // These represent the visual top and left of drag view if a dragRect was provided. + // If a dragRect was not provided, then they correspond to the actual view left and + // top, as the dragRect is in that case taken to be the entire dragView. + // R.dimen.dragViewOffsetY. + int left = x - xOffset; + int top = y - yOffset; - void onDragEnter() { - dragParity++; - if (dragParity != 1) { - Log.e(TAG, "onDragEnter: Drag contract violated: " + dragParity); - } - } + // In order to find the visual center, we shift by half the dragRect + res[0] = left + dragView.getDragRegion().width() / 2; + res[1] = top + dragView.getDragRegion().height() / 2; - void onDragExit() { - dragParity--; - if (dragParity != 0) { - Log.e(TAG, "onDragExit: Drag contract violated: " + dragParity); - } - } - - @Override - public void onDragStart(DragSource source, Object info, int dragAction) { - if (dragParity != 0) { - Log.e(TAG, "onDragEnter: Drag contract violated: " + dragParity); - } - } - - @Override - public void onDragEnd() { - if (dragParity != 0) { - Log.e(TAG, "onDragExit: Drag contract violated: " + dragParity); - } + return res; } } @@ -113,7 +100,7 @@ public interface DropTarget { /** * Handle an object being dropped on the DropTarget - * + * * @param source DragSource where the drag started * @param x X coordinate of the drop location * @param y Y coordinate of the drop location @@ -138,12 +125,12 @@ public interface DropTarget { * of onDrop(). (This is only called on objects that are set as the DragController's * fling-to-delete target. */ - void onFlingToDelete(DragObject dragObject, int x, int y, PointF vec); + void onFlingToDelete(DragObject dragObject, PointF vec); /** * Check if a drop action can occur at, or near, the requested location. * This will be called just before onDrop. - * + * * @param source DragSource where the drag started * @param x X coordinate of the drop location * @param y Y coordinate of the drop location @@ -157,6 +144,8 @@ public interface DropTarget { */ boolean acceptDrop(DragObject dragObject); + void prepareAccessibilityDrop(); + // These methods are implemented in Views void getHitRectRelativeToDragLayer(Rect outRect); void getLocationInDragLayer(int[] loc); diff --git a/src/com/android/launcher3/DummyWidget.java b/src/com/android/launcher3/DummyWidget.java new file mode 100644 index 0000000000..59cd805012 --- /dev/null +++ b/src/com/android/launcher3/DummyWidget.java @@ -0,0 +1,50 @@ +package com.android.launcher3; + +import android.appwidget.AppWidgetProviderInfo; + +public class DummyWidget implements CustomAppWidget { + @Override + public String getLabel() { + return "Dumb Launcher Widget"; + } + + @Override + public int getPreviewImage() { + return 0; + } + + @Override + public int getIcon() { + return 0; + } + + @Override + public int getWidgetLayout() { + return R.layout.dummy_widget; + } + + @Override + public int getSpanX() { + return 2; + } + + @Override + public int getSpanY() { + return 2; + } + + @Override + public int getMinSpanX() { + return 1; + } + + @Override + public int getMinSpanY() { + return 1; + } + + @Override + public int getResizeMode() { + return AppWidgetProviderInfo.RESIZE_BOTH; + } +} diff --git a/src/com/android/launcher3/DynamicGrid.java b/src/com/android/launcher3/DynamicGrid.java deleted file mode 100644 index aa08148d2a..0000000000 --- a/src/com/android/launcher3/DynamicGrid.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.content.res.Resources; -import android.util.DisplayMetrics; -import android.util.TypedValue; - -import java.util.ArrayList; - - -public class DynamicGrid { - @SuppressWarnings("unused") - private static final String TAG = "DynamicGrid"; - - private DeviceProfile mProfile; - private float mMinWidth; - private float mMinHeight; - - // This is a static that we use for the default icon size on a 4/5-inch phone - static float DEFAULT_ICON_SIZE_DP = 60; - static float DEFAULT_ICON_SIZE_PX = 0; - - public static float dpiFromPx(int size, DisplayMetrics metrics){ - float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; - return (size / densityRatio); - } - public static int pxFromDp(float size, DisplayMetrics metrics) { - return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - size, metrics)); - } - public static int pxFromSp(float size, DisplayMetrics metrics) { - return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, - size, metrics)); - } - - public DynamicGrid(Context context, Resources resources, - int minWidthPx, int minHeightPx, - int widthPx, int heightPx, - int awPx, int ahPx) { - DisplayMetrics dm = resources.getDisplayMetrics(); - ArrayList deviceProfiles = - new ArrayList(); - boolean hasAA = !LauncherAppState.isDisableAllApps(); - DEFAULT_ICON_SIZE_PX = pxFromDp(DEFAULT_ICON_SIZE_DP, dm); - // Our phone profiles include the bar sizes in each orientation - deviceProfiles.add(new DeviceProfile("Super Short Stubby", - 255, 300, 2, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Shorter Stubby", - 255, 400, 3, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Short Stubby", - 275, 420, 3, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Stubby", - 255, 450, 3, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Nexus S", - 296, 491.33f, 4, 4, 48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Nexus 4", - 335, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Nexus 5", - 359, 567, 4, 4, DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4)); - deviceProfiles.add(new DeviceProfile("Large Phone", - 406, 694, 5, 5, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); - // The tablet profile is odd in that the landscape orientation - // also includes the nav bar on the side - deviceProfiles.add(new DeviceProfile("Nexus 7", - 575, 904, 5, 6, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); - // Larger tablet profiles always have system bars on the top & bottom - deviceProfiles.add(new DeviceProfile("Nexus 10", - 727, 1207, 5, 6, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); - deviceProfiles.add(new DeviceProfile("20-inch Tablet", - 1527, 2527, 7, 7, 100, 20, 7, 72, R.xml.default_workspace_4x4)); - mMinWidth = dpiFromPx(minWidthPx, dm); - mMinHeight = dpiFromPx(minHeightPx, dm); - mProfile = new DeviceProfile(context, deviceProfiles, - mMinWidth, mMinHeight, - widthPx, heightPx, - awPx, ahPx, - resources); - } - - public DeviceProfile getDeviceProfile() { - return mProfile; - } - - public String toString() { - return "-------- DYNAMIC GRID ------- \n" + - "Wd: " + mProfile.minWidthDps + ", Hd: " + mProfile.minHeightDps + - ", W: " + mProfile.widthPx + ", H: " + mProfile.heightPx + - " [r: " + mProfile.numRows + ", c: " + mProfile.numColumns + - ", is: " + mProfile.iconSizePx + ", its: " + mProfile.iconTextSizePx + - ", cw: " + mProfile.cellWidthPx + ", ch: " + mProfile.cellHeightPx + - ", hc: " + mProfile.numHotseatIcons + ", his: " + mProfile.hotseatIconSizePx + "]"; - } -} diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index ff02bbbc33..28e923e67a 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -32,7 +32,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.SparseArray; -class FastBitmapDrawable extends Drawable { +public class FastBitmapDrawable extends Drawable { static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { @@ -72,7 +72,7 @@ class FastBitmapDrawable extends Drawable { private boolean mPressed = false; private ObjectAnimator mPressedAnimator; - FastBitmapDrawable(Bitmap b) { + public FastBitmapDrawable(Bitmap b) { mAlpha = 255; mBitmap = b; setBounds(0, 0, b.getWidth(), b.getHeight()); diff --git a/src/com/android/launcher3/FastBitmapView.java b/src/com/android/launcher3/FastBitmapView.java deleted file mode 100644 index 0937eb75eb..0000000000 --- a/src/com/android/launcher3/FastBitmapView.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.view.View; - -public class FastBitmapView extends View { - - private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); - private Bitmap mBitmap; - - public FastBitmapView(Context context) { - super(context); - } - - /** - * Applies the new bitmap. - * @return true if the view was invalidated. - */ - public boolean setBitmap(Bitmap b) { - if (b != mBitmap){ - if (mBitmap != null) { - invalidate(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); - } - mBitmap = b; - if (mBitmap != null) { - invalidate(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); - } - return true; - } - return false; - } - - @Override - protected void onDraw(Canvas canvas) { - if (mBitmap != null) { - canvas.drawBitmap(mBitmap, 0, 0, mPaint); - } - } -} diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java index 095c5631d5..a51ddd4b88 100644 --- a/src/com/android/launcher3/FirstFrameAnimatorHelper.java +++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java @@ -24,6 +24,8 @@ import android.view.View; import android.view.ViewPropertyAnimator; import android.view.ViewTreeObserver; +import com.android.launcher3.util.Thunk; + /* * This is a helper class that listens to updates from the corresponding animation. * For the first two frames, it adjusts the current play time of the animation to @@ -41,7 +43,7 @@ public class FirstFrameAnimatorHelper extends AnimatorListenerAdapter private boolean mAdjustedSecondFrameTime; private static ViewTreeObserver.OnDrawListener sGlobalDrawListener; - private static long sGlobalFrameCounter; + @Thunk static long sGlobalFrameCounter; private static boolean sVisible; public FirstFrameAnimatorHelper(ValueAnimator animator, View target) { diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java index e607047188..57aec32801 100644 --- a/src/com/android/launcher3/FocusHelper.java +++ b/src/com/android/launcher3/FocusHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,658 +16,445 @@ package com.android.launcher3; -import android.content.res.Configuration; +import android.util.Log; import android.view.KeyEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewGroup; -import android.widget.ScrollView; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; +import com.android.launcher3.util.FocusLogic; +import com.android.launcher3.util.Thunk; /** * A keyboard listener we set on all the workspace icons. */ class IconKeyEventListener implements View.OnKeyListener { + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleIconKeyEvent(v, keyCode, event); } } -/** - * A keyboard listener we set on all the workspace icons. - */ -class FolderKeyEventListener implements View.OnKeyListener { - public boolean onKey(View v, int keyCode, KeyEvent event) { - return FocusHelper.handleFolderKeyEvent(v, keyCode, event); - } -} - /** * A keyboard listener we set on all the hotseat buttons. */ class HotseatIconKeyEventListener implements View.OnKeyListener { + @Override public boolean onKey(View v, int keyCode, KeyEvent event) { - final Configuration configuration = v.getResources().getConfiguration(); - return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation); + return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event); } } public class FocusHelper { + private static final String TAG = "FocusHelper"; + private static final boolean DEBUG = false; + /** - * Returns the Viewgroup containing page contents for the page at the index specified. + * Handles key events in paged folder. */ - private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) { - ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index); - if (page instanceof CellLayout) { - // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren - page = ((CellLayout) page).getShortcutsAndWidgets(); + public static class PagedFolderKeyEventListener implements View.OnKeyListener { + + private final Folder mFolder; + + public PagedFolderKeyEventListener(Folder folder) { + mFolder = folder; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP) { + return consume; + } + if (DEBUG) { + Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].", + KeyEvent.keyCodeToString(keyCode))); + } + + + if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) { + if (LauncherAppState.isDogfoodBuild()) { + throw new IllegalStateException("Parent of the focused item is not supported."); + } else { + return false; + } + } + + // Initialize variables. + final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout cellLayout = (CellLayout) itemContainer.getParent(); + final int countX = cellLayout.getCountX(); + final int countY = cellLayout.getCountY(); + + final int iconIndex = itemContainer.indexOfChild(v); + final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent(); + + final int pageIndex = pagedView.indexOfChild(cellLayout); + final int pageCount = pagedView.getPageCount(); + final boolean isLayoutRtl = Utilities.isRtl(v.getResources()); + + int[][] matrix = FocusLogic.createSparseMatrix(cellLayout); + // Process focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, + countY, matrix, iconIndex, pageIndex, pageCount, isLayoutRtl); + if (newIconIndex == FocusLogic.NOOP) { + handleNoopKey(keyCode, v); + return consume; + } + ShortcutAndWidgetContainer newParent = null; + View child = null; + + switch (newIconIndex) { + case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: + case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt( + ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) + ^ newParent.invertLayoutHorizontally()) ? 0 : countX - 1, row); + } + break; + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt(0, 0); + } + break; + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex - 1); + child = newParent.getChildAt(countX - 1, countY - 1); + } + break; + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex + 1); + child = newParent.getChildAt(0, 0); + } + break; + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: + newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); + if (newParent != null) { + pagedView.snapToPage(pageIndex + 1); + child = FocusLogic.getAdjacentChildInNextPage(newParent, v, newIconIndex); + } + break; + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + child = cellLayout.getChildAt(0, 0); + break; + case FocusLogic.CURRENT_PAGE_LAST_ITEM: + child = pagedView.getLastItem(); + break; + default: // Go to some item on the current page. + child = itemContainer.getChildAt(newIconIndex); + break; + } + if (child != null) { + child.requestFocus(); + playSoundEffect(keyCode, v); + } else { + handleNoopKey(keyCode, v); + } + return consume; + } + + public void handleNoopKey(int keyCode, View v) { + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + mFolder.mFolderName.requestFocus(); + playSoundEffect(keyCode, v); + } } - return page; } /** - * Handles key events in a PageViewCellLayout containing PagedViewIcons. + * Handles key events in the workspace hot seat (bottom of the screen). + *

Currently we don't special case for the phone UI in different orientations, even though + * the hotseat is on the side in landscape mode. This is to ensure that accessibility + * consistency is maintained across rotations. */ - static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) { - ViewGroup parentLayout; - ViewGroup itemContainer; - int countX; - int countY; - if (v.getParent() instanceof ShortcutAndWidgetContainer) { - itemContainer = (ViewGroup) v.getParent(); - parentLayout = (ViewGroup) itemContainer.getParent(); - countX = ((CellLayout) parentLayout).getCountX(); - countY = ((CellLayout) parentLayout).getCountY(); - } else { - itemContainer = parentLayout = (ViewGroup) v.getParent(); - countX = ((PagedViewGridLayout) parentLayout).getCellCountX(); - countY = ((PagedViewGridLayout) parentLayout).getCellCountY(); + static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP || !consume) { + return consume; } - // Note we have an extra parent because of the - // PagedViewCellLayout/PagedViewCellLayoutChildren relationship - final PagedView container = (PagedView) parentLayout.getParent(); - final int iconIndex = itemContainer.indexOfChild(v); - final int itemCount = itemContainer.getChildCount(); - final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parentLayout)); - final int pageCount = container.getChildCount(); + DeviceProfile profile = ((Launcher) v.getContext()).getDeviceProfile(); - final int x = iconIndex % countX; - final int y = iconIndex / countX; - - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - ViewGroup newParent = null; - // Side pages do not always load synchronously, so check before focusing child siblings - // willy-nilly - View child = null; - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon or the last icon on the previous page - if (iconIndex > 0) { - itemContainer.getChildAt(iconIndex - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } else { - if (pageIndex > 0) { - newParent = getAppsCustomizePage(container, pageIndex - 1); - if (newParent != null) { - container.snapToPage(pageIndex - 1); - child = newParent.getChildAt(newParent.getChildCount() - 1); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - } - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon or the first icon on the next page - if (iconIndex < (itemCount - 1)) { - itemContainer.getChildAt(iconIndex + 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } else { - if (pageIndex < (pageCount - 1)) { - newParent = getAppsCustomizePage(container, pageIndex + 1); - if (newParent != null) { - container.snapToPage(pageIndex + 1); - child = newParent.getChildAt(0); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - } - } - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous row, otherwise select the tab bar - if (y > 0) { - int newiconIndex = ((y - 1) * countX) + x; - itemContainer.getChildAt(newiconIndex).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // Select the closest icon in the next row, otherwise do nothing - if (y < (countY - 1)) { - int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x); - int newIconY = newiconIndex / countX; - if (newIconY != y) { - itemContainer.getChildAt(newiconIndex).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_PAGE_UP: - if (handleKeyEvent) { - // Select the first icon on the previous page, or the first icon on this page - // if there is no previous page - if (pageIndex > 0) { - newParent = getAppsCustomizePage(container, pageIndex - 1); - if (newParent != null) { - container.snapToPage(pageIndex - 1); - child = newParent.getChildAt(0); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - } else { - itemContainer.getChildAt(0).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_PAGE_DOWN: - if (handleKeyEvent) { - // Select the first icon on the next page, or the last icon on this page - // if there is no next page - if (pageIndex < (pageCount - 1)) { - newParent = getAppsCustomizePage(container, pageIndex + 1); - if (newParent != null) { - container.snapToPage(pageIndex + 1); - child = newParent.getChildAt(0); - if (child != null) { - child.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - } else { - itemContainer.getChildAt(itemCount - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - itemContainer.getChildAt(0).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - itemContainer.getChildAt(itemCount - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - wasHandled = true; - break; - default: break; + if (DEBUG) { + Log.v(TAG, String.format( + "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s", + KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); } - return wasHandled; + + // Initialize the variables. + final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent(); + final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent(); + Hotseat hotseat = (Hotseat) hotseatLayout.getParent(); + + Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); + int pageIndex = workspace.getNextPage(); + int pageCount = workspace.getChildCount(); + int countX = -1; + int countY = -1; + int iconIndex = hotseatParent.indexOfChild(v); + int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets() + .getChildAt(iconIndex).getLayoutParams()).cellX; + + final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex); + if (iconLayout == null) { + // This check is to guard against cases where key strokes rushes in when workspace + // child creation/deletion is still in flux. (e.g., during drop or fling + // animation.) + return consume; + } + final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + + ViewGroup parent = null; + int[][] matrix = null; + + if (keyCode == KeyEvent.KEYCODE_DPAD_UP && + !profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, + true /* hotseat horizontal */, profile.inv.hotseatAllAppsRank, + iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */); + iconIndex += iconParent.getChildCount(); + countX = iconLayout.getCountX(); + countY = iconLayout.getCountY() + hotseatLayout.getCountY(); + parent = iconParent; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && + profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, + false /* hotseat horizontal */, profile.inv.hotseatAllAppsRank, + iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */); + iconIndex += iconParent.getChildCount(); + countX = iconLayout.getCountX() + hotseatLayout.getCountX(); + countY = iconLayout.getCountY(); + parent = iconParent; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && + profile.isVerticalBarLayout()) { + keyCode = KeyEvent.KEYCODE_PAGE_DOWN; + }else { + // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the + // matrix extended with hotseat. + matrix = FocusLogic.createSparseMatrix(hotseatLayout); + countX = hotseatLayout.getCountX(); + countY = hotseatLayout.getCountY(); + parent = hotseatParent; + } + + // Process the focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, + countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources())); + + View newIcon = null; + if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = parent.getChildAt(0); + // TODO(hyunyoungs): handle cases where the child is not an icon but + // a folder or a widget. + workspace.snapToPage(pageIndex + 1); + } + if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) { + newIconIndex -= iconParent.getChildCount(); + } + if (parent != null) { + if (newIcon == null && newIconIndex >=0) { + newIcon = parent.getChildAt(newIconIndex); + } + if (newIcon != null) { + newIcon.requestFocus(); + playSoundEffect(keyCode, v); + } + } + return consume; } /** - * Handles key events in the workspace hotseat (bottom of the screen). + * Handles key events in a workspace containing icons. */ - static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) { + static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { + boolean consume = FocusLogic.shouldConsume(keyCode); + if (e.getAction() == KeyEvent.ACTION_UP || !consume) { + return consume; + } + + Launcher launcher = (Launcher) v.getContext(); + DeviceProfile profile = launcher.getDeviceProfile(); + + if (DEBUG) { + Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s", + KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); + } + + // Initialize the variables. ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); + CellLayout iconLayout = (CellLayout) parent.getParent(); + final Workspace workspace = (Workspace) iconLayout.getParent(); + final ViewGroup dragLayer = (ViewGroup) workspace.getParent(); + final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.search_drop_target_bar); + final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat); - // NOTE: currently we don't special case for the phone UI in different - // orientations, even though the hotseat is on the side in landscape mode. This - // is to ensure that accessibility consistency is maintained across rotations. - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - ArrayList views = getCellLayoutChildrenSortedSpatially(layout, parent); - int myIndex = views.indexOf(v); - // Select the previous button, otherwise do nothing - if (myIndex > 0) { - views.get(myIndex - 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - ArrayList views = getCellLayoutChildrenSortedSpatially(layout, parent); - int myIndex = views.indexOf(v); - // Select the next button, otherwise do nothing - if (myIndex < views.size() - 1) { - views.get(myIndex + 1).requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - final Workspace workspace = (Workspace) - v.getRootView().findViewById(R.id.workspace); - if (workspace != null) { - int pageIndex = workspace.getCurrentPage(); - CellLayout topLayout = (CellLayout) workspace.getChildAt(pageIndex); - ShortcutAndWidgetContainer children = topLayout.getShortcutsAndWidgets(); - final View newIcon = getIconInDirection(layout, children, -1, 1); - // Select the first bubble text view in the current page of the workspace - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } else { - workspace.requestFocus(); - } - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - // Do nothing - wasHandled = true; - break; - default: break; + final int iconIndex = parent.indexOfChild(v); + final int pageIndex = workspace.indexOfChild(iconLayout); + final int pageCount = workspace.getChildCount(); + int countX = iconLayout.getCountX(); + int countY = iconLayout.getCountY(); + + CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0); + ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + int[][] matrix; + + // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed + // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended + // with the hotseat. + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */, + profile.inv.hotseatAllAppsRank, + !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */); + countY = countY + 1; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && + profile.isVerticalBarLayout()) { + matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */, + profile.inv.hotseatAllAppsRank, + !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */); + countX = countX + 1; + } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) { + workspace.removeWorkspaceItem(v); + return consume; + } else { + matrix = FocusLogic.createSparseMatrix(iconLayout); } - return wasHandled; + + // Process the focus. + int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, + countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources())); + View newIcon = null; + switch (newIconIndex) { + case FocusLogic.NOOP: + if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + newIcon = tabs; + } + break; + case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: + case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: + int newPageIndex = pageIndex - 1; + if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) { + newPageIndex = pageIndex + 1; + } + int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); + workspace.snapToPage(newPageIndex); + if (parent != null) { + workspace.snapToPage(newPageIndex); + iconLayout = (CellLayout) parent.getParent(); + matrix = FocusLogic.createSparseMatrix(iconLayout, + iconLayout.getCountX(), row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, + matrix, FocusLogic.PIVOT, newPageIndex, pageCount, + Utilities.isRtl(v.getResources())); + newIcon = parent.getChildAt(newIconIndex); + } + break; + case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(0); + workspace.snapToPage(pageIndex - 1); + break; + case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); + newIcon = parent.getChildAt(parent.getChildCount() - 1); + workspace.snapToPage(pageIndex - 1); + break; + case FocusLogic.NEXT_PAGE_FIRST_ITEM: + parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); + newIcon = parent.getChildAt(0); + workspace.snapToPage(pageIndex + 1); + break; + case FocusLogic.NEXT_PAGE_LEFT_COLUMN: + case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: + newPageIndex = pageIndex + 1; + if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) { + newPageIndex = pageIndex - 1; + } + workspace.snapToPage(newPageIndex); + row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; + parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); + if (parent != null) { + workspace.snapToPage(newPageIndex); + iconLayout = (CellLayout) parent.getParent(); + matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row); + newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, + matrix, FocusLogic.PIVOT, newPageIndex, pageCount, + Utilities.isRtl(v.getResources())); + newIcon = parent.getChildAt(newIconIndex); + } + break; + case FocusLogic.CURRENT_PAGE_FIRST_ITEM: + newIcon = parent.getChildAt(0); + break; + case FocusLogic.CURRENT_PAGE_LAST_ITEM: + newIcon = parent.getChildAt(parent.getChildCount() - 1); + break; + default: + // current page, some item. + if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) { + newIcon = parent.getChildAt(newIconIndex); + } else if (parent.getChildCount() <= newIconIndex && + newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) { + newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount()); + } + break; + } + if (newIcon != null) { + newIcon.requestFocus(); + playSoundEffect(keyCode, v); + } + return consume; } + // + // Helper methods. + // + /** * Private helper method to get the CellLayoutChildren given a CellLayout index. */ - private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( + @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( ViewGroup container, int i) { CellLayout parent = (CellLayout) container.getChildAt(i); return parent.getShortcutsAndWidgets(); } /** - * Private helper method to sort all the CellLayout children in order of their (x,y) spatially - * from top left to bottom right. + * Helper method to be used for playing sound effects. */ - private static ArrayList getCellLayoutChildrenSortedSpatially(CellLayout layout, - ViewGroup parent) { - // First we order each the CellLayout children by their x,y coordinates - final int cellCountX = layout.getCountX(); - final int count = parent.getChildCount(); - ArrayList views = new ArrayList(); - for (int j = 0; j < count; ++j) { - views.add(parent.getChildAt(j)); - } - Collections.sort(views, new Comparator() { - @Override - public int compare(View lhs, View rhs) { - CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams(); - CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams(); - int lvIndex = (llp.cellY * cellCountX) + llp.cellX; - int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX; - return lvIndex - rvIndex; - } - }); - return views; - } - /** - * Private helper method to find the index of the next BubbleTextView or FolderIcon in the - * direction delta. - * - * @param delta either -1 or 1 depending on the direction we want to search - */ - private static View findIndexOfIcon(ArrayList views, int i, int delta) { - // Then we find the next BubbleTextView offset by delta from i - final int count = views.size(); - int newI = i + delta; - while (0 <= newI && newI < count) { - View newV = views.get(newI); - if (newV instanceof BubbleTextView || newV instanceof FolderIcon) { - return newV; - } - newI += delta; - } - return null; - } - private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i, - int delta) { - final ArrayList views = getCellLayoutChildrenSortedSpatially(layout, parent); - return findIndexOfIcon(views, i, delta); - } - private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v, - int delta) { - final ArrayList views = getCellLayoutChildrenSortedSpatially(layout, parent); - return findIndexOfIcon(views, views.indexOf(v), delta); - } - /** - * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction - * delta on the next line. - * - * @param delta either -1 or 1 depending on the line and direction we want to search - */ - private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v, - int lineDelta) { - final ArrayList views = getCellLayoutChildrenSortedSpatially(layout, parent); - final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); - final int cellCountY = layout.getCountY(); - final int row = lp.cellY; - final int newRow = row + lineDelta; - if (0 <= newRow && newRow < cellCountY) { - float closestDistance = Float.MAX_VALUE; - int closestIndex = -1; - int index = views.indexOf(v); - int endIndex = (lineDelta < 0) ? -1 : views.size(); - while (index != endIndex) { - View newV = views.get(index); - CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams(); - boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row); - if (satisfiesRow && - (newV instanceof BubbleTextView || newV instanceof FolderIcon)) { - float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) + - Math.pow(tmpLp.cellY - lp.cellY, 2)); - if (tmpDistance < closestDistance) { - closestIndex = index; - closestDistance = tmpDistance; - } - } - if (index <= endIndex) { - ++index; - } else { - --index; - } - } - if (closestIndex > -1) { - return views.get(closestIndex); - } - } - return null; - } - - /** - * Handles key events in a Workspace containing. - */ - static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { - ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); - final Workspace workspace = (Workspace) layout.getParent(); - final ViewGroup launcher = (ViewGroup) workspace.getParent(); - final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar); - final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat); - int pageIndex = workspace.indexOfChild(layout); - int pageCount = workspace.getChildCount(); - - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; + @Thunk static void playSoundEffect(int keyCode, View v) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon or the last icon on the previous page if possible - View newIcon = getIconInDirection(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } else { - if (pageIndex > 0) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); - newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the previous page - workspace.snapToPage(pageIndex - 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); break; case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon or the first icon on the next page if possible - View newIcon = getIconInDirection(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } else { - if (pageIndex < (pageCount - 1)) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); - newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the next page - workspace.snapToPage(pageIndex + 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous line, otherwise select the tab bar - View newIcon = getClosestIconOnLine(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - wasHandled = true; - } else { - tabs.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } + v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); break; case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // Select the closest icon in the next line, otherwise select the button bar - View newIcon = getClosestIconOnLine(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - wasHandled = true; - } else if (hotseat != null) { - hotseat.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - break; - case KeyEvent.KEYCODE_PAGE_UP: - if (handleKeyEvent) { - // Select the first icon on the previous page or the first icon on this page - // if there is no previous page - if (pageIndex > 0) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the previous page - workspace.snapToPage(pageIndex - 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } else { - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - } - wasHandled = true; - break; case KeyEvent.KEYCODE_PAGE_DOWN: - if (handleKeyEvent) { - // Select the first icon on the next page or the last icon on this page - // if there is no previous page - if (pageIndex < (pageCount - 1)) { - parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - // Snap to the next page - workspace.snapToPage(pageIndex + 1); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } else { - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; - break; case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - wasHandled = true; - break; - default: break; - } - return wasHandled; - } - - /** - * Handles key events for items in a Folder. - */ - static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) { - ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); - final CellLayout layout = (CellLayout) parent.getParent(); - final ScrollView scrollView = (ScrollView) layout.getParent(); - final Folder folder = (Folder) scrollView.getParent(); - View title = folder.mFolderName; - - final int action = e.getAction(); - final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); - boolean wasHandled = false; - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (handleKeyEvent) { - // Select the previous icon - View newIcon = getIconInDirection(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (handleKeyEvent) { - // Select the next icon - View newIcon = getIconInDirection(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - title.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); break; case KeyEvent.KEYCODE_DPAD_UP: - if (handleKeyEvent) { - // Select the closest icon in the previous line - View newIcon = getClosestIconOnLine(layout, parent, v, -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - if (handleKeyEvent) { - // Select the closest icon in the next line - View newIcon = getClosestIconOnLine(layout, parent, v, 1); - if (newIcon != null) { - newIcon.requestFocus(); - } else { - title.requestFocus(); - } - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - wasHandled = true; - break; + case KeyEvent.KEYCODE_PAGE_UP: case KeyEvent.KEYCODE_MOVE_HOME: - if (handleKeyEvent) { - // Select the first icon on this page - View newIcon = getIconInDirection(layout, parent, -1, 1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); - } - } - wasHandled = true; + v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); break; - case KeyEvent.KEYCODE_MOVE_END: - if (handleKeyEvent) { - // Select the last icon on this page - View newIcon = getIconInDirection(layout, parent, - parent.getChildCount(), -1); - if (newIcon != null) { - newIcon.requestFocus(); - v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); - } - } - wasHandled = true; + default: break; - default: break; } - return wasHandled; } } diff --git a/src/com/android/launcher3/FocusIndicatorView.java b/src/com/android/launcher3/FocusIndicatorView.java index 7d4664abb1..ecf93e4b3d 100644 --- a/src/com/android/launcher3/FocusIndicatorView.java +++ b/src/com/android/launcher3/FocusIndicatorView.java @@ -23,7 +23,8 @@ import android.graphics.Canvas; import android.util.AttributeSet; import android.util.Pair; import android.view.View; -import android.view.ViewParent; + +import com.android.launcher3.util.Thunk; public class FocusIndicatorView extends View implements View.OnFocusChangeListener { @@ -32,9 +33,6 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen private static final float MIN_VISIBLE_ALPHA = 0.2f; private static final long ANIM_DURATION = 150; - private static final int[] sTempPos = new int[2]; - private static final int[] sTempShift = new int[2]; - private final int[] mIndicatorPos = new int[2]; private final int[] mTargetViewPos = new int[2]; @@ -80,7 +78,8 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen } if (!mInitiated) { - getLocationRelativeToParentPagedView(this, mIndicatorPos); + // The parent view should always the a parent of the target view. + computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos); mInitiated = true; } @@ -93,7 +92,7 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth; nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight; - getLocationRelativeToParentPagedView(v, mTargetViewPos); + computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos); nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2; nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2; @@ -150,31 +149,36 @@ public class FocusIndicatorView extends View implements View.OnFocusChangeListen } /** - * Gets the location of a view relative in the window, off-setting any shift due to - * page view scroll + * Computes the location of a view relative to {@param parent}, off-setting + * any shift due to page view scroll. + * @param pos an array of two integers in which to hold the coordinates */ - private static void getLocationRelativeToParentPagedView(View v, int[] pos) { - getPagedViewScrollShift(v, sTempShift); - v.getLocationInWindow(sTempPos); - pos[0] = sTempPos[0] + sTempShift[0]; - pos[1] = sTempPos[1] + sTempShift[1]; + private static void computeLocationRelativeToParent(View v, View parent, int[] pos) { + pos[0] = pos[1] = 0; + computeLocationRelativeToParentHelper(v, parent, pos); + + // If a view is scaled, its position will also shift accordingly. For optimization, only + // consider this for the last node. + pos[0] += (1 - v.getScaleX()) * v.getWidth() / 2; + pos[1] += (1 - v.getScaleY()) * v.getHeight() / 2; } - private static void getPagedViewScrollShift(View child, int[] shift) { - ViewParent parent = child.getParent(); + private static void computeLocationRelativeToParentHelper(View child, + View commonParent, int[] shift) { + View parent = (View) child.getParent(); + shift[0] += child.getLeft(); + shift[1] += child.getTop(); if (parent instanceof PagedView) { - View parentView = (View) parent; - child.getLocationInWindow(sTempPos); - shift[0] = parentView.getPaddingLeft() - sTempPos[0]; - shift[1] = -(int) child.getTranslationY(); - } else if (parent instanceof View) { - getPagedViewScrollShift((View) parent, shift); - } else { - shift[0] = shift[1] = 0; + PagedView page = (PagedView) parent; + shift[0] -= page.getScrollForPage(page.indexOfChild(child)); + } + + if (parent != commonParent) { + computeLocationRelativeToParentHelper(parent, commonParent, shift); } } - private static final class ViewAnimState { + @Thunk static final class ViewAnimState { float x, y, scaleX, scaleY; } } diff --git a/src/com/android/launcher3/FocusOnlyTabWidget.java b/src/com/android/launcher3/FocusOnlyTabWidget.java deleted file mode 100644 index 08fc311bca..0000000000 --- a/src/com/android/launcher3/FocusOnlyTabWidget.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TabWidget; - -public class FocusOnlyTabWidget extends TabWidget { - public FocusOnlyTabWidget(Context context) { - super(context); - } - - public FocusOnlyTabWidget(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public FocusOnlyTabWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public View getSelectedTab() { - final int count = getTabCount(); - for (int i = 0; i < count; ++i) { - View v = getChildTabViewAt(i); - if (v.isSelected()) { - return v; - } - } - return null; - } - - public int getChildTabIndex(View v) { - final int tabCount = getTabCount(); - for (int i = 0; i < tabCount; ++i) { - if (getChildTabViewAt(i) == v) { - return i; - } - } - return -1; - } - - public void setCurrentTabToFocusedTab() { - View tab = null; - int index = -1; - final int count = getTabCount(); - for (int i = 0; i < count; ++i) { - View v = getChildTabViewAt(i); - if (v.hasFocus()) { - tab = v; - index = i; - break; - } - } - if (index > -1) { - super.setCurrentTab(index); - super.onFocusChange(tab, true); - } - } - public void superOnFocusChange(View v, boolean hasFocus) { - super.onFocusChange(v, hasFocus); - } - - @Override - public void onFocusChange(android.view.View v, boolean hasFocus) { - if (v == this && hasFocus && getTabCount() > 0) { - getSelectedTab().requestFocus(); - return; - } - } -} diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index 1890af47dd..2e19f6eba2 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -21,12 +21,15 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; -import android.os.SystemClock; -import android.support.v4.widget.AutoScrollHelper; +import android.os.Build; +import android.os.Bundle; import android.text.InputType; import android.text.Selection; import android.text.Spannable; @@ -34,21 +37,28 @@ import android.util.AttributeSet; import android.util.Log; import android.view.ActionMode; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; -import android.widget.ScrollView; import android.widget.TextView; +import com.android.launcher3.CellLayout.CellInfo; +import com.android.launcher3.DragController.DragListener; import com.android.launcher3.FolderInfo.FolderListener; +import com.android.launcher3.UninstallDropTarget.UninstallSource; +import com.android.launcher3.Workspace.ItemOperator; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.UiThreadCircularReveal; import java.util.ArrayList; import java.util.Collections; @@ -59,106 +69,109 @@ import java.util.Comparator; */ public class Folder extends LinearLayout implements DragSource, View.OnClickListener, View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, - View.OnFocusChangeListener { + View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource, + Stats.LaunchSourceProvider { private static final String TAG = "Launcher.Folder"; - protected DragController mDragController; - protected Launcher mLauncher; - protected FolderInfo mInfo; + /** + * We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this + * results in CellLayout being measured as UNSPECIFIED, which it does not support. + */ + private static final int MIN_CONTENT_DIMEN = 5; static final int STATE_NONE = -1; static final int STATE_SMALL = 0; static final int STATE_ANIMATING = 1; static final int STATE_OPEN = 2; - private static final int CLOSE_FOLDER_DELAY_MS = 150; + /** + * Time for which the scroll hint is shown before automatically changing page. + */ + public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY; + + /** + * Fraction of icon width which behave as scroll region. + */ + private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f; + + private static final int FOLDER_NAME_ANIMATION_DURATION = 633; - private int mExpandDuration; - private int mMaterialExpandDuration; - private int mMaterialExpandStagger; - protected CellLayout mContent; - private ScrollView mScrollView; - private final LayoutInflater mInflater; - private final IconCache mIconCache; - private int mState = STATE_NONE; - private static final int REORDER_ANIMATION_DURATION = 230; private static final int REORDER_DELAY = 250; private static final int ON_EXIT_CLOSE_DELAY = 400; + private static final Rect sTempRect = new Rect(); + + private static String sDefaultFolderName; + private static String sHintText; + + private final Alarm mReorderAlarm = new Alarm(); + private final Alarm mOnExitAlarm = new Alarm(); + private final Alarm mOnScrollHintAlarm = new Alarm(); + @Thunk final Alarm mScrollPauseAlarm = new Alarm(); + + @Thunk final ArrayList mItemsInReadingOrder = new ArrayList(); + + private final int mExpandDuration; + private final int mMaterialExpandDuration; + private final int mMaterialExpandStagger; + + private final InputMethodManager mInputMethodManager; + + protected final Launcher mLauncher; + protected DragController mDragController; + protected FolderInfo mInfo; + + @Thunk FolderIcon mFolderIcon; + + @Thunk FolderPagedView mContent; + @Thunk View mContentWrapper; + FolderEditText mFolderName; + + private View mFooter; + private int mFooterHeight; + + // Cell ranks used for drag and drop + @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank; + + @Thunk int mState = STATE_NONE; private boolean mRearrangeOnClose = false; - private FolderIcon mFolderIcon; - private int mMaxCountX; - private int mMaxCountY; - private int mMaxNumItems; - private ArrayList mItemsInReadingOrder = new ArrayList(); boolean mItemsInvalidated = false; private ShortcutInfo mCurrentDragInfo; private View mCurrentDragView; private boolean mIsExternalDrag; boolean mSuppressOnAdd = false; - private int[] mTargetCell = new int[2]; - private int[] mPreviousTargetCell = new int[2]; - private int[] mEmptyCell = new int[2]; - private Alarm mReorderAlarm = new Alarm(); - private Alarm mOnExitAlarm = new Alarm(); - private int mFolderNameHeight; - private Rect mTempRect = new Rect(); private boolean mDragInProgress = false; private boolean mDeleteFolderOnDropCompleted = false; private boolean mSuppressFolderDeletion = false; private boolean mItemAddedBackToSelfViaIcon = false; - FolderEditText mFolderName; - private float mFolderIconPivotX; - private float mFolderIconPivotY; - + @Thunk float mFolderIconPivotX; + @Thunk float mFolderIconPivotY; private boolean mIsEditingName = false; - private InputMethodManager mInputMethodManager; - - private static String sDefaultFolderName; - private static String sHintText; - - private FocusIndicatorView mFocusIndicatorHandler; - - // We avoid measuring the scroll view with a 0 width or height, as this - // results in CellLayout being measured as UNSPECIFIED, which it does - // not support. - private static final int MIN_CONTENT_DIMEN = 5; private boolean mDestroyed; - private AutoScrollHelper mAutoScrollHelper; - - private Runnable mDeferredAction; + @Thunk Runnable mDeferredAction; private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; + // Folder scrolling + private int mScrollAreaOffset; + + @Thunk int mScrollHintDir = DragController.SCROLL_NONE; + @Thunk int mCurrentScrollDir = DragController.SCROLL_NONE; + /** * Used to inflate the Workspace from XML. * * @param context The application's context. - * @param attrs The attribtues set containing the Workspace's customization values. + * @param attrs The attributes set containing the Workspace's customization values. */ public Folder(Context context, AttributeSet attrs) { super(context, attrs); - - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); setAlwaysDrawnWithCacheEnabled(false); - mInflater = LayoutInflater.from(context); - mIconCache = app.getIconCache(); - - Resources res = getResources(); - mMaxCountX = (int) grid.numColumns; - // Allow scrolling folders when DISABLE_ALL_APPS is true. - if (LauncherAppState.isDisableAllApps()) { - mMaxCountY = mMaxNumItems = Integer.MAX_VALUE; - } else { - mMaxCountY = (int) grid.numRows; - mMaxNumItems = mMaxCountX * mMaxCountY; - } - mInputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + Resources res = getResources(); mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration); mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger); @@ -172,45 +185,35 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mLauncher = (Launcher) context; // We need this view to be focusable in touch mode so that when text editing of the folder // name is complete, we have something to focus on, thus hiding the cursor and giving - // reliable behvior when clicking the text field (since it will always gain focus on click). + // reliable behavior when clicking the text field (since it will always gain focus on click). setFocusableInTouchMode(true); } @Override protected void onFinishInflate() { super.onFinishInflate(); - mScrollView = (ScrollView) findViewById(R.id.scroll_view); - mContent = (CellLayout) findViewById(R.id.folder_content); + mContentWrapper = findViewById(R.id.folder_content_wrapper); + mContent = (FolderPagedView) findViewById(R.id.folder_content); + mContent.setFolder(this); - mFocusIndicatorHandler = new FocusIndicatorView(getContext()); - mContent.addView(mFocusIndicatorHandler, 0); - mFocusIndicatorHandler.getLayoutParams().height = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; - mFocusIndicatorHandler.getLayoutParams().width = FocusIndicatorView.DEFAULT_LAYOUT_SIZE; - - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - mContent.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); - mContent.setGridSize(0, 0); - mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); - mContent.setInvertIfRtl(true); mFolderName = (FolderEditText) findViewById(R.id.folder_name); mFolderName.setFolder(this); mFolderName.setOnFocusChangeListener(this); - // We find out how tall the text view wants to be (it is set to wrap_content), so that - // we can allocate the appropriate amount of space for it. - int measureSpec = MeasureSpec.UNSPECIFIED; - mFolderName.measure(measureSpec, measureSpec); - mFolderNameHeight = mFolderName.getMeasuredHeight(); - // We disable action mode for now since it messes up the view on phones mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback); mFolderName.setOnEditorActionListener(this); mFolderName.setSelectAllOnFocus(true); mFolderName.setInputType(mFolderName.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); - mAutoScrollHelper = new FolderAutoScrollHelper(mScrollView); + + mFooter = findViewById(R.id.folder_footer); + + // We find out how tall footer wants to be (it is set to wrap_content), so that + // we can allocate the appropriate amount of space for it. + int measureSpec = MeasureSpec.UNSPECIFIED; + mFooter.measure(measureSpec, measureSpec); + mFooterHeight = mFooter.getMeasuredHeight(); } private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @@ -240,7 +243,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public boolean onLongClick(View v) { // Return if global dragging is not enabled if (!mLauncher.isDraggingEnabled()) return true; + return beginDrag(v, false); + } + private boolean beginDrag(View v, boolean accessible) { Object tag = v.getTag(); if (tag instanceof ShortcutInfo) { ShortcutInfo item = (ShortcutInfo) tag; @@ -248,14 +254,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return false; } - mLauncher.getWorkspace().beginDragShared(v, this); + mLauncher.getWorkspace().beginDragShared(v, new Point(), this, accessible); mCurrentDragInfo = item; - mEmptyCell[0] = item.cellX; - mEmptyCell[1] = item.cellY; + mEmptyCellRank = item.rank; mCurrentDragView = v; - mContent.removeView(mCurrentDragView); + mContent.removeItem(mCurrentDragView); mInfo.remove(mCurrentDragInfo); mDragInProgress = true; mItemAddedBackToSelfViaIcon = false; @@ -263,6 +268,23 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return true; } + @Override + public void startDrag(CellInfo cellInfo, boolean accessible) { + beginDrag(cellInfo.cell, accessible); + } + + @Override + public void enableAccessibleDrag(boolean enable) { + mLauncher.getSearchBar().enableAccessibleDrag(enable); + for (int i = 0; i < mContent.getChildCount(); i++) { + mContent.getPageAt(i).enableAccessibleDrag(enable, CellLayout.FOLDER_ACCESSIBILITY_DRAG); + } + + mFooter.setImportantForAccessibility(enable ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS : + IMPORTANT_FOR_ACCESSIBILITY_AUTO); + mLauncher.getWorkspace().setAddNewPageOnDrag(!enable); + } + public boolean isEditingName() { return mIsEditingName; } @@ -309,13 +331,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mFolderName; } - public CellLayout getContent() { - return mContent; - } - /** * We need to handle touch events to prevent them from falling through to the workspace below. */ + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent ev) { return true; @@ -325,7 +344,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mDragController = dragController; } - void setFolderIcon(FolderIcon icon) { + public void setFolderIcon(FolderIcon icon) { mFolderIcon = icon; } @@ -338,64 +357,16 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList /** * @return the FolderInfo object associated with this folder */ - FolderInfo getInfo() { + public FolderInfo getInfo() { return mInfo; } - private class GridComparator implements Comparator { - int mNumCols; - public GridComparator(int numCols) { - mNumCols = numCols; - } - - @Override - public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { - int lhIndex = lhs.cellY * mNumCols + lhs.cellX; - int rhIndex = rhs.cellY * mNumCols + rhs.cellX; - return (lhIndex - rhIndex); - } - } - - private void placeInReadingOrder(ArrayList items) { - int maxX = 0; - int count = items.size(); - for (int i = 0; i < count; i++) { - ShortcutInfo item = items.get(i); - if (item.cellX > maxX) { - maxX = item.cellX; - } - } - - GridComparator gridComparator = new GridComparator(maxX + 1); - Collections.sort(items, gridComparator); - final int countX = mContent.getCountX(); - for (int i = 0; i < count; i++) { - int x = i % countX; - int y = i / countX; - ShortcutInfo item = items.get(i); - item.cellX = x; - item.cellY = y; - } - } - void bind(FolderInfo info) { mInfo = info; ArrayList children = info.contents; - ArrayList overflow = new ArrayList(); - setupContentForNumItems(children.size()); - placeInReadingOrder(children); - int count = 0; - for (int i = 0; i < children.size(); i++) { - ShortcutInfo child = (ShortcutInfo) children.get(i); - if (createAndAddShortcut(child) == null) { - overflow.add(child); - } else { - count++; - } - } + Collections.sort(children, ITEM_POS_COMPARATOR); - // We rearrange the items in case there are any empty gaps - setupContentForNumItems(count); + ArrayList overflow = mContent.bindItems(children); // If our folder has too many items we prune them from the list. This is an issue // when upgrading from the old Folders implementation which could contain an unlimited @@ -405,6 +376,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList LauncherModel.deleteItemFromDatabase(mLauncher, item); } + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + if (lp == null) { + lp = new DragLayer.LayoutParams(0, 0); + lp.customPosition = true; + setLayoutParams(lp); + } + centerAboutIcon(); + mItemsInvalidated = true; updateTextViewFocus(); mInfo.addListener(this); @@ -414,7 +393,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } else { mFolderName.setText(""); } - updateItemLocationsInDatabase(); // In case any children didn't come across during loading, clean up the folder accordingly mFolderIcon.post(new Runnable() { @@ -433,8 +411,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList * * @return A new UserFolder. */ - static Folder fromXml(Context context) { - return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null); + @SuppressLint("InflateParams") + static Folder fromXml(Launcher launcher) { + return (Folder) launcher.getLayoutInflater().inflate(R.layout.user_folder, null); } /** @@ -459,6 +438,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void animateOpen() { if (!(getParent() instanceof DragLayer)) return; + mContent.completePendingPageChanges(); + if (!mDragInProgress) { + // Open on the first page. + mContent.snapToPageImmediately(0); + } + Animator openFolderAnim = null; final Runnable onCompleteRunnable; if (!Utilities.isLmpOrAbove()) { @@ -484,6 +469,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList prepareReveal(); centerAboutIcon(); + AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); int height = getFolderHeight(); @@ -494,32 +480,32 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList PropertyValuesHolder tx = PropertyValuesHolder.ofFloat("translationX", transX, 0); PropertyValuesHolder ty = PropertyValuesHolder.ofFloat("translationY", transY, 0); + Animator drift = ObjectAnimator.ofPropertyValuesHolder(this, tx, ty); + drift.setDuration(mMaterialExpandDuration); + drift.setStartDelay(mMaterialExpandStagger); + drift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX()); int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY()); - float radius = (float) Math.sqrt(rx * rx + ry * ry); - AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); - Animator reveal = LauncherAnimUtils.createCircularReveal(this, (int) getPivotX(), + float radius = (float) Math.hypot(rx, ry); + + Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(), (int) getPivotY(), 0, radius); reveal.setDuration(mMaterialExpandDuration); reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); - mContent.setAlpha(0f); - Animator iconsAlpha = LauncherAnimUtils.ofFloat(mContent, "alpha", 0f, 1f); + mContentWrapper.setAlpha(0f); + Animator iconsAlpha = ObjectAnimator.ofFloat(mContentWrapper, "alpha", 0f, 1f); iconsAlpha.setDuration(mMaterialExpandDuration); iconsAlpha.setStartDelay(mMaterialExpandStagger); iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - mFolderName.setAlpha(0f); - Animator textAlpha = LauncherAnimUtils.ofFloat(mFolderName, "alpha", 0f, 1f); + mFooter.setAlpha(0f); + Animator textAlpha = ObjectAnimator.ofFloat(mFooter, "alpha", 0f, 1f); textAlpha.setDuration(mMaterialExpandDuration); textAlpha.setStartDelay(mMaterialExpandStagger); textAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - Animator drift = LauncherAnimUtils.ofPropertyValuesHolder(this, tx, ty); - drift.setDuration(mMaterialExpandDuration); - drift.setStartDelay(mMaterialExpandStagger); - drift.setInterpolator(new LogDecelerateInterpolator(60, 0)); - anim.play(drift); anim.play(iconsAlpha); anim.play(textAlpha); @@ -527,11 +513,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList openFolderAnim = anim; - mContent.setLayerType(LAYER_TYPE_HARDWARE, null); + mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null); + mFooter.setLayerType(LAYER_TYPE_HARDWARE, null); onCompleteRunnable = new Runnable() { @Override public void run() { - mContent.setLayerType(LAYER_TYPE_NONE, null); + mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); + mContentWrapper.setLayerType(LAYER_TYPE_NONE, null); } }; } @@ -539,8 +527,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void onAnimationStart(Animator animation) { sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, - String.format(getContext().getString(R.string.folder_opened), - mContent.getCountX(), mContent.getCountY())); + mContent.getAccessibilityDescription()); mState = STATE_ANIMATING; } @Override @@ -551,30 +538,79 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList onCompleteRunnable.run(); } - setFocusOnFirstChild(); + mContent.setFocusOnFirstChild(); } }); + + // Footer animation + if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) { + int footerWidth = mContent.getDesiredWidth() + - mFooter.getPaddingLeft() - mFooter.getPaddingRight(); + + float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString()); + float translation = (footerWidth - textWidth) / 2; + mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation); + mContent.setMarkerScale(0); + + // Do not update the flag if we are in drag mode. The flag will be updated, when we + // actually drop the icon. + final boolean updateAnimationFlag = !mDragInProgress; + openFolderAnim.addListener(new AnimatorListenerAdapter() { + + @Override + public void onAnimationEnd(Animator animation) { + mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION) + .translationX(0) + .setInterpolator(Utilities.isLmpOrAbove() ? + AnimationUtils.loadInterpolator(mLauncher, + android.R.interpolator.fast_out_slow_in) + : new LogDecelerateInterpolator(100, 0)); + mContent.animateMarkers(); + + if (updateAnimationFlag) { + mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher); + } + } + }); + } else { + mFolderName.setTranslationX(0); + mContent.setMarkerScale(1); + } + openFolderAnim.start(); // Make sure the folder picks up the last drag move even if the finger doesn't move. if (mDragController.isDragging()) { mDragController.forceTouchMove(); } + + FolderPagedView pages = (FolderPagedView) mContent; + pages.verifyVisibleHighResIcons(pages.getNextPage()); } public void beginExternalDrag(ShortcutInfo item) { - setupContentForNumItems(getItemCount() + 1); - findAndSetEmptyCells(item); - mCurrentDragInfo = item; - mEmptyCell[0] = item.cellX; - mEmptyCell[1] = item.cellY; + mEmptyCellRank = mContent.allocateRankForNewItem(item); mIsExternalDrag = true; - mDragInProgress = true; + + // Since this folder opened by another controller, it might not get onDrop or + // onDropComplete. Perform cleanup once drag-n-drop ends. + mDragController.addDragListener(this); } - private void sendCustomAccessibilityEvent(int type, String text) { + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { } + + @Override + public void onDragEnd() { + if (mIsExternalDrag && mDragInProgress) { + completeDragExit(); + } + mDragController.removeDragListener(this); + } + + @Thunk void sendCustomAccessibilityEvent(int type, String text) { AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (accessibilityManager.isEnabled()) { @@ -585,13 +621,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } - private void setFocusOnFirstChild() { - View firstChild = mContent.getChildAt(0, 0); - if (firstChild != null) { - firstChild.requestFocus(); - } - } - public void animateClosed() { if (!(getParent() instanceof DragLayer)) return; PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); @@ -627,174 +656,90 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList !isFull()); } - protected boolean findAndSetEmptyCells(ShortcutInfo item) { - int[] emptyCell = new int[2]; - if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) { - item.cellX = emptyCell[0]; - item.cellY = emptyCell[1]; - return true; - } else { - return false; - } - } - - protected View createAndAddShortcut(ShortcutInfo item) { - final BubbleTextView textView = - (BubbleTextView) mInflater.inflate(R.layout.folder_application, this, false); - textView.applyFromShortcutInfo(item, mIconCache, false); - - textView.setOnClickListener(this); - textView.setOnLongClickListener(this); - textView.setOnFocusChangeListener(mFocusIndicatorHandler); - - // We need to check here to verify that the given item's location isn't already occupied - // by another item. - if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0 - || item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) { - // This shouldn't happen, log it. - Log.e(TAG, "Folder order not properly persisted during bind"); - if (!findAndSetEmptyCells(item)) { - return null; - } - } - - CellLayout.LayoutParams lp = - new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); - boolean insert = false; - textView.setOnKeyListener(new FolderKeyEventListener()); - mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); - return textView; - } - public void onDragEnter(DragObject d) { - mPreviousTargetCell[0] = -1; - mPreviousTargetCell[1] = -1; + mPrevTargetRank = -1; mOnExitAlarm.cancelAlarm(); + // Get the area offset such that the folder only closes if half the drag icon width + // is outside the folder area + mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset; } OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { public void onAlarm(Alarm alarm) { - realTimeReorder(mEmptyCell, mTargetCell); + mContent.realTimeReorder(mEmptyCellRank, mTargetRank); + mEmptyCellRank = mTargetRank; } }; - boolean readingOrderGreaterThan(int[] v1, int[] v2) { - if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) { - return true; - } else { - return false; - } - } - - private void realTimeReorder(int[] empty, int[] target) { - boolean wrap; - int startX; - int endX; - int startY; - int delay = 0; - float delayAmount = 30; - if (readingOrderGreaterThan(target, empty)) { - wrap = empty[0] >= mContent.getCountX() - 1; - startY = wrap ? empty[1] + 1 : empty[1]; - for (int y = startY; y <= target[1]; y++) { - startX = y == empty[1] ? empty[0] + 1 : 0; - endX = y < target[1] ? mContent.getCountX() - 1 : target[0]; - for (int x = startX; x <= endX; x++) { - View v = mContent.getChildAt(x,y); - if (mContent.animateChildToPosition(v, empty[0], empty[1], - REORDER_ANIMATION_DURATION, delay, true, true)) { - empty[0] = x; - empty[1] = y; - delay += delayAmount; - delayAmount *= 0.9; - } - } - } - } else { - wrap = empty[0] == 0; - startY = wrap ? empty[1] - 1 : empty[1]; - for (int y = startY; y >= target[1]; y--) { - startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1; - endX = y > target[1] ? 0 : target[0]; - for (int x = startX; x >= endX; x--) { - View v = mContent.getChildAt(x,y); - if (mContent.animateChildToPosition(v, empty[0], empty[1], - REORDER_ANIMATION_DURATION, delay, true, true)) { - empty[0] = x; - empty[1] = y; - delay += delayAmount; - delayAmount *= 0.9; - } - } - } - } - } - + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public boolean isLayoutRtl() { return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); } + @Override public void onDragOver(DragObject d) { - final DragView dragView = d.dragView; - final int scrollOffset = mScrollView.getScrollY(); - final float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, dragView, null); - r[0] -= getPaddingLeft(); - r[1] -= getPaddingTop(); + onDragOver(d, REORDER_DELAY); + } - final long downTime = SystemClock.uptimeMillis(); - final MotionEvent translatedEv = MotionEvent.obtain( - downTime, downTime, MotionEvent.ACTION_MOVE, d.x, d.y, 0); + private int getTargetRank(DragObject d, float[] recycle) { + recycle = d.getVisualCenter(recycle); + return mContent.findNearestArea( + (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop()); + } - if (!mAutoScrollHelper.isEnabled()) { - mAutoScrollHelper.setEnabled(true); + @Thunk void onDragOver(DragObject d, int reorderDelay) { + if (mScrollPauseAlarm.alarmPending()) { + return; + } + final float[] r = new float[2]; + mTargetRank = getTargetRank(d, r); + + if (mTargetRank != mPrevTargetRank) { + mReorderAlarm.cancelAlarm(); + mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); + mReorderAlarm.setAlarm(REORDER_DELAY); + mPrevTargetRank = mTargetRank; } - final boolean handled = mAutoScrollHelper.onTouch(this, translatedEv); - translatedEv.recycle(); + float x = r[0]; + int currentPage = mContent.getNextPage(); - if (handled) { - mReorderAlarm.cancelAlarm(); + float cellOverlap = mContent.getCurrentCellLayout().getCellWidth() + * ICON_OVERSCROLL_WIDTH_FACTOR; + boolean isOutsideLeftEdge = x < cellOverlap; + boolean isOutsideRightEdge = x > (getWidth() - cellOverlap); + + if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) { + showScrollHint(DragController.SCROLL_LEFT, d); + } else if (currentPage < (mContent.getPageCount() - 1) + && (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) { + showScrollHint(DragController.SCROLL_RIGHT, d); } else { - mTargetCell = mContent.findNearestArea( - (int) r[0], (int) r[1] + scrollOffset, 1, 1, mTargetCell); - if (isLayoutRtl()) { - mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1; - } - if (mTargetCell[0] != mPreviousTargetCell[0] - || mTargetCell[1] != mPreviousTargetCell[1]) { - mReorderAlarm.cancelAlarm(); - mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); - mReorderAlarm.setAlarm(REORDER_DELAY); - mPreviousTargetCell[0] = mTargetCell[0]; - mPreviousTargetCell[1] = mTargetCell[1]; + mOnScrollHintAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mContent.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; } } } - // This is used to compute the visual center of the dragView. The idea is that - // the visual center represents the user's interpretation of where the item is, and hence - // is the appropriate point to use when determining drop location. - private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, - DragView dragView, float[] recycle) { - float res[]; - if (recycle == null) { - res = new float[2]; - } else { - res = recycle; + private void showScrollHint(int direction, DragObject d) { + // Show scroll hint on the right + if (mScrollHintDir != direction) { + mContent.showScrollHint(direction); + mScrollHintDir = direction; } - // These represent the visual top and left of drag view if a dragRect was provided. - // If a dragRect was not provided, then they correspond to the actual view left and - // top, as the dragRect is in that case taken to be the entire dragView. - // R.dimen.dragViewOffsetY. - int left = x - xOffset; - int top = y - yOffset; + // Set alarm for when the hint is complete + if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) { + mCurrentScrollDir = direction; + mOnScrollHintAlarm.cancelAlarm(); + mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); + mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); - // In order to find the visual center, we shift by half the dragRect - res[0] = left + dragView.getDragRegion().width() / 2; - res[1] = top + dragView.getDragRegion().height() / 2; - - return res; + mReorderAlarm.cancelAlarm(); + mTargetRank = mEmptyCellRank; + } } OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { @@ -804,17 +749,25 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }; public void completeDragExit() { - mLauncher.closeFolder(); + if (mInfo.opened) { + mLauncher.closeFolder(); + mRearrangeOnClose = true; + } else if (mState == STATE_ANIMATING) { + mRearrangeOnClose = true; + } else { + rearrangeChildren(); + clearDragInfo(); + } + } + + private void clearDragInfo() { mCurrentDragInfo = null; mCurrentDragView = null; mSuppressOnAdd = false; - mRearrangeOnClose = true; mIsExternalDrag = false; } public void onDragExit(DragObject d) { - // Exiting folder; stop the auto scroller. - mAutoScrollHelper.setEnabled(false); // We only close the folder if this is a true drag exit, ie. not because // a drop has occurred above the folder. if (!d.dragComplete) { @@ -822,6 +775,25 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); } mReorderAlarm.cancelAlarm(); + + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + if (mScrollHintDir != DragController.SCROLL_NONE) { + mContent.clearScrollHint(); + mScrollHintDir = DragController.SCROLL_NONE; + } + } + + /** + * When performing an accessibility drop, onDrop is sent immediately after onDragEnter. So we + * need to complete all transient states based on timers. + */ + @Override + public void prepareAccessibilityDrop() { + if (mReorderAlarm.alarmPending()) { + mReorderAlarm.cancelAlarm(); + mReorderAlarmListener.onAlarm(mReorderAlarm); + } } public void onDropCompleted(final View target, final DragObject d, @@ -846,9 +818,18 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList replaceFolderWithFinalItem(); } } else { - setupContentForNumItems(getItemCount()); // The drag failed, we need to return the item to the folder + ShortcutInfo info = (ShortcutInfo) d.dragInfo; + View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info) + ? mCurrentDragView : mContent.createNewView(info); + ArrayList views = getItemsInReadingOrder(); + views.add(info.rank, icon); + mContent.arrangeChildren(views, views.size()); + mItemsInvalidated = true; + + mSuppressOnAdd = true; mFolderIcon.onDrop(d); + mSuppressOnAdd = false; } if (target != this) { @@ -857,6 +838,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList if (!successfulDrop) { mSuppressFolderDeletion = true; } + mScrollPauseAlarm.cancelAlarm(); completeDragExit(); } } @@ -871,12 +853,22 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Reordering may have occured, and we need to save the new item locations. We do this once // at the end to prevent unnecessary database operations. updateItemLocationsInDatabaseBatch(); + + // Use the item count to check for multi-page as the folder UI may not have + // been refreshed yet. + if (getItemCount() <= mContent.itemsPerPage()) { + // Show the animation, next time something is added to the folder. + mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher); + } + } + @Override public void deferCompleteDropAfterUninstallActivity() { mDeferDropAfterUninstall = true; } + @Override public void onUninstallActivityReturned(boolean success) { mDeferDropAfterUninstall = false; mUninstallSuccessful = success; @@ -905,7 +897,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return true; } - public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + @Override + public void onFlingToDelete(DragObject d, PointF vec) { // Do nothing } @@ -914,22 +907,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Do nothing } - private void updateItemLocationsInDatabase() { - ArrayList list = getItemsInReadingOrder(); - for (int i = 0; i < list.size(); i++) { - View v = list.get(i); - ItemInfo info = (ItemInfo) v.getTag(); - LauncherModel.moveItemInDatabase(mLauncher, info, mInfo.id, 0, - info.cellX, info.cellY); - } - } - private void updateItemLocationsInDatabaseBatch() { ArrayList list = getItemsInReadingOrder(); ArrayList items = new ArrayList(); for (int i = 0; i < list.size(); i++) { View v = list.get(i); ItemInfo info = (ItemInfo) v.getTag(); + info.rank = i; items.add(info); } @@ -942,7 +926,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList View v = list.get(i); ItemInfo info = (ItemInfo) v.getTag(); LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0, - info.cellX, info.cellY, false); + info.cellX, info.cellY); } } @@ -956,37 +940,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return true; } - private void setupContentDimensions(int count) { - ArrayList list = getItemsInReadingOrder(); - - int countX = mContent.getCountX(); - int countY = mContent.getCountY(); - boolean done = false; - - while (!done) { - int oldCountX = countX; - int oldCountY = countY; - if (countX * countY < count) { - // Current grid is too small, expand it - if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) { - countX++; - } else if (countY < mMaxCountY) { - countY++; - } - if (countY == 0) countY++; - } else if ((countY - 1) * countX >= count && countY >= countX) { - countY = Math.max(0, countY - 1); - } else if ((countX - 1) * countY >= count) { - countX = Math.max(0, countX - 1); - } - done = countX == oldCountX && countY == oldCountY; - } - mContent.setGridSize(countX, countY); - arrangeChildren(list); - } - public boolean isFull() { - return getItemCount() >= mMaxNumItems; + return mContent.isFull(); } private void centerAboutIcon() { @@ -996,41 +951,30 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); int height = getFolderHeight(); - float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect); + float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); - int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2); - int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2); + int centerX = (int) (sTempRect.left + sTempRect.width() * scale / 2); + int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2); int centeredLeft = centerX - width / 2; int centeredTop = centerY - height / 2; - int currentPage = mLauncher.getWorkspace().getNextPage(); - // In case the workspace is scrolling, we need to use the final scroll to compute - // the folders bounds. - mLauncher.getWorkspace().setFinalScrollForPageChange(currentPage); - // We first fetch the currently visible CellLayoutChildren - CellLayout currentLayout = (CellLayout) mLauncher.getWorkspace().getChildAt(currentPage); - ShortcutAndWidgetContainer boundingLayout = currentLayout.getShortcutsAndWidgets(); - Rect bounds = new Rect(); - parent.getDescendantRectRelativeToSelf(boundingLayout, bounds); - // We reset the workspaces scroll - mLauncher.getWorkspace().resetFinalScrollForPageChange(currentPage); - // We need to bound the folder to the currently visible CellLayoutChildren - int left = Math.min(Math.max(bounds.left, centeredLeft), - bounds.left + bounds.width() - width); - int top = Math.min(Math.max(bounds.top, centeredTop), - bounds.top + bounds.height() - height); - if (grid.isPhone() && (grid.availableWidthPx - width) < grid.iconSizePx) { + // We need to bound the folder to the currently visible workspace area + mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect); + int left = Math.min(Math.max(sTempRect.left, centeredLeft), + sTempRect.left + sTempRect.width() - width); + int top = Math.min(Math.max(sTempRect.top, centeredTop), + sTempRect.top + sTempRect.height() - height); + if (grid.isPhone && (grid.availableWidthPx - width) < grid.iconSizePx) { // Center the folder if it is full (on phones only) left = (grid.availableWidthPx - width) / 2; - } else if (width >= bounds.width()) { + } else if (width >= sTempRect.width()) { // If the folder doesn't fit within the bounds, center it about the desired bounds - left = bounds.left + (bounds.width() - width) / 2; + left = sTempRect.left + (sTempRect.width() - width) / 2; } - if (height >= bounds.height()) { - top = bounds.top + (bounds.height() - height) / 2; + if (height >= sTempRect.height()) { + top = sTempRect.top + (sTempRect.height() - height) / 2; } int folderPivotX = width / 2 + (centeredLeft - left); @@ -1055,26 +999,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList return mFolderIconPivotY; } - private void setupContentForNumItems(int count) { - setupContentDimensions(count); - - DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); - if (lp == null) { - lp = new DragLayer.LayoutParams(0, 0); - lp.customPosition = true; - setLayoutParams(lp); - } - centerAboutIcon(); - } - private int getContentAreaHeight() { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - Rect workspacePadding = grid.getWorkspacePadding(grid.isLandscape ? - CellLayout.LANDSCAPE : CellLayout.PORTRAIT); + DeviceProfile grid = mLauncher.getDeviceProfile(); + Rect workspacePadding = grid.getWorkspacePadding(mContent.mIsRtl); int maxContentAreaHeight = grid.availableHeightPx - workspacePadding.top - workspacePadding.bottom - - mFolderNameHeight; + mFooterHeight; int height = Math.min(maxContentAreaHeight, mContent.getDesiredHeight()); return Math.max(height, MIN_CONTENT_DIMEN); @@ -1085,67 +1015,67 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } private int getFolderHeight() { - int height = getPaddingTop() + getPaddingBottom() - + getContentAreaHeight() + mFolderNameHeight; - return height; + return getFolderHeight(getContentAreaHeight()); + } + + private int getFolderHeight(int contentAreaHeight) { + return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight; } protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); - int height = getFolderHeight(); - int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(getContentAreaWidth(), - MeasureSpec.EXACTLY); - int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(getContentAreaHeight(), - MeasureSpec.EXACTLY); + int contentWidth = getContentAreaWidth(); + int contentHeight = getContentAreaHeight(); - if (LauncherAppState.isDisableAllApps()) { - // Don't cap the height of the content to allow scrolling. - mContent.setFixedSize(getContentAreaWidth(), mContent.getDesiredHeight()); - } else { - mContent.setFixedSize(getContentAreaWidth(), getContentAreaHeight()); + int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY); + int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY); + + mContent.setFixedSize(contentWidth, contentHeight); + mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec); + + if (mContent.getChildCount() > 0) { + int cellIconGap = (mContent.getPageAt(0).getCellWidth() + - mLauncher.getDeviceProfile().iconSizePx) / 2; + mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap, + mFooter.getPaddingTop(), + mContent.getPaddingRight() + cellIconGap, + mFooter.getPaddingBottom()); } + mFooter.measure(contentAreaWidthSpec, + MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY)); - mScrollView.measure(contentAreaWidthSpec, contentAreaHeightSpec); - mFolderName.measure(contentAreaWidthSpec, - MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); - setMeasuredDimension(width, height); + int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth; + int folderHeight = getFolderHeight(contentHeight); + setMeasuredDimension(folderWidth, folderHeight); } - private void arrangeChildren(ArrayList list) { - int[] vacant = new int[2]; - if (list == null) { - list = getItemsInReadingOrder(); - } - mContent.removeAllViews(); + /** + * Rearranges the children based on their rank. + */ + public void rearrangeChildren() { + rearrangeChildren(-1); + } - for (int i = 0; i < list.size(); i++) { - View v = list.get(i); - mContent.getVacantCell(vacant, 1, 1); - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); - lp.cellX = vacant[0]; - lp.cellY = vacant[1]; - ItemInfo info = (ItemInfo) v.getTag(); - if (info.cellX != vacant[0] || info.cellY != vacant[1]) { - info.cellX = vacant[0]; - info.cellY = vacant[1]; - LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, - info.cellX, info.cellY); - } - boolean insert = false; - mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); - } + /** + * Rearranges the children based on their rank. + * @param itemCount if greater than the total children count, empty spaces are left at the end, + * otherwise it is ignored. + */ + public void rearrangeChildren(int itemCount) { + ArrayList views = getItemsInReadingOrder(); + mContent.arrangeChildren(views, Math.max(itemCount, views.size())); mItemsInvalidated = true; } + // TODO remove this once GSA code fix is submitted + public ViewGroup getContent() { + return (ViewGroup) mContent; + } + public int getItemCount() { - return mContent.getShortcutsAndWidgets().getChildCount(); + return mContent.getItemCount(); } - public View getItemAt(int index) { - return mContent.getShortcutsAndWidgets().getChildAt(index); - } - - private void onCloseComplete() { + @Thunk void onCloseComplete() { DragLayer parent = (DragLayer) getParent(); if (parent != null) { parent.removeView(this); @@ -1155,7 +1085,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mFolderIcon.requestFocus(); if (mRearrangeOnClose) { - setupContentForNumItems(getItemCount()); + rearrangeChildren(); mRearrangeOnClose = false; } if (getItemCount() <= 1) { @@ -1166,9 +1096,10 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } mSuppressFolderDeletion = false; + clearDragInfo(); } - private void replaceFolderWithFinalItem() { + @Thunk void replaceFolderWithFinalItem() { // Add the last remaining child to the workspace in place of the folder Runnable onCompleteRunnable = new Runnable() { @Override @@ -1179,8 +1110,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // Move the item from the folder to the workspace, in the position of the folder if (getItemCount() == 1) { ShortcutInfo finalItem = mInfo.contents.get(0); - child = mLauncher.createShortcut(R.layout.application, cellLayout, - finalItem); + child = mLauncher.createShortcut(cellLayout, finalItem); LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, mInfo.screenId, mInfo.cellX, mInfo.cellY); } @@ -1205,7 +1135,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } } }; - View finalChild = getItemAt(0); + View finalChild = mContent.getLastItem(); if (finalChild != null) { mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); } else { @@ -1220,9 +1150,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // This method keeps track of the last item in the folder for the purposes // of keyboard focus - private void updateTextViewFocus() { - View lastChild = getItemAt(getItemCount() - 1); - getItemAt(getItemCount() - 1); + public void updateTextViewFocus() { + View lastChild = mContent.getLastItem(); if (lastChild != null) { mFolderName.setNextFocusDownId(lastChild.getId()); mFolderName.setNextFocusRightId(lastChild.getId()); @@ -1247,12 +1176,24 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList }; } + // If the icon was dropped while the page was being scrolled, we need to compute + // the target location again such that the icon is placed of the final page. + if (!mContent.rankOnCurrentPage(mEmptyCellRank)) { + // Reorder again. + mTargetRank = getTargetRank(d, null); + + // Rearrange items immediately. + mReorderAlarmListener.onAlarm(mReorderAlarm); + + mOnScrollHintAlarm.cancelAlarm(); + mScrollPauseAlarm.cancelAlarm(); + } + mContent.completePendingPageChanges(); + View currentDragView; ShortcutInfo si = mCurrentDragInfo; if (mIsExternalDrag) { - si.cellX = mEmptyCell[0]; - si.cellY = mEmptyCell[1]; - + currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank); // Actually move the item in the database if it was an external drag. Call this // before creating the view, so that ShortcutInfo is updated appropriately. LauncherModel.addOrMoveItemInDatabase( @@ -1263,14 +1204,9 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList updateItemLocationsInDatabaseBatch(); } mIsExternalDrag = false; - - currentDragView = createAndAddShortcut(si); } else { currentDragView = mCurrentDragView; - CellLayout.LayoutParams lp = (CellLayout.LayoutParams) currentDragView.getLayoutParams(); - si.cellX = lp.cellX = mEmptyCell[0]; - si.cellX = lp.cellY = mEmptyCell[1]; - mContent.addViewToCellLayout(currentDragView, -1, (int) si.id, lp, true); + mContent.addViewForRank(currentDragView, si, mEmptyCellRank); } if (d.dragView.hasDrawn()) { @@ -1289,7 +1225,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList currentDragView.setVisibility(VISIBLE); } mItemsInvalidated = true; - setupContentDimensions(getItemCount()); + rearrangeChildren(); // Temporarily suppress the listener, as we did all the work already here. mSuppressOnAdd = true; @@ -1297,6 +1233,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mSuppressOnAdd = false; // Clear the drag info, as it is no longer being dragged. mCurrentDragInfo = null; + mDragInProgress = false; + + if (mContent.getPageCount() > 1) { + // The animation has already been shown while opening the folder. + mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher); + } } // This is used so the item doesn't immediately appear in the folder when added. In one case @@ -1311,17 +1253,13 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList v.setVisibility(VISIBLE); } + @Override public void onAdd(ShortcutInfo item) { - mItemsInvalidated = true; // If the item was dropped onto this open folder, we have done the work associated // with adding the item to the folder, as indicated by mSuppressOnAdd being set if (mSuppressOnAdd) return; - if (!findAndSetEmptyCells(item)) { - // The current layout is full, can we expand it? - setupContentForNumItems(getItemCount() + 1); - findAndSetEmptyCells(item); - } - createAndAddShortcut(item); + mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item)); + mItemsInvalidated = true; LauncherModel.addOrMoveItemInDatabase( mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); } @@ -1332,27 +1270,25 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList // the work associated with removing the item, so we don't have to do anything here. if (item == mCurrentDragInfo) return; View v = getViewForInfo(item); - mContent.removeView(v); + mContent.removeItem(v); if (mState == STATE_ANIMATING) { mRearrangeOnClose = true; } else { - setupContentForNumItems(getItemCount()); + rearrangeChildren(); } if (getItemCount() <= 1) { replaceFolderWithFinalItem(); } } - private View getViewForInfo(ShortcutInfo item) { - for (int j = 0; j < mContent.getCountY(); j++) { - for (int i = 0; i < mContent.getCountX(); i++) { - View v = mContent.getChildAt(i, j); - if (v.getTag() == item) { - return v; - } + private View getViewForInfo(final ShortcutInfo item) { + return mContent.iterateOverItems(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View view, View parent) { + return info == item; } - } - return null; + }); } public void onItemsChanged() { @@ -1365,14 +1301,14 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public ArrayList getItemsInReadingOrder() { if (mItemsInvalidated) { mItemsInReadingOrder.clear(); - for (int j = 0; j < mContent.getCountY(); j++) { - for (int i = 0; i < mContent.getCountX(); i++) { - View v = mContent.getChildAt(i, j); - if (v != null) { - mItemsInReadingOrder.add(v); - } + mContent.iterateOverItems(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View view, View parent) { + mItemsInReadingOrder.add(view); + return false; } - } + }); mItemsInvalidated = false; } return mItemsInReadingOrder; @@ -1391,5 +1327,79 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void getHitRectRelativeToDragLayer(Rect outRect) { getHitRect(outRect); + outRect.left -= mScrollAreaOffset; + outRect.right += mScrollAreaOffset; } + + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + // Fill in from the folder icon's launch source provider first + Stats.LaunchSourceUtils.populateSourceDataFromAncestorProvider(mFolderIcon, sourceData); + sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_FOLDER); + sourceData.putInt(Stats.SOURCE_EXTRA_SUB_CONTAINER_PAGE, mContent.getCurrentPage()); + } + + private class OnScrollHintListener implements OnAlarmListener { + + private final DragObject mDragObject; + + OnScrollHintListener(DragObject object) { + mDragObject = object; + } + + /** + * Scroll hint has been shown long enough. Now scroll to appropriate page. + */ + @Override + public void onAlarm(Alarm alarm) { + if (mCurrentScrollDir == DragController.SCROLL_LEFT) { + mContent.scrollLeft(); + mScrollHintDir = DragController.SCROLL_NONE; + } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) { + mContent.scrollRight(); + mScrollHintDir = DragController.SCROLL_NONE; + } else { + // This should not happen + return; + } + mCurrentScrollDir = DragController.SCROLL_NONE; + + // Pause drag event until the scrolling is finished + mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject)); + mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY); + } + } + + private class OnScrollFinishedListener implements OnAlarmListener { + + private final DragObject mDragObject; + + OnScrollFinishedListener(DragObject object) { + mDragObject = object; + } + + /** + * Page scroll is complete. + */ + @Override + public void onAlarm(Alarm alarm) { + // Reorder immediately on page change. + onDragOver(mDragObject, 1); + } + } + + // Compares item position based on rank and position giving priority to the rank. + private static final Comparator ITEM_POS_COMPARATOR = new Comparator() { + + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + if (lhs.rank != rhs.rank) { + return lhs.rank - rhs.rank; + } else if (lhs.cellY != rhs.cellY) { + return lhs.cellY - rhs.cellY; + } else { + return lhs.cellX - rhs.cellX; + } + } + }; } diff --git a/src/com/android/launcher3/FolderAutoScrollHelper.java b/src/com/android/launcher3/FolderAutoScrollHelper.java deleted file mode 100644 index 40e8884647..0000000000 --- a/src/com/android/launcher3/FolderAutoScrollHelper.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.support.v4.widget.AutoScrollHelper; -import android.widget.ScrollView; - -/** - * An implementation of {@link AutoScrollHelper} that knows how to scroll - * through a {@link Folder}. - */ -public class FolderAutoScrollHelper extends AutoScrollHelper { - private static final float MAX_SCROLL_VELOCITY = 1500f; - - private final ScrollView mTarget; - - public FolderAutoScrollHelper(ScrollView target) { - super(target); - - mTarget = target; - - setActivationDelay(0); - setEdgeType(EDGE_TYPE_INSIDE_EXTEND); - setExclusive(true); - setMaximumVelocity(MAX_SCROLL_VELOCITY, MAX_SCROLL_VELOCITY); - setRampDownDuration(0); - setRampUpDuration(0); - } - - @Override - public void scrollTargetBy(int deltaX, int deltaY) { - mTarget.scrollBy(deltaX, deltaY); - } - - @Override - public boolean canTargetScrollHorizontally(int direction) { - // List do not scroll horizontally. - return false; - } - - @Override - public boolean canTargetScrollVertically(int direction) { - return mTarget.canScrollVertically(direction); - } -} \ No newline at end of file diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java index a359f11805..8d534d2fef 100644 --- a/src/com/android/launcher3/FolderIcon.java +++ b/src/com/android/launcher3/FolderIcon.java @@ -43,6 +43,7 @@ import android.widget.TextView; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.FolderInfo.FolderListener; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -50,15 +51,16 @@ import java.util.ArrayList; * An icon that can appear on in the workspace representing an {@link UserFolder}. */ public class FolderIcon extends FrameLayout implements FolderListener { - private Launcher mLauncher; - private Folder mFolder; + @Thunk Launcher mLauncher; + @Thunk Folder mFolder; private FolderInfo mInfo; - private static boolean sStaticValuesDirty = true; + @Thunk static boolean sStaticValuesDirty = true; private CheckLongPressHelper mLongPressHelper; + private StylusEventHelper mStylusEventHelper; // The number of icons to display in the - private static final int NUM_ITEMS_IN_PREVIEW = 3; + public static final int NUM_ITEMS_IN_PREVIEW = 3; private static final int CONSUMPTION_ANIMATION_DURATION = 100; private static final int DROP_IN_ANIMATION_DURATION = 400; private static final int INITIAL_ITEM_ANIMATION_DURATION = 350; @@ -88,8 +90,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { public static Drawable sSharedFolderLeaveBehind = null; - private ImageView mPreviewBackground; - private BubbleTextView mFolderName; + @Thunk ImageView mPreviewBackground; + @Thunk BubbleTextView mFolderName; FolderRingAnimator mFolderRingAnimator = null; @@ -109,11 +111,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { private float mSlop; private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0); - private PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0); - private ArrayList mHiddenItems = new ArrayList(); + @Thunk PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0); + @Thunk ArrayList mHiddenItems = new ArrayList(); private Alarm mOpenAlarm = new Alarm(); - private ItemInfo mDragInfo; + @Thunk ItemInfo mDragInfo; public FolderIcon(Context context, AttributeSet attrs) { super(context, attrs); @@ -127,6 +129,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { private void init() { mLongPressHelper = new CheckLongPressHelper(this); + mStylusEventHelper = new StylusEventHelper(this); + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); } public boolean isDropEnabled() { @@ -145,8 +149,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + "is dependent on this"); } - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + + DeviceProfile grid = launcher.getDeviceProfile(); FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false); icon.setClipToPadding(false); @@ -191,7 +195,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { public static class FolderRingAnimator { public int mCellX; public int mCellY; - private CellLayout mCellLayout; + @Thunk CellLayout mCellLayout; public float mOuterRingSize; public float mInnerRingSize; public FolderIcon mFolderIcon = null; @@ -215,12 +219,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { + Thread.currentThread()); } - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = launcher.getDeviceProfile(); sPreviewSize = grid.folderIconSizePx; sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding); - sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo); - sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_nolip_holo); + sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer); + sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_nolip); sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest); sStaticValuesDirty = false; } @@ -488,8 +491,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { private void computePreviewDrawingParams(int drawableSize, int totalSize) { if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); mIntrinsicIconSize = drawableSize; mTotalWidth = totalSize; @@ -643,9 +645,10 @@ public class FolderIcon extends FrameLayout implements FolderListener { final Runnable onCompleteRunnable) { final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null); - final float scale0 = 1.0f; - final float transX0 = (mAvailableSpaceInPreview - d.getIntrinsicWidth()) / 2; - final float transY0 = (mAvailableSpaceInPreview - d.getIntrinsicHeight()) / 2 + getPaddingTop(); + float iconSize = mLauncher.getDeviceProfile().iconSizePx; + final float scale0 = iconSize / d.getIntrinsicWidth() ; + final float transX0 = (mAvailableSpaceInPreview - iconSize) / 2; + final float transY0 = (mAvailableSpaceInPreview - iconSize) / 2 + getPaddingTop(); mAnimParams.drawable = d; ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f); @@ -708,7 +711,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } public void onTitleChanged(CharSequence title) { - mFolderName.setText(title.toString()); + mFolderName.setText(title); setContentDescription(String.format(getContext().getString(R.string.folder_name_format), title)); } @@ -719,6 +722,12 @@ public class FolderIcon extends FrameLayout implements FolderListener { // isPressed() on an ACTION_UP boolean result = super.onTouchEvent(event); + // Check for a stylus button press, if it occurs cancel any long press checks. + if (mStylusEventHelper.checkAndPerformStylusEvent(event)) { + mLongPressHelper.cancelLongPress(); + return true; + } + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mLongPressHelper.postCheckForLongPress(); diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java index 85a792f4b4..aea21c95b5 100644 --- a/src/com/android/launcher3/FolderInfo.java +++ b/src/com/android/launcher3/FolderInfo.java @@ -29,19 +29,38 @@ import java.util.Arrays; */ public class FolderInfo extends ItemInfo { + public static final int NO_FLAGS = 0x00000000; + + /** + * The folder is locked in sorted mode + */ + public static final int FLAG_ITEMS_SORTED = 0x00000001; + + /** + * It is a work folder + */ + public static final int FLAG_WORK_FOLDER = 0x00000002; + + /** + * The multi-page animation has run for this folder + */ + public static final int FLAG_MULTI_PAGE_ANIMATION = 0x00000004; + /** * Whether this folder has been opened */ boolean opened; + public int options; + /** * The apps and shortcuts */ - ArrayList contents = new ArrayList(); + public ArrayList contents = new ArrayList(); ArrayList listeners = new ArrayList(); - FolderInfo() { + public FolderInfo() { itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; user = UserHandleCompat.myUserHandle(); } @@ -83,6 +102,8 @@ public class FolderInfo extends ItemInfo { void onAddToDatabase(Context context, ContentValues values) { super.onAddToDatabase(context, values); values.put(LauncherSettings.Favorites.TITLE, title.toString()); + values.put(LauncherSettings.Favorites.OPTIONS, options); + } void addListener(FolderListener listener) { @@ -121,4 +142,25 @@ public class FolderInfo extends ItemInfo { + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + ")"; } + + public boolean hasOption(int optionFlag) { + return (options & optionFlag) != 0; + } + + /** + * @param option flag to set or clear + * @param isEnabled whether to set or clear the flag + * @param context if not null, save changes to the db. + */ + public void setOption(int option, boolean isEnabled, Context context) { + int oldOptions = options; + if (isEnabled) { + options |= option; + } else { + options &= ~option; + } + if (context != null && oldOptions != options) { + LauncherModel.updateItemInDatabase(context, this); + } + } } diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java new file mode 100644 index 0000000000..f2ec1b68c5 --- /dev/null +++ b/src/com/android/launcher3/FolderPagedView.java @@ -0,0 +1,678 @@ +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; + +import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener; +import com.android.launcher3.PageIndicator.PageMarkerResources; +import com.android.launcher3.Workspace.ItemOperator; +import com.android.launcher3.util.Thunk; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class FolderPagedView extends PagedView { + + private static final String TAG = "FolderPagedView"; + + private static final boolean ALLOW_FOLDER_SCROLL = true; + + private static final int REORDER_ANIMATION_DURATION = 230; + private static final int START_VIEW_REORDER_DELAY = 30; + private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f; + + private static final int PAGE_INDICATOR_ANIMATION_START_DELAY = 300; + private static final int PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY = 150; + private static final int PAGE_INDICATOR_ANIMATION_DURATION = 400; + + // This value approximately overshoots to 1.5 times the original size. + private static final float PAGE_INDICATOR_OVERSHOOT_TENSION = 4.9f; + + /** + * Fraction of the width to scroll when showing the next page hint. + */ + private static final float SCROLL_HINT_FRACTION = 0.07f; + + private static final int[] sTempPosArray = new int[2]; + + public final boolean mIsRtl; + + private final LayoutInflater mInflater; + private final IconCache mIconCache; + + @Thunk final HashMap mPendingAnimations = new HashMap<>(); + + private final int mMaxCountX; + private final int mMaxCountY; + private final int mMaxItemsPerPage; + + private int mAllocatedContentSize; + private int mGridCountX; + private int mGridCountY; + + private Folder mFolder; + private FocusIndicatorView mFocusIndicatorView; + private PagedFolderKeyEventListener mKeyListener; + + private PageIndicator mPageIndicator; + + public FolderPagedView(Context context, AttributeSet attrs) { + super(context, attrs); + LauncherAppState app = LauncherAppState.getInstance(); + + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + mMaxCountX = profile.numFolderColumns; + mMaxCountY = profile.numFolderRows; + + mMaxItemsPerPage = mMaxCountX * mMaxCountY; + + mInflater = LayoutInflater.from(context); + mIconCache = app.getIconCache(); + + mIsRtl = Utilities.isRtl(getResources()); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + + setEdgeGlowColor(getResources().getColor(R.color.folder_edge_effect_color)); + } + + public void setFolder(Folder folder) { + mFolder = folder; + mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator); + mKeyListener = new PagedFolderKeyEventListener(folder); + mPageIndicator = (PageIndicator) folder.findViewById(R.id.folder_page_indicator); + } + + /** + * Sets up the grid size such that {@param count} items can fit in the grid. + * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while + * maintaining the restrictions of {@link #mMaxCountX} & {@link #mMaxCountY}. + */ + private void setupContentDimensions(int count) { + mAllocatedContentSize = count; + boolean done; + if (count >= mMaxItemsPerPage) { + mGridCountX = mMaxCountX; + mGridCountY = mMaxCountY; + done = true; + } else { + done = false; + } + + while (!done) { + int oldCountX = mGridCountX; + int oldCountY = mGridCountY; + if (mGridCountX * mGridCountY < count) { + // Current grid is too small, expand it + if ((mGridCountX <= mGridCountY || mGridCountY == mMaxCountY) && mGridCountX < mMaxCountX) { + mGridCountX++; + } else if (mGridCountY < mMaxCountY) { + mGridCountY++; + } + if (mGridCountY == 0) mGridCountY++; + } else if ((mGridCountY - 1) * mGridCountX >= count && mGridCountY >= mGridCountX) { + mGridCountY = Math.max(0, mGridCountY - 1); + } else if ((mGridCountX - 1) * mGridCountY >= count) { + mGridCountX = Math.max(0, mGridCountX - 1); + } + done = mGridCountX == oldCountX && mGridCountY == oldCountY; + } + + // Update grid size + for (int i = getPageCount() - 1; i >= 0; i--) { + getPageAt(i).setGridSize(mGridCountX, mGridCountY); + } + } + + /** + * Binds items to the layout. + * @return list of items that could not be bound, probably because we hit the max size limit. + */ + public ArrayList bindItems(ArrayList items) { + ArrayList icons = new ArrayList(); + ArrayList extra = new ArrayList(); + + for (ShortcutInfo item : items) { + if (!ALLOW_FOLDER_SCROLL && icons.size() >= mMaxItemsPerPage) { + extra.add(item); + } else { + icons.add(createNewView(item)); + } + } + arrangeChildren(icons, icons.size(), false); + return extra; + } + + /** + * Create space for a new item at the end, and returns the rank for that item. + * Also sets the current page to the last page. + */ + public int allocateRankForNewItem(ShortcutInfo info) { + int rank = getItemCount(); + ArrayList views = new ArrayList(mFolder.getItemsInReadingOrder()); + views.add(rank, null); + arrangeChildren(views, views.size(), false); + setCurrentPage(rank / mMaxItemsPerPage); + return rank; + } + + public View createAndAddViewForRank(ShortcutInfo item, int rank) { + View icon = createNewView(item); + addViewForRank(icon, item, rank); + return icon; + } + + /** + * Adds the {@param view} to the layout based on {@param rank} and updated the position + * related attributes. It assumes that {@param item} is already attached to the view. + */ + public void addViewForRank(View view, ShortcutInfo item, int rank) { + int pagePos = rank % mMaxItemsPerPage; + int pageNo = rank / mMaxItemsPerPage; + + item.rank = rank; + item.cellX = pagePos % mGridCountX; + item.cellY = pagePos / mGridCountX; + + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); + lp.cellX = item.cellX; + lp.cellY = item.cellY; + getPageAt(pageNo).addViewToCellLayout( + view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true); + } + + @SuppressLint("InflateParams") + public View createNewView(ShortcutInfo item) { + final BubbleTextView textView = (BubbleTextView) mInflater.inflate( + R.layout.folder_application, null, false); + textView.applyFromShortcutInfo(item, mIconCache); + textView.setOnClickListener(mFolder); + textView.setOnLongClickListener(mFolder); + textView.setOnFocusChangeListener(mFocusIndicatorView); + textView.setOnKeyListener(mKeyListener); + + textView.setLayoutParams(new CellLayout.LayoutParams( + item.cellX, item.cellY, item.spanX, item.spanY)); + return textView; + } + + @Override + public CellLayout getPageAt(int index) { + return (CellLayout) getChildAt(index); + } + + public void removeCellLayoutView(View view) { + for (int i = getChildCount() - 1; i >= 0; i --) { + getPageAt(i).removeView(view); + } + } + + public CellLayout getCurrentCellLayout() { + return getPageAt(getNextPage()); + } + + private CellLayout createAndAddNewPage() { + DeviceProfile grid = ((Launcher) getContext()).getDeviceProfile(); + CellLayout page = new CellLayout(getContext()); + page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); + page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); + page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + page.setInvertIfRtl(true); + page.setGridSize(mGridCountX, mGridCountY); + + addView(page, -1, generateDefaultLayoutParams()); + return page; + } + + @Override + protected int getChildGap() { + return getPaddingLeft() + getPaddingRight(); + } + + public void setFixedSize(int width, int height) { + width -= (getPaddingLeft() + getPaddingRight()); + height -= (getPaddingTop() + getPaddingBottom()); + for (int i = getChildCount() - 1; i >= 0; i --) { + ((CellLayout) getChildAt(i)).setFixedSize(width, height); + } + } + + public void removeItem(View v) { + for (int i = getChildCount() - 1; i >= 0; i --) { + getPageAt(i).removeView(v); + } + } + + /** + * Updates position and rank of all the children in the view. + * It essentially removes all views from all the pages and then adds them again in appropriate + * page. + * + * @param list the ordered list of children. + * @param itemCount if greater than the total children count, empty spaces are left + * at the end, otherwise it is ignored. + * + */ + public void arrangeChildren(ArrayList list, int itemCount) { + arrangeChildren(list, itemCount, true); + } + + @SuppressLint("RtlHardcoded") + private void arrangeChildren(ArrayList list, int itemCount, boolean saveChanges) { + ArrayList pages = new ArrayList(); + for (int i = 0; i < getChildCount(); i++) { + CellLayout page = (CellLayout) getChildAt(i); + page.removeAllViews(); + pages.add(page); + } + setupContentDimensions(itemCount); + + Iterator pageItr = pages.iterator(); + CellLayout currentPage = null; + + int position = 0; + int newX, newY, rank; + + rank = 0; + for (int i = 0; i < itemCount; i++) { + View v = list.size() > i ? list.get(i) : null; + if (currentPage == null || position >= mMaxItemsPerPage) { + // Next page + if (pageItr.hasNext()) { + currentPage = pageItr.next(); + } else { + currentPage = createAndAddNewPage(); + } + position = 0; + } + + if (v != null) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); + newX = position % mGridCountX; + newY = position / mGridCountX; + ItemInfo info = (ItemInfo) v.getTag(); + if (info.cellX != newX || info.cellY != newY || info.rank != rank) { + info.cellX = newX; + info.cellY = newY; + info.rank = rank; + if (saveChanges) { + LauncherModel.addOrMoveItemInDatabase(getContext(), info, + mFolder.mInfo.id, 0, info.cellX, info.cellY); + } + } + lp.cellX = info.cellX; + lp.cellY = info.cellY; + currentPage.addViewToCellLayout( + v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true); + } + + rank ++; + position++; + } + + // Remove extra views. + boolean removed = false; + while (pageItr.hasNext()) { + removeView(pageItr.next()); + removed = true; + } + if (removed) { + setCurrentPage(0); + } + + setEnableOverscroll(getPageCount() > 1); + + // Update footer + mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE); + // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text. + mFolder.mFolderName.setGravity(getPageCount() > 1 ? + (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL); + } + + public int getDesiredWidth() { + return getPageCount() > 0 ? + (getPageAt(0).getDesiredWidth() + getPaddingLeft() + getPaddingRight()) : 0; + } + + public int getDesiredHeight() { + return getPageCount() > 0 ? + (getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0; + } + + public int getItemCount() { + int lastPageIndex = getChildCount() - 1; + if (lastPageIndex < 0) { + // If there are no pages, nothing has yet been added to the folder. + return 0; + } + return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount() + + lastPageIndex * mMaxItemsPerPage; + } + + /** + * @return the rank of the cell nearest to the provided pixel position. + */ + public int findNearestArea(int pixelX, int pixelY) { + int pageIndex = getNextPage(); + CellLayout page = getPageAt(pageIndex); + page.findNearestArea(pixelX, pixelY, 1, 1, sTempPosArray); + if (mFolder.isLayoutRtl()) { + sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1; + } + return Math.min(mAllocatedContentSize - 1, + pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]); + } + + @Override + protected PageMarkerResources getPageIndicatorMarker(int pageIndex) { + return new PageMarkerResources(R.drawable.ic_pageindicator_current_folder, + R.drawable.ic_pageindicator_default_folder); + } + + public boolean isFull() { + return !ALLOW_FOLDER_SCROLL && getItemCount() >= mMaxItemsPerPage; + } + + public View getLastItem() { + if (getChildCount() < 1) { + return null; + } + ShortcutAndWidgetContainer lastContainer = getCurrentCellLayout().getShortcutsAndWidgets(); + int lastRank = lastContainer.getChildCount() - 1; + if (mGridCountX > 0) { + return lastContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX); + } else { + return lastContainer.getChildAt(lastRank); + } + } + + /** + * Iterates over all its items in a reading order. + * @return the view for which the operator returned true. + */ + public View iterateOverItems(ItemOperator op) { + for (int k = 0 ; k < getChildCount(); k++) { + CellLayout page = getPageAt(k); + for (int j = 0; j < page.getCountY(); j++) { + for (int i = 0; i < page.getCountX(); i++) { + View v = page.getChildAt(i, j); + if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v, this)) { + return v; + } + } + } + } + return null; + } + + public String getAccessibilityDescription() { + return String.format(getContext().getString(R.string.folder_opened), + mGridCountX, mGridCountY); + } + + /** + * Sets the focus on the first visible child. + */ + public void setFocusOnFirstChild() { + View firstChild = getCurrentCellLayout().getChildAt(0, 0); + if (firstChild != null) { + firstChild.requestFocus(); + } + } + + @Override + protected void notifyPageSwitchListener() { + super.notifyPageSwitchListener(); + if (mFolder != null) { + mFolder.updateTextViewFocus(); + } + } + + /** + * Scrolls the current view by a fraction + */ + public void showScrollHint(int direction) { + float fraction = (direction == DragController.SCROLL_LEFT) ^ mIsRtl + ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION; + int hint = (int) (fraction * getWidth()); + int scroll = getScrollForPage(getNextPage()) + hint; + int delta = scroll - getScrollX(); + if (delta != 0) { + mScroller.setInterpolator(new DecelerateInterpolator()); + mScroller.startScroll(getScrollX(), 0, delta, 0, Folder.SCROLL_HINT_DURATION); + invalidate(); + } + } + + public void clearScrollHint() { + if (getScrollX() != getScrollForPage(getNextPage())) { + snapToPage(getNextPage()); + } + } + + /** + * Finish animation all the views which are animating across pages + */ + public void completePendingPageChanges() { + if (!mPendingAnimations.isEmpty()) { + HashMap pendingViews = new HashMap<>(mPendingAnimations); + for (Map.Entry e : pendingViews.entrySet()) { + e.getKey().animate().cancel(); + e.getValue().run(); + } + } + } + + public boolean rankOnCurrentPage(int rank) { + int p = rank / mMaxItemsPerPage; + return p == getNextPage(); + } + + @Override + protected void onPageBeginMoving() { + super.onPageBeginMoving(); + getVisiblePages(sTempPosArray); + for (int i = sTempPosArray[0]; i <= sTempPosArray[1]; i++) { + verifyVisibleHighResIcons(i); + } + } + + /** + * Ensures that all the icons on the given page are of high-res + */ + public void verifyVisibleHighResIcons(int pageNo) { + CellLayout page = getPageAt(pageNo); + if (page != null) { + ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets(); + for (int i = parent.getChildCount() - 1; i >= 0; i--) { + ((BubbleTextView) parent.getChildAt(i)).verifyHighRes(); + } + } + } + + public int getAllocatedContentSize() { + return mAllocatedContentSize; + } + + /** + * Reorders the items such that the {@param empty} spot moves to {@param target} + */ + public void realTimeReorder(int empty, int target) { + completePendingPageChanges(); + int delay = 0; + float delayAmount = START_VIEW_REORDER_DELAY; + + // Animation only happens on the current page. + int pageToAnimate = getNextPage(); + + int pageT = target / mMaxItemsPerPage; + int pagePosT = target % mMaxItemsPerPage; + + if (pageT != pageToAnimate) { + Log.e(TAG, "Cannot animate when the target cell is invisible"); + } + int pagePosE = empty % mMaxItemsPerPage; + int pageE = empty / mMaxItemsPerPage; + + int startPos, endPos; + int moveStart, moveEnd; + int direction; + + if (target == empty) { + // No animation + return; + } else if (target > empty) { + // Items will move backwards to make room for the empty cell. + direction = 1; + + // If empty cell is in a different page, move them instantly. + if (pageE < pageToAnimate) { + moveStart = empty; + // Instantly move the first item in the current page. + moveEnd = pageToAnimate * mMaxItemsPerPage; + // Animate the 2nd item in the current page, as the first item was already moved to + // the last page. + startPos = 0; + } else { + moveStart = moveEnd = -1; + startPos = pagePosE; + } + + endPos = pagePosT; + } else { + // The items will move forward. + direction = -1; + + if (pageE > pageToAnimate) { + // Move the items immediately. + moveStart = empty; + // Instantly move the last item in the current page. + moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1; + + // Animations start with the second last item in the page + startPos = mMaxItemsPerPage - 1; + } else { + moveStart = moveEnd = -1; + startPos = pagePosE; + } + + endPos = pagePosT; + } + + // Instant moving views. + while (moveStart != moveEnd) { + int rankToMove = moveStart + direction; + int p = rankToMove / mMaxItemsPerPage; + int pagePos = rankToMove % mMaxItemsPerPage; + int x = pagePos % mGridCountX; + int y = pagePos / mGridCountX; + + final CellLayout page = getPageAt(p); + final View v = page.getChildAt(x, y); + if (v != null) { + if (pageToAnimate != p) { + page.removeView(v); + addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart); + } else { + // Do a fake animation before removing it. + final int newRank = moveStart; + final float oldTranslateX = v.getTranslationX(); + + Runnable endAction = new Runnable() { + + @Override + public void run() { + mPendingAnimations.remove(v); + v.setTranslationX(oldTranslateX); + ((CellLayout) v.getParent().getParent()).removeView(v); + addViewForRank(v, (ShortcutInfo) v.getTag(), newRank); + } + }; + v.animate() + .translationXBy((direction > 0 ^ mIsRtl) ? -v.getWidth() : v.getWidth()) + .setDuration(REORDER_ANIMATION_DURATION) + .setStartDelay(0) + .withEndAction(endAction); + mPendingAnimations.put(v, endAction); + } + } + moveStart = rankToMove; + } + + if ((endPos - startPos) * direction <= 0) { + // No animation + return; + } + + CellLayout page = getPageAt(pageToAnimate); + for (int i = startPos; i != endPos; i += direction) { + int nextPos = i + direction; + View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX); + if (v != null) { + ((ItemInfo) v.getTag()).rank -= direction; + } + if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX, + REORDER_ANIMATION_DURATION, delay, true, true)) { + delay += delayAmount; + delayAmount *= VIEW_REORDER_DELAY_FACTOR; + } + } + } + + public void setMarkerScale(float scale) { + int count = mPageIndicator.getChildCount(); + for (int i = 0; i < count; i++) { + View marker = mPageIndicator.getChildAt(i); + marker.animate().cancel(); + marker.setScaleX(scale); + marker.setScaleY(scale); + } + } + + public void animateMarkers() { + int count = mPageIndicator.getChildCount(); + Interpolator interpolator = new OvershootInterpolator(PAGE_INDICATOR_OVERSHOOT_TENSION); + for (int i = 0; i < count; i++) { + mPageIndicator.getChildAt(i).animate().scaleX(1).scaleY(1) + .setInterpolator(interpolator) + .setDuration(PAGE_INDICATOR_ANIMATION_DURATION) + .setStartDelay(PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY * i + + PAGE_INDICATOR_ANIMATION_START_DELAY); + } + } + + public int itemsPerPage() { + return mMaxItemsPerPage; + } + + @Override + protected void getEdgeVerticalPostion(int[] pos) { + pos[0] = 0; + pos[1] = getViewportHeight(); + } +} diff --git a/src/com/android/launcher3/HolographicOutlineHelper.java b/src/com/android/launcher3/HolographicOutlineHelper.java index b1e0e68a4c..5ff85d664c 100644 --- a/src/com/android/launcher3/HolographicOutlineHelper.java +++ b/src/com/android/launcher3/HolographicOutlineHelper.java @@ -17,6 +17,7 @@ package com.android.launcher3; import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; @@ -25,11 +26,16 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.util.SparseArray; +/** + * Utility class to generate shadow and outline effect, which are used for click feedback + * and drag-n-drop respectively. + */ public class HolographicOutlineHelper { - private static final Rect sTempRect = new Rect(); + private static HolographicOutlineHelper sInstance; private final Canvas mCanvas = new Canvas(); private final Paint mDrawPaint = new Paint(); @@ -40,26 +46,23 @@ public class HolographicOutlineHelper { private final BlurMaskFilter mThinOuterBlurMaskFilter; private final BlurMaskFilter mMediumInnerBlurMaskFilter; - private final BlurMaskFilter mShaowBlurMaskFilter; - private final int mShadowOffset; + private final BlurMaskFilter mShadowBlurMaskFilter; - /** - * Padding used when creating shadow bitmap; - */ - final int shadowBitmapPadding; - - static HolographicOutlineHelper INSTANCE; + // We have 4 different icon sizes: homescreen, hotseat, folder & all-apps + private final SparseArray mBitmapCache = new SparseArray<>(4); private HolographicOutlineHelper(Context context) { - final float scale = LauncherAppState.getInstance().getScreenDensity(); + Resources res = context.getResources(); - mMediumOuterBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.OUTER); - mThinOuterBlurMaskFilter = new BlurMaskFilter(scale * 1.0f, BlurMaskFilter.Blur.OUTER); - mMediumInnerBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.NORMAL); + float mediumBlur = res.getDimension(R.dimen.blur_size_medium_outline); + mMediumOuterBlurMaskFilter = new BlurMaskFilter(mediumBlur, BlurMaskFilter.Blur.OUTER); + mMediumInnerBlurMaskFilter = new BlurMaskFilter(mediumBlur, BlurMaskFilter.Blur.NORMAL); - mShaowBlurMaskFilter = new BlurMaskFilter(scale * 4.0f, BlurMaskFilter.Blur.NORMAL); - mShadowOffset = (int) (scale * 2.0f); - shadowBitmapPadding = (int) (scale * 4.0f); + mThinOuterBlurMaskFilter = new BlurMaskFilter( + res.getDimension(R.dimen.blur_size_thin_outline), BlurMaskFilter.Blur.OUTER); + + mShadowBlurMaskFilter = new BlurMaskFilter( + res.getDimension(R.dimen.blur_size_click_shadow), BlurMaskFilter.Blur.NORMAL); mDrawPaint.setFilterBitmap(true); mDrawPaint.setAntiAlias(true); @@ -71,10 +74,10 @@ public class HolographicOutlineHelper { } public static HolographicOutlineHelper obtain(Context context) { - if (INSTANCE == null) { - INSTANCE = new HolographicOutlineHelper(context); + if (sInstance == null) { + sInstance = new HolographicOutlineHelper(context); } - return INSTANCE; + return sInstance; } /** @@ -153,51 +156,34 @@ public class HolographicOutlineHelper { } Bitmap createMediumDropShadow(BubbleTextView view) { - final Bitmap result = Bitmap.createBitmap( - view.getWidth() + shadowBitmapPadding + shadowBitmapPadding, - view.getHeight() + shadowBitmapPadding + shadowBitmapPadding + mShadowOffset, - Bitmap.Config.ARGB_8888); + Drawable icon = view.getIcon(); + if (icon == null) { + return null; + } + Rect rect = icon.getBounds(); - mCanvas.setBitmap(result); + int bitmapWidth = (int) (rect.width() * view.getScaleX()); + int bitmapHeight = (int) (rect.height() * view.getScaleY()); - final Rect clipRect = sTempRect; - view.getDrawingRect(sTempRect); - // adjust the clip rect so that we don't include the text label - clipRect.bottom = view.getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V - + view.getLayout().getLineTop(0); + int key = (bitmapWidth << 16) | bitmapHeight; + Bitmap cache = mBitmapCache.get(key); + if (cache == null) { + cache = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + mCanvas.setBitmap(cache); + mBitmapCache.put(key, cache); + } else { + mCanvas.setBitmap(cache); + mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } - // Draw the View into the bitmap. - // The translate of scrollX and scrollY is necessary when drawing TextViews, because - // they set scrollX and scrollY to large values to achieve centered text - mCanvas.save(); - mCanvas.scale(view.getScaleX(), view.getScaleY(), - view.getWidth() / 2 + shadowBitmapPadding, - view.getHeight() / 2 + shadowBitmapPadding); - mCanvas.translate(-view.getScrollX() + shadowBitmapPadding, - -view.getScrollY() + shadowBitmapPadding); - mCanvas.clipRect(clipRect, Op.REPLACE); - view.draw(mCanvas); + mCanvas.save(Canvas.MATRIX_SAVE_FLAG); + mCanvas.scale(view.getScaleX(), view.getScaleY()); + mCanvas.translate(-rect.left, -rect.top); + icon.draw(mCanvas); mCanvas.restore(); - - int[] blurOffst = new int[2]; - mBlurPaint.setMaskFilter(mShaowBlurMaskFilter); - Bitmap blurBitmap = result.extractAlpha(mBlurPaint, blurOffst); - - mCanvas.save(); - mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - mCanvas.translate(blurOffst[0], blurOffst[1]); - - mDrawPaint.setColor(Color.BLACK); - mDrawPaint.setAlpha(30); - mCanvas.drawBitmap(blurBitmap, 0, 0, mDrawPaint); - - mDrawPaint.setAlpha(60); - mCanvas.drawBitmap(blurBitmap, 0, mShadowOffset, mDrawPaint); - mCanvas.restore(); - mCanvas.setBitmap(null); - blurBitmap.recycle(); - return result; + mBlurPaint.setMaskFilter(mShadowBlurMaskFilter); + return cache.extractAlpha(mBlurPaint, null); } } diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java index b08272f369..17fdeb1dc8 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -16,24 +16,19 @@ package com.android.launcher3; -import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; -import java.util.ArrayList; - -public class Hotseat extends FrameLayout { - private static final String TAG = "Hotseat"; +public class Hotseat extends FrameLayout + implements Stats.LaunchSourceProvider{ private CellLayout mContent; @@ -41,8 +36,7 @@ public class Hotseat extends FrameLayout { private int mAllAppsButtonRank; - private boolean mTransposeLayoutWithOrientation; - private boolean mIsLandscape; + private final boolean mHasVerticalHotseat; public Hotseat(Context context) { this(context, null); @@ -54,22 +48,21 @@ public class Hotseat extends FrameLayout { public Hotseat(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - - Resources r = context.getResources(); - mTransposeLayoutWithOrientation = - r.getBoolean(R.bool.hotseat_transpose_layout_with_orientation); - mIsLandscape = context.getResources().getConfiguration().orientation == - Configuration.ORIENTATION_LANDSCAPE; - } - - public void setup(Launcher launcher) { - mLauncher = launcher; + mLauncher = (Launcher) context; + mHasVerticalHotseat = mLauncher.getDeviceProfile().isVerticalBarLayout(); } CellLayout getLayout() { return mContent; } + /** + * Returns whether there are other icons than the all apps button in the hotseat. + */ + public boolean hasIcons() { + return mContent.getShortcutsAndWidgets().getChildCount() > 1; + } + /** * Registers the specified listener on the cell layout of the hotseat. */ @@ -78,60 +71,35 @@ public class Hotseat extends FrameLayout { mContent.setOnLongClickListener(l); } - private boolean hasVerticalHotseat() { - return (mIsLandscape && mTransposeLayoutWithOrientation); - } - /* Get the orientation invariant order of the item in the hotseat for persistence. */ int getOrderInHotseat(int x, int y) { - return hasVerticalHotseat() ? (mContent.getCountY() - y - 1) : x; + return mHasVerticalHotseat ? (mContent.getCountY() - y - 1) : x; } + /* Get the orientation specific coordinates given an invariant order in the hotseat. */ int getCellXFromOrder(int rank) { - return hasVerticalHotseat() ? 0 : rank; + return mHasVerticalHotseat ? 0 : rank; } + int getCellYFromOrder(int rank) { - return hasVerticalHotseat() ? (mContent.getCountY() - (rank + 1)) : 0; + return mHasVerticalHotseat ? (mContent.getCountY() - (rank + 1)) : 0; } + public boolean isAllAppsButtonRank(int rank) { - if (LauncherAppState.isDisableAllApps()) { - return false; - } else { - return rank == mAllAppsButtonRank; - } - } - - /** This returns the coordinates of an app in a given cell, relative to the DragLayer */ - Rect getCellCoordinates(int cellX, int cellY) { - Rect coords = new Rect(); - mContent.cellToRect(cellX, cellY, 1, 1, coords); - int[] hotseatInParent = new int[2]; - Utilities.getDescendantCoordRelativeToParent(this, mLauncher.getDragLayer(), - hotseatInParent, false); - coords.offset(hotseatInParent[0], hotseatInParent[1]); - - // Center the icon - int cWidth = mContent.getShortcutsAndWidgets().getCellContentWidth(); - int cHeight = mContent.getShortcutsAndWidgets().getCellContentHeight(); - int cellPaddingX = (int) Math.max(0, ((coords.width() - cWidth) / 2f)); - int cellPaddingY = (int) Math.max(0, ((coords.height() - cHeight) / 2f)); - coords.offset(cellPaddingX, cellPaddingY); - - return coords; + return rank == mAllAppsButtonRank; } @Override protected void onFinishInflate() { super.onFinishInflate(); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); - mAllAppsButtonRank = grid.hotseatAllAppsRank; + mAllAppsButtonRank = grid.inv.hotseatAllAppsRank; mContent = (CellLayout) findViewById(R.id.layout); - if (grid.isLandscape && !grid.isLargeTablet()) { - mContent.setGridSize(1, (int) grid.numHotseatIcons); + if (grid.isLandscape && !grid.isLargeTablet) { + mContent.setGridSize(1, (int) grid.inv.numHotseatIcons); } else { - mContent.setGridSize((int) grid.numHotseatIcons, 1); + mContent.setGridSize((int) grid.inv.numHotseatIcons, 1); } mContent.setIsHotseat(true); @@ -141,35 +109,34 @@ public class Hotseat extends FrameLayout { void resetLayout() { mContent.removeAllViewsInLayout(); - if (!LauncherAppState.isDisableAllApps()) { - // Add the Apps button - Context context = getContext(); + // Add the Apps button + Context context = getContext(); - LayoutInflater inflater = LayoutInflater.from(context); - TextView allAppsButton = (TextView) - inflater.inflate(R.layout.all_apps_button, mContent, false); - Drawable d = context.getResources().getDrawable(R.drawable.all_apps_button_icon); + LayoutInflater inflater = LayoutInflater.from(context); + TextView allAppsButton = (TextView) + inflater.inflate(R.layout.all_apps_button, mContent, false); + Drawable d = context.getResources().getDrawable(R.drawable.all_apps_button_icon); - Utilities.resizeIconDrawable(d); - allAppsButton.setCompoundDrawables(null, d, null, null); + mLauncher.resizeIconDrawable(d); + allAppsButton.setCompoundDrawables(null, d, null, null); - allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label)); - allAppsButton.setOnKeyListener(new HotseatIconKeyEventListener()); - if (mLauncher != null) { - allAppsButton.setOnTouchListener(mLauncher.getHapticFeedbackTouchListener()); - mLauncher.setAllAppsButton(allAppsButton); - allAppsButton.setOnClickListener(mLauncher); - allAppsButton.setOnFocusChangeListener(mLauncher.mFocusHandler); - } - - // Note: We do this to ensure that the hotseat is always laid out in the orientation of - // the hotseat in order regardless of which orientation they were added - int x = getCellXFromOrder(mAllAppsButtonRank); - int y = getCellYFromOrder(mAllAppsButtonRank); - CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); - lp.canReorder = false; - mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true); + allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label)); + allAppsButton.setOnKeyListener(new HotseatIconKeyEventListener()); + if (mLauncher != null) { + mLauncher.setAllAppsButton(allAppsButton); + allAppsButton.setOnTouchListener(mLauncher.getHapticFeedbackTouchListener()); + allAppsButton.setOnClickListener(mLauncher); + allAppsButton.setOnLongClickListener(mLauncher); + allAppsButton.setOnFocusChangeListener(mLauncher.mFocusHandler); } + + // Note: We do this to ensure that the hotseat is always laid out in the orientation of + // the hotseat in order regardless of which orientation they were added + int x = getCellXFromOrder(mAllAppsButtonRank); + int y = getCellYFromOrder(mAllAppsButtonRank); + CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1); + lp.canReorder = false; + mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true); } @Override @@ -182,54 +149,8 @@ public class Hotseat extends FrameLayout { return false; } - void addAllAppsFolder(IconCache iconCache, - ArrayList allApps, ArrayList onWorkspace, - Launcher launcher, Workspace workspace) { - if (LauncherAppState.isDisableAllApps()) { - FolderInfo fi = new FolderInfo(); - - fi.cellX = getCellXFromOrder(mAllAppsButtonRank); - fi.cellY = getCellYFromOrder(mAllAppsButtonRank); - fi.spanX = 1; - fi.spanY = 1; - fi.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT; - fi.screenId = mAllAppsButtonRank; - fi.itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; - fi.title = "More Apps"; - LauncherModel.addItemToDatabase(launcher, fi, fi.container, fi.screenId, fi.cellX, - fi.cellY, false); - FolderIcon folder = FolderIcon.fromXml(R.layout.folder_icon, launcher, - getLayout(), fi, iconCache); - workspace.addInScreen(folder, fi.container, fi.screenId, fi.cellX, fi.cellY, - fi.spanX, fi.spanY); - - for (AppInfo info: allApps) { - ComponentName cn = info.intent.getComponent(); - if (!onWorkspace.contains(cn)) { - Log.d(TAG, "Adding to 'more apps': " + info.intent); - ShortcutInfo si = info.makeShortcut(); - fi.add(si); - } - } - } - } - - void addAppsToAllAppsFolder(ArrayList apps) { - if (LauncherAppState.isDisableAllApps()) { - View v = mContent.getChildAt(getCellXFromOrder(mAllAppsButtonRank), getCellYFromOrder(mAllAppsButtonRank)); - FolderIcon fi = null; - - if (v instanceof FolderIcon) { - fi = (FolderIcon) v; - } else { - return; - } - - FolderInfo info = fi.getFolderInfo(); - for (AppInfo a: apps) { - ShortcutInfo si = a.makeShortcut(); - info.add(si); - } - } + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_HOTSEAT); } } diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 5a0875b305..916418f18c 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -16,19 +16,28 @@ package com.android.launcher3; -import android.app.ActivityManager; import android.content.ComponentName; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; @@ -36,17 +45,17 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Thunk; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.Map.Entry; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.Stack; /** * Cache of application icons. Icons can be made from any thread. @@ -56,66 +65,71 @@ public class IconCache { private static final String TAG = "Launcher.IconCache"; private static final int INITIAL_ICON_CACHE_CAPACITY = 50; - private static final String RESOURCE_FILE_PREFIX = "icon_"; // Empty class name is used for storing package default entry. private static final String EMPTY_CLASS_NAME = "."; private static final boolean DEBUG = false; - private static class CacheEntry { + private static final int LOW_RES_SCALE_FACTOR = 5; + + @Thunk static final Object ICON_UPDATE_TOKEN = new Object(); + + @Thunk static class CacheEntry { public Bitmap icon; - public CharSequence title; - public CharSequence contentDescription; + public CharSequence title = ""; + public CharSequence contentDescription = ""; + public boolean isLowResIcon; } - private static class CacheKey { - public ComponentName componentName; - public UserHandleCompat user; + private final HashMap mDefaultIcons = new HashMap<>(); + @Thunk final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); - CacheKey(ComponentName componentName, UserHandleCompat user) { - this.componentName = componentName; - this.user = user; - } - - @Override - public int hashCode() { - return componentName.hashCode() + user.hashCode(); - } - - @Override - public boolean equals(Object o) { - CacheKey other = (CacheKey) o; - return other.componentName.equals(componentName) && other.user.equals(user); - } - } - - private final HashMap mDefaultIcons = - new HashMap(); private final Context mContext; private final PackageManager mPackageManager; - private final UserManagerCompat mUserManager; + @Thunk final UserManagerCompat mUserManager; private final LauncherAppsCompat mLauncherApps; - private final HashMap mCache = - new HashMap(INITIAL_ICON_CACHE_CAPACITY); - private int mIconDpi; + private final HashMap mCache = + new HashMap(INITIAL_ICON_CACHE_CAPACITY); + private final int mIconDpi; + @Thunk final IconDB mIconDb; - public IconCache(Context context) { - ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + @Thunk final Handler mWorkerHandler; + // The background color used for activity icons. Since these icons are displayed in all-apps + // and folders, this would be same as the light quantum panel background. This color + // is used to convert icons to RGB_565. + private final int mActivityBgColor; + // The background color used for package icons. These are displayed in widget tray, which + // has a dark quantum panel background. + private final int mPackageBgColor; + private final BitmapFactory.Options mLowResOptions; + + private String mSystemState; + private Bitmap mLowResBitmap; + private Canvas mLowResCanvas; + private Paint mLowResPaint; + + public IconCache(Context context, InvariantDeviceProfile inv) { mContext = context; mPackageManager = context.getPackageManager(); mUserManager = UserManagerCompat.getInstance(mContext); mLauncherApps = LauncherAppsCompat.getInstance(mContext); - mIconDpi = activityManager.getLauncherLargeIconDensity(); + mIconDpi = inv.fillResIconDpi; + mIconDb = new IconDB(context); - // need to set mIconDpi before getting default icon - UserHandleCompat myUser = UserHandleCompat.myUserHandle(); - mDefaultIcons.put(myUser, makeDefaultIcon(myUser)); + mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); + + mActivityBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color); + mPackageBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color_dark); + mLowResOptions = new BitmapFactory.Options(); + // Always prefer RGB_565 config for low res. If the bitmap has transparency, it will + // automatically be loaded as ALPHA_8888. + mLowResOptions.inPreferredConfig = Bitmap.Config.RGB_565; + updateSystemStateString(); } - public Drawable getFullResDefaultActivityIcon() { + private Drawable getFullResDefaultActivityIcon() { return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon); } @@ -145,10 +159,6 @@ public class IconCache { return getFullResDefaultActivityIcon(); } - public int getFullResIconDpi() { - return mIconDpi; - } - public Drawable getFullResIcon(ActivityInfo info) { Resources resources; try { @@ -184,59 +194,269 @@ public class IconCache { * Remove any records for the supplied ComponentName. */ public synchronized void remove(ComponentName componentName, UserHandleCompat user) { - mCache.remove(new CacheKey(componentName, user)); + mCache.remove(new ComponentKey(componentName, user)); } /** - * Remove any records for the supplied package name. + * Remove any records for the supplied package name from memory. */ - public synchronized void remove(String packageName, UserHandleCompat user) { - HashSet forDeletion = new HashSet(); - for (CacheKey key: mCache.keySet()) { + private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) { + HashSet forDeletion = new HashSet(); + for (ComponentKey key: mCache.keySet()) { if (key.componentName.getPackageName().equals(packageName) && key.user.equals(user)) { forDeletion.add(key); } } - for (CacheKey condemned: forDeletion) { + for (ComponentKey condemned: forDeletion) { mCache.remove(condemned); } } /** - * Empty out the cache. + * Updates the entries related to the given package in memory and persistent DB. */ - public synchronized void flush() { - mCache.clear(); + public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) { + removeIconsForPkg(packageName, user); + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, + PackageManager.GET_UNINSTALLED_PACKAGES); + long userSerial = mUserManager.getSerialNumberForUser(user); + for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) { + addIconToDBAndMemCache(app, info, userSerial); + } + } catch (NameNotFoundException e) { + Log.d(TAG, "Package not found", e); + return; + } } /** - * Empty out the cache that aren't of the correct grid size + * Removes the entries related to the given package in memory and persistent DB. */ - public synchronized void flushInvalidIcons(DeviceProfile grid) { - Iterator> it = mCache.entrySet().iterator(); - while (it.hasNext()) { - final CacheEntry e = it.next().getValue(); - if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx - || e.icon.getHeight() < grid.iconSizePx)) { - it.remove(); + public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) { + removeFromMemCacheLocked(packageName, user); + long userSerial = mUserManager.getSerialNumberForUser(user); + mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {packageName + "/%", Long.toString(userSerial)}); + } + + public void updateDbIcons(Set ignorePackagesForMainUser) { + // Remove all active icon update tasks. + mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN); + + updateSystemStateString(); + for (UserHandleCompat user : mUserManager.getUserProfiles()) { + // Query for the set of apps + final List apps = mLauncherApps.getActivityList(null, user); + // Fail if we don't have any apps + // TODO: Fix this. Only fail for the current user. + if (apps == null || apps.isEmpty()) { + return; + } + + // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} + // is called by the icon cache when the job is complete. + updateDBIcons(user, apps, UserHandleCompat.myUserHandle().equals(user) + ? ignorePackagesForMainUser : Collections.emptySet()); + } + } + + /** + * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in + * the DB and are updated. + * @return The set of packages for which icons have updated. + */ + private void updateDBIcons(UserHandleCompat user, List apps, + Set ignorePackages) { + long userSerial = mUserManager.getSerialNumberForUser(user); + PackageManager pm = mContext.getPackageManager(); + HashMap pkgInfoMap = new HashMap(); + for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { + pkgInfoMap.put(info.packageName, info); + } + + HashMap componentMap = new HashMap<>(); + for (LauncherActivityInfoCompat app : apps) { + componentMap.put(app.getComponentName(), app); + } + + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + new String[] {IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, + IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION, + IconDB.COLUMN_SYSTEM_STATE}, + IconDB.COLUMN_USER + " = ? ", + new String[] {Long.toString(userSerial)}, + null, null, null); + + final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); + final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED); + final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION); + final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); + final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE); + + HashSet itemsToRemove = new HashSet(); + Stack appsToUpdate = new Stack<>(); + + while (c.moveToNext()) { + String cn = c.getString(indexComponent); + ComponentName component = ComponentName.unflattenFromString(cn); + PackageInfo info = pkgInfoMap.get(component.getPackageName()); + if (info == null) { + if (!ignorePackages.contains(component.getPackageName())) { + remove(component, user); + itemsToRemove.add(c.getInt(rowIndex)); + } + continue; + } + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) { + // Application is not present + continue; + } + + long updateTime = c.getLong(indexLastUpdate); + int version = c.getInt(indexVersion); + LauncherActivityInfoCompat app = componentMap.remove(component); + if (version == info.versionCode && updateTime == info.lastUpdateTime && + TextUtils.equals(mSystemState, c.getString(systemStateIndex))) { + continue; + } + if (app == null) { + remove(component, user); + itemsToRemove.add(c.getInt(rowIndex)); + } else { + appsToUpdate.add(app); } } + c.close(); + if (!itemsToRemove.isEmpty()) { + mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, + Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), + null); + } + + // Insert remaining apps. + if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) { + Stack appsToAdd = new Stack<>(); + appsToAdd.addAll(componentMap.values()); + new SerializedIconUpdateTask(userSerial, pkgInfoMap, + appsToAdd, appsToUpdate).scheduleNext(); + } + } + + @Thunk void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info, + long userSerial) { + // Reuse the existing entry if it already exists in the DB. This ensures that we do not + // create bitmap if it was already created during loader. + ContentValues values = updateCacheAndGetContentValues(app, false); + addIconToDB(values, app.getComponentName(), info, userSerial); + } + + /** + * Updates {@param values} to contain versoning information and adds it to the DB. + * @param values {@link ContentValues} containing icon & title + */ + private void addIconToDB(ContentValues values, ComponentName key, + PackageInfo info, long userSerial) { + values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); + values.put(IconDB.COLUMN_VERSION, info.versionCode); + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, + boolean replaceExisting) { + final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser()); + CacheEntry entry = null; + if (!replaceExisting) { + entry = mCache.get(key); + // We can't reuse the entry if the high-res icon is not present. + if (entry == null || entry.isLowResIcon || entry.icon == null) { + entry = null; + } + } + if (entry == null) { + entry = new CacheEntry(); + entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext); + } + entry.title = app.getLabel(); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser()); + mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry); + + return newContentValues(entry.icon, entry.title.toString(), mActivityBgColor); + } + + /** + * Fetches high-res icon for the provided ItemInfo and updates the caller when done. + * @return a request ID that can be used to cancel the request. + */ + public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) { + Runnable request = new Runnable() { + + @Override + public void run() { + if (info instanceof AppInfo) { + getTitleAndIcon((AppInfo) info, null, false); + } else if (info instanceof ShortcutInfo) { + ShortcutInfo st = (ShortcutInfo) info; + getTitleAndIcon(st, + st.promisedIntent != null ? st.promisedIntent : st.intent, + st.user, false); + } else if (info instanceof PackageItemInfo) { + PackageItemInfo pti = (PackageItemInfo) info; + getTitleAndIconForApp(pti.packageName, pti.user, false, pti); + } + mMainThreadExecutor.execute(new Runnable() { + + @Override + public void run() { + caller.reapplyItemInfo(info); + } + }); + } + }; + mWorkerHandler.post(request); + return new IconLoadRequest(request, mWorkerHandler); + } + + private Bitmap getNonNullIcon(CacheEntry entry, UserHandleCompat user) { + return entry.icon == null ? getDefaultIcon(user) : entry.icon; } /** * Fill in "application" with the icon and label for "info." */ - public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info, - HashMap labelCache) { - CacheEntry entry = cacheLocked(application.componentName, info, labelCache, - info.getUser(), false); - - application.title = entry.title; - application.iconBitmap = entry.icon; + public synchronized void getTitleAndIcon(AppInfo application, + LauncherActivityInfoCompat info, boolean useLowResIcon) { + UserHandleCompat user = info == null ? application.user : info.getUser(); + CacheEntry entry = cacheLocked(application.componentName, info, user, + false, useLowResIcon); + application.title = Utilities.trim(entry.title); + application.iconBitmap = getNonNullIcon(entry, user); application.contentDescription = entry.contentDescription; + application.usingLowResIcon = entry.isLowResIcon; } + /** + * Updates {@param application} only if a valid entry is found. + */ + public synchronized void updateTitleAndIcon(AppInfo application) { + CacheEntry entry = cacheLocked(application.componentName, null, application.user, + false, application.usingLowResIcon); + if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) { + application.title = Utilities.trim(entry.title); + application.iconBitmap = entry.icon; + application.contentDescription = entry.contentDescription; + application.usingLowResIcon = entry.isLowResIcon; + } + } + + /** + * Returns a high res icon for the given intent and user + */ public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) { ComponentName component = intent.getComponent(); // null info means not installed, but if we have a component from the intent then @@ -246,15 +466,16 @@ public class IconCache { } LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user); - CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, true); + CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, false /* useLowRes */); return entry.icon; } /** - * Fill in "shortcutInfo" with the icon and label for "info." + * Fill in {@param shortcutInfo} with the icon and label for {@param intent}. If the + * corresponding activity is not found, it reverts to the package icon. */ public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent, - UserHandleCompat user, boolean usePkgIcon) { + UserHandleCompat user, boolean useLowResIcon) { ComponentName component = intent.getComponent(); // null info means not installed, but if we have a component from the intent then // we should still look in the cache for restored app icons. @@ -262,17 +483,38 @@ public class IconCache { shortcutInfo.setIcon(getDefaultIcon(user)); shortcutInfo.title = ""; shortcutInfo.usingFallbackIcon = true; + shortcutInfo.usingLowResIcon = false; } else { - LauncherActivityInfoCompat launcherActInfo = - mLauncherApps.resolveActivity(intent, user); - CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon); - - shortcutInfo.setIcon(entry.icon); - shortcutInfo.title = entry.title; - shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user); + getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon); } } + /** + * Fill in {@param shortcutInfo} with the icon and label for {@param info} + */ + public synchronized void getTitleAndIcon( + ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info, + UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) { + CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon); + shortcutInfo.setIcon(getNonNullIcon(entry, user)); + shortcutInfo.title = Utilities.trim(entry.title); + shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); + shortcutInfo.usingLowResIcon = entry.isLowResIcon; + } + + /** + * Fill in {@param appInfo} with the icon and label for {@param packageName} + */ + public synchronized void getTitleAndIconForApp( + String packageName, UserHandleCompat user, boolean useLowResIcon, + PackageItemInfo infoOut) { + CacheEntry entry = getEntryForPackageLocked(packageName, user, useLowResIcon); + infoOut.iconBitmap = getNonNullIcon(entry, user); + infoOut.title = Utilities.trim(entry.title); + infoOut.usingLowResIcon = entry.isLowResIcon; + infoOut.contentDescription = entry.contentDescription; + } public synchronized Bitmap getDefaultIcon(UserHandleCompat user) { if (!mDefaultIcons.containsKey(user)) { @@ -281,16 +523,6 @@ public class IconCache { return mDefaultIcons.get(user); } - public synchronized Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info, - HashMap labelCache) { - if (info == null || component == null) { - return null; - } - - CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false); - return entry.icon; - } - public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) { return mDefaultIcons.get(user) == icon; } @@ -300,44 +532,27 @@ public class IconCache { * This method is not thread safe, it must be called from a synchronized method. */ private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info, - HashMap labelCache, UserHandleCompat user, boolean usePackageIcon) { - CacheKey cacheKey = new CacheKey(componentName, user); + UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) { + ComponentKey cacheKey = new ComponentKey(componentName, user); CacheEntry entry = mCache.get(cacheKey); - if (entry == null) { + if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { entry = new CacheEntry(); - mCache.put(cacheKey, entry); - if (info != null) { - ComponentName labelKey = info.getComponentName(); - if (labelCache != null && labelCache.containsKey(labelKey)) { - entry.title = labelCache.get(labelKey).toString(); - } else { - entry.title = info.getLabel().toString(); - if (labelCache != null) { - labelCache.put(labelKey, entry.title); - } - } - - entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); - entry.icon = Utilities.createIconBitmap( - info.getBadgedIcon(mIconDpi), mContext); - } else { - entry.title = ""; - Bitmap preloaded = getPreloadedIcon(componentName, user); - if (preloaded != null) { - if (DEBUG) Log.d(TAG, "using preloaded icon for " + - componentName.toShortString()); - entry.icon = preloaded; + // Check the DB first. + if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) { + if (info != null) { + entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext); } else { if (usePackageIcon) { - CacheEntry packageEntry = getEntryForPackage( - componentName.getPackageName(), user); + CacheEntry packageEntry = getEntryForPackageLocked( + componentName.getPackageName(), user, false); if (packageEntry != null) { if (DEBUG) Log.d(TAG, "using package default icon for " + componentName.toShortString()); entry.icon = packageEntry.icon; entry.title = packageEntry.title; + entry.contentDescription = packageEntry.contentDescription; } } if (entry.icon == null) { @@ -347,6 +562,11 @@ public class IconCache { } } } + + if (TextUtils.isEmpty(entry.title) && info != null) { + entry.title = info.getLabel(); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + } } return entry; } @@ -357,9 +577,9 @@ public class IconCache { */ public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user, Bitmap icon, CharSequence title) { - remove(packageName, user); + removeFromMemCacheLocked(packageName, user); - CacheEntry entry = getEntryForPackage(packageName, user); + CacheEntry entry = getEntryForPackageLocked(packageName, user, false); if (!TextUtils.isEmpty(title)) { entry.title = title; } @@ -371,56 +591,68 @@ public class IconCache { /** * Gets an entry for the package, which can be used as a fallback entry for various components. * This method is not thread safe, it must be called from a synchronized method. + * */ - private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) { - ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);; - CacheKey cacheKey = new CacheKey(cn, user); + private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user, + boolean useLowResIcon) { + ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); + ComponentKey cacheKey = new ComponentKey(cn, user); CacheEntry entry = mCache.get(cacheKey); - if (entry == null) { - entry = new CacheEntry(); - entry.title = ""; - mCache.put(cacheKey, entry); - try { - ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0); - entry.title = info.loadLabel(mPackageManager); - entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext); - } catch (NameNotFoundException e) { - if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { + entry = new CacheEntry(); + boolean entryUpdated = true; + + // Check the DB first. + if (!getEntryFromDB(cn, user, entry, useLowResIcon)) { + try { + PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); + ApplicationInfo appInfo = info.applicationInfo; + if (appInfo == null) { + throw new NameNotFoundException("ApplicationInfo is null"); + } + Drawable drawable = mUserManager.getBadgedDrawableForUser( + appInfo.loadIcon(mPackageManager), user); + entry.icon = Utilities.createIconBitmap(drawable, mContext); + entry.title = appInfo.loadLabel(mPackageManager); + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + entry.isLowResIcon = false; + + // Add the icon in the DB here, since these do not get written during + // package updates. + ContentValues values = + newContentValues(entry.icon, entry.title.toString(), mPackageBgColor); + addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user)); + + } catch (NameNotFoundException e) { + if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + entryUpdated = false; + } } - if (entry.icon == null) { - entry.icon = getPreloadedIcon(cn, user); + // Only add a filled-out entry to the cache + if (entryUpdated) { + mCache.put(cacheKey, entry); } } return entry; } - public synchronized HashMap getAllIcons() { - HashMap set = new HashMap(); - for (CacheKey ck : mCache.keySet()) { - final CacheEntry e = mCache.get(ck); - set.put(ck.componentName, e.icon); - } - return set; - } - /** * Pre-load an icon into the persistent cache. * *

Queries for a component that does not exist in the package manager * will be answered by the persistent cache. * - * @param context application context * @param componentName the icon should be returned for this component * @param icon the icon to be persisted * @param dpi the native density of the icon */ - public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon, - int dpi) { + public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label, + long userSerial) { // TODO rescale to the correct native DPI try { - PackageManager packageManager = context.getPackageManager(); + PackageManager packageManager = mContext.getPackageManager(); packageManager.getActivityIcon(componentName); // component is present on the system already, do nothing return; @@ -428,100 +660,208 @@ public class IconCache { // pass } - final String key = componentName.flattenToString(); - FileOutputStream resourceFile = null; + ContentValues values = newContentValues(icon, label, Color.TRANSPARENT); + values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + + private boolean getEntryFromDB(ComponentName component, UserHandleCompat user, + CacheEntry entry, boolean lowRes) { + Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, + new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, + IconDB.COLUMN_LABEL}, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {component.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(user))}, + null, null, null); try { - resourceFile = context.openFileOutput(getResourceFilename(componentName), - Context.MODE_PRIVATE); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) { - byte[] buffer = os.toByteArray(); - resourceFile.write(buffer, 0, buffer.length); - } else { - Log.w(TAG, "failed to encode cache for " + key); - return; - } - } catch (FileNotFoundException e) { - Log.w(TAG, "failed to pre-load cache for " + key, e); - } catch (IOException e) { - Log.w(TAG, "failed to pre-load cache for " + key, e); - } finally { - if (resourceFile != null) { - try { - resourceFile.close(); - } catch (IOException e) { - Log.d(TAG, "failed to save restored icon for: " + key, e); + if (c.moveToNext()) { + entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null); + entry.isLowResIcon = lowRes; + entry.title = c.getString(1); + if (entry.title == null) { + entry.title = ""; + entry.contentDescription = ""; + } else { + entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); } + return true; } + } finally { + c.close(); + } + return false; + } + + public static class IconLoadRequest { + private final Runnable mRunnable; + private final Handler mHandler; + + IconLoadRequest(Runnable runnable, Handler handler) { + mRunnable = runnable; + mHandler = handler; + } + + public void cancel() { + mHandler.removeCallbacks(mRunnable); } } /** - * Read a pre-loaded icon from the persistent icon cache. - * - * @param componentName the component that should own the icon - * @returns a bitmap if one is cached, or null. + * A runnable that updates invalid icons and adds missing icons in the DB for the provided + * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the + * worker thread doesn't get blocked. */ - private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) { - final String key = componentName.flattenToShortString(); + @Thunk class SerializedIconUpdateTask implements Runnable { + private final long mUserSerial; + private final HashMap mPkgInfoMap; + private final Stack mAppsToAdd; + private final Stack mAppsToUpdate; + private final HashSet mUpdatedPackages = new HashSet(); - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle())) { + @Thunk SerializedIconUpdateTask(long userSerial, HashMap pkgInfoMap, + Stack appsToAdd, + Stack appsToUpdate) { + mUserSerial = userSerial; + mPkgInfoMap = pkgInfoMap; + mAppsToAdd = appsToAdd; + mAppsToUpdate = appsToUpdate; + } + + @Override + public void run() { + if (!mAppsToUpdate.isEmpty()) { + LauncherActivityInfoCompat app = mAppsToUpdate.pop(); + String cn = app.getComponentName().flattenToString(); + ContentValues values = updateCacheAndGetContentValues(app, true); + mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[] {cn, Long.toString(mUserSerial)}); + mUpdatedPackages.add(app.getComponentName().getPackageName()); + + if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) { + // No more app to update. Notify model. + LauncherAppState.getInstance().getModel().onPackageIconsUpdated( + mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial)); + } + + // Let it run one more time. + scheduleNext(); + } else if (!mAppsToAdd.isEmpty()) { + LauncherActivityInfoCompat app = mAppsToAdd.pop(); + PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName()); + if (info != null) { + synchronized (IconCache.this) { + addIconToDBAndMemCache(app, info, mUserSerial); + } + } + + if (!mAppsToAdd.isEmpty()) { + scheduleNext(); + } + } + } + + public void scheduleNext() { + mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, SystemClock.uptimeMillis() + 1); + } + } + + private void updateSystemStateString() { + mSystemState = Locale.getDefault().toString(); + } + + private static final class IconDB extends SQLiteOpenHelper { + private final static int DB_VERSION = 6; + + private final static String TABLE_NAME = "icons"; + private final static String COLUMN_ROWID = "rowid"; + private final static String COLUMN_COMPONENT = "componentName"; + private final static String COLUMN_USER = "profileId"; + private final static String COLUMN_LAST_UPDATED = "lastUpdated"; + private final static String COLUMN_VERSION = "version"; + private final static String COLUMN_ICON = "icon"; + private final static String COLUMN_ICON_LOW_RES = "icon_low_res"; + private final static String COLUMN_LABEL = "label"; + private final static String COLUMN_SYSTEM_STATE = "system_state"; + + public IconDB(Context context) { + super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + + COLUMN_COMPONENT + " TEXT NOT NULL, " + + COLUMN_USER + " INTEGER NOT NULL, " + + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_ICON + " BLOB, " + + COLUMN_ICON_LOW_RES + " BLOB, " + + COLUMN_LABEL + " TEXT, " + + COLUMN_SYSTEM_STATE + " TEXT, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } + + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } + } + + private ContentValues newContentValues(Bitmap icon, String label, int lowResBackgroundColor) { + ContentValues values = new ContentValues(); + values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon)); + + values.put(IconDB.COLUMN_LABEL, label); + values.put(IconDB.COLUMN_SYSTEM_STATE, mSystemState); + + if (lowResBackgroundColor == Color.TRANSPARENT) { + values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap( + Bitmap.createScaledBitmap(icon, + icon.getWidth() / LOW_RES_SCALE_FACTOR, + icon.getHeight() / LOW_RES_SCALE_FACTOR, true))); + } else { + synchronized (this) { + if (mLowResBitmap == null) { + mLowResBitmap = Bitmap.createBitmap(icon.getWidth() / LOW_RES_SCALE_FACTOR, + icon.getHeight() / LOW_RES_SCALE_FACTOR, Bitmap.Config.RGB_565); + mLowResCanvas = new Canvas(mLowResBitmap); + mLowResPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); + } + mLowResCanvas.drawColor(lowResBackgroundColor); + mLowResCanvas.drawBitmap(icon, new Rect(0, 0, icon.getWidth(), icon.getHeight()), + new Rect(0, 0, mLowResBitmap.getWidth(), mLowResBitmap.getHeight()), + mLowResPaint); + values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(mLowResBitmap)); + } + } + return values; + } + + private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) { + byte[] data = c.getBlob(iconIndex); + try { + return BitmapFactory.decodeByteArray(data, 0, data.length, options); + } catch (Exception e) { return null; } - - if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key); - Bitmap icon = null; - FileInputStream resourceFile = null; - try { - resourceFile = mContext.openFileInput(getResourceFilename(componentName)); - byte[] buffer = new byte[1024]; - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int bytesRead = 0; - while(bytesRead >= 0) { - bytes.write(buffer, 0, bytesRead); - bytesRead = resourceFile.read(buffer, 0, buffer.length); - } - if (DEBUG) Log.d(TAG, "read " + bytes.size()); - icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size()); - if (icon == null) { - Log.w(TAG, "failed to decode pre-load icon for " + key); - } - } catch (FileNotFoundException e) { - if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key); - } catch (IOException e) { - Log.w(TAG, "failed to read pre-load icon for: " + key, e); - } finally { - if(resourceFile != null) { - try { - resourceFile.close(); - } catch (IOException e) { - Log.d(TAG, "failed to manage pre-load icon file: " + key, e); - } - } - } - - return icon; - } - - /** - * Remove a pre-loaded icon from the persistent icon cache. - * - * @param componentName the component that should own the icon - */ - public void deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) { - // We don't keep icons for other profiles in persistent cache. - if (!user.equals(UserHandleCompat.myUserHandle()) || componentName == null) { - return; - } - remove(componentName, user); - boolean success = mContext.deleteFile(getResourceFilename(componentName)); - if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache"); - } - - private static String getResourceFilename(ComponentName component) { - String resourceName = component.flattenToShortString(); - String filename = resourceName.replace(File.separatorChar, '_'); - return RESOURCE_FILE_PREFIX + filename; } } diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java index 7e55af2286..d93cdcc1ba 100644 --- a/src/com/android/launcher3/InfoDropTarget.java +++ b/src/com/android/launcher3/InfoDropTarget.java @@ -18,21 +18,12 @@ package com.android.launcher3; import android.content.ComponentName; import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.drawable.TransitionDrawable; import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; import com.android.launcher3.compat.UserHandleCompat; public class InfoDropTarget extends ButtonDropTarget { - private ColorStateList mOriginalTextColor; - private TransitionDrawable mDrawable; - public InfoDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -44,96 +35,44 @@ public class InfoDropTarget extends ButtonDropTarget { @Override protected void onFinishInflate() { super.onFinishInflate(); - - mOriginalTextColor = getTextColors(); - // Get the hover color - Resources r = getResources(); - mHoverColor = r.getColor(R.color.info_target_hover_tint); - mDrawable = (TransitionDrawable) getCurrentDrawable(); + mHoverColor = getResources().getColor(R.color.info_target_hover_tint); - if (mDrawable == null) { - // TODO: investigate why this is ever happening. Presently only on one known device. - mDrawable = (TransitionDrawable) r.getDrawable(R.drawable.info_target_selector); - setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null); - } - - if (null != mDrawable) { - mDrawable.setCrossFadeEnabled(true); - } - - // Remove the text in the Phone UI in landscape - int orientation = getResources().getConfiguration().orientation; - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - if (!LauncherAppState.getInstance().isScreenLarge()) { - setText(""); - } - } + setDrawable(R.drawable.ic_info_launcher); } - @Override - public boolean acceptDrop(DragObject d) { - // acceptDrop is called just before onDrop. We do the work here, rather than - // in onDrop, because it allows us to reject the drop (by returning false) - // so that the object being dragged isn't removed from the drag source. + public static void startDetailsActivityForInfo(Object info, Launcher launcher) { ComponentName componentName = null; - if (d.dragInfo instanceof AppInfo) { - componentName = ((AppInfo) d.dragInfo).componentName; - } else if (d.dragInfo instanceof ShortcutInfo) { - componentName = ((ShortcutInfo) d.dragInfo).intent.getComponent(); - } else if (d.dragInfo instanceof PendingAddItemInfo) { - componentName = ((PendingAddItemInfo) d.dragInfo).componentName; + if (info instanceof AppInfo) { + componentName = ((AppInfo) info).componentName; + } else if (info instanceof ShortcutInfo) { + componentName = ((ShortcutInfo) info).intent.getComponent(); + } else if (info instanceof PendingAddItemInfo) { + componentName = ((PendingAddItemInfo) info).componentName; } final UserHandleCompat user; - if (d.dragInfo instanceof ItemInfo) { - user = ((ItemInfo) d.dragInfo).user; + if (info instanceof ItemInfo) { + user = ((ItemInfo) info).user; } else { user = UserHandleCompat.myUserHandle(); } if (componentName != null) { - mLauncher.startApplicationDetailsActivity(componentName, user); + launcher.startApplicationDetailsActivity(componentName, user); } - - // There is no post-drop animation, so clean up the DragView now - d.deferDragViewCleanupPostAnimation = false; - return false; } @Override - public void onDragStart(DragSource source, Object info, int dragAction) { - boolean isVisible = true; + protected boolean supportsDrop(DragSource source, Object info) { + return source.supportsAppInfoDropTarget() && supportsDrop(getContext(), info); + } - // Hide this button unless we are dragging something from AllApps - if (!source.supportsAppInfoDropTarget()) { - isVisible = false; - } - - mActive = isVisible; - mDrawable.resetTransition(); - setTextColor(mOriginalTextColor); - ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); + public static boolean supportsDrop(Context context, Object info) { + return info instanceof AppInfo || info instanceof PendingAddItemInfo; } @Override - public void onDragEnd() { - super.onDragEnd(); - mActive = false; - } - - public void onDragEnter(DragObject d) { - super.onDragEnter(d); - - mDrawable.startTransition(mTransitionDuration); - setTextColor(mHoverColor); - } - - public void onDragExit(DragObject d) { - super.onDragExit(d); - - if (!d.dragComplete) { - mDrawable.resetTransition(); - setTextColor(mOriginalTextColor); - } + void completeDrop(DragObject d) { + startDetailsActivityForInfo(d.dragInfo, mLauncher); } } diff --git a/src/com/android/launcher3/Insettable.java b/src/com/android/launcher3/Insettable.java index 1d2356c652..3b8ef2f931 100644 --- a/src/com/android/launcher3/Insettable.java +++ b/src/com/android/launcher3/Insettable.java @@ -18,6 +18,10 @@ package com.android.launcher3; import android.graphics.Rect; +/** + * Allows the implementing {@link View} to not draw underneath system bars. + * e.g., notification bar on top and home key area on the bottom. + */ public interface Insettable { void setInsets(Rect insets); diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 1ab308558b..3fc8fc0a98 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.text.TextUtils; @@ -32,6 +33,7 @@ import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.Thunk; import org.json.JSONException; import org.json.JSONObject; @@ -144,12 +146,26 @@ public class InstallShortcutReceiver extends BroadcastReceiver { return; } - if (DBG) Log.d(TAG, "Got INSTALL_SHORTCUT: " + data.toUri(0)); PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); + if (info.launchIntent == null || info.label == null) { + if (DBG) Log.e(TAG, "Invalid install shortcut intent"); + return; + } + info = convertToLauncherActivityIfPossible(info); queuePendingShortcutInfo(info, context); } + public static ShortcutInfo fromShortcutIntent(Context context, Intent data) { + PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context); + if (info.launchIntent == null || info.label == null) { + if (DBG) Log.e(TAG, "Invalid install shortcut intent"); + return null; + } + info = convertToLauncherActivityIfPossible(info); + return info.getShortcutInfo(); + } + static void queueInstallShortcut(LauncherActivityInfoCompat info, Context context) { queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context); } @@ -186,11 +202,6 @@ public class InstallShortcutReceiver extends BroadcastReceiver { final PendingInstallShortcutInfo pendingInfo = iter.next(); final Intent intent = pendingInfo.launchIntent; - if (LauncherAppState.isDisableAllApps() && !isValidShortcutLaunchIntent(intent)) { - if (DBG) Log.d(TAG, "Ignoring shortcut with launchIntent:" + intent); - continue; - } - // If the intent specifies a package, make sure the package exists String packageName = pendingInfo.getTargetPackage(); if (!TextUtils.isEmpty(packageName)) { @@ -201,56 +212,28 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } } - final boolean exists = LauncherModel.shortcutExists(context, pendingInfo.label, - intent, pendingInfo.user); - if (!exists) { - // Generate a shortcut info to add into the model - addShortcuts.add(pendingInfo.getShortcutInfo()); - } + // Generate a shortcut info to add into the model + addShortcuts.add(pendingInfo.getShortcutInfo()); } // Add the new apps to the model and bind them if (!addShortcuts.isEmpty()) { LauncherAppState app = LauncherAppState.getInstance(); - app.getModel().addAndBindAddedWorkspaceApps(context, addShortcuts); + app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts); } } } - /** - * Returns true if the intent is a valid launch intent for a shortcut. - * This is used to identify shortcuts which are different from the ones exposed by the - * applications' manifest file. - * - * When DISABLE_ALL_APPS is true, shortcuts exposed via the app's manifest should never be - * duplicated or removed(unless the app is un-installed). - * - * @param launchIntent The intent that will be launched when the shortcut is clicked. - */ - static boolean isValidShortcutLaunchIntent(Intent launchIntent) { - if (launchIntent != null - && Intent.ACTION_MAIN.equals(launchIntent.getAction()) - && launchIntent.getComponent() != null - && launchIntent.getCategories() != null - && launchIntent.getCategories().size() == 1 - && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER) - && launchIntent.getExtras() == null - && TextUtils.isEmpty(launchIntent.getDataString())) { - return false; - } - return true; - } - /** * Ensures that we have a valid, non-null name. If the provided name is null, we will return * the application name instead. */ - private static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) { + @Thunk static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) { if (name == null) { try { PackageManager pm = context.getPackageManager(); ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); - name = info.loadLabel(pm).toString(); + name = info.loadLabel(pm); } catch (PackageManager.NameNotFoundException nnfe) { return ""; } @@ -335,7 +318,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0)) .key(NAME_KEY).value(name); if (icon != null) { - byte[] iconByteArray = ItemInfo.flattenBitmap(icon); + byte[] iconByteArray = Utilities.flattenBitmap(icon); json = json.key(ICON_KEY).value( Base64.encodeToString( iconByteArray, 0, iconByteArray.length, Base64.DEFAULT)); @@ -354,16 +337,7 @@ public class InstallShortcutReceiver extends BroadcastReceiver { public ShortcutInfo getShortcutInfo() { if (activityInfo != null) { - final ShortcutInfo info = new ShortcutInfo(); - info.user = user; - info.title = label; - info.contentDescription = label; - info.customIcon = false; - info.intent = launchIntent; - info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; - info.flags = AppInfo.initFlags(activityInfo); - info.firstInstallTime = activityInfo.getFirstInstallTime(); - return info; + return ShortcutInfo.fromActivityInfo(activityInfo, mContext); } else { return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data); } @@ -377,6 +351,10 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } return packageName; } + + public boolean isLuncherActivity() { + return activityInfo != null; + } } private static PendingInstallShortcutInfo decode(String encoded, Context context) { @@ -424,4 +402,34 @@ public class InstallShortcutReceiver extends BroadcastReceiver { } return null; } + + /** + * Tries to create a new PendingInstallShortcutInfo which represents the same target, + * but is an app target and not a shortcut. + * @return the newly created info or the original one. + */ + private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible( + PendingInstallShortcutInfo original) { + if (original.isLuncherActivity()) { + // Already an activity target + return original; + } + if (!Utilities.isLauncherAppTarget(original.launchIntent) + || !original.user.equals(UserHandleCompat.myUserHandle())) { + // We can only convert shortcuts which point to a main activity in the current user. + return original; + } + + PackageManager pm = original.mContext.getPackageManager(); + ResolveInfo info = pm.resolveActivity(original.launchIntent, 0); + + if (info == null) { + return original; + } + + // Ignore any conflicts in the label name, as that can change based on locale. + LauncherActivityInfoCompat launcherInfo = LauncherActivityInfoCompat + .fromResolveInfo(info, original.mContext); + return new PendingInstallShortcutInfo(launcherInfo, original.mContext); + } } diff --git a/src/com/android/launcher3/InterruptibleInOutAnimator.java b/src/com/android/launcher3/InterruptibleInOutAnimator.java index 2898b347dd..29df38bae1 100644 --- a/src/com/android/launcher3/InterruptibleInOutAnimator.java +++ b/src/com/android/launcher3/InterruptibleInOutAnimator.java @@ -21,6 +21,8 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.view.View; +import com.android.launcher3.util.Thunk; + /** * A convenience class for two-way animations, e.g. a fadeIn/fadeOut animation. * With a regular ValueAnimator, if you call reverse to show the 'out' animation, you'll get @@ -43,7 +45,7 @@ public class InterruptibleInOutAnimator { private static final int OUT = 2; // TODO: This isn't really necessary, but is here to help diagnose a bug in the drag viz - private int mDirection = STOPPED; + @Thunk int mDirection = STOPPED; public InterruptibleInOutAnimator(View view, long duration, float fromValue, float toValue) { mAnimator = LauncherAnimUtils.ofFloat(view, fromValue, toValue).setDuration(duration); diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java new file mode 100644 index 0000000000..ae204c40c3 --- /dev/null +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.WindowManager; + +import com.android.launcher3.util.Thunk; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +public class InvariantDeviceProfile { + + // This is a static that we use for the default icon size on a 4/5-inch phone + private static float DEFAULT_ICON_SIZE_DP = 60; + + private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48; + + // Constants that affects the interpolation curve between statically defined device profile + // buckets. + private static float KNEARESTNEIGHBOR = 3; + private static float WEIGHT_POWER = 5; + + // used to offset float not being able to express extremely small weights in extreme cases. + private static float WEIGHT_EFFICIENT = 100000f; + + // Profile-defining invariant properties + String name; + float minWidthDps; + float minHeightDps; + + /** + * Number of icons per row and column in the workspace. + */ + public int numRows; + public int numColumns; + + /** + * The minimum number of predicted apps in all apps. + */ + int minAllAppsPredictionColumns; + + /** + * Number of icons per row and column in the folder. + */ + public int numFolderRows; + public int numFolderColumns; + float iconSize; + int iconBitmapSize; + int fillResIconDpi; + float iconTextSize; + + /** + * Number of icons inside the hotseat area. + */ + float numHotseatIcons; + float hotseatIconSize; + int defaultLayoutId; + + // Derived invariant properties + int hotseatAllAppsRank; + + DeviceProfile landscapeProfile; + DeviceProfile portraitProfile; + + InvariantDeviceProfile() { + } + + public InvariantDeviceProfile(InvariantDeviceProfile p) { + this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, + p.numFolderRows, p.numFolderColumns, p.minAllAppsPredictionColumns, + p.iconSize, p.iconTextSize, p.numHotseatIcons, p.hotseatIconSize, + p.defaultLayoutId); + } + + InvariantDeviceProfile(String n, float w, float h, int r, int c, int fr, int fc, int maapc, + float is, float its, float hs, float his, int dlId) { + // Ensure that we have an odd number of hotseat items (since we need to place all apps) + if (hs % 2 == 0) { + throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); + } + + name = n; + minWidthDps = w; + minHeightDps = h; + numRows = r; + numColumns = c; + numFolderRows = fr; + numFolderColumns = fc; + minAllAppsPredictionColumns = maapc; + iconSize = is; + iconTextSize = its; + numHotseatIcons = hs; + hotseatIconSize = his; + defaultLayoutId = dlId; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + InvariantDeviceProfile(Context context) { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + DisplayMetrics dm = new DisplayMetrics(); + display.getMetrics(dm); + + Point smallestSize = new Point(); + Point largestSize = new Point(); + display.getCurrentSizeRange(smallestSize, largestSize); + + // This guarantees that width < height + minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm); + minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); + + ArrayList closestProfiles = + findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles()); + InvariantDeviceProfile interpolatedDeviceProfileOut = + invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles); + + InvariantDeviceProfile closestProfile = closestProfiles.get(0); + numRows = closestProfile.numRows; + numColumns = closestProfile.numColumns; + numHotseatIcons = closestProfile.numHotseatIcons; + hotseatAllAppsRank = (int) (numHotseatIcons / 2); + defaultLayoutId = closestProfile.defaultLayoutId; + numFolderRows = closestProfile.numFolderRows; + numFolderColumns = closestProfile.numFolderColumns; + minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns; + + iconSize = interpolatedDeviceProfileOut.iconSize; + iconBitmapSize = Utilities.pxFromDp(iconSize, dm); + iconTextSize = interpolatedDeviceProfileOut.iconTextSize; + hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize; + fillResIconDpi = getLauncherIconDensity(iconBitmapSize); + + // If the partner customization apk contains any grid overrides, apply them + // Supported overrides: numRows, numColumns, iconSize + applyPartnerDeviceProfileOverrides(context, dm); + + Point realSize = new Point(); + display.getRealSize(realSize); + // The real size never changes. smallSide and largeSide will remain the + // same in any orientation. + int smallSide = Math.min(realSize.x, realSize.y); + int largeSide = Math.max(realSize.x, realSize.y); + + landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize, + largeSide, smallSide, true /* isLandscape */); + portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize, + smallSide, largeSide, false /* isLandscape */); + } + + ArrayList getPredefinedDeviceProfiles() { + ArrayList predefinedDeviceProfiles = new ArrayList<>(); + // width, height, #rows, #columns, #folder rows, #folder columns, + // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId. + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby", + 255, 300, 2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby", + 255, 400, 3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby", + 275, 420, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby", + 255, 450, 3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S", + 296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4", + 335, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5", + 359, 567, 4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone", + 406, 694, 5, 5, 4, 4, 4, 64, 14.4f, 5, 56, R.xml.default_workspace_5x5)); + // The tablet profile is odd in that the landscape orientation + // also includes the nav bar on the side + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7", + 575, 904, 5, 6, 4, 5, 4, 72, 14.4f, 7, 60, R.xml.default_workspace_5x6)); + // Larger tablet profiles always have system bars on the top & bottom + predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10", + 727, 1207, 5, 6, 4, 5, 4, 76, 14.4f, 7, 64, R.xml.default_workspace_5x6)); + predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet", + 1527, 2527, 7, 7, 6, 6, 4, 100, 20, 7, 72, R.xml.default_workspace_4x4)); + return predefinedDeviceProfiles; + } + + private int getLauncherIconDensity(int requiredSize) { + // Densities typically defined by an app. + int[] densityBuckets = new int[] { + DisplayMetrics.DENSITY_LOW, + DisplayMetrics.DENSITY_MEDIUM, + DisplayMetrics.DENSITY_TV, + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_XHIGH, + DisplayMetrics.DENSITY_XXHIGH, + DisplayMetrics.DENSITY_XXXHIGH + }; + + int density = DisplayMetrics.DENSITY_XXXHIGH; + for (int i = densityBuckets.length - 1; i >= 0; i--) { + float expectedSize = ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i] + / DisplayMetrics.DENSITY_DEFAULT; + if (expectedSize >= requiredSize) { + density = densityBuckets[i]; + } + } + + return density; + } + + /** + * Apply any Partner customization grid overrides. + * + * Currently we support: all apps row / column count. + */ + private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { + Partner p = Partner.get(context.getPackageManager()); + if (p != null) { + p.applyInvariantDeviceProfileOverrides(this, dm); + } + } + + @Thunk float dist(float x0, float y0, float x1, float y1) { + return (float) Math.hypot(x1 - x0, y1 - y0); + } + + /** + * Returns the closest device profiles ordered by closeness to the specified width and height + */ + // Package private visibility for testing. + ArrayList findClosestDeviceProfiles( + final float width, final float height, ArrayList points) { + + // Sort the profiles by their closeness to the dimensions + ArrayList pointsByNearness = points; + Collections.sort(pointsByNearness, new Comparator() { + public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) { + return (int) (dist(width, height, a.minWidthDps, a.minHeightDps) + - dist(width, height, b.minWidthDps, b.minHeightDps)); + } + }); + + return pointsByNearness; + } + + // Package private visibility for testing. + InvariantDeviceProfile invDistWeightedInterpolate(float width, float height, + ArrayList points) { + float weights = 0; + + InvariantDeviceProfile p = points.get(0); + if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) { + return p; + } + + InvariantDeviceProfile out = new InvariantDeviceProfile(); + for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) { + p = new InvariantDeviceProfile(points.get(i)); + float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER); + weights += w; + out.add(p.multiply(w)); + } + return out.multiply(1.0f/weights); + } + + private void add(InvariantDeviceProfile p) { + iconSize += p.iconSize; + iconTextSize += p.iconTextSize; + hotseatIconSize += p.hotseatIconSize; + } + + private InvariantDeviceProfile multiply(float w) { + iconSize *= w; + iconTextSize *= w; + hotseatIconSize *= w; + return this; + } + + private float weight(float x0, float y0, float x1, float y1, float pow) { + float d = dist(x0, y0, x1, y1); + if (Float.compare(d, 0f) == 0) { + return Float.POSITIVE_INFINITY; + } + return (float) (WEIGHT_EFFICIENT / Math.pow(d, pow)); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java index 09b77f7561..f7e0ea488e 100644 --- a/src/com/android/launcher3/ItemInfo.java +++ b/src/com/android/launcher3/ItemInfo.java @@ -20,13 +20,10 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.util.Log; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.util.Arrays; /** @@ -39,7 +36,7 @@ public class ItemInfo { */ static final String EXTRA_PROFILE = "profile"; - static final int NO_ID = -1; + public static final int NO_ID = -1; /** * The id in the settings database for this item @@ -85,7 +82,7 @@ public class ItemInfo { /** * Indicates the Y cell span. */ - int spanY = 1; + public int spanY = 1; /** * Indicates the minimum X cell span. @@ -97,6 +94,11 @@ public class ItemInfo { */ public int minSpanY = 1; + /** + * Indicates the position in an ordered list. + */ + public int rank = 0; + /** * Indicates that this item needs to be updated in the db */ @@ -105,21 +107,21 @@ public class ItemInfo { /** * Title of the item */ - CharSequence title; + public CharSequence title; /** * Content description of the item. */ - CharSequence contentDescription; + public CharSequence contentDescription; /** * The position of the item in a drag-and-drop operation. */ - int[] dropPos = null; + public int[] dropPos = null; - UserHandleCompat user; + public UserHandleCompat user; - ItemInfo() { + public ItemInfo() { user = UserHandleCompat.myUserHandle(); } @@ -135,6 +137,7 @@ public class ItemInfo { cellY = info.cellY; spanX = info.spanX; spanY = info.spanY; + rank = info.rank; screenId = info.screenId; itemType = info.itemType; container = info.container; @@ -161,6 +164,7 @@ public class ItemInfo { values.put(LauncherSettings.Favorites.CELLY, cellY); values.put(LauncherSettings.Favorites.SPANX, spanX); values.put(LauncherSettings.Favorites.SPANY, spanY); + values.put(LauncherSettings.Favorites.RANK, rank); long serialNumber = UserManagerCompat.getInstance(context).getSerialNumberForUser(user); values.put(LauncherSettings.Favorites.PROFILE_ID, serialNumber); @@ -170,30 +174,9 @@ public class ItemInfo { } } - void updateValuesWithCoordinates(ContentValues values, int cellX, int cellY) { - values.put(LauncherSettings.Favorites.CELLX, cellX); - values.put(LauncherSettings.Favorites.CELLY, cellY); - } - - static byte[] flattenBitmap(Bitmap bitmap) { - // Try go guesstimate how much space the icon will take when serialized - // to avoid unnecessary allocations/copies during the write. - int size = bitmap.getWidth() * bitmap.getHeight() * 4; - ByteArrayOutputStream out = new ByteArrayOutputStream(size); - try { - bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); - out.flush(); - out.close(); - return out.toByteArray(); - } catch (IOException e) { - Log.w("Favorite", "Could not write icon"); - return null; - } - } - static void writeBitmap(ContentValues values, Bitmap bitmap) { if (bitmap != null) { - byte[] data = flattenBitmap(bitmap); + byte[] data = Utilities.flattenBitmap(bitmap); values.put(LauncherSettings.Favorites.ICON, data); } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index ac46fd33d7..6648b6e74d 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -1,4 +1,3 @@ - /* * Copyright (C) 2008 The Android Open Source Project * @@ -22,8 +21,8 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; -import android.animation.TimeInterpolator; import android.animation.ValueAnimator; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; @@ -37,25 +36,24 @@ import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentCallbacks2; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.ContentObserver; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; @@ -66,6 +64,7 @@ import android.os.Handler; import android.os.Message; import android.os.StrictMode; import android.os.SystemClock; +import android.os.UserHandle; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -82,40 +81,41 @@ import android.view.Surface; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; -import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.ViewStub; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; +import android.view.animation.OvershootInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Advanceable; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.TextView; import android.widget.Toast; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.PagedView.PageSwitchListener; +import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.LauncherAppsCompat; -import com.android.launcher3.compat.PackageInstallerCompat; -import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.PendingAddWidgetInfo; +import com.android.launcher3.widget.WidgetHostViewLoader; +import com.android.launcher3.widget.WidgetsContainerView; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.File; import java.io.FileDescriptor; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.DateFormat; @@ -132,12 +132,13 @@ import java.util.concurrent.atomic.AtomicInteger; */ public class Launcher extends Activity implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, - View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener { + View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener, + LauncherStateTransitionAnimation.Callbacks { static final String TAG = "Launcher"; static final boolean LOGD = false; static final boolean PROFILE_STARTUP = false; - static final boolean DEBUG_WIDGETS = false; + static final boolean DEBUG_WIDGETS = true; static final boolean DEBUG_STRICT_MODE = false; static final boolean DEBUG_RESUME_TIME = false; static final boolean DEBUG_DUMP_LOG = false; @@ -146,28 +147,29 @@ public class Launcher extends Activity private static final int REQUEST_CREATE_SHORTCUT = 1; private static final int REQUEST_CREATE_APPWIDGET = 5; - private static final int REQUEST_PICK_SHORTCUT = 7; private static final int REQUEST_PICK_APPWIDGET = 9; private static final int REQUEST_PICK_WALLPAPER = 10; private static final int REQUEST_BIND_APPWIDGET = 11; private static final int REQUEST_RECONFIGURE_APPWIDGET = 12; + private static final int WORKSPACE_BACKGROUND_GRADIENT = 0; + private static final int WORKSPACE_BACKGROUND_TRANSPARENT = 1; + private static final int WORKSPACE_BACKGROUND_BLACK = 2; + + private static final float BOUNCE_ANIMATION_TENSION = 1.3f; + /** * IntentStarter uses request codes starting with this. This must be greater than all activity * request codes used internally. */ protected static final int REQUEST_LAST = 100; - static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate"; - static final int SCREEN_COUNT = 5; - static final int DEFAULT_SCREEN = 2; // To turn on these properties, type // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] static final String DUMP_STATE_PROPERTY = "launcher_dump_state"; - static final String DISABLE_ALL_APPS_PROPERTY = "launcher_noallapps"; // The Intent extra that defines whether to ignore the launch animation static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = @@ -185,10 +187,6 @@ public class Launcher extends Activity private static final String RUNTIME_STATE_PENDING_ADD_CELL_X = "launcher.add_cell_x"; // Type: int private static final String RUNTIME_STATE_PENDING_ADD_CELL_Y = "launcher.add_cell_y"; - // Type: boolean - private static final String RUNTIME_STATE_PENDING_FOLDER_RENAME = "launcher.rename_folder"; - // Type: long - private static final String RUNTIME_STATE_PENDING_FOLDER_RENAME_ID = "launcher.rename_folder_id"; // Type: int private static final String RUNTIME_STATE_PENDING_ADD_SPAN_X = "launcher.add_span_x"; // Type: int @@ -216,9 +214,10 @@ public class Launcher extends Activity public static final String USER_HAS_MIGRATED = "launcher.user_migrated_from_old_data"; /** The different states that Launcher can be in. */ - private enum State { NONE, WORKSPACE, APPS_CUSTOMIZE, APPS_CUSTOMIZE_SPRING_LOADED }; - private State mState = State.WORKSPACE; - private AnimatorSet mStateAnimation; + enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED } + + @Thunk State mState = State.WORKSPACE; + @Thunk LauncherStateTransitionAnimation mStateTransitionAnimation; private boolean mIsSafeModeEnabled; @@ -231,50 +230,50 @@ public class Launcher extends Activity private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500; private static final int ACTIVITY_START_DELAY = 1000; - private static final Object sLock = new Object(); - private static int sScreen = DEFAULT_SCREEN; - private HashMap mItemIdToViewId = new HashMap(); private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1); // How long to wait before the new-shortcut animation automatically pans the workspace private static int NEW_APPS_PAGE_MOVE_DELAY = 500; private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5; - private static int NEW_APPS_ANIMATION_DELAY = 500; - private static final int SINGLE_FRAME_DELAY = 16; + @Thunk static int NEW_APPS_ANIMATION_DELAY = 500; private final BroadcastReceiver mCloseSystemDialogsReceiver = new CloseSystemDialogsIntentReceiver(); - private final ContentObserver mWidgetObserver = new AppWidgetResetObserver(); private LayoutInflater mInflater; - private Workspace mWorkspace; + @Thunk Workspace mWorkspace; private View mLauncherView; private View mPageIndicators; - private DragLayer mDragLayer; + @Thunk DragLayer mDragLayer; private DragController mDragController; private View mWeightWatcher; private AppWidgetManagerCompat mAppWidgetManager; private LauncherAppWidgetHost mAppWidgetHost; - private ItemInfo mPendingAddInfo = new ItemInfo(); - private AppWidgetProviderInfo mPendingAddWidgetInfo; + @Thunk ItemInfo mPendingAddInfo = new ItemInfo(); + private LauncherAppWidgetProviderInfo mPendingAddWidgetInfo; private int mPendingAddWidgetId = -1; private int[] mTmpAddItemCellCoordinates = new int[2]; - private FolderInfo mFolderInfo; - - private Hotseat mHotseat; + @Thunk Hotseat mHotseat; private ViewGroup mOverviewPanel; private View mAllAppsButton; + private View mWidgetsButton; private SearchDropTargetBar mSearchDropTargetBar; - private AppsCustomizeTabHost mAppsCustomizeTabHost; - private AppsCustomizePagedView mAppsCustomizeContent; + + // Main container view for the all apps screen. + @Thunk AllAppsContainerView mAppsView; + + // Main container view and the model for the widget tray screen. + @Thunk WidgetsContainerView mWidgetsView; + @Thunk WidgetsModel mWidgetsModel; + private boolean mAutoAdvanceRunning = false; private AppWidgetHostView mQsb; @@ -286,7 +285,7 @@ public class Launcher extends Activity private SpannableStringBuilder mDefaultKeySsb = null; - private boolean mWorkspaceLoading = true; + @Thunk boolean mWorkspaceLoading = true; private boolean mPaused = true; private boolean mRestoring; @@ -300,14 +299,12 @@ public class Launcher extends Activity private LauncherModel mModel; private IconCache mIconCache; - private boolean mUserPresent = true; + @Thunk boolean mUserPresent = true; private boolean mVisible = false; private boolean mHasFocus = false; private boolean mAttached = false; - private static LocaleConfiguration sLocaleConfiguration = null; - - private static HashMap sFolders = new HashMap(); + private static LongArrayMap sFolders = new LongArrayMap<>(); private View.OnTouchListener mHapticFeedbackTouchListener; @@ -317,14 +314,14 @@ public class Launcher extends Activity private final int mAdvanceStagger = 250; private long mAutoAdvanceSentTime; private long mAutoAdvanceTimeLeft = -1; - private HashMap mWidgetsToAdvance = + @Thunk HashMap mWidgetsToAdvance = new HashMap(); // Determines how long to wait after a rotation before restoring the screen orientation to // match the sensor state. private final int mRestoreScreenOrientationDelay = 500; - private Drawable mWorkspaceBackgroundDrawable; + @Thunk Drawable mWorkspaceBackgroundDrawable; private final ArrayList mSynchronouslyBoundPages = new ArrayList(); private static final boolean DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE = false; @@ -340,18 +337,43 @@ public class Launcher extends Activity // it from the context. private SharedPreferences mSharedPrefs; - private static ArrayList mIntentsOnWorkspaceFromUpgradePath = null; - // Holds the page that we need to animate to, and the icon views that we need to animate up // when we scroll to that page on resume. - private ImageView mFolderIconImageView; + @Thunk ImageView mFolderIconImageView; private Bitmap mFolderIconBitmap; private Canvas mFolderIconCanvas; private Rect mRectForFolderAnimation = new Rect(); + private DeviceProfile mDeviceProfile; + + // This is set to the view that launched the activity that navigated the user away from + // launcher. Since there is no callback for when the activity has finished launching, enable + // the press state and keep this reference to reset the press state when we return to launcher. private BubbleTextView mWaitingForResume; - private Runnable mBuildLayersRunnable = new Runnable() { + protected static HashMap sCustomAppWidgets = + new HashMap(); + + private static final boolean ENABLE_CUSTOM_WIDGET_TEST = false; + static { + if (ENABLE_CUSTOM_WIDGET_TEST) { + sCustomAppWidgets.put(DummyWidget.class.getName(), new DummyWidget()); + } + } + + // TODO: remove this field and call method directly when Launcher3 can depend on M APIs + private static Method sClipRevealMethod = null; + static { + Class activityOptionsClass = ActivityOptions.class; + try { + sClipRevealMethod = activityOptionsClass.getDeclaredMethod("makeClipRevealAnimation", + View.class, int.class, int.class, int.class, int.class); + } catch (Exception e) { + // Earlier version + } + } + + @Thunk Runnable mBuildLayersRunnable = new Runnable() { public void run() { if (mWorkspace != null) { mWorkspace.buildPageHardwareLayers(); @@ -361,7 +383,7 @@ public class Launcher extends Activity private static PendingAddArguments sPendingAddItem; - private static class PendingAddArguments { + @Thunk static class PendingAddArguments { int requestCode; Intent intent; long container; @@ -372,8 +394,23 @@ public class Launcher extends Activity } private Stats mStats; - FocusIndicatorView mFocusHandler; + private boolean mRotationEnabled = false; + + @Thunk void setOrientation() { + if (mRotationEnabled) { + unlockScreenOrientation(true); + } else { + setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); + } + } + + private Runnable mUpdateOrientationRunnable = new Runnable() { + public void run() { + setOrientation(); + } + }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -400,20 +437,22 @@ public class Launcher extends Activity LauncherAppState.setApplicationContext(getApplicationContext()); LauncherAppState app = LauncherAppState.getInstance(); - LauncherAppState.getLauncherProvider().setLauncherProviderChangeListener(this); - // Lazy-initialize the dynamic grid - DeviceProfile grid = app.initDynamicGrid(this); + // Load configuration-specific DeviceProfile + mDeviceProfile = getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE ? + app.getInvariantDeviceProfile().landscapeProfile + : app.getInvariantDeviceProfile().portraitProfile; - // the LauncherApplication should call this, but in case of Instrumentation it might not be present yet mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); mIsSafeModeEnabled = getPackageManager().isSafeMode(); mModel = app.setLauncher(this); mIconCache = app.getIconCache(); - mIconCache.flushInvalidIcons(grid); + mDragController = new DragController(this); mInflater = getLayoutInflater(); + mStateTransitionAnimation = new LauncherStateTransitionAnimation(this, this); mStats = new Stats(this); @@ -432,13 +471,10 @@ public class Launcher extends Activity Environment.getExternalStorageDirectory() + "/launcher"); } - checkForLocaleChange(); setContentView(R.layout.launcher); setupViews(); - grid.layout(this); - - registerContentObservers(); + mDeviceProfile.layout(this); lockAllApps(); @@ -453,11 +489,11 @@ public class Launcher extends Activity if (DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE) { // If the user leaves launcher, then we should just load items asynchronously when // they return. - mModel.startLoader(true, PagedView.INVALID_RESTORE_PAGE); + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE); } else { // We only load the page synchronously if the user rotates (or triggers a // configuration change) while launcher is in the foreground - mModel.startLoader(true, mWorkspace.getRestorePage()); + mModel.startLoader(mWorkspace.getRestorePage()); } } @@ -468,8 +504,16 @@ public class Launcher extends Activity IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(mCloseSystemDialogsReceiver, filter); - // On large interfaces, we want the screen to auto-rotate based on the current orientation - unlockScreenOrientation(true); + mRotationEnabled = Utilities.isRotationAllowedForDevice(getApplicationContext()); + // In case we are on a device with locked rotation, we should look at preferences to check + // if the user has specifically allowed rotation. + if (!mRotationEnabled) { + mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext(), false); + } + + // On large interfaces, or on devices that a user has specifically enabled screen rotation, + // we want the screen to auto-rotate based on the current orientation + setOrientation(); if (mLauncherCallbacks != null) { mLauncherCallbacks.onCreate(savedInstanceState); @@ -490,6 +534,16 @@ public class Launcher extends Activity } } + @Override + public void onSettingsChanged(String settings, boolean value) { + if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(settings)) { + mRotationEnabled = value; + if (!waitUntilResume(mUpdateOrientationRunnable, true)) { + mUpdateOrientationRunnable.run(); + } + } + } + private LauncherCallbacks mLauncherCallbacks; public void onPostCreate(Bundle savedInstanceState) { @@ -501,6 +555,47 @@ public class Launcher extends Activity public boolean setLauncherCallbacks(LauncherCallbacks callbacks) { mLauncherCallbacks = callbacks; + mLauncherCallbacks.setLauncherSearchCallback(new Launcher.LauncherSearchCallbacks() { + private boolean mWorkspaceImportanceStored = false; + private boolean mHotseatImportanceStored = false; + private int mWorkspaceImportanceForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + private int mHotseatImportanceForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + + @Override + public void onSearchOverlayOpened() { + if (mWorkspaceImportanceStored || mHotseatImportanceStored) { + return; + } + // The underlying workspace and hotseat are temporarily suppressed by the search + // overlay. So they sholudn't be accessible. + if (mWorkspace != null) { + mWorkspaceImportanceForAccessibility = + mWorkspace.getImportantForAccessibility(); + mWorkspace.setImportantForAccessibility( + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + mWorkspaceImportanceStored = true; + } + if (mHotseat != null) { + mHotseatImportanceForAccessibility = mHotseat.getImportantForAccessibility(); + mHotseat.setImportantForAccessibility( + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + mHotseatImportanceStored = true; + } + } + + @Override + public void onSearchOverlayClosed() { + if (mWorkspaceImportanceStored && mWorkspace != null) { + mWorkspace.setImportantForAccessibility(mWorkspaceImportanceForAccessibility); + } + if (mHotseatImportanceStored && mHotseat != null) { + mHotseat.setImportantForAccessibility(mHotseatImportanceForAccessibility); + } + mWorkspaceImportanceStored = false; + mHotseatImportanceStored = false; + } + }); return true; } @@ -511,6 +606,14 @@ public class Launcher extends Activity } } + /** + * Updates the bounds of all the overlays to match the new fixed bounds. + */ + public void updateOverlayBounds(Rect newBounds) { + mAppsView.setSearchBarBounds(newBounds); + mWidgetsView.setSearchBarBounds(newBounds); + } + /** To be overridden by subclasses to hint to Launcher that we have custom content */ protected boolean hasCustomContentToLeft() { if (mLauncherCallbacks != null) { @@ -549,108 +652,6 @@ public class Launcher extends Activity } } - private void checkForLocaleChange() { - if (sLocaleConfiguration == null) { - new AsyncTask() { - @Override - protected LocaleConfiguration doInBackground(Void... unused) { - LocaleConfiguration localeConfiguration = new LocaleConfiguration(); - readConfiguration(Launcher.this, localeConfiguration); - return localeConfiguration; - } - - @Override - protected void onPostExecute(LocaleConfiguration result) { - sLocaleConfiguration = result; - checkForLocaleChange(); // recursive, but now with a locale configuration - } - }.execute(); - return; - } - - final Configuration configuration = getResources().getConfiguration(); - - final String previousLocale = sLocaleConfiguration.locale; - final String locale = configuration.locale.toString(); - - final int previousMcc = sLocaleConfiguration.mcc; - final int mcc = configuration.mcc; - - final int previousMnc = sLocaleConfiguration.mnc; - final int mnc = configuration.mnc; - - boolean localeChanged = !locale.equals(previousLocale) || mcc != previousMcc || mnc != previousMnc; - - if (localeChanged) { - sLocaleConfiguration.locale = locale; - sLocaleConfiguration.mcc = mcc; - sLocaleConfiguration.mnc = mnc; - - mIconCache.flush(); - - final LocaleConfiguration localeConfiguration = sLocaleConfiguration; - new AsyncTask() { - public Void doInBackground(Void ... args) { - writeConfiguration(Launcher.this, localeConfiguration); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - } - } - - private static class LocaleConfiguration { - public String locale; - public int mcc = -1; - public int mnc = -1; - } - - private static void readConfiguration(Context context, LocaleConfiguration configuration) { - DataInputStream in = null; - try { - in = new DataInputStream(context.openFileInput(LauncherFiles.LAUNCHER_PREFERENCES)); - configuration.locale = in.readUTF(); - configuration.mcc = in.readInt(); - configuration.mnc = in.readInt(); - } catch (FileNotFoundException e) { - // Ignore - } catch (IOException e) { - // Ignore - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - // Ignore - } - } - } - } - - private static void writeConfiguration(Context context, LocaleConfiguration configuration) { - DataOutputStream out = null; - try { - out = new DataOutputStream(context.openFileOutput( - LauncherFiles.LAUNCHER_PREFERENCES, MODE_PRIVATE)); - out.writeUTF(configuration.locale); - out.writeInt(configuration.mcc); - out.writeInt(configuration.mnc); - out.flush(); - } catch (FileNotFoundException e) { - // Ignore - } catch (IOException e) { - //noinspection ResultOfMethodCallIgnored - context.getFileStreamPath(LauncherFiles.LAUNCHER_PREFERENCES).delete(); - } finally { - if (out != null) { - try { - out.close(); - } catch (IOException e) { - // Ignore - } - } - } - } - public Stats getStats() { return mStats; } @@ -659,24 +660,13 @@ public class Launcher extends Activity return mInflater; } - boolean isDraggingEnabled() { + public boolean isDraggingEnabled() { // We prevent dragging when we are loading the workspace as it is possible to pick up a view // that is subsequently removed from the workspace in startBinding(). return !mModel.isLoadingWorkspace(); } - static int getScreen() { - synchronized (sLock) { - return sScreen; - } - } - - static void setScreen(int screen) { - synchronized (sLock) { - sScreen = screen; - } - } - + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static int generateViewId() { if (Build.VERSION.SDK_INT >= 17) { return View.generateViewId(); @@ -766,7 +756,7 @@ public class Launcher extends Activity return; } else if (requestCode == REQUEST_PICK_WALLPAPER) { if (resultCode == RESULT_OK && mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(false); + showWorkspace(false); } return; } @@ -882,6 +872,15 @@ public class Launcher extends Activity } } + /** @Override for MNC */ + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + if (mLauncherCallbacks != null) { + mLauncherCallbacks.onRequestPermissionsResult(requestCode, permissions, + grantResults); + } + } + private PendingAddArguments preparePendingAddArgs(int requestCode, Intent data, int appWidgetId, ItemInfo info) { PendingAddArguments args = new PendingAddArguments(); @@ -914,7 +913,7 @@ public class Launcher extends Activity } } - private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { + @Thunk void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { CellLayout cellLayout = (CellLayout) mWorkspace.getScreenWithId(mPendingAddInfo.screenId); Runnable onCompleteRunnable = null; @@ -986,18 +985,26 @@ public class Launcher extends Activity // Restore the previous launcher state if (mOnResumeState == State.WORKSPACE) { showWorkspace(false); - } else if (mOnResumeState == State.APPS_CUSTOMIZE) { - showAllApps(false, mAppsCustomizeContent.getContentType(), false); + } else if (mOnResumeState == State.APPS) { + boolean launchedFromApp = (mWaitingForResume != null); + // Don't update the predicted apps if the user is returning to launcher in the apps + // view after launching an app, as they may be depending on the UI to be static to + // switch to another app, otherwise, if it was + showAppsView(false /* animated */, false /* resetListToTop */, + !launchedFromApp /* updatePredictedApps */, false /* focusSearchBar */); + } else if (mOnResumeState == State.WIDGETS) { + showWidgetsView(false, false); } mOnResumeState = State.NONE; - // Background was set to gradient in onPause(), restore to black if in all apps. - setWorkspaceBackground(mState == State.WORKSPACE); + // Background was set to gradient in onPause(), restore to transparent if in all apps. + setWorkspaceBackground(mState == State.WORKSPACE ? WORKSPACE_BACKGROUND_GRADIENT + : WORKSPACE_BACKGROUND_TRANSPARENT); mPaused = false; if (mRestoring || mOnResumeNeedsLoad) { setWorkspaceLoading(true); - mModel.startLoader(true, PagedView.INVALID_RESTORE_PAGE); + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE); mRestoring = false; mOnResumeNeedsLoad = false; } @@ -1009,15 +1016,9 @@ public class Launcher extends Activity startTimeCallbacks = System.currentTimeMillis(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setBulkBind(true); - } for (int i = 0; i < mBindOnResumeCallbacks.size(); i++) { mBindOnResumeCallbacks.get(i).run(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setBulkBind(false); - } mBindOnResumeCallbacks.clear(); if (DEBUG_RESUME_TIME) { Log.d(TAG, "Time spent processing callbacks in onResume: " + @@ -1043,9 +1044,7 @@ public class Launcher extends Activity // (framework issue). On resuming, we ensure that any widgets are inflated for the current // orientation. getWorkspace().reinflateWidgetsIfNecessary(); - - // Process any items that were added while Launcher was away. - InstallShortcutReceiver.disableAndFlushInstallQueue(this); + reinflateQSBIfNecessary(); if (DEBUG_RESUME_TIME) { Log.d(TAG, "Time spent in onResume: " + (System.currentTimeMillis() - startTime)); @@ -1059,10 +1058,13 @@ public class Launcher extends Activity mWorkspace.getCustomContentCallbacks().onShow(true); } } - mWorkspace.updateInteractionForState(); + updateInteraction(Workspace.State.NORMAL, mWorkspace.getState()); mWorkspace.onResume(); - PackageInstallerCompat.getInstance(this).onResume(); + if (!isWorkspaceLoading()) { + // Process any items that were added while Launcher was away. + InstallShortcutReceiver.disableAndFlushInstallQueue(this); + } if (mLauncherCallbacks != null) { mLauncherCallbacks.onResume(); @@ -1073,7 +1075,6 @@ public class Launcher extends Activity protected void onPause() { // Ensure that items added to Launcher are queued until Launcher returns InstallShortcutReceiver.enableInstallQueue(); - PackageInstallerCompat.getInstance(this).onPause(); super.onPause(); mPaused = true; @@ -1136,6 +1137,18 @@ public class Launcher extends Activity public void forceExitFullImmersion(); } + public interface LauncherSearchCallbacks { + /** + * Called when the search overlay is shown. + */ + public void onSearchOverlayOpened(); + + /** + * Called when the search overlay is dismissed. + */ + public void onSearchOverlayClosed(); + } + public interface LauncherOverlayCallbacks { /** * This method indicates whether a call to {@link #enterFullImmersion()} will succeed, @@ -1187,11 +1200,13 @@ public class Launcher extends Activity protected boolean hasSettings() { if (mLauncherCallbacks != null) { return mLauncherCallbacks.hasSettings(); + } else { + // On devices with a locked orientation, we will at least have the allow rotation + // setting. + return !Utilities.isRotationAllowedForDevice(this); } - return false; } - public void addToCustomContentPage(View customContent, CustomContentCallbacks callbacks, String description) { mWorkspace.addToCustomContentPage(customContent, callbacks, description); @@ -1208,9 +1223,8 @@ public class Launcher extends Activity if (mModel.isCurrentCallbacks(this)) { mModel.stopLoader(); } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.surrender(); - } + //TODO(hyunyoungs): stop the widgets loader when there is a rotation. + return Boolean.TRUE; } @@ -1296,8 +1310,8 @@ public class Launcher extends Activity } State state = intToState(savedState.getInt(RUNTIME_STATE, State.WORKSPACE.ordinal())); - if (state == State.APPS_CUSTOMIZE) { - mOnResumeState = State.APPS_CUSTOMIZE; + if (state == State.APPS || state == State.WIDGETS) { + mOnResumeState = state; } int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, @@ -1316,32 +1330,16 @@ public class Launcher extends Activity mPendingAddInfo.cellY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_Y); mPendingAddInfo.spanX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_X); mPendingAddInfo.spanY = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y); - mPendingAddWidgetInfo = savedState.getParcelable(RUNTIME_STATE_PENDING_ADD_WIDGET_INFO); + AppWidgetProviderInfo info = savedState.getParcelable( + RUNTIME_STATE_PENDING_ADD_WIDGET_INFO); + mPendingAddWidgetInfo = info == null ? + null : LauncherAppWidgetProviderInfo.fromProviderInfo(this, info); + mPendingAddWidgetId = savedState.getInt(RUNTIME_STATE_PENDING_ADD_WIDGET_ID); setWaitingForResult(true); mRestoring = true; } - boolean renameFolder = savedState.getBoolean(RUNTIME_STATE_PENDING_FOLDER_RENAME, false); - if (renameFolder) { - long id = savedState.getLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID); - mFolderInfo = mModel.getFolderById(this, sFolders, id); - mRestoring = true; - } - - // Restore the AppsCustomize tab - if (mAppsCustomizeTabHost != null) { - String curTab = savedState.getString("apps_customize_currentTab"); - if (curTab != null) { - mAppsCustomizeTabHost.setContentTypeImmediate( - mAppsCustomizeTabHost.getContentTypeForTabTag(curTab)); - mAppsCustomizeContent.loadAssociatedPages( - mAppsCustomizeContent.getCurrentPage()); - } - - int currentIndex = savedState.getInt("apps_customize_currentIndex"); - mAppsCustomizeContent.restorePageForIndex(currentIndex); - } mItemIdToViewId = (HashMap) savedState.getSerializable(RUNTIME_STATE_VIEW_IDS); } @@ -1369,13 +1367,12 @@ public class Launcher extends Activity // Setup the hotseat mHotseat = (Hotseat) findViewById(R.id.hotseat); if (mHotseat != null) { - mHotseat.setup(this); mHotseat.setOnLongClickListener(this); } mOverviewPanel = (ViewGroup) findViewById(R.id.overview_panel); - View widgetButton = findViewById(R.id.widget_button); - widgetButton.setOnClickListener(new OnClickListener() { + mWidgetsButton = findViewById(R.id.widget_button); + mWidgetsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { if (!mWorkspace.isSwitchingState()) { @@ -1383,7 +1380,7 @@ public class Launcher extends Activity } } }); - widgetButton.setOnTouchListener(getHapticFeedbackTouchListener()); + mWidgetsButton.setOnTouchListener(getHapticFeedbackTouchListener()); View wallpaperButton = findViewById(R.id.wallpaper_button); wallpaperButton.setOnClickListener(new OnClickListener() { @@ -1423,11 +1420,14 @@ public class Launcher extends Activity mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.search_drop_target_bar); - // Setup AppsCustomize - mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane); - mAppsCustomizeContent = (AppsCustomizePagedView) - mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content); - mAppsCustomizeContent.setup(this, dragController); + // Setup Apps and Widgets + mAppsView = (AllAppsContainerView) findViewById(R.id.apps_view); + mWidgetsView = (WidgetsContainerView) findViewById(R.id.widgets_view); + if (mLauncherCallbacks != null && mLauncherCallbacks.getAllAppsSearchBarController() != null) { + mAppsView.setSearchBarController(mLauncherCallbacks.getAllAppsSearchBarController()); + } else { + mAppsView.setSearchBarController(mAppsView.newDefaultAppSearchController()); + } // Setup the drag controller (drop targets have to be added in reverse order in priority) dragController.setDragScoller(mWorkspace); @@ -1436,7 +1436,7 @@ public class Launcher extends Activity dragController.addDropTarget(mWorkspace); if (mSearchDropTargetBar != null) { mSearchDropTargetBar.setup(this, dragController); - mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); } if (getResources().getBoolean(R.bool.debug_memory_enabled)) { @@ -1466,30 +1466,32 @@ public class Launcher extends Activity return mAllAppsButton; } + public View getWidgetsButton() { + return mWidgetsButton; + } + /** * Creates a view representing a shortcut. * * @param info The data structure describing the shortcut. - * - * @return A View inflated from R.layout.application. */ View createShortcut(ShortcutInfo info) { - return createShortcut(R.layout.application, - (ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); + return createShortcut((ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info); } /** * Creates a view representing a shortcut inflated from the specified resource. * - * @param layoutResId The id of the XML layout used to create the shortcut. * @param parent The group the shortcut belongs to. * @param info The data structure describing the shortcut. * * @return A View inflated from layoutResId. */ - View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) { - BubbleTextView favorite = (BubbleTextView) mInflater.inflate(layoutResId, parent, false); - favorite.applyFromShortcutInfo(info, mIconCache, true); + public View createShortcut(ViewGroup parent, ShortcutInfo info) { + BubbleTextView favorite = (BubbleTextView) mInflater.inflate(R.layout.app_icon, + parent, false); + favorite.applyFromShortcutInfo(info, mIconCache); + favorite.setCompoundDrawablePadding(mDeviceProfile.iconDrawablePaddingPx); favorite.setOnClickListener(this); favorite.setOnFocusChangeListener(mFocusHandler); return favorite; @@ -1499,7 +1501,6 @@ public class Launcher extends Activity * Add a shortcut to the workspace. * * @param data The intent describing the shortcut. - * @param cellInfo The position on screen where to create the shortcut. */ private void completeAddShortcut(Intent data, long container, long screenId, int cellX, int cellY) { @@ -1507,14 +1508,13 @@ public class Launcher extends Activity int[] touchXY = mPendingAddInfo.dropPos; CellLayout layout = getCellLayout(container, screenId); - boolean foundCellSpan = false; - - ShortcutInfo info = mModel.infoFromShortcutIntent(this, data); + ShortcutInfo info = InstallShortcutReceiver.fromShortcutIntent(this, data); if (info == null) { return; } final View view = createShortcut(info); + boolean foundCellSpan = false; // First we check if we already know the exact location where we want to add this item. if (cellX >= 0 && cellY >= 0) { cellXY[0] = cellX; @@ -1545,7 +1545,7 @@ public class Launcher extends Activity return; } - LauncherModel.addItemToDatabase(this, info, container, screenId, cellXY[0], cellXY[1], false); + LauncherModel.addItemToDatabase(this, info, container, screenId, cellXY[0], cellXY[1]); if (!mRestoring) { mWorkspace.addInScreen(view, container, screenId, cellXY[0], cellXY[1], 1, 1, @@ -1553,119 +1553,67 @@ public class Launcher extends Activity } } - static int[] getSpanForWidget(Context context, ComponentName component, int minWidth, - int minHeight) { - Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context, component, null); + private int[] getSpanForWidget(ComponentName component, int minWidth, int minHeight) { + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(this, component, null); // We want to account for the extra amount of padding that we are adding to the widget // to ensure that it gets the full amount of space that it has requested int requiredWidth = minWidth + padding.left + padding.right; int requiredHeight = minHeight + padding.top + padding.bottom; - return CellLayout.rectToCell(requiredWidth, requiredHeight, null); + return CellLayout.rectToCell(this, requiredWidth, requiredHeight, null); } - static int[] getSpanForWidget(Context context, AppWidgetProviderInfo info) { - return getSpanForWidget(context, info.provider, info.minWidth, info.minHeight); + public int[] getSpanForWidget(AppWidgetProviderInfo info) { + return getSpanForWidget(info.provider, info.minWidth, info.minHeight); } - static int[] getMinSpanForWidget(Context context, AppWidgetProviderInfo info) { - return getSpanForWidget(context, info.provider, info.minResizeWidth, info.minResizeHeight); - } - - static int[] getSpanForWidget(Context context, PendingAddWidgetInfo info) { - return getSpanForWidget(context, info.componentName, info.minWidth, info.minHeight); - } - - static int[] getMinSpanForWidget(Context context, PendingAddWidgetInfo info) { - return getSpanForWidget(context, info.componentName, info.minResizeWidth, - info.minResizeHeight); + public int[] getMinSpanForWidget(AppWidgetProviderInfo info) { + return getSpanForWidget(info.provider, info.minResizeWidth, info.minResizeHeight); } /** * Add a widget to the workspace. * * @param appWidgetId The app widget id - * @param cellInfo The position on screen where to create the widget. */ - private void completeAddAppWidget(final int appWidgetId, long container, long screenId, - AppWidgetHostView hostView, AppWidgetProviderInfo appWidgetInfo) { + @Thunk void completeAddAppWidget(int appWidgetId, long container, long screenId, + AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) { + + ItemInfo info = mPendingAddInfo; if (appWidgetInfo == null) { - appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); + appWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(this, + mAppWidgetManager.getAppWidgetInfo(appWidgetId)); } - // Calculate the grid spans needed to fit this widget - CellLayout layout = getCellLayout(container, screenId); - - int[] minSpanXY = getMinSpanForWidget(this, appWidgetInfo); - int[] spanXY = getSpanForWidget(this, appWidgetInfo); - - // Try finding open space on Launcher screen - // We have saved the position to which the widget was dragged-- this really only matters - // if we are placing widgets on a "spring-loaded" screen - int[] cellXY = mTmpAddItemCellCoordinates; - int[] touchXY = mPendingAddInfo.dropPos; - int[] finalSpan = new int[2]; - boolean foundCellSpan = false; - if (mPendingAddInfo.cellX >= 0 && mPendingAddInfo.cellY >= 0) { - cellXY[0] = mPendingAddInfo.cellX; - cellXY[1] = mPendingAddInfo.cellY; - spanXY[0] = mPendingAddInfo.spanX; - spanXY[1] = mPendingAddInfo.spanY; - foundCellSpan = true; - } else if (touchXY != null) { - // when dragging and dropping, just find the closest free spot - int[] result = layout.findNearestVacantArea( - touchXY[0], touchXY[1], minSpanXY[0], minSpanXY[1], spanXY[0], - spanXY[1], cellXY, finalSpan); - spanXY[0] = finalSpan[0]; - spanXY[1] = finalSpan[1]; - foundCellSpan = (result != null); - } else { - foundCellSpan = layout.findCellForSpan(cellXY, minSpanXY[0], minSpanXY[1]); + if (appWidgetInfo.isCustomWidget) { + appWidgetId = LauncherAppWidgetInfo.CUSTOM_WIDGET_ID; } - if (!foundCellSpan) { - if (appWidgetId != -1) { - // Deleting an app widget ID is a void call but writes to disk before returning - // to the caller... - new AsyncTask() { - public Void doInBackground(Void ... args) { - mAppWidgetHost.deleteAppWidgetId(appWidgetId); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - } - showOutOfSpaceMessage(isHotseatLayout(layout)); - return; - } - - // Build Launcher-specific widget info and save to database - LauncherAppWidgetInfo launcherInfo = new LauncherAppWidgetInfo(appWidgetId, - appWidgetInfo.provider); - launcherInfo.spanX = spanXY[0]; - launcherInfo.spanY = spanXY[1]; - launcherInfo.minSpanX = mPendingAddInfo.minSpanX; - launcherInfo.minSpanY = mPendingAddInfo.minSpanY; + LauncherAppWidgetInfo launcherInfo; + launcherInfo = new LauncherAppWidgetInfo(appWidgetId, appWidgetInfo.provider); + launcherInfo.spanX = info.spanX; + launcherInfo.spanY = info.spanY; + launcherInfo.minSpanX = info.minSpanX; + launcherInfo.minSpanY = info.minSpanY; launcherInfo.user = mAppWidgetManager.getUser(appWidgetInfo); LauncherModel.addItemToDatabase(this, launcherInfo, - container, screenId, cellXY[0], cellXY[1], false); + container, screenId, info.cellX, info.cellY); if (!mRestoring) { if (hostView == null) { // Perform actual inflation because we're live - launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); - launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo); + launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, + appWidgetInfo); } else { // The AppWidgetHostView has already been inflated and instantiated launcherInfo.hostView = hostView; } - launcherInfo.hostView.setTag(launcherInfo); launcherInfo.hostView.setVisibility(View.VISIBLE); launcherInfo.notifyWidgetSizeChanged(this); - mWorkspace.addInScreen(launcherInfo.hostView, container, screenId, cellXY[0], cellXY[1], - launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); + mWorkspace.addInScreen(launcherInfo.hostView, container, screenId, info.cellX, + info.cellY, launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); addWidgetToAutoAdvanceIfNeeded(launcherInfo.hostView, appWidgetInfo); } @@ -1679,28 +1627,26 @@ public class Launcher extends Activity if (Intent.ACTION_SCREEN_OFF.equals(action)) { mUserPresent = false; mDragLayer.clearAllResizeFrames(); - updateRunning(); + updateAutoAdvanceState(); // Reset AllApps to its initial state only if we are not in the middle of // processing a multi-step drop - if (mAppsCustomizeTabHost != null && mPendingAddInfo.container == ItemInfo.NO_ID) { + if (mAppsView != null && mWidgetsView != null && + mPendingAddInfo.container == ItemInfo.NO_ID) { showWorkspace(false); } } else if (Intent.ACTION_USER_PRESENT.equals(action)) { mUserPresent = true; - updateRunning(); + updateAutoAdvanceState(); } else if (ENABLE_DEBUG_INTENTS && DebugIntents.DELETE_DATABASE.equals(action)) { mModel.resetLoadedState(false, true); - mModel.startLoader(false, PagedView.INVALID_RESTORE_PAGE, + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE, LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE); } else if (ENABLE_DEBUG_INTENTS && DebugIntents.MIGRATE_DATABASE.equals(action)) { mModel.resetLoadedState(false, true); - mModel.startLoader(false, PagedView.INVALID_RESTORE_PAGE, + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE, LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE | LauncherModel.LOADER_FLAG_MIGRATE_SHORTCUTS); - } else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED.equals(action) - || LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { - getModel().forceReload(); } } }; @@ -1714,8 +1660,6 @@ public class Launcher extends Activity filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_USER_PRESENT); // For handling managed profiles - filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED); - filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED); if (ENABLE_DEBUG_INTENTS) { filter.addAction(DebugIntents.DELETE_DATABASE); filter.addAction(DebugIntents.MIGRATE_DATABASE); @@ -1731,40 +1675,19 @@ public class Launcher extends Activity * Sets up transparent navigation and status bars in LMP. * This method is a no-op for other platform versions. */ - @TargetApi(19) + @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void setupTransparentSystemBarsForLmp() { - // TODO(sansid): use the APIs directly when compiling against L sdk. - // Currently we use reflection to access the flags and the API to set the transparency - // on the System bars. if (Utilities.isLmpOrAbove()) { - try { - getWindow().getAttributes().systemUiVisibility |= - (View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS - | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); - Field drawsSysBackgroundsField = WindowManager.LayoutParams.class.getField( - "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS"); - getWindow().addFlags(drawsSysBackgroundsField.getInt(null)); - - Method setStatusBarColorMethod = - Window.class.getDeclaredMethod("setStatusBarColor", int.class); - Method setNavigationBarColorMethod = - Window.class.getDeclaredMethod("setNavigationBarColor", int.class); - setStatusBarColorMethod.invoke(getWindow(), Color.TRANSPARENT); - setNavigationBarColorMethod.invoke(getWindow(), Color.TRANSPARENT); - } catch (NoSuchFieldException e) { - Log.w(TAG, "NoSuchFieldException while setting up transparent bars"); - } catch (NoSuchMethodException ex) { - Log.w(TAG, "NoSuchMethodException while setting up transparent bars"); - } catch (IllegalAccessException e) { - Log.w(TAG, "IllegalAccessException while setting up transparent bars"); - } catch (IllegalArgumentException e) { - Log.w(TAG, "IllegalArgumentException while setting up transparent bars"); - } catch (InvocationTargetException e) { - Log.w(TAG, "InvocationTargetException while setting up transparent bars"); - } finally {} + Window window = getWindow(); + window.getAttributes().systemUiVisibility |= + (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(Color.TRANSPARENT); + window.setNavigationBarColor(Color.TRANSPARENT); } } @@ -1777,17 +1700,16 @@ public class Launcher extends Activity unregisterReceiver(mReceiver); mAttached = false; } - updateRunning(); + updateAutoAdvanceState(); } public void onWindowVisibilityChanged(int visibility) { mVisible = visibility == View.VISIBLE; - updateRunning(); + updateAutoAdvanceState(); // The following code used to be in onResume, but it turns out onResume is called when // you're in All Apps and click home to go to the workspace. onWindowVisibilityChanged // is a more appropriate event to handle if (mVisible) { - mAppsCustomizeTabHost.onWindowVisible(); if (!mWorkspaceLoading) { final ViewTreeObserver observer = mWorkspace.getViewTreeObserver(); // We want to let Launcher draw itself at least once before we force it to build @@ -1822,14 +1744,14 @@ public class Launcher extends Activity } } - private void sendAdvanceMessage(long delay) { + @Thunk void sendAdvanceMessage(long delay) { mHandler.removeMessages(ADVANCE_MSG); Message msg = mHandler.obtainMessage(ADVANCE_MSG); mHandler.sendMessageDelayed(msg, delay); mAutoAdvanceSentTime = System.currentTimeMillis(); } - private void updateRunning() { + @Thunk void updateAutoAdvanceState() { boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty(); if (autoAdvanceRunning != mAutoAdvanceRunning) { mAutoAdvanceRunning = autoAdvanceRunning; @@ -1847,16 +1769,17 @@ public class Launcher extends Activity } } - private final Handler mHandler = new Handler() { + @Thunk final Handler mHandler = new Handler(new Handler.Callback() { + @Override - public void handleMessage(Message msg) { + public boolean handleMessage(Message msg) { if (msg.what == ADVANCE_MSG) { int i = 0; for (View key: mWidgetsToAdvance.keySet()) { final View v = key.findViewById(mWidgetsToAdvance.get(key).autoAdvanceViewId); final int delay = mAdvanceStagger * i; if (v instanceof Advanceable) { - postDelayed(new Runnable() { + mHandler.postDelayed(new Runnable() { public void run() { ((Advanceable) v).advance(); } @@ -1866,8 +1789,9 @@ public class Launcher extends Activity } sendAdvanceMessage(mAdvanceInterval); } + return true; } - }; + }); void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) { if (appWidgetInfo == null || appWidgetInfo.autoAdvanceViewId == -1) return; @@ -1875,14 +1799,14 @@ public class Launcher extends Activity if (v instanceof Advanceable) { mWidgetsToAdvance.put(hostView, appWidgetInfo); ((Advanceable) v).fyiWillBeAdvancedByHostKThx(); - updateRunning(); + updateAutoAdvanceState(); } } void removeWidgetToAutoAdvance(View hostView) { if (mWidgetsToAdvance.containsKey(hostView)) { mWidgetsToAdvance.remove(hostView); - updateRunning(); + updateAutoAdvanceState(); } } @@ -1891,7 +1815,7 @@ public class Launcher extends Activity launcherInfo.hostView = null; } - void showOutOfSpaceMessage(boolean isHotseatLayout) { + public void showOutOfSpaceMessage(boolean isHotseatLayout) { int strId = (isHotseatLayout ? R.string.hotseat_out_of_space : R.string.out_of_space); Toast.makeText(this, getString(strId), Toast.LENGTH_SHORT).show(); } @@ -1900,6 +1824,14 @@ public class Launcher extends Activity return mDragLayer; } + public AllAppsContainerView getAppsView() { + return mAppsView; + } + + public WidgetsContainerView getWidgetsView() { + return mWidgetsView; + } + public Workspace getWorkspace() { return mWorkspace; } @@ -1928,6 +1860,10 @@ public class Launcher extends Activity return mSharedPrefs; } + public DeviceProfile getDeviceProfile() { + return mDeviceProfile; + } + public void closeSystemDialogs() { getWindow().closeAllPanels(); @@ -1985,9 +1921,14 @@ public class Launcher extends Activity imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } - // Reset the apps customize page - if (!alreadyOnHome && mAppsCustomizeTabHost != null) { - mAppsCustomizeTabHost.reset(); + // Reset the apps view + if (!alreadyOnHome && mAppsView != null) { + mAppsView.scrollToTop(); + } + + // Reset the widgets view + if (!alreadyOnHome && mWidgetsView != null) { + mWidgetsView.scrollToTop(); } if (mLauncherCallbacks != null) { @@ -2037,21 +1978,8 @@ public class Launcher extends Activity outState.putInt(RUNTIME_STATE_PENDING_ADD_WIDGET_ID, mPendingAddWidgetId); } - if (mFolderInfo != null && mWaitingForResult) { - outState.putBoolean(RUNTIME_STATE_PENDING_FOLDER_RENAME, true); - outState.putLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID, mFolderInfo.id); - } - - // Save the current AppsCustomize tab - if (mAppsCustomizeTabHost != null) { - AppsCustomizePagedView.ContentType type = mAppsCustomizeContent.getContentType(); - String currentTabTag = mAppsCustomizeTabHost.getTabTagForContentType(type); - if (currentTabTag != null) { - outState.putString("apps_customize_currentTab", currentTabTag); - } - int currentIndex = mAppsCustomizeContent.getSaveInstanceStateIndex(); - outState.putInt("apps_customize_currentIndex", currentIndex); - } + // Save the current widgets tray? + // TODO(hyunyoungs) outState.putSerializable(RUNTIME_STATE_VIEW_IDS, mItemIdToViewId); if (mLauncherCallbacks != null) { @@ -2089,13 +2017,6 @@ public class Launcher extends Activity TextKeyListener.getInstance().release(); - // Disconnect any of the callbacks and drawables associated with ItemInfos on the workspace - // to prevent leaking Launcher activities on orientation change. - if (mModel != null) { - mModel.unbindItemInfosAndClearQueuedBindRunnables(); - } - - getContentResolver().unregisterContentObserver(mWidgetObserver); unregisterReceiver(mCloseSystemDialogsReceiver); mDragLayer.clearAllResizeFrames(); @@ -2117,10 +2038,26 @@ public class Launcher extends Activity @Override public void startActivityForResult(Intent intent, int requestCode) { + onStartForResult(requestCode); + super.startActivityForResult(intent, requestCode); + } + + @Override + public void startIntentSenderForResult (IntentSender intent, int requestCode, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) { + onStartForResult(requestCode); + try { + super.startIntentSenderForResult(intent, requestCode, + fillInIntent, flagsMask, flagsValues, extraFlags, options); + } catch (IntentSender.SendIntentException e) { + throw new ActivityNotFoundException(); + } + } + + private void onStartForResult(int requestCode) { if (requestCode >= 0) { setWaitingForResult(true); } - super.startActivityForResult(intent, requestCode); } /** @@ -2131,8 +2068,6 @@ public class Launcher extends Activity public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { - showWorkspace(true); - if (initialQuery == null) { // Use any text typed in the launcher as the initial query initialQuery = getTypedText(); @@ -2151,6 +2086,9 @@ public class Launcher extends Activity if (clearTextImmediately) { clearTypedText(); } + + // We need to show the workspace after starting the search + showWorkspace(true); } /** @@ -2284,14 +2222,14 @@ public class Launcher extends Activity mPendingAddInfo.dropPos = null; } - void addAppWidgetImpl(final int appWidgetId, final ItemInfo info, - final AppWidgetHostView boundWidget, final AppWidgetProviderInfo appWidgetInfo) { + void addAppWidgetImpl(final int appWidgetId, final ItemInfo info, final + AppWidgetHostView boundWidget, final LauncherAppWidgetProviderInfo appWidgetInfo) { addAppWidgetImpl(appWidgetId, info, boundWidget, appWidgetInfo, 0); } void addAppWidgetImpl(final int appWidgetId, final ItemInfo info, - final AppWidgetHostView boundWidget, final AppWidgetProviderInfo appWidgetInfo, int - delay) { + final AppWidgetHostView boundWidget, final LauncherAppWidgetProviderInfo appWidgetInfo, + int delay) { if (appWidgetInfo.configure != null) { mPendingAddWidgetInfo = appWidgetInfo; mPendingAddWidgetId = appWidgetId; @@ -2321,20 +2259,39 @@ public class Launcher extends Activity closeFolder(); mWorkspace.moveToCustomContentScreen(animate); } + + public void addPendingItem(PendingAddItemInfo info, long container, long screenId, + int[] cell, int spanX, int spanY) { + switch (info.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + int span[] = new int[2]; + span[0] = spanX; + span[1] = spanY; + addAppWidgetFromDrop((PendingAddWidgetInfo) info, + container, screenId, cell, span); + break; + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + processShortcutFromDrop(info.componentName, container, screenId, cell); + break; + default: + throw new IllegalStateException("Unknown item type: " + info.itemType); + } + } + /** * Process a shortcut drop. * * @param componentName The name of the component * @param screenId The ID of the screen where it should be added * @param cell The cell it should be added to, optional - * @param position The location on the screen where it was dropped, optional */ - void processShortcutFromDrop(ComponentName componentName, long container, long screenId, - int[] cell, int[] loc) { + private void processShortcutFromDrop(ComponentName componentName, long container, long screenId, + int[] cell) { resetAddInfo(); mPendingAddInfo.container = container; mPendingAddInfo.screenId = screenId; - mPendingAddInfo.dropPos = loc; + mPendingAddInfo.dropPos = null; if (cell != null) { mPendingAddInfo.cellX = cell[0]; @@ -2343,7 +2300,7 @@ public class Launcher extends Activity Intent createShortcutIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); createShortcutIntent.setComponent(componentName); - processShortcut(createShortcutIntent); + Utilities.startActivityForResultSafely(this, createShortcutIntent, REQUEST_CREATE_SHORTCUT); } /** @@ -2352,14 +2309,13 @@ public class Launcher extends Activity * @param info The PendingAppWidgetInfo of the widget being added. * @param screenId The ID of the screen where it should be added * @param cell The cell it should be added to, optional - * @param position The location on the screen where it was dropped, optional */ - void addAppWidgetFromDrop(PendingAddWidgetInfo info, long container, long screenId, - int[] cell, int[] span, int[] loc) { + private void addAppWidgetFromDrop(PendingAddWidgetInfo info, long container, long screenId, + int[] cell, int[] span) { resetAddInfo(); mPendingAddInfo.container = info.container = container; mPendingAddInfo.screenId = info.screenId = screenId; - mPendingAddInfo.dropPos = loc; + mPendingAddInfo.dropPos = null; mPendingAddInfo.minSpanX = info.minSpanX; mPendingAddInfo.minSpanY = info.minSpanY; @@ -2377,6 +2333,9 @@ public class Launcher extends Activity if (hostView != null) { appWidgetId = hostView.getAppWidgetId(); addAppWidgetImpl(appWidgetId, info, hostView, info.info); + + // Clear the boundWidget so that it doesn't get destroyed. + info.boundWidget = null; } else { // In this case, we either need to start an activity to get permission to bind // the widget, or we need to start an activity to configure the widget, or both. @@ -2401,22 +2360,14 @@ public class Launcher extends Activity } } - void processShortcut(Intent intent) { - Utilities.startActivityForResultSafely(this, intent, REQUEST_CREATE_SHORTCUT); - } - - void processWallpaper(Intent intent) { - startActivityForResult(intent, REQUEST_PICK_WALLPAPER); - } - FolderIcon addFolder(CellLayout layout, long container, final long screenId, int cellX, int cellY) { final FolderInfo folderInfo = new FolderInfo(); folderInfo.title = getText(R.string.folder_name); // Update the model - LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId, cellX, cellY, - false); + LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId, + cellX, cellY); sFolders.put(folderInfo.id, folderInfo); // Create the view @@ -2434,23 +2385,6 @@ public class Launcher extends Activity sFolders.remove(folder.id); } - protected ComponentName getWallpaperPickerComponent() { - if (mLauncherCallbacks != null) { - return mLauncherCallbacks.getWallpaperPickerComponent(); - } - return new ComponentName(getPackageName(), LauncherWallpaperPickerActivity.class.getName()); - } - - /** - * Registers various content observers. The current implementation registers - * only a favorites observer to keep track of the favorites applications. - */ - private void registerContentObservers() { - ContentResolver resolver = getContentResolver(); - resolver.registerContentObserver(LauncherProvider.CONTENT_APPWIDGET_RESET_URI, - true, mWidgetObserver); - } - @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { @@ -2480,15 +2414,17 @@ public class Launcher extends Activity return; } - if (isAllAppsVisible()) { - if (mAppsCustomizeContent.getContentType() == - AppsCustomizePagedView.ContentType.Applications) { - showWorkspace(true); - } else { - showOverviewMode(true); - } + if (mDragController.isDragging()) { + mDragController.cancelDrag(); + return; + } + + if (isAppsViewVisible()) { + showWorkspace(true); + } else if (isWidgetsViewVisible()) { + showOverviewMode(true); } else if (mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(true); + showWorkspace(true); } else if (mWorkspace.getOpenFolder() != null) { Folder openFolder = mWorkspace.getOpenFolder(); if (openFolder.isEditingName()) { @@ -2505,9 +2441,10 @@ public class Launcher extends Activity } /** - * Re-listen when widgets are reset. + * Re-listen when widget host is reset. */ - private void onAppWidgetReset() { + @Override + public void onAppWidgetHostReset() { if (mAppWidgetHost != null) { mAppWidgetHost.startListening(); } @@ -2531,14 +2468,14 @@ public class Launcher extends Activity if (v instanceof Workspace) { if (mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(true); + showWorkspace(true); } return; } if (v instanceof CellLayout) { if (mWorkspace.isInOverviewMode()) { - mWorkspace.exitOverviewMode(mWorkspace.indexOfChild(v), true); + showWorkspace(mWorkspace.indexOfChild(v), true); } } @@ -2560,13 +2497,7 @@ public class Launcher extends Activity } } - public void onClickPagedViewIcon(View v) { - startAppShortcutOrInfoActivity(v); - if (mLauncherCallbacks != null) { - mLauncherCallbacks.onClickPagedViewIcon(v); - } - } - + @SuppressLint("ClickableViewAccessibility") public boolean onTouch(View v, MotionEvent event) { return false; } @@ -2585,7 +2516,8 @@ public class Launcher extends Activity int widgetId = info.appWidgetId; AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(widgetId); if (appWidgetInfo != null) { - mPendingAddWidgetInfo = appWidgetInfo; + mPendingAddWidgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo( + this, appWidgetInfo); mPendingAddInfo.copyFrom(info); mPendingAddWidgetId = widgetId; @@ -2616,13 +2548,17 @@ public class Launcher extends Activity */ protected void onClickAllAppsButton(View v) { if (LOGD) Log.d(TAG, "onClickAllAppsButton"); - if (isAllAppsVisible()) { - showWorkspace(true); - } else { - showAllApps(true, AppsCustomizePagedView.ContentType.Applications, false); + if (!isAppsViewVisible()) { + showAppsView(true /* animated */, false /* resetListToTop */, + true /* updatePredictedApps */, false /* focusSearchBar */); } - if (mLauncherCallbacks != null) { - mLauncherCallbacks.onClickAllAppsButton(v); + } + + protected void onLongClickAllAppsButton(View v) { + if (LOGD) Log.d(TAG, "onLongClickAllAppsButton"); + if (!isAppsViewVisible()) { + showAppsView(true /* animated */, false /* resetListToTop */, + true /* updatePredictedApps */, true /* focusSearchBar */); } } @@ -2704,7 +2640,7 @@ public class Launcher extends Activity } } - private void startAppShortcutOrInfoActivity(View v) { + @Thunk void startAppShortcutOrInfoActivity(View v) { Object tag = v.getTag(); final ShortcutInfo shortcut; final Intent intent; @@ -2724,7 +2660,7 @@ public class Launcher extends Activity } boolean success = startActivitySafely(v, intent, tag); - mStats.recordLaunch(intent, shortcut); + mStats.recordLaunch(v, intent, shortcut); if (success && v instanceof BubbleTextView) { mWaitingForResume = (BubbleTextView) v; @@ -2790,7 +2726,7 @@ public class Launcher extends Activity if (mIsSafeModeEnabled) { Toast.makeText(this, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show(); } else { - showAllApps(true, AppsCustomizePagedView.ContentType.Widgets, true); + showWidgetsView(true /* animated */, true /* resetPageToZero */); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickAddWidgetButton(view); } @@ -2803,9 +2739,8 @@ public class Launcher extends Activity */ protected void onClickWallpaperPicker(View v) { if (LOGD) Log.d(TAG, "onClickWallpaperPicker"); - final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER); - pickWallpaper.setComponent(getWallpaperPickerComponent()); - startActivityForResult(pickWallpaper, REQUEST_PICK_WALLPAPER); + startActivityForResult(new Intent(Intent.ACTION_SET_WALLPAPER).setPackage(getPackageName()), + REQUEST_PICK_WALLPAPER); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickWallpaperPicker(v); @@ -2820,22 +2755,15 @@ public class Launcher extends Activity if (LOGD) Log.d(TAG, "onClickSettingsButton"); if (mLauncherCallbacks != null) { mLauncherCallbacks.onClickSettingsButton(v); + } else { + startActivity(new Intent(this, SettingsActivity.class)); } } - public void onTouchDownAllAppsButton(View v) { - // Provide the same haptic feedback that the system offers for virtual keys. - v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - } - - public void performHapticFeedbackOnTouchDown(View v) { - // Provide the same haptic feedback that the system offers for virtual keys. - v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - } - public View.OnTouchListener getHapticFeedbackTouchListener() { if (mHapticFeedbackTouchListener == null) { mHapticFeedbackTouchListener = new View.OnTouchListener() { + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { @@ -2885,6 +2813,19 @@ public class Launcher extends Activity } } + /** Updates the interaction state. */ + public void updateInteraction(Workspace.State fromState, Workspace.State toState) { + // Only update the interacting state if we are transitioning to/from a view with an + // overlay + boolean fromStateWithOverlay = fromState != Workspace.State.NORMAL; + boolean toStateWithOverlay = toState != Workspace.State.NORMAL; + if (toStateWithOverlay) { + onInteractionBegin(); + } else if (fromStateWithOverlay) { + onInteractionEnd(); + } + } + void startApplicationDetailsActivity(ComponentName componentName, UserHandleCompat user) { try { LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(this); @@ -2922,7 +2863,7 @@ public class Launcher extends Activity } } - boolean startActivity(View v, Intent intent, Object tag) { + private boolean startActivity(View v, Intent intent, Object tag) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { // Only launch using the new animation if the shortcut has not opted out (this is a @@ -2940,10 +2881,45 @@ public class Launcher extends Activity Bundle optsBundle = null; if (useLaunchAnimation) { - ActivityOptions opts = Utilities.isLmpOrAbove() ? - ActivityOptions.makeCustomAnimation(this, R.anim.task_open_enter, R.anim.no_anim) : - ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight()); - optsBundle = opts.toBundle(); + ActivityOptions opts = null; + if (sClipRevealMethod != null) { + // TODO: call method directly when Launcher3 can depend on M APIs + int left = 0, top = 0; + int width = v.getMeasuredWidth(), height = v.getMeasuredHeight(); + if (v instanceof TextView) { + // Launch from center of icon, not entire view + Drawable icon = Workspace.getTextViewIcon((TextView) v); + if (icon != null) { + Rect bounds = icon.getBounds(); + left = (width - bounds.width()) / 2; + top = v.getPaddingTop(); + width = bounds.width(); + height = bounds.height(); + } + } + try { + opts = (ActivityOptions) sClipRevealMethod.invoke(null, v, + left, top, width, height); + } catch (IllegalAccessException e) { + Log.d(TAG, "Could not call makeClipRevealAnimation: " + e); + sClipRevealMethod = null; + } catch (InvocationTargetException e) { + Log.d(TAG, "Could not call makeClipRevealAnimation: " + e); + sClipRevealMethod = null; + } + } + if (opts == null && !Utilities.isLmpOrAbove()) { + // Below L, we use a scale up animation + opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, + v.getMeasuredWidth(), v.getMeasuredHeight()); + } else if (opts == null && Utilities.isLmpMR1()) { + // On L devices, we use the device default slide-up transition. + // On L MR1 devices, we a custom version of the slide-up transition which + // doesn't have the delay present in the device default. + opts = ActivityOptions.makeCustomAnimation(this, + R.anim.task_open_enter, R.anim.no_anim); + } + optsBundle = opts != null ? opts.toBundle() : null; } if (user == null || user.equals(UserHandleCompat.myUserHandle())) { @@ -2965,7 +2941,7 @@ public class Launcher extends Activity return false; } - boolean startActivitySafely(View v, Intent intent, Object tag) { + @Thunk boolean startActivitySafely(View v, Intent intent, Object tag) { boolean success = false; if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) { Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show(); @@ -3095,10 +3071,19 @@ public class Launcher extends Activity */ public void openFolder(FolderIcon folderIcon) { Folder folder = folderIcon.getFolder(); + Folder openFolder = mWorkspace != null ? mWorkspace.getOpenFolder() : null; + if (openFolder != null && openFolder != folder) { + // Close any open folder before opening a folder. + closeFolder(); + } + FolderInfo info = folder.mInfo; info.opened = true; + // While the folder is open, the position of the icon cannot change. + ((CellLayout.LayoutParams) folderIcon.getLayoutParams()).canReorder = false; + // Just verify that the folder hasn't already been added to the DragLayer. // There was a one-off crash where the folder had a parent already. if (folder.getParent() == null) { @@ -3127,13 +3112,16 @@ public class Launcher extends Activity } } - void closeFolder(Folder folder) { + public void closeFolder(Folder folder) { folder.getInfo().opened = false; ViewGroup parent = (ViewGroup) folder.getParent().getParent(); if (parent != null) { FolderIcon fi = (FolderIcon) mWorkspace.getViewForTag(folder.mInfo); shrinkAndFadeInFolderIcon(fi); + if (fi != null) { + ((CellLayout.LayoutParams) fi.getLayoutParams()).canReorder = true; + } } folder.animateClosed(); @@ -3147,9 +3135,15 @@ public class Launcher extends Activity if (isWorkspaceLocked()) return false; if (mState != State.WORKSPACE) return false; + if (v == mAllAppsButton) { + onLongClickAllAppsButton(v); + return true; + } + if (v instanceof Workspace) { if (!mWorkspace.isInOverviewMode()) { - if (mWorkspace.enterOverviewMode()) { + if (!mWorkspace.isTouchActive()) { + showOverviewMode(true); mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); return true; @@ -3165,7 +3159,7 @@ public class Launcher extends Activity View itemUnderLongClick = null; if (v.getTag() instanceof ItemInfo) { ItemInfo info = (ItemInfo) v.getTag(); - longClickCellInfo = new CellLayout.CellInfo(v, info);; + longClickCellInfo = new CellLayout.CellInfo(v, info); itemUnderLongClick = longClickCellInfo.cell; resetAddInfo(); } @@ -3173,8 +3167,7 @@ public class Launcher extends Activity // The hotseat touch handling does not go through Workspace, and we always allow long press // on hotseat items. final boolean inHotseat = isHotseatLayout(v); - boolean allowLongPress = inHotseat || mWorkspace.allowLongPress(); - if (allowLongPress && !mDragController.isDragging()) { + if (!mDragController.isDragging()) { if (itemUnderLongClick == null) { // User long pressed on empty space mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, @@ -3182,7 +3175,7 @@ public class Launcher extends Activity if (mWorkspace.isInOverviewMode()) { mWorkspace.startReordering(v); } else { - mWorkspace.enterOverviewMode(); + showOverviewMode(true); } } else { final boolean isAllAppsButton = inHotseat && isAllAppsButtonRank( @@ -3206,7 +3199,7 @@ public class Launcher extends Activity /** * Returns the CellLayout of the specified container at the specified screen. */ - CellLayout getCellLayout(long container, long screenId) { + public CellLayout getCellLayout(long container, long screenId) { if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { if (mHotseat != null) { return mHotseat.getLayout(); @@ -3214,17 +3207,36 @@ public class Launcher extends Activity return null; } } else { - return (CellLayout) mWorkspace.getScreenWithId(screenId); + return mWorkspace.getScreenWithId(screenId); } } + /** + * For overridden classes. + */ public boolean isAllAppsVisible() { - return (mState == State.APPS_CUSTOMIZE) || (mOnResumeState == State.APPS_CUSTOMIZE); + return isAppsViewVisible(); } - private void setWorkspaceBackground(boolean workspace) { - mLauncherView.setBackground(workspace ? - mWorkspaceBackgroundDrawable : null); + public boolean isAppsViewVisible() { + return (mState == State.APPS) || (mOnResumeState == State.APPS); + } + + public boolean isWidgetsViewVisible() { + return (mState == State.WIDGETS) || (mOnResumeState == State.WIDGETS); + } + + private void setWorkspaceBackground(int background) { + switch (background) { + case WORKSPACE_BACKGROUND_TRANSPARENT: + getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + break; + case WORKSPACE_BACKGROUND_BLACK: + getWindow().setBackgroundDrawable(null); + break; + default: + getWindow().setBackgroundDrawable(mWorkspaceBackgroundDrawable); + } } protected void changeWallpaperVisiblity(boolean visible) { @@ -3234,577 +3246,7 @@ public class Launcher extends Activity if (wpflags != curflags) { getWindow().setFlags(wpflags, WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); } - setWorkspaceBackground(visible); - } - - private void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionPrepare(this, animated, toWorkspace); - } - } - - private void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionStart(this, animated, toWorkspace); - } - - // Update the workspace transition step as well - dispatchOnLauncherTransitionStep(v, 0f); - } - - private void dispatchOnLauncherTransitionStep(View v, float t) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionStep(this, t); - } - } - - private void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) { - if (v instanceof LauncherTransitionable) { - ((LauncherTransitionable) v).onLauncherTransitionEnd(this, animated, toWorkspace); - } - - // Update the workspace transition step as well - dispatchOnLauncherTransitionStep(v, 1f); - } - - /** - * Things to test when changing the following seven functions. - * - Home from workspace - * - from center screen - * - from other screens - * - Home from all apps - * - from center screen - * - from other screens - * - Back from all apps - * - from center screen - * - from other screens - * - Launch app from workspace and quit - * - with back - * - with home - * - Launch app from all apps and quit - * - with back - * - with home - * - Go to a screen that's not the default, then all - * apps, and launch and app, and go back - * - with back - * -with home - * - On workspace, long press power and go back - * - with back - * - with home - * - On all apps, long press power and go back - * - with back - * - with home - * - On workspace, power off - * - On all apps, power off - * - Launch an app and turn off the screen while in that app - * - Go back with home key - * - Go back with back key TODO: make this not go to workspace - * - From all apps - * - From workspace - * - Enter and exit car mode (becuase it causes an extra configuration changed) - * - From all apps - * - From the center workspace - * - From another workspace - */ - - /** - * Zoom the camera out from the workspace to reveal 'toView'. - * Assumes that the view to show is anchored at either the very top or very bottom - * of the screen. - */ - private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded) { - AppsCustomizePagedView.ContentType contentType = mAppsCustomizeContent.getContentType(); - showAppsCustomizeHelper(animated, springLoaded, contentType); - } - - private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded, - final AppsCustomizePagedView.ContentType contentType) { - if (mStateAnimation != null) { - mStateAnimation.setDuration(0); - mStateAnimation.cancel(); - mStateAnimation = null; - } - - boolean material = Utilities.isLmpOrAbove(); - - final Resources res = getResources(); - - final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime); - final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime); - final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime); - final int itemsAlphaStagger = - res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); - - final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); - final View fromView = mWorkspace; - final AppsCustomizeTabHost toView = mAppsCustomizeTabHost; - - final ArrayList layerViews = new ArrayList(); - - Workspace.State workspaceState = contentType == AppsCustomizePagedView.ContentType.Widgets ? - Workspace.State.OVERVIEW_HIDDEN : Workspace.State.NORMAL_HIDDEN; - Animator workspaceAnim = - mWorkspace.getChangeStateAnimation(workspaceState, animated, layerViews); - if (!LauncherAppState.isDisableAllApps() - || contentType == AppsCustomizePagedView.ContentType.Widgets) { - // Set the content type for the all apps/widgets space - mAppsCustomizeTabHost.setContentTypeImmediate(contentType); - } - - // If for some reason our views aren't initialized, don't animate - boolean initialized = getAllAppsButton() != null; - - if (animated && initialized) { - mStateAnimation = LauncherAnimUtils.createAnimatorSet(); - final AppsCustomizePagedView content = (AppsCustomizePagedView) - toView.findViewById(R.id.apps_customize_pane_content); - - final View page = content.getPageAt(content.getCurrentPage()); - final View revealView = toView.findViewById(R.id.fake_page); - - final boolean isWidgetTray = contentType == AppsCustomizePagedView.ContentType.Widgets; - if (isWidgetTray) { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); - } else { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); - } - - // Hide the real page background, and swap in the fake one - content.setPageBackgroundsVisible(false); - revealView.setVisibility(View.VISIBLE); - // We need to hide this view as the animation start will be posted. - revealView.setAlpha(0); - - int width = revealView.getMeasuredWidth(); - int height = revealView.getMeasuredHeight(); - float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4); - - revealView.setTranslationY(0); - revealView.setTranslationX(0); - - // Get the y delta between the center of the page and the center of the all apps button - int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, - getAllAppsButton(), null); - - float alpha = 0; - float xDrift = 0; - float yDrift = 0; - if (material) { - alpha = isWidgetTray ? 0.3f : 1f; - yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1]; - xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0]; - } else { - yDrift = 2 * height / 3; - xDrift = 0; - } - final float initAlpha = alpha; - - revealView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - layerViews.add(revealView); - PropertyValuesHolder panelAlpha = PropertyValuesHolder.ofFloat("alpha", initAlpha, 1f); - PropertyValuesHolder panelDriftY = - PropertyValuesHolder.ofFloat("translationY", yDrift, 0); - PropertyValuesHolder panelDriftX = - PropertyValuesHolder.ofFloat("translationX", xDrift, 0); - - ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView, - panelAlpha, panelDriftY, panelDriftX); - - panelAlphaAndDrift.setDuration(revealDuration); - panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); - - mStateAnimation.play(panelAlphaAndDrift); - - if (page != null) { - page.setVisibility(View.VISIBLE); - page.setLayerType(View.LAYER_TYPE_HARDWARE, null); - layerViews.add(page); - - ObjectAnimator pageDrift = ObjectAnimator.ofFloat(page, "translationY", yDrift, 0); - page.setTranslationY(yDrift); - pageDrift.setDuration(revealDuration); - pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); - pageDrift.setStartDelay(itemsAlphaStagger); - mStateAnimation.play(pageDrift); - - page.setAlpha(0f); - ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(page, "alpha", 0f, 1f); - itemsAlpha.setDuration(revealDuration); - itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); - itemsAlpha.setStartDelay(itemsAlphaStagger); - mStateAnimation.play(itemsAlpha); - } - - View pageIndicators = toView.findViewById(R.id.apps_customize_page_indicator); - pageIndicators.setAlpha(0.01f); - ObjectAnimator indicatorsAlpha = - ObjectAnimator.ofFloat(pageIndicators, "alpha", 1f); - indicatorsAlpha.setDuration(revealDuration); - mStateAnimation.play(indicatorsAlpha); - - if (material) { - final View allApps = getAllAppsButton(); - int allAppsButtonSize = LauncherAppState.getInstance(). - getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize; - float startRadius = isWidgetTray ? 0 : allAppsButtonSize / 2; - Animator reveal = ViewAnimationUtils.createCircularReveal(revealView, width / 2, - height / 2, startRadius, revealRadius); - reveal.setDuration(revealDuration); - reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); - - reveal.addListener(new AnimatorListenerAdapter() { - public void onAnimationStart(Animator animation) { - if (!isWidgetTray) { - allApps.setVisibility(View.INVISIBLE); - } - } - public void onAnimationEnd(Animator animation) { - if (!isWidgetTray) { - allApps.setVisibility(View.VISIBLE); - } - } - }); - mStateAnimation.play(reveal); - } - - mStateAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - dispatchOnLauncherTransitionEnd(fromView, animated, false); - dispatchOnLauncherTransitionEnd(toView, animated, false); - - revealView.setVisibility(View.INVISIBLE); - revealView.setLayerType(View.LAYER_TYPE_NONE, null); - if (page != null) { - page.setLayerType(View.LAYER_TYPE_NONE, null); - } - content.setPageBackgroundsVisible(true); - - // Hide the search bar - if (mSearchDropTargetBar != null) { - mSearchDropTargetBar.hideSearchBar(false); - } - - // This can hold unnecessary references to views. - mStateAnimation = null; - } - - }); - - if (workspaceAnim != null) { - mStateAnimation.play(workspaceAnim); - } - - dispatchOnLauncherTransitionPrepare(fromView, animated, false); - dispatchOnLauncherTransitionPrepare(toView, animated, false); - final AnimatorSet stateAnimation = mStateAnimation; - final Runnable startAnimRunnable = new Runnable() { - public void run() { - // Check that mStateAnimation hasn't changed while - // we waited for a layout/draw pass - if (mStateAnimation != stateAnimation) - return; - dispatchOnLauncherTransitionStart(fromView, animated, false); - dispatchOnLauncherTransitionStart(toView, animated, false); - - revealView.setAlpha(initAlpha); - if (Utilities.isLmpOrAbove()) { - for (int i = 0; i < layerViews.size(); i++) { - View v = layerViews.get(i); - if (v != null) { - if (Utilities.isViewAttachedToWindow(v)) v.buildLayer(); - } - } - } - mStateAnimation.start(); - } - }; - toView.bringToFront(); - toView.setVisibility(View.VISIBLE); - toView.post(startAnimRunnable); - } else { - toView.setTranslationX(0.0f); - toView.setTranslationY(0.0f); - toView.setScaleX(1.0f); - toView.setScaleY(1.0f); - toView.setVisibility(View.VISIBLE); - toView.bringToFront(); - - if (!springLoaded && !LauncherAppState.getInstance().isScreenLarge()) { - // Hide the search bar - if (mSearchDropTargetBar != null) { - mSearchDropTargetBar.hideSearchBar(false); - } - } - dispatchOnLauncherTransitionPrepare(fromView, animated, false); - dispatchOnLauncherTransitionStart(fromView, animated, false); - dispatchOnLauncherTransitionEnd(fromView, animated, false); - dispatchOnLauncherTransitionPrepare(toView, animated, false); - dispatchOnLauncherTransitionStart(toView, animated, false); - dispatchOnLauncherTransitionEnd(toView, animated, false); - } - } - - /** - * Zoom the camera back into the workspace, hiding 'fromView'. - * This is the opposite of showAppsCustomizeHelper. - * @param animated If true, the transition will be animated. - */ - private void hideAppsCustomizeHelper(Workspace.State toState, final boolean animated, - final boolean springLoaded, final Runnable onCompleteRunnable) { - - if (mStateAnimation != null) { - mStateAnimation.setDuration(0); - mStateAnimation.cancel(); - mStateAnimation = null; - } - - boolean material = Utilities.isLmpOrAbove(); - Resources res = getResources(); - - final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime); - final int fadeOutDuration = res.getInteger(R.integer.config_appsCustomizeFadeOutTime); - final int revealDuration = res.getInteger(R.integer.config_appsCustomizeConcealTime); - final int itemsAlphaStagger = - res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger); - - final float scaleFactor = (float) - res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); - final View fromView = mAppsCustomizeTabHost; - final View toView = mWorkspace; - Animator workspaceAnim = null; - final ArrayList layerViews = new ArrayList(); - - if (toState == Workspace.State.NORMAL) { - workspaceAnim = mWorkspace.getChangeStateAnimation( - toState, animated, layerViews); - } else if (toState == Workspace.State.SPRING_LOADED || - toState == Workspace.State.OVERVIEW) { - workspaceAnim = mWorkspace.getChangeStateAnimation( - toState, animated, layerViews); - } - - // If for some reason our views aren't initialized, don't animate - boolean initialized = getAllAppsButton() != null; - - if (animated && initialized) { - mStateAnimation = LauncherAnimUtils.createAnimatorSet(); - if (workspaceAnim != null) { - mStateAnimation.play(workspaceAnim); - } - - final AppsCustomizePagedView content = (AppsCustomizePagedView) - fromView.findViewById(R.id.apps_customize_pane_content); - - final View page = content.getPageAt(content.getNextPage()); - - // We need to hide side pages of the Apps / Widget tray to avoid some ugly edge cases - int count = content.getChildCount(); - for (int i = 0; i < count; i++) { - View child = content.getChildAt(i); - if (child != page) { - child.setVisibility(View.INVISIBLE); - } - } - final View revealView = fromView.findViewById(R.id.fake_page); - - // hideAppsCustomizeHelper is called in some cases when it is already hidden - // don't perform all these no-op animations. In particularly, this was causing - // the all-apps button to pop in and out. - if (fromView.getVisibility() == View.VISIBLE) { - AppsCustomizePagedView.ContentType contentType = content.getContentType(); - final boolean isWidgetTray = - contentType == AppsCustomizePagedView.ContentType.Widgets; - - if (isWidgetTray) { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark)); - } else { - revealView.setBackground(res.getDrawable(R.drawable.quantum_panel)); - } - - int width = revealView.getMeasuredWidth(); - int height = revealView.getMeasuredHeight(); - float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4); - - // Hide the real page background, and swap in the fake one - revealView.setVisibility(View.VISIBLE); - content.setPageBackgroundsVisible(false); - - final View allAppsButton = getAllAppsButton(); - revealView.setTranslationY(0); - int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, - allAppsButton, null); - - float xDrift = 0; - float yDrift = 0; - if (material) { - yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1]; - xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0]; - } else { - yDrift = 2 * height / 3; - xDrift = 0; - } - - revealView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - TimeInterpolator decelerateInterpolator = material ? - new LogDecelerateInterpolator(100, 0) : - new DecelerateInterpolator(1f); - - // The vertical motion of the apps panel should be delayed by one frame - // from the conceal animation in order to give the right feel. We correpsondingly - // shorten the duration so that the slide and conceal end at the same time. - ObjectAnimator panelDriftY = LauncherAnimUtils.ofFloat(revealView, "translationY", - 0, yDrift); - panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY); - panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); - panelDriftY.setInterpolator(decelerateInterpolator); - mStateAnimation.play(panelDriftY); - - ObjectAnimator panelDriftX = LauncherAnimUtils.ofFloat(revealView, "translationX", - 0, xDrift); - panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY); - panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); - panelDriftX.setInterpolator(decelerateInterpolator); - mStateAnimation.play(panelDriftX); - - if (isWidgetTray || !material) { - float finalAlpha = material ? 0.4f : 0f; - revealView.setAlpha(1f); - ObjectAnimator panelAlpha = LauncherAnimUtils.ofFloat(revealView, "alpha", - 1f, finalAlpha); - panelAlpha.setDuration(material ? revealDuration : 150); - panelAlpha.setInterpolator(decelerateInterpolator); - panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); - mStateAnimation.play(panelAlpha); - } - - if (page != null) { - page.setLayerType(View.LAYER_TYPE_HARDWARE, null); - - ObjectAnimator pageDrift = LauncherAnimUtils.ofFloat(page, "translationY", - 0, yDrift); - page.setTranslationY(0); - pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY); - pageDrift.setInterpolator(decelerateInterpolator); - pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); - mStateAnimation.play(pageDrift); - - page.setAlpha(1f); - ObjectAnimator itemsAlpha = LauncherAnimUtils.ofFloat(page, "alpha", 1f, 0f); - itemsAlpha.setDuration(100); - itemsAlpha.setInterpolator(decelerateInterpolator); - mStateAnimation.play(itemsAlpha); - } - - View pageIndicators = fromView.findViewById(R.id.apps_customize_page_indicator); - pageIndicators.setAlpha(1f); - ObjectAnimator indicatorsAlpha = - LauncherAnimUtils.ofFloat(pageIndicators, "alpha", 0f); - indicatorsAlpha.setDuration(revealDuration); - indicatorsAlpha.setInterpolator(new DecelerateInterpolator(1.5f)); - mStateAnimation.play(indicatorsAlpha); - - width = revealView.getMeasuredWidth(); - - if (material) { - if (!isWidgetTray) { - allAppsButton.setVisibility(View.INVISIBLE); - } - int allAppsButtonSize = LauncherAppState.getInstance(). - getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize; - float finalRadius = isWidgetTray ? 0 : allAppsButtonSize / 2; - Animator reveal = - LauncherAnimUtils.createCircularReveal(revealView, width / 2, - height / 2, revealRadius, finalRadius); - reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); - reveal.setDuration(revealDuration); - reveal.setStartDelay(itemsAlphaStagger); - - reveal.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - revealView.setVisibility(View.INVISIBLE); - if (!isWidgetTray) { - allAppsButton.setVisibility(View.VISIBLE); - } - } - }); - - mStateAnimation.play(reveal); - } - - dispatchOnLauncherTransitionPrepare(fromView, animated, true); - dispatchOnLauncherTransitionPrepare(toView, animated, true); - mAppsCustomizeContent.stopScrolling(); - } - - mStateAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - fromView.setVisibility(View.GONE); - dispatchOnLauncherTransitionEnd(fromView, animated, true); - dispatchOnLauncherTransitionEnd(toView, animated, true); - if (onCompleteRunnable != null) { - onCompleteRunnable.run(); - } - - revealView.setLayerType(View.LAYER_TYPE_NONE, null); - if (page != null) { - page.setLayerType(View.LAYER_TYPE_NONE, null); - } - content.setPageBackgroundsVisible(true); - // Unhide side pages - int count = content.getChildCount(); - for (int i = 0; i < count; i++) { - View child = content.getChildAt(i); - child.setVisibility(View.VISIBLE); - } - - // Reset page transforms - if (page != null) { - page.setTranslationX(0); - page.setTranslationY(0); - page.setAlpha(1); - } - content.setCurrentPage(content.getNextPage()); - - mAppsCustomizeContent.updateCurrentPageScroll(); - - // This can hold unnecessary references to views. - mStateAnimation = null; - } - }); - - final AnimatorSet stateAnimation = mStateAnimation; - final Runnable startAnimRunnable = new Runnable() { - public void run() { - // Check that mStateAnimation hasn't changed while - // we waited for a layout/draw pass - if (mStateAnimation != stateAnimation) - return; - dispatchOnLauncherTransitionStart(fromView, animated, false); - dispatchOnLauncherTransitionStart(toView, animated, false); - - if (Utilities.isLmpOrAbove()) { - for (int i = 0; i < layerViews.size(); i++) { - View v = layerViews.get(i); - if (v != null) { - if (Utilities.isViewAttachedToWindow(v)) v.buildLayer(); - } - } - } - mStateAnimation.start(); - } - }; - fromView.post(startAnimRunnable); - } else { - fromView.setVisibility(View.GONE); - dispatchOnLauncherTransitionPrepare(fromView, animated, true); - dispatchOnLauncherTransitionStart(fromView, animated, true); - dispatchOnLauncherTransitionEnd(fromView, animated, true); - dispatchOnLauncherTransitionPrepare(toView, animated, true); - dispatchOnLauncherTransitionStart(toView, animated, true); - dispatchOnLauncherTransitionEnd(toView, animated, true); - } + setWorkspaceBackground(visible ? WORKSPACE_BACKGROUND_GRADIENT : WORKSPACE_BACKGROUND_BLACK); } @Override @@ -3816,25 +3258,42 @@ public class Launcher extends Activity SQLiteDatabase.releaseMemory(); // This clears all widget bitmaps from the widget tray - if (mAppsCustomizeTabHost != null) { - mAppsCustomizeTabHost.trimMemory(); - } + // TODO(hyunyoungs) + } + if (mLauncherCallbacks != null) { + mLauncherCallbacks.onTrimMemory(level); } } - protected void showWorkspace(boolean animated) { - showWorkspace(animated, null); + @Override + public void onStateTransitionHideSearchBar() { + // Hide the search bar + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.hideSearchBar(false /* animated */); + } } - protected void showWorkspace() { - showWorkspace(true); + public void showWorkspace(boolean animated) { + showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, null); } - void showWorkspace(boolean animated, Runnable onCompleteRunnable) { - if (mState != State.WORKSPACE || mWorkspace.getState() != Workspace.State.NORMAL) { + public void showWorkspace(boolean animated, Runnable onCompleteRunnable) { + showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, + onCompleteRunnable); + } + + protected void showWorkspace(int snapToPage, boolean animated) { + showWorkspace(snapToPage, animated, null); + } + + void showWorkspace(int snapToPage, boolean animated, Runnable onCompleteRunnable) { + boolean changed = mState != State.WORKSPACE || + mWorkspace.getState() != Workspace.State.NORMAL; + if (changed) { boolean wasInSpringLoadedMode = (mState != State.WORKSPACE); mWorkspace.setVisibility(View.VISIBLE); - hideAppsCustomizeHelper(Workspace.State.NORMAL, animated, false, onCompleteRunnable); + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.NORMAL, + snapToPage, animated, onCompleteRunnable); // Show the search bar (only animate if we were showing the drop target bar in spring // loaded mode) @@ -3853,73 +3312,131 @@ public class Launcher extends Activity // Resume the auto-advance of widgets mUserPresent = true; - updateRunning(); + updateAutoAdvanceState(); - // Send an accessibility event to announce the context change - getWindow().getDecorView() - .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - - onWorkspaceShown(animated); + if (changed) { + // Send an accessibility event to announce the context change + getWindow().getDecorView() + .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } } void showOverviewMode(boolean animated) { mWorkspace.setVisibility(View.VISIBLE); - hideAppsCustomizeHelper(Workspace.State.OVERVIEW, animated, false, null); + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.OVERVIEW, + WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, + null /* onCompleteRunnable */); mState = State.WORKSPACE; - onWorkspaceShown(animated); } - public void onWorkspaceShown(boolean animated) { - } - - void showAllApps(boolean animated, AppsCustomizePagedView.ContentType contentType, - boolean resetPageToZero) { - if (mState != State.WORKSPACE) return; - - if (resetPageToZero) { - mAppsCustomizeTabHost.reset(); + /** + * Shows the apps view. + */ + void showAppsView(boolean animated, boolean resetListToTop, boolean updatePredictedApps, + boolean focusSearchBar) { + if (resetListToTop) { + mAppsView.scrollToTop(); } - showAppsCustomizeHelper(animated, false, contentType); - mAppsCustomizeTabHost.post(new Runnable() { + if (updatePredictedApps) { + tryAndUpdatePredictedApps(); + } + showAppsOrWidgets(State.APPS, animated, focusSearchBar); + } + + /** + * Shows the widgets view. + */ + void showWidgetsView(boolean animated, boolean resetPageToZero) { + if (LOGD) Log.d(TAG, "showWidgetsView:" + animated + " resetPageToZero:" + resetPageToZero); + if (resetPageToZero) { + mWidgetsView.scrollToTop(); + } + showAppsOrWidgets(State.WIDGETS, animated, false); + + mWidgetsView.post(new Runnable() { @Override public void run() { - // We post this in-case the all apps view isn't yet constructed. - mAppsCustomizeTabHost.requestFocus(); + mWidgetsView.requestFocus(); } }); + } + + /** + * Sets up the transition to show the apps/widgets view. + * + * @return whether the current from and to state allowed this operation + */ + // TODO: calling method should use the return value so that when {@code false} is returned + // the workspace transition doesn't fall into invalid state. + private boolean showAppsOrWidgets(State toState, boolean animated, boolean focusSearchBar) { + if (mState != State.WORKSPACE && mState != State.APPS_SPRING_LOADED && + mState != State.WIDGETS_SPRING_LOADED) { + return false; + } + if (toState != State.APPS && toState != State.WIDGETS) { + return false; + } + + if (toState == State.APPS) { + mStateTransitionAnimation.startAnimationToAllApps(animated, focusSearchBar); + } else { + mStateTransitionAnimation.startAnimationToWidgets(animated); + } // Change the state *after* we've called all the transition code - mState = State.APPS_CUSTOMIZE; + mState = toState; // Pause the auto-advance of widgets until we are out of AllApps mUserPresent = false; - updateRunning(); + updateAutoAdvanceState(); closeFolder(); // Send an accessibility event to announce the context change getWindow().getDecorView() .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + return true; } - void enterSpringLoadedDragMode() { - if (isAllAppsVisible()) { - hideAppsCustomizeHelper(Workspace.State.SPRING_LOADED, true, true, null); - mState = State.APPS_CUSTOMIZE_SPRING_LOADED; + /** + * Updates the workspace and interaction state on state change, and return the animation to this + * new state. + */ + public Animator startWorkspaceStateChangeAnimation(Workspace.State toState, int toPage, + boolean animated, boolean hasOverlaySearchBar, HashMap layerViews) { + Workspace.State fromState = mWorkspace.getState(); + Animator anim = mWorkspace.setStateWithAnimation(toState, toPage, animated, + hasOverlaySearchBar, layerViews); + updateInteraction(fromState, toState); + return anim; + } + + public void enterSpringLoadedDragMode() { + if (LOGD) Log.d(TAG, String.format("enterSpringLoadedDragMode [mState=%s", mState.name())); + if (mState == State.WORKSPACE || mState == State.APPS_SPRING_LOADED || + mState == State.WIDGETS_SPRING_LOADED) { + return; } + + mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.SPRING_LOADED, + WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, true /* animated */, + null /* onCompleteRunnable */); + mState = isAppsViewVisible() ? State.APPS_SPRING_LOADED : State.WIDGETS_SPRING_LOADED; } - void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay, + public void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay, final Runnable onCompleteRunnable) { - if (mState != State.APPS_CUSTOMIZE_SPRING_LOADED) return; + if (mState != State.APPS_SPRING_LOADED && mState != State.WIDGETS_SPRING_LOADED) return; mHandler.postDelayed(new Runnable() { @Override public void run() { if (successfulDrop) { + // TODO(hyunyoungs): verify if this hack is still needed, if not, delete. + // // Before we show workspace, hide all apps again because // exitSpringLoadedDragMode made it visible. This is a bit hacky; we should // clean up our state transition functions - mAppsCustomizeTabHost.setVisibility(View.GONE); + mWidgetsView.setVisibility(View.GONE); showWorkspace(true, onCompleteRunnable); } else { exitSpringLoadedDragMode(); @@ -3929,13 +3446,25 @@ public class Launcher extends Activity } void exitSpringLoadedDragMode() { - if (mState == State.APPS_CUSTOMIZE_SPRING_LOADED) { - final boolean animated = true; - final boolean springLoaded = true; - showAppsCustomizeHelper(animated, springLoaded); - mState = State.APPS_CUSTOMIZE; + if (mState == State.APPS_SPRING_LOADED) { + showAppsView(true /* animated */, false /* resetListToTop */, + false /* updatePredictedApps */, false /* focusSearchBar */); + } else if (mState == State.WIDGETS_SPRING_LOADED) { + showWidgetsView(true, false); + } + } + + /** + * Updates the set of predicted apps if it hasn't been updated since the last time Launcher was + * resumed. + */ + private void tryAndUpdatePredictedApps() { + if (mLauncherCallbacks != null) { + List apps = mLauncherCallbacks.getPredictedApps(); + if (apps != null) { + mAppsView.setPredictedApps(apps); + } } - // Otherwise, we are not in spring loaded mode, so don't do anything. } void lockAllApps() { @@ -3950,7 +3479,7 @@ public class Launcher extends Activity // NO-OP } - public View getQsbBar() { + public View getOrCreateQsbBar() { if (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()) { return mLauncherCallbacks.getQsbBar(); } @@ -3994,24 +3523,39 @@ public class Launcher extends Activity .commit(); } + mAppWidgetHost.setQsbWidgetId(widgetId); if (widgetId != -1) { mQsb = mAppWidgetHost.createView(this, widgetId, searchProvider); mQsb.updateAppWidgetOptions(opts); mQsb.setPadding(0, 0, 0, 0); mSearchDropTargetBar.addView(mQsb); + mSearchDropTargetBar.setQsbSearchBar(mQsb); } } return mQsb; } + private void reinflateQSBIfNecessary() { + if (mQsb instanceof LauncherAppWidgetHostView && + ((LauncherAppWidgetHostView) mQsb).isReinflateRequired()) { + mSearchDropTargetBar.removeView(mQsb); + mQsb = null; + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); + } + } + @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { final boolean result = super.dispatchPopulateAccessibilityEvent(event); final List text = event.getText(); text.clear(); // Populate event with a fake title based on the current state. - if (mState == State.APPS_CUSTOMIZE) { - text.add(mAppsCustomizeTabHost.getContentTag()); + if (mState == State.APPS) { + text.add(getString(R.string.all_apps_button_label)); + } else if (mState == State.WIDGETS) { + text.add(getString(R.string.widget_button_text)); + } else if (mWorkspace != null) { + text.add(mWorkspace.getCurrentPageDescription()); } else { text.add(getString(R.string.all_apps_home_button_label)); } @@ -4021,44 +3565,29 @@ public class Launcher extends Activity /** * Receives notifications when system dialogs are to be closed. */ - private class CloseSystemDialogsIntentReceiver extends BroadcastReceiver { + @Thunk class CloseSystemDialogsIntentReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { closeSystemDialogs(); } } - /** - * Receives notifications whenever the appwidgets are reset. - */ - private class AppWidgetResetObserver extends ContentObserver { - public AppWidgetResetObserver() { - super(new Handler()); - } - - @Override - public void onChange(boolean selfChange) { - onAppWidgetReset(); - } - } - /** * If the activity is currently paused, signal that we need to run the passed Runnable * in onResume. * * This needs to be called from incoming places where resources might have been loaded - * while we are paused. That is becaues the Configuration might be wrong - * when we're not running, and if it comes back to what it was when we - * were paused, we are not restarted. + * while the activity is paused. That is because the Configuration (e.g., rotation) might be + * wrong when we're not running, and if the activity comes back to what the configuration was + * when we were paused, activity is not restarted. * * Implementation of the method from LauncherModel.Callbacks. * - * @return true if we are currently paused. The caller might be able to - * skip some work in that case since we will come back again. + * @return {@code true} if we are currently paused. The caller might be able to skip some work */ - private boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) { + @Thunk boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) { if (mPaused) { - Log.i(TAG, "Deferring update until onResume"); + if (LOGD) Log.d(TAG, "Deferring update until onResume"); if (deletePreviousRunnables) { while (mBindOnResumeCallbacks.remove(run)) { } @@ -4094,7 +3623,7 @@ public class Launcher extends Activity */ public boolean setLoadOnResume() { if (mPaused) { - Log.i(TAG, "setLoadOnResume"); + if (LOGD) Log.d(TAG, "setLoadOnResume"); mOnResumeNeedsLoad = true; return true; } else { @@ -4155,10 +3684,6 @@ public class Launcher extends Activity @Override public void bindAddScreens(ArrayList orderedScreenIds) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - bindAddScreens()", true); - Launcher.addDumpLog(TAG, "11683562 - orderedScreenIds: " + - TextUtils.join(", ", orderedScreenIds), true); int count = orderedScreenIds.size(); for (int i = 0; i < count; i++) { mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(orderedScreenIds.get(i)); @@ -4221,9 +3746,8 @@ public class Launcher extends Activity // Remove the extra empty screen mWorkspace.removeExtraEmptyScreen(false, false); - if (!LauncherAppState.isDisableAllApps() && - addedApps != null && mAppsCustomizeContent != null) { - mAppsCustomizeContent.addApps(addedApps); + if (addedApps != null && mAppsView != null) { + mAppsView.addApps(addedApps); } } @@ -4339,7 +3863,7 @@ public class Launcher extends Activity /** * Implementation of the method from LauncherModel.Callbacks. */ - public void bindFolders(final HashMap folders) { + public void bindFolders(final LongArrayMap folders) { Runnable r = new Runnable() { public void run() { bindFolders(folders); @@ -4348,8 +3872,7 @@ public class Launcher extends Activity if (waitUntilResume(r)) { return; } - sFolders.clear(); - sFolders.putAll(folders); + sFolders = folders.clone(); } /** @@ -4373,12 +3896,12 @@ public class Launcher extends Activity } final Workspace workspace = mWorkspace; - AppWidgetProviderInfo appWidgetInfo; + LauncherAppWidgetProviderInfo appWidgetInfo = + LauncherModel.getProviderInfo(this, item.providerName, item.user); + if (!mIsSafeModeEnabled && ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0) && ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) != 0)) { - - appWidgetInfo = mModel.findAppWidgetProviderInfoWithComponent(this, item.providerName); if (appWidgetInfo == null) { if (DEBUG_WIDGETS) { Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId @@ -4391,13 +3914,13 @@ public class Launcher extends Activity // Note: This assumes that the id remap broadcast is received before this step. // If that is not the case, the id remap will be ignored and user may see the // click to setup view. - PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(appWidgetInfo, null, null); + PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(this, appWidgetInfo, null); pendingInfo.spanX = item.spanX; pendingInfo.spanY = item.spanY; pendingInfo.minSpanX = item.minSpanX; pendingInfo.minSpanY = item.minSpanY; - Bundle options = - AppsCustomizePagedView.getDefaultOptionsForWidget(this, pendingInfo); + Bundle options = null; + WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo); int newWidgetId = mAppWidgetHost.allocateAppWidgetId(); boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( @@ -4428,9 +3951,9 @@ public class Launcher extends Activity if (!mIsSafeModeEnabled && item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { final int appWidgetId = item.appWidgetId; - appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); if (DEBUG_WIDGETS) { - Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider); + Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + + appWidgetInfo.provider); } item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); @@ -4449,7 +3972,9 @@ public class Launcher extends Activity workspace.addInScreen(item.hostView, item.container, item.screenId, item.cellX, item.cellY, item.spanX, item.spanY, false); - addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); + if (!item.isCustomWidget()) { + addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); + } workspace.requestLayout(); @@ -4488,10 +4013,10 @@ public class Launcher extends Activity * * Implementation of the method from LauncherModel.Callbacks. */ - public void finishBindingItems(final boolean upgradePath) { + public void finishBindingItems() { Runnable r = new Runnable() { public void run() { - finishBindingItems(upgradePath); + finishBindingItems(); } }; if (waitUntilResume(r)) { @@ -4526,14 +4051,10 @@ public class Launcher extends Activity sPendingAddItem = null; } - if (upgradePath) { - mWorkspace.getUniqueComponents(true, null); - mIntentsOnWorkspaceFromUpgradePath = mWorkspace.getUniqueComponents(true, null); - } - PackageInstallerCompat.getInstance(this).onFinishBind(); + InstallShortcutReceiver.disableAndFlushInstallQueue(this); if (mLauncherCallbacks != null) { - mLauncherCallbacks.finishBindingItems(upgradePath); + mLauncherCallbacks.finishBindingItems(false); } } @@ -4568,18 +4089,16 @@ public class Launcher extends Activity PropertyValuesHolder.ofFloat("scaleY", 1f)); bounceAnim.setDuration(InstallShortcutReceiver.NEW_SHORTCUT_BOUNCE_DURATION); bounceAnim.setStartDelay(i * InstallShortcutReceiver.NEW_SHORTCUT_STAGGER_DELAY); - bounceAnim.setInterpolator(new SmoothPagedView.OvershootInterpolator()); + bounceAnim.setInterpolator(new OvershootInterpolator(BOUNCE_ANIMATION_TENSION)); return bounceAnim; } public boolean useVerticalBarLayout() { - return LauncherAppState.getInstance().getDynamicGrid(). - getDeviceProfile().isVerticalBarLayout(); + return mDeviceProfile.isVerticalBarLayout(); } protected Rect getSearchBarBounds() { - return LauncherAppState.getInstance().getDynamicGrid(). - getDeviceProfile().getSearchBarBounds(); + return mDeviceProfile.getSearchBarBounds(Utilities.isRtl(getResources())); } public void bindSearchablesChanged() { @@ -4590,33 +4109,34 @@ public class Launcher extends Activity mSearchDropTargetBar.removeView(mQsb); mQsb = null; } - mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); + mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar()); } + /** + * A runnable that we can dequeue and re-enqueue when all applications are bound (to prevent + * multiple calls to bind the same list.) + */ + @Thunk ArrayList mTmpAppsList; + private Runnable mBindAllApplicationsRunnable = new Runnable() { + public void run() { + bindAllApplications(mTmpAppsList); + mTmpAppsList = null; + } + }; + /** * Add the icons for all apps. * * Implementation of the method from LauncherModel.Callbacks. */ public void bindAllApplications(final ArrayList apps) { - if (LauncherAppState.isDisableAllApps()) { - if (mIntentsOnWorkspaceFromUpgradePath != null) { - if (LauncherModel.UPGRADE_USE_MORE_APPS_FOLDER) { - getHotseat().addAllAppsFolder(mIconCache, apps, - mIntentsOnWorkspaceFromUpgradePath, Launcher.this, mWorkspace); - } - mIntentsOnWorkspaceFromUpgradePath = null; - } - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.onPackagesUpdated( - LauncherModel.getSortedWidgetsAndShortcuts(this)); - } - } else { - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.setApps(apps); - mAppsCustomizeContent.onPackagesUpdated( - LauncherModel.getSortedWidgetsAndShortcuts(this)); - } + if (waitUntilResume(mBindAllApplicationsRunnable, true)) { + mTmpAppsList = apps; + return; + } + + if (mAppsView != null) { + mAppsView.setApps(apps); } if (mLauncherCallbacks != null) { mLauncherCallbacks.bindAllApplications(apps); @@ -4638,9 +4158,8 @@ public class Launcher extends Activity return; } - if (!LauncherAppState.isDisableAllApps() && - mAppsCustomizeContent != null) { - mAppsCustomizeContent.updateApps(apps); + if (mAppsView != null) { + mAppsView.updateApps(apps); } } @@ -4695,22 +4214,17 @@ public class Launcher extends Activity * Implementation of the method from LauncherModel.Callbacks. */ @Override - public void updatePackageState(ArrayList installInfo) { - if (mWorkspace != null) { - mWorkspace.updatePackageState(installInfo); + public void bindRestoreItemsChange(final HashSet updates) { + Runnable r = new Runnable() { + public void run() { + bindRestoreItemsChange(updates); + } + }; + if (waitUntilResume(r)) { + return; } - } - /** - * Update the label and icon of all the icons in a package - * - * Implementation of the method from LauncherModel.Callbacks. - */ - @Override - public void updatePackageBadge(String packageName) { - if (mWorkspace != null) { - mWorkspace.updatePackageBadge(packageName, UserHandleCompat.myUserHandle()); - } + mWorkspace.updateRestoreItems(updates); } /** @@ -4754,31 +4268,27 @@ public class Launcher extends Activity } // Update AllApps - if (!LauncherAppState.isDisableAllApps() && - mAppsCustomizeContent != null) { - mAppsCustomizeContent.removeApps(appInfos); + if (mAppsView != null) { + mAppsView.removeApps(appInfos); } } - /** - * A number of packages were updated. - */ - private ArrayList mWidgetsAndShortcuts; private Runnable mBindPackagesUpdatedRunnable = new Runnable() { public void run() { - bindPackagesUpdated(mWidgetsAndShortcuts); - mWidgetsAndShortcuts = null; + bindAllPackages(mWidgetsModel); } }; - public void bindPackagesUpdated(final ArrayList widgetsAndShortcuts) { + + @Override + public void bindAllPackages(final WidgetsModel model) { if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) { - mWidgetsAndShortcuts = widgetsAndShortcuts; + mWidgetsModel = model; return; } - // Update the widgets pane - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.onPackagesUpdated(widgetsAndShortcuts); + if (mWidgetsView != null && model != null) { + mWidgetsView.addWidgets(model); + mWidgetsModel = null; } } @@ -4815,13 +4325,18 @@ public class Launcher extends Activity } public void lockScreenOrientation() { - if (Utilities.isRotationEnabled(this)) { - setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources() - .getConfiguration().orientation)); + if (mRotationEnabled) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources() + .getConfiguration().orientation)); + } else { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); + } } } + public void unlockScreenOrientation(boolean immediate) { - if (Utilities.isRotationEnabled(this)) { + if (mRotationEnabled) { if (immediate) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } else { @@ -4984,7 +4499,7 @@ public class Launcher extends Activity editor.apply(); } - private void showFirstRunClings() { + @Thunk void showFirstRunClings() { // The two first run cling paths are mutually exclusive, if the launcher is preinstalled // on the device, then we always show the first run cling experience (or if there is no // launcher2). Otherwise, we prompt the user upon started for migration @@ -5012,42 +4527,54 @@ public class Launcher extends Activity if (mSearchDropTargetBar != null) mSearchDropTargetBar.hideSearchBar(false); } + // TODO: These method should be a part of LauncherSearchCallback + @TargetApi(Build.VERSION_CODES.LOLLIPOP) public ItemInfo createAppDragInfo(Intent appLaunchIntent) { - // Called from search suggestion, not supported in other profiles. - final UserHandleCompat myUser = UserHandleCompat.myUserHandle(); + // Called from search suggestion + UserHandleCompat user = null; + if (Utilities.isLmpOrAbove()) { + UserHandle userHandle = appLaunchIntent.getParcelableExtra(Intent.EXTRA_USER); + if (userHandle != null) { + user = UserHandleCompat.fromUser(userHandle); + } + } + return createAppDragInfo(appLaunchIntent, user); + } + + // TODO: This method should be a part of LauncherSearchCallback + public ItemInfo createAppDragInfo(Intent intent, UserHandleCompat user) { + if (user == null) { + user = UserHandleCompat.myUserHandle(); + } + + // Called from search suggestion, add the profile extra to the intent to ensure that we + // can launch it correctly LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(this); - LauncherActivityInfoCompat activityInfo = launcherApps.resolveActivity(appLaunchIntent, - myUser); + LauncherActivityInfoCompat activityInfo = launcherApps.resolveActivity(intent, user); if (activityInfo == null) { return null; } - return new AppInfo(this, activityInfo, myUser, mIconCache, null); + return new AppInfo(this, activityInfo, user, mIconCache); } + // TODO: This method should be a part of LauncherSearchCallback public ItemInfo createShortcutDragInfo(Intent shortcutIntent, CharSequence caption, Bitmap icon) { - // Called from search suggestion, not supported in other profiles. - return createShortcutDragInfo(shortcutIntent, caption, icon, + return new ShortcutInfo(shortcutIntent, caption, caption, icon, UserHandleCompat.myUserHandle()); } - public ItemInfo createShortcutDragInfo(Intent shortcutIntent, CharSequence caption, - Bitmap icon, UserHandleCompat user) { - UserManagerCompat userManager = UserManagerCompat.getInstance(this); - CharSequence contentDescription = userManager.getBadgedLabelForUser(caption, user); - return new ShortcutInfo(shortcutIntent, caption, contentDescription, icon, user); - } - - protected void moveWorkspaceToDefaultScreen() { - mWorkspace.moveToDefaultScreen(false); - } - + // TODO: This method should be a part of LauncherSearchCallback public void startDrag(View dragView, ItemInfo dragInfo, DragSource source) { dragView.setTag(dragInfo); mWorkspace.onExternalDragStartedWithItem(dragView); mWorkspace.beginExternalDragShared(dragView, source); } + protected void moveWorkspaceToDefaultScreen() { + mWorkspace.moveToDefaultScreen(false); + } + @Override public void onPageSwitch(View newPage, int newPageIndex) { if (mLauncherCallbacks != null) { @@ -5055,6 +4582,23 @@ public class Launcher extends Activity } } + /** + * Returns a FastBitmapDrawable with the icon, accurately sized. + */ + public FastBitmapDrawable createIconDrawable(Bitmap icon) { + FastBitmapDrawable d = new FastBitmapDrawable(icon); + d.setFilterBitmap(true); + resizeIconDrawable(d); + return d; + } + + /** + * Resizes an icon drawable to the correct icon size. + */ + public void resizeIconDrawable(Drawable icon) { + icon.setBounds(0, 0, mDeviceProfile.iconSizePx, mDeviceProfile.iconSizePx); + } + /** * Prints out out state for debugging. */ @@ -5067,10 +4611,8 @@ public class Launcher extends Activity Log.d(TAG, "mSavedInstanceState=" + mSavedInstanceState); Log.d(TAG, "sFolders.size=" + sFolders.size()); mModel.dumpState(); + // TODO(hyunyoungs): add mWidgetsView.dumpState(); or mWidgetsModel.dumpState(); - if (mAppsCustomizeContent != null) { - mAppsCustomizeContent.dumpState(); - } Log.d(TAG, "END launcher3 dump state"); } @@ -5125,6 +4667,14 @@ public class Launcher extends Activity } } + public static CustomAppWidget getCustomAppWidget(String name) { + return sCustomAppWidgets.get(name); + } + + public static HashMap getCustomAppWidgets() { + return sCustomAppWidgets; + } + public void dumpLogsToLocalData() { if (DEBUG_DUMP_LOG) { new AsyncTask() { @@ -5173,14 +4723,6 @@ public class Launcher extends Activity } } -interface LauncherTransitionable { - View getContent(); - void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace); - void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace); - void onLauncherTransitionStep(Launcher l, float t); - void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace); -} - interface DebugIntents { static final String DELETE_DATABASE = "com.android.launcher3.action.DELETE_DATABASE"; static final String MIGRATE_DATABASE = "com.android.launcher3.action.MIGRATE_DATABASE"; diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index be295f8b3e..6a248a3325 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -21,10 +21,14 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.os.Build; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewTreeObserver; +import com.android.launcher3.util.UiThreadCircularReveal; + import java.util.HashSet; import java.util.WeakHashMap; @@ -128,13 +132,12 @@ public class LauncherAnimUtils { return anim; } - public static Animator createCircularReveal(View view, int centerX, + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static ValueAnimator createCircularReveal(View view, int centerX, int centerY, float startRadius, float endRadius) { - Animator anim = ViewAnimationUtils.createCircularReveal(view, centerX, + ValueAnimator anim = UiThreadCircularReveal.createCircularReveal(view, centerX, centerY, startRadius, endRadius); - if (anim instanceof ValueAnimator) { - new FirstFrameAnimatorHelper((ValueAnimator) anim, view); - } + new FirstFrameAnimatorHelper(anim, view); return anim; } } diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index b7c45a3401..0b7b1fdc48 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -16,44 +16,28 @@ package com.android.launcher3; -import android.annotation.TargetApi; import android.app.SearchManager; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.graphics.Point; -import android.os.Build; -import android.os.Handler; -import android.util.DisplayMetrics; import android.util.Log; -import android.view.Display; -import android.view.WindowManager; + +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat; -import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; +import com.android.launcher3.util.Thunk; + import java.lang.ref.WeakReference; -import java.util.ArrayList; -public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { - private static final String TAG = "LauncherAppState"; - - private static final boolean DEBUG = false; +public class LauncherAppState { private final AppFilter mAppFilter; private final BuildInfo mBuildInfo; - private final LauncherModel mModel; + @Thunk final LauncherModel mModel; private final IconCache mIconCache; + private final WidgetPreviewLoader mWidgetCache; - private final boolean mIsScreenLarge; - private final float mScreenDensity; - private final int mLongPressTimeout = 300; - - private WidgetPreviewLoader.CacheDb mWidgetPreviewCacheDb; private boolean mWallpaperChangedSinceLastCheck; private static WeakReference sLauncherProvider; @@ -61,7 +45,9 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { private static LauncherAppState INSTANCE; - private DynamicGrid mDynamicGrid; + private InvariantDeviceProfile mInvariantDeviceProfile; + + private LauncherAccessibilityDelegate mAccessibilityDelegate; public static LauncherAppState getInstance() { if (INSTANCE == null) { @@ -96,42 +82,26 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { MemoryTracker.startTrackingMe(sContext, "L"); } - // set sIsScreenXLarge and mScreenDensity *before* creating icon cache - mIsScreenLarge = isScreenLarge(sContext.getResources()); - mScreenDensity = sContext.getResources().getDisplayMetrics().density; - - recreateWidgetPreviewDb(); - mIconCache = new IconCache(sContext); + mInvariantDeviceProfile = new InvariantDeviceProfile(sContext); + mIconCache = new IconCache(sContext, mInvariantDeviceProfile); + mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache); mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class)); mBuildInfo = BuildInfo.loadByName(sContext.getString(R.string.build_info_class)); mModel = new LauncherModel(this, mIconCache, mAppFilter); - final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext); - launcherApps.addOnAppsChangedCallback(mModel); + + LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel); // Register intent receivers IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_LOCALE_CHANGED); - filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - sContext.registerReceiver(mModel, filter); - filter = new IntentFilter(); filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); - sContext.registerReceiver(mModel, filter); - filter = new IntentFilter(); filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); + // For handling managed profiles + filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED); + filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED); + sContext.registerReceiver(mModel, filter); - - // Register for changes to the favorites - ContentResolver resolver = sContext.getContentResolver(); - resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, - mFavoritesObserver); - } - - public void recreateWidgetPreviewDb() { - if (mWidgetPreviewCacheDb != null) { - mWidgetPreviewCacheDb.close(); - } - mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(sContext); } /** @@ -142,45 +112,37 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext); launcherApps.removeOnAppsChangedCallback(mModel); PackageInstallerCompat.getInstance(sContext).onStop(); - - ContentResolver resolver = sContext.getContentResolver(); - resolver.unregisterContentObserver(mFavoritesObserver); } /** - * Receives notifications whenever the user favorites have changed. + * Reloads the workspace items from the DB and re-binds the workspace. This should generally + * not be called as DB updates are automatically followed by UI update */ - private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - // If the database has ever changed, then we really need to force a reload of the - // workspace on the next load - mModel.resetLoadedState(false, true); - mModel.startLoaderFromBackground(); - } - }; + public void reloadWorkspace() { + mModel.resetLoadedState(false, true); + mModel.startLoaderFromBackground(); + } LauncherModel setLauncher(Launcher launcher) { + getLauncherProvider().setLauncherProviderChangeListener(launcher); mModel.initialize(launcher); + mAccessibilityDelegate = ((launcher != null) && Utilities.isLmpOrAbove()) ? + new LauncherAccessibilityDelegate(launcher) : null; return mModel; } + public LauncherAccessibilityDelegate getAccessibilityDelegate() { + return mAccessibilityDelegate; + } + public IconCache getIconCache() { return mIconCache; } - LauncherModel getModel() { + public LauncherModel getModel() { return mModel; } - boolean shouldShowAppOrWidgetProvider(ComponentName componentName) { - return mAppFilter == null || mAppFilter.shouldShowApp(componentName); - } - - WidgetPreviewLoader.CacheDb getWidgetPreviewCacheDb() { - return mWidgetPreviewCacheDb; - } - static void setLauncherProvider(LauncherProvider provider) { sLauncherProvider = new WeakReference(provider); } @@ -193,71 +155,10 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return LauncherFiles.SHARED_PREFERENCES_KEY; } - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - DeviceProfile initDynamicGrid(Context context) { - mDynamicGrid = createDynamicGrid(context, mDynamicGrid); - mDynamicGrid.getDeviceProfile().addCallback(this); - return mDynamicGrid.getDeviceProfile(); + public WidgetPreviewLoader getWidgetCache() { + return mWidgetCache; } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - static DynamicGrid createDynamicGrid(Context context, DynamicGrid dynamicGrid) { - // Determine the dynamic grid properties - WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - - Point realSize = new Point(); - display.getRealSize(realSize); - DisplayMetrics dm = new DisplayMetrics(); - display.getMetrics(dm); - - if (dynamicGrid == null) { - Point smallestSize = new Point(); - Point largestSize = new Point(); - display.getCurrentSizeRange(smallestSize, largestSize); - - dynamicGrid = new DynamicGrid(context, - context.getResources(), - Math.min(smallestSize.x, smallestSize.y), - Math.min(largestSize.x, largestSize.y), - realSize.x, realSize.y, - dm.widthPixels, dm.heightPixels); - } - - // Update the icon size - DeviceProfile grid = dynamicGrid.getDeviceProfile(); - grid.updateFromConfiguration(context, context.getResources(), - realSize.x, realSize.y, - dm.widthPixels, dm.heightPixels); - return dynamicGrid; - } - - public DynamicGrid getDynamicGrid() { - return mDynamicGrid; - } - - public boolean isScreenLarge() { - return mIsScreenLarge; - } - - // Need a version that doesn't require an instance of LauncherAppState for the wallpaper picker - public static boolean isScreenLarge(Resources res) { - return res.getBoolean(R.bool.is_large_tablet); - } - - public static boolean isScreenLandscape(Context context) { - return context.getResources().getConfiguration().orientation == - Configuration.ORIENTATION_LANDSCAPE; - } - - public float getScreenDensity() { - return mScreenDensity; - } - - public int getLongPressTimeout() { - return mLongPressTimeout; - } - + public void onWallpaperChanged() { mWallpaperChangedSinceLastCheck = true; } @@ -268,29 +169,11 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return result; } - @Override - public void onAvailableSizeChanged(DeviceProfile grid) { - Utilities.setIconSize(grid.iconSizePx); - } - - public static boolean isDisableAllApps() { - // Returns false on non-dogfood builds. - return getInstance().mBuildInfo.isDogfoodBuild() && - Utilities.isPropertyEnabled(Launcher.DISABLE_ALL_APPS_PROPERTY); + public InvariantDeviceProfile getInvariantDeviceProfile() { + return mInvariantDeviceProfile; } public static boolean isDogfoodBuild() { return getInstance().mBuildInfo.isDogfoodBuild(); } - - public void setPackageState(ArrayList installInfo) { - mModel.setPackageState(installInfo); - } - - /** - * Updates the icons and label of all icons for the provided package name. - */ - public void updatePackageBadge(String packageName) { - mModel.updatePackageBadge(packageName); - } } diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java index a309f268c8..de7c610008 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/LauncherAppWidgetHost.java @@ -21,9 +21,12 @@ import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.os.TransactionTooLargeException; +import android.view.LayoutInflater; +import android.view.View; import java.util.ArrayList; + /** * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView} * which correctly captures all long-press events. This ensures that users can @@ -33,16 +36,31 @@ public class LauncherAppWidgetHost extends AppWidgetHost { private final ArrayList mProviderChangeListeners = new ArrayList(); - Launcher mLauncher; + private int mQsbWidgetId = -1; + private Launcher mLauncher; public LauncherAppWidgetHost(Launcher launcher, int hostId) { super(launcher, hostId); mLauncher = launcher; } + public void setQsbWidgetId(int widgetId) { + mQsbWidgetId = widgetId; + } + @Override protected AppWidgetHostView onCreateView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { + if (appWidgetId == mQsbWidgetId) { + return new LauncherAppWidgetHostView(context) { + + @Override + protected View getErrorView() { + // For the QSB, show an empty view instead of an error view. + return new View(getContext()); + } + }; + } return new LauncherAppWidgetHostView(context); } @@ -77,12 +95,37 @@ public class LauncherAppWidgetHost extends AppWidgetHost { } protected void onProvidersChanged() { - // Once we get the message that widget packages are updated, we need to rebind items - // in AppsCustomize accordingly. - mLauncher.bindPackagesUpdated(LauncherModel.getSortedWidgetsAndShortcuts(mLauncher)); - - for (Runnable callback : mProviderChangeListeners) { - callback.run(); + mLauncher.getModel().loadAndBindWidgetsAndShortcuts(mLauncher, mLauncher, + true /* refresh */); + if (!mProviderChangeListeners.isEmpty()) { + for (Runnable callback : new ArrayList<>(mProviderChangeListeners)) { + callback.run(); + } } } + + public AppWidgetHostView createView(Context context, int appWidgetId, + LauncherAppWidgetProviderInfo appWidget) { + if (appWidget.isCustomWidget) { + LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context); + LayoutInflater inflater = (LayoutInflater) + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(appWidget.initialLayout, lahv); + lahv.setAppWidget(0, appWidget); + lahv.updateLastInflationOrientation(); + return lahv; + } else { + return super.createView(context, appWidgetId, appWidget); + } + } + + /** + * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk. + */ + @Override + protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) { + LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo( + mLauncher, appWidget); + super.onProviderChanged(appWidgetId, info); + } } diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java index e39727b175..cf461a5b82 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java @@ -17,6 +17,7 @@ package com.android.launcher3; import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -35,6 +36,7 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc LayoutInflater mInflater; private CheckLongPressHelper mLongPressHelper; + private StylusEventHelper mStylusEventHelper; private Context mContext; private int mPreviousOrientation; private DragLayer mDragLayer; @@ -45,8 +47,10 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc super(context); mContext = context; mLongPressHelper = new CheckLongPressHelper(this); + mStylusEventHelper = new StylusEventHelper(this); mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mDragLayer = ((Launcher) context).getDragLayer(); + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); } @Override @@ -54,10 +58,14 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc return mInflater.inflate(R.layout.appwidget_error, this, false); } + public void updateLastInflationOrientation() { + mPreviousOrientation = mContext.getResources().getConfiguration().orientation; + } + @Override public void updateAppWidget(RemoteViews remoteViews) { // Store the orientation in which the widget was inflated - mPreviousOrientation = mContext.getResources().getConfiguration().orientation; + updateLastInflationOrientation(); super.updateAppWidget(remoteViews); } @@ -83,11 +91,17 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc return true; } - // Watch for longpress events at this level to make sure - // users can always pick up this widget + // Watch for longpress or stylus button press events at this level to + // make sure users can always pick up this widget + if (mStylusEventHelper.checkAndPerformStylusEvent(ev)) { + mLongPressHelper.cancelLongPress(); + return true; + } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { - mLongPressHelper.postCheckForLongPress(); + if (!mStylusEventHelper.inStylusButtonPressed()) { + mLongPressHelper.postCheckForLongPress(); + } mDragLayer.setTouchCompleteListener(this); break; } @@ -136,6 +150,20 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc mLongPressHelper.cancelLongPress(); } + @Override + public AppWidgetProviderInfo getAppWidgetInfo() { + AppWidgetProviderInfo info = super.getAppWidgetInfo(); + if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) { + throw new IllegalStateException("Launcher widget must have" + + " LauncherAppWidgetProviderInfo"); + } + return info; + } + + public LauncherAppWidgetProviderInfo getLauncherAppWidgetProviderInfo() { + return (LauncherAppWidgetProviderInfo) getAppWidgetInfo(); + } + @Override public void onTouchComplete() { if (!mLongPressHelper.hasPerformedLongPress()) { diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java index 5c6535a243..aad18b578e 100644 --- a/src/com/android/launcher3/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java @@ -55,6 +55,11 @@ public class LauncherAppWidgetInfo extends ItemInfo { */ static final int NO_ID = -1; + /** + * Indicates that this is a locally defined widget and hence has no system allocated id. + */ + static final int CUSTOM_WIDGET_ID = -100; + /** * Identifier for this widget when talking with * {@link android.appwidget.AppWidgetManager} for updates. @@ -86,7 +91,12 @@ public class LauncherAppWidgetInfo extends ItemInfo { AppWidgetHostView hostView = null; LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) { - itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + if (appWidgetId == CUSTOM_WIDGET_ID) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } else { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + } + this.appWidgetId = appWidgetId; this.providerName = providerName; @@ -99,6 +109,10 @@ public class LauncherAppWidgetInfo extends ItemInfo { restoreStatus = RESTORE_COMPLETED; } + public boolean isCustomWidget() { + return appWidgetId == CUSTOM_WIDGET_ID; + } + @Override void onAddToDatabase(Context context, ContentValues values) { super.onAddToDatabase(context, values); diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java new file mode 100644 index 0000000000..85af92f30a --- /dev/null +++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java @@ -0,0 +1,113 @@ +package com.android.launcher3; + +import android.annotation.TargetApi; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; + +/** + * This class is a thin wrapper around the framework AppWidgetProviderInfo class. This class affords + * a common object for describing both framework provided AppWidgets as well as custom widgets + * (who's implementation is owned by the launcher). This object represents a widget type / class, + * as opposed to a widget instance, and so should not be confused with {@link LauncherAppWidgetInfo} + */ +public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { + + public boolean isCustomWidget = false; + + private int mSpanX = -1; + private int mSpanY = -1; + private int mMinSpanX = -1; + private int mMinSpanY = -1; + + public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context, + AppWidgetProviderInfo info) { + + // In lieu of a public super copy constructor, we first write the AppWidgetProviderInfo + // into a parcel, and then construct a new LauncherAppWidgetProvider info from the + // associated super parcel constructor. This allows us to copy non-public members without + // using reflection. + Parcel p = Parcel.obtain(); + info.writeToParcel(p, 0); + p.setDataPosition(0); + LauncherAppWidgetProviderInfo lawpi = new LauncherAppWidgetProviderInfo(p); + p.recycle(); + return lawpi; + } + + public LauncherAppWidgetProviderInfo(Parcel in) { + super(in); + } + + public LauncherAppWidgetProviderInfo(Context context, CustomAppWidget widget) { + isCustomWidget = true; + + provider = new ComponentName(context, widget.getClass().getName()); + icon = widget.getIcon(); + label = widget.getLabel(); + previewImage = widget.getPreviewImage(); + initialLayout = widget.getWidgetLayout(); + resizeMode = widget.getResizeMode(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public String getLabel(PackageManager packageManager) { + if (isCustomWidget) { + return Utilities.trim(label); + } + return super.loadLabel(packageManager); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Drawable getIcon(Context context, IconCache cache) { + if (isCustomWidget) { + return cache.getFullResIcon(provider.getPackageName(), icon); + } + return super.loadIcon(context, + LauncherAppState.getInstance().getInvariantDeviceProfile().fillResIconDpi); + } + + public String toString(PackageManager pm) { + if (isCustomWidget) { + return "WidgetProviderInfo(" + provider + ")"; + } + return String.format("WidgetProviderInfo provider:%s package:%s short:%s label:%s", + provider.toString(), provider.getPackageName(), provider.getShortClassName(), getLabel(pm)); + } + + public int getSpanX(Launcher launcher) { + lazyLoadSpans(launcher); + return mSpanX; + } + + public int getSpanY(Launcher launcher) { + lazyLoadSpans(launcher); + return mSpanY; + } + + public int getMinSpanX(Launcher launcher) { + lazyLoadSpans(launcher); + return mMinSpanX; + } + + public int getMinSpanY(Launcher launcher) { + lazyLoadSpans(launcher); + return mMinSpanY; + } + + private void lazyLoadSpans(Launcher launcher) { + if (mSpanX < 0 || mSpanY < 0 || mMinSpanX < 0 || mMinSpanY < 0) { + int[] minResizeSpan = launcher.getMinSpanForWidget(this); + int[] span = launcher.getSpanForWidget(this); + + mSpanX = span[0]; + mSpanY = span[1]; + mMinSpanX = minResizeSpan[0]; + mMinSpanY = minResizeSpan[1]; + } + } + } diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java index 3868a57f13..a92a889f9a 100644 --- a/src/com/android/launcher3/LauncherBackupAgentHelper.java +++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java @@ -32,7 +32,7 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { private static final String LAUNCHER_DATA_PREFIX = "L"; - static final boolean VERBOSE = true; + static final boolean VERBOSE = false; static final boolean DEBUG = false; private static BackupManager sBackupManager; @@ -78,7 +78,7 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { super.onRestore(data, appVersionCode, newState); // If no favorite was migrated, clear the data and start fresh. final Cursor c = getContentResolver().query( - LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, null, null, null, null); + LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); hasData = c.moveToNext(); c.close(); } catch (Exception e) { @@ -90,6 +90,12 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { if (hasData && mHelper.restoreSuccessful) { LauncherAppState.getLauncherProvider().clearFlagEmptyDbCreated(); LauncherClings.synchonouslyMarkFirstRunClingDismissed(this); + + // TODO: Update the backup set to include rank. + if (mHelper.restoredBackupVersion <= 3) { + LauncherAppState.getLauncherProvider().updateFolderItemsRank(); + LauncherAppState.getLauncherProvider().convertShortcutsToLauncherActivities(); + } } else { if (VERBOSE) Log.v(TAG, "Nothing was restored, clearing DB"); LauncherAppState.getLauncherProvider().createEmptyDB(); diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index 4374347488..8c6fedbdc2 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -19,14 +19,16 @@ import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; import android.app.backup.BackupHelper; import android.app.backup.BackupManager; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.XmlResourceParser; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -49,18 +51,20 @@ import com.android.launcher3.backup.BackupProtos.Screen; import com.android.launcher3.backup.BackupProtos.Widget; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.Thunk; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; -import java.io.ByteArrayOutputStream; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.zip.CRC32; /** @@ -71,7 +75,7 @@ public class LauncherBackupHelper implements BackupHelper { private static final boolean VERBOSE = LauncherBackupAgentHelper.VERBOSE; private static final boolean DEBUG = LauncherBackupAgentHelper.DEBUG; - private static final int BACKUP_VERSION = 2; + private static final int BACKUP_VERSION = 3; private static final int MAX_JOURNAL_SIZE = 1000000; // Journal key is such that it is always smaller than any dynamically generated @@ -84,11 +88,6 @@ public class LauncherBackupHelper implements BackupHelper { /** widgets contain previews, which are very large, dribble them out */ private static final int MAX_WIDGETS_PER_PASS = 5; - private static final int IMAGE_COMPRESSION_QUALITY = 75; - - private static final Bitmap.CompressFormat IMAGE_FORMAT = - android.graphics.Bitmap.CompressFormat.PNG; - private static final String[] FAVORITE_PROJECTION = { Favorites._ID, // 0 Favorites.MODIFIED, // 1 @@ -136,24 +135,33 @@ public class LauncherBackupHelper implements BackupHelper { private static final int SCREEN_RANK_INDEX = 2; - private final Context mContext; + @Thunk final Context mContext; private final HashSet mExistingKeys; private final ArrayList mKeys; + private final ItemTypeMatcher[] mItemTypeMatchers; + private final long mUserSerial; - private IconCache mIconCache; private BackupManager mBackupManager; - private HashMap mWidgetMap; private byte[] mBuffer = new byte[512]; private long mLastBackupRestoreTime; + private boolean mBackupDataWasUpdated; + + private IconCache mIconCache; + private DeviceProfieData mDeviceProfileData; + private InvariantDeviceProfile mIdp; - private DeviceProfieData mCurrentProfile; boolean restoreSuccessful; + int restoredBackupVersion = 1; public LauncherBackupHelper(Context context) { mContext = context; mExistingKeys = new HashSet(); mKeys = new ArrayList(); restoreSuccessful = true; + mItemTypeMatchers = new ItemTypeMatcher[CommonAppTypeParser.SUPPORTED_TYPE_COUNT]; + + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + mUserSerial = userManager.getSerialNumberForUser(UserHandleCompat.myUserHandle()); } private void dataChanged() { @@ -171,6 +179,7 @@ public class LauncherBackupHelper implements BackupHelper { mExistingKeys.add(keyToBackupKey(key)); } } + restoredBackupVersion = journal.backupVersion; } /** @@ -191,10 +200,19 @@ public class LauncherBackupHelper implements BackupHelper { Journal in = readJournal(oldState); if (!launcherIsReady()) { + dataChanged(); // Perform backup later. writeJournal(newState, in); return; } + + if (mDeviceProfileData == null) { + LauncherAppState app = LauncherAppState.getInstance(); + mIdp = app.getInvariantDeviceProfile(); + mDeviceProfileData = initDeviceProfileData(mIdp); + mIconCache = app.getIconCache(); + } + Log.v(TAG, "lastBackupTime = " + in.t); mKeys.clear(); applyJournal(in); @@ -202,7 +220,7 @@ public class LauncherBackupHelper implements BackupHelper { // Record the time before performing backup so that entries edited while the backup // was going on, do not get missed in next backup. long newBackupTime = System.currentTimeMillis(); - + mBackupDataWasUpdated = false; try { backupFavorites(data); backupScreens(data); @@ -220,16 +238,30 @@ public class LauncherBackupHelper implements BackupHelper { for (String deleted: mExistingKeys) { if (VERBOSE) Log.v(TAG, "dropping deleted item " + deleted); data.writeEntityHeader(deleted, -1); + mBackupDataWasUpdated = true; } mExistingKeys.clear(); - mLastBackupRestoreTime = newBackupTime; + if (!mBackupDataWasUpdated) { + // Check if any metadata has changed + mBackupDataWasUpdated = (in.profile == null) + || !Arrays.equals(DeviceProfieData.toByteArray(in.profile), + DeviceProfieData.toByteArray(mDeviceProfileData)) + || (in.backupVersion != BACKUP_VERSION) + || (in.appVersion != getAppVersion()); + } - // We store the journal at two places. - // 1) Storing it in newState allows us to do partial backups by comparing old state - // 2) Storing it in backup data allows us to validate keys during restore - Journal state = getCurrentStateJournal(); - writeRowToBackup(JOURNAL_KEY, state, data); + if (mBackupDataWasUpdated) { + mLastBackupRestoreTime = newBackupTime; + + // We store the journal at two places. + // 1) Storing it in newState allows us to do partial backups by comparing old state + // 2) Storing it in backup data allows us to validate keys during restore + Journal state = getCurrentStateJournal(); + writeRowToBackup(JOURNAL_KEY, state, data); + } else { + if (DEBUG) Log.d(TAG, "Nothing was written during backup"); + } } catch (IOException e) { Log.e(TAG, "launcher backup has failed", e); } @@ -242,8 +274,7 @@ public class LauncherBackupHelper implements BackupHelper { * to this device. */ private boolean isBackupCompatible(Journal oldState) { - DeviceProfieData currentProfile = getDeviceProfieData(); - + DeviceProfieData currentProfile = mDeviceProfileData; DeviceProfieData oldProfile = oldState.profile; if (oldProfile == null || oldProfile.desktopCols == 0) { @@ -277,6 +308,14 @@ public class LauncherBackupHelper implements BackupHelper { return; } + if (mDeviceProfileData == null) { + // This call does not happen on a looper thread. So LauncherAppState + // can't be created . Instead initialize required dependencies directly. + mIdp = new InvariantDeviceProfile(mContext); + mDeviceProfileData = initDeviceProfileData(mIdp); + mIconCache = new IconCache(mContext, mIdp); + } + int dataSize = data.size(); if (mBuffer.length < dataSize) { mBuffer = new byte[dataSize]; @@ -351,7 +390,7 @@ public class LauncherBackupHelper implements BackupHelper { journal.key = mKeys.toArray(new BackupProtos.Key[mKeys.size()]); journal.appVersion = getAppVersion(); journal.backupVersion = BACKUP_VERSION; - journal.profile = getDeviceProfieData(); + journal.profile = mDeviceProfileData; return journal; } @@ -364,23 +403,13 @@ public class LauncherBackupHelper implements BackupHelper { } } - /** - * @return the current device profile information. - */ - private DeviceProfieData getDeviceProfieData() { - if (mCurrentProfile != null) { - return mCurrentProfile; - } - final Context applicationContext = mContext.getApplicationContext(); - DeviceProfile profile = LauncherAppState.createDynamicGrid(applicationContext, null) - .getDeviceProfile(); - - mCurrentProfile = new DeviceProfieData(); - mCurrentProfile.desktopRows = profile.numRows; - mCurrentProfile.desktopCols = profile.numColumns; - mCurrentProfile.hotseatCount = profile.numHotseatIcons; - mCurrentProfile.allappsRank = profile.hotseatAllAppsRank; - return mCurrentProfile; + private DeviceProfieData initDeviceProfileData(InvariantDeviceProfile profile) { + DeviceProfieData data = new DeviceProfieData(); + data.desktopRows = profile.numRows; + data.desktopCols = profile.numColumns; + data.hotseatCount = profile.numHotseatIcons; + data.allappsRank = profile.hotseatAllAppsRank; + return data; } /** @@ -430,7 +459,7 @@ public class LauncherBackupHelper implements BackupHelper { ContentResolver cr = mContext.getContentResolver(); ContentValues values = unpackFavorite(buffer, dataSize); - cr.insert(Favorites.CONTENT_URI_NO_NOTIFICATION, values); + cr.insert(Favorites.CONTENT_URI, values); } /** @@ -491,11 +520,6 @@ public class LauncherBackupHelper implements BackupHelper { */ private void backupIcons(BackupDataOutput data) throws IOException { // persist icons that haven't been persisted yet - if (!initializeIconCache()) { - dataChanged(); // try again later - if (DEBUG) Log.d(TAG, "Launcher is not initialized, delaying icon backup"); - return; - } final ContentResolver cr = mContext.getContentResolver(); final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; final UserHandleCompat myUserHandle = UserHandleCompat.myUserHandle(); @@ -579,7 +603,8 @@ public class LauncherBackupHelper implements BackupHelper { Log.w(TAG, "failed to unpack icon for " + key.name); } if (VERBOSE) Log.v(TAG, "saving restored icon as: " + key.name); - IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(key.name), icon, res.dpi); + mIconCache.preloadIcon(ComponentName.unflattenFromString(key.name), icon, res.dpi, + "" /* label */, mUserSerial); } /** @@ -591,17 +616,8 @@ public class LauncherBackupHelper implements BackupHelper { */ private void backupWidgets(BackupDataOutput data) throws IOException { // persist static widget info that hasn't been persisted yet - final LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); - if (appState == null || !initializeIconCache()) { - Log.w(TAG, "Failed to get icon cache during restore"); - return; - } final ContentResolver cr = mContext.getContentResolver(); - final WidgetPreviewLoader previewLoader = new WidgetPreviewLoader(mContext); - final PagedViewCellLayout widgetSpacingLayout = new PagedViewCellLayout(mContext); final int dpi = mContext.getResources().getDisplayMetrics().densityDpi; - final DeviceProfile profile = appState.getDynamicGrid().getDeviceProfile(); - if (DEBUG) Log.d(TAG, "cellWidthPx: " + profile.cellWidthPx); int backupWidgetCount = 0; String where = Favorites.ITEM_TYPE + "=" + Favorites.ITEM_TYPE_APPWIDGET + " AND " @@ -613,8 +629,6 @@ public class LauncherBackupHelper implements BackupHelper { while(cursor.moveToNext()) { final long id = cursor.getLong(ID_INDEX); final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX); - final int spanX = cursor.getInt(SPANX_INDEX); - final int spanY = cursor.getInt(SPANY_INDEX); final ComponentName provider = ComponentName.unflattenFromString(providerName); Key key = null; String backupKey = null; @@ -624,7 +638,7 @@ public class LauncherBackupHelper implements BackupHelper { } else { Log.w(TAG, "empty intent on appwidget: " + id); } - if (mExistingKeys.contains(backupKey)) { + if (mExistingKeys.contains(backupKey) && restoredBackupVersion >= BACKUP_VERSION) { if (DEBUG) Log.d(TAG, "already saved widget " + backupKey); // remember that we already backed this up previously @@ -633,9 +647,8 @@ public class LauncherBackupHelper implements BackupHelper { if (DEBUG) Log.d(TAG, "I can count this high: " + backupWidgetCount); if (backupWidgetCount < MAX_WIDGETS_PER_PASS) { if (DEBUG) Log.d(TAG, "saving widget " + backupKey); - previewLoader.setPreviewSize(spanX * profile.cellWidthPx, - spanY * profile.cellHeightPx, widgetSpacingLayout); - writeRowToBackup(key, packWidget(dpi, previewLoader, mIconCache, provider), data); + UserHandleCompat user = UserHandleCompat.myUserHandle(); + writeRowToBackup(key, packWidget(dpi, provider, user), data); mKeys.add(key); backupWidgetCount ++; } else { @@ -671,8 +684,8 @@ public class LauncherBackupHelper implements BackupHelper { if (icon == null) { Log.w(TAG, "failed to unpack widget icon for " + key.name); } else { - IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(widget.provider), - icon, widget.icon.dpi); + mIconCache.preloadIcon(ComponentName.unflattenFromString(widget.provider), + icon, widget.icon.dpi, widget.label, mUserSerial); } } @@ -738,6 +751,17 @@ public class LauncherBackupHelper implements BackupHelper { return checksum.getValue(); } + /** + * @return true if its an hotseat item, that can be replaced during restore. + * TODO: Extend check for folders in hotseat. + */ + private boolean isReplaceableHotseatItem(Favorite favorite) { + return favorite.container == Favorites.CONTAINER_HOTSEAT + && favorite.intent != null + && (favorite.itemType == Favorites.ITEM_TYPE_APPLICATION + || favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT); + } + /** Serialize a Favorite for persistence, including a checksum wrapper. */ private Favorite packFavorite(Cursor c) { Favorite favorite = new Favorite(); @@ -749,30 +773,16 @@ public class LauncherBackupHelper implements BackupHelper { favorite.spanX = c.getInt(SPANX_INDEX); favorite.spanY = c.getInt(SPANY_INDEX); favorite.iconType = c.getInt(ICON_TYPE_INDEX); - if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { - String iconPackage = c.getString(ICON_PACKAGE_INDEX); - if (!TextUtils.isEmpty(iconPackage)) { - favorite.iconPackage = iconPackage; - } - String iconResource = c.getString(ICON_RESOURCE_INDEX); - if (!TextUtils.isEmpty(iconResource)) { - favorite.iconResource = iconResource; - } - } - if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) { - byte[] blob = c.getBlob(ICON_INDEX); - if (blob != null && blob.length > 0) { - favorite.icon = blob; - } - } + String title = c.getString(TITLE_INDEX); if (!TextUtils.isEmpty(title)) { favorite.title = title; } String intentDescription = c.getString(INTENT_INDEX); + Intent intent = null; if (!TextUtils.isEmpty(intentDescription)) { try { - Intent intent = Intent.parseUri(intentDescription, 0); + intent = Intent.parseUri(intentDescription, 0); intent.removeExtra(ItemInfo.EXTRA_PROFILE); favorite.intent = intent.toUri(0); } catch (URISyntaxException e) { @@ -786,6 +796,47 @@ public class LauncherBackupHelper implements BackupHelper { if (!TextUtils.isEmpty(appWidgetProvider)) { favorite.appWidgetProvider = appWidgetProvider; } + } else if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) { + if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { + String iconPackage = c.getString(ICON_PACKAGE_INDEX); + if (!TextUtils.isEmpty(iconPackage)) { + favorite.iconPackage = iconPackage; + } + String iconResource = c.getString(ICON_RESOURCE_INDEX); + if (!TextUtils.isEmpty(iconResource)) { + favorite.iconResource = iconResource; + } + } + + byte[] blob = c.getBlob(ICON_INDEX); + if (blob != null && blob.length > 0) { + favorite.icon = blob; + } + } + + if (isReplaceableHotseatItem(favorite)) { + if (intent != null && intent.getComponent() != null) { + PackageManager pm = mContext.getPackageManager(); + ActivityInfo activity = null;; + try { + activity = pm.getActivityInfo(intent.getComponent(), 0); + } catch (NameNotFoundException e) { + Log.e(TAG, "Target not found", e); + } + if (activity == null) { + return favorite; + } + for (int i = 0; i < mItemTypeMatchers.length; i++) { + if (mItemTypeMatchers[i] == null) { + mItemTypeMatchers[i] = new ItemTypeMatcher( + CommonAppTypeParser.getResourceForItemType(i)); + } + if (mItemTypeMatchers[i].matches(activity, pm)) { + favorite.targetType = i; + break; + } + } + } } return favorite; @@ -795,6 +846,7 @@ public class LauncherBackupHelper implements BackupHelper { private ContentValues unpackFavorite(byte[] buffer, int dataSize) throws IOException { Favorite favorite = unpackProto(new Favorite(), buffer, dataSize); + ContentValues values = new ContentValues(); values.put(Favorites._ID, favorite.id); values.put(Favorites.SCREEN, favorite.screen); @@ -803,14 +855,16 @@ public class LauncherBackupHelper implements BackupHelper { values.put(Favorites.CELLY, favorite.cellY); values.put(Favorites.SPANX, favorite.spanX); values.put(Favorites.SPANY, favorite.spanY); - values.put(Favorites.ICON_TYPE, favorite.iconType); - if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { - values.put(Favorites.ICON_PACKAGE, favorite.iconPackage); - values.put(Favorites.ICON_RESOURCE, favorite.iconResource); - } - if (favorite.iconType == Favorites.ICON_TYPE_BITMAP) { + + if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) { + values.put(Favorites.ICON_TYPE, favorite.iconType); + if (favorite.iconType == Favorites.ICON_TYPE_RESOURCE) { + values.put(Favorites.ICON_PACKAGE, favorite.iconPackage); + values.put(Favorites.ICON_RESOURCE, favorite.iconResource); + } values.put(Favorites.ICON, favorite.icon); } + if (!TextUtils.isEmpty(favorite.title)) { values.put(Favorites.TITLE, favorite.title); } else { @@ -826,7 +880,7 @@ public class LauncherBackupHelper implements BackupHelper { UserManagerCompat.getInstance(mContext).getSerialNumberForUser(myUserHandle); values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber); - DeviceProfieData currentProfile = getDeviceProfieData(); + DeviceProfieData currentProfile = mDeviceProfileData; if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) { if (!TextUtils.isEmpty(favorite.appWidgetProvider)) { @@ -845,8 +899,17 @@ public class LauncherBackupHelper implements BackupHelper { throw new InvalidBackupException("Widget not in screen bounds, aborting restore"); } } else { - // Let LauncherModel know we've been here. - values.put(LauncherSettings.Favorites.RESTORED, 1); + // Check if it is an hotseat item, that can be replaced. + if (isReplaceableHotseatItem(favorite) + && favorite.targetType != Favorite.TARGET_NONE + && favorite.targetType < CommonAppTypeParser.SUPPORTED_TYPE_COUNT) { + Log.e(TAG, "Added item type flag"); + values.put(LauncherSettings.Favorites.RESTORED, + 1 | CommonAppTypeParser.encodeItemTypeToFlag(favorite.targetType)); + } else { + // Let LauncherModel know we've been here. + values.put(LauncherSettings.Favorites.RESTORED, 1); + } // Verify placement if (favorite.container == Favorites.CONTAINER_HOTSEAT) { @@ -889,40 +952,35 @@ public class LauncherBackupHelper implements BackupHelper { private Resource packIcon(int dpi, Bitmap icon) { Resource res = new Resource(); res.dpi = dpi; - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { - res.data = os.toByteArray(); - } + res.data = Utilities.flattenBitmap(icon); return res; } /** Serialize a widget for persistence, including a checksum wrapper. */ - private Widget packWidget(int dpi, WidgetPreviewLoader previewLoader, IconCache iconCache, - ComponentName provider) { - final AppWidgetProviderInfo info = findAppWidgetProviderInfo(provider); + private Widget packWidget(int dpi, ComponentName provider, UserHandleCompat user) { + final LauncherAppWidgetProviderInfo info = + LauncherModel.getProviderInfo(mContext, provider, user); Widget widget = new Widget(); widget.provider = provider.flattenToShortString(); widget.label = info.label; widget.configure = info.configure != null; if (info.icon != 0) { widget.icon = new Resource(); - Drawable fullResIcon = iconCache.getFullResIcon(provider.getPackageName(), info.icon); + Drawable fullResIcon = mIconCache.getFullResIcon(provider.getPackageName(), info.icon); Bitmap icon = Utilities.createIconBitmap(fullResIcon, mContext); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (icon.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { - widget.icon.data = os.toByteArray(); - widget.icon.dpi = dpi; - } - } - if (info.previewImage != 0) { - widget.preview = new Resource(); - Bitmap preview = previewLoader.generateWidgetPreview(info, null); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - if (preview.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) { - widget.preview.data = os.toByteArray(); - widget.preview.dpi = dpi; - } + widget.icon.data = Utilities.flattenBitmap(icon); + widget.icon.dpi = dpi; } + + // Calculate the spans corresponding to any one of the orientations as it should not change + // based on orientation. + int[] minSpans = CellLayout.rectToCell( + mIdp.portraitProfile, mContext, info.minResizeWidth, info.minResizeHeight, null); + widget.minSpanX = (info.resizeMode & LauncherAppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0 + ? minSpans[0] : -1; + widget.minSpanY = (info.resizeMode & LauncherAppWidgetProviderInfo.RESIZE_VERTICAL) != 0 + ? minSpans[1] : -1; + return widget; } @@ -1019,6 +1077,7 @@ public class LauncherBackupHelper implements BackupHelper { byte[] blob = writeCheckedBytes(proto); data.writeEntityHeader(backupKey, blob.length); data.writeEntityData(blob, blob.length); + mBackupDataWasUpdated = true; if (VERBOSE) Log.v(TAG, "Writing New entry " + backupKey); } @@ -1067,36 +1126,6 @@ public class LauncherBackupHelper implements BackupHelper { return wrapper.payload; } - private AppWidgetProviderInfo findAppWidgetProviderInfo(ComponentName component) { - if (mWidgetMap == null) { - List widgets = - AppWidgetManager.getInstance(mContext).getInstalledProviders(); - mWidgetMap = new HashMap(widgets.size()); - for (AppWidgetProviderInfo info : widgets) { - mWidgetMap.put(info.provider, info); - } - } - return mWidgetMap.get(component); - } - - - private boolean initializeIconCache() { - if (mIconCache != null) { - return true; - } - - final LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); - if (appState == null) { - Throwable stackTrace = new Throwable(); - stackTrace.fillInStackTrace(); - Log.w(TAG, "Failed to get app state during backup/restore", stackTrace); - return false; - } - mIconCache = appState.getIconCache(); - return mIconCache != null; - } - - /** * @return true if the launcher is in a state to support backup */ @@ -1109,9 +1138,8 @@ public class LauncherBackupHelper implements BackupHelper { } cursor.close(); - if (!initializeIconCache()) { + if (LauncherAppState.getInstanceNoCreate() == null) { // launcher services are unavailable, try again later - dataChanged(); return false; } @@ -1123,13 +1151,66 @@ public class LauncherBackupHelper implements BackupHelper { .getSerialNumberForUser(UserHandleCompat.myUserHandle()); } - private class InvalidBackupException extends IOException { - private InvalidBackupException(Throwable cause) { + @Thunk class InvalidBackupException extends IOException { + + private static final long serialVersionUID = 8931456637211665082L; + + @Thunk InvalidBackupException(Throwable cause) { super(cause); } - public InvalidBackupException(String reason) { + @Thunk InvalidBackupException(String reason) { super(reason); } } + + /** + * A class to check if an activity can handle one of the intents from a list of + * predefined intents. + */ + private class ItemTypeMatcher { + + private final ArrayList mIntents; + + ItemTypeMatcher(int xml_res) { + mIntents = xml_res == 0 ? new ArrayList() : parseIntents(xml_res); + } + + private ArrayList parseIntents(int xml_res) { + ArrayList intents = new ArrayList(); + XmlResourceParser parser = mContext.getResources().getXml(xml_res); + try { + DefaultLayoutParser.beginDocument(parser, DefaultLayoutParser.TAG_RESOLVE); + final int depth = parser.getDepth(); + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } else if (DefaultLayoutParser.TAG_FAVORITE.equals(parser.getName())) { + final String uri = DefaultLayoutParser.getAttributeValue( + parser, DefaultLayoutParser.ATTR_URI); + intents.add(Intent.parseUri(uri, 0)); + } + } + } catch (URISyntaxException | XmlPullParserException | IOException e) { + Log.e(TAG, "Unable to parse " + xml_res, e); + } finally { + parser.close(); + } + return intents; + } + + public boolean matches(ActivityInfo activity, PackageManager pm) { + for (Intent intent : mIntents) { + intent.setPackage(activity.packageName); + ResolveInfo info = pm.resolveActivity(intent, 0); + if (info != null && (info.activityInfo.name.equals(activity.name) + || info.activityInfo.name.equals(activity.targetActivity))) { + return true; + } + } + return false; + } + } } diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java index a1f4e0b901..6618cca781 100644 --- a/src/com/android/launcher3/LauncherCallbacks.java +++ b/src/com/android/launcher3/LauncherCallbacks.java @@ -7,10 +7,13 @@ import android.os.Bundle; import android.view.Menu; import android.view.View; import android.view.ViewGroup; +import com.android.launcher3.allapps.AllAppsSearchBarController; +import com.android.launcher3.util.ComponentKey; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; /** * LauncherCallbacks is an interface used to extend the Launcher activity. It includes many hooks @@ -37,11 +40,14 @@ public interface LauncherCallbacks { public void onPostCreate(Bundle savedInstanceState); public void onNewIntent(Intent intent); public void onActivityResult(int requestCode, int resultCode, Intent data); + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults); public void onWindowFocusChanged(boolean hasFocus); public boolean onPrepareOptionsMenu(Menu menu); public void dump(String prefix, FileDescriptor fd, PrintWriter w, String[] args); public void onHomeIntent(); public boolean handleBackPressed(); + public void onTrimMemory(int level); /* * Extension points for providing custom behavior on certain user interactions. @@ -52,6 +58,7 @@ public interface LauncherCallbacks { public void bindAllApplications(ArrayList apps); public void onClickFolderIcon(View v); public void onClickAppShortcut(View v); + @Deprecated public void onClickPagedViewIcon(View v); public void onClickWallpaperPicker(View v); public void onClickSettingsButton(View v); @@ -65,10 +72,12 @@ public interface LauncherCallbacks { /* * Extension points for replacing the search experience */ + @Deprecated public boolean forceDisableVoiceButtonProxy(); public boolean providesSearch(); public boolean startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, Rect sourceBounds); + @Deprecated public void startVoice(); public boolean hasCustomContentToLeft(); public void populateCustomContentContainer(); @@ -83,9 +92,12 @@ public interface LauncherCallbacks { public View getIntroScreen(); public boolean shouldMoveToDefaultScreenOnHomeIntent(); public boolean hasSettings(); + @Deprecated public ComponentName getWallpaperPickerComponent(); public boolean overrideWallpaperDimensions(); public boolean isLauncherPreinstalled(); + public AllAppsSearchBarController getAllAppsSearchBarController(); + public List getPredictedApps(); /** * Returning true will immediately result in a call to {@link #setLauncherOverlayView(ViewGroup, @@ -105,4 +117,11 @@ public interface LauncherCallbacks { public Launcher.LauncherOverlay setLauncherOverlayView(InsettableFrameLayout container, Launcher.LauncherOverlayCallbacks callbacks); + /** + * Sets the callbacks to allow reacting the actions of search overlays of the launcher. + * + * @param callbacks A set of callbacks to the Launcher, is actually a LauncherSearchCallback, + * but for implementation purposes is passed around as an object. + */ + public void setLauncherSearchCallback(Object callbacks); } diff --git a/src/com/android/launcher3/LauncherClings.java b/src/com/android/launcher3/LauncherClings.java index ef8e8abcfe..c13752cb1f 100644 --- a/src/com/android/launcher3/LauncherClings.java +++ b/src/com/android/launcher3/LauncherClings.java @@ -20,10 +20,12 @@ import android.accounts.Account; import android.accounts.AccountManager; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.Context; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.os.UserManager; import android.provider.Settings; @@ -34,6 +36,7 @@ import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.accessibility.AccessibilityManager; +import com.android.launcher3.util.Thunk; class LauncherClings implements OnClickListener { private static final String MIGRATION_CLING_DISMISSED_KEY = "cling_gel.migration.dismissed"; @@ -49,7 +52,7 @@ class LauncherClings implements OnClickListener { // New Secure Setting in L private static final String SKIP_FIRST_USE_HINTS = "skip_first_use_hints"; - private Launcher mLauncher; + @Thunk Launcher mLauncher; private LayoutInflater mInflater; /** Ctor */ @@ -68,7 +71,7 @@ class LauncherClings implements OnClickListener { // Copy the shortcuts from the old database LauncherModel model = mLauncher.getModel(); model.resetLoadedState(false, true); - model.startLoader(false, PagedView.INVALID_RESTORE_PAGE, + model.startLoader(PagedView.INVALID_RESTORE_PAGE, LauncherModel.LOADER_FLAG_CLEAR_WORKSPACE | LauncherModel.LOADER_FLAG_MIGRATE_SHORTCUTS); // Set the flag to skip the folder cling @@ -124,7 +127,7 @@ class LauncherClings implements OnClickListener { @Override public boolean onLongClick(View v) { - mLauncher.getWorkspace().enterOverviewMode(); + mLauncher.showOverviewMode(true); dismissLongPressCling(); return true; } @@ -174,7 +177,7 @@ class LauncherClings implements OnClickListener { }); } - private void dismissLongPressCling() { + @Thunk void dismissLongPressCling() { Runnable dismissCb = new Runnable() { public void run() { dismissCling(mLauncher.findViewById(R.id.longpress_cling), null, @@ -185,7 +188,7 @@ class LauncherClings implements OnClickListener { } /** Hides the specified Cling */ - private void dismissCling(final View cling, final Runnable postAnimationCb, + @Thunk void dismissCling(final View cling, final Runnable postAnimationCb, final String flag, int duration) { // To catch cases where siblings of top-level views are made invisible, just check whether // the cling is directly set to GONE before dismissing it. @@ -210,6 +213,7 @@ class LauncherClings implements OnClickListener { } /** Returns whether the clings are enabled or should be shown */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private boolean areClingsEnabled() { if (DISABLE_CLINGS) { return false; diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java index fa053650f9..c08cd0bf52 100644 --- a/src/com/android/launcher3/LauncherFiles.java +++ b/src/com/android/launcher3/LauncherFiles.java @@ -17,24 +17,30 @@ public class LauncherFiles { public static final String DEFAULT_WALLPAPER_THUMBNAIL = "default_thumb2.jpg"; public static final String DEFAULT_WALLPAPER_THUMBNAIL_OLD = "default_thumb.jpg"; public static final String LAUNCHER_DB = "launcher.db"; - public static final String LAUNCHER_PREFERENCES = "launcher.preferences"; - public static final String LAUNCHES_LOG = "launches.log"; public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs"; - public static final String STATS_LOG = "stats.log"; public static final String WALLPAPER_CROP_PREFERENCES_KEY = - WallpaperCropActivity.class.getName(); + "com.android.launcher3.WallpaperCropActivity"; + public static final String MANAGED_USER_PREFERENCES_KEY = "com.android.launcher3.managedusers.prefs"; + public static final String WALLPAPER_IMAGES_DB = "saved_wallpaper_images.db"; public static final String WIDGET_PREVIEWS_DB = "widgetpreviews.db"; + public static final String APP_ICONS_DB = "app_icons.db"; public static final List ALL_FILES = Collections.unmodifiableList(Arrays.asList( DEFAULT_WALLPAPER_THUMBNAIL, DEFAULT_WALLPAPER_THUMBNAIL_OLD, LAUNCHER_DB, - LAUNCHER_PREFERENCES, - LAUNCHES_LOG, SHARED_PREFERENCES_KEY + XML, - STATS_LOG, WALLPAPER_CROP_PREFERENCES_KEY + XML, WALLPAPER_IMAGES_DB, - WIDGET_PREVIEWS_DB)); + WIDGET_PREVIEWS_DB, + MANAGED_USER_PREFERENCES_KEY, + APP_ICONS_DB)); + + // TODO: Delete these files on upgrade + public static final List OBSOLETE_FILES = Collections.unmodifiableList(Arrays.asList( + "launches.log", + "stats.log", + "launcher.preferences", + "com.android.launcher3.compat.PackageInstallerCompatV16.queue")); } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 954887dc31..b60477fa00 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -17,11 +17,9 @@ package com.android.launcher3; import android.app.SearchManager; -import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; -import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; @@ -29,27 +27,25 @@ import android.content.Context; import android.content.Intent; import android.content.Intent.ShortcutIconResource; import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.LauncherApps.Callback; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; -import android.content.res.Configuration; -import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; +import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.os.Parcelable; import android.os.Process; -import android.os.RemoteException; import android.os.SystemClock; +import android.os.TransactionTooLargeException; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; +import android.util.LongSparseArray; import android.util.Pair; import com.android.launcher3.compat.AppWidgetManagerCompat; @@ -59,11 +55,16 @@ import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.CursorIconInfo; +import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.ManagedProfileHeuristic; +import com.android.launcher3.util.Thunk; import java.lang.ref.WeakReference; import java.net.URISyntaxException; import java.security.InvalidParameterException; -import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -75,7 +76,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Set; -import java.util.TreeMap; /** * Maintains in-memory state of the Launcher. It is expected that there should be only one @@ -87,13 +87,9 @@ public class LauncherModel extends BroadcastReceiver static final boolean DEBUG_LOADERS = false; private static final boolean DEBUG_RECEIVER = false; private static final boolean REMOVE_UNRESTORED_ICONS = true; - private static final boolean ADD_MANAGED_PROFILE_SHORTCUTS = false; static final String TAG = "Launcher.Model"; - // true = use a "More Apps" folder for non-workspace apps on upgrade - // false = strew non-workspace apps across the workspace on upgrade - public static final boolean UPGRADE_USE_MORE_APPS_FOLDER = false; public static final int LOADER_FLAG_NONE = 0; public static final int LOADER_FLAG_CLEAR_WORKSPACE = 1 << 0; public static final int LOADER_FLAG_MIGRATE_SHORTCUTS = 1 << 1; @@ -101,39 +97,29 @@ public class LauncherModel extends BroadcastReceiver private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private static final long INVALID_SCREEN_ID = -1L; - private final boolean mAppsCanBeOnRemoveableStorage; + @Thunk final boolean mAppsCanBeOnRemoveableStorage; private final boolean mOldContentProviderExists; - private final LauncherAppState mApp; - private final Object mLock = new Object(); - private DeferredHandler mHandler = new DeferredHandler(); - private LoaderTask mLoaderTask; - private boolean mIsLoaderTaskRunning; - private volatile boolean mFlushingWorkerThread; - - /** - * Maintain a set of packages per user, for which we added a shortcut on the workspace. - */ - private static final String INSTALLED_SHORTCUTS_SET_PREFIX = "installed_shortcuts_set_for_user_"; - - // Specific runnable types that are run on the main thread deferred handler, this allows us to - // clear all queued binding runnables when the Launcher activity is destroyed. - private static final int MAIN_THREAD_NORMAL_RUNNABLE = 0; - private static final int MAIN_THREAD_BINDING_RUNNABLE = 1; + @Thunk final LauncherAppState mApp; + @Thunk final Object mLock = new Object(); + @Thunk DeferredHandler mHandler = new DeferredHandler(); + @Thunk LoaderTask mLoaderTask; + @Thunk boolean mIsLoaderTaskRunning; + @Thunk boolean mHasLoaderCompletedOnce; private static final String MIGRATE_AUTHORITY = "com.android.launcher2.settings"; - private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); + @Thunk static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); static { sWorkerThread.start(); } - private static final Handler sWorker = new Handler(sWorkerThread.getLooper()); + @Thunk static final Handler sWorker = new Handler(sWorkerThread.getLooper()); // We start off with everything not loaded. After that, we assume that // our monitoring of the package manager provides all updates and we never // need to do a requery. These are only ever touched from the loader thread. - private boolean mWorkspaceLoaded; - private boolean mAllAppsLoaded; + @Thunk boolean mWorkspaceLoaded; + @Thunk boolean mAllAppsLoaded; // When we are loading pages synchronously, we can't just post the binding of items on the side // pages as this delays the rotation process. Instead, we wait for a callback from the first @@ -141,10 +127,18 @@ public class LauncherModel extends BroadcastReceiver // a normal load, we also clear this set of Runnables. static final ArrayList mDeferredBindRunnables = new ArrayList(); - private WeakReference mCallbacks; + /** + * Set of runnables to be called on the background thread after the workspace binding + * is complete. + */ + static final ArrayList mBindCompleteRunnables = new ArrayList(); + + @Thunk WeakReference mCallbacks; // < only access in worker thread > AllAppsList mBgAllAppsList; + // Entire list of widgets. + WidgetsModel mBgWidgetsModel; // The lock that must be acquired before referencing any static bg data structures. Unlike // other locks, this one can generally be held long-term because we never expect any of these @@ -154,7 +148,7 @@ public class LauncherModel extends BroadcastReceiver // sBgItemsIdMap maps *all* the ItemInfos (shortcuts, folders, and widgets) created by // LauncherModel to their ids - static final HashMap sBgItemsIdMap = new HashMap(); + static final LongArrayMap sBgItemsIdMap = new LongArrayMap<>(); // sBgWorkspaceItems is passed to bindItems, which expects a list of all folders and shortcuts // created by LauncherModel that are directly on the home screen (however, no widgets or @@ -166,26 +160,24 @@ public class LauncherModel extends BroadcastReceiver new ArrayList(); // sBgFolders is all FolderInfos created by LauncherModel. Passed to bindFolders() - static final HashMap sBgFolders = new HashMap(); - - // sBgDbIconCache is the set of ItemInfos that need to have their icons updated in the database - static final HashMap sBgDbIconCache = new HashMap(); + static final LongArrayMap sBgFolders = new LongArrayMap<>(); // sBgWorkspaceScreens is the ordered set of workspace screens. static final ArrayList sBgWorkspaceScreens = new ArrayList(); + // sBgWidgetProviders is the set of widget providers including custom internal widgets + public static HashMap sBgWidgetProviders; + // sPendingPackages is a set of packages which could be on sdcard and are not available yet static final HashMap> sPendingPackages = new HashMap>(); // - private IconCache mIconCache; + @Thunk IconCache mIconCache; - protected int mPreviousConfigMcc; - - private final LauncherAppsCompat mLauncherApps; - private final UserManagerCompat mUserManager; + @Thunk final LauncherAppsCompat mLauncherApps; + @Thunk final UserManagerCompat mUserManager; public interface Callbacks { public boolean setLoadOnResume(); @@ -195,8 +187,8 @@ public class LauncherModel extends BroadcastReceiver boolean forceAnimateIcons); public void bindScreens(ArrayList orderedScreenIds); public void bindAddScreens(ArrayList orderedScreenIds); - public void bindFolders(HashMap folders); - public void finishBindingItems(boolean upgradePath); + public void bindFolders(LongArrayMap folders); + public void finishBindingItems(); public void bindAppWidget(LauncherAppWidgetInfo info); public void bindAllApplications(ArrayList apps); public void bindAppsAdded(ArrayList newScreens, @@ -207,11 +199,10 @@ public class LauncherModel extends BroadcastReceiver public void bindShortcutsChanged(ArrayList updated, ArrayList removed, UserHandleCompat user); public void bindWidgetsRestored(ArrayList widgets); - public void updatePackageState(ArrayList installInfo); - public void updatePackageBadge(String packageName); + public void bindRestoreItemsChange(HashSet updates); public void bindComponentsRemoved(ArrayList packageNames, ArrayList appInfos, UserHandleCompat user, int reason); - public void bindPackagesUpdated(ArrayList widgetsAndShortcuts); + public void bindAllPackages(WidgetsModel model); public void bindSearchablesChanged(); public boolean isAllAppsButtonRank(int rank); public void onPageBoundSynchronously(int page); @@ -246,21 +237,16 @@ public class LauncherModel extends BroadcastReceiver mApp = app; mBgAllAppsList = new AllAppsList(iconCache, appFilter); + mBgWidgetsModel = new WidgetsModel(context, iconCache, appFilter); mIconCache = iconCache; - final Resources res = context.getResources(); - Configuration config = res.getConfiguration(); - mPreviousConfigMcc = config.mcc; mLauncherApps = LauncherAppsCompat.getInstance(context); mUserManager = UserManagerCompat.getInstance(context); } /** Runs the specified runnable immediately if called from the main thread, otherwise it is * posted on the main thread handler. */ - private void runOnMainThread(Runnable r) { - runOnMainThread(r, 0); - } - private void runOnMainThread(Runnable r, int type) { + @Thunk void runOnMainThread(Runnable r) { if (sWorkerThread.getThreadId() == Process.myTid()) { // If we are on the worker thread, post onto the main handler mHandler.post(r); @@ -280,95 +266,127 @@ public class LauncherModel extends BroadcastReceiver } } + /** + * Runs the specified runnable after the loader is complete + */ + @Thunk void runAfterBindCompletes(Runnable r) { + if (isLoadingWorkspace() || !mHasLoaderCompletedOnce) { + synchronized (mBindCompleteRunnables) { + mBindCompleteRunnables.add(r); + } + } else { + runOnWorkerThread(r); + } + } + boolean canMigrateFromOldLauncherDb(Launcher launcher) { return mOldContentProviderExists && !launcher.isLauncherPreinstalled() ; } - static boolean findNextAvailableIconSpaceInScreen(ArrayList items, int[] xy, - long screen) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - final int xCount = (int) grid.numColumns; - final int yCount = (int) grid.numRows; - boolean[][] occupied = new boolean[xCount][yCount]; + public void setPackageState(final PackageInstallInfo installInfo) { + Runnable updateRunnable = new Runnable() { - int cellX, cellY, spanX, spanY; - for (int i = 0; i < items.size(); ++i) { - final ItemInfo item = items.get(i); - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { - if (item.screenId == screen) { - cellX = item.cellX; - cellY = item.cellY; - spanX = item.spanX; - spanY = item.spanY; - for (int x = cellX; 0 <= x && x < cellX + spanX && x < xCount; x++) { - for (int y = cellY; 0 <= y && y < cellY + spanY && y < yCount; y++) { - occupied[x][y] = true; + @Override + public void run() { + synchronized (sBgLock) { + final HashSet updates = new HashSet<>(); + + if (installInfo.state == PackageInstallerCompat.STATUS_INSTALLED) { + // Ignore install success events as they are handled by Package add events. + return; + } + + for (ItemInfo info : sBgItemsIdMap) { + if (info instanceof ShortcutInfo) { + ShortcutInfo si = (ShortcutInfo) info; + ComponentName cn = si.getTargetComponent(); + if (si.isPromise() && (cn != null) + && installInfo.packageName.equals(cn.getPackageName())) { + si.setInstallProgress(installInfo.progress); + + if (installInfo.state == PackageInstallerCompat.STATUS_FAILED) { + // Mark this info as broken. + si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; + } + updates.add(si); + } } } + + for (LauncherAppWidgetInfo widget : sBgAppWidgets) { + if (widget.providerName.getPackageName().equals(installInfo.packageName)) { + widget.installProgress = installInfo.progress; + updates.add(widget); + } + } + + if (!updates.isEmpty()) { + // Push changes to the callback. + Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = getCallback(); + if (callbacks != null) { + callbacks.bindRestoreItemsChange(updates); + } + } + }; + mHandler.post(r); + } } } - } - - return CellLayout.findVacantCell(xy, 1, 1, xCount, yCount, occupied); - } - static Pair findNextAvailableIconSpace(Context context, String name, - Intent launchIntent, - int firstScreenIndex, - ArrayList workspaceScreens) { - // Lock on the app so that we don't try and get the items while apps are being added - LauncherAppState app = LauncherAppState.getInstance(); - LauncherModel model = app.getModel(); - boolean found = false; - synchronized (app) { - if (sWorkerThread.getThreadId() != Process.myTid()) { - // Flush the LauncherModel worker thread, so that if we just did another - // processInstallShortcut, we give it time for its shortcut to get added to the - // database (getItemsInLocalCoordinates reads the database) - model.flushWorkerThread(); - } - final ArrayList items = LauncherModel.getItemsInLocalCoordinates(context); - - // Try adding to the workspace screens incrementally, starting at the default or center - // screen and alternating between +1, -1, +2, -2, etc. (using ~ ceil(i/2f)*(-1)^(i-1)) - firstScreenIndex = Math.min(firstScreenIndex, workspaceScreens.size()); - int count = workspaceScreens.size(); - for (int screen = firstScreenIndex; screen < count && !found; screen++) { - int[] tmpCoordinates = new int[2]; - if (findNextAvailableIconSpaceInScreen(items, tmpCoordinates, - workspaceScreens.get(screen))) { - // Update the Launcher db - return new Pair(workspaceScreens.get(screen), tmpCoordinates); - } - } - } - return null; + }; + runOnWorkerThread(updateRunnable); } - public void setPackageState(final ArrayList installInfo) { - // Process the updated package state - Runnable r = new Runnable() { + /** + * Updates the icons and label of all pending icons for the provided package name. + */ + public void updateSessionDisplayInfo(final String packageName) { + Runnable updateRunnable = new Runnable() { + + @Override public void run() { - Callbacks callbacks = getCallback(); - if (callbacks != null) { - callbacks.updatePackageState(installInfo); + synchronized (sBgLock) { + final ArrayList updates = new ArrayList<>(); + final UserHandleCompat user = UserHandleCompat.myUserHandle(); + + for (ItemInfo info : sBgItemsIdMap) { + if (info instanceof ShortcutInfo) { + ShortcutInfo si = (ShortcutInfo) info; + ComponentName cn = si.getTargetComponent(); + if (si.isPromise() && (cn != null) + && packageName.equals(cn.getPackageName())) { + if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { + // For auto install apps update the icon as well as label. + mIconCache.getTitleAndIcon(si, + si.promisedIntent, user, + si.shouldUseLowResIcon()); + } else { + // Only update the icon for restored apps. + si.updateIcon(mIconCache); + } + updates.add(si); + } + } + } + + if (!updates.isEmpty()) { + // Push changes to the callback. + Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = getCallback(); + if (callbacks != null) { + callbacks.bindShortcutsChanged(updates, + new ArrayList(), user); + } + } + }; + mHandler.post(r); + } } } }; - mHandler.post(r); - } - - public void updatePackageBadge(final String packageName) { - // Process the updated package badge - Runnable r = new Runnable() { - public void run() { - Callbacks callbacks = getCallback(); - if (callbacks != null) { - callbacks.updatePackageBadge(packageName); - } - } - }; - mHandler.post(r); + runOnWorkerThread(updateRunnable); } public void addAppsToAllApps(final Context ctx, final ArrayList allAppsApps) { @@ -397,13 +415,103 @@ public class LauncherModel extends BroadcastReceiver runOnWorkerThread(r); } - public void addAndBindAddedWorkspaceApps(final Context context, - final ArrayList workspaceApps) { - final Callbacks callbacks = getCallback(); - - if (workspaceApps == null) { - throw new RuntimeException("workspaceApps and allAppsApps must not be null"); + private static boolean findNextAvailableIconSpaceInScreen(ArrayList occupiedPos, + int[] xy, int spanX, int spanY) { + LauncherAppState app = LauncherAppState.getInstance(); + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + final int xCount = (int) profile.numColumns; + final int yCount = (int) profile.numRows; + boolean[][] occupied = new boolean[xCount][yCount]; + if (occupiedPos != null) { + for (ItemInfo r : occupiedPos) { + int right = r.cellX + r.spanX; + int bottom = r.cellY + r.spanY; + for (int x = r.cellX; 0 <= x && x < right && x < xCount; x++) { + for (int y = r.cellY; 0 <= y && y < bottom && y < yCount; y++) { + occupied[x][y] = true; + } + } + } } + return Utilities.findVacantCell(xy, spanX, spanY, xCount, yCount, occupied); + } + + /** + * Find a position on the screen for the given size or adds a new screen. + * @return screenId and the coordinates for the item. + */ + @Thunk Pair findSpaceForItem( + Context context, + ArrayList workspaceScreens, + ArrayList addedWorkspaceScreensFinal, + int spanX, int spanY) { + LongSparseArray> screenItems = new LongSparseArray<>(); + + // Use sBgItemsIdMap as all the items are already loaded. + assertWorkspaceLoaded(); + synchronized (sBgLock) { + for (ItemInfo info : sBgItemsIdMap) { + if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + ArrayList items = screenItems.get(info.screenId); + if (items == null) { + items = new ArrayList<>(); + screenItems.put(info.screenId, items); + } + items.add(info); + } + } + } + + // Find appropriate space for the item. + long screenId = 0; + int[] cordinates = new int[2]; + boolean found = false; + + int screenCount = workspaceScreens.size(); + // First check the preferred screen. + int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1; + if (preferredScreenIndex < screenCount) { + screenId = workspaceScreens.get(preferredScreenIndex); + found = findNextAvailableIconSpaceInScreen( + screenItems.get(screenId), cordinates, spanX, spanY); + } + + if (!found) { + // Search on any of the screens starting from the first screen. + for (int screen = 1; screen < screenCount; screen++) { + screenId = workspaceScreens.get(screen); + if (findNextAvailableIconSpaceInScreen( + screenItems.get(screenId), cordinates, spanX, spanY)) { + // We found a space for it + found = true; + break; + } + } + } + + if (!found) { + // Still no position found. Add a new screen to the end. + screenId = LauncherAppState.getLauncherProvider().generateNewScreenId(); + + // Save the screen id for binding in the workspace + workspaceScreens.add(screenId); + addedWorkspaceScreensFinal.add(screenId); + + // If we still can't find an empty space, then God help us all!!! + if (!findNextAvailableIconSpaceInScreen( + screenItems.get(screenId), cordinates, spanX, spanY)) { + throw new RuntimeException("Can't find space to add the item"); + } + } + return Pair.create(screenId, cordinates); + } + + /** + * Adds the provided items to the workspace. + */ + public void addAndBindAddedWorkspaceItems(final Context context, + final ArrayList workspaceApps) { + final Callbacks callbacks = getCallback(); if (workspaceApps.isEmpty()) { return; } @@ -416,71 +524,38 @@ public class LauncherModel extends BroadcastReceiver // Get the list of workspace screens. We need to append to this list and // can not use sBgWorkspaceScreens because loadWorkspace() may not have been // called. - ArrayList workspaceScreens = new ArrayList(); - TreeMap orderedScreens = loadWorkspaceScreensDb(context); - for (Integer i : orderedScreens.keySet()) { - long screenId = orderedScreens.get(i); - workspaceScreens.add(screenId); - } - + ArrayList workspaceScreens = loadWorkspaceScreensDb(context); synchronized(sBgLock) { - Iterator iter = workspaceApps.iterator(); - while (iter.hasNext()) { - ItemInfo a = iter.next(); - final String name = a.title.toString(); - final Intent launchIntent = a.getIntent(); - - // Short-circuit this logic if the icon exists somewhere on the workspace - if (shortcutExists(context, name, launchIntent, a.user)) { - continue; - } - - // Add this icon to the db, creating a new page if necessary. If there - // is only the empty page then we just add items to the first page. - // Otherwise, we add them to the next pages. - int startSearchPageIndex = workspaceScreens.isEmpty() ? 0 : 1; - Pair coords = LauncherModel.findNextAvailableIconSpace(context, - name, launchIntent, startSearchPageIndex, workspaceScreens); - if (coords == null) { - LauncherProvider lp = LauncherAppState.getLauncherProvider(); - - // If we can't find a valid position, then just add a new screen. - // This takes time so we need to re-queue the add until the new - // page is added. Create as many screens as necessary to satisfy - // the startSearchPageIndex. - int numPagesToAdd = Math.max(1, startSearchPageIndex + 1 - - workspaceScreens.size()); - while (numPagesToAdd > 0) { - long screenId = lp.generateNewScreenId(); - // Save the screen id for binding in the workspace - workspaceScreens.add(screenId); - addedWorkspaceScreensFinal.add(screenId); - numPagesToAdd--; + for (ItemInfo item : workspaceApps) { + if (item instanceof ShortcutInfo) { + // Short-circuit this logic if the icon exists somewhere on the workspace + if (shortcutExists(context, item.getIntent(), item.user)) { + continue; } - - // Find the coordinate again - coords = LauncherModel.findNextAvailableIconSpace(context, - name, launchIntent, startSearchPageIndex, workspaceScreens); - } - if (coords == null) { - throw new RuntimeException("Coordinates should not be null"); } - ShortcutInfo shortcutInfo; - if (a instanceof ShortcutInfo) { - shortcutInfo = (ShortcutInfo) a; - } else if (a instanceof AppInfo) { - shortcutInfo = ((AppInfo) a).makeShortcut(); + // Find appropriate space for the item. + Pair coords = findSpaceForItem(context, + workspaceScreens, addedWorkspaceScreensFinal, + 1, 1); + long screenId = coords.first; + int[] cordinates = coords.second; + + ItemInfo itemInfo; + if (item instanceof ShortcutInfo || item instanceof FolderInfo) { + itemInfo = item; + } else if (item instanceof AppInfo) { + itemInfo = ((AppInfo) item).makeShortcut(); } else { throw new RuntimeException("Unexpected info type"); } // Add the shortcut to the db - addItemToDatabase(context, shortcutInfo, + addItemToDatabase(context, itemInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, - coords.first, coords.second[0], coords.second[1], false); + screenId, cordinates[0], cordinates[1]); // Save the ShortcutInfo for binding in the workspace - addedShortcutsFinal.add(shortcutInfo); + addedShortcutsFinal.add(itemInfo); } } @@ -516,7 +591,7 @@ public class LauncherModel extends BroadcastReceiver runOnWorkerThread(r); } - public void unbindItemInfosAndClearQueuedBindRunnables() { + private void unbindItemInfosAndClearQueuedBindRunnables() { if (sWorkerThread.getThreadId() == Process.myTid()) { throw new RuntimeException("Expected unbindLauncherItemInfos() to be called from the " + "main thread"); @@ -526,8 +601,9 @@ public class LauncherModel extends BroadcastReceiver synchronized (mDeferredBindRunnables) { mDeferredBindRunnables.clear(); } - // Remove any queued bind runnables - mHandler.cancelAllRunnablesOfType(MAIN_THREAD_BINDING_RUNNABLE); + + // Remove any queued UI runnables + mHandler.cancelAll(); // Unbind all the workspace items unbindWorkspaceItemsOnMainThread(); } @@ -536,19 +612,15 @@ public class LauncherModel extends BroadcastReceiver void unbindWorkspaceItemsOnMainThread() { // Ensure that we don't use the same workspace items data structure on the main thread // by making a copy of workspace items first. - final ArrayList tmpWorkspaceItems = new ArrayList(); - final ArrayList tmpAppWidgets = new ArrayList(); + final ArrayList tmpItems = new ArrayList(); synchronized (sBgLock) { - tmpWorkspaceItems.addAll(sBgWorkspaceItems); - tmpAppWidgets.addAll(sBgAppWidgets); + tmpItems.addAll(sBgWorkspaceItems); + tmpItems.addAll(sBgAppWidgets); } Runnable r = new Runnable() { @Override public void run() { - for (ItemInfo item : tmpWorkspaceItems) { - item.unbind(); - } - for (ItemInfo item : tmpAppWidgets) { + for (ItemInfo item : tmpItems) { item.unbind(); } } @@ -564,7 +636,7 @@ public class LauncherModel extends BroadcastReceiver long screenId, int cellX, int cellY) { if (item.container == ItemInfo.NO_ID) { // From all apps - addItemToDatabase(context, item, container, screenId, cellX, cellY, false); + addItemToDatabase(context, item, container, screenId, cellX, cellY); } else { // From somewhere else moveItemInDatabase(context, item, container, screenId, cellX, cellY); @@ -630,7 +702,7 @@ public class LauncherModel extends BroadcastReceiver static void updateItemInDatabaseHelper(Context context, final ContentValues values, final ItemInfo item, final String callingFunction) { final long itemId = item.id; - final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(itemId); final ContentResolver cr = context.getContentResolver(); final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); @@ -656,7 +728,7 @@ public class LauncherModel extends BroadcastReceiver for (int i = 0; i < count; i++) { ItemInfo item = items.get(i); final long itemId = item.id; - final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(itemId); ContentValues values = valuesList.get(i); ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); @@ -714,39 +786,10 @@ public class LauncherModel extends BroadcastReceiver } } - public void flushWorkerThread() { - mFlushingWorkerThread = true; - Runnable waiter = new Runnable() { - public void run() { - synchronized (this) { - notifyAll(); - mFlushingWorkerThread = false; - } - } - }; - - synchronized(waiter) { - runOnWorkerThread(waiter); - if (mLoaderTask != null) { - synchronized(mLoaderTask) { - mLoaderTask.notify(); - } - } - boolean success = false; - while (!success) { - try { - waiter.wait(); - success = true; - } catch (InterruptedException e) { - } - } - } - } - /** * Move an item in the DB to a new */ - static void moveItemInDatabase(Context context, final ItemInfo item, final long container, + public static void moveItemInDatabase(Context context, final ItemInfo item, final long container, final long screenId, final int cellX, final int cellY) { item.container = container; item.cellX = cellX; @@ -765,6 +808,7 @@ public class LauncherModel extends BroadcastReceiver values.put(LauncherSettings.Favorites.CONTAINER, item.container); values.put(LauncherSettings.Favorites.CELLX, item.cellX); values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.RANK, item.rank); values.put(LauncherSettings.Favorites.SCREEN, item.screenId); updateItemInDatabaseHelper(context, values, item, "moveItemInDatabase"); @@ -798,6 +842,7 @@ public class LauncherModel extends BroadcastReceiver values.put(LauncherSettings.Favorites.CONTAINER, item.container); values.put(LauncherSettings.Favorites.CELLX, item.cellX); values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.RANK, item.rank); values.put(LauncherSettings.Favorites.SCREEN, item.screenId); contentValues.add(values); @@ -829,6 +874,7 @@ public class LauncherModel extends BroadcastReceiver values.put(LauncherSettings.Favorites.CONTAINER, item.container); values.put(LauncherSettings.Favorites.CELLX, item.cellX); values.put(LauncherSettings.Favorites.CELLY, item.cellY); + values.put(LauncherSettings.Favorites.RANK, item.rank); values.put(LauncherSettings.Favorites.SPANX, item.spanX); values.put(LauncherSettings.Favorites.SPANY, item.spanY); values.put(LauncherSettings.Favorites.SCREEN, item.screenId); @@ -839,104 +885,63 @@ public class LauncherModel extends BroadcastReceiver /** * Update an item to the database in a specified container. */ - static void updateItemInDatabase(Context context, final ItemInfo item) { + public static void updateItemInDatabase(Context context, final ItemInfo item) { final ContentValues values = new ContentValues(); item.onAddToDatabase(context, values); - item.updateValuesWithCoordinates(values, item.cellX, item.cellY); updateItemInDatabaseHelper(context, values, item, "updateItemInDatabase"); } - /** - * Returns true if the shortcuts already exists in the database. - * we identify a shortcut by its title and intent. - */ - static boolean shortcutExists(Context context, String title, Intent intent, - UserHandleCompat user) { - final ContentResolver cr = context.getContentResolver(); - final Intent intentWithPkg, intentWithoutPkg; - - if (intent.getComponent() != null) { - // If component is not null, an intent with null package will produce - // the same result and should also be a match. - if (intent.getPackage() != null) { - intentWithPkg = intent; - intentWithoutPkg = new Intent(intent).setPackage(null); - } else { - intentWithPkg = new Intent(intent).setPackage( - intent.getComponent().getPackageName()); - intentWithoutPkg = intent; - } - } else { - intentWithPkg = intent; - intentWithoutPkg = intent; - } - String userSerial = Long.toString(UserManagerCompat.getInstance(context) - .getSerialNumberForUser(user)); - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, - new String[] { "title", "intent", "profileId" }, - "title=? and (intent=? or intent=?) and profileId=?", - new String[] { title, intentWithPkg.toUri(0), intentWithoutPkg.toUri(0), userSerial }, - null); - try { - return c.moveToFirst(); - } finally { - c.close(); + private void assertWorkspaceLoaded() { + if (LauncherAppState.isDogfoodBuild() && (isLoadingWorkspace() || !mHasLoaderCompletedOnce)) { + throw new RuntimeException("Trying to add shortcut while loader is running"); } } /** - * Returns an ItemInfo array containing all the items in the LauncherModel. - * The ItemInfo.id is not set through this function. + * Returns true if the shortcuts already exists on the workspace. This must be called after + * the workspace has been loaded. We identify a shortcut by its intent. */ - static ArrayList getItemsInLocalCoordinates(Context context) { - ArrayList items = new ArrayList(); - final ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, new String[] { - LauncherSettings.Favorites.ITEM_TYPE, LauncherSettings.Favorites.CONTAINER, - LauncherSettings.Favorites.SCREEN, - LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, - LauncherSettings.Favorites.SPANX, LauncherSettings.Favorites.SPANY, - LauncherSettings.Favorites.PROFILE_ID }, null, null, null); - - final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); - final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); - final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); - final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); - final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); - final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX); - final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY); - final int profileIdIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID); - UserManagerCompat userManager = UserManagerCompat.getInstance(context); - try { - while (c.moveToNext()) { - ItemInfo item = new ItemInfo(); - item.cellX = c.getInt(cellXIndex); - item.cellY = c.getInt(cellYIndex); - item.spanX = Math.max(1, c.getInt(spanXIndex)); - item.spanY = Math.max(1, c.getInt(spanYIndex)); - item.container = c.getInt(containerIndex); - item.itemType = c.getInt(itemTypeIndex); - item.screenId = c.getInt(screenIndex); - long serialNumber = c.getInt(profileIdIndex); - item.user = userManager.getUserForSerialNumber(serialNumber); - // Skip if user has been deleted. - if (item.user != null) { - items.add(item); - } + @Thunk boolean shortcutExists(Context context, Intent intent, UserHandleCompat user) { + assertWorkspaceLoaded(); + final String intentWithPkg, intentWithoutPkg; + if (intent.getComponent() != null) { + // If component is not null, an intent with null package will produce + // the same result and should also be a match. + String packageName = intent.getComponent().getPackageName(); + if (intent.getPackage() != null) { + intentWithPkg = intent.toUri(0); + intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0); + } else { + intentWithPkg = new Intent(intent).setPackage(packageName).toUri(0); + intentWithoutPkg = intent.toUri(0); } - } catch (Exception e) { - items.clear(); - } finally { - c.close(); + } else { + intentWithPkg = intent.toUri(0); + intentWithoutPkg = intent.toUri(0); } - return items; + synchronized (sBgLock) { + for (ItemInfo item : sBgItemsIdMap) { + if (item instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) item; + Intent targetIntent = info.promisedIntent == null + ? info.intent : info.promisedIntent; + if (targetIntent != null && info.user.equals(user)) { + String s = targetIntent.toUri(0); + if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) { + return true; + } + } + } + } + } + return false; } /** * Find a folder in the db, creating the FolderInfo if necessary, and adding it to folderList. */ - FolderInfo getFolderById(Context context, HashMap folderList, long id) { + FolderInfo getFolderById(Context context, LongArrayMap folderList, long id) { final ContentResolver cr = context.getContentResolver(); Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, null, "_id=? and (itemType=? or itemType=?)", @@ -951,6 +956,7 @@ public class LauncherModel extends BroadcastReceiver final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); + final int optionsIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.OPTIONS); FolderInfo folderInfo = null; switch (c.getInt(itemTypeIndex)) { @@ -959,12 +965,14 @@ public class LauncherModel extends BroadcastReceiver break; } + // Do not trim the folder label, as is was set by the user. folderInfo.title = c.getString(titleIndex); folderInfo.id = id; folderInfo.container = c.getInt(containerIndex); folderInfo.screenId = c.getInt(screenIndex); folderInfo.cellX = c.getInt(cellXIndex); folderInfo.cellY = c.getInt(cellYIndex); + folderInfo.options = c.getInt(optionsIndex); return folderInfo; } @@ -979,8 +987,8 @@ public class LauncherModel extends BroadcastReceiver * Add an item to the database in a specified container. Sets the container, screen, cellX and * cellY fields of the item. Also assigns an ID to the item. */ - static void addItemToDatabase(Context context, final ItemInfo item, final long container, - final long screenId, final int cellX, final int cellY, final boolean notify) { + public static void addItemToDatabase(Context context, final ItemInfo item, final long container, + final long screenId, final int cellX, final int cellY) { item.container = container; item.cellX = cellX; item.cellY = cellY; @@ -999,13 +1007,11 @@ public class LauncherModel extends BroadcastReceiver item.id = LauncherAppState.getLauncherProvider().generateNewItemId(); values.put(LauncherSettings.Favorites._ID, item.id); - item.updateValuesWithCoordinates(values, item.cellX, item.cellY); final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); Runnable r = new Runnable() { public void run() { - cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : - LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values); + cr.insert(LauncherSettings.Favorites.CONTENT_URI, values); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { @@ -1056,7 +1062,7 @@ public class LauncherModel extends BroadcastReceiver return cn.getPackageName().equals(pn) && info.user.equals(user); } }; - return filterItemInfos(sBgItemsIdMap.values(), filter); + return filterItemInfos(sBgItemsIdMap, filter); } /** @@ -1072,7 +1078,7 @@ public class LauncherModel extends BroadcastReceiver * @param context * @param item */ - static void deleteItemFromDatabase(Context context, final ItemInfo item) { + public static void deleteItemFromDatabase(Context context, final ItemInfo item) { ArrayList items = new ArrayList(); items.add(item); deleteItemsFromDatabase(context, items); @@ -1085,11 +1091,10 @@ public class LauncherModel extends BroadcastReceiver */ static void deleteItemsFromDatabase(Context context, final ArrayList items) { final ContentResolver cr = context.getContentResolver(); - Runnable r = new Runnable() { public void run() { for (ItemInfo item : items) { - final Uri uri = LauncherSettings.Favorites.getContentUri(item.id, false); + final Uri uri = LauncherSettings.Favorites.getContentUri(item.id); cr.delete(uri, null, null); // Lock on mBgLock *after* the db operation @@ -1097,7 +1102,7 @@ public class LauncherModel extends BroadcastReceiver switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: sBgFolders.remove(item.id); - for (ItemInfo info: sBgItemsIdMap.values()) { + for (ItemInfo info: sBgItemsIdMap) { if (info.container == item.id) { // We are deleting a folder which still contains items that // think they are contained by that folder. @@ -1117,7 +1122,6 @@ public class LauncherModel extends BroadcastReceiver break; } sBgItemsIdMap.remove(item.id); - sBgDbIconCache.remove(item); } } } @@ -1130,10 +1134,6 @@ public class LauncherModel extends BroadcastReceiver * a list of screen ids in the order that they should appear. */ void updateWorkspaceScreenOrder(Context context, final ArrayList screens) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - updateWorkspaceScreenOrder()", true); - Launcher.addDumpLog(TAG, "11683562 - screens: " + TextUtils.join(", ", screens), true); - final ArrayList screensCopy = new ArrayList(screens); final ContentResolver cr = context.getContentResolver(); final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI; @@ -1180,27 +1180,25 @@ public class LauncherModel extends BroadcastReceiver /** * Remove the contents of the specified folder from the database */ - static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { + public static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { final ContentResolver cr = context.getContentResolver(); Runnable r = new Runnable() { public void run() { - cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null); + cr.delete(LauncherSettings.Favorites.getContentUri(info.id), null, null); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { sBgItemsIdMap.remove(info.id); sBgFolders.remove(info.id); - sBgDbIconCache.remove(info); sBgWorkspaceItems.remove(info); } - cr.delete(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, + cr.delete(LauncherSettings.Favorites.CONTENT_URI, LauncherSettings.Favorites.CONTAINER + "=" + info.id, null); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { for (ItemInfo childInfo : info.contents) { sBgItemsIdMap.remove(childInfo.id); - sBgDbIconCache.remove(childInfo); } } } @@ -1213,6 +1211,9 @@ public class LauncherModel extends BroadcastReceiver */ public void initialize(Callbacks callbacks) { synchronized (mLock) { + // Disconnect any of the callbacks and drawables associated with ItemInfos on the + // workspace to prevent leaking Launcher activities on orientation change. + unbindItemInfosAndClearQueuedBindRunnables(); mCallbacks = new WeakReference(callbacks); } } @@ -1265,7 +1266,6 @@ public class LauncherModel extends BroadcastReceiver PackageUpdatedTask.OP_UNAVAILABLE, packageNames, user)); } - } /** @@ -1280,24 +1280,15 @@ public class LauncherModel extends BroadcastReceiver if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { // If we have changed locale we need to clear out the labels in all apps/workspace. forceReload(); - } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) { - // Check if configuration change was an mcc/mnc change which would affect app resources - // and we would need to clear out the labels in all apps/workspace. Same handling as - // above for ACTION_LOCALE_CHANGED - Configuration currentConfig = context.getResources().getConfiguration(); - if (mPreviousConfigMcc != currentConfig.mcc) { - Log.d(TAG, "Reload apps on config change. curr_mcc:" - + currentConfig.mcc + " prevmcc:" + mPreviousConfigMcc); - forceReload(); - } - // Update previousConfig - mPreviousConfigMcc = currentConfig.mcc; } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action) || SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)) { Callbacks callbacks = getCallback(); if (callbacks != null) { callbacks.bindSearchablesChanged(); } + } else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED.equals(action) + || LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { + forceReload(); } } @@ -1336,38 +1327,32 @@ public class LauncherModel extends BroadcastReceiver } } if (runLoader) { - startLoader(false, PagedView.INVALID_RESTORE_PAGE); + startLoader(PagedView.INVALID_RESTORE_PAGE); } } - // If there is already a loader task running, tell it to stop. - // returns true if isLaunching() was true on the old task - private boolean stopLoaderLocked() { - boolean isLaunching = false; + /** + * If there is already a loader task running, tell it to stop. + */ + private void stopLoaderLocked() { LoaderTask oldTask = mLoaderTask; if (oldTask != null) { - if (oldTask.isLaunching()) { - isLaunching = true; - } oldTask.stopLocked(); } - return isLaunching; } public boolean isCurrentCallbacks(Callbacks callbacks) { return (mCallbacks != null && mCallbacks.get() == callbacks); } - public void startLoader(boolean isLaunching, int synchronousBindPage) { - startLoader(isLaunching, synchronousBindPage, LOADER_FLAG_NONE); + public void startLoader(int synchronousBindPage) { + startLoader(synchronousBindPage, LOADER_FLAG_NONE); } - public void startLoader(boolean isLaunching, int synchronousBindPage, int loadFlags) { + public void startLoader(int synchronousBindPage, int loadFlags) { + // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems + InstallShortcutReceiver.enableInstallQueue(); synchronized (mLock) { - if (DEBUG_LOADERS) { - Log.d(TAG, "startLoader isLaunching=" + isLaunching); - } - // Clear any deferred bind-runnables from the synchronized load process // We must do this before any loading/binding is scheduled below. synchronized (mDeferredBindRunnables) { @@ -1377,11 +1362,10 @@ public class LauncherModel extends BroadcastReceiver // Don't bother to start the thread if we know it's not going to do anything if (mCallbacks != null && mCallbacks.get() != null) { // If there is already one running, tell it to stop. - // also, don't downgrade isLaunching if we're already running - isLaunching = isLaunching || stopLoaderLocked(); - mLoaderTask = new LoaderTask(mApp.getContext(), isLaunching, loadFlags); + stopLoaderLocked(); + mLoaderTask = new LoaderTask(mApp.getContext(), loadFlags); if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE - && mAllAppsLoaded && mWorkspaceLoaded) { + && mAllAppsLoaded && mWorkspaceLoaded && !mIsLoaderTaskRunning) { mLoaderTask.runBindSynchronousPage(synchronousBindPage); } else { sWorkerThread.setPriority(Thread.NORM_PRIORITY); @@ -1401,7 +1385,17 @@ public class LauncherModel extends BroadcastReceiver mDeferredBindRunnables.clear(); } for (final Runnable r : deferredBindRunnables) { - mHandler.post(r, MAIN_THREAD_BINDING_RUNNABLE); + mHandler.post(r); + } + } + + // Run all the bind complete runnables after workspace is bound. + if (!mBindCompleteRunnables.isEmpty()) { + synchronized (mBindCompleteRunnables) { + for (final Runnable r : mBindCompleteRunnables) { + runOnWorkerThread(r); + } + mBindCompleteRunnables.clear(); } } } @@ -1414,40 +1408,31 @@ public class LauncherModel extends BroadcastReceiver } } - /** Loads the workspace screens db into a map of Rank -> ScreenId */ - private static TreeMap loadWorkspaceScreensDb(Context context) { + /** + * Loads the workspace screen ids in an ordered list. + */ + @Thunk static ArrayList loadWorkspaceScreensDb(Context context) { final ContentResolver contentResolver = context.getContentResolver(); final Uri screensUri = LauncherSettings.WorkspaceScreens.CONTENT_URI; - final Cursor sc = contentResolver.query(screensUri, null, null, null, null); - TreeMap orderedScreens = new TreeMap(); + // Get screens ordered by rank. + final Cursor sc = contentResolver.query(screensUri, null, null, null, + LauncherSettings.WorkspaceScreens.SCREEN_RANK); + ArrayList screenIds = new ArrayList(); try { - final int idIndex = sc.getColumnIndexOrThrow( - LauncherSettings.WorkspaceScreens._ID); - final int rankIndex = sc.getColumnIndexOrThrow( - LauncherSettings.WorkspaceScreens.SCREEN_RANK); + final int idIndex = sc.getColumnIndexOrThrow(LauncherSettings.WorkspaceScreens._ID); while (sc.moveToNext()) { try { - long screenId = sc.getLong(idIndex); - int rank = sc.getInt(rankIndex); - orderedScreens.put(rank, screenId); + screenIds.add(sc.getLong(idIndex)); } catch (Exception e) { - Launcher.addDumpLog(TAG, "Desktop items loading interrupted - invalid screens: " + e, true); + Launcher.addDumpLog(TAG, "Desktop items loading interrupted" + + " - invalid screens: " + e, true); } } } finally { sc.close(); } - - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - loadWorkspaceScreensDb()", true); - ArrayList orderedScreensPairs= new ArrayList(); - for (Integer i : orderedScreens.keySet()) { - orderedScreensPairs.add("{ " + i + ": " + orderedScreens.get(i) + " }"); - } - Launcher.addDumpLog(TAG, "11683562 - screens: " + - TextUtils.join(", ", orderedScreensPairs), true); - return orderedScreens; + return screenIds; } public boolean isAllAppsLoaded() { @@ -1471,31 +1456,21 @@ public class LauncherModel extends BroadcastReceiver */ private class LoaderTask implements Runnable { private Context mContext; - private boolean mIsLaunching; - private boolean mIsLoadingAndBindingWorkspace; + @Thunk boolean mIsLoadingAndBindingWorkspace; private boolean mStopped; - private boolean mLoadAndBindStepFinished; + @Thunk boolean mLoadAndBindStepFinished; private int mFlags; - private HashMap mLabelCache; - - LoaderTask(Context context, boolean isLaunching, int flags) { + LoaderTask(Context context, int flags) { mContext = context; - mIsLaunching = isLaunching; - mLabelCache = new HashMap(); mFlags = flags; } - boolean isLaunching() { - return mIsLaunching; - } - boolean isLoadingWorkspace() { return mIsLoadingAndBindingWorkspace; } - /** Returns whether this is an upgrade path */ - private boolean loadAndBindWorkspace() { + private void loadAndBindWorkspace() { mIsLoadingAndBindingWorkspace = true; // Load the workspace @@ -1503,20 +1478,18 @@ public class LauncherModel extends BroadcastReceiver Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded); } - boolean isUpgradePath = false; if (!mWorkspaceLoaded) { - isUpgradePath = loadWorkspace(); + loadWorkspace(); synchronized (LoaderTask.this) { if (mStopped) { - return isUpgradePath; + return; } mWorkspaceLoaded = true; } } // Bind the workspace - bindWorkspace(-1, isUpgradePath); - return isUpgradePath; + bindWorkspace(-1); } private void waitForIdle() { @@ -1538,7 +1511,7 @@ public class LauncherModel extends BroadcastReceiver } }); - while (!mStopped && !mLoadAndBindStepFinished && !mFlushingWorkerThread) { + while (!mStopped && !mLoadAndBindStepFinished) { try { // Just in case mFlushingWorkerThread changes but we aren't woken up, // wait no longer than 1sec at a time @@ -1585,72 +1558,35 @@ public class LauncherModel extends BroadcastReceiver // Divide the set of loaded items into those that we are binding synchronously, and // everything else that is to be bound normally (asynchronously). - bindWorkspace(synchronousBindPage, false); + bindWorkspace(synchronousBindPage); // XXX: For now, continue posting the binding of AllApps as there are other issues that // arise from that. onlyBindAllApps(); } public void run() { - boolean isUpgrade = false; - synchronized (mLock) { + if (mStopped) { + return; + } mIsLoaderTaskRunning = true; } // Optimize for end-user experience: if the Launcher is up and // running with the // All Apps interface in the foreground, load All Apps first. Otherwise, load the // workspace first (default). keep_running: { - // Elevate priority when Home launches for the first time to avoid - // starving at boot time. Staring at a blank home is not cool. - synchronized (mLock) { - if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to " + - (mIsLaunching ? "DEFAULT" : "BACKGROUND")); - android.os.Process.setThreadPriority(mIsLaunching - ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND); - } if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); - isUpgrade = loadAndBindWorkspace(); + loadAndBindWorkspace(); if (mStopped) { break keep_running; } - // Whew! Hard work done. Slow us down, and wait until the UI thread has - // settled down. - synchronized (mLock) { - if (mIsLaunching) { - if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to BACKGROUND"); - android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - } - } waitForIdle(); // second step if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps"); loadAndBindAllApps(); - - // Restore the default thread priority after we are done loading items - synchronized (mLock) { - android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); - } - } - - // Update the saved icons if necessary - if (DEBUG_LOADERS) Log.d(TAG, "Comparing loaded icons to database icons"); - synchronized (sBgLock) { - for (Object key : sBgDbIconCache.keySet()) { - updateSavedIcon(mContext, (ShortcutInfo) key, sBgDbIconCache.get(key)); - } - sBgDbIconCache.clear(); - } - - if (LauncherAppState.isDisableAllApps()) { - // Ensure that all the applications that are in the system are - // represented on the home screen. - if (!UPGRADE_USE_MORE_APPS_FOLDER || !isUpgrade) { - verifyApplications(); - } } // Clear out this reference, otherwise we end up holding it until all of the @@ -1663,6 +1599,7 @@ public class LauncherModel extends BroadcastReceiver mLoaderTask = null; } mIsLoaderTaskRunning = false; + mHasLoaderCompletedOnce = true; } } @@ -1703,34 +1640,12 @@ public class LauncherModel extends BroadcastReceiver } } - private void verifyApplications() { - final Context context = mApp.getContext(); - - // Cross reference all the applications in our apps list with items in the workspace - ArrayList tmpInfos; - ArrayList added = new ArrayList(); - synchronized (sBgLock) { - for (AppInfo app : mBgAllAppsList.data) { - tmpInfos = getItemInfoForComponentName(app.componentName, app.user); - if (tmpInfos.isEmpty()) { - // We are missing an application icon, so add this to the workspace - added.add(app); - // This is a rare event, so lets log it - Log.e(TAG, "Missing Application on load: " + app); - } - } - } - if (!added.isEmpty()) { - addAndBindAddedWorkspaceApps(context, added); - } - } - // check & update map of what's occupied; used to discard overlapping/invalid items - private boolean checkItemPlacement(HashMap occupied, ItemInfo item) { + private boolean checkItemPlacement(LongArrayMap occupied, ItemInfo item) { LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - final int countX = (int) grid.numColumns; - final int countY = (int) grid.numRows; + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + final int countX = (int) profile.numColumns; + final int countY = (int) profile.numRows; long containerIndex = item.screenId; if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { @@ -1746,10 +1661,10 @@ public class LauncherModel extends BroadcastReceiver final ItemInfo[][] hotseatItems = occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT); - if (item.screenId >= grid.numHotseatIcons) { + if (item.screenId >= profile.numHotseatIcons) { Log.e(TAG, "Error loading shortcut " + item + " into hotseat position " + item.screenId - + ", position out of bounds: (0 to " + (grid.numHotseatIcons - 1) + + ", position out of bounds: (0 to " + (profile.numHotseatIcons - 1) + ")"); return false; } @@ -1767,7 +1682,7 @@ public class LauncherModel extends BroadcastReceiver return true; } } else { - final ItemInfo[][] items = new ItemInfo[(int) grid.numHotseatIcons][1]; + final ItemInfo[][] items = new ItemInfo[(int) profile.numHotseatIcons][1]; items[(int) item.screenId][0] = item; occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, items); return true; @@ -1822,31 +1737,25 @@ public class LauncherModel extends BroadcastReceiver sBgAppWidgets.clear(); sBgFolders.clear(); sBgItemsIdMap.clear(); - sBgDbIconCache.clear(); sBgWorkspaceScreens.clear(); } } - /** Returns whether this is an upgrade path */ - private boolean loadWorkspace() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - loadWorkspace()", true); - + private void loadWorkspace() { final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; final Context context = mContext; final ContentResolver contentResolver = context.getContentResolver(); final PackageManager manager = context.getPackageManager(); - final AppWidgetManager widgets = AppWidgetManager.getInstance(context); final boolean isSafeMode = manager.isSafeMode(); final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); final boolean isSdCardReady = context.registerReceiver(null, new IntentFilter(StartupReceiver.SYSTEM_READY)) != null; LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - int countX = (int) grid.numColumns; - int countY = (int) grid.numRows; + InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + int countX = (int) profile.numColumns; + int countY = (int) profile.numRows; if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) { Launcher.addDumpLog(TAG, "loadWorkspace: resetting launcher database", true); @@ -1863,27 +1772,21 @@ public class LauncherModel extends BroadcastReceiver LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary(); } - // This code path is for our old migration code and should no longer be exercised - boolean loadedOldDb = false; - - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - loadedOldDb: " + loadedOldDb, true); - synchronized (sBgLock) { clearSBgDataStructures(); - final HashSet installingPkgs = PackageInstallerCompat + final HashMap installingPkgs = PackageInstallerCompat .getInstance(mContext).updateAndGetActiveSessionCache(); final ArrayList itemsToRemove = new ArrayList(); final ArrayList restoredRows = new ArrayList(); - final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION; + final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI; if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri); final Cursor c = contentResolver.query(contentUri, null, null, null, null); // +1 for the hotseat (it can be larger than the workspace) // Load workspace in reverse order to ensure that latest items are loaded first (and // before any earlier duplicates) - final HashMap occupied = new HashMap(); + final LongArrayMap occupied = new LongArrayMap<>(); try { final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); @@ -1891,13 +1794,6 @@ public class LauncherModel extends BroadcastReceiver (LauncherSettings.Favorites.INTENT); final int titleIndex = c.getColumnIndexOrThrow (LauncherSettings.Favorites.TITLE); - final int iconTypeIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.ICON_TYPE); - final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); - final int iconPackageIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.ICON_PACKAGE); - final int iconResourceIndex = c.getColumnIndexOrThrow( - LauncherSettings.Favorites.ICON_RESOURCE); final int containerIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.CONTAINER); final int itemTypeIndex = c.getColumnIndexOrThrow( @@ -1916,19 +1812,27 @@ public class LauncherModel extends BroadcastReceiver (LauncherSettings.Favorites.SPANX); final int spanYIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.SPANY); + final int rankIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.RANK); final int restoredIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.RESTORED); final int profileIdIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.PROFILE_ID); - //final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); - //final int displayModeIndex = c.getColumnIndexOrThrow( - // LauncherSettings.Favorites.DISPLAY_MODE); + final int optionsIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.OPTIONS); + final CursorIconInfo cursorIconInfo = new CursorIconInfo(c); + + final LongSparseArray allUsers = new LongSparseArray<>(); + for (UserHandleCompat user : mUserManager.getUserProfiles()) { + allUsers.put(mUserManager.getSerialNumberForUser(user), user); + } ShortcutInfo info; String intentDescription; LauncherAppWidgetInfo appWidgetInfo; int container; long id; + long serialNumber; Intent intent; UserHandleCompat user; @@ -1937,16 +1841,18 @@ public class LauncherModel extends BroadcastReceiver int itemType = c.getInt(itemTypeIndex); boolean restored = 0 != c.getInt(restoredIndex); boolean allowMissingTarget = false; + container = c.getInt(containerIndex); switch (itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: id = c.getLong(idIndex); intentDescription = c.getString(intentIndex); - long serialNumber = c.getInt(profileIdIndex); - user = mUserManager.getUserForSerialNumber(serialNumber); + serialNumber = c.getInt(profileIdIndex); + user = allUsers.get(serialNumber); int promiseType = c.getInt(restoredIndex); int disabledState = 0; + boolean itemReplaced = false; if (user == null) { // User has been deleted remove the item. itemsToRemove.add(id); @@ -1978,9 +1884,7 @@ public class LauncherModel extends BroadcastReceiver ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites.INTENT, intent.toUri(0)); - String where = BaseColumns._ID + "= ?"; - String[] args = {Long.toString(id)}; - contentResolver.update(contentUri, values, where, args); + updateItem(id, values); } } @@ -2004,16 +1908,33 @@ public class LauncherModel extends BroadcastReceiver if ((promiseType & ShortcutInfo.FLAG_RESTORE_STARTED) != 0) { // Restore has started once. - } else if (installingPkgs.contains(cn.getPackageName())) { + } else if (installingPkgs.containsKey(cn.getPackageName())) { // App restore has started. Update the flag promiseType |= ShortcutInfo.FLAG_RESTORE_STARTED; ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites.RESTORED, promiseType); - String where = BaseColumns._ID + "= ?"; - String[] args = {Long.toString(id)}; - contentResolver.update(contentUri, values, where, args); + updateItem(id, values); + } else if ((promiseType & ShortcutInfo.FLAG_RESTORED_APP_TYPE) != 0) { + // This is a common app. Try to replace this. + int appType = CommonAppTypeParser.decodeItemTypeFromFlag(promiseType); + CommonAppTypeParser parser = new CommonAppTypeParser(id, appType, context); + if (parser.findDefaultApp()) { + // Default app found. Replace it. + intent = parser.parsedIntent; + cn = intent.getComponent(); + ContentValues values = parser.parsedValues; + values.put(LauncherSettings.Favorites.RESTORED, 0); + updateItem(id, values); + restored = false; + itemReplaced = true; + } else if (REMOVE_UNRESTORED_ICONS) { + Launcher.addDumpLog(TAG, + "Unrestored package removed: " + cn, true); + itemsToRemove.add(id); + continue; + } } else if (REMOVE_UNRESTORED_ICONS) { Launcher.addDumpLog(TAG, "Unrestored package removed: " + cn, true); @@ -2059,12 +1980,26 @@ public class LauncherModel extends BroadcastReceiver continue; } - if (restored) { + boolean useLowResIcon = container >= 0 && + c.getInt(rankIndex) >= FolderIcon.NUM_ITEMS_IN_PREVIEW; + + if (itemReplaced) { + if (user.equals(UserHandleCompat.myUserHandle())) { + info = getAppShortcutInfo(manager, intent, user, context, null, + cursorIconInfo.iconIndex, titleIndex, + false, useLowResIcon); + } else { + // Don't replace items for other profiles. + itemsToRemove.add(id); + continue; + } + } else if (restored) { if (user.equals(UserHandleCompat.myUserHandle())) { Launcher.addDumpLog(TAG, "constructing info for partially restored package", true); - info = getRestoredItemInfo(c, titleIndex, intent, promiseType); + info = getRestoredItemInfo(c, titleIndex, intent, + promiseType, itemType, cursorIconInfo, context); intent = getRestoredItemIntent(c, context, intent); } else { // Don't restore items for other profiles. @@ -2073,12 +2008,11 @@ public class LauncherModel extends BroadcastReceiver } } else if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { - info = getShortcutInfo(manager, intent, user, context, c, - iconIndex, titleIndex, mLabelCache, allowMissingTarget); + info = getAppShortcutInfo(manager, intent, user, context, c, + cursorIconInfo.iconIndex, titleIndex, + allowMissingTarget, useLowResIcon); } else { - info = getShortcutInfo(c, context, iconTypeIndex, - iconPackageIndex, iconResourceIndex, iconIndex, - titleIndex); + info = getShortcutInfo(c, context, titleIndex, cursorIconInfo); // App shortcuts that used to be automatically added to Launcher // didn't always have the correct intent flags set, so do that @@ -2096,14 +2030,17 @@ public class LauncherModel extends BroadcastReceiver if (info != null) { info.id = id; info.intent = intent; - container = c.getInt(containerIndex); info.container = container; info.screenId = c.getInt(screenIndex); info.cellX = c.getInt(cellXIndex); info.cellY = c.getInt(cellYIndex); + info.rank = c.getInt(rankIndex); info.spanX = 1; info.spanY = 1; info.intent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber); + if (info.promisedIntent != null) { + info.promisedIntent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber); + } info.isDisabled = disabledState; if (isSafeMode && !Utilities.isSystemApp(context, intent)) { info.isDisabled |= ShortcutInfo.FLAG_DISABLED_SAFEMODE; @@ -2115,6 +2052,18 @@ public class LauncherModel extends BroadcastReceiver break; } + if (restored) { + ComponentName cn = info.getTargetComponent(); + if (cn != null) { + Integer progress = installingPkgs.get(cn.getPackageName()); + if (progress != null) { + info.setInstallProgress(progress); + } else { + info.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; + } + } + } + switch (container) { case LauncherSettings.Favorites.CONTAINER_DESKTOP: case LauncherSettings.Favorites.CONTAINER_HOTSEAT: @@ -2128,10 +2077,6 @@ public class LauncherModel extends BroadcastReceiver break; } sBgItemsIdMap.put(info.id, info); - - // now that we've loaded everthing re-save it with the - // icon in case it disappears somehow. - queueIconToBeChecked(sBgDbIconCache, info, c, iconIndex); } else { throw new RuntimeException("Unexpected null ShortcutInfo"); } @@ -2141,15 +2086,16 @@ public class LauncherModel extends BroadcastReceiver id = c.getLong(idIndex); FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id); + // Do not trim the folder label, as is was set by the user. folderInfo.title = c.getString(titleIndex); folderInfo.id = id; - container = c.getInt(containerIndex); folderInfo.container = container; folderInfo.screenId = c.getInt(screenIndex); folderInfo.cellX = c.getInt(cellXIndex); folderInfo.cellY = c.getInt(cellYIndex); folderInfo.spanX = 1; folderInfo.spanY = 1; + folderInfo.options = c.getInt(optionsIndex); // check & update map of what's occupied if (!checkItemPlacement(occupied, folderInfo)) { @@ -2174,28 +2120,41 @@ public class LauncherModel extends BroadcastReceiver break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: // Read all Launcher-specific widget details + boolean customWidget = itemType == + LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + int appWidgetId = c.getInt(appWidgetIdIndex); + serialNumber = c.getLong(profileIdIndex); String savedProvider = c.getString(appWidgetProviderIndex); id = c.getLong(idIndex); + user = allUsers.get(serialNumber); + if (user == null) { + itemsToRemove.add(id); + continue; + } + final ComponentName component = ComponentName.unflattenFromString(savedProvider); final int restoreStatus = c.getInt(restoredIndex); final boolean isIdValid = (restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) == 0; - final boolean wasProviderReady = (restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0; - final AppWidgetProviderInfo provider = isIdValid - ? widgets.getAppWidgetInfo(appWidgetId) - : findAppWidgetProviderInfoWithComponent(context, component); + final LauncherAppWidgetProviderInfo provider = + LauncherModel.getProviderInfo(context, + ComponentName.unflattenFromString(savedProvider), + user); final boolean isProviderReady = isValidProvider(provider); - if (!isSafeMode && wasProviderReady && !isProviderReady) { + if (!isSafeMode && !customWidget && + wasProviderReady && !isProviderReady) { String log = "Deleting widget that isn't installed anymore: " + "id=" + id + " appWidgetId=" + appWidgetId; + Log.e(TAG, log); Launcher.addDumpLog(TAG, log, false); itemsToRemove.add(id); @@ -2203,10 +2162,6 @@ public class LauncherModel extends BroadcastReceiver if (isProviderReady) { appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, provider.provider); - int[] minSpan = - Launcher.getMinSpanForWidget(context, provider); - appWidgetInfo.minSpanX = minSpan[0]; - appWidgetInfo.minSpanY = minSpan[1]; int status = restoreStatus; if (!wasProviderReady) { @@ -2229,10 +2184,11 @@ public class LauncherModel extends BroadcastReceiver appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component); appWidgetInfo.restoreStatus = restoreStatus; + Integer installProgress = installingPkgs.get(component.getPackageName()); if ((restoreStatus & LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) != 0) { // Restore has started once. - } else if (installingPkgs.contains(component.getPackageName())) { + } else if (installProgress != null) { // App restore has started. Update the flag appWidgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; @@ -2242,6 +2198,9 @@ public class LauncherModel extends BroadcastReceiver itemsToRemove.add(id); continue; } + + appWidgetInfo.installProgress = + installProgress == null ? 0 : installProgress; } appWidgetInfo.id = id; @@ -2250,8 +2209,8 @@ public class LauncherModel extends BroadcastReceiver appWidgetInfo.cellY = c.getInt(cellYIndex); appWidgetInfo.spanX = c.getInt(spanXIndex); appWidgetInfo.spanY = c.getInt(spanYIndex); + appWidgetInfo.user = user; - container = c.getInt(containerIndex); if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP && container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) { Log.e(TAG, "Widget found where container != " + @@ -2259,24 +2218,26 @@ public class LauncherModel extends BroadcastReceiver continue; } - appWidgetInfo.container = c.getInt(containerIndex); + appWidgetInfo.container = container; // check & update map of what's occupied if (!checkItemPlacement(occupied, appWidgetInfo)) { itemsToRemove.add(id); break; } - String providerName = appWidgetInfo.providerName.flattenToString(); - if (!providerName.equals(savedProvider) || - (appWidgetInfo.restoreStatus != restoreStatus)) { - ContentValues values = new ContentValues(); - values.put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, - providerName); - values.put(LauncherSettings.Favorites.RESTORED, - appWidgetInfo.restoreStatus); - String where = BaseColumns._ID + "= ?"; - String[] args = {Long.toString(id)}; - contentResolver.update(contentUri, values, where, args); + if (!customWidget) { + String providerName = + appWidgetInfo.providerName.flattenToString(); + if (!providerName.equals(savedProvider) || + (appWidgetInfo.restoreStatus != restoreStatus)) { + ContentValues values = new ContentValues(); + values.put( + LauncherSettings.Favorites.APPWIDGET_PROVIDER, + providerName); + values.put(LauncherSettings.Favorites.RESTORED, + appWidgetInfo.restoreStatus); + updateItem(id, values); + } } sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); sBgAppWidgets.add(appWidgetInfo); @@ -2296,44 +2257,35 @@ public class LauncherModel extends BroadcastReceiver // Break early if we've stopped loading if (mStopped) { clearSBgDataStructures(); - return false; + return; } if (itemsToRemove.size() > 0) { - ContentProviderClient client = contentResolver.acquireContentProviderClient( - contentUri); // Remove dead items - for (long id : itemsToRemove) { - if (DEBUG_LOADERS) { - Log.d(TAG, "Removed id = " + id); - } - // Don't notify content observers - try { - client.delete(LauncherSettings.Favorites.getContentUri(id, false), - null, null); - } catch (RemoteException e) { - Log.w(TAG, "Could not remove id = " + id); - } + contentResolver.delete(LauncherSettings.Favorites.CONTENT_URI, + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, itemsToRemove), null); + if (DEBUG_LOADERS) { + Log.d(TAG, "Removed = " + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, itemsToRemove)); + } + + // Remove any empty folder + for (long folderId : LauncherAppState.getLauncherProvider() + .deleteEmptyFolders()) { + sBgWorkspaceItems.remove(sBgFolders.get(folderId)); + sBgFolders.remove(folderId); + sBgItemsIdMap.remove(folderId); } } if (restoredRows.size() > 0) { - ContentProviderClient updater = contentResolver.acquireContentProviderClient( - contentUri); // Update restored items that no longer require special handling - try { - StringBuilder selectionBuilder = new StringBuilder(); - selectionBuilder.append(LauncherSettings.Favorites._ID); - selectionBuilder.append(" IN ("); - selectionBuilder.append(TextUtils.join(", ", restoredRows)); - selectionBuilder.append(")"); - ContentValues values = new ContentValues(); - values.put(LauncherSettings.Favorites.RESTORED, 0); - updater.update(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, - values, selectionBuilder.toString(), null); - } catch (RemoteException e) { - Log.w(TAG, "Could not update restored rows"); - } + ContentValues values = new ContentValues(); + values.put(LauncherSettings.Favorites.RESTORED, 0); + contentResolver.update(LauncherSettings.Favorites.CONTENT_URI, values, + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, restoredRows), null); } if (!isSdCardReady && !sPendingPackages.isEmpty()) { @@ -2342,63 +2294,22 @@ public class LauncherModel extends BroadcastReceiver null, sWorker); } - if (loadedOldDb) { - long maxScreenId = 0; - // If we're importing we use the old screen order. - for (ItemInfo item: sBgItemsIdMap.values()) { - long screenId = item.screenId; - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && - !sBgWorkspaceScreens.contains(screenId)) { - sBgWorkspaceScreens.add(screenId); - if (screenId > maxScreenId) { - maxScreenId = screenId; - } - } - } - Collections.sort(sBgWorkspaceScreens); - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - maxScreenId: " + maxScreenId, true); - Launcher.addDumpLog(TAG, "11683562 - sBgWorkspaceScreens: " + - TextUtils.join(", ", sBgWorkspaceScreens), true); + sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext)); - LauncherAppState.getLauncherProvider().updateMaxScreenId(maxScreenId); + // Remove any empty screens + ArrayList unusedScreens = new ArrayList(sBgWorkspaceScreens); + for (ItemInfo item: sBgItemsIdMap) { + long screenId = item.screenId; + if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && + unusedScreens.contains(screenId)) { + unusedScreens.remove(screenId); + } + } + + // If there are any empty screens remove them, and update. + if (unusedScreens.size() != 0) { + sBgWorkspaceScreens.removeAll(unusedScreens); updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); - - // Update the max item id after we load an old db - long maxItemId = 0; - // If we're importing we use the old screen order. - for (ItemInfo item: sBgItemsIdMap.values()) { - maxItemId = Math.max(maxItemId, item.id); - } - LauncherAppState.getLauncherProvider().updateMaxItemId(maxItemId); - } else { - TreeMap orderedScreens = loadWorkspaceScreensDb(mContext); - for (Integer i : orderedScreens.keySet()) { - sBgWorkspaceScreens.add(orderedScreens.get(i)); - } - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - sBgWorkspaceScreens: " + - TextUtils.join(", ", sBgWorkspaceScreens), true); - - // Remove any empty screens - ArrayList unusedScreens = new ArrayList(sBgWorkspaceScreens); - for (ItemInfo item: sBgItemsIdMap.values()) { - long screenId = item.screenId; - if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && - unusedScreens.contains(screenId)) { - unusedScreens.remove(screenId); - } - } - - // If there are any empty screens remove them, and update. - if (unusedScreens.size() != 0) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - unusedScreens (to be removed): " + - TextUtils.join(", ", unusedScreens), true); - - sBgWorkspaceScreens.removeAll(unusedScreens); - updateWorkspaceScreenOrder(context, sBgWorkspaceScreens); - } } if (DEBUG_LOADERS) { @@ -2408,14 +2319,13 @@ public class LauncherModel extends BroadcastReceiver for (int y = 0; y < countY; y++) { String line = ""; - Iterator iter = occupied.keySet().iterator(); - while (iter.hasNext()) { - long screenId = iter.next(); + for (int i = 0; i < nScreens; i++) { + long screenId = occupied.keyAt(i); if (screenId > 0) { line += " | "; } + ItemInfo[][] screen = occupied.valueAt(i); for (int x = 0; x < countX; x++) { - ItemInfo[][] screen = occupied.get(screenId); if (x < screen.length && y < screen[x].length) { line += (screen[x][y] != null) ? "#" : "."; } else { @@ -2427,7 +2337,17 @@ public class LauncherModel extends BroadcastReceiver } } } - return loadedOldDb; + } + + /** + * Partially updates the item without any notification. Must be called on the worker thread. + */ + private void updateItem(long itemId, ContentValues update) { + mContext.getContentResolver().update( + LauncherSettings.Favorites.CONTENT_URI, + update, + BaseColumns._ID + "= ?", + new String[]{Long.toString(itemId)}); } /** Filters the set of items who are directly or indirectly (via another container) on the @@ -2496,14 +2416,17 @@ public class LauncherModel extends BroadcastReceiver /** Filters the set of folders which are on the specified screen. */ private void filterCurrentFolders(long currentScreenId, - HashMap itemsIdMap, - HashMap folders, - HashMap currentScreenFolders, - HashMap otherScreenFolders) { + LongArrayMap itemsIdMap, + LongArrayMap folders, + LongArrayMap currentScreenFolders, + LongArrayMap otherScreenFolders) { + + int total = folders.size(); + for (int i = 0; i < total; i++) { + long id = folders.keyAt(i); + FolderInfo folder = folders.valueAt(i); - for (long id : folders.keySet()) { ItemInfo info = itemsIdMap.get(id); - FolderInfo folder = folders.get(id); if (info == null || folder == null) continue; if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && info.screenId == currentScreenId) { @@ -2518,13 +2441,13 @@ public class LauncherModel extends BroadcastReceiver * right) */ private void sortWorkspaceItemsSpatially(ArrayList workspaceItems) { final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + final InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); // XXX: review this Collections.sort(workspaceItems, new Comparator() { @Override public int compare(ItemInfo lhs, ItemInfo rhs) { - int cellCountX = (int) grid.numColumns; - int cellCountY = (int) grid.numRows; + int cellCountX = (int) profile.numColumns; + int cellCountY = (int) profile.numRows; int screenOffset = cellCountX * cellCountY; int containerOffset = screenOffset * (Launcher.SCREEN_COUNT + 1); // +1 hotseat long lr = (lhs.container * containerOffset + lhs.screenId * screenOffset + @@ -2547,13 +2470,13 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } private void bindWorkspaceItems(final Callbacks oldCallbacks, final ArrayList workspaceItems, final ArrayList appWidgets, - final HashMap folders, + final LongArrayMap folders, ArrayList deferredBindRunnables) { final boolean postOnMainThread = (deferredBindRunnables != null); @@ -2578,7 +2501,7 @@ public class LauncherModel extends BroadcastReceiver deferredBindRunnables.add(r); } } else { - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } } @@ -2597,7 +2520,7 @@ public class LauncherModel extends BroadcastReceiver deferredBindRunnables.add(r); } } else { - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } } @@ -2616,7 +2539,7 @@ public class LauncherModel extends BroadcastReceiver if (postOnMainThread) { deferredBindRunnables.add(r); } else { - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } } } @@ -2624,7 +2547,7 @@ public class LauncherModel extends BroadcastReceiver /** * Binds all loaded data to actual views on the main thread. */ - private void bindWorkspace(int synchronizeBindPage, final boolean isUpgradePath) { + private void bindWorkspace(int synchronizeBindPage) { final long t = SystemClock.uptimeMillis(); Runnable r; @@ -2641,15 +2564,18 @@ public class LauncherModel extends BroadcastReceiver ArrayList workspaceItems = new ArrayList(); ArrayList appWidgets = new ArrayList(); - HashMap folders = new HashMap(); - HashMap itemsIdMap = new HashMap(); ArrayList orderedScreenIds = new ArrayList(); + + final LongArrayMap folders; + final LongArrayMap itemsIdMap; + synchronized (sBgLock) { workspaceItems.addAll(sBgWorkspaceItems); appWidgets.addAll(sBgAppWidgets); - folders.putAll(sBgFolders); - itemsIdMap.putAll(sBgItemsIdMap); orderedScreenIds.addAll(sBgWorkspaceScreens); + + folders = sBgFolders.clone(); + itemsIdMap = sBgItemsIdMap.clone(); } final boolean isLoadingSynchronously = @@ -2675,8 +2601,8 @@ public class LauncherModel extends BroadcastReceiver new ArrayList(); ArrayList otherAppWidgets = new ArrayList(); - HashMap currentFolders = new HashMap(); - HashMap otherFolders = new HashMap(); + LongArrayMap currentFolders = new LongArrayMap<>(); + LongArrayMap otherFolders = new LongArrayMap<>(); filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems, otherWorkspaceItems); @@ -2696,7 +2622,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); bindWorkspaceScreens(oldCallbacks, orderedScreenIds); @@ -2712,7 +2638,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } // Load all the remaining pages (if we are loading synchronously, we want to defer this @@ -2728,7 +2654,7 @@ public class LauncherModel extends BroadcastReceiver public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { - callbacks.finishBindingItems(isUpgradePath); + callbacks.finishBindingItems(); } // If we're profiling, ensure this is the last thing in the queue. @@ -2745,7 +2671,7 @@ public class LauncherModel extends BroadcastReceiver mDeferredBindRunnables.add(r); } } else { - runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + runOnMainThread(r); } } @@ -2755,6 +2681,12 @@ public class LauncherModel extends BroadcastReceiver } if (!mAllAppsLoaded) { loadAllApps(); + synchronized (LoaderTask.this) { + if (mStopped) { + return; + } + } + updateIconCache(); synchronized (LoaderTask.this) { if (mStopped) { return; @@ -2766,6 +2698,27 @@ public class LauncherModel extends BroadcastReceiver } } + private void updateIconCache() { + // Ignore packages which have a promise icon. + HashSet packagesToIgnore = new HashSet<>(); + synchronized (sBgLock) { + for (ItemInfo info : sBgItemsIdMap) { + if (info instanceof ShortcutInfo) { + ShortcutInfo si = (ShortcutInfo) info; + if (si.isPromise() && si.getTargetComponent() != null) { + packagesToIgnore.add(si.getTargetComponent().getPackageName()); + } + } else if (info instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo lawi = (LauncherAppWidgetInfo) info; + if (lawi.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) { + packagesToIgnore.add(lawi.providerName.getPackageName()); + } + } + } + } + mIconCache.updateDbIcons(packagesToIgnore); + } + private void onlyBindAllApps() { final Callbacks oldCallbacks = mCallbacks.get(); if (oldCallbacks == null) { @@ -2778,12 +2731,14 @@ public class LauncherModel extends BroadcastReceiver @SuppressWarnings("unchecked") final ArrayList list = (ArrayList) mBgAllAppsList.data.clone(); + final WidgetsModel widgetList = mBgWidgetsModel.clone(); Runnable r = new Runnable() { public void run() { final long t = SystemClock.uptimeMillis(); final Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.bindAllApplications(list); + callbacks.bindAllPackages(widgetList); } if (DEBUG_LOADERS) { Log.d(TAG, "bound all " + list.size() + " apps from cache in " @@ -2809,19 +2764,14 @@ public class LauncherModel extends BroadcastReceiver return; } - final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); - mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); - final List profiles = mUserManager.getUserProfiles(); // Clear the list of apps mBgAllAppsList.clear(); - SharedPreferences prefs = mContext.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); for (UserHandleCompat user : profiles) { // Query for the set of apps final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; - List apps = mLauncherApps.getActivityList(null, user); + final List apps = mLauncherApps.getActivityList(null, user); if (DEBUG_LOADERS) { Log.d(TAG, "getActivityList took " + (SystemClock.uptimeMillis()-qiaTime) + "ms for user " + user); @@ -2832,39 +2782,23 @@ public class LauncherModel extends BroadcastReceiver if (apps == null || apps.isEmpty()) { return; } - // Sort the applications by name - final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; - Collections.sort(apps, - new LauncherModel.ShortcutNameComparator(mLabelCache)); - if (DEBUG_LOADERS) { - Log.d(TAG, "sort took " - + (SystemClock.uptimeMillis()-sortTime) + "ms"); - } // Create the ApplicationInfos for (int i = 0; i < apps.size(); i++) { LauncherActivityInfoCompat app = apps.get(i); // This builds the icon bitmaps. - mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache, mLabelCache)); + mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache)); } - if (ADD_MANAGED_PROFILE_SHORTCUTS && !user.equals(UserHandleCompat.myUserHandle())) { - // Add shortcuts for packages which were installed while launcher was dead. - String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX - + mUserManager.getSerialNumberForUser(user); - Set packagesAdded = prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET); - HashSet newPackageSet = new HashSet(); + final ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user); + if (heuristic != null) { + runAfterBindCompletes(new Runnable() { - for (LauncherActivityInfoCompat info : apps) { - String packageName = info.getComponentName().getPackageName(); - if (!packagesAdded.contains(packageName) - && !newPackageSet.contains(packageName)) { - InstallShortcutReceiver.queueInstallShortcut(info, mContext); + @Override + public void run() { + heuristic.processUserApps(apps); } - newPackageSet.add(packageName); - } - - prefs.edit().putStringSet(shortcutsSetKey, newPackageSet).commit(); + }); } } // Huh? Shouldn't this be inside the Runnable below? @@ -2874,6 +2808,7 @@ public class LauncherModel extends BroadcastReceiver // Post callback on main thread mHandler.post(new Runnable() { public void run() { + final long bindTime = SystemClock.uptimeMillis(); final Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { @@ -2887,7 +2822,11 @@ public class LauncherModel extends BroadcastReceiver } } }); + // Cleanup any data stored for a deleted user. + ManagedProfileHeuristic.processAllUsers(profiles, mContext); + loadAndBindWidgetsAndShortcuts(mApp.getContext(), tryGetCallbacks(oldCallbacks), + true /* refresh */); if (DEBUG_LOADERS) { Log.d(TAG, "Icons processed in " + (SystemClock.uptimeMillis() - loadTime) + "ms"); @@ -2897,7 +2836,6 @@ public class LauncherModel extends BroadcastReceiver public void dumpState() { synchronized (sBgLock) { Log.d(TAG, "mLoaderTask.mContext=" + mContext); - Log.d(TAG, "mLoaderTask.mIsLaunching=" + mIsLaunching); Log.d(TAG, "mLoaderTask.mStopped=" + mStopped); Log.d(TAG, "mLoaderTask.mLoadAndBindStepFinished=" + mLoadAndBindStepFinished); Log.d(TAG, "mItems size=" + sBgWorkspaceItems.size()); @@ -2905,11 +2843,66 @@ public class LauncherModel extends BroadcastReceiver } } + /** + * Called when the icons for packages have been updated in the icon cache. + */ + public void onPackageIconsUpdated(HashSet updatedPackages, UserHandleCompat user) { + final Callbacks callbacks = getCallback(); + final ArrayList updatedApps = new ArrayList<>(); + final ArrayList updatedShortcuts = new ArrayList<>(); + + // If any package icon has changed (app was updated while launcher was dead), + // update the corresponding shortcuts. + synchronized (sBgLock) { + for (ItemInfo info : sBgItemsIdMap) { + if (info instanceof ShortcutInfo && user.equals(info.user) + && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { + ShortcutInfo si = (ShortcutInfo) info; + ComponentName cn = si.getTargetComponent(); + if (cn != null && updatedPackages.contains(cn.getPackageName())) { + si.updateIcon(mIconCache); + updatedShortcuts.add(si); + } + } + } + mBgAllAppsList.updateIconsAndLabels(updatedPackages, user, updatedApps); + } + + if (!updatedShortcuts.isEmpty()) { + final UserHandleCompat userFinal = user; + mHandler.post(new Runnable() { + + public void run() { + Callbacks cb = getCallback(); + if (cb != null && callbacks == cb) { + cb.bindShortcutsChanged(updatedShortcuts, + new ArrayList(), userFinal); + } + } + }); + } + + if (!updatedApps.isEmpty()) { + mHandler.post(new Runnable() { + + public void run() { + Callbacks cb = getCallback(); + if (cb != null && callbacks == cb) { + cb.bindAppsUpdated(updatedApps); + } + } + }); + } + + // Reload widget list. No need to refresh, as we only want to update the icons and labels. + loadAndBindWidgetsAndShortcuts(mApp.getContext(), callbacks, false); + } + void enqueuePackageUpdated(PackageUpdatedTask task) { sWorker.post(task); } - private class AppsAvailabilityCheck extends BroadcastReceiver { + @Thunk class AppsAvailabilityCheck extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -2969,73 +2962,52 @@ public class LauncherModel extends BroadcastReceiver } public void run() { + if (!mHasLoaderCompletedOnce) { + // Loader has not yet run. + return; + } final Context context = mApp.getContext(); final String[] packages = mPackages; final int N = packages.length; switch (mOp) { - case OP_ADD: + case OP_ADD: { for (int i=0; i shortcutSet = new HashSet( - prefs.getStringSet(shortcutsSetKey,Collections.EMPTY_SET)); - - for (int i=0; i activities = - mLauncherApps.getActivityList(packages[i], mUser); - if (activities != null && !activities.isEmpty()) { - InstallShortcutReceiver.queueInstallShortcut( - activities.get(0), context); - } - } - } - - prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit(); + ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser); + if (heuristic != null) { + heuristic.processPackageAdd(mPackages); } break; + } case OP_UPDATE: for (int i=0; i shortcutSet = new HashSet( - prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET)); - shortcutSet.removeAll(Arrays.asList(mPackages)); - prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit(); + case OP_REMOVE: { + ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser); + if (heuristic != null) { + heuristic.processPackageRemoved(mPackages); } - // Fall through - case OP_UNAVAILABLE: - boolean clearCache = mOp == OP_REMOVE; for (int i=0; i(); if (added != null) { - // Ensure that we add all the workspace applications to the db - if (LauncherAppState.isDisableAllApps()) { - final ArrayList addedInfos = new ArrayList(added); - addAndBindAddedWorkspaceApps(context, addedInfos); - } else { - addAppsToAllApps(context, added); - } + addAppsToAllApps(context, added); for (AppInfo ai : added) { addedOrUpdatedApps.put(ai.componentName, ai); } @@ -3103,7 +3069,7 @@ public class LauncherModel extends BroadcastReceiver HashSet packageSet = new HashSet(Arrays.asList(packages)); synchronized (sBgLock) { - for (ItemInfo info : sBgItemsIdMap.values()) { + for (ItemInfo info : sBgItemsIdMap) { if (info instanceof ShortcutInfo && mUser.equals(info.user)) { ShortcutInfo si = (ShortcutInfo) info; boolean infoUpdated = false; @@ -3112,8 +3078,9 @@ public class LauncherModel extends BroadcastReceiver // Update shortcuts which use iconResource. if ((si.iconResource != null) && packageSet.contains(si.iconResource.packageName)) { - Bitmap icon = Utilities.createIconBitmap(si.iconResource.packageName, - si.iconResource.resourceName, mIconCache, context); + Bitmap icon = Utilities.createIconBitmap( + si.iconResource.packageName, + si.iconResource.resourceName, context); if (icon != null) { si.setIcon(icon); si.usingFallbackIcon = false; @@ -3126,7 +3093,6 @@ public class LauncherModel extends BroadcastReceiver AppInfo appInfo = addedOrUpdatedApps.get(cn); if (si.isPromise()) { - mIconCache.deletePreloadedIcon(cn, mUser); if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { // Auto install icon PackageManager pm = context.getPackageManager(); @@ -3152,12 +3118,13 @@ public class LauncherModel extends BroadcastReceiver } // Restore the shortcut. + if (appInfo != null) { + si.flags = appInfo.flags; + } + si.intent = si.promisedIntent; si.promisedIntent = null; - si.status &= ~ShortcutInfo.FLAG_RESTORED_ICON - & ~ShortcutInfo.FLAG_AUTOINTALL_ICON - & ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; - + si.status = ShortcutInfo.DEFAULT; infoUpdated = true; si.updateIcon(mIconCache); } @@ -3165,7 +3132,7 @@ public class LauncherModel extends BroadcastReceiver if (appInfo != null && Intent.ACTION_MAIN.equals(si.intent.getAction()) && si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { si.updateIcon(mIconCache); - si.title = appInfo.title.toString(); + si.title = Utilities.trim(appInfo.title); si.contentDescription = appInfo.contentDescription; infoUpdated = true; } @@ -3268,17 +3235,8 @@ public class LauncherModel extends BroadcastReceiver }); } - final ArrayList widgetsAndShortcuts = - getSortedWidgetsAndShortcuts(context); - mHandler.post(new Runnable() { - @Override - public void run() { - Callbacks cb = getCallback(); - if (callbacks == cb && cb != null) { - callbacks.bindPackagesUpdated(widgetsAndShortcuts); - } - } - }); + // onProvidersChanged method (API >= 17) already refreshed the widget list + loadAndBindWidgetsAndShortcuts(context, callbacks, Build.VERSION.SDK_INT < 17); // Write all the logs to disk mHandler.post(new Runnable() { @@ -3292,19 +3250,106 @@ public class LauncherModel extends BroadcastReceiver } } - // Returns a list of ResolveInfos/AppWindowInfos in sorted order - public static ArrayList getSortedWidgetsAndShortcuts(Context context) { - PackageManager packageManager = context.getPackageManager(); - final ArrayList widgetsAndShortcuts = new ArrayList(); - widgetsAndShortcuts.addAll(AppWidgetManagerCompat.getInstance(context).getAllProviders()); + public static List getWidgetProviders(Context context, + boolean refresh) { + ArrayList results = + new ArrayList(); + try { + synchronized (sBgLock) { + if (sBgWidgetProviders == null || refresh) { + HashMap tmpWidgetProviders + = new HashMap<>(); + AppWidgetManagerCompat wm = AppWidgetManagerCompat.getInstance(context); + LauncherAppWidgetProviderInfo info; - Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); - widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0)); - Collections.sort(widgetsAndShortcuts, new WidgetAndShortcutNameComparator(context)); - return widgetsAndShortcuts; + List widgets = wm.getAllProviders(); + for (AppWidgetProviderInfo pInfo : widgets) { + info = LauncherAppWidgetProviderInfo.fromProviderInfo(context, pInfo); + UserHandleCompat user = wm.getUser(info); + tmpWidgetProviders.put(new ComponentKey(info.provider, user), info); + } + + Collection customWidgets = Launcher.getCustomAppWidgets().values(); + for (CustomAppWidget widget : customWidgets) { + info = new LauncherAppWidgetProviderInfo(context, widget); + UserHandleCompat user = wm.getUser(info); + tmpWidgetProviders.put(new ComponentKey(info.provider, user), info); + } + // Replace the global list at the very end, so that if there is an exception, + // previously loaded provider list is used. + sBgWidgetProviders = tmpWidgetProviders; + } + results.addAll(sBgWidgetProviders.values()); + return results; + } + } catch (Exception e) { + if (e.getCause() instanceof TransactionTooLargeException) { + // the returned value may be incomplete and will not be refreshed until the next + // time Launcher starts. + // TODO: after figuring out a repro step, introduce a dirty bit to check when + // onResume is called to refresh the widget provider list. + synchronized (sBgLock) { + if (sBgWidgetProviders != null) { + results.addAll(sBgWidgetProviders.values()); + } + return results; + } + } else { + throw e; + } + } } - private static boolean isPackageDisabled(Context context, String packageName, + public static LauncherAppWidgetProviderInfo getProviderInfo(Context ctx, ComponentName name, + UserHandleCompat user) { + synchronized (sBgLock) { + if (sBgWidgetProviders == null) { + getWidgetProviders(ctx, false /* refresh */); + } + return sBgWidgetProviders.get(new ComponentKey(name, user)); + } + } + + public void loadAndBindWidgetsAndShortcuts(final Context context, final Callbacks callbacks, + final boolean refresh) { + + runOnWorkerThread(new Runnable() { + @Override + public void run() { + updateWidgetsModel(context, refresh); + final WidgetsModel model = mBgWidgetsModel.clone(); + + mHandler.post(new Runnable() { + @Override + public void run() { + Callbacks cb = getCallback(); + if (callbacks == cb && cb != null) { + callbacks.bindAllPackages(model); + } + } + }); + // update the Widget entries inside DB on the worker thread. + LauncherAppState.getInstance().getWidgetCache().removeObsoletePreviews( + model.getRawList()); + } + }); + } + + /** + * Returns a list of ResolveInfos/AppWidgetInfos. + * + * @see #loadAndBindWidgetsAndShortcuts + */ + @Thunk void updateWidgetsModel(Context context, boolean refresh) { + PackageManager packageManager = context.getPackageManager(); + final ArrayList widgetsAndShortcuts = new ArrayList(); + widgetsAndShortcuts.addAll(getWidgetProviders(context, refresh)); + Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); + widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0)); + mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts); + } + + @Thunk static boolean isPackageDisabled(Context context, String packageName, UserHandleCompat user) { final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); return !launcherApps.isPackageEnabledForProfile(packageName, user); @@ -3335,31 +3380,36 @@ public class LauncherModel extends BroadcastReceiver * Make an ShortcutInfo object for a restored application or shortcut item that points * to a package that is not yet installed on the system. */ - public ShortcutInfo getRestoredItemInfo(Cursor cursor, int titleIndex, Intent intent, - int promiseType) { + public ShortcutInfo getRestoredItemInfo(Cursor c, int titleIndex, Intent intent, + int promiseType, int itemType, CursorIconInfo iconInfo, Context context) { final ShortcutInfo info = new ShortcutInfo(); info.user = UserHandleCompat.myUserHandle(); - mIconCache.getTitleAndIcon(info, intent, info.user, true); + + Bitmap icon = iconInfo.loadIcon(c, info, context); + // the fallback icon + if (icon == null) { + mIconCache.getTitleAndIcon(info, intent, info.user, false /* useLowResIcon */); + } else { + info.setIcon(icon); + } if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) { - String title = (cursor != null) ? cursor.getString(titleIndex) : null; + String title = (c != null) ? c.getString(titleIndex) : null; if (!TextUtils.isEmpty(title)) { - info.title = title; + info.title = Utilities.trim(title); } - info.status = ShortcutInfo.FLAG_RESTORED_ICON; } else if ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) { if (TextUtils.isEmpty(info.title)) { - info.title = (cursor != null) ? cursor.getString(titleIndex) : ""; + info.title = (c != null) ? Utilities.trim(c.getString(titleIndex)) : ""; } - info.status = ShortcutInfo.FLAG_AUTOINTALL_ICON; } else { throw new InvalidParameterException("Invalid restoreType " + promiseType); } - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); - info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); + info.itemType = itemType; info.promisedIntent = intent; + info.status = promiseType; return info; } @@ -3367,7 +3417,7 @@ public class LauncherModel extends BroadcastReceiver * Make an Intent object for a restored application or shortcut item that points * to the market page for the item. */ - private Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) { + @Thunk Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) { ComponentName componentName = intent.getComponent(); return getMarketIntent(componentName.getPackageName()); } @@ -3381,23 +3431,14 @@ public class LauncherModel extends BroadcastReceiver .build()); } - /** - * This is called from the code that adds shortcuts from the intent receiver. This - * doesn't have a Cursor, but - */ - public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, - UserHandleCompat user, Context context) { - return getShortcutInfo(manager, intent, user, context, null, -1, -1, null, false); - } - /** * Make an ShortcutInfo object for a shortcut that is an application. * * If c is not null, then it will be used to fill in missing data like the title and icon. */ - public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, + public ShortcutInfo getAppShortcutInfo(PackageManager manager, Intent intent, UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex, - HashMap labelCache, boolean allowMissingTarget) { + boolean allowMissingTarget, boolean useLowResIcon) { if (user == null) { Log.d(TAG, "Null user found in getShortcutInfo"); return null; @@ -3419,56 +3460,32 @@ public class LauncherModel extends BroadcastReceiver } final ShortcutInfo info = new ShortcutInfo(); - - // the resource -- This may implicitly give us back the fallback icon, - // but don't worry about that. All we're doing with usingFallbackIcon is - // to avoid saving lots of copies of that in the database, and most apps - // have icons anyway. - Bitmap icon = mIconCache.getIcon(componentName, lai, labelCache); - - // the db - if (icon == null) { - if (c != null) { - icon = getIconFromCursor(c, iconIndex, context); - } - } - // the fallback icon - if (icon == null) { - icon = mIconCache.getDefaultIcon(user); - info.usingFallbackIcon = true; - } - info.setIcon(icon); - - // From the cache. - if (labelCache != null) { - info.title = labelCache.get(componentName); + mIconCache.getTitleAndIcon(info, componentName, lai, user, false, useLowResIcon); + if (mIconCache.isDefaultIcon(info.getIcon(mIconCache), user) && c != null) { + Bitmap icon = Utilities.createIconBitmap(c, iconIndex, context); + info.setIcon(icon == null ? mIconCache.getDefaultIcon(user) : icon); } - // from the resource - if (info.title == null && lai != null) { - info.title = lai.getLabel(); - if (labelCache != null) { - labelCache.put(componentName, info.title); - } - } // from the db - if (info.title == null) { - if (c != null) { - info.title = c.getString(titleIndex); - } + if (TextUtils.isEmpty(info.title) && c != null) { + info.title = Utilities.trim(c.getString(titleIndex)); } + // fall back to the class name of the activity if (info.title == null) { info.title = componentName.getClassName(); } + info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; info.user = user; - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); + if (lai != null) { + info.flags = AppInfo.initFlags(lai); + } return info; } - static ArrayList filterItemInfos(Collection infos, + static ArrayList filterItemInfos(Iterable infos, ItemInfoFilter f) { HashSet filtered = new HashSet(); for (ItemInfo i : infos) { @@ -3497,7 +3514,7 @@ public class LauncherModel extends BroadcastReceiver return new ArrayList(filtered); } - private ArrayList getItemInfoForComponentName(final ComponentName cname, + @Thunk ArrayList getItemInfoForComponentName(final ComponentName cname, final UserHandleCompat user) { ItemInfoFilter filter = new ItemInfoFilter() { @Override @@ -3509,17 +3526,14 @@ public class LauncherModel extends BroadcastReceiver } } }; - return filterItemInfos(sBgItemsIdMap.values(), filter); + return filterItemInfos(sBgItemsIdMap, filter); } /** * Make an ShortcutInfo object for a shortcut that isn't an application. */ - private ShortcutInfo getShortcutInfo(Cursor c, Context context, - int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex, - int titleIndex) { - - Bitmap icon = null; + @Thunk ShortcutInfo getShortcutInfo(Cursor c, Context context, + int titleIndex, CursorIconInfo iconInfo) { final ShortcutInfo info = new ShortcutInfo(); // Non-app shortcuts are only supported for current user. info.user = UserHandleCompat.myUserHandle(); @@ -3527,77 +3541,18 @@ public class LauncherModel extends BroadcastReceiver // TODO: If there's an explicit component and we can't install that, delete it. - info.title = c.getString(titleIndex); + info.title = Utilities.trim(c.getString(titleIndex)); - int iconType = c.getInt(iconTypeIndex); - switch (iconType) { - case LauncherSettings.Favorites.ICON_TYPE_RESOURCE: - String packageName = c.getString(iconPackageIndex); - String resourceName = c.getString(iconResourceIndex); - info.customIcon = false; - // the resource - icon = Utilities.createIconBitmap(packageName, resourceName, mIconCache, context); - // the db - if (icon == null) { - icon = getIconFromCursor(c, iconIndex, context); - } - // the fallback icon - if (icon == null) { - icon = mIconCache.getDefaultIcon(info.user); - info.usingFallbackIcon = true; - } - break; - case LauncherSettings.Favorites.ICON_TYPE_BITMAP: - icon = getIconFromCursor(c, iconIndex, context); - if (icon == null) { - icon = mIconCache.getDefaultIcon(info.user); - info.customIcon = false; - info.usingFallbackIcon = true; - } else { - info.customIcon = true; - } - break; - default: + Bitmap icon = iconInfo.loadIcon(c, info, context); + // the fallback icon + if (icon == null) { icon = mIconCache.getDefaultIcon(info.user); info.usingFallbackIcon = true; - info.customIcon = false; - break; } info.setIcon(icon); return info; } - Bitmap getIconFromCursor(Cursor c, int iconIndex, Context context) { - @SuppressWarnings("all") // suppress dead code warning - final boolean debug = false; - if (debug) { - Log.d(TAG, "getIconFromCursor app=" - + c.getString(c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE))); - } - byte[] data = c.getBlob(iconIndex); - try { - return Utilities.createIconBitmap( - BitmapFactory.decodeByteArray(data, 0, data.length), context); - } catch (Exception e) { - return null; - } - } - - /** - * Attempts to find an AppWidgetProviderInfo that matches the given component. - */ - static AppWidgetProviderInfo findAppWidgetProviderInfoWithComponent(Context context, - ComponentName component) { - List widgets = - AppWidgetManager.getInstance(context).getInstalledProviders(); - for (AppWidgetProviderInfo info : widgets) { - if (info.provider.equals(component)) { - return info; - } - } - return null; - } - ShortcutInfo infoFromShortcutIntent(Context context, Intent data) { Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); @@ -3621,7 +3576,7 @@ public class LauncherModel extends BroadcastReceiver if (extra instanceof ShortcutIconResource) { iconResource = (ShortcutIconResource) extra; icon = Utilities.createIconBitmap(iconResource.packageName, - iconResource.resourceName, mIconCache, context); + iconResource.resourceName, context); } } @@ -3636,9 +3591,8 @@ public class LauncherModel extends BroadcastReceiver } info.setIcon(icon); - info.title = name; - info.contentDescription = mUserManager.getBadgedLabelForUser( - info.title.toString(), info.user); + info.title = Utilities.trim(name); + info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user); info.intent = intent; info.customIcon = customIcon; info.iconResource = iconResource; @@ -3646,50 +3600,11 @@ public class LauncherModel extends BroadcastReceiver return info; } - boolean queueIconToBeChecked(HashMap cache, ShortcutInfo info, Cursor c, - int iconIndex) { - // If apps can't be on SD, don't even bother. - if (!mAppsCanBeOnRemoveableStorage) { - return false; - } - // If this icon doesn't have a custom icon, check to see - // what's stored in the DB, and if it doesn't match what - // we're going to show, store what we are going to show back - // into the DB. We do this so when we're loading, if the - // package manager can't find an icon (for example because - // the app is on SD) then we can use that instead. - if (!info.customIcon && !info.usingFallbackIcon) { - cache.put(info, c.getBlob(iconIndex)); - return true; - } - return false; - } - void updateSavedIcon(Context context, ShortcutInfo info, byte[] data) { - boolean needSave = false; - try { - if (data != null) { - Bitmap saved = BitmapFactory.decodeByteArray(data, 0, data.length); - Bitmap loaded = info.getIcon(mIconCache); - needSave = !saved.sameAs(loaded); - } else { - needSave = true; - } - } catch (Exception e) { - needSave = true; - } - if (needSave) { - Log.d(TAG, "going to save icon bitmap for info=" + info); - // This is slower than is ideal, but this only happens once - // or when the app is updated with a new icon. - updateItemInDatabase(context, info); - } - } - /** * Return an existing FolderInfo object if we have encountered this ID previously, * or make a new one. */ - private static FolderInfo findOrMakeFolder(HashMap folders, long id) { + @Thunk static FolderInfo findOrMakeFolder(LongArrayMap folders, long id) { // See if a placeholder was created for us already FolderInfo folderInfo = folders.get(id); if (folderInfo == null) { @@ -3700,105 +3615,6 @@ public class LauncherModel extends BroadcastReceiver return folderInfo; } - public static final Comparator getAppNameComparator() { - final Collator collator = Collator.getInstance(); - return new Comparator() { - public final int compare(AppInfo a, AppInfo b) { - if (a.user.equals(b.user)) { - int result = collator.compare(a.title.toString().trim(), - b.title.toString().trim()); - if (result == 0) { - result = a.componentName.compareTo(b.componentName); - } - return result; - } else { - // TODO Need to figure out rules for sorting - // profiles, this puts work second. - return a.user.toString().compareTo(b.user.toString()); - } - } - }; - } - public static final Comparator APP_INSTALL_TIME_COMPARATOR - = new Comparator() { - public final int compare(AppInfo a, AppInfo b) { - if (a.firstInstallTime < b.firstInstallTime) return 1; - if (a.firstInstallTime > b.firstInstallTime) return -1; - return 0; - } - }; - static ComponentName getComponentNameFromResolveInfo(ResolveInfo info) { - if (info.activityInfo != null) { - return new ComponentName(info.activityInfo.packageName, info.activityInfo.name); - } else { - return new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name); - } - } - public static class ShortcutNameComparator implements Comparator { - private Collator mCollator; - private HashMap mLabelCache; - ShortcutNameComparator(PackageManager pm) { - mLabelCache = new HashMap(); - mCollator = Collator.getInstance(); - } - ShortcutNameComparator(HashMap labelCache) { - mLabelCache = labelCache; - mCollator = Collator.getInstance(); - } - public final int compare(LauncherActivityInfoCompat a, LauncherActivityInfoCompat b) { - String labelA, labelB; - ComponentName keyA = a.getComponentName(); - ComponentName keyB = b.getComponentName(); - if (mLabelCache.containsKey(keyA)) { - labelA = mLabelCache.get(keyA).toString(); - } else { - labelA = a.getLabel().toString().trim(); - - mLabelCache.put(keyA, labelA); - } - if (mLabelCache.containsKey(keyB)) { - labelB = mLabelCache.get(keyB).toString(); - } else { - labelB = b.getLabel().toString().trim(); - - mLabelCache.put(keyB, labelB); - } - return mCollator.compare(labelA, labelB); - } - }; - public static class WidgetAndShortcutNameComparator implements Comparator { - private final AppWidgetManagerCompat mManager; - private final PackageManager mPackageManager; - private final HashMap mLabelCache; - private final Collator mCollator; - - WidgetAndShortcutNameComparator(Context context) { - mManager = AppWidgetManagerCompat.getInstance(context); - mPackageManager = context.getPackageManager(); - mLabelCache = new HashMap(); - mCollator = Collator.getInstance(); - } - public final int compare(Object a, Object b) { - String labelA, labelB; - if (mLabelCache.containsKey(a)) { - labelA = mLabelCache.get(a); - } else { - labelA = (a instanceof AppWidgetProviderInfo) - ? mManager.loadLabel((AppWidgetProviderInfo) a) - : ((ResolveInfo) a).loadLabel(mPackageManager).toString().trim(); - mLabelCache.put(a, labelA); - } - if (mLabelCache.containsKey(b)) { - labelB = mLabelCache.get(b); - } else { - labelB = (b instanceof AppWidgetProviderInfo) - ? mManager.loadLabel((AppWidgetProviderInfo) b) - : ((ResolveInfo) b).loadLabel(mPackageManager).toString().trim(); - mLabelCache.put(b, labelB); - } - return mCollator.compare(labelA, labelB); - } - }; static boolean isValidProvider(AppWidgetProviderInfo provider) { return (provider != null) && (provider.provider != null) @@ -3821,4 +3637,20 @@ public class LauncherModel extends BroadcastReceiver public Callbacks getCallback() { return mCallbacks != null ? mCallbacks.get() : null; } + + /** + * @return {@link FolderInfo} if its already loaded. + */ + public FolderInfo findFolderById(Long folderId) { + synchronized (sBgLock) { + return sBgFolders.get(folderId); + } + } + + /** + * @return the looper for the worker thread which can be used to start background tasks. + */ + public static Looper getWorkerLooper() { + return sWorkerThread.getLooper(); + } } diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 1715b02bf7..cc5e18bc17 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -16,9 +16,9 @@ package com.android.launcher3; +import android.annotation.TargetApi; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentProvider; import android.content.ContentProviderOperation; @@ -30,6 +30,7 @@ import android.content.Context; import android.content.Intent; import android.content.OperationApplicationException; import android.content.SharedPreferences; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; @@ -37,10 +38,13 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.database.sqlite.SQLiteStatement; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; -import android.provider.Settings; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import android.os.StrictMode; +import android.os.UserManager; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; @@ -50,52 +54,42 @@ import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.util.ManagedProfileHeuristic; +import com.android.launcher3.util.Thunk; import java.io.File; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; public class LauncherProvider extends ContentProvider { private static final String TAG = "Launcher.LauncherProvider"; private static final boolean LOGD = false; - private static final int DATABASE_VERSION = 20; + private static final int DATABASE_VERSION = 26; static final String OLD_AUTHORITY = "com.android.launcher2.settings"; static final String AUTHORITY = ProviderConfig.AUTHORITY; - // Should we attempt to load anything from the com.android.launcher2 provider? - static final boolean IMPORT_LAUNCHER2_DATABASE = false; - - static final String TABLE_FAVORITES = "favorites"; - static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens"; - static final String PARAMETER_NOTIFY = "notify"; - static final String UPGRADED_FROM_OLD_DATABASE = - "UPGRADED_FROM_OLD_DATABASE"; - static final String EMPTY_DATABASE_CREATED = - "EMPTY_DATABASE_CREATED"; + static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME; + static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME; + static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd"; - private LauncherProviderChangeListener mListener; + private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name"; - /** - * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when - * {@link AppWidgetHost#deleteHost()} is called during database creation. - * Use this to recall {@link AppWidgetHost#startListening()} if needed. - */ - static final Uri CONTENT_APPWIDGET_RESET_URI = - Uri.parse("content://" + AUTHORITY + "/appWidgetReset"); - - private DatabaseHelper mOpenHelper; - private static boolean sJustLoadedFromOldDb; + @Thunk LauncherProviderChangeListener mListener; + @Thunk DatabaseHelper mOpenHelper; @Override public boolean onCreate() { final Context context = getContext(); + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); mOpenHelper = new DatabaseHelper(context); + StrictMode.setThreadPolicy(oldPolicy); LauncherAppState.setLauncherProvider(this); return true; } @@ -106,6 +100,7 @@ public class LauncherProvider extends ContentProvider { public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) { mListener = listener; + mOpenHelper.mListener = mListener; } @Override @@ -133,7 +128,7 @@ public class LauncherProvider extends ContentProvider { return result; } - private static long dbInsertAndCheck(DatabaseHelper helper, + @Thunk static long dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { if (values == null) { throw new RuntimeException("Error: attempting to insert null values"); @@ -151,7 +146,8 @@ public class LauncherProvider extends ContentProvider { // In very limited cases, we support system|signature permission apps to add to the db String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD); - if (externalAdd != null && "true".equals(externalAdd)) { + final boolean isExternalAll = externalAdd != null && "true".equals(externalAdd); + if (isExternalAll) { if (!mOpenHelper.initializeExternalAdd(initialValues)) { return null; } @@ -160,10 +156,17 @@ public class LauncherProvider extends ContentProvider { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); addModifiedTime(initialValues); final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); - if (rowId <= 0) return null; + if (rowId < 0) return null; uri = ContentUris.withAppendedId(uri, rowId); - sendNotify(uri); + notifyListeners(); + + if (isExternalAll) { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app != null) { + app.reloadWorkspace(); + } + } return uri; } @@ -188,7 +191,7 @@ public class LauncherProvider extends ContentProvider { db.endTransaction(); } - sendNotify(uri); + notifyListeners(); return values.length; } @@ -212,7 +215,7 @@ public class LauncherProvider extends ContentProvider { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.delete(args.table, args.where, args.args); - if (count > 0) sendNotify(uri); + if (count > 0) notifyListeners(); return count; } @@ -224,17 +227,80 @@ public class LauncherProvider extends ContentProvider { addModifiedTime(values); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.update(args.table, values, args.where, args.args); - if (count > 0) sendNotify(uri); + if (count > 0) notifyListeners(); return count; } - private void sendNotify(Uri uri) { - String notify = uri.getQueryParameter(PARAMETER_NOTIFY); - if (notify == null || "true".equals(notify)) { - getContext().getContentResolver().notifyChange(uri, null); + @Override + public Bundle call(String method, String arg, Bundle extras) { + if (Binder.getCallingUid() != Process.myUid()) { + return null; } + switch (method) { + case LauncherSettings.Settings.METHOD_GET_BOOLEAN: { + Bundle result = new Bundle(); + result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, + getContext().getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE) + .getBoolean(arg, extras.getBoolean( + LauncherSettings.Settings.EXTRA_DEFAULT_VALUE))); + return result; + } + case LauncherSettings.Settings.METHOD_SET_BOOLEAN: { + boolean value = extras.getBoolean(LauncherSettings.Settings.EXTRA_VALUE); + getContext().getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE) + .edit().putBoolean(arg, value).apply(); + if (mListener != null) { + mListener.onSettingsChanged(arg, value); + } + Bundle result = new Bundle(); + result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, value); + return result; + } + } + return null; + } + + /** + * Deletes any empty folder from the DB. + * @return Ids of deleted folders. + */ + public List deleteEmptyFolders() { + ArrayList folderIds = new ArrayList(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + // Select folders whose id do not match any container value. + String selection = LauncherSettings.Favorites.ITEM_TYPE + " = " + + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND " + + LauncherSettings.Favorites._ID + " NOT IN (SELECT " + + LauncherSettings.Favorites.CONTAINER + " FROM " + + TABLE_FAVORITES + ")"; + Cursor c = db.query(TABLE_FAVORITES, + new String[] {LauncherSettings.Favorites._ID}, + selection, null, null, null, null); + while (c.moveToNext()) { + folderIds.add(c.getLong(0)); + } + c.close(); + if (folderIds.size() > 0) { + db.delete(TABLE_FAVORITES, Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, folderIds), null); + } + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + folderIds.clear(); + } finally { + db.endTransaction(); + } + return folderIds; + } + + private void notifyListeners() { // always notify the backup agent LauncherBackupAgentHelper.dataChanged(getContext()); if (mListener != null) { @@ -242,7 +308,7 @@ public class LauncherProvider extends ContentProvider { } } - private void addModifiedTime(ContentValues values) { + @Thunk static void addModifiedTime(ContentValues values) { values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis()); } @@ -258,32 +324,6 @@ public class LauncherProvider extends ContentProvider { return mOpenHelper.generateNewScreenId(); } - // This is only required one time while loading the workspace during the - // upgrade path, and should never be called from anywhere else. - public void updateMaxScreenId(long maxScreenId) { - mOpenHelper.updateMaxScreenId(maxScreenId); - } - - /** - * @param Should we load the old db for upgrade? first run only. - */ - synchronized public boolean justLoadedOldDb() { - String spKey = LauncherAppState.getSharedPreferencesKey(); - SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE); - - boolean loadedOldDb = false || sJustLoadedFromOldDb; - - sJustLoadedFromOldDb = false; - if (sp.getBoolean(UPGRADED_FROM_OLD_DATABASE, false)) { - - SharedPreferences.Editor editor = sp.edit(); - editor.remove(UPGRADED_FROM_OLD_DATABASE); - editor.commit(); - loadedOldDb = true; - } - return loadedOldDb; - } - /** * Clears all the data for a fresh start. */ @@ -301,9 +341,10 @@ public class LauncherProvider extends ContentProvider { /** * Loads the default workspace based on the following priority scheme: - * 1) From a package provided by play store - * 2) From a partner configuration APK, already in the system image - * 3) The default configuration for the particular device + * 1) From the app restrictions + * 2) From a package provided by play store + * 3) From a partner configuration APK, already in the system image + * 4) The default configuration for the particular device */ synchronized public void loadDefaultFavoritesIfNecessary() { String spKey = LauncherAppState.getSharedPreferencesKey(); @@ -312,9 +353,11 @@ public class LauncherProvider extends ContentProvider { if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) { Log.d(TAG, "loading default workspace"); - AutoInstallsLayout loader = AutoInstallsLayout.get(getContext(), - mOpenHelper.mAppWidgetHost, mOpenHelper); - + AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(); + if (loader == null) { + loader = AutoInstallsLayout.get(getContext(), + mOpenHelper.mAppWidgetHost, mOpenHelper); + } if (loader == null) { final Partner partner = Partner.get(getContext().getPackageManager()); if (partner != null && partner.hasDefaultLayout()) { @@ -332,6 +375,10 @@ public class LauncherProvider extends ContentProvider { if (loader == null) { loader = getDefaultLayoutParser(); } + + // There might be some partially restored DB items, due to buggy restore logic in + // previous versions of launcher. + createEmptyDB(); // Populate favorites table with initial favorites if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0) && usingExternallyProvidedLayout) { @@ -344,9 +391,43 @@ public class LauncherProvider extends ContentProvider { } } + /** + * Creates workspace loader from an XML resource listed in the app restrictions. + * + * @return the loader if the restrictions are set and the resource exists; null otherwise. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() { + // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18 + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + return null; + } + + Context ctx = getContext(); + UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE); + Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName()); + if (bundle == null) { + return null; + } + + String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME); + if (packageName != null) { + try { + Resources targetResources = ctx.getPackageManager() + .getResourcesForApplication(packageName); + return AutoInstallsLayout.get(ctx, packageName, targetResources, + mOpenHelper.mAppWidgetHost, mOpenHelper); + } catch (NameNotFoundException e) { + Log.e(TAG, "Target package for restricted profile not found", e); + return null; + } + } + return null; + } + private DefaultLayoutParser getDefaultLayoutParser() { int defaultLayout = LauncherAppState.getInstance() - .getDynamicGrid().getDeviceProfile().defaultLayoutId; + .getInvariantDeviceProfile().defaultLayoutId; return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost, mOpenHelper, getContext().getResources(), defaultLayout); } @@ -356,17 +437,15 @@ public class LauncherProvider extends ContentProvider { Uri.parse(getContext().getString(R.string.old_launcher_provider_uri))); } - private static interface ContentValuesCallback { - public void onRow(ContentValues values); + public void updateFolderItemsRank() { + mOpenHelper.updateFolderItemsRank(mOpenHelper.getWritableDatabase(), false); } - private static boolean shouldImportLauncher2Database(Context context) { - boolean isTablet = context.getResources().getBoolean(R.bool.is_tablet); - - // We don't import the old databse for tablets, as the grid size has changed. - return !isTablet && IMPORT_LAUNCHER2_DATABASE; + public void convertShortcutsToLauncherActivities() { + mOpenHelper.convertShortcutsToLauncherActivities(mOpenHelper.getWritableDatabase()); } + public void deleteDatabase() { // Are you sure? (y/n) final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); @@ -376,16 +455,19 @@ public class LauncherProvider extends ContentProvider { SQLiteDatabase.deleteDatabase(dbFile); } mOpenHelper = new DatabaseHelper(getContext()); + mOpenHelper.mListener = mListener; } private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback { private final Context mContext; - private final AppWidgetHost mAppWidgetHost; + @Thunk final AppWidgetHost mAppWidgetHost; private long mMaxItemId = -1; private long mMaxScreenId = -1; private boolean mNewDbCreated = false; + @Thunk LauncherProviderChangeListener mListener; + DatabaseHelper(Context context) { super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION); mContext = context; @@ -405,17 +487,6 @@ public class LauncherProvider extends ContentProvider { return mNewDbCreated; } - /** - * Send notification that we've deleted the {@link AppWidgetHost}, - * probably as part of the initial database creation. The receiver may - * want to re-call {@link AppWidgetHost#startListening()} to ensure - * callbacks are correctly set. - */ - private void sendAppWidgetResetNotify() { - final ContentResolver resolver = mContext.getContentResolver(); - resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null); - } - @Override public void onCreate(SQLiteDatabase db) { if (LOGD) Log.d(TAG, "creating new launcher database"); @@ -450,53 +521,44 @@ public class LauncherProvider extends ContentProvider { "appWidgetProvider TEXT," + "modified INTEGER NOT NULL DEFAULT 0," + "restored INTEGER NOT NULL DEFAULT 0," + - "profileId INTEGER DEFAULT " + userSerialNumber + + "profileId INTEGER DEFAULT " + userSerialNumber + "," + + "rank INTEGER NOT NULL DEFAULT 0," + + "options INTEGER NOT NULL DEFAULT 0" + ");"); addWorkspacesTable(db); // Database was just created, so wipe any previous widgets if (mAppWidgetHost != null) { mAppWidgetHost.deleteHost(); - sendAppWidgetResetNotify(); - } - if (shouldImportLauncher2Database(mContext)) { - // Try converting the old database - ContentValuesCallback permuteScreensCb = new ContentValuesCallback() { - public void onRow(ContentValues values) { - int container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER); - if (container == Favorites.CONTAINER_DESKTOP) { - int screen = values.getAsInteger(LauncherSettings.Favorites.SCREEN); - screen = (int) upgradeLauncherDb_permuteScreens(screen); - values.put(LauncherSettings.Favorites.SCREEN, screen); + /** + * Send notification that we've deleted the {@link AppWidgetHost}, + * probably as part of the initial database creation. The receiver may + * want to re-call {@link AppWidgetHost#startListening()} to ensure + * callbacks are correctly set. + */ + new MainThreadExecutor().execute(new Runnable() { + + @Override + public void run() { + if (mListener != null) { + mListener.onAppWidgetHostReset(); } } - }; - Uri uri = Uri.parse("content://" + Settings.AUTHORITY + - "/old_favorites?notify=true"); - if (!convertDatabase(db, uri, permuteScreensCb, true)) { - // Try and upgrade from the Launcher2 db - uri = Uri.parse(mContext.getString(R.string.old_launcher_provider_uri)); - if (!convertDatabase(db, uri, permuteScreensCb, false)) { - // If we fail, then set a flag to load the default workspace - setFlagEmptyDbCreated(); - return; - } - } - // Right now, in non-default workspace cases, we want to run the final - // upgrade code (ie. to fix workspace screen indices -> ids, etc.), so - // set that flag too. - setFlagJustLoadedOldDb(); - } else { - // Fresh and clean launcher DB. - mMaxItemId = initializeMaxItemId(db); - setFlagEmptyDbCreated(); + }); } + + // Fresh and clean launcher DB. + mMaxItemId = initializeMaxItemId(db); + setFlagEmptyDbCreated(); + + // When a new DB is created, remove all previously stored managed profile information. + ManagedProfileHeuristic.processAllUsers(Collections.emptyList(), mContext); } private void addWorkspacesTable(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" + - LauncherSettings.WorkspaceScreens._ID + " INTEGER," + + LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," + LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," + LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" + ");"); @@ -536,333 +598,120 @@ public class LauncherProvider extends ContentProvider { private void setFlagJustLoadedOldDb() { String spKey = LauncherAppState.getSharedPreferencesKey(); SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true); - editor.putBoolean(EMPTY_DATABASE_CREATED, false); - editor.commit(); + sp.edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit(); } private void setFlagEmptyDbCreated() { String spKey = LauncherAppState.getSharedPreferencesKey(); SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sp.edit(); - editor.putBoolean(EMPTY_DATABASE_CREATED, true); - editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false); - editor.commit(); - } - - // We rearrange the screens from the old launcher - // 12345 -> 34512 - private long upgradeLauncherDb_permuteScreens(long screen) { - if (screen >= 2) { - return screen - 2; - } else { - return screen + 3; - } - } - - private boolean convertDatabase(SQLiteDatabase db, Uri uri, - ContentValuesCallback cb, boolean deleteRows) { - if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade"); - boolean converted = false; - - final ContentResolver resolver = mContext.getContentResolver(); - Cursor cursor = null; - - try { - cursor = resolver.query(uri, null, null, null, null); - } catch (Exception e) { - // Ignore - } - - // We already have a favorites database in the old provider - if (cursor != null) { - try { - if (cursor.getCount() > 0) { - converted = copyFromCursor(db, cursor, cb) > 0; - if (converted && deleteRows) { - resolver.delete(uri, null, null); - } - } - } finally { - cursor.close(); - } - } - - if (converted) { - // Convert widgets from this import into widgets - if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade"); - convertWidgets(db); - - // Update max item id - mMaxItemId = initializeMaxItemId(db); - if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId); - } - - return converted; - } - - private int copyFromCursor(SQLiteDatabase db, Cursor c, ContentValuesCallback cb) { - final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); - final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); - final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); - final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); - final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); - final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); - final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); - final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); - final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); - final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); - final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); - final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); - final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); - final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE); - - ContentValues[] rows = new ContentValues[c.getCount()]; - int i = 0; - while (c.moveToNext()) { - ContentValues values = new ContentValues(c.getColumnCount()); - values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex)); - values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex)); - values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex)); - values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex)); - values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex)); - values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex)); - values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex)); - values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex)); - values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex)); - values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1); - values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex)); - values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex)); - values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex)); - values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex)); - values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex)); - if (cb != null) { - cb.onRow(values); - } - rows[i++] = values; - } - - int total = 0; - if (i > 0) { - db.beginTransaction(); - try { - int numValues = rows.length; - for (i = 0; i < numValues; i++) { - if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) { - return 0; - } else { - total++; - } - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - return total; + sp.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit(); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion); - - int version = oldVersion; - if (version < 3) { - // upgrade 1,2 -> 3 added appWidgetId column - db.beginTransaction(); - try { - // Insert new column for holding appWidgetIds - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;"); - db.setTransactionSuccessful(); - version = 3; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); + switch (oldVersion) { + // The version cannot be lower that 12, as Launcher3 never supported a lower + // version of the DB. + case 12: { + // With the new shrink-wrapped and re-orderable workspaces, it makes sense + // to persist workspace screens and their relative order. + mMaxScreenId = 0; + addWorkspacesTable(db); } - - // Convert existing widgets only if table upgrade was successful - if (version == 3) { - convertWidgets(db); + case 13: { + db.beginTransaction(); + try { + // Insert new column for holding widget provider name + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN appWidgetProvider TEXT;"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + // Old version remains, which means we wipe old data + break; + } finally { + db.endTransaction(); + } + } + case 14: { + db.beginTransaction(); + try { + // Insert new column for holding update timestamp + db.execSQL("ALTER TABLE favorites " + + "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); + db.execSQL("ALTER TABLE workspaceScreens " + + "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.e(TAG, ex.getMessage(), ex); + // Old version remains, which means we wipe old data + break; + } finally { + db.endTransaction(); + } + } + case 15: { + if (!addIntegerColumn(db, Favorites.RESTORED, 0)) { + // Old version remains, which means we wipe old data + break; + } + } + case 16: { + // We use the db version upgrade here to identify users who may not have seen + // clings yet (because they weren't available), but for whom the clings are now + // available (tablet users). Because one of the possible cling flows (migration) + // is very destructive (wipes out workspaces), we want to prevent this from showing + // until clear data. We do so by marking that the clings have been shown. + LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext); + } + case 17: { + // No-op + } + case 18: { + // Due to a data loss bug, some users may have items associated with screen ids + // which no longer exist. Since this can cause other problems, and since the user + // will never see these items anyway, we use database upgrade as an opportunity to + // clean things up. + removeOrphanedItems(db); + } + case 19: { + // Add userId column + if (!addProfileColumn(db)) { + // Old version remains, which means we wipe old data + break; + } + } + case 20: + if (!updateFolderItemsRank(db, true)) { + break; + } + case 21: + // Recreate workspace table with screen id a primary key + if (!recreateWorkspaceTable(db)) { + break; + } + case 22: { + if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) { + // Old version remains, which means we wipe old data + break; + } + } + case 23: + // No-op + case 24: + ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext); + case 25: + convertShortcutsToLauncherActivities(db); + case 26: { + // DB Upgraded successfully + return; } } - if (version < 4) { - version = 4; - } - - // Where's version 5? - // - Donut and sholes on 2.0 shipped with version 4 of launcher1. - // - Passion shipped on 2.1 with version 6 of launcher3 - // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1 - // but version 5 on there was the updateContactsShortcuts change - // which was version 6 in launcher 2 (first shipped on passion 2.1r1). - // The updateContactsShortcuts change is idempotent, so running it twice - // is okay so we'll do that when upgrading the devices that shipped with it. - if (version < 6) { - // We went from 3 to 5 screens. Move everything 1 to the right - db.beginTransaction(); - try { - db.execSQL("UPDATE favorites SET screen=(screen + 1);"); - db.setTransactionSuccessful(); - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); - } - - // We added the fast track. - if (updateContactsShortcuts(db)) { - version = 6; - } - } - - if (version < 7) { - // Version 7 gets rid of the special search widget. - convertWidgets(db); - version = 7; - } - - if (version < 8) { - // Version 8 (froyo) has the icons all normalized. This should - // already be the case in practice, but we now rely on it and don't - // resample the images each time. - normalizeIcons(db); - version = 8; - } - - if (version < 9) { - // The max id is not yet set at this point (onUpgrade is triggered in the ctor - // before it gets a change to get set, so we need to read it here when we use it) - if (mMaxItemId == -1) { - mMaxItemId = initializeMaxItemId(db); - } - - // Add default hotseat icons - loadFavorites(db, new DefaultLayoutParser(mContext, mAppWidgetHost, this, - mContext.getResources(), R.xml.update_workspace)); - version = 9; - } - - // We bumped the version three time during JB, once to update the launch flags, once to - // update the override for the default launch animation and once to set the mimetype - // to improve startup performance - if (version < 12) { - // Contact shortcuts need a different set of flags to be launched now - // The updateContactsShortcuts change is idempotent, so we can keep using it like - // back in the Donut days - updateContactsShortcuts(db); - version = 12; - } - - if (version < 13) { - // With the new shrink-wrapped and re-orderable workspaces, it makes sense - // to persist workspace screens and their relative order. - mMaxScreenId = 0; - - // This will never happen in the wild, but when we switch to using workspace - // screen ids, redo the import from old launcher. - sJustLoadedFromOldDb = true; - - addWorkspacesTable(db); - version = 13; - } - - if (version < 14) { - db.beginTransaction(); - try { - // Insert new column for holding widget provider name - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN appWidgetProvider TEXT;"); - db.setTransactionSuccessful(); - version = 14; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); - } - } - - if (version < 15) { - db.beginTransaction(); - try { - // Insert new column for holding update timestamp - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); - db.execSQL("ALTER TABLE workspaceScreens " + - "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;"); - db.setTransactionSuccessful(); - version = 15; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); - } - } - - - if (version < 16) { - db.beginTransaction(); - try { - // Insert new column for holding restore status - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;"); - db.setTransactionSuccessful(); - version = 16; - } catch (SQLException ex) { - // Old version remains, which means we wipe old data - Log.e(TAG, ex.getMessage(), ex); - } finally { - db.endTransaction(); - } - } - - if (version < 17) { - // We use the db version upgrade here to identify users who may not have seen - // clings yet (because they weren't available), but for whom the clings are now - // available (tablet users). Because one of the possible cling flows (migration) - // is very destructive (wipes out workspaces), we want to prevent this from showing - // until clear data. We do so by marking that the clings have been shown. - LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext); - version = 17; - } - - if (version < 18) { - // No-op - version = 18; - } - - if (version < 19) { - // Due to a data loss bug, some users may have items associated with screen ids - // which no longer exist. Since this can cause other problems, and since the user - // will never see these items anyway, we use database upgrade as an opportunity to - // clean things up. - removeOrphanedItems(db); - version = 19; - } - - if (version < 20) { - // Add userId column - if (addProfileColumn(db)) { - version = 20; - } - // else old version remains, which means we wipe old data - } - - if (version != DATABASE_VERSION) { - Log.w(TAG, "Destroying all old data."); - db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); - db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); - - onCreate(db); - } + // DB was not upgraded + Log.w(TAG, "Destroying all old data."); + createEmptyDB(db); } @Override @@ -873,7 +722,6 @@ public class LauncherProvider extends ContentProvider { createEmptyDB(db); } - /** * Clears all the data for a fresh start. */ @@ -883,18 +731,132 @@ public class LauncherProvider extends ContentProvider { onCreate(db); } - private boolean addProfileColumn(SQLiteDatabase db) { + /** + * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid + * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}. + */ + @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) { + db.beginTransaction(); + Cursor c = null; + SQLiteStatement updateStmt = null; + + try { + // Only consider the primary user as other users can't have a shortcut. + long userSerial = UserManagerCompat.getInstance(mContext) + .getSerialNumberForUser(UserHandleCompat.myUserHandle()); + c = db.query(TABLE_FAVORITES, new String[] { + Favorites._ID, + Favorites.INTENT, + }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial, + null, null, null, null); + + updateStmt = db.compileStatement("UPDATE favorites SET itemType=" + + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?"); + + final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); + final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT); + + while (c.moveToNext()) { + String intentDescription = c.getString(intentIndex); + Intent intent; + try { + intent = Intent.parseUri(intentDescription, 0); + } catch (URISyntaxException e) { + Log.e(TAG, "Unable to parse intent", e); + continue; + } + + if (!Utilities.isLauncherAppTarget(intent)) { + continue; + } + + long id = c.getLong(idIndex); + updateStmt.bindLong(1, id); + updateStmt.executeUpdateDelete(); + } + db.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.w(TAG, "Error deduping shortcuts", ex); + } finally { + db.endTransaction(); + if (c != null) { + c.close(); + } + if (updateStmt != null) { + updateStmt.close(); + } + } + } + + /** + * Recreates workspace table and migrates data to the new table. + */ + public boolean recreateWorkspaceTable(SQLiteDatabase db) { db.beginTransaction(); try { - UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); - // Default to the serial number of this user, for older - // shortcuts. - long userSerialNumber = userManager.getSerialNumberForUser( - UserHandleCompat.myUserHandle()); - // Insert new column for holding user serial number - db.execSQL("ALTER TABLE favorites " + - "ADD COLUMN profileId INTEGER DEFAULT " - + userSerialNumber + ";"); + Cursor c = db.query(TABLE_WORKSPACE_SCREENS, + new String[] {LauncherSettings.WorkspaceScreens._ID}, + null, null, null, null, + LauncherSettings.WorkspaceScreens.SCREEN_RANK); + ArrayList sortedIDs = new ArrayList(); + long maxId = 0; + try { + while (c.moveToNext()) { + Long id = c.getLong(0); + if (!sortedIDs.contains(id)) { + sortedIDs.add(id); + maxId = Math.max(maxId, id); + } + } + } finally { + c.close(); + } + + db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS); + addWorkspacesTable(db); + + // Add all screen ids back + int total = sortedIDs.size(); + for (int i = 0; i < total; i++) { + ContentValues values = new ContentValues(); + values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i)); + values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); + addModifiedTime(values); + db.insertOrThrow(TABLE_WORKSPACE_SCREENS, null, values); + } + db.setTransactionSuccessful(); + mMaxScreenId = maxId; + } catch (SQLException ex) { + // Old version remains, which means we wipe old data + Log.e(TAG, ex.getMessage(), ex); + return false; + } finally { + db.endTransaction(); + } + return true; + } + + @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) { + db.beginTransaction(); + try { + if (addRankColumn) { + // Insert new column for holding rank + db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;"); + } + + // Get a map for folder ID to folder width + Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites" + + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)" + + " GROUP BY container;", + new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}); + + while (c.moveToNext()) { + db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE " + + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;", + new Object[] {c.getLong(1) + 1, c.getLong(0)}); + } + + c.close(); db.setTransactionSuccessful(); } catch (SQLException ex) { // Old version remains, which means we wipe old data @@ -906,142 +868,30 @@ public class LauncherProvider extends ContentProvider { return true; } - private boolean updateContactsShortcuts(SQLiteDatabase db) { - final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, - new int[] { Favorites.ITEM_TYPE_SHORTCUT }); + private boolean addProfileColumn(SQLiteDatabase db) { + UserManagerCompat userManager = UserManagerCompat.getInstance(mContext); + // Default to the serial number of this user, for older + // shortcuts. + long userSerialNumber = userManager.getSerialNumberForUser( + UserHandleCompat.myUserHandle()); + return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber); + } - Cursor c = null; - final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT"; + private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) { db.beginTransaction(); try { - // Select and iterate through each matching widget - c = db.query(TABLE_FAVORITES, - new String[] { Favorites._ID, Favorites.INTENT }, - selectWhere, null, null, null, null); - if (c == null) return false; - - if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount()); - - final int idIndex = c.getColumnIndex(Favorites._ID); - final int intentIndex = c.getColumnIndex(Favorites.INTENT); - - while (c.moveToNext()) { - long favoriteId = c.getLong(idIndex); - final String intentUri = c.getString(intentIndex); - if (intentUri != null) { - try { - final Intent intent = Intent.parseUri(intentUri, 0); - android.util.Log.d("Home", intent.toString()); - final Uri uri = intent.getData(); - if (uri != null) { - final String data = uri.toString(); - if ((Intent.ACTION_VIEW.equals(intent.getAction()) || - actionQuickContact.equals(intent.getAction())) && - (data.startsWith("content://contacts/people/") || - data.startsWith("content://com.android.contacts/" + - "contacts/lookup/"))) { - - final Intent newIntent = new Intent(actionQuickContact); - // When starting from the launcher, start in a new, cleared task - // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we - // clear the whole thing preemptively here since - // QuickContactActivity will finish itself when launching other - // detail activities. - newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_CLEAR_TASK); - newIntent.putExtra( - Launcher.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true); - newIntent.setData(uri); - // Determine the type and also put that in the shortcut - // (that can speed up launch a bit) - newIntent.setDataAndType(uri, newIntent.resolveType(mContext)); - - final ContentValues values = new ContentValues(); - values.put(LauncherSettings.Favorites.INTENT, - newIntent.toUri(0)); - - String updateWhere = Favorites._ID + "=" + favoriteId; - db.update(TABLE_FAVORITES, values, updateWhere, null); - } - } - } catch (RuntimeException ex) { - Log.e(TAG, "Problem upgrading shortcut", ex); - } catch (URISyntaxException e) { - Log.e(TAG, "Problem upgrading shortcut", e); - } - } - } - + db.execSQL("ALTER TABLE favorites ADD COLUMN " + + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";"); db.setTransactionSuccessful(); } catch (SQLException ex) { - Log.w(TAG, "Problem while upgrading contacts", ex); + Log.e(TAG, ex.getMessage(), ex); return false; } finally { db.endTransaction(); - if (c != null) { - c.close(); - } } - return true; } - private void normalizeIcons(SQLiteDatabase db) { - Log.d(TAG, "normalizing icons"); - - db.beginTransaction(); - Cursor c = null; - SQLiteStatement update = null; - try { - boolean logged = false; - update = db.compileStatement("UPDATE favorites " - + "SET icon=? WHERE _id=?"); - - c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" + - Favorites.ICON_TYPE_BITMAP, null); - - final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); - final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON); - - while (c.moveToNext()) { - long id = c.getLong(idIndex); - byte[] data = c.getBlob(iconIndex); - try { - Bitmap bitmap = Utilities.createIconBitmap( - BitmapFactory.decodeByteArray(data, 0, data.length), - mContext); - if (bitmap != null) { - update.bindLong(1, id); - data = ItemInfo.flattenBitmap(bitmap); - if (data != null) { - update.bindBlob(2, data); - update.execute(); - } - bitmap.recycle(); - } - } catch (Exception e) { - if (!logged) { - Log.e(TAG, "Failed normalizing icon " + id, e); - } else { - Log.e(TAG, "Also failed normalizing icon " + id); - } - logged = true; - } - } - db.setTransactionSuccessful(); - } catch (SQLException ex) { - Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex); - } finally { - db.endTransaction(); - if (update != null) { - update.close(); - } - if (c != null) { - c.close(); - } - } - } - // Generates a new ID to use for an object in your database. This method should be only // called from the main UI thread. As an exception, we do call it when we call the // constructor from the worker thread; however, this doesn't extend until after the @@ -1075,23 +925,7 @@ public class LauncherProvider extends ContentProvider { } private long initializeMaxItemId(SQLiteDatabase db) { - Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null); - - // get the result - final int maxIdIndex = 0; - long id = -1; - if (c != null && c.moveToNext()) { - id = c.getLong(maxIdIndex); - } - if (c != null) { - c.close(); - } - - if (id == -1) { - throw new RuntimeException("Error: could not query max item id"); - } - - return id; + return getMaxId(db, TABLE_FAVORITES); } // Generates a new ID to use for an workspace screen in your database. This method @@ -1104,128 +938,14 @@ public class LauncherProvider extends ContentProvider { throw new RuntimeException("Error: max screen id was not initialized"); } mMaxScreenId += 1; - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true); return mMaxScreenId; } - public void updateMaxScreenId(long maxScreenId) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - updateMaxScreenId(): " + maxScreenId, true); - mMaxScreenId = maxScreenId; - } - private long initializeMaxScreenId(SQLiteDatabase db) { - Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null); - - // get the result - final int maxIdIndex = 0; - long id = -1; - if (c != null && c.moveToNext()) { - id = c.getLong(maxIdIndex); - } - if (c != null) { - c.close(); - } - - if (id == -1) { - throw new RuntimeException("Error: could not query max screen id"); - } - - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true); - return id; + return getMaxId(db, TABLE_WORKSPACE_SCREENS); } - /** - * Upgrade existing clock and photo frame widgets into their new widget - * equivalents. - */ - private void convertWidgets(SQLiteDatabase db) { - final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); - final int[] bindSources = new int[] { - Favorites.ITEM_TYPE_WIDGET_CLOCK, - Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME, - Favorites.ITEM_TYPE_WIDGET_SEARCH, - }; - - final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources); - - Cursor c = null; - - db.beginTransaction(); - try { - // Select and iterate through each matching widget - c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE }, - selectWhere, null, null, null, null); - - if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount()); - - final ContentValues values = new ContentValues(); - while (c != null && c.moveToNext()) { - long favoriteId = c.getLong(0); - int favoriteType = c.getInt(1); - - // Allocate and update database with new appWidgetId - try { - int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); - - if (LOGD) { - Log.d(TAG, "allocated appWidgetId=" + appWidgetId - + " for favoriteId=" + favoriteId); - } - values.clear(); - values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); - values.put(Favorites.APPWIDGET_ID, appWidgetId); - - // Original widgets might not have valid spans when upgrading - if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) { - values.put(LauncherSettings.Favorites.SPANX, 4); - values.put(LauncherSettings.Favorites.SPANY, 1); - } else { - values.put(LauncherSettings.Favorites.SPANX, 2); - values.put(LauncherSettings.Favorites.SPANY, 2); - } - - String updateWhere = Favorites._ID + "=" + favoriteId; - db.update(TABLE_FAVORITES, values, updateWhere, null); - - if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) { - // TODO: check return value - appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, - new ComponentName("com.android.alarmclock", - "com.android.alarmclock.AnalogAppWidgetProvider")); - } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) { - // TODO: check return value - appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, - new ComponentName("com.android.camera", - "com.android.camera.PhotoAppWidgetProvider")); - } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) { - // TODO: check return value - appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, - getSearchWidgetProvider()); - } - } catch (RuntimeException ex) { - Log.e(TAG, "Problem allocating appWidgetId", ex); - } - } - - db.setTransactionSuccessful(); - } catch (SQLException ex) { - Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex); - } finally { - db.endTransaction(); - if (c != null) { - c.close(); - } - } - - // Update max item id - mMaxItemId = initializeMaxItemId(db); - if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId); - } - - private boolean initializeExternalAdd(ContentValues values) { + @Thunk boolean initializeExternalAdd(ContentValues values) { // 1. Ensure that externally added items have a valid item id long id = generateNewItemId(); values.put(LauncherSettings.Favorites._ID, id); @@ -1312,7 +1032,7 @@ public class LauncherProvider extends ContentProvider { return rank; } - private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { + @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { ArrayList screenIds = new ArrayList(); // TODO: Use multiple loaders with fall-back and transaction. int count = loader.loadLayout(db, screenIds); @@ -1339,12 +1059,7 @@ public class LauncherProvider extends ContentProvider { return count; } - private ComponentName getSearchWidgetProvider() { - AppWidgetProviderInfo searchProvider = Utilities.getSearchWidgetProvider(mContext); - return (searchProvider == null) ? null : searchProvider.provider; - } - - private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { + @Thunk void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) { final ContentResolver resolver = mContext.getContentResolver(); Cursor c = null; int count = 0; @@ -1395,10 +1110,10 @@ public class LauncherProvider extends ContentProvider { int curY = 0; final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - final int width = (int) grid.numColumns; - final int height = (int) grid.numRows; - final int hotseatWidth = (int) grid.numHotseatIcons; + final InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); + final int width = (int) profile.numColumns; + final int height = (int) profile.numRows; + final int hotseatWidth = (int) profile.numHotseatIcons; final HashSet seenIntents = new HashSet(c.getCount()); @@ -1540,7 +1255,7 @@ public class LauncherProvider extends ContentProvider { int hotseatX = hotseat.keyAt(idx); ContentValues values = hotseat.valueAt(idx); - if (hotseatX == grid.hotseatAllAppsRank) { + if (hotseatX == profile.hotseatAllAppsRank) { // let's drop this in the next available hole in the hotseat while (++hotseatX < hotseatWidth) { if (hotseat.get(hotseatX) == null) { @@ -1619,6 +1334,8 @@ public class LauncherProvider extends ContentProvider { } finally { db.endTransaction(); } + + updateFolderItemsRank(db, false); } } finally { c.close(); @@ -1639,18 +1356,24 @@ public class LauncherProvider extends ContentProvider { } /** - * Build a query string that will match any row where the column matches - * anything in the values list. + * @return the max _id in the provided table. */ - private static String buildOrWhereString(String column, int[] values) { - StringBuilder selectWhere = new StringBuilder(); - for (int i = values.length - 1; i >= 0; i--) { - selectWhere.append(column).append("=").append(values[i]); - if (i > 0) { - selectWhere.append(" OR "); - } + @Thunk static long getMaxId(SQLiteDatabase db, String table) { + Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null); + // get the result + long id = -1; + if (c != null && c.moveToNext()) { + id = c.getLong(0); } - return selectWhere.toString(); + if (c != null) { + c.close(); + } + + if (id == -1) { + throw new RuntimeException("Error: could not query max id in " + table); + } + + return id; } static class SqlArguments { diff --git a/src/com/android/launcher3/LauncherProviderChangeListener.java b/src/com/android/launcher3/LauncherProviderChangeListener.java index 0de96fbc48..1b78e9c186 100644 --- a/src/com/android/launcher3/LauncherProviderChangeListener.java +++ b/src/com/android/launcher3/LauncherProviderChangeListener.java @@ -8,4 +8,8 @@ package com.android.launcher3; public interface LauncherProviderChangeListener { public void onLauncherProviderChange(); + + public void onSettingsChanged(String settings, boolean value); + + public void onAppWidgetHostReset(); } diff --git a/src/com/android/launcher3/LauncherScroller.java b/src/com/android/launcher3/LauncherScroller.java index 3bd0a78c47..a9b49556bb 100644 --- a/src/com/android/launcher3/LauncherScroller.java +++ b/src/com/android/launcher3/LauncherScroller.java @@ -20,7 +20,6 @@ import android.animation.TimeInterpolator; import android.content.Context; import android.hardware.SensorManager; import android.os.Build; -import android.util.FloatMath; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -409,7 +408,7 @@ public class LauncherScroller { float dx = (float) (mFinalX - mStartX); float dy = (float) (mFinalY - mStartY); - float hyp = FloatMath.sqrt(dx * dx + dy * dy); + float hyp = (float) Math.hypot(dx, dy); float ndx = dx / hyp; float ndy = dy / hyp; @@ -426,7 +425,7 @@ public class LauncherScroller { mMode = FLING_MODE; mFinished = false; - float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); + float velocity = (float) Math.hypot(velocityX, velocityY); mVelocity = velocity; mDuration = getSplineFlingDuration(velocity); diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 3553702836..f2c85a1950 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -19,17 +19,19 @@ package com.android.launcher3; import android.net.Uri; import android.provider.BaseColumns; +import com.android.launcher3.config.ProviderConfig; + /** * Settings related utilities. */ -class LauncherSettings { +public class LauncherSettings { /** Columns required on table staht will be subject to backup and restore. */ static interface ChangeLogColumns extends BaseColumns { /** * The time of the last update to this row. *

Type: INTEGER

*/ - static final String MODIFIED = "modified"; + public static final String MODIFIED = "modified"; } static interface BaseLauncherColumns extends ChangeLogColumns { @@ -37,7 +39,7 @@ class LauncherSettings { * Descriptive name of the gesture that can be displayed to the user. *

Type: TEXT

*/ - static final String TITLE = "title"; + public static final String TITLE = "title"; /** * The Intent URL of the gesture, describing what it points to. This @@ -45,58 +47,58 @@ class LauncherSettings { * an Intent that can be launched. *

Type: TEXT

*/ - static final String INTENT = "intent"; + public static final String INTENT = "intent"; /** * The type of the gesture * *

Type: INTEGER

*/ - static final String ITEM_TYPE = "itemType"; + public static final String ITEM_TYPE = "itemType"; /** * The gesture is an application */ - static final int ITEM_TYPE_APPLICATION = 0; + public static final int ITEM_TYPE_APPLICATION = 0; /** * The gesture is an application created shortcut */ - static final int ITEM_TYPE_SHORTCUT = 1; + public static final int ITEM_TYPE_SHORTCUT = 1; /** * The icon type. *

Type: INTEGER

*/ - static final String ICON_TYPE = "iconType"; + public static final String ICON_TYPE = "iconType"; /** * The icon is a resource identified by a package name and an integer id. */ - static final int ICON_TYPE_RESOURCE = 0; + public static final int ICON_TYPE_RESOURCE = 0; /** * The icon is a bitmap. */ - static final int ICON_TYPE_BITMAP = 1; + public static final int ICON_TYPE_BITMAP = 1; /** * The icon package name, if icon type is ICON_TYPE_RESOURCE. *

Type: TEXT

*/ - static final String ICON_PACKAGE = "iconPackage"; + public static final String ICON_PACKAGE = "iconPackage"; /** * The icon resource id, if icon type is ICON_TYPE_RESOURCE. *

Type: TEXT

*/ - static final String ICON_RESOURCE = "iconResource"; + public static final String ICON_RESOURCE = "iconResource"; /** * The custom icon bitmap, if icon type is ICON_TYPE_BITMAP. *

Type: BLOB

*/ - static final String ICON = "icon"; + public static final String ICON = "icon"; } /** @@ -104,72 +106,59 @@ class LauncherSettings { * * Tracks the order of workspace screens. */ - static final class WorkspaceScreens implements ChangeLogColumns { + public static final class WorkspaceScreens implements ChangeLogColumns { + + public static final String TABLE_NAME = "workspaceScreens"; + /** * The content:// style URL for this table */ static final Uri CONTENT_URI = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_WORKSPACE_SCREENS + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + ProviderConfig.AUTHORITY + "/" + TABLE_NAME); /** * The rank of this screen -- ie. how it is ordered relative to the other screens. *

Type: INTEGER

*/ - static final String SCREEN_RANK = "screenRank"; + public static final String SCREEN_RANK = "screenRank"; } /** * Favorites. */ - static final class Favorites implements BaseLauncherColumns { - /** - * The content:// style URL for this table - */ - static final Uri CONTENT_URI = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); + public static final class Favorites implements BaseLauncherColumns { + + public static final String TABLE_NAME = "favorites"; /** * The content:// style URL for this table */ - static final Uri OLD_CONTENT_URI = Uri.parse("content://" + - LauncherProvider.OLD_AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); - - /** - * The content:// style URL for this table. When this Uri is used, no notification is - * sent if the content changes. - */ - static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" + - LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + - "?" + LauncherProvider.PARAMETER_NOTIFY + "=false"); + public static final Uri CONTENT_URI = Uri.parse("content://" + + ProviderConfig.AUTHORITY + "/" + TABLE_NAME); /** * The content:// style URL for a given row, identified by its id. * * @param id The row id. - * @param notify True to send a notification is the content changes. * * @return The unique content URL for the specified row. */ - static Uri getContentUri(long id, boolean notify) { - return Uri.parse("content://" + LauncherProvider.AUTHORITY + - "/" + LauncherProvider.TABLE_FAVORITES + "/" + id + "?" + - LauncherProvider.PARAMETER_NOTIFY + "=" + notify); + static Uri getContentUri(long id) { + return Uri.parse("content://" + ProviderConfig.AUTHORITY + + "/" + TABLE_NAME + "/" + id); } /** * The container holding the favorite *

Type: INTEGER

*/ - static final String CONTAINER = "container"; + public static final String CONTAINER = "container"; /** * The icon is a resource identified by a package name and an integer id. */ - static final int CONTAINER_DESKTOP = -100; - static final int CONTAINER_HOTSEAT = -101; + public static final int CONTAINER_DESKTOP = -100; + public static final int CONTAINER_HOTSEAT = -101; static final String containerToString(int container) { switch (container) { @@ -183,33 +172,33 @@ class LauncherSettings { * The screen holding the favorite (if container is CONTAINER_DESKTOP) *

Type: INTEGER

*/ - static final String SCREEN = "screen"; + public static final String SCREEN = "screen"; /** * The X coordinate of the cell holding the favorite * (if container is CONTAINER_HOTSEAT or CONTAINER_HOTSEAT) *

Type: INTEGER

*/ - static final String CELLX = "cellX"; + public static final String CELLX = "cellX"; /** * The Y coordinate of the cell holding the favorite * (if container is CONTAINER_DESKTOP) *

Type: INTEGER

*/ - static final String CELLY = "cellY"; + public static final String CELLY = "cellY"; /** * The X span of the cell holding the favorite *

Type: INTEGER

*/ - static final String SPANX = "spanX"; + public static final String SPANX = "spanX"; /** * The Y span of the cell holding the favorite *

Type: INTEGER

*/ - static final String SPANY = "spanY"; + public static final String SPANY = "spanY"; /** * The profile id of the item in the cell. @@ -217,12 +206,12 @@ class LauncherSettings { * Type: INTEGER *

*/ - static final String PROFILE_ID = "profileId"; + public static final String PROFILE_ID = "profileId"; /** * The favorite is a user created folder */ - static final int ITEM_TYPE_FOLDER = 2; + public static final int ITEM_TYPE_FOLDER = 2; /** * The favorite is a live folder @@ -231,26 +220,35 @@ class LauncherSettings { * exist within the launcher database will be ignored when loading. That said, these * entries in the database may still exist, and are not automatically stripped. */ + @Deprecated static final int ITEM_TYPE_LIVE_FOLDER = 3; /** * The favorite is a widget */ - static final int ITEM_TYPE_APPWIDGET = 4; + public static final int ITEM_TYPE_APPWIDGET = 4; + + /** + * The favorite is a custom widget provided by the launcher + */ + public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5; /** * The favorite is a clock */ + @Deprecated static final int ITEM_TYPE_WIDGET_CLOCK = 1000; /** * The favorite is a search widget */ + @Deprecated static final int ITEM_TYPE_WIDGET_SEARCH = 1001; /** * The favorite is a photo frame */ + @Deprecated static final int ITEM_TYPE_WIDGET_PHOTO_FRAME = 1002; /** @@ -258,7 +256,7 @@ class LauncherSettings { * *

Type: INTEGER

*/ - static final String APPWIDGET_ID = "appWidgetId"; + public static final String APPWIDGET_ID = "appWidgetId"; /** * The ComponentName of the widget provider @@ -281,6 +279,7 @@ class LauncherSettings { * live folders to find the content provider. *

Type: TEXT

*/ + @Deprecated static final String URI = "uri"; /** @@ -290,12 +289,40 @@ class LauncherSettings { * @see android.provider.LiveFolders#DISPLAY_MODE_GRID * @see android.provider.LiveFolders#DISPLAY_MODE_LIST */ + @Deprecated static final String DISPLAY_MODE = "displayMode"; /** * Boolean indicating that his item was restored and not yet successfully bound. *

Type: INTEGER

*/ - static final String RESTORED = "restored"; + public static final String RESTORED = "restored"; + + /** + * Indicates the position of the item inside an auto-arranged view like folder or hotseat. + *

Type: INTEGER

+ */ + public static final String RANK = "rank"; + + /** + * Stores general flag based options for {@link ItemInfo}s. + *

Type: INTEGER

+ */ + public static final String OPTIONS = "options"; + } + + /** + * Launcher settings + */ + public static final class Settings { + + public static final Uri CONTENT_URI = Uri.parse("content://" + + ProviderConfig.AUTHORITY + "/settings"); + + public static final String METHOD_GET_BOOLEAN = "get_boolean_setting"; + public static final String METHOD_SET_BOOLEAN = "set_boolean_setting"; + + public static final String EXTRA_VALUE = "value"; + public static final String EXTRA_DEFAULT_VALUE = "default_value"; } } diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java new file mode 100644 index 0000000000..d69b7432de --- /dev/null +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -0,0 +1,762 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; +import android.annotation.SuppressLint; +import android.content.res.Resources; +import android.util.Log; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.util.UiThreadCircularReveal; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.WidgetsContainerView; + +import java.util.HashMap; + +/** + * TODO: figure out what kind of tests we can write for this + * + * Things to test when changing the following class. + * - Home from workspace + * - from center screen + * - from other screens + * - Home from all apps + * - from center screen + * - from other screens + * - Back from all apps + * - from center screen + * - from other screens + * - Launch app from workspace and quit + * - with back + * - with home + * - Launch app from all apps and quit + * - with back + * - with home + * - Go to a screen that's not the default, then all + * apps, and launch and app, and go back + * - with back + * -with home + * - On workspace, long press power and go back + * - with back + * - with home + * - On all apps, long press power and go back + * - with back + * - with home + * - On workspace, power off + * - On all apps, power off + * - Launch an app and turn off the screen while in that app + * - Go back with home key + * - Go back with back key TODO: make this not go to workspace + * - From all apps + * - From workspace + * - Enter and exit car mode (becuase it causes an extra configuration changed) + * - From all apps + * - From the center workspace + * - From another workspace + */ +public class LauncherStateTransitionAnimation { + + /** + * Callbacks made during the state transition + */ + interface Callbacks { + public void onStateTransitionHideSearchBar(); + } + + /** + * Private callbacks made during transition setup. + */ + static abstract class PrivateTransitionCallbacks { + float getMaterialRevealViewFinalAlpha(View revealView) { + return 0; + } + float getMaterialRevealViewStartFinalRadius() { + return 0; + } + AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(View revealView, + View buttonView) { + return null; + } + void onTransitionComplete() {} + } + + public static final String TAG = "LauncherStateTransitionAnimation"; + + // Flags to determine how to set the layers on views before the transition animation + public static final int BUILD_LAYER = 0; + public static final int BUILD_AND_SET_LAYER = 1; + public static final int SINGLE_FRAME_DELAY = 16; + + @Thunk Launcher mLauncher; + @Thunk Callbacks mCb; + @Thunk AnimatorSet mStateAnimation; + + public LauncherStateTransitionAnimation(Launcher l, Callbacks cb) { + mLauncher = l; + mCb = cb; + } + + /** + * Starts an animation to the apps view. + * + * @param startSearchAfterTransition Immediately starts app search after the transition to + * All Apps is completed. + */ + public void startAnimationToAllApps(final boolean animated, + final boolean startSearchAfterTransition) { + final AllAppsContainerView toView = mLauncher.getAppsView(); + final View buttonView = mLauncher.getAllAppsButton(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + public float getMaterialRevealViewFinalAlpha(View revealView) { + return 1f; + } + @Override + public float getMaterialRevealViewStartFinalRadius() { + int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize; + return allAppsButtonSize / 2; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View allAppsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + allAppsButtonView.setVisibility(View.INVISIBLE); + } + public void onAnimationEnd(Animator animation) { + allAppsButtonView.setVisibility(View.VISIBLE); + } + }; + } + @Override + void onTransitionComplete() { + if (startSearchAfterTransition) { + toView.startAppsSearch(); + } + } + }; + // Only animate the search bar if animating from spring loaded mode back to all apps + startAnimationToOverlay(Workspace.State.NORMAL_HIDDEN, buttonView, toView, + toView.getContentView(), toView.getRevealView(), toView.getSearchBarView(), + animated, true /* hideSearchBar */, cb); + } + + /** + * Starts an animation to the widgets view. + */ + public void startAnimationToWidgets(final boolean animated) { + final WidgetsContainerView toView = mLauncher.getWidgetsView(); + final View buttonView = mLauncher.getWidgetsButton(); + + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + public float getMaterialRevealViewFinalAlpha(View revealView) { + return 0.3f; + } + }; + startAnimationToOverlay(Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, + toView.getContentView(), toView.getRevealView(), null, animated, + true /* hideSearchBar */, cb); + } + + /** + * Starts and animation to the workspace from the current overlay view. + */ + public void startAnimationToWorkspace(final Launcher.State fromState, + final Workspace.State toWorkspaceState, final int toWorkspacePage, + final boolean animated, final Runnable onCompleteRunnable) { + if (toWorkspaceState != Workspace.State.NORMAL && + toWorkspaceState != Workspace.State.SPRING_LOADED && + toWorkspaceState != Workspace.State.OVERVIEW) { + Log.e(TAG, "Unexpected call to startAnimationToWorkspace"); + } + + if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED) { + startAnimationToWorkspaceFromAllApps(toWorkspaceState, toWorkspacePage, + animated, onCompleteRunnable); + } else { + startAnimationToWorkspaceFromWidgets(toWorkspaceState, toWorkspacePage, + animated, onCompleteRunnable); + } + } + + /** + * Creates and starts a new animation to a particular overlay view. + */ + @SuppressLint("NewApi") + private void startAnimationToOverlay(final Workspace.State toWorkspaceState, + final View buttonView, final View toView, final View contentView, final View revealView, + final View overlaySearchBarView, final boolean animated, final boolean hideSearchBar, + final PrivateTransitionCallbacks pCb) { + final Resources res = mLauncher.getResources(); + final boolean material = Utilities.isLmpOrAbove(); + final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); + final int itemsAlphaStagger = + res.getInteger(R.integer.config_overlayItemsAlphaStagger); + + final View fromView = mLauncher.getWorkspace(); + + final HashMap layerViews = new HashMap<>(); + + // If for some reason our views aren't initialized, don't animate + boolean initialized = buttonView != null; + + // Cancel the current animation + cancelAnimation(); + + // Create the workspace animation. + // NOTE: this call apparently also sets the state for the workspace if !animated + Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState, -1, + animated, overlaySearchBarView != null /* hasOverlaySearchBar */, layerViews); + + if (animated && initialized) { + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + + // Setup the reveal view animation + int width = revealView.getMeasuredWidth(); + int height = revealView.getMeasuredHeight(); + float revealRadius = (float) Math.hypot(width / 2, height / 2); + revealView.setVisibility(View.VISIBLE); + revealView.setAlpha(0f); + revealView.setTranslationY(0f); + revealView.setTranslationX(0f); + + // Calculate the final animation values + final float revealViewToAlpha; + final float revealViewToXDrift; + final float revealViewToYDrift; + if (material) { + int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, + buttonView, null); + revealViewToAlpha = pCb.getMaterialRevealViewFinalAlpha(revealView); + revealViewToYDrift = buttonViewToPanelDelta[1]; + revealViewToXDrift = buttonViewToPanelDelta[0]; + } else { + revealViewToAlpha = 0f; + revealViewToYDrift = 2 * height / 3; + revealViewToXDrift = 0; + } + + // Create the animators + PropertyValuesHolder panelAlpha = + PropertyValuesHolder.ofFloat("alpha", revealViewToAlpha, 1f); + PropertyValuesHolder panelDriftY = + PropertyValuesHolder.ofFloat("translationY", revealViewToYDrift, 0); + PropertyValuesHolder panelDriftX = + PropertyValuesHolder.ofFloat("translationX", revealViewToXDrift, 0); + ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView, + panelAlpha, panelDriftY, panelDriftX); + panelAlphaAndDrift.setDuration(revealDuration); + panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + + // Play the animation + layerViews.put(revealView, BUILD_AND_SET_LAYER); + mStateAnimation.play(panelAlphaAndDrift); + + if (overlaySearchBarView != null) { + overlaySearchBarView.setAlpha(0f); + ObjectAnimator searchBarAlpha = ObjectAnimator.ofFloat(overlaySearchBarView, "alpha", 0f, 1f); + searchBarAlpha.setDuration(100); + searchBarAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER); + mStateAnimation.play(searchBarAlpha); + } + + // Setup the animation for the content view + contentView.setVisibility(View.VISIBLE); + contentView.setAlpha(0f); + contentView.setTranslationY(revealViewToYDrift); + layerViews.put(contentView, BUILD_AND_SET_LAYER); + + // Create the individual animators + ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY", + revealViewToYDrift, 0); + pageDrift.setDuration(revealDuration); + pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); + pageDrift.setStartDelay(itemsAlphaStagger); + mStateAnimation.play(pageDrift); + + ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f); + itemsAlpha.setDuration(revealDuration); + itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); + itemsAlpha.setStartDelay(itemsAlphaStagger); + mStateAnimation.play(itemsAlpha); + + if (material) { + float startRadius = pCb.getMaterialRevealViewStartFinalRadius(); + AnimatorListenerAdapter listener = pCb.getMaterialRevealViewAnimatorListener( + revealView, buttonView); + Animator reveal = UiThreadCircularReveal.createCircularReveal(revealView, width / 2, + height / 2, startRadius, revealRadius); + reveal.setDuration(revealDuration); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + if (listener != null) { + reveal.addListener(listener); + } + mStateAnimation.play(reveal); + } + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + + // Disable all necessary layers + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + if (hideSearchBar) { + mCb.onStateTransitionHideSearchBar(); + } + + // This can hold unnecessary references to views. + mStateAnimation = null; + pCb.onTransitionComplete(); + } + + }); + + // Play the workspace animation + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + + // Dispatch the prepare transition signal + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + + + final AnimatorSet stateAnimation = mStateAnimation; + final Runnable startAnimRunnable = new Runnable() { + public void run() { + // Check that mStateAnimation hasn't changed while + // we waited for a layout/draw pass + if (mStateAnimation != stateAnimation) + return; + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + + // Enable all necessary layers + boolean isLmpOrAbove = Utilities.isLmpOrAbove(); + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) { + v.buildLayer(); + } + } + + // Focus the new view + toView.requestFocus(); + + mStateAnimation.start(); + } + }; + toView.bringToFront(); + toView.setVisibility(View.VISIBLE); + toView.post(startAnimRunnable); + } else { + toView.setTranslationX(0.0f); + toView.setTranslationY(0.0f); + toView.setScaleX(1.0f); + toView.setScaleY(1.0f); + toView.setVisibility(View.VISIBLE); + toView.bringToFront(); + + // Show the content view + contentView.setVisibility(View.VISIBLE); + + if (hideSearchBar) { + mCb.onStateTransitionHideSearchBar(); + } + + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + pCb.onTransitionComplete(); + } + } + + /** + * Starts and animation to the workspace from the apps view. + */ + private void startAnimationToWorkspaceFromAllApps(final Workspace.State toWorkspaceState, + final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { + AllAppsContainerView appsView = mLauncher.getAppsView(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + int[] mAllAppsToPanelDelta; + + @Override + float getMaterialRevealViewFinalAlpha(View revealView) { + // No alpha anim from all apps + return 1f; + } + @Override + float getMaterialRevealViewStartFinalRadius() { + int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize; + return allAppsButtonSize / 2; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View allAppsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + // We set the alpha instead of visibility to ensure that the focus does not + // get taken from the all apps view + allAppsButtonView.setVisibility(View.VISIBLE); + allAppsButtonView.setAlpha(0f); + } + public void onAnimationEnd(Animator animation) { + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + + // Show the all apps button, and focus it + allAppsButtonView.setAlpha(1f); + } + }; + } + }; + // Only animate the search bar if animating to spring loaded mode from all apps + startAnimationToWorkspaceFromOverlay(toWorkspaceState, toWorkspacePage, + mLauncher.getAllAppsButton(), appsView, appsView.getContentView(), + appsView.getRevealView(), appsView.getSearchBarView(), animated, + onCompleteRunnable, cb); + } + + /** + * Starts and animation to the workspace from the widgets view. + */ + private void startAnimationToWorkspaceFromWidgets(final Workspace.State toWorkspaceState, + final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { + final WidgetsContainerView widgetsView = mLauncher.getWidgetsView(); + PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { + @Override + float getMaterialRevealViewFinalAlpha(View revealView) { + return 0.3f; + } + @Override + public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener( + final View revealView, final View widgetsButtonView) { + return new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + // Hide the reveal view + revealView.setVisibility(View.INVISIBLE); + } + }; + } + }; + startAnimationToWorkspaceFromOverlay(toWorkspaceState, toWorkspacePage, + mLauncher.getWidgetsButton(), widgetsView, widgetsView.getContentView(), + widgetsView.getRevealView(), null, animated, onCompleteRunnable, cb); + } + + /** + * Creates and starts a new animation to the workspace. + */ + private void startAnimationToWorkspaceFromOverlay(final Workspace.State toWorkspaceState, + final int toWorkspacePage, final View buttonView, final View fromView, + final View contentView, final View revealView, final View overlaySearchBarView, + final boolean animated, final Runnable onCompleteRunnable, + final PrivateTransitionCallbacks pCb) { + final Resources res = mLauncher.getResources(); + final boolean material = Utilities.isLmpOrAbove(); + final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); + final int itemsAlphaStagger = + res.getInteger(R.integer.config_overlayItemsAlphaStagger); + + final View toView = mLauncher.getWorkspace(); + + final HashMap layerViews = new HashMap<>(); + + // If for some reason our views aren't initialized, don't animate + boolean initialized = buttonView != null; + + // Cancel the current animation + cancelAnimation(); + + // Create the workspace animation. + // NOTE: this call apparently also sets the state for the workspace if !animated + Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState, + toWorkspacePage, animated, overlaySearchBarView != null /* hasOverlaySearchBar */, + layerViews); + + if (animated && initialized) { + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + + // Play the workspace animation + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + + // hideAppsCustomizeHelper is called in some cases when it is already hidden + // don't perform all these no-op animations. In particularly, this was causing + // the all-apps button to pop in and out. + if (fromView.getVisibility() == View.VISIBLE) { + int width = revealView.getMeasuredWidth(); + int height = revealView.getMeasuredHeight(); + float revealRadius = (float) Math.hypot(width / 2, height / 2); + revealView.setVisibility(View.VISIBLE); + revealView.setAlpha(1f); + revealView.setTranslationY(0); + layerViews.put(revealView, BUILD_AND_SET_LAYER); + + // Calculate the final animation values + final float revealViewToXDrift; + final float revealViewToYDrift; + if (material) { + int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, + buttonView, null); + revealViewToYDrift = buttonViewToPanelDelta[1]; + revealViewToXDrift = buttonViewToPanelDelta[0]; + } else { + revealViewToYDrift = 2 * height / 3; + revealViewToXDrift = 0; + } + + // The vertical motion of the apps panel should be delayed by one frame + // from the conceal animation in order to give the right feel. We correspondingly + // shorten the duration so that the slide and conceal end at the same time. + TimeInterpolator decelerateInterpolator = material ? + new LogDecelerateInterpolator(100, 0) : + new DecelerateInterpolator(1f); + ObjectAnimator panelDriftY = ObjectAnimator.ofFloat(revealView, "translationY", + 0, revealViewToYDrift); + panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY); + panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelDriftY.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelDriftY); + + ObjectAnimator panelDriftX = ObjectAnimator.ofFloat(revealView, "translationX", + 0, revealViewToXDrift); + panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY); + panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelDriftX.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelDriftX); + + // Setup animation for the reveal panel alpha + final float revealViewToAlpha = !material ? 0f : + pCb.getMaterialRevealViewFinalAlpha(revealView); + if (revealViewToAlpha != 1f) { + ObjectAnimator panelAlpha = ObjectAnimator.ofFloat(revealView, "alpha", + 1f, revealViewToAlpha); + panelAlpha.setDuration(material ? revealDuration : 150); + panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); + panelAlpha.setInterpolator(decelerateInterpolator); + mStateAnimation.play(panelAlpha); + } + + // Setup the animation for the content view + layerViews.put(contentView, BUILD_AND_SET_LAYER); + + // Create the individual animators + ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY", + 0, revealViewToYDrift); + contentView.setTranslationY(0); + pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY); + pageDrift.setInterpolator(decelerateInterpolator); + pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); + mStateAnimation.play(pageDrift); + + contentView.setAlpha(1f); + ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 1f, 0f); + itemsAlpha.setDuration(100); + itemsAlpha.setInterpolator(decelerateInterpolator); + mStateAnimation.play(itemsAlpha); + + if (overlaySearchBarView != null) { + overlaySearchBarView.setAlpha(1f); + ObjectAnimator searchAlpha = ObjectAnimator.ofFloat(overlaySearchBarView, "alpha", 1f, 0f); + searchAlpha.setDuration(material ? 100 : 150); + searchAlpha.setInterpolator(decelerateInterpolator); + searchAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); + layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER); + mStateAnimation.play(searchAlpha); + } + + if (material) { + // Animate the all apps button + float finalRadius = pCb.getMaterialRevealViewStartFinalRadius(); + AnimatorListenerAdapter listener = + pCb.getMaterialRevealViewAnimatorListener(revealView, buttonView); + Animator reveal = UiThreadCircularReveal.createCircularReveal(revealView, width / 2, + height / 2, revealRadius, finalRadius); + reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); + reveal.setDuration(revealDuration); + reveal.setStartDelay(itemsAlphaStagger); + if (listener != null) { + reveal.addListener(listener); + } + mStateAnimation.play(reveal); + } + + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + } + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + + // Run any queued runnables + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + + // Disable all necessary layers + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + // Reset page transforms + if (contentView != null) { + contentView.setTranslationX(0); + contentView.setTranslationY(0); + contentView.setAlpha(1); + } + if (overlaySearchBarView != null) { + overlaySearchBarView.setAlpha(1f); + } + + // This can hold unnecessary references to views. + mStateAnimation = null; + pCb.onTransitionComplete(); + } + }); + + final AnimatorSet stateAnimation = mStateAnimation; + final Runnable startAnimRunnable = new Runnable() { + public void run() { + // Check that mStateAnimation hasn't changed while + // we waited for a layout/draw pass + if (mStateAnimation != stateAnimation) + return; + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + + // Enable all necessary layers + boolean isLmpOrAbove = Utilities.isLmpOrAbove(); + for (View v : layerViews.keySet()) { + if (layerViews.get(v) == BUILD_AND_SET_LAYER) { + v.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) { + v.buildLayer(); + } + } + mStateAnimation.start(); + } + }; + fromView.post(startAnimRunnable); + } else { + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionStart(fromView, animated, true); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + dispatchOnLauncherTransitionStart(toView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + pCb.onTransitionComplete(); + + // Run any queued runnables + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + } + + + /** + * Dispatches the prepare-transition event to suitable views. + */ + void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionPrepare(mLauncher, animated, + toWorkspace); + } + } + + /** + * Dispatches the start-transition event to suitable views. + */ + void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStart(mLauncher, animated, + toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 0f); + } + + /** + * Dispatches the step-transition event to suitable views. + */ + void dispatchOnLauncherTransitionStep(View v, float t) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionStep(mLauncher, t); + } + } + + /** + * Dispatches the end-transition event to suitable views. + */ + void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) { + if (v instanceof LauncherTransitionable) { + ((LauncherTransitionable) v).onLauncherTransitionEnd(mLauncher, animated, + toWorkspace); + } + + // Update the workspace transition step as well + dispatchOnLauncherTransitionStep(v, 1f); + } + + /** + * Cancels the current animation. + */ + private void cancelAnimation() { + if (mStateAnimation != null) { + mStateAnimation.setDuration(0); + mStateAnimation.cancel(); + mStateAnimation = null; + } + } +} diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherTransitionable.java similarity index 55% rename from src/com/android/launcher3/LauncherApplication.java rename to src/com/android/launcher3/LauncherTransitionable.java index 8b179f1e7f..49af6928a6 100644 --- a/src/com/android/launcher3/LauncherApplication.java +++ b/src/com/android/launcher3/LauncherTransitionable.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,12 @@ package com.android.launcher3; -import android.app.Application; - -public class LauncherApplication extends Application { - @Override - public void onCreate() { - super.onCreate(); - LauncherAppState.setApplicationContext(this); - LauncherAppState.getInstance(); - } - - @Override - public void onTerminate() { - super.onTerminate(); - LauncherAppState.getInstance().onTerminate(); - } -} \ No newline at end of file +/** + * An interface to get callbacks during a launcher transition. + */ +public interface LauncherTransitionable { + void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace); + void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace); + void onLauncherTransitionStep(Launcher l, float t); + void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace); +} diff --git a/src/com/android/launcher3/MemoryTracker.java b/src/com/android/launcher3/MemoryTracker.java index 2d37c809ee..067a50f970 100644 --- a/src/com/android/launcher3/MemoryTracker.java +++ b/src/com/android/launcher3/MemoryTracker.java @@ -101,7 +101,7 @@ public class MemoryTracker extends Service { public void startTrackingProcess(int pid, String name, long start) { synchronized (mLock) { - final Long lpid = new Long(pid); + final Long lpid = Long.valueOf(pid); if (mPids.contains(lpid)) return; diff --git a/src/com/android/launcher3/PackageChangedReceiver.java b/src/com/android/launcher3/PackageChangedReceiver.java deleted file mode 100644 index e59f6d81d7..0000000000 --- a/src/com/android/launcher3/PackageChangedReceiver.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.launcher3; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class PackageChangedReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, Intent intent) { - final String packageName = intent.getData().getSchemeSpecificPart(); - - if (packageName == null || packageName.length() == 0) { - // they sent us a bad intent - return; - } - // in rare cases the receiver races with the application to set up LauncherAppState - LauncherAppState.setApplicationContext(context.getApplicationContext()); - LauncherAppState app = LauncherAppState.getInstance(); - WidgetPreviewLoader.removePackageFromDb(app.getWidgetPreviewCacheDb(), packageName); - } -} diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 7d65f46861..218c1a36f3 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -19,16 +19,16 @@ package com.android.launcher3; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.LayoutTransition; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Matrix; -import android.graphics.PointF; import android.graphics.Rect; +import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -46,21 +46,13 @@ import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; -import android.view.animation.AnimationUtils; -import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; + +import com.android.launcher3.util.LauncherEdgeEffect; +import com.android.launcher3.util.Thunk; import java.util.ArrayList; -interface Page { - public int getPageChildCount(); - public View getChildOnPageAt(int i); - public void removeAllViewsOnPage(); - public void removeViewOnPageAt(int i); - public int indexOfChildOnPage(View v); -} - /** * An abstraction of the original Workspace which supports browsing through a * sequential list of "pages" @@ -74,28 +66,21 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private static final int MIN_LENGTH_FOR_FLING = 25; protected static final int PAGE_SNAP_ANIMATION_DURATION = 750; - protected static final int OVER_SCROLL_PAGE_SNAP_ANIMATION_DURATION = 350; protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; protected static final float NANOTIME_DIV = 1000000000.0f; - private static final float OVERSCROLL_ACCELERATE_FACTOR = 2; - private static final float OVERSCROLL_DAMP_FACTOR = 0.07f; - private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; // The page is moved more than halfway, automatically move to the next page on touch up. private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; + private static final float MAX_SCROLL_PROGRESS = 1.0f; + // The following constants need to be scaled based on density. The scaled versions will be // assigned to the corresponding member variables below. private static final int FLING_THRESHOLD_VELOCITY = 500; private static final int MIN_SNAP_VELOCITY = 1500; private static final int MIN_FLING_VELOCITY = 250; - // We are disabling touch interaction of the widget region for factory ROM. - private static final boolean DISABLE_TOUCH_INTERACTION = false; - private static final boolean DISABLE_TOUCH_SIDE_PAGES = true; - private static final boolean DISABLE_FLING_TO_DELETE = true; - public static final int INVALID_RESTORE_PAGE = -1001; private boolean mFreeScroll = false; @@ -124,7 +109,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected LauncherScroller mScroller; private Interpolator mDefaultInterpolator; private VelocityTracker mVelocityTracker; - private int mPageSpacing = 0; + @Thunk int mPageSpacing = 0; private float mParentDownMotionX; private float mParentDownMotionY; @@ -156,7 +141,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected OnLongClickListener mLongClickListener; protected int mTouchSlop; - private int mPagingTouchSlop; private int mMaximumVelocity; protected int mPageLayoutWidthGap; protected int mPageLayoutHeightGap; @@ -164,15 +148,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected int mCellCountY = 0; protected boolean mCenterPagesVertically; protected boolean mAllowOverScroll = true; - protected int mUnboundedScrollX; protected int[] mTempVisiblePagesRange = new int[2]; protected boolean mForceDrawAllChildrenNextFrame; - private boolean mSpacePagesAutomatically = false; - - // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise - // it is equal to the scaled overscroll position. We use a separate value so as to prevent - // the screens from continuing to translate beyond the normal bounds. - protected int mOverScrollX; protected static final int INVALID_POINTER = -1; @@ -180,37 +157,16 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private PageSwitchListener mPageSwitchListener; - protected ArrayList mDirtyPageContent; - - // If true, syncPages and syncPageItems will be called to refresh pages - protected boolean mContentIsRefreshable = true; - // If true, modify alpha of neighboring pages as user scrolls left/right protected boolean mFadeInAdjacentScreens = false; - // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding - // to switch to a new page - protected boolean mUsePagingTouchSlop = true; - - // If true, the subclass should directly update scrollX itself in its computeScroll method - // (SmoothPagedView does this) - protected boolean mDeferScrollUpdate = false; - protected boolean mDeferLoadAssociatedPagesUntilScrollCompletes = false; - protected boolean mIsPageMoving = false; - // All syncs and layout passes are deferred until data is ready. - protected boolean mIsDataReady = false; - - protected boolean mAllowLongPress = true; - private boolean mWasInOverscroll = false; // Page Indicator - private int mPageIndicatorViewId; - private PageIndicator mPageIndicator; - private boolean mAllowPagedViewAnimations = true; - + @Thunk int mPageIndicatorViewId; + @Thunk PageIndicator mPageIndicator; // The viewport whether the pages are to be contained (the actual view may be larger than the // viewport) private Rect mViewport = new Rect(); @@ -218,16 +174,15 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Reordering // We use the min scale to determine how much to expand the actually PagedView measured // dimensions such that when we are zoomed out, the view is not clipped - private int REORDERING_DROP_REPOSITION_DURATION = 200; - protected int REORDERING_REORDER_REPOSITION_DURATION = 300; - protected int REORDERING_ZOOM_IN_OUT_DURATION = 250; - private int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80; + private static int REORDERING_DROP_REPOSITION_DURATION = 200; + @Thunk static int REORDERING_REORDER_REPOSITION_DURATION = 300; + private static int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80; + private float mMinScale = 1f; private boolean mUseMinScale = false; protected View mDragView; - protected AnimatorSet mZoomInOutAnim; private Runnable mSidePageHoverRunnable; - private int mSidePageHoverIndex = -1; + @Thunk int mSidePageHoverIndex = -1; // This variable's scope is only for the duration of startReordering() and endReordering() private boolean mReorderingStarted = false; // This variable's scope is for the duration of startReordering() and after the zoomIn() @@ -239,30 +194,17 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc private Runnable mPostReorderingPreZoomInRunnable; // Convenience/caching - private Matrix mTmpInvMatrix = new Matrix(); - private float[] mTmpPoint = new float[2]; - private int[] mTmpIntPoint = new int[2]; - private Rect mTmpRect = new Rect(); - private Rect mAltTmpRect = new Rect(); - - // Fling to delete - private int FLING_TO_DELETE_FADE_OUT_DURATION = 350; - private float FLING_TO_DELETE_FRICTION = 0.035f; - // The degrees specifies how much deviation from the up vector to still consider a fling "up" - private float FLING_TO_DELETE_MAX_FLING_DEGREES = 65f; - protected int mFlingToDeleteThresholdVelocity = -1400; - // Drag to delete - private boolean mDeferringForDelete = false; - private int DELETE_SLIDE_IN_SIDE_PAGE_DURATION = 250; - private int DRAG_TO_DELETE_FADE_OUT_DURATION = 350; - - // Drop to delete - private View mDeleteDropTarget; - - // Bouncer - private boolean mTopAlignPageWhenShrinkingForBouncer = false; + private static final Matrix sTmpInvMatrix = new Matrix(); + private static final float[] sTmpPoint = new float[2]; + private static final int[] sTmpIntPoint = new int[2]; + private static final Rect sTmpRect = new Rect(); protected final Rect mInsets = new Rect(); + protected final boolean mIsRtl; + + // Edge effect + private final LauncherEdgeEffect mEdgeGlowLeft = new LauncherEdgeEffect(); + private final LauncherEdgeEffect mEdgeGlowRight = new LauncherEdgeEffect(); public interface PageSwitchListener { void onPageSwitch(View newPage, int newPageIndex); @@ -290,6 +232,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc a.recycle(); setHapticFeedbackEnabled(false); + mIsRtl = Utilities.isRtl(getResources()); init(); } @@ -297,8 +240,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc * Initializes various states for this workspace. */ protected void init() { - mDirtyPageContent = new ArrayList(); - mDirtyPageContent.ensureCapacity(32); mScroller = new LauncherScroller(getContext()); setDefaultInterpolator(new ScrollInterpolator()); mCurrentPage = 0; @@ -306,18 +247,19 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledPagingTouchSlop(); - mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mDensity = getResources().getDisplayMetrics().density; - // Scale the fling-to-delete threshold by the density - mFlingToDeleteThresholdVelocity = - (int) (mFlingToDeleteThresholdVelocity * mDensity); - mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); setOnHierarchyChangeListener(this); + setWillNotDraw(false); + } + + protected void setEdgeGlowColor(int color) { + mEdgeGlowLeft.setColor(color); + mEdgeGlowRight.setColor(color); } protected void setDefaultInterpolator(Interpolator interpolator) { @@ -333,7 +275,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc ViewGroup grandParent = (ViewGroup) parent.getParent(); if (mPageIndicator == null && mPageIndicatorViewId > -1) { mPageIndicator = (PageIndicator) grandParent.findViewById(mPageIndicatorViewId); - mPageIndicator.removeAllMarkers(mAllowPagedViewAnimations); + mPageIndicator.removeAllMarkers(true); ArrayList markers = new ArrayList(); @@ -341,7 +283,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc markers.add(getPageIndicatorMarker(i)); } - mPageIndicator.addMarkers(markers, mAllowPagedViewAnimations); + mPageIndicator.addMarkers(markers, true); OnClickListener listener = getPageIndicatorClickListener(); if (listener != null) { @@ -359,33 +301,31 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return null; } + @Override protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); // Unhook the page indicator mPageIndicator = null; } - void setDeleteDropTarget(View v) { - mDeleteDropTarget = v; - } - // Convenience methods to map points from self to parent and vice versa - float[] mapPointFromViewToParent(View v, float x, float y) { - mTmpPoint[0] = x; - mTmpPoint[1] = y; - v.getMatrix().mapPoints(mTmpPoint); - mTmpPoint[0] += v.getLeft(); - mTmpPoint[1] += v.getTop(); - return mTmpPoint; + private float[] mapPointFromViewToParent(View v, float x, float y) { + sTmpPoint[0] = x; + sTmpPoint[1] = y; + v.getMatrix().mapPoints(sTmpPoint); + sTmpPoint[0] += v.getLeft(); + sTmpPoint[1] += v.getTop(); + return sTmpPoint; } - float[] mapPointFromParentToView(View v, float x, float y) { - mTmpPoint[0] = x - v.getLeft(); - mTmpPoint[1] = y - v.getTop(); - v.getMatrix().invert(mTmpInvMatrix); - mTmpInvMatrix.mapPoints(mTmpPoint); - return mTmpPoint; + private float[] mapPointFromParentToView(View v, float x, float y) { + sTmpPoint[0] = x - v.getLeft(); + sTmpPoint[1] = y - v.getTop(); + v.getMatrix().invert(sTmpInvMatrix); + sTmpInvMatrix.mapPoints(sTmpPoint); + return sTmpPoint; } - void updateDragViewTranslationDuringDrag() { + private void updateDragViewTranslationDuringDrag() { if (mDragView != null) { float x = (mLastMotionX - mDownMotionX) + (getScrollX() - mDownScrollX) + (mDragViewBaselineLeft - mDragView.getLeft()); @@ -452,34 +392,16 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - /** - * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. - */ - public boolean isLayoutRtl() { - return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); - } - - /** - * Called by subclasses to mark that data is ready, and that we can begin loading and laying - * out pages. - */ - protected void setDataIsReady() { - mIsDataReady = true; - } - - protected boolean isDataReady() { - return mIsDataReady; - } - /** * Returns the index of the currently displayed page. - * - * @return The index of the currently displayed page. */ - int getCurrentPage() { + public int getCurrentPage() { return mCurrentPage; } + /** + * Returns the index of page to be shown immediately afterwards. + */ int getNextPage() { return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; } @@ -488,7 +410,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return getChildCount(); } - View getPageAt(int index) { + public View getPageAt(int index) { return getChildAt(index); } @@ -512,17 +434,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc forceFinishScroller(); } - /** - * Called during AllApps/Home transitions to avoid unnecessary work. When that other animation - * {@link #updateCurrentPageScroll()} should be called, to correctly set the final state and - * re-enable scrolling. - */ - void stopScrolling() { - mCurrentPage = getNextPage(); - notifyPageSwitchListener(); - forceFinishScroller(); - } - private void abortScrollerAnimation(boolean resetNextPage) { mScroller.abortAnimation(); // We need to clean up the next page here to avoid computeScrollHelper from @@ -555,7 +466,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc /** * Sets the current page. */ - void setCurrentPage(int currentPage) { + public void setCurrentPage(int currentPage) { if (!mScroller.isFinished()) { abortScrollerAnimation(true); } @@ -647,37 +558,41 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override public void scrollBy(int x, int y) { - scrollTo(mUnboundedScrollX + x, getScrollY() + y); + scrollTo(getScrollX() + x, getScrollY() + y); } @Override public void scrollTo(int x, int y) { // In free scroll mode, we clamp the scrollX if (mFreeScroll) { + // If the scroller is trying to move to a location beyond the maximum allowed + // in the free scroll mode, we make sure to end the scroll operation. + if (!mScroller.isFinished() && + (x > mFreeScrollMaxScrollX || x < mFreeScrollMinScrollX)) { + forceFinishScroller(); + } + x = Math.min(x, mFreeScrollMaxScrollX); x = Math.max(x, mFreeScrollMinScrollX); } - final boolean isRtl = isLayoutRtl(); - mUnboundedScrollX = x; - - boolean isXBeforeFirstPage = isRtl ? (x > mMaxScrollX) : (x < 0); - boolean isXAfterLastPage = isRtl ? (x < 0) : (x > mMaxScrollX); + boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < 0); + boolean isXAfterLastPage = mIsRtl ? (x < 0) : (x > mMaxScrollX); if (isXBeforeFirstPage) { - super.scrollTo(0, y); + super.scrollTo(mIsRtl ? mMaxScrollX : 0, y); if (mAllowOverScroll) { mWasInOverscroll = true; - if (isRtl) { + if (mIsRtl) { overScroll(x - mMaxScrollX); } else { overScroll(x); } } } else if (isXAfterLastPage) { - super.scrollTo(mMaxScrollX, y); + super.scrollTo(mIsRtl ? 0 : mMaxScrollX, y); if (mAllowOverScroll) { mWasInOverscroll = true; - if (isRtl) { + if (mIsRtl) { overScroll(x); } else { overScroll(x - mMaxScrollX); @@ -688,7 +603,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc overScroll(0); mWasInOverscroll = false; } - mOverScrollX = x; super.scrollTo(x, y); } @@ -708,21 +622,17 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); if (am.isEnabled()) { - AccessibilityEvent ev = - AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); - ev.setItemCount(getChildCount()); - ev.setFromIndex(mCurrentPage); - ev.setToIndex(getNextPage()); + if (mCurrentPage != getNextPage()) { + AccessibilityEvent ev = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); + ev.setScrollable(true); + ev.setScrollX(getScrollX()); + ev.setScrollY(getScrollY()); + ev.setMaxScrollX(mMaxScrollX); + ev.setMaxScrollY(0); - final int action; - if (getNextPage() >= mCurrentPage) { - action = AccessibilityNodeInfo.ACTION_SCROLL_FORWARD; - } else { - action = AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD; + sendAccessibilityEventUnchecked(ev); } - - ev.setAction(action); - sendAccessibilityEventUnchecked(ev); } } @@ -731,8 +641,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (mScroller.computeScrollOffset()) { // Don't bother scrolling if the page does not need to be moved if (getScrollX() != mScroller.getCurrX() - || getScrollY() != mScroller.getCurrY() - || mOverScrollX != mScroller.getCurrX()) { + || getScrollY() != mScroller.getCurrY()) { float scaleX = mFreeScroll ? getScaleX() : 1f; int scrollX = (int) (mScroller.getCurrX() * (1 / scaleX)); scrollTo(scrollX, mScroller.getCurrY()); @@ -746,12 +655,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc mNextPage = INVALID_PAGE; notifyPageSwitchListener(); - // Load the associated pages if necessary - if (mDeferLoadAssociatedPagesUntilScrollCompletes) { - loadAssociatedPages(mCurrentPage); - mDeferLoadAssociatedPagesUntilScrollCompletes = false; - } - // We don't want to trigger a page end moving unless the page has settled // and the user has stopped scrolling if (mTouchState == TOUCH_STATE_REST) { @@ -775,10 +678,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc computeScrollHelper(); } - protected boolean shouldSetTopAlignedPivotForWidget(int childIndex) { - return mTopAlignPageWhenShrinkingForBouncer; - } - public static class LayoutParams extends ViewGroup.LayoutParams { public boolean isFullScreenPage = false; @@ -789,15 +688,35 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc super(width, height); } + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + } + public LayoutParams(ViewGroup.LayoutParams source) { super(source); } } + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + public void addFullScreenPage(View page) { LayoutParams lp = generateDefaultLayoutParams(); lp.isFullScreenPage = true; @@ -810,7 +729,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (!mIsDataReady || getChildCount() == 0) { + if (getChildCount() == 0) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } @@ -914,27 +833,12 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } - if (mSpacePagesAutomatically) { - int spacing = (getViewportWidth() - mInsets.left - mInsets.right - - referenceChildWidth) / 2; - if (spacing >= 0) { - setPageSpacing(spacing); - } - mSpacePagesAutomatically = false; - } setMeasuredDimension(scaledWidthSize, scaledHeightSize); } - /** - * This method should be called once before first layout / measure pass. - */ - protected void setSinglePageInViewport() { - mSpacePagesAutomatically = true; - } - @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (!mIsDataReady || getChildCount() == 0) { + if (getChildCount() == 0) { return; } @@ -945,13 +849,11 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int offsetY = getViewportOffsetY(); // Update the viewport offsets - mViewport.offset(offsetX, offsetY); + mViewport.offset(offsetX, offsetY); - final boolean isRtl = isLayoutRtl(); - - final int startIndex = isRtl ? childCount - 1 : 0; - final int endIndex = isRtl ? -1 : childCount; - final int delta = isRtl ? -1 : 1; + final int startIndex = mIsRtl ? childCount - 1 : 0; + final int endIndex = mIsRtl ? -1 : childCount; + final int delta = mIsRtl ? -1 : 1; int verticalPadding = getPaddingTop() + getPaddingBottom(); @@ -959,8 +861,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc LayoutParams nextLp; int childLeft = offsetX + (lp.isFullScreenPage ? 0 : getPaddingLeft()); - if (mPageScrolls == null || getChildCount() != mChildCountOnLastLayout) { - mPageScrolls = new int[getChildCount()]; + if (mPageScrolls == null || childCount != mChildCountOnLastLayout) { + mPageScrolls = new int[childCount]; } for (int i = startIndex; i != endIndex; i += delta) { @@ -1003,24 +905,40 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc pageGap = getPaddingRight(); } - childLeft += childWidth + pageGap; + childLeft += childWidth + pageGap + getChildGap(); } } - if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + final LayoutTransition transition = getLayoutTransition(); + // If the transition is running defer updating max scroll, as some empty pages could + // still be present, and a max scroll change could cause sudden jumps in scroll. + if (transition != null && transition.isRunning()) { + transition.addTransitionListener(new LayoutTransition.TransitionListener() { + + @Override + public void startTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType) { } + + @Override + public void endTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType) { + // Wait until all transitions are complete. + if (!transition.isRunning()) { + transition.removeTransitionListener(this); + updateMaxScrollX(); + } + } + }); + } else { + updateMaxScrollX(); + } + + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) { updateCurrentPageScroll(); mFirstLayout = false; } - if (childCount > 0) { - final int index = isLayoutRtl() ? 0 : childCount - 1; - mMaxScrollX = getScrollForPage(index); - } else { - mMaxScrollX = 0; - } - - if (mScroller.isFinished() && mChildCountOnLastLayout != getChildCount() && - !mDeferringForDelete) { + if (mScroller.isFinished() && mChildCountOnLastLayout != childCount) { if (mRestorePage != INVALID_RESTORE_PAGE) { setCurrentPage(mRestorePage); mRestorePage = INVALID_RESTORE_PAGE; @@ -1028,41 +946,36 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc setCurrentPage(getNextPage()); } } - mChildCountOnLastLayout = getChildCount(); + mChildCountOnLastLayout = childCount; if (isReordering(true)) { updateDragViewTranslationDuringDrag(); } } + protected int getChildGap() { + return 0; + } + + @Thunk void updateMaxScrollX() { + int childCount = getChildCount(); + if (childCount > 0) { + final int index = mIsRtl ? 0 : childCount - 1; + mMaxScrollX = getScrollForPage(index); + } else { + mMaxScrollX = 0; + } + } + public void setPageSpacing(int pageSpacing) { mPageSpacing = pageSpacing; requestLayout(); } - protected void screenScrolled(int screenCenter) { - boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; - - if (mFadeInAdjacentScreens && !isInOverscroll) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (child != null) { - float scrollProgress = getScrollProgress(screenCenter, child, i); - float alpha = 1 - Math.abs(scrollProgress); - child.setAlpha(alpha); - } - } - invalidate(); - } - } - - protected void enablePagedViewAnimations() { - mAllowPagedViewAnimations = true; - - } - protected void disablePagedViewAnimations() { - mAllowPagedViewAnimations = false; - } + /** + * Called when the center screen changes during scrolling. + */ + protected void screenScrolled(int screenCenter) { } @Override public void onChildViewAdded(View parent, View child) { @@ -1072,7 +985,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int pageIndex = indexOfChild(child); mPageIndicator.addMarker(pageIndex, getPageIndicatorMarker(pageIndex), - mAllowPagedViewAnimations); + true); } // This ensures that when children are added, they get the correct transforms / alphas @@ -1093,7 +1006,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Update the page indicator, we don't update the page indicator as we // add/remove pages if (mPageIndicator != null && !isReordering(false)) { - mPageIndicator.removeMarker(index, mAllowPagedViewAnimations); + mPageIndicator.removeMarker(index, true); } } @@ -1115,7 +1028,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc public void removeViewAt(int index) { // XXX: We should find a better way to hook into this before the view // gets removed form its parent... - removeViewAt(index); + removeMarkerForView(index); super.removeViewAt(index); } @Override @@ -1123,7 +1036,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Update the page indicator, we don't update the page indicator as we // add/remove pages if (mPageIndicator != null) { - mPageIndicator.removeAllMarkers(mAllowPagedViewAnimations); + mPageIndicator.removeAllMarkers(true); } super.removeAllViewsInLayout(); @@ -1144,7 +1057,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc protected void getVisiblePages(int[] range) { final int pageCount = getChildCount(); - mTmpIntPoint[0] = mTmpIntPoint[1] = 0; + sTmpIntPoint[0] = sTmpIntPoint[1] = 0; range[0] = -1; range[1] = -1; @@ -1157,9 +1070,9 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc for (int i = 0; i < count; i++) { View currPage = getPageAt(i); - mTmpIntPoint[0] = 0; - Utilities.getDescendantCoordRelativeToParent(currPage, this, mTmpIntPoint, false); - if (mTmpIntPoint[0] > viewportWidth) { + sTmpIntPoint[0] = 0; + Utilities.getDescendantCoordRelativeToParent(currPage, this, sTmpIntPoint, false); + if (sTmpIntPoint[0] > viewportWidth) { if (range[0] == -1) { continue; } else { @@ -1167,9 +1080,9 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - mTmpIntPoint[0] = currPage.getMeasuredWidth(); - Utilities.getDescendantCoordRelativeToParent(currPage, this, mTmpIntPoint, false); - if (mTmpIntPoint[0] < 0) { + sTmpIntPoint[0] = currPage.getMeasuredWidth(); + Utilities.getDescendantCoordRelativeToParent(currPage, this, sTmpIntPoint, false); + if (sTmpIntPoint[0] < 0) { if (range[0] == -1) { continue; } else { @@ -1199,9 +1112,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc final int pageCount = getChildCount(); if (pageCount > 0) { int halfScreenSize = getViewportWidth() / 2; - // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. - // Otherwise it is equal to the scaled overscroll position. - int screenCenter = mOverScrollX + halfScreenSize; + int screenCenter = getScrollX() + halfScreenSize; if (screenCenter != mLastScreenCenter || mForceScreenScrolled) { // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can @@ -1241,6 +1152,46 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (getPageCount() > 0) { + if (!mEdgeGlowLeft.isFinished()) { + final int restoreCount = canvas.save(); + Rect display = mViewport; + canvas.translate(display.left, display.top); + canvas.rotate(270); + + getEdgeVerticalPostion(sTmpIntPoint); + canvas.translate(display.top - sTmpIntPoint[1], 0); + mEdgeGlowLeft.setSize(sTmpIntPoint[1] - sTmpIntPoint[0], display.width()); + if (mEdgeGlowLeft.draw(canvas)) { + postInvalidateOnAnimation(); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowRight.isFinished()) { + final int restoreCount = canvas.save(); + Rect display = mViewport; + canvas.translate(display.left + mPageScrolls[mIsRtl ? 0 : (getPageCount() - 1)], display.top); + canvas.rotate(90); + + getEdgeVerticalPostion(sTmpIntPoint); + canvas.translate(sTmpIntPoint[0] - display.top, -display.width()); + mEdgeGlowRight.setSize(sTmpIntPoint[1] - sTmpIntPoint[0], display.width()); + if (mEdgeGlowRight.draw(canvas)) { + postInvalidateOnAnimation(); + } + canvas.restoreToCount(restoreCount); + } + } + } + + /** + * Returns the top and bottom position for the edge effect. + */ + protected abstract void getEdgeVerticalPostion(int[] pos); + @Override public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { int page = indexToPage(indexOfChild(child)); @@ -1346,7 +1297,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc * Return true if a tap at (x, y) should trigger a flip to the previous page. */ protected boolean hitsPreviousPage(float x, float y) { - if (isLayoutRtl()) { + if (mIsRtl) { return (x > (getViewportOffsetX() + getViewportWidth() - getPaddingRight() - mPageSpacing)); } @@ -1357,7 +1308,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc * Return true if a tap at (x, y) should trigger a flip to the next page. */ protected boolean hitsNextPage(float x, float y) { - if (isLayoutRtl()) { + if (mIsRtl) { return (x < getViewportOffsetX() + getPaddingLeft() + mPageSpacing); } return (x > (getViewportOffsetX() + getViewportWidth() - @@ -1366,17 +1317,13 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc /** Returns whether x and y originated within the buffered viewport */ private boolean isTouchPointInViewportWithBuffer(int x, int y) { - mTmpRect.set(mViewport.left - mViewport.width() / 2, mViewport.top, + sTmpRect.set(mViewport.left - mViewport.width() / 2, mViewport.top, mViewport.right + mViewport.width() / 2, mViewport.bottom); - return mTmpRect.contains(x, y); + return sTmpRect.contains(x, y); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - if (DISABLE_TOUCH_INTERACTION) { - return false; - } - /* * This method JUST determines whether we want to intercept the motion. * If we return true, onTouchEvent will be called and we do the actual @@ -1453,19 +1400,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - // check if this can be the beginning of a tap on the side of the pages - // to scroll the current page - if (!DISABLE_TOUCH_SIDE_PAGES) { - if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) { - if (getChildCount() > 0) { - if (hitsPreviousPage(x, y)) { - mTouchState = TOUCH_STATE_PREV_PAGE; - } else if (hitsNextPage(x, y)) { - mTouchState = TOUCH_STATE_NEXT_PAGE; - } - } - } - } break; } @@ -1506,54 +1440,33 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return; final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); final int touchSlop = Math.round(touchSlopScale * mTouchSlop); - boolean xPaged = xDiff > mPagingTouchSlop; boolean xMoved = xDiff > touchSlop; - boolean yMoved = yDiff > touchSlop; - if (xMoved || xPaged || yMoved) { - if (mUsePagingTouchSlop ? xPaged : xMoved) { - // Scroll if the user moved far enough along the X axis - mTouchState = TOUCH_STATE_SCROLLING; - mTotalMotionX += Math.abs(mLastMotionX - x); - mLastMotionX = x; - mLastMotionXRemainder = 0; - mTouchX = getViewportOffsetX() + getScrollX(); - mSmoothingTime = System.nanoTime() / NANOTIME_DIV; - onScrollInteractionBegin(); - pageBeginMoving(); - } + if (xMoved) { + // Scroll if the user moved far enough along the X axis + mTouchState = TOUCH_STATE_SCROLLING; + mTotalMotionX += Math.abs(mLastMotionX - x); + mLastMotionX = x; + mLastMotionXRemainder = 0; + mTouchX = getViewportOffsetX() + getScrollX(); + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + onScrollInteractionBegin(); + pageBeginMoving(); } } - protected float getMaxScrollProgress() { - return 1.0f; - } - protected void cancelCurrentPageLongPress() { - if (mAllowLongPress) { - //mAllowLongPress = false; - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - final View currentPage = getPageAt(mCurrentPage); - if (currentPage != null) { - currentPage.cancelLongPress(); - } + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + final View currentPage = getPageAt(mCurrentPage); + if (currentPage != null) { + currentPage.cancelLongPress(); } } - protected float getBoundedScrollProgress(int screenCenter, View v, int page) { - final int halfScreenSize = getViewportWidth() / 2; - - screenCenter = Math.min(getScrollX() + halfScreenSize, screenCenter); - screenCenter = Math.max(halfScreenSize, screenCenter); - - return getScrollProgress(screenCenter, v, page); - } - protected float getScrollProgress(int screenCenter, View v, int page) { final int halfScreenSize = getViewportWidth() / 2; @@ -1563,7 +1476,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc final int totalDistance; int adjacentPage = page + 1; - if ((delta < 0 && !isLayoutRtl()) || (delta > 0 && isLayoutRtl())) { + if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) { adjacentPage = page - 1; } @@ -1574,8 +1487,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } float scrollProgress = delta / (totalDistance * 1.0f); - scrollProgress = Math.min(scrollProgress, getMaxScrollProgress()); - scrollProgress = Math.max(scrollProgress, - getMaxScrollProgress()); + scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS); + scrollProgress = Math.max(scrollProgress, - MAX_SCROLL_PROGRESS); return scrollProgress; } @@ -1598,7 +1511,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int scrollOffset = 0; LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isFullScreenPage) { - scrollOffset = isLayoutRtl() ? getPaddingRight() : getPaddingLeft(); + scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft(); } int baselineX = mPageScrolls[index] + scrollOffset + getViewportOffsetX(); @@ -1606,49 +1519,15 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - // This curve determines how the effect of scrolling over the limits of the page dimishes - // as the user pulls further and further from the bounds - private float overScrollInfluenceCurve(float f) { - f -= 1.0f; - return f * f * f + 1.0f; - } - - protected float acceleratedOverFactor(float amount) { - int screenSize = getViewportWidth(); - - // We want to reach the max over scroll effect when the user has - // over scrolled half the size of the screen - float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize); - - if (f == 0) return 0; - - // Clamp this factor, f, to -1 < f < 1 - if (Math.abs(f) >= 1) { - f /= Math.abs(f); - } - return f; - } - protected void dampedOverScroll(float amount) { int screenSize = getViewportWidth(); - float f = (amount / screenSize); - - if (f == 0) return; - f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); - - // Clamp this factor, f, to -1 < f < 1 - if (Math.abs(f) >= 1) { - f /= Math.abs(f); - } - - int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize); - if (amount < 0) { - mOverScrollX = overScrollAmount; - super.scrollTo(mOverScrollX, getScrollY()); + if (f < 0) { + mEdgeGlowLeft.onPull(-f); + } else if (f > 0) { + mEdgeGlowRight.onPull(f); } else { - mOverScrollX = mMaxScrollX + overScrollAmount; - super.scrollTo(mOverScrollX, getScrollY()); + return; } invalidate(); } @@ -1657,25 +1536,17 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc dampedOverScroll(amount); } - protected float maxOverScroll() { - // Using the formula in overScroll, assuming that f = 1.0 (which it should generally not - // exceed). Used to find out how much extra wallpaper we need for the over scroll effect - float f = 1.0f; - f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); - return OVERSCROLL_DAMP_FACTOR * f; - } - - protected void enableFreeScroll() { + public void enableFreeScroll() { setEnableFreeScroll(true); } - protected void disableFreeScroll() { + public void disableFreeScroll() { setEnableFreeScroll(false); } void updateFreescrollBounds() { getFreeScrollPageRange(mTempVisiblePagesRange); - if (isLayoutRtl()) { + if (mIsRtl) { mFreeScrollMinScrollX = getScrollForPage(mTempVisiblePagesRange[1]); mFreeScrollMaxScrollX = getScrollForPage(mTempVisiblePagesRange[0]); } else { @@ -1700,11 +1571,11 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc setEnableOverscroll(!freeScroll); } - private void setEnableOverscroll(boolean enable) { + protected void setEnableOverscroll(boolean enable) { mAllowOverScroll = enable; } - int getNearestHoverOverPageIndex() { + private int getNearestHoverOverPageIndex() { if (mDragView != null) { int dragX = (int) (mDragView.getLeft() + (mDragView.getMeasuredWidth() / 2) + mDragView.getTranslationX()); @@ -1727,10 +1598,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override public boolean onTouchEvent(MotionEvent ev) { - if (DISABLE_TOUCH_INTERACTION) { - return false; - } - super.onTouchEvent(ev); // Skip touch handling if there are no pages to swipe @@ -1785,12 +1652,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (Math.abs(deltaX) >= 1.0f) { mTouchX += deltaX; mSmoothingTime = System.nanoTime() / NANOTIME_DIV; - if (!mDeferScrollUpdate) { - scrollBy((int) deltaX, 0); - if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX); - } else { - invalidate(); - } + scrollBy((int) deltaX, 0); mLastMotionX = x; mLastMotionXRemainder = deltaX - (int) deltaX; } else { @@ -1811,19 +1673,13 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Find the closest page to the touch point final int dragViewIndex = indexOfChild(mDragView); - // Change the drag view if we are hovering over the drop target - boolean isHoveringOverDelete = isHoveringOverDeleteDropTarget( - (int) mParentDownMotionX, (int) mParentDownMotionY); - setPageHoveringOverDeleteDropTarget(dragViewIndex, isHoveringOverDelete); - if (DEBUG) Log.d(TAG, "mLastMotionX: " + mLastMotionX); if (DEBUG) Log.d(TAG, "mLastMotionY: " + mLastMotionY); if (DEBUG) Log.d(TAG, "mParentDownMotionX: " + mParentDownMotionX); if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY); final int pageUnderPointIndex = getNearestHoverOverPageIndex(); - if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView) && - !isHoveringOverDelete) { + if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView)) { mTempVisiblePagesRange[0] = 0; mTempVisiblePagesRange[1] = getPageCount() - 1; getFreeScrollPageRange(mTempVisiblePagesRange); @@ -1870,9 +1726,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } removeView(mDragView); - onRemoveView(mDragView, false); addView(mDragView, pageUnderPointIndex); - onAddView(mDragView, pageUnderPointIndex); mSidePageHoverIndex = -1; if (mPageIndicator != null) { mPageIndicator.setActiveMarker(getNextPage()); @@ -1922,9 +1776,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // We give flings precedence over large moves, which is why we short-circuit our // test for a large move if a fling has been registered. That is, a large // move to the left and fling to the right will register as a fling to the right. - final boolean isRtl = isLayoutRtl(); - boolean isDeltaXLeft = isRtl ? deltaX > 0 : deltaX < 0; - boolean isVelocityXLeft = isRtl ? velocityX > 0 : velocityX < 0; + boolean isDeltaXLeft = mIsRtl ? deltaX > 0 : deltaX < 0; + boolean isVelocityXLeft = mIsRtl ? velocityX > 0 : velocityX < 0; if (((isSignificantMove && !isDeltaXLeft && !isFling) || (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; @@ -1983,19 +1836,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc mParentDownMotionX = pt[0]; mParentDownMotionY = pt[1]; updateDragViewTranslationDuringDrag(); - boolean handledFling = false; - if (!DISABLE_FLING_TO_DELETE) { - // Check the velocity and see if we are flinging-to-delete - PointF flingToDeleteVector = isFlingingToDelete(); - if (flingToDeleteVector != null) { - onFlingToDelete(flingToDeleteVector); - handledFling = true; - } - } - if (!handledFling && isHoveringOverDeleteDropTarget((int) mParentDownMotionX, - (int) mParentDownMotionY)) { - onDropToDelete(); - } } else { if (!mCancelTap) { onUnhandledTap(ev); @@ -2024,17 +1864,14 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return true; } - public void onFlingToDelete(View v) {} - public void onRemoveView(View v, boolean deletePermanently) {} - public void onRemoveViewAnimationCompleted() {} - public void onAddView(View v, int index) {} - private void resetTouchState() { releaseVelocityTracker(); endReordering(); mCancelTap = false; mTouchState = TOUCH_STATE_REST; mActivePointerId = INVALID_POINTER; + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); } /** @@ -2066,7 +1903,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); } if (hscroll != 0 || vscroll != 0) { - boolean isForwardScroll = isLayoutRtl() ? (hscroll < 0 || vscroll < 0) + boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0) : (hscroll > 0 || vscroll > 0); if (isForwardScroll) { scrollRight(); @@ -2124,22 +1961,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - protected int getChildWidth(int index) { - return getPageAt(index).getMeasuredWidth(); - } - - int getPageNearestToPoint(float x) { - int index = 0; - for (int i = 0; i < getChildCount(); ++i) { - if (x < getChildAt(i).getRight() - getScrollX()) { - return index; - } else { - index++; - } - } - return Math.min(index, getChildCount() - 1); - } - int getPageNearestToCenterOfScreen() { int minDistanceFromScreenCenter = Integer.MAX_VALUE; int minDistanceFromScreenCenterIndex = -1; @@ -2159,20 +1980,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return minDistanceFromScreenCenterIndex; } - protected boolean isInOverScroll() { - return (mOverScrollX > mMaxScrollX || mOverScrollX < 0); - } - - protected int getPageSnapDuration() { - if (isInOverScroll()) { - return OVER_SCROLL_PAGE_SNAP_ANIMATION_DURATION; - } - return PAGE_SNAP_ANIMATION_DURATION; - - } - protected void snapToDestination() { - snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration()); + snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); } private static class ScrollInterpolator implements Interpolator { @@ -2189,7 +1998,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // the screen has to travel, however, we don't want this duration to be effected in a // purely linear fashion. Instead, we use this method to moderate the effect that the distance // of travel has on the overall snap duration. - float distanceInfluenceForSnapDuration(float f) { + private float distanceInfluenceForSnapDuration(float f) { f -= 0.5f; // center the values about 0. f *= 0.3f * Math.PI / 2.0f; return (float) Math.sin(f); @@ -2200,13 +2009,13 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc int halfScreenSize = getViewportWidth() / 2; final int newX = getScrollForPage(whichPage); - int delta = newX - mUnboundedScrollX; + int delta = newX - getScrollX(); int duration = 0; - if (Math.abs(velocity) < mMinFlingVelocity || isInOverScroll()) { + if (Math.abs(velocity) < mMinFlingVelocity) { // If the velocity is low enough, then treat this more as an automatic page advance // as opposed to an apparent physical response to flinging - snapToPage(whichPage, getPageSnapDuration()); + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); return; } @@ -2229,12 +2038,12 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc snapToPage(whichPage, delta, duration); } - protected void snapToPage(int whichPage) { - snapToPage(whichPage, getPageSnapDuration()); + public void snapToPage(int whichPage) { + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); } protected void snapToPageImmediately(int whichPage) { - snapToPage(whichPage, getPageSnapDuration(), true, null); + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null); } protected void snapToPage(int whichPage, int duration) { @@ -2250,8 +2059,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc whichPage = validateNewPage(whichPage); int newX = getScrollForPage(whichPage); - final int sX = mUnboundedScrollX; - final int delta = newX - sX; + final int delta = newX - getScrollX(); snapToPage(whichPage, delta, duration, immediate, interpolator); } @@ -2270,8 +2078,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc focusedChild.clearFocus(); } - sendScrollAccessibilityEvent(); - pageBeginMoving(); awakenScrollBars(duration); if (immediate) { @@ -2290,7 +2096,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc mScroller.setInterpolator(mDefaultInterpolator); } - mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); + mScroller.startScroll(getScrollX(), 0, delta, 0, duration); updatePageIndicator(); @@ -2299,9 +2105,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc computeScroll(); } - // Defer loading associated pages until the scroll settles - mDeferLoadAssociatedPagesUntilScrollCompletes = true; - mForceScreenScrolled = true; invalidate(); } @@ -2328,27 +2131,12 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc return result; } - /** - * @return True is long presses are still allowed for the current touch - */ - public boolean allowLongPress() { - return mAllowLongPress; - } - @Override public boolean performLongClick() { mCancelTap = true; return super.performLongClick(); } - /** - * Set true to allow long-press events to be triggered, usually checked by - * {@link Launcher} to accept or block dpad-initiated long-presses. - */ - public void setAllowLongPress(boolean allowLongPress) { - mAllowLongPress = allowLongPress; - } - public static class SavedState extends BaseSavedState { int currentPage = -1; @@ -2356,7 +2144,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc super(superState); } - private SavedState(Parcel in) { + @Thunk SavedState(Parcel in) { super(in); currentPage = in.readInt(); } @@ -2379,113 +2167,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc }; } - protected void loadAssociatedPages(int page) { - loadAssociatedPages(page, false); - } - protected void loadAssociatedPages(int page, boolean immediateAndOnly) { - if (mContentIsRefreshable) { - final int count = getChildCount(); - if (page < count) { - int lowerPageBound = getAssociatedLowerPageBound(page); - int upperPageBound = getAssociatedUpperPageBound(page); - if (DEBUG) Log.d(TAG, "loadAssociatedPages: " + lowerPageBound + "/" - + upperPageBound); - // First, clear any pages that should no longer be loaded - for (int i = 0; i < count; ++i) { - Page layout = (Page) getPageAt(i); - if ((i < lowerPageBound) || (i > upperPageBound)) { - if (layout.getPageChildCount() > 0) { - layout.removeAllViewsOnPage(); - } - mDirtyPageContent.set(i, true); - } - } - // Next, load any new pages - for (int i = 0; i < count; ++i) { - if ((i != page) && immediateAndOnly) { - continue; - } - if (lowerPageBound <= i && i <= upperPageBound) { - if (mDirtyPageContent.get(i)) { - syncPageItems(i, (i == page) && immediateAndOnly); - mDirtyPageContent.set(i, false); - } - } - } - } - } - } - - protected int getAssociatedLowerPageBound(int page) { - return Math.max(0, page - 1); - } - protected int getAssociatedUpperPageBound(int page) { - final int count = getChildCount(); - return Math.min(page + 1, count - 1); - } - - /** - * This method is called ONLY to synchronize the number of pages that the paged view has. - * To actually fill the pages with information, implement syncPageItems() below. It is - * guaranteed that syncPageItems() will be called for a particular page before it is shown, - * and therefore, individual page items do not need to be updated in this method. - */ - public abstract void syncPages(); - - /** - * This method is called to synchronize the items that are on a particular page. If views on - * the page can be reused, then they should be updated within this method. - */ - public abstract void syncPageItems(int page, boolean immediate); - - protected void invalidatePageData() { - invalidatePageData(-1, false); - } - protected void invalidatePageData(int currentPage) { - invalidatePageData(currentPage, false); - } - protected void invalidatePageData(int currentPage, boolean immediateAndOnly) { - if (!mIsDataReady) { - return; - } - - if (mContentIsRefreshable) { - // Force all scrolling-related behavior to end - forceFinishScroller(); - - // Update all the pages - syncPages(); - - // We must force a measure after we've loaded the pages to update the content width and - // to determine the full scroll width - measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); - - // Set a new page as the current page if necessary - if (currentPage > -1) { - setCurrentPage(Math.min(getPageCount() - 1, currentPage)); - } - - // Mark each of the pages as dirty - final int count = getChildCount(); - mDirtyPageContent.clear(); - for (int i = 0; i < count; ++i) { - mDirtyPageContent.add(true); - } - - // Load any pages that are necessary for the current window of views - loadAssociatedPages(mCurrentPage, immediateAndOnly); - requestLayout(); - } - if (isPageMoving()) { - // If the page is moving, then snap it to the final position to ensure we don't get - // stuck between pages - snapToDestination(); - } - } - // Animate the drag view back to the original position - void animateDragViewToOriginalPosition() { + private void animateDragViewToOriginalPosition() { if (mDragView != null) { AnimatorSet anim = new AnimatorSet(); anim.setDuration(REORDERING_DROP_REPOSITION_DURATION); @@ -2504,7 +2187,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - protected void onStartReordering() { + public void onStartReordering() { // Set the touch state to reordering (allows snapping to pages, dragging a child, etc.) mTouchState = TOUCH_STATE_REORDERING; mIsReordering = true; @@ -2514,7 +2197,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc invalidate(); } - private void onPostReorderingAnimationCompleted() { + @Thunk void onPostReorderingAnimationCompleted() { // Trigger the callback when reordering has settled --mPostReorderingPreZoomInRemainingAnimationCount; if (mPostReorderingPreZoomInRunnable != null && @@ -2524,7 +2207,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc } } - protected void onEndReordering() { + public void onEndReordering() { mIsReordering = false; } @@ -2574,281 +2257,26 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc onEndReordering(); } }; - if (!mDeferringForDelete) { - mPostReorderingPreZoomInRunnable = new Runnable() { - public void run() { - onCompleteRunnable.run(); - enableFreeScroll(); - }; + + mPostReorderingPreZoomInRunnable = new Runnable() { + public void run() { + onCompleteRunnable.run(); + enableFreeScroll(); }; + }; - mPostReorderingPreZoomInRemainingAnimationCount = - NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT; - // Snap to the current page - snapToPage(indexOfChild(mDragView), 0); - // Animate the drag view back to the front position - animateDragViewToOriginalPosition(); - } else { - // Handled in post-delete-animation-callbacks - } + mPostReorderingPreZoomInRemainingAnimationCount = + NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT; + // Snap to the current page + snapToPage(indexOfChild(mDragView), 0); + // Animate the drag view back to the front position + animateDragViewToOriginalPosition(); } - /* - * Flinging to delete - IN PROGRESS - */ - private PointF isFlingingToDelete() { - ViewConfiguration config = ViewConfiguration.get(getContext()); - mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity()); - - if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) { - // Do a quick dot product test to ensure that we are flinging upwards - PointF vel = new PointF(mVelocityTracker.getXVelocity(), - mVelocityTracker.getYVelocity()); - PointF upVec = new PointF(0f, -1f); - float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) / - (vel.length() * upVec.length())); - if (theta <= Math.toRadians(FLING_TO_DELETE_MAX_FLING_DEGREES)) { - return vel; - } - } - return null; - } - - /** - * Creates an animation from the current drag view along its current velocity vector. - * For this animation, the alpha runs for a fixed duration and we update the position - * progressively. - */ - private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { - private View mDragView; - private PointF mVelocity; - private Rect mFrom; - private long mPrevTime; - private float mFriction; - - private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); - - public FlingAlongVectorAnimatorUpdateListener(View dragView, PointF vel, Rect from, - long startTime, float friction) { - mDragView = dragView; - mVelocity = vel; - mFrom = from; - mPrevTime = startTime; - mFriction = 1f - (mDragView.getResources().getDisplayMetrics().density * friction); - } - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - float t = ((Float) animation.getAnimatedValue()).floatValue(); - long curTime = AnimationUtils.currentAnimationTimeMillis(); - - mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); - mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); - - mDragView.setTranslationX(mFrom.left); - mDragView.setTranslationY(mFrom.top); - mDragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); - - mVelocity.x *= mFriction; - mVelocity.y *= mFriction; - mPrevTime = curTime; - } - }; - private static final int ANIM_TAG_KEY = 100; - private Runnable createPostDeleteAnimationRunnable(final View dragView) { - return new Runnable() { - @Override - public void run() { - int dragViewIndex = indexOfChild(dragView); - - // For each of the pages around the drag view, animate them from the previous - // position to the new position in the layout (as a result of the drag view moving - // in the layout) - // NOTE: We can make an assumption here because we have side-bound pages that we - // will always have pages to animate in from the left - getFreeScrollPageRange(mTempVisiblePagesRange); - boolean isLastWidgetPage = (mTempVisiblePagesRange[0] == mTempVisiblePagesRange[1]); - boolean slideFromLeft = (isLastWidgetPage || - dragViewIndex > mTempVisiblePagesRange[0]); - - // Setup the scroll to the correct page before we swap the views - if (slideFromLeft) { - snapToPageImmediately(dragViewIndex - 1); - } - - int firstIndex = (isLastWidgetPage ? 0 : mTempVisiblePagesRange[0]); - int lastIndex = Math.min(mTempVisiblePagesRange[1], getPageCount() - 1); - int lowerIndex = (slideFromLeft ? firstIndex : dragViewIndex + 1 ); - int upperIndex = (slideFromLeft ? dragViewIndex - 1 : lastIndex); - ArrayList animations = new ArrayList(); - for (int i = lowerIndex; i <= upperIndex; ++i) { - View v = getChildAt(i); - // dragViewIndex < pageUnderPointIndex, so after we remove the - // drag view all subsequent views to pageUnderPointIndex will - // shift down. - int oldX = 0; - int newX = 0; - if (slideFromLeft) { - if (i == 0) { - // Simulate the page being offscreen with the page spacing - oldX = getViewportOffsetX() + getChildOffset(i) - getChildWidth(i) - - mPageSpacing; - } else { - oldX = getViewportOffsetX() + getChildOffset(i - 1); - } - newX = getViewportOffsetX() + getChildOffset(i); - } else { - oldX = getChildOffset(i) - getChildOffset(i - 1); - newX = 0; - } - - // Animate the view translation from its old position to its new - // position - AnimatorSet anim = (AnimatorSet) v.getTag(); - if (anim != null) { - anim.cancel(); - } - - // Note: Hacky, but we want to skip any optimizations to not draw completely - // hidden views - v.setAlpha(Math.max(v.getAlpha(), 0.01f)); - v.setTranslationX(oldX - newX); - anim = new AnimatorSet(); - anim.playTogether( - ObjectAnimator.ofFloat(v, "translationX", 0f), - ObjectAnimator.ofFloat(v, "alpha", 1f)); - animations.add(anim); - v.setTag(ANIM_TAG_KEY, anim); - } - - AnimatorSet slideAnimations = new AnimatorSet(); - slideAnimations.playTogether(animations); - slideAnimations.setDuration(DELETE_SLIDE_IN_SIDE_PAGE_DURATION); - slideAnimations.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mDeferringForDelete = false; - onEndReordering(); - onRemoveViewAnimationCompleted(); - } - }); - slideAnimations.start(); - - removeView(dragView); - onRemoveView(dragView, true); - } - }; - } - - public void onFlingToDelete(PointF vel) { - final long startTime = AnimationUtils.currentAnimationTimeMillis(); - - // NOTE: Because it takes time for the first frame of animation to actually be - // called and we expect the animation to be a continuation of the fling, we have - // to account for the time that has elapsed since the fling finished. And since - // we don't have a startDelay, we will always get call to update when we call - // start() (which we want to ignore). - final TimeInterpolator tInterpolator = new TimeInterpolator() { - private int mCount = -1; - private long mStartTime; - private float mOffset; - /* Anonymous inner class ctor */ { - mStartTime = startTime; - } - - @Override - public float getInterpolation(float t) { - if (mCount < 0) { - mCount++; - } else if (mCount == 0) { - mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - - mStartTime) / FLING_TO_DELETE_FADE_OUT_DURATION); - mCount++; - } - return Math.min(1f, mOffset + t); - } - }; - - final Rect from = new Rect(); - final View dragView = mDragView; - from.left = (int) dragView.getTranslationX(); - from.top = (int) dragView.getTranslationY(); - AnimatorUpdateListener updateCb = new FlingAlongVectorAnimatorUpdateListener(dragView, vel, - from, startTime, FLING_TO_DELETE_FRICTION); - - final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); - - // Create and start the animation - ValueAnimator mDropAnim = new ValueAnimator(); - mDropAnim.setInterpolator(tInterpolator); - mDropAnim.setDuration(FLING_TO_DELETE_FADE_OUT_DURATION); - mDropAnim.setFloatValues(0f, 1f); - mDropAnim.addUpdateListener(updateCb); - mDropAnim.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - onAnimationEndRunnable.run(); - } - }); - mDropAnim.start(); - mDeferringForDelete = true; - } - - /* Drag to delete */ - private boolean isHoveringOverDeleteDropTarget(int x, int y) { - if (mDeleteDropTarget != null) { - mAltTmpRect.set(0, 0, 0, 0); - View parent = (View) mDeleteDropTarget.getParent(); - if (parent != null) { - parent.getGlobalVisibleRect(mAltTmpRect); - } - mDeleteDropTarget.getGlobalVisibleRect(mTmpRect); - mTmpRect.offset(-mAltTmpRect.left, -mAltTmpRect.top); - return mTmpRect.contains(x, y); - } - return false; - } - - protected void setPageHoveringOverDeleteDropTarget(int viewIndex, boolean isHovering) {} - - private void onDropToDelete() { - final View dragView = mDragView; - - final float toScale = 0f; - final float toAlpha = 0f; - - // Create and start the complex animation - ArrayList animations = new ArrayList(); - AnimatorSet motionAnim = new AnimatorSet(); - motionAnim.setInterpolator(new DecelerateInterpolator(2)); - motionAnim.playTogether( - ObjectAnimator.ofFloat(dragView, "scaleX", toScale), - ObjectAnimator.ofFloat(dragView, "scaleY", toScale)); - animations.add(motionAnim); - - AnimatorSet alphaAnim = new AnimatorSet(); - alphaAnim.setInterpolator(new LinearInterpolator()); - alphaAnim.playTogether( - ObjectAnimator.ofFloat(dragView, "alpha", toAlpha)); - animations.add(alphaAnim); - - final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); - - AnimatorSet anim = new AnimatorSet(); - anim.playTogether(animations); - anim.setDuration(DRAG_TO_DELETE_FADE_OUT_DURATION); - anim.addListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - onAnimationEndRunnable.run(); - } - }); - anim.start(); - - mDeferringForDelete = true; - } - /* Accessibility */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); @@ -2859,6 +2287,15 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc if (getCurrentPage() > 0) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } + info.setClassName(getClass().getName()); + + // Accessibility-wise, PagedView doesn't support long click, so disabling it. + // Besides disabling the accessibility long-click, this also prevents this view from getting + // accessibility focus. + info.setLongClickable(false); + if (Utilities.isLmpOrAbove()) { + info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); + } } @Override @@ -2872,7 +2309,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); - event.setScrollable(true); + event.setScrollable(getPageCount() > 1); } @Override diff --git a/src/com/android/launcher3/PagedViewCellLayout.java b/src/com/android/launcher3/PagedViewCellLayout.java deleted file mode 100644 index 2d9e10b9de..0000000000 --- a/src/com/android/launcher3/PagedViewCellLayout.java +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewDebug; -import android.view.ViewGroup; - -/** - * An abstraction of the original CellLayout which supports laying out items - * which span multiple cells into a grid-like layout. Also supports dimming - * to give a preview of its contents. - */ -public class PagedViewCellLayout extends ViewGroup implements Page { - static final String TAG = "PagedViewCellLayout"; - - private int mCellCountX; - private int mCellCountY; - private int mOriginalCellWidth; - private int mOriginalCellHeight; - private int mCellWidth; - private int mCellHeight; - private int mOriginalWidthGap; - private int mOriginalHeightGap; - private int mWidthGap; - private int mHeightGap; - protected PagedViewCellLayoutChildren mChildren; - - public PagedViewCellLayout(Context context) { - this(context, null); - } - - public PagedViewCellLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedViewCellLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - setAlwaysDrawnWithCacheEnabled(false); - - // setup default cell parameters - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mOriginalCellWidth = mCellWidth = grid.cellWidthPx; - mOriginalCellHeight = mCellHeight = grid.cellHeightPx; - mCellCountX = (int) grid.numColumns; - mCellCountY = (int) grid.numRows; - mOriginalWidthGap = mOriginalHeightGap = mWidthGap = mHeightGap = -1; - - mChildren = new PagedViewCellLayoutChildren(context); - mChildren.setCellDimensions(mCellWidth, mCellHeight); - mChildren.setGap(mWidthGap, mHeightGap); - - addView(mChildren); - } - - public int getCellWidth() { - return mCellWidth; - } - - public int getCellHeight() { - return mCellHeight; - } - - @Override - public void cancelLongPress() { - super.cancelLongPress(); - - // Cancel long press for all children - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - child.cancelLongPress(); - } - } - - public boolean addViewToCellLayout(View child, int index, int childId, - PagedViewCellLayout.LayoutParams params) { - final PagedViewCellLayout.LayoutParams lp = params; - - // Generate an id for each view, this assumes we have at most 256x256 cells - // per workspace screen - if (lp.cellX >= 0 && lp.cellX <= (mCellCountX - 1) && - lp.cellY >= 0 && (lp.cellY <= mCellCountY - 1)) { - // If the horizontal or vertical span is set to -1, it is taken to - // mean that it spans the extent of the CellLayout - if (lp.cellHSpan < 0) lp.cellHSpan = mCellCountX; - if (lp.cellVSpan < 0) lp.cellVSpan = mCellCountY; - - child.setId(childId); - mChildren.addView(child, index, lp); - - return true; - } - return false; - } - - @Override - public void removeAllViewsOnPage() { - mChildren.removeAllViews(); - setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void removeViewOnPageAt(int index) { - mChildren.removeViewAt(index); - } - - /** - * Clears all the key listeners for the individual icons. - */ - public void resetChildrenOnKeyListeners() { - int childCount = mChildren.getChildCount(); - for (int j = 0; j < childCount; ++j) { - mChildren.getChildAt(j).setOnKeyListener(null); - } - } - - @Override - public int getPageChildCount() { - return mChildren.getChildCount(); - } - - public PagedViewCellLayoutChildren getChildrenLayout() { - return mChildren; - } - - @Override - public View getChildOnPageAt(int i) { - return mChildren.getChildAt(i); - } - - @Override - public int indexOfChildOnPage(View v) { - return mChildren.indexOfChild(v); - } - - public int getCellCountX() { - return mCellCountX; - } - - public int getCellCountY() { - return mCellCountY; - } - - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { - throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); - } - - int numWidthGaps = mCellCountX - 1; - int numHeightGaps = mCellCountY - 1; - - if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) { - int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight(); - int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom(); - int hFreeSpace = hSpace - (mCellCountX * mOriginalCellWidth); - int vFreeSpace = vSpace - (mCellCountY * mOriginalCellHeight); - mWidthGap = numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0; - mHeightGap = numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0; - - mChildren.setGap(mWidthGap, mHeightGap); - } else { - mWidthGap = mOriginalWidthGap; - mHeightGap = mOriginalHeightGap; - } - - // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY - int newWidth = widthSpecSize; - int newHeight = heightSpecSize; - if (widthSpecMode == MeasureSpec.AT_MOST) { - newWidth = getPaddingLeft() + getPaddingRight() + (mCellCountX * mCellWidth) + - ((mCellCountX - 1) * mWidthGap); - newHeight = getPaddingTop() + getPaddingBottom() + (mCellCountY * mCellHeight) + - ((mCellCountY - 1) * mHeightGap); - setMeasuredDimension(newWidth, newHeight); - } - - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - int childWidthMeasureSpec = - MeasureSpec.makeMeasureSpec(newWidth - getPaddingLeft() - - getPaddingRight(), MeasureSpec.EXACTLY); - int childheightMeasureSpec = - MeasureSpec.makeMeasureSpec(newHeight - getPaddingTop() - - getPaddingBottom(), MeasureSpec.EXACTLY); - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - } - - setMeasuredDimension(newWidth, newHeight); - } - - int getContentWidth() { - return getWidthBeforeFirstLayout() + getPaddingLeft() + getPaddingRight(); - } - - int getContentHeight() { - if (mCellCountY > 0) { - return mCellCountY * mCellHeight + (mCellCountY - 1) * Math.max(0, mHeightGap); - } - return 0; - } - - int getWidthBeforeFirstLayout() { - if (mCellCountX > 0) { - return mCellCountX * mCellWidth + (mCellCountX - 1) * Math.max(0, mWidthGap); - } - return 0; - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - child.layout(getPaddingLeft(), getPaddingTop(), - r - l - getPaddingRight(), b - t - getPaddingBottom()); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - boolean result = super.onTouchEvent(event); - int count = getPageChildCount(); - if (count > 0) { - // We only intercept the touch if we are tapping in empty space after the final row - View child = getChildOnPageAt(count - 1); - int bottom = child.getBottom(); - int numRows = (int) Math.ceil((float) getPageChildCount() / getCellCountX()); - if (numRows < getCellCountY()) { - // Add a little bit of buffer if there is room for another row - bottom += mCellHeight / 2; - } - result = result || (event.getY() < bottom); - } - return result; - } - - public void enableCenteredContent(boolean enabled) { - mChildren.enableCenteredContent(enabled); - } - - @Override - protected void setChildrenDrawingCacheEnabled(boolean enabled) { - mChildren.setChildrenDrawingCacheEnabled(enabled); - } - - public void setCellCount(int xCount, int yCount) { - mCellCountX = xCount; - mCellCountY = yCount; - requestLayout(); - } - - public void setGap(int widthGap, int heightGap) { - mOriginalWidthGap = mWidthGap = widthGap; - mOriginalHeightGap = mHeightGap = heightGap; - mChildren.setGap(widthGap, heightGap); - } - - public int[] getCellCountForDimensions(int width, int height) { - // Always assume we're working with the smallest span to make sure we - // reserve enough space in both orientations - int smallerSize = Math.min(mCellWidth, mCellHeight); - - // Always round up to next largest cell - int spanX = (width + smallerSize) / smallerSize; - int spanY = (height + smallerSize) / smallerSize; - - return new int[] { spanX, spanY }; - } - - /** - * Start dragging the specified child - * - * @param child The child that is being dragged - */ - void onDragChild(View child) { - PagedViewCellLayout.LayoutParams lp = (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - lp.isDragging = true; - } - - /** - * Estimates the number of cells that the specified width would take up. - */ - public int estimateCellHSpan(int width) { - // We don't show the next/previous pages any more, so we use the full width, minus the - // padding - int availWidth = width - (getPaddingLeft() + getPaddingRight()); - - // We know that we have to fit N cells with N-1 width gaps, so we just juggle to solve for N - int n = Math.max(1, (availWidth + mWidthGap) / (mCellWidth + mWidthGap)); - - // We don't do anything fancy to determine if we squeeze another row in. - return n; - } - - /** - * Estimates the number of cells that the specified height would take up. - */ - public int estimateCellVSpan(int height) { - // The space for a page is the height - top padding (current page) - bottom padding (current - // page) - int availHeight = height - (getPaddingTop() + getPaddingBottom()); - - // We know that we have to fit N cells with N-1 height gaps, so we juggle to solve for N - int n = Math.max(1, (availHeight + mHeightGap) / (mCellHeight + mHeightGap)); - - // We don't do anything fancy to determine if we squeeze another row in. - return n; - } - - /** Returns an estimated center position of the cell at the specified index */ - public int[] estimateCellPosition(int x, int y) { - return new int[] { - getPaddingLeft() + (x * mCellWidth) + (x * mWidthGap) + (mCellWidth / 2), - getPaddingTop() + (y * mCellHeight) + (y * mHeightGap) + (mCellHeight / 2) - }; - } - - public void calculateCellCount(int width, int height, int maxCellCountX, int maxCellCountY) { - mCellCountX = Math.min(maxCellCountX, estimateCellHSpan(width)); - mCellCountY = Math.min(maxCellCountY, estimateCellVSpan(height)); - requestLayout(); - } - - /** - * Estimates the width that the number of hSpan cells will take up. - */ - public int estimateCellWidth(int hSpan) { - // TODO: we need to take widthGap into effect - return hSpan * mCellWidth; - } - - /** - * Estimates the height that the number of vSpan cells will take up. - */ - public int estimateCellHeight(int vSpan) { - // TODO: we need to take heightGap into effect - return vSpan * mCellHeight; - } - - @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return new PagedViewCellLayout.LayoutParams(getContext(), attrs); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof PagedViewCellLayout.LayoutParams; - } - - @Override - protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return new PagedViewCellLayout.LayoutParams(p); - } - - public static class LayoutParams extends ViewGroup.MarginLayoutParams { - /** - * Horizontal location of the item in the grid. - */ - @ViewDebug.ExportedProperty - public int cellX; - - /** - * Vertical location of the item in the grid. - */ - @ViewDebug.ExportedProperty - public int cellY; - - /** - * Number of cells spanned horizontally by the item. - */ - @ViewDebug.ExportedProperty - public int cellHSpan; - - /** - * Number of cells spanned vertically by the item. - */ - @ViewDebug.ExportedProperty - public int cellVSpan; - - /** - * Is this item currently being dragged - */ - public boolean isDragging; - - // a data object that you can bind to this layout params - private Object mTag; - - // X coordinate of the view in the layout. - @ViewDebug.ExportedProperty - int x; - // Y coordinate of the view in the layout. - @ViewDebug.ExportedProperty - int y; - - public LayoutParams() { - super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(ViewGroup.LayoutParams source) { - super(source); - cellHSpan = 1; - cellVSpan = 1; - } - - public LayoutParams(LayoutParams source) { - super(source); - this.cellX = source.cellX; - this.cellY = source.cellY; - this.cellHSpan = source.cellHSpan; - this.cellVSpan = source.cellVSpan; - } - - public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { - super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - this.cellX = cellX; - this.cellY = cellY; - this.cellHSpan = cellHSpan; - this.cellVSpan = cellVSpan; - } - - public void setup(Context context, - int cellWidth, int cellHeight, int widthGap, int heightGap, - int hStartPadding, int vStartPadding) { - - final int myCellHSpan = cellHSpan; - final int myCellVSpan = cellVSpan; - final int myCellX = cellX; - final int myCellY = cellY; - - width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - - leftMargin - rightMargin; - height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - - topMargin - bottomMargin; - - if (LauncherAppState.getInstance().isScreenLarge()) { - x = hStartPadding + myCellX * (cellWidth + widthGap) + leftMargin; - y = vStartPadding + myCellY * (cellHeight + heightGap) + topMargin; - } else { - x = myCellX * (cellWidth + widthGap) + leftMargin; - y = myCellY * (cellHeight + heightGap) + topMargin; - } - } - - public Object getTag() { - return mTag; - } - - public void setTag(Object tag) { - mTag = tag; - } - - public String toString() { - return "(" + this.cellX + ", " + this.cellY + ", " + - this.cellHSpan + ", " + this.cellVSpan + ")"; - } - } -} \ No newline at end of file diff --git a/src/com/android/launcher3/PagedViewCellLayoutChildren.java b/src/com/android/launcher3/PagedViewCellLayoutChildren.java deleted file mode 100644 index 84d2b1dd38..0000000000 --- a/src/com/android/launcher3/PagedViewCellLayoutChildren.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.graphics.Rect; -import android.view.View; -import android.view.ViewGroup; - -/** - * An abstraction of the original CellLayout which supports laying out items - * which span multiple cells into a grid-like layout. Also supports dimming - * to give a preview of its contents. - */ -public class PagedViewCellLayoutChildren extends ViewGroup { - static final String TAG = "PagedViewCellLayout"; - - private boolean mCenterContent; - - private int mCellWidth; - private int mCellHeight; - private int mWidthGap; - private int mHeightGap; - - public PagedViewCellLayoutChildren(Context context) { - super(context); - } - - @Override - public void cancelLongPress() { - super.cancelLongPress(); - - // Cancel long press for all children - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - child.cancelLongPress(); - } - } - - public void setGap(int widthGap, int heightGap) { - mWidthGap = widthGap; - mHeightGap = heightGap; - requestLayout(); - } - - public void setCellDimensions(int width, int height) { - mCellWidth = width; - mCellHeight = height; - requestLayout(); - } - - @Override - public void requestChildFocus(View child, View focused) { - super.requestChildFocus(child, focused); - if (child != null) { - Rect r = new Rect(); - child.getDrawingRect(r); - requestRectangleOnScreen(r); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { - throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); - } - - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - lp.setup(getContext(), - mCellWidth, mCellHeight, mWidthGap, mHeightGap, - getPaddingLeft(), - getPaddingTop()); - - int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, - MeasureSpec.EXACTLY); - int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, - MeasureSpec.EXACTLY); - - child.measure(childWidthMeasureSpec, childheightMeasureSpec); - } - - setMeasuredDimension(widthSpecSize, heightSpecSize); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int count = getChildCount(); - - int offsetX = 0; - if (mCenterContent && count > 0) { - // determine the max width of all the rows and center accordingly - int maxRowX = 0; - int minRowX = Integer.MAX_VALUE; - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - minRowX = Math.min(minRowX, lp.x); - maxRowX = Math.max(maxRowX, lp.x + lp.width); - } - } - int maxRowWidth = maxRowX - minRowX; - offsetX = (getMeasuredWidth() - maxRowWidth) / 2; - } - - for (int i = 0; i < count; i++) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - PagedViewCellLayout.LayoutParams lp = - (PagedViewCellLayout.LayoutParams) child.getLayoutParams(); - - int childLeft = offsetX + lp.x; - int childTop = lp.y; - child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height); - } - } - } - - public void enableCenteredContent(boolean enabled) { - mCenterContent = enabled; - } - - @Override - protected void setChildrenDrawingCacheEnabled(boolean enabled) { - final int count = getChildCount(); - for (int i = 0; i < count; i++) { - final View view = getChildAt(i); - view.setDrawingCacheEnabled(enabled); - // Update the drawing caches - if (!view.isHardwareAccelerated()) { - view.buildDrawingCache(true); - } - } - } -} diff --git a/src/com/android/launcher3/PagedViewGridLayout.java b/src/com/android/launcher3/PagedViewGridLayout.java deleted file mode 100644 index f69fa562d4..0000000000 --- a/src/com/android/launcher3/PagedViewGridLayout.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.GridLayout; - -/** - * The grid based layout used strictly for the widget/wallpaper tab of the AppsCustomize pane - */ -public class PagedViewGridLayout extends GridLayout implements Page { - static final String TAG = "PagedViewGridLayout"; - - private int mCellCountX; - private int mCellCountY; - private Runnable mOnLayoutListener; - - public PagedViewGridLayout(Context context, int cellCountX, int cellCountY) { - super(context, null, 0); - mCellCountX = cellCountX; - mCellCountY = cellCountY; - } - - int getCellCountX() { - return mCellCountX; - } - - int getCellCountY() { - return mCellCountY; - } - - /** - * Clears all the key listeners for the individual widgets. - */ - public void resetChildrenOnKeyListeners() { - int childCount = getChildCount(); - for (int j = 0; j < childCount; ++j) { - getChildAt(j).setOnKeyListener(null); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mOnLayoutListener = null; - } - - public void setOnLayoutListener(Runnable r) { - mOnLayoutListener = r; - } - - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (mOnLayoutListener != null) { - mOnLayoutListener.run(); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - boolean result = super.onTouchEvent(event); - int count = getPageChildCount(); - if (count > 0) { - // We only intercept the touch if we are tapping in empty space after the final row - View child = getChildOnPageAt(count - 1); - int bottom = child.getBottom(); - result = result || (event.getY() < bottom); - } - return result; - } - - @Override - public void removeAllViewsOnPage() { - removeAllViews(); - mOnLayoutListener = null; - setLayerType(LAYER_TYPE_NONE, null); - } - - @Override - public void removeViewOnPageAt(int index) { - removeViewAt(index); - } - - @Override - public int getPageChildCount() { - return getChildCount(); - } - - @Override - public View getChildOnPageAt(int i) { - return getChildAt(i); - } - - @Override - public int indexOfChildOnPage(View v) { - return indexOfChild(v); - } - - public static class LayoutParams extends FrameLayout.LayoutParams { - public LayoutParams(int width, int height) { - super(width, height); - } - } -} diff --git a/src/com/android/launcher3/PagedViewWidget.java b/src/com/android/launcher3/PagedViewWidget.java deleted file mode 100644 index e6e11a3120..0000000000 --- a/src/com/android/launcher3/PagedViewWidget.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.appwidget.AppWidgetProviderInfo; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.launcher3.compat.AppWidgetManagerCompat; - -/** - * The linear layout used strictly for the widget/wallpaper tab of the customization tray - */ -public class PagedViewWidget extends LinearLayout { - static final String TAG = "PagedViewWidgetLayout"; - - private static boolean sDeletePreviewsWhenDetachedFromWindow = true; - private static boolean sRecyclePreviewsWhenDetachedFromWindow = true; - - private String mDimensionsFormatString; - CheckForShortPress mPendingCheckForShortPress = null; - ShortPressListener mShortPressListener = null; - boolean mShortPressTriggered = false; - static PagedViewWidget sShortpressTarget = null; - boolean mIsAppWidget; - private final Rect mOriginalImagePadding = new Rect(); - private Object mInfo; - private WidgetPreviewLoader mWidgetPreviewLoader; - - public PagedViewWidget(Context context) { - this(context, null); - } - - public PagedViewWidget(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedViewWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final Resources r = context.getResources(); - mDimensionsFormatString = r.getString(R.string.widget_dims_format); - - setWillNotDraw(false); - setClipToPadding(false); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - final ImageView image = (ImageView) findViewById(R.id.widget_preview); - mOriginalImagePadding.left = image.getPaddingLeft(); - mOriginalImagePadding.top = image.getPaddingTop(); - mOriginalImagePadding.right = image.getPaddingRight(); - mOriginalImagePadding.bottom = image.getPaddingBottom(); - - // Ensure we are using the right text size - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - TextView name = (TextView) findViewById(R.id.widget_name); - if (name != null) { - name.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); - } - TextView dims = (TextView) findViewById(R.id.widget_dims); - if (dims != null) { - dims.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx); - } - } - - public static void setDeletePreviewsWhenDetachedFromWindow(boolean value) { - sDeletePreviewsWhenDetachedFromWindow = value; - } - - public static void setRecyclePreviewsWhenDetachedFromWindow(boolean value) { - sRecyclePreviewsWhenDetachedFromWindow = value; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (sDeletePreviewsWhenDetachedFromWindow) { - final ImageView image = (ImageView) findViewById(R.id.widget_preview); - if (image != null) { - FastBitmapDrawable preview = (FastBitmapDrawable) image.getDrawable(); - if (sRecyclePreviewsWhenDetachedFromWindow && - mInfo != null && preview != null && preview.getBitmap() != null) { - mWidgetPreviewLoader.recycleBitmap(mInfo, preview.getBitmap()); - } - image.setImageDrawable(null); - } - } - } - - public void applyFromAppWidgetProviderInfo(AppWidgetProviderInfo info, - int maxWidth, int[] cellSpan, WidgetPreviewLoader loader) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - - mIsAppWidget = true; - mInfo = info; - final ImageView image = (ImageView) findViewById(R.id.widget_preview); - if (maxWidth > -1) { - image.setMaxWidth(maxWidth); - } - final TextView name = (TextView) findViewById(R.id.widget_name); - name.setText(AppWidgetManagerCompat.getInstance(getContext()).loadLabel(info)); - final TextView dims = (TextView) findViewById(R.id.widget_dims); - if (dims != null) { - int hSpan = Math.min(cellSpan[0], (int) grid.numColumns); - int vSpan = Math.min(cellSpan[1], (int) grid.numRows); - dims.setText(String.format(mDimensionsFormatString, hSpan, vSpan)); - } - mWidgetPreviewLoader = loader; - } - - public void applyFromResolveInfo( - PackageManager pm, ResolveInfo info, WidgetPreviewLoader loader) { - mIsAppWidget = false; - mInfo = info; - CharSequence label = info.loadLabel(pm); - final TextView name = (TextView) findViewById(R.id.widget_name); - name.setText(label); - final TextView dims = (TextView) findViewById(R.id.widget_dims); - if (dims != null) { - dims.setText(String.format(mDimensionsFormatString, 1, 1)); - } - mWidgetPreviewLoader = loader; - } - - public int[] getPreviewSize() { - final ImageView i = (ImageView) findViewById(R.id.widget_preview); - int[] maxSize = new int[2]; - maxSize[0] = i.getWidth() - mOriginalImagePadding.left - mOriginalImagePadding.right; - maxSize[1] = i.getHeight() - mOriginalImagePadding.top; - return maxSize; - } - - void applyPreview(FastBitmapDrawable preview, int index) { - final PagedViewWidgetImageView image = - (PagedViewWidgetImageView) findViewById(R.id.widget_preview); - if (preview != null) { - image.mAllowRequestLayout = false; - image.setImageDrawable(preview); - if (mIsAppWidget) { - // center horizontally - int[] imageSize = getPreviewSize(); - int centerAmount = (imageSize[0] - preview.getIntrinsicWidth()) / 2; - image.setPadding(mOriginalImagePadding.left + centerAmount, - mOriginalImagePadding.top, - mOriginalImagePadding.right, - mOriginalImagePadding.bottom); - } - image.setAlpha(1f); - image.mAllowRequestLayout = true; - } - } - - void setShortPressListener(ShortPressListener listener) { - mShortPressListener = listener; - } - - interface ShortPressListener { - void onShortPress(View v); - void cleanUpShortPress(View v); - } - - class CheckForShortPress implements Runnable { - public void run() { - if (sShortpressTarget != null) return; - if (mShortPressListener != null) { - mShortPressListener.onShortPress(PagedViewWidget.this); - sShortpressTarget = PagedViewWidget.this; - } - mShortPressTriggered = true; - } - } - - private void checkForShortPress() { - if (sShortpressTarget != null) return; - if (mPendingCheckForShortPress == null) { - mPendingCheckForShortPress = new CheckForShortPress(); - } - postDelayed(mPendingCheckForShortPress, 120); - } - - /** - * Remove the longpress detection timer. - */ - private void removeShortPressCallback() { - if (mPendingCheckForShortPress != null) { - removeCallbacks(mPendingCheckForShortPress); - } - } - - private void cleanUpShortPress() { - removeShortPressCallback(); - if (mShortPressTriggered) { - if (mShortPressListener != null) { - mShortPressListener.cleanUpShortPress(PagedViewWidget.this); - } - mShortPressTriggered = false; - } - } - - static void resetShortPressTarget() { - sShortpressTarget = null; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - super.onTouchEvent(event); - - switch (event.getAction()) { - case MotionEvent.ACTION_UP: - cleanUpShortPress(); - break; - case MotionEvent.ACTION_DOWN: - checkForShortPress(); - break; - case MotionEvent.ACTION_CANCEL: - cleanUpShortPress(); - break; - case MotionEvent.ACTION_MOVE: - break; - } - - // We eat up the touch events here, since the PagedView (which uses the same swiping - // touch code as Workspace previously) uses onInterceptTouchEvent() to determine when - // the user is scrolling between pages. This means that if the pages themselves don't - // handle touch events, it gets forwarded up to PagedView itself, and it's own - // onTouchEvent() handling will prevent further intercept touch events from being called - // (it's the same view in that case). This is not ideal, but to prevent more changes, - // we just always mark the touch event as handled. - return true; - } -} diff --git a/src/com/android/launcher3/PagedViewWidgetImageView.java b/src/com/android/launcher3/PagedViewWidgetImageView.java deleted file mode 100644 index 7d8279547b..0000000000 --- a/src/com/android/launcher3/PagedViewWidgetImageView.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.widget.ImageView; - -public class PagedViewWidgetImageView extends ImageView { - public boolean mAllowRequestLayout = true; - - public PagedViewWidgetImageView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void requestLayout() { - if (mAllowRequestLayout) { - super.requestLayout(); - } - } - - @Override - protected void onDraw(Canvas canvas) { - canvas.save(); - canvas.clipRect(getScrollX() + getPaddingLeft(), - getScrollY() + getPaddingTop(), - getScrollX() + getRight() - getLeft() - getPaddingRight(), - getScrollY() + getBottom() - getTop() - getPaddingBottom()); - - super.onDraw(canvas); - canvas.restore(); - - } -} diff --git a/src/com/android/launcher3/PagedViewWithDraggableItems.java b/src/com/android/launcher3/PagedViewWithDraggableItems.java deleted file mode 100644 index 0e593698d5..0000000000 --- a/src/com/android/launcher3/PagedViewWithDraggableItems.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - - -/* Class that does most of the work of enabling dragging items out of a PagedView by performing a - * vertical drag. Used by both CustomizePagedView and AllAppsPagedView. - * Subclasses must do the following: - * * call setDragSlopeThreshold after making an instance of the PagedViewWithDraggableItems - * * call child.setOnLongClickListener(this) and child.setOnTouchListener(this) on all children - * (good place to do it is in syncPageItems) - * * override beginDragging(View) (but be careful to call super.beginDragging(View) - * - */ -public abstract class PagedViewWithDraggableItems extends PagedView - implements View.OnLongClickListener, View.OnTouchListener { - private View mLastTouchedItem; - private boolean mIsDragging; - private boolean mIsDragEnabled; - private float mDragSlopeThreshold; - private Launcher mLauncher; - - public PagedViewWithDraggableItems(Context context) { - this(context, null); - } - - public PagedViewWithDraggableItems(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PagedViewWithDraggableItems(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mLauncher = (Launcher) context; - } - - protected boolean beginDragging(View v) { - boolean wasDragging = mIsDragging; - mIsDragging = true; - return !wasDragging; - } - - protected void cancelDragging() { - mIsDragging = false; - mLastTouchedItem = null; - mIsDragEnabled = false; - } - - private void handleTouchEvent(MotionEvent ev) { - final int action = ev.getAction(); - switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - cancelDragging(); - mIsDragEnabled = true; - break; - case MotionEvent.ACTION_MOVE: - if (mTouchState != TOUCH_STATE_SCROLLING && !mIsDragging && mIsDragEnabled) { - determineDraggingStart(ev); - } - break; - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - handleTouchEvent(ev); - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - handleTouchEvent(ev); - return super.onTouchEvent(ev); - } - - public void trimMemory() { - mLastTouchedItem = null; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - mLastTouchedItem = v; - mIsDragEnabled = true; - return false; - } - - @Override - public boolean onLongClick(View v) { - // Return early if this is not initiated from a touch - if (!v.isInTouchMode()) return false; - // Return early if we are still animating the pages - if (mNextPage != INVALID_PAGE) return false; - // When we have exited all apps or are in transition, disregard long clicks - if (!mLauncher.isAllAppsVisible() || - mLauncher.getWorkspace().isSwitchingState()) return false; - // Return if global dragging is not enabled - if (!mLauncher.isDraggingEnabled()) return false; - - return beginDragging(v); - } - - /* - * Determines if we should change the touch state to start scrolling after the - * user moves their touch point too far. - */ - protected void determineScrollingStart(MotionEvent ev) { - if (!mIsDragging) super.determineScrollingStart(ev); - } - - /* - * Determines if we should change the touch state to start dragging after the - * user moves their touch point far enough. - */ - protected void determineDraggingStart(MotionEvent ev) { - /* - * Locally do absolute value. mLastMotionX is set to the y value - * of the down event. - */ - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - final float x = ev.getX(pointerIndex); - final float y = ev.getY(pointerIndex); - final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); - - final int touchSlop = mTouchSlop; - boolean yMoved = yDiff > touchSlop; - boolean isUpwardMotion = (yDiff / (float) xDiff) > mDragSlopeThreshold; - - if (isUpwardMotion && yMoved && mLastTouchedItem != null) { - // Drag if the user moved far enough along the Y axis - beginDragging(mLastTouchedItem); - - // Cancel any pending long press - if (mAllowLongPress) { - mAllowLongPress = false; - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - final View currentPage = getPageAt(mCurrentPage); - if (currentPage != null) { - currentPage.cancelLongPress(); - } - } - } - } - - public void setDragSlopeThreshold(float dragSlopeThreshold) { - mDragSlopeThreshold = dragSlopeThreshold; - } - - @Override - protected void onDetachedFromWindow() { - cancelDragging(); - super.onDetachedFromWindow(); - } -} diff --git a/src/com/android/launcher3/Partner.java b/src/com/android/launcher3/Partner.java index e1913193b1..380078b267 100644 --- a/src/com/android/launcher3/Partner.java +++ b/src/com/android/launcher3/Partner.java @@ -47,8 +47,6 @@ public class Partner { public static final String RES_REQUIRE_FIRST_RUN_FLOW = "requires_first_run_flow"; /** These resources are used to override the device profile */ - public static final String RES_GRID_AA_SHORT_EDGE_COUNT = "grid_aa_short_edge_count"; - public static final String RES_GRID_AA_LONG_EDGE_COUNT = "grid_aa_long_edge_count"; public static final String RES_GRID_NUM_ROWS = "grid_num_rows"; public static final String RES_GRID_NUM_COLUMNS = "grid_num_columns"; public static final String RES_GRID_ICON_SIZE_DP = "grid_icon_size_dp"; @@ -116,56 +114,42 @@ public class Partner { return resId != 0 && getResources().getBoolean(resId); } - public DeviceProfile getDeviceProfileOverride(DisplayMetrics dm) { - boolean containsProfileOverrides = false; - - DeviceProfile dp = new DeviceProfile(); - - // We initialize customizable fields to be invalid - dp.numRows = -1; - dp.numColumns = -1; - dp.allAppsShortEdgeCount = -1; - dp.allAppsLongEdgeCount = -1; + public void applyInvariantDeviceProfileOverrides(InvariantDeviceProfile inv, DisplayMetrics dm) { + int numRows = -1; + int numColumns = -1; + float iconSize = -1; try { int resId = getResources().getIdentifier(RES_GRID_NUM_ROWS, "integer", getPackageName()); if (resId > 0) { - containsProfileOverrides = true; - dp.numRows = getResources().getInteger(resId); + numRows = getResources().getInteger(resId); } resId = getResources().getIdentifier(RES_GRID_NUM_COLUMNS, "integer", getPackageName()); if (resId > 0) { - containsProfileOverrides = true; - dp.numColumns = getResources().getInteger(resId); - } - - resId = getResources().getIdentifier(RES_GRID_AA_SHORT_EDGE_COUNT, - "integer", getPackageName()); - if (resId > 0) { - containsProfileOverrides = true; - dp.allAppsShortEdgeCount = getResources().getInteger(resId); - } - - resId = getResources().getIdentifier(RES_GRID_AA_LONG_EDGE_COUNT, - "integer", getPackageName()); - if (resId > 0) { - containsProfileOverrides = true; - dp.allAppsLongEdgeCount = getResources().getInteger(resId); + numColumns = getResources().getInteger(resId); } resId = getResources().getIdentifier(RES_GRID_ICON_SIZE_DP, "dimen", getPackageName()); if (resId > 0) { - containsProfileOverrides = true; int px = getResources().getDimensionPixelSize(resId); - dp.iconSize = DynamicGrid.dpiFromPx(px, dm); + iconSize = Utilities.dpiFromPx(px, dm); } } catch (Resources.NotFoundException ex) { Log.e(TAG, "Invalid Partner grid resource!", ex); + return; + } + + if (numRows > 0 && numColumns > 0) { + inv.numRows = numRows; + inv.numColumns = numColumns; + } + + if (iconSize > 0) { + inv.iconSize = iconSize; } - return containsProfileOverrides ? dp : null; } } diff --git a/src/com/android/launcher3/PendingAddItemInfo.java b/src/com/android/launcher3/PendingAddItemInfo.java index 967cc928e1..1aaf85bbda 100644 --- a/src/com/android/launcher3/PendingAddItemInfo.java +++ b/src/com/android/launcher3/PendingAddItemInfo.java @@ -16,92 +16,17 @@ package com.android.launcher3; -import android.appwidget.AppWidgetHostView; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; -import android.content.pm.ActivityInfo; -import android.os.Bundle; -import android.os.Parcelable; /** - * We pass this object with a drag from the customization tray + * Meta data that is used for deferred binding. + * e.g., this object is used to pass information on dragable targets when they are dropped onto + * the workspace from another container. */ -class PendingAddItemInfo extends ItemInfo { +public class PendingAddItemInfo extends ItemInfo { + /** * The component that will be created. */ - ComponentName componentName; -} - -class PendingAddShortcutInfo extends PendingAddItemInfo { - - ActivityInfo shortcutActivityInfo; - - public PendingAddShortcutInfo(ActivityInfo activityInfo) { - shortcutActivityInfo = activityInfo; - } - - @Override - public String toString() { - return "Shortcut: " + shortcutActivityInfo.packageName; - } -} - -class PendingAddWidgetInfo extends PendingAddItemInfo { - int minWidth; - int minHeight; - int minResizeWidth; - int minResizeHeight; - int previewImage; - int icon; - AppWidgetProviderInfo info; - AppWidgetHostView boundWidget; - Bundle bindOptions = null; - - // Any configuration data that we want to pass to a configuration activity when - // starting up a widget - String mimeType; - Parcelable configurationData; - - public PendingAddWidgetInfo(AppWidgetProviderInfo i, String dataMimeType, Parcelable data) { - itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; - this.info = i; - componentName = i.provider; - minWidth = i.minWidth; - minHeight = i.minHeight; - minResizeWidth = i.minResizeWidth; - minResizeHeight = i.minResizeHeight; - previewImage = i.previewImage; - icon = i.icon; - if (dataMimeType != null && data != null) { - mimeType = dataMimeType; - configurationData = data; - } - } - - // Copy constructor - public PendingAddWidgetInfo(PendingAddWidgetInfo copy) { - minWidth = copy.minWidth; - minHeight = copy.minHeight; - minResizeWidth = copy.minResizeWidth; - minResizeHeight = copy.minResizeHeight; - previewImage = copy.previewImage; - icon = copy.icon; - info = copy.info; - boundWidget = copy.boundWidget; - mimeType = copy.mimeType; - configurationData = copy.configurationData; - componentName = copy.componentName; - itemType = copy.itemType; - spanX = copy.spanX; - spanY = copy.spanY; - minSpanX = copy.minSpanX; - minSpanY = copy.minSpanY; - bindOptions = copy.bindOptions == null ? null : (Bundle) copy.bindOptions.clone(); - } - - @Override - public String toString() { - return "Widget: " + componentName.toShortString(); - } + public ComponentName componentName; } diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java index 179c60a982..08f8e56018 100644 --- a/src/com/android/launcher3/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/PendingAppWidgetHostView.java @@ -42,6 +42,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen private final int mStartState; private final Intent mIconLookupIntent; private final boolean mDisabledForSafeMode; + private Launcher mLauncher; private Bitmap mIcon; @@ -56,6 +57,8 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info, boolean disabledForSafeMode) { super(context); + + mLauncher = (Launcher) context; mInfo = info; mStartState = info.restoreStatus; mIconLookupIntent = new Intent().setComponent(info.providerName); @@ -64,7 +67,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen mPaint = new TextPaint(); mPaint.setColor(0xFFFFFFFF); mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, - getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics())); + mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics())); setBackgroundResource(R.drawable.quantum_panel_dark); setWillNotDraw(false); } @@ -118,7 +121,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen // 2) Preload icon in the center // 3) Setup icon in the center and app icon in the top right corner. if (mDisabledForSafeMode) { - FastBitmapDrawable disabledIcon = Utilities.createIconDrawable(mIcon); + FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon); disabledIcon.setGhostModeEnabled(true); mCenterDrawable = disabledIcon; mTopCornerDrawable = null; @@ -131,7 +134,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen sPreloaderTheme.applyStyle(R.style.PreloadIcon, true); } - FastBitmapDrawable drawable = Utilities.createIconDrawable(mIcon); + FastBitmapDrawable drawable = mLauncher.createIconDrawable(mIcon); mCenterDrawable = new PreloadIconDrawable(drawable, sPreloaderTheme); mCenterDrawable.setCallback(this); mTopCornerDrawable = null; @@ -173,12 +176,12 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen return; } + DeviceProfile grid = mLauncher.getDeviceProfile(); if (mTopCornerDrawable == null) { if (mDrawableSizeChanged) { int outset = (mCenterDrawable instanceof PreloadIconDrawable) ? ((PreloadIconDrawable) mCenterDrawable).getOutset() : 0; - int maxSize = LauncherAppState.getInstance().getDynamicGrid() - .getDeviceProfile().iconSizePx + 2 * outset; + int maxSize = grid.iconSizePx + 2 * outset; int size = Math.min(maxSize, Math.min( getWidth() - getPaddingLeft() - getPaddingRight(), getHeight() - getPaddingTop() - getPaddingBottom())); @@ -193,7 +196,6 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen } else { // Draw the top corner icon and "Setup" text is possible if (mDrawableSizeChanged) { - DeviceProfile grid = getDeviceProfile(); int iconSize = grid.iconSizePx; int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); @@ -251,8 +253,4 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen } } } - - private DeviceProfile getDeviceProfile() { - return LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile(); - } } diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java index 2972c4f9b4..bcb59c448d 100644 --- a/src/com/android/launcher3/PreloadIconDrawable.java +++ b/src/com/android/launcher3/PreloadIconDrawable.java @@ -54,12 +54,11 @@ class PreloadIconDrawable extends Drawable { mPaint.setStrokeCap(Paint.Cap.ROUND); setBounds(icon.getBounds()); - applyTheme(theme); + applyPreloaderTheme(theme); onLevelChange(0); } - @Override - public void applyTheme(Theme t) { + public void applyPreloaderTheme(Theme t) { TypedArray ta = t.obtainStyledAttributes(R.styleable.PreloadIconDrawable); mBgDrawable = ta.getDrawable(R.styleable.PreloadIconDrawable_background); mBgDrawable.setFilterBitmap(true); diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java index 99c2e0859c..4cdf1cac9a 100644 --- a/src/com/android/launcher3/SearchDropTargetBar.java +++ b/src/com/android/launcher3/SearchDropTargetBar.java @@ -33,23 +33,22 @@ import android.widget.FrameLayout; */ public class SearchDropTargetBar extends FrameLayout implements DragController.DragListener { - private static final int sTransitionInDuration = 200; - private static final int sTransitionOutDuration = 175; + private static final int TRANSITION_DURATION = 200; - private ObjectAnimator mDropTargetBarAnim; - private ValueAnimator mQSBSearchBarAnim; + private ObjectAnimator mShowDropTargetBarAnim; + private ValueAnimator mHideSearchBarAnim; private static final AccelerateInterpolator sAccelerateInterpolator = new AccelerateInterpolator(); private boolean mIsSearchBarHidden; private View mQSBSearchBar; private View mDropTargetBar; - private ButtonDropTarget mInfoDropTarget; - private ButtonDropTarget mDeleteDropTarget; - private int mBarHeight; private boolean mDeferOnDragEnd = false; - private boolean mEnableDropDownDropTargets; + // Drop targets + private ButtonDropTarget mInfoDropTarget; + private ButtonDropTarget mDeleteDropTarget; + private ButtonDropTarget mUninstallDropTarget; public SearchDropTargetBar(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -61,29 +60,30 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D public void setup(Launcher launcher, DragController dragController) { dragController.addDragListener(this); + dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); + dragController.addDragListener(mInfoDropTarget); dragController.addDragListener(mDeleteDropTarget); + dragController.addDragListener(mUninstallDropTarget); + dragController.addDropTarget(mInfoDropTarget); dragController.addDropTarget(mDeleteDropTarget); - dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); + dragController.addDropTarget(mUninstallDropTarget); + mInfoDropTarget.setLauncher(launcher); mDeleteDropTarget.setLauncher(launcher); + mUninstallDropTarget.setLauncher(launcher); } public void setQsbSearchBar(View qsb) { mQSBSearchBar = qsb; if (mQSBSearchBar != null) { - if (mEnableDropDownDropTargets) { - mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "translationY", 0, - -mBarHeight); - } else { - mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f); - } - setupAnimation(mQSBSearchBarAnim, mQSBSearchBar); + mHideSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f); + setupAnimation(mHideSearchBarAnim, mQSBSearchBar); } else { // Create a no-op animation of the search bar is null - mQSBSearchBarAnim = ValueAnimator.ofFloat(0, 0); - mQSBSearchBarAnim.setDuration(sTransitionInDuration); + mHideSearchBarAnim = ValueAnimator.ofFloat(0, 0); + mHideSearchBarAnim.setDuration(TRANSITION_DURATION); } } @@ -97,7 +97,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D private void setupAnimation(ValueAnimator anim, final View v) { anim.setInterpolator(sAccelerateInterpolator); - anim.setDuration(sTransitionInDuration); + anim.setDuration(TRANSITION_DURATION); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -116,80 +116,80 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D mDropTargetBar = findViewById(R.id.drag_target_bar); mInfoDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.info_target_text); mDeleteDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.delete_target_text); + mUninstallDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.uninstall_target_text); mInfoDropTarget.setSearchDropTargetBar(this); mDeleteDropTarget.setSearchDropTargetBar(this); - - mEnableDropDownDropTargets = - getResources().getBoolean(R.bool.config_useDropTargetDownTransition); + mUninstallDropTarget.setSearchDropTargetBar(this); // Create the various fade animations - if (mEnableDropDownDropTargets) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - mBarHeight = grid.searchBarSpaceHeightPx; - mDropTargetBar.setTranslationY(-mBarHeight); - mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "translationY", - -mBarHeight, 0f); - - } else { - mDropTargetBar.setAlpha(0f); - mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "alpha", 0f, 1f); - } - setupAnimation(mDropTargetBarAnim, mDropTargetBar); + mDropTargetBar.setAlpha(0f); + mShowDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "alpha", 0f, 1f); + setupAnimation(mShowDropTargetBarAnim, mDropTargetBar); } + /** + * Finishes all the animations on the search and drop target bars. + */ public void finishAnimations() { prepareStartAnimation(mDropTargetBar); - mDropTargetBarAnim.reverse(); + mShowDropTargetBarAnim.reverse(); prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.reverse(); + mHideSearchBarAnim.reverse(); } - /* - * Shows and hides the search bar. + /** + * Shows the search bar. */ public void showSearchBar(boolean animated) { - boolean needToCancelOngoingAnimation = mQSBSearchBarAnim.isRunning() && !animated; - if (!mIsSearchBarHidden && !needToCancelOngoingAnimation) return; + if (!mIsSearchBarHidden) return; if (animated) { prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.reverse(); + mHideSearchBarAnim.reverse(); } else { - mQSBSearchBarAnim.cancel(); - if (mQSBSearchBar != null && mEnableDropDownDropTargets) { - mQSBSearchBar.setTranslationY(0); - } else if (mQSBSearchBar != null) { + mHideSearchBarAnim.cancel(); + if (mQSBSearchBar != null) { mQSBSearchBar.setAlpha(1f); } } mIsSearchBarHidden = false; } + + /** + * Hides the search bar. We only use this for clings. + */ public void hideSearchBar(boolean animated) { - boolean needToCancelOngoingAnimation = mQSBSearchBarAnim.isRunning() && !animated; - if (mIsSearchBarHidden && !needToCancelOngoingAnimation) return; + if (mIsSearchBarHidden) return; if (animated) { prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.start(); + mHideSearchBarAnim.start(); } else { - mQSBSearchBarAnim.cancel(); - if (mQSBSearchBar != null && mEnableDropDownDropTargets) { - mQSBSearchBar.setTranslationY(-mBarHeight); - } else if (mQSBSearchBar != null) { + mHideSearchBarAnim.cancel(); + if (mQSBSearchBar != null) { mQSBSearchBar.setAlpha(0f); } } mIsSearchBarHidden = true; } - /* - * Gets various transition durations. + /** + * Shows the drop target bar. */ - public int getTransitionInDuration() { - return sTransitionInDuration; + public void showDeleteTarget() { + // Animate out the QSB search bar, and animate in the drop target bar + prepareStartAnimation(mDropTargetBar); + mShowDropTargetBarAnim.start(); + hideSearchBar(true); } - public int getTransitionOutDuration() { - return sTransitionOutDuration; + + /** + * Hides the drop target bar. + */ + public void hideDeleteTarget() { + // Restore the QSB search bar, and animate out the drop target bar + prepareStartAnimation(mDropTargetBar); + mShowDropTargetBarAnim.reverse(); + showSearchBar(true); } /* @@ -197,13 +197,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D */ @Override public void onDragStart(DragSource source, Object info, int dragAction) { - // Animate out the QSB search bar, and animate in the drop target bar - prepareStartAnimation(mDropTargetBar); - mDropTargetBarAnim.start(); - if (!mIsSearchBarHidden) { - prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.start(); - } + showDeleteTarget(); } public void deferOnDragEnd() { @@ -213,13 +207,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D @Override public void onDragEnd() { if (!mDeferOnDragEnd) { - // Restore the QSB search bar, and animate out the drop target bar - prepareStartAnimation(mDropTargetBar); - mDropTargetBarAnim.reverse(); - if (!mIsSearchBarHidden) { - prepareStartAnimation(mQSBSearchBar); - mQSBSearchBarAnim.reverse(); - } + hideDeleteTarget(); } else { mDeferOnDragEnd = false; } @@ -240,4 +228,13 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D return null; } } + + public void enableAccessibleDrag(boolean enable) { + if (mQSBSearchBar != null) { + mQSBSearchBar.setVisibility(enable ? View.GONE : View.VISIBLE); + } + mInfoDropTarget.enableAccessibleDrag(enable); + mDeleteDropTarget.enableAccessibleDrag(enable); + mUninstallDropTarget.enableAccessibleDrag(enable); + } } diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java new file mode 100644 index 0000000000..dab71c8627 --- /dev/null +++ b/src/com/android/launcher3/SettingsActivity.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.app.Activity; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceFragment; +import android.preference.SwitchPreference; + +/** + * Settings activity for Launcher. Currently implements the following setting: Allow rotation + */ +public class SettingsActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Display the fragment as the main content. + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new LauncherSettingsFragment()) + .commit(); + } + + /** + * This fragment shows the launcher preferences. + */ + public static class LauncherSettingsFragment extends PreferenceFragment + implements OnPreferenceChangeListener { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.launcher_preferences); + + SwitchPreference pref = (SwitchPreference) findPreference( + Utilities.ALLOW_ROTATION_PREFERENCE_KEY); + pref.setPersistent(false); + + Bundle extras = new Bundle(); + extras.putBoolean(LauncherSettings.Settings.EXTRA_DEFAULT_VALUE, false); + Bundle value = getActivity().getContentResolver().call( + LauncherSettings.Settings.CONTENT_URI, + LauncherSettings.Settings.METHOD_GET_BOOLEAN, + Utilities.ALLOW_ROTATION_PREFERENCE_KEY, extras); + pref.setChecked(value.getBoolean(LauncherSettings.Settings.EXTRA_VALUE)); + + pref.setOnPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Bundle extras = new Bundle(); + extras.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, (Boolean) newValue); + getActivity().getContentResolver().call( + LauncherSettings.Settings.CONTENT_URI, + LauncherSettings.Settings.METHOD_SET_BOOLEAN, + preference.getKey(), extras); + return true; + } + } +} diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java index bb5601e903..157b48a397 100644 --- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java +++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java @@ -23,6 +23,7 @@ import android.graphics.Paint; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; public class ShortcutAndWidgetContainer extends ViewGroup { static final String TAG = "CellLayoutChildren"; @@ -44,10 +45,13 @@ public class ShortcutAndWidgetContainer extends ViewGroup { private int mCountX; private int mCountY; + private Launcher mLauncher; + private boolean mInvertIfRtl = false; public ShortcutAndWidgetContainer(Context context) { super(context); + mLauncher = (Launcher) context; mWallpaperManager = WallpaperManager.getInstance(context); } @@ -124,22 +128,19 @@ public class ShortcutAndWidgetContainer extends ViewGroup { } int getCellContentWidth() { - final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + final DeviceProfile grid = mLauncher.getDeviceProfile(); return Math.min(getMeasuredHeight(), mIsHotseatLayout ? grid.hotseatCellWidthPx: grid.cellWidthPx); } int getCellContentHeight() { - final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + final DeviceProfile grid = mLauncher.getDeviceProfile(); return Math.min(getMeasuredHeight(), mIsHotseatLayout ? grid.hotseatCellHeightPx : grid.cellHeightPx); } public void measureChild(View child) { - final LauncherAppState app = LauncherAppState.getInstance(); - final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + final DeviceProfile grid = mLauncher.getDeviceProfile(); final int cellWidth = mCellWidth; final int cellHeight = mCellHeight; CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); @@ -168,12 +169,8 @@ public class ShortcutAndWidgetContainer extends ViewGroup { child.measure(childWidthMeasureSpec, childheightMeasureSpec); } - private boolean invertLayoutHorizontally() { - return mInvertIfRtl && isLayoutRtl(); - } - - public boolean isLayoutRtl() { - return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + public boolean invertLayoutHorizontally() { + return mInvertIfRtl && Utilities.isRtl(getResources()); } @Override diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 01f79314e0..56c0b9d2fc 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -23,7 +23,10 @@ import android.content.Intent; import android.graphics.Bitmap; import android.util.Log; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.compat.LauncherActivityInfoCompat; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; import java.util.ArrayList; import java.util.Arrays; @@ -46,18 +49,24 @@ public class ShortcutInfo extends ItemInfo { * be present along with {@link #FLAG_RESTORED_ICON}, and is set during default layout * parsing. */ - public static final int FLAG_AUTOINTALL_ICON = 2; + public static final int FLAG_AUTOINTALL_ICON = 2; //0B10; /** * The icon is being installed. If {@link FLAG_RESTORED_ICON} or {@link FLAG_AUTOINTALL_ICON} * is set, then the icon is either being installed or is in a broken state. */ - public static final int FLAG_INSTALL_SESSION_ACTIVE = 4; + public static final int FLAG_INSTALL_SESSION_ACTIVE = 4; // 0B100; /** * Indicates that the widget restore has started. */ - public static final int FLAG_RESTORE_STARTED = 8; + public static final int FLAG_RESTORE_STARTED = 8; //0B1000; + + /** + * Indicates if it represents a common type mentioned in {@link CommonAppTypeParser}. + * Upto 15 different types supported. + */ + public static final int FLAG_RESTORED_APP_TYPE = 0B0011110000; /** * The intent used to start the application. @@ -67,8 +76,9 @@ public class ShortcutInfo extends ItemInfo { /** * Indicates whether the icon comes from an application's resource (if false) * or from a custom Bitmap (if true.) + * TODO: remove this flag */ - boolean customIcon; + public boolean customIcon; /** * Indicates whether we're using the default fallback icon instead of something from the @@ -76,11 +86,16 @@ public class ShortcutInfo extends ItemInfo { */ boolean usingFallbackIcon; + /** + * Indicates whether we're using a low res icon + */ + boolean usingLowResIcon; + /** * If isShortcut=true and customIcon=false, this contains a reference to the * shortcut icon as an application's resource. */ - Intent.ShortcutIconResource iconResource; + public Intent.ShortcutIconResource iconResource; /** * The application icon. @@ -113,7 +128,7 @@ public class ShortcutInfo extends ItemInfo { /** * Refer {@link AppInfo#firstInstallTime}. */ - long firstInstallTime; + public long firstInstallTime; /** * TODO move this to {@link status} @@ -139,7 +154,7 @@ public class ShortcutInfo extends ItemInfo { Bitmap icon, UserHandleCompat user) { this(); this.intent = intent; - this.title = title; + this.title = Utilities.trim(title); this.contentDescription = contentDescription; mIcon = icon; this.user = user; @@ -147,7 +162,7 @@ public class ShortcutInfo extends ItemInfo { public ShortcutInfo(Context context, ShortcutInfo info) { super(info); - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); if (info.iconResource != null) { iconResource = new Intent.ShortcutIconResource(); @@ -165,7 +180,7 @@ public class ShortcutInfo extends ItemInfo { /** TODO: Remove this. It's only called by ApplicationInfo.makeShortcut. */ public ShortcutInfo(AppInfo info) { super(info); - title = info.title.toString(); + title = Utilities.trim(info.title); intent = new Intent(info.intent); customIcon = false; flags = info.flags; @@ -184,8 +199,10 @@ public class ShortcutInfo extends ItemInfo { } public void updateIcon(IconCache iconCache) { - mIcon = iconCache.getIcon(promisedIntent != null ? promisedIntent : intent, user); - usingFallbackIcon = iconCache.isDefaultIcon(mIcon, user); + if (itemType == Favorites.ITEM_TYPE_APPLICATION) { + iconCache.getTitleAndIcon(this, promisedIntent != null ? promisedIntent : intent, user, + shouldUseLowResIcon()); + } } @Override @@ -198,6 +215,7 @@ public class ShortcutInfo extends ItemInfo { String uri = promisedIntent != null ? promisedIntent.toUri(0) : (intent != null ? intent.toUri(0) : null); values.put(LauncherSettings.BaseLauncherColumns.INTENT, uri); + values.put(LauncherSettings.Favorites.RESTORED, status); if (customIcon) { values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, @@ -207,9 +225,9 @@ public class ShortcutInfo extends ItemInfo { if (!usingFallbackIcon) { writeBitmap(values, mIcon); } - values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, - LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE); if (iconResource != null) { + values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, + LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE); values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE, iconResource.packageName); values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE, @@ -256,5 +274,23 @@ public class ShortcutInfo extends ItemInfo { mInstallProgress = progress; status |= FLAG_INSTALL_SESSION_ACTIVE; } + + public boolean shouldUseLowResIcon() { + return usingLowResIcon && container >= 0 && rank >= FolderIcon.NUM_ITEMS_IN_PREVIEW; + } + + public static ShortcutInfo fromActivityInfo(LauncherActivityInfoCompat info, Context context) { + final ShortcutInfo shortcut = new ShortcutInfo(); + shortcut.user = info.getUser(); + shortcut.title = Utilities.trim(info.getLabel()); + shortcut.contentDescription = UserManagerCompat.getInstance(context) + .getBadgedLabelForUser(info.getLabel(), info.getUser()); + shortcut.customIcon = false; + shortcut.intent = AppInfo.makeLaunchIntent(context, info, info.getUser()); + shortcut.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + shortcut.flags = AppInfo.initFlags(info); + shortcut.firstInstallTime = info.getFirstInstallTime(); + return shortcut; + } } diff --git a/src/com/android/launcher3/SmoothPagedView.java b/src/com/android/launcher3/SmoothPagedView.java deleted file mode 100644 index 4e331aa2c8..0000000000 --- a/src/com/android/launcher3/SmoothPagedView.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.animation.Interpolator; - -public abstract class SmoothPagedView extends PagedView { - private static final float SMOOTHING_SPEED = 0.75f; - private static final float SMOOTHING_CONSTANT = (float) (0.016 / Math.log(SMOOTHING_SPEED)); - - private float mBaseLineFlingVelocity; - private float mFlingVelocityInfluence; - - static final int DEFAULT_MODE = 0; - static final int X_LARGE_MODE = 1; - - int mScrollMode; - - private Interpolator mScrollInterpolator; - - public static class OvershootInterpolator implements Interpolator { - private static final float DEFAULT_TENSION = 1.3f; - private float mTension; - - public OvershootInterpolator() { - mTension = DEFAULT_TENSION; - } - - public void setDistance(int distance) { - mTension = distance > 0 ? DEFAULT_TENSION / distance : DEFAULT_TENSION; - } - - public void disableSettle() { - mTension = 0.f; - } - - public float getInterpolation(float t) { - t -= 1.0f; - return t * t * ((mTension + 1) * t + mTension) + 1.0f; - } - } - - /** - * Used to inflate the Workspace from XML. - * - * @param context The application's context. - * @param attrs The attributes set containing the Workspace's customization values. - */ - public SmoothPagedView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - /** - * Used to inflate the Workspace from XML. - * - * @param context The application's context. - * @param attrs The attributes set containing the Workspace's customization values. - * @param defStyle Unused. - */ - public SmoothPagedView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - mUsePagingTouchSlop = false; - - // This means that we'll take care of updating the scroll parameter ourselves (we do it - // in computeScroll), we only do this in the OVERSHOOT_MODE, ie. on phones - mDeferScrollUpdate = mScrollMode != X_LARGE_MODE; - } - - protected int getScrollMode() { - return X_LARGE_MODE; - } - - /** - * Initializes various states for this workspace. - */ - @Override - protected void init() { - super.init(); - - mScrollMode = getScrollMode(); - if (mScrollMode == DEFAULT_MODE) { - mBaseLineFlingVelocity = 2500.0f; - mFlingVelocityInfluence = 0.4f; - mScrollInterpolator = new OvershootInterpolator(); - setDefaultInterpolator(mScrollInterpolator); - } - } - - @Override - protected void snapToDestination() { - if (mScrollMode == X_LARGE_MODE) { - super.snapToDestination(); - } else { - snapToPageWithVelocity(getPageNearestToCenterOfScreen(), 0); - } - } - - @Override - protected void snapToPageWithVelocity(int whichPage, int velocity) { - if (mScrollMode == X_LARGE_MODE) { - super.snapToPageWithVelocity(whichPage, velocity); - } else { - snapToPageWithVelocity(whichPage, 0, true); - } - } - - private void snapToPageWithVelocity(int whichPage, int velocity, boolean settle) { - // if (!mScroller.isFinished()) return; - - whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); - - final int screenDelta = Math.max(1, Math.abs(whichPage - mCurrentPage)); - final int newX = getScrollForPage(whichPage); - final int delta = newX - mUnboundedScrollX; - int duration = (screenDelta + 1) * 100; - - if (!mScroller.isFinished()) { - mScroller.abortAnimation(); - } - - if (settle) { - ((OvershootInterpolator) mScrollInterpolator).setDistance(screenDelta); - } else { - ((OvershootInterpolator) mScrollInterpolator).disableSettle(); - } - - velocity = Math.abs(velocity); - if (velocity > 0) { - duration += (duration / (velocity / mBaseLineFlingVelocity)) * mFlingVelocityInfluence; - } else { - duration += 100; - } - - snapToPage(whichPage, delta, duration); - } - - @Override - protected void snapToPage(int whichPage) { - if (mScrollMode == X_LARGE_MODE) { - super.snapToPage(whichPage); - } else { - snapToPageWithVelocity(whichPage, 0, false); - } - } - - @Override - public void computeScroll() { - if (mScrollMode == X_LARGE_MODE) { - super.computeScroll(); - } else { - boolean scrollComputed = computeScrollHelper(); - - if (!scrollComputed && mTouchState == TOUCH_STATE_SCROLLING) { - final float now = System.nanoTime() / NANOTIME_DIV; - final float e = (float) Math.exp((now - mSmoothingTime) / SMOOTHING_CONSTANT); - - final float dx = mTouchX - mUnboundedScrollX; - scrollTo(Math.round(mUnboundedScrollX + dx * e), getScrollY()); - mSmoothingTime = now; - - // Keep generating points as long as we're more than 1px away from the target - if (dx > 1.f || dx < -1.f) { - invalidate(); - } - } - } - } -} diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java index a87986562a..cb0e252b2e 100644 --- a/src/com/android/launcher3/Stats.java +++ b/src/com/android/launcher3/Stats.java @@ -20,16 +20,64 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Bundle; import android.util.Log; - -import java.io.*; -import java.util.ArrayList; +import android.view.View; +import android.view.ViewParent; public class Stats { - private static final boolean DEBUG_BROADCASTS = false; - private static final String TAG = "Launcher3/Stats"; - private static final boolean LOCAL_LAUNCH_LOG = true; + /** + * Implemented by containers to provide a launch source for a given child. + */ + public interface LaunchSourceProvider { + void fillInLaunchSourceData(Bundle sourceData); + } + + /** + * Helpers to add the source to a launch intent. + */ + public static class LaunchSourceUtils { + /** + * Create a default bundle for LaunchSourceProviders to fill in their data. + */ + public static Bundle createSourceData() { + Bundle sourceData = new Bundle(); + sourceData.putString(SOURCE_EXTRA_CONTAINER, CONTAINER_HOMESCREEN); + // Have default container/sub container pages + sourceData.putInt(SOURCE_EXTRA_CONTAINER_PAGE, 0); + sourceData.putInt(SOURCE_EXTRA_SUB_CONTAINER_PAGE, 0); + return sourceData; + } + + /** + * Finds the next launch source provider in the parents of the view hierarchy and populates + * the source data from that provider. + */ + public static void populateSourceDataFromAncestorProvider(View v, Bundle sourceData) { + if (v == null) { + return; + } + + Stats.LaunchSourceProvider provider = null; + ViewParent parent = v.getParent(); + while (parent != null && parent instanceof View) { + if (parent instanceof Stats.LaunchSourceProvider) { + provider = (Stats.LaunchSourceProvider) parent; + break; + } + parent = parent.getParent(); + } + + if (provider != null) { + provider.fillInLaunchSourceData(sourceData); + } else if (LauncherAppState.isDogfoodBuild()) { + throw new RuntimeException("Expected LaunchSourceProvider"); + } + } + } + + private static final boolean DEBUG_BROADCASTS = false; public static final String ACTION_LAUNCH = "com.android.launcher3.action.LAUNCH"; public static final String EXTRA_INTENT = "intent"; @@ -37,55 +85,37 @@ public class Stats { public static final String EXTRA_SCREEN = "screen"; public static final String EXTRA_CELLX = "cellX"; public static final String EXTRA_CELLY = "cellY"; + public static final String EXTRA_SOURCE = "source"; - private static final int LOG_VERSION = 1; - private static final int LOG_TAG_VERSION = 0x1; - private static final int LOG_TAG_LAUNCH = 0x1000; + public static final String SOURCE_EXTRA_CONTAINER = "container"; + public static final String SOURCE_EXTRA_CONTAINER_PAGE = "container_page"; + public static final String SOURCE_EXTRA_SUB_CONTAINER = "sub_container"; + public static final String SOURCE_EXTRA_SUB_CONTAINER_PAGE = "sub_container_page"; - private static final int STATS_VERSION = 1; - private static final int INITIAL_STATS_SIZE = 100; + public static final String CONTAINER_SEARCH_BOX = "search_box"; + public static final String CONTAINER_ALL_APPS = "all_apps"; + public static final String CONTAINER_HOMESCREEN = "homescreen"; // aka. Workspace + public static final String CONTAINER_HOTSEAT = "hotseat"; - // TODO: delayed/batched writes - private static final boolean FLUSH_IMMEDIATELY = true; + public static final String SUB_CONTAINER_FOLDER = "folder"; + public static final String SUB_CONTAINER_ALL_APPS_A_Z = "a-z"; + public static final String SUB_CONTAINER_ALL_APPS_PREDICTION = "prediction"; + public static final String SUB_CONTAINER_ALL_APPS_SEARCH = "search"; private final Launcher mLauncher; - private final String mLaunchBroadcastPermission; - DataOutputStream mLog; - - ArrayList mIntents; - ArrayList mHistogram; - public Stats(Launcher launcher) { mLauncher = launcher; - mLaunchBroadcastPermission = launcher.getResources().getString(R.string.receive_launch_broadcasts_permission); - loadStats(); - - if (LOCAL_LAUNCH_LOG) { - try { - mLog = new DataOutputStream(mLauncher.openFileOutput( - LauncherFiles.LAUNCHES_LOG, Context.MODE_APPEND)); - mLog.writeInt(LOG_TAG_VERSION); - mLog.writeInt(LOG_VERSION); - } catch (FileNotFoundException e) { - Log.e(TAG, "unable to create stats log: " + e); - mLog = null; - } catch (IOException e) { - Log.e(TAG, "unable to write to stats log: " + e); - mLog = null; - } - } - if (DEBUG_BROADCASTS) { launcher.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - android.util.Log.v("Stats", "got broadcast: " + intent + " for launched intent: " + Log.v("Stats", "got broadcast: " + intent + " for launched intent: " + intent.getStringExtra(EXTRA_INTENT)); } }, @@ -96,26 +126,11 @@ public class Stats { } } - public void incrementLaunch(String intentStr) { - int pos = mIntents.indexOf(intentStr); - if (pos < 0) { - mIntents.add(intentStr); - mHistogram.add(1); - } else { - mHistogram.set(pos, mHistogram.get(pos) + 1); - } - } - - public void recordLaunch(Intent intent) { - recordLaunch(intent, null); - } - - public void recordLaunch(Intent intent, ShortcutInfo shortcut) { + public void recordLaunch(View v, Intent intent, ShortcutInfo shortcut) { intent = new Intent(intent); intent.setSourceBounds(null); final String flat = intent.toUri(0); - Intent broadcastIntent = new Intent(ACTION_LAUNCH).putExtra(EXTRA_INTENT, flat); if (shortcut != null) { broadcastIntent.putExtra(EXTRA_CONTAINER, shortcut.container) @@ -123,95 +138,10 @@ public class Stats { .putExtra(EXTRA_CELLX, shortcut.cellX) .putExtra(EXTRA_CELLY, shortcut.cellY); } + + Bundle sourceExtras = LaunchSourceUtils.createSourceData(); + LaunchSourceUtils.populateSourceDataFromAncestorProvider(v, sourceExtras); + broadcastIntent.putExtra(EXTRA_SOURCE, sourceExtras); mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission); - - incrementLaunch(flat); - - if (FLUSH_IMMEDIATELY) { - saveStats(); - } - - if (LOCAL_LAUNCH_LOG && mLog != null) { - try { - mLog.writeInt(LOG_TAG_LAUNCH); - mLog.writeLong(System.currentTimeMillis()); - if (shortcut == null) { - mLog.writeShort(0); - mLog.writeShort(0); - mLog.writeShort(0); - mLog.writeShort(0); - } else { - mLog.writeShort((short) shortcut.container); - mLog.writeShort((short) shortcut.screenId); - mLog.writeShort((short) shortcut.cellX); - mLog.writeShort((short) shortcut.cellY); - } - mLog.writeUTF(flat); - if (FLUSH_IMMEDIATELY) { - mLog.flush(); // TODO: delayed writes - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private void saveStats() { - DataOutputStream stats = null; - try { - stats = new DataOutputStream(mLauncher.openFileOutput( - LauncherFiles.STATS_LOG + ".tmp", Context.MODE_PRIVATE)); - stats.writeInt(STATS_VERSION); - final int N = mHistogram.size(); - stats.writeInt(N); - for (int i=0; i(INITIAL_STATS_SIZE); - mHistogram = new ArrayList(INITIAL_STATS_SIZE); - DataInputStream stats = null; - try { - stats = new DataInputStream(mLauncher.openFileInput(LauncherFiles.STATS_LOG)); - final int version = stats.readInt(); - if (version == STATS_VERSION) { - final int N = stats.readInt(); - for (int i=0; i= Build.VERSION_CODES.JELLY_BEAN_MR2) { + UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + Bundle restrictions = userManager.getUserRestrictions(); + if (restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false) + || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false)) { + return false; + } + } + + Pair componentInfo = getAppInfoFlags(info); + return componentInfo != null && (componentInfo.second & AppInfo.DOWNLOADED_FLAG) != 0; + } + + /** + * @return the component name and flags if {@param info} is an AppInfo or an app shortcut. + */ + private static Pair getAppInfoFlags(Object item) { + if (item instanceof AppInfo) { + AppInfo info = (AppInfo) item; + return Pair.create(info.componentName, info.flags); + } else if (item instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) item; + ComponentName component = info.getTargetComponent(); + if (info.itemType == LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION + && component != null) { + return Pair.create(component, info.flags); + } + } + return null; + } + + @Override + public void onDrop(DragObject d) { + // Differ item deletion + if (d.dragSource instanceof UninstallSource) { + ((UninstallSource) d.dragSource).deferCompleteDropAfterUninstallActivity(); + } + super.onDrop(d); + } + + @Override + void completeDrop(final DragObject d) { + final Pair componentInfo = getAppInfoFlags(d.dragInfo); + final UserHandleCompat user = ((ItemInfo) d.dragInfo).user; + if (startUninstallActivity(mLauncher, d.dragInfo)) { + + final Runnable checkIfUninstallWasSuccess = new Runnable() { + @Override + public void run() { + String packageName = componentInfo.first.getPackageName(); + boolean uninstallSuccessful = !AllAppsList.packageHasActivities( + getContext(), packageName, user); + sendUninstallResult(d.dragSource, uninstallSuccessful); + } + }; + mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess); + } else { + sendUninstallResult(d.dragSource, false); + } + } + + public static boolean startUninstallActivity(Launcher launcher, Object info) { + final Pair componentInfo = getAppInfoFlags(info); + final UserHandleCompat user = ((ItemInfo) info).user; + return launcher.startApplicationUninstallActivity( + componentInfo.first, componentInfo.second, user); + } + + @Thunk void sendUninstallResult(DragSource target, boolean result) { + if (target instanceof UninstallSource) { + ((UninstallSource) target).onUninstallActivityReturned(result); + } + } + + /** + * Interface defining an object that can provide uninstallable drag objects. + */ + public static interface UninstallSource { + + /** + * A pending uninstall operation was complete. + * @param result true if uninstall was successful, false otherwise. + */ + void onUninstallActivityReturned(boolean result); + + /** + * Indicates that an uninstall request are made and the actual result may come + * after some time. + */ + void deferCompleteDropAfterUninstallActivity(); + } +} diff --git a/src/com/android/launcher3/UninstallShortcutReceiver.java b/src/com/android/launcher3/UninstallShortcutReceiver.java deleted file mode 100644 index ccea4ec0c3..0000000000 --- a/src/com/android/launcher3/UninstallShortcutReceiver.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.widget.Toast; - -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Iterator; - -public class UninstallShortcutReceiver extends BroadcastReceiver { - private static final String ACTION_UNINSTALL_SHORTCUT = - "com.android.launcher.action.UNINSTALL_SHORTCUT"; - - // The set of shortcuts that are pending uninstall - private static ArrayList mUninstallQueue = - new ArrayList(); - - // Determines whether to defer uninstalling shortcuts immediately until - // disableAndFlushUninstallQueue() is called. - private static boolean mUseUninstallQueue = false; - - private static class PendingUninstallShortcutInfo { - Intent data; - - public PendingUninstallShortcutInfo(Intent rawData) { - data = rawData; - } - } - - public void onReceive(Context context, Intent data) { - if (!ACTION_UNINSTALL_SHORTCUT.equals(data.getAction())) { - return; - } - - PendingUninstallShortcutInfo info = new PendingUninstallShortcutInfo(data); - if (mUseUninstallQueue) { - mUninstallQueue.add(info); - } else { - processUninstallShortcut(context, info); - } - } - - static void enableUninstallQueue() { - mUseUninstallQueue = true; - } - - static void disableAndFlushUninstallQueue(Context context) { - mUseUninstallQueue = false; - Iterator iter = mUninstallQueue.iterator(); - while (iter.hasNext()) { - processUninstallShortcut(context, iter.next()); - iter.remove(); - } - } - - private static void processUninstallShortcut(Context context, - PendingUninstallShortcutInfo pendingInfo) { - final Intent data = pendingInfo.data; - - LauncherAppState.setApplicationContext(context.getApplicationContext()); - LauncherAppState app = LauncherAppState.getInstance(); - synchronized (app) { // TODO: make removeShortcut internally threadsafe - removeShortcut(context, data); - } - } - - private static void removeShortcut(Context context, Intent data) { - Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); - String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); - boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); - - if (intent != null && name != null) { - final ContentResolver cr = context.getContentResolver(); - Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, - new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT }, - LauncherSettings.Favorites.TITLE + "=?", new String[] { name }, null); - - final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); - final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); - - boolean changed = false; - - try { - while (c.moveToNext()) { - try { - if (intent.filterEquals(Intent.parseUri(c.getString(intentIndex), 0))) { - final long id = c.getLong(idIndex); - final Uri uri = LauncherSettings.Favorites.getContentUri(id, false); - cr.delete(uri, null, null); - changed = true; - if (!duplicate) { - break; - } - } - } catch (URISyntaxException e) { - // Ignore - } - } - } finally { - c.close(); - } - - if (changed) { - cr.notifyChange(LauncherSettings.Favorites.CONTENT_URI, null); - Toast.makeText(context, context.getString(R.string.shortcut_uninstalled, name), - Toast.LENGTH_SHORT).show(); - } - } - } -} diff --git a/src/com/android/launcher3/UserInitializeReceiver.java b/src/com/android/launcher3/UserInitializeReceiver.java deleted file mode 100644 index d8e17b12fb..0000000000 --- a/src/com/android/launcher3/UserInitializeReceiver.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.launcher3; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Takes care of setting initial wallpaper for a user, by selecting the - * first wallpaper that is not in use by another user. - */ -public class UserInitializeReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - // TODO: initial wallpaper now that wallpapers are owned by another app - } -} diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 215d63d2e7..8fd298df72 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -25,13 +25,16 @@ import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; +import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; @@ -42,26 +45,38 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import android.util.TypedValue; import android.view.View; import android.widget.Toast; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayList; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Various utilities shared amongst the Launcher's classes. */ public final class Utilities { - private static final String TAG = "Launcher.Utilities"; - private static int sIconWidth = -1; - private static int sIconHeight = -1; + private static final String TAG = "Launcher.Utilities"; private static final Rect sOldBounds = new Rect(); private static final Canvas sCanvas = new Canvas(); + private static final Pattern sTrimPattern = + Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); + static { sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, Paint.FILTER_BITMAP_FLAG)); @@ -69,39 +84,30 @@ public final class Utilities { static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff }; static int sColorIndex = 0; - static int[] sLoc0 = new int[2]; - static int[] sLoc1 = new int[2]; + private static final int[] sLoc0 = new int[2]; + private static final int[] sLoc1 = new int[2]; // To turn on these properties, type // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] - static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; - public static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); + private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; + private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); - /** - * Returns a FastBitmapDrawable with the icon, accurately sized. - */ - public static FastBitmapDrawable createIconDrawable(Bitmap icon) { - FastBitmapDrawable d = new FastBitmapDrawable(icon); - d.setFilterBitmap(true); - resizeIconDrawable(d); - return d; - } - - /** - * Resizes an icon drawable to the correct icon size. - */ - static void resizeIconDrawable(Drawable icon) { - icon.setBounds(0, 0, sIconWidth, sIconHeight); - } + public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation"; public static boolean isPropertyEnabled(String propertyName) { return Log.isLoggable(propertyName, Log.VERBOSE); } - public static boolean isRotationEnabled(Context c) { - boolean enableRotation = sForceEnableRotation || - c.getResources().getBoolean(R.bool.allow_rotation); - return enableRotation; + public static boolean isAllowRotationPrefEnabled(Context context, boolean multiProcess) { + SharedPreferences sharedPrefs = context.getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE | (multiProcess ? + Context.MODE_MULTI_PROCESS : 0)); + boolean allowRotationPref = sharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false); + return sForceEnableRotation || allowRotationPref; + } + + public static boolean isRotationAllowedForDevice(Context context) { + return sForceEnableRotation || context.getResources().getBoolean(R.bool.allow_rotation); } /** @@ -111,11 +117,30 @@ public final class Utilities { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } + public static boolean isLmpMR1OrAbove() { + // TODO(adamcohen): update to Build.VERSION_CODES.LOLLIPOP_MR1 once building against 22; + return Build.VERSION.SDK_INT >= 22; + } + + public static boolean isLmpMR1() { + // TODO(adamcohen): update to Build.VERSION_CODES.LOLLIPOP_MR1 once building against 22; + return Build.VERSION.SDK_INT == 22; + } + + public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) { + byte[] data = c.getBlob(iconIndex); + try { + return createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context); + } catch (Exception e) { + return null; + } + } + /** * Returns a bitmap suitable for the all apps view. If the package or the resource do not * exist, it returns null. */ - static Bitmap createIconBitmap(String packageName, String resourceName, IconCache cache, + public static Bitmap createIconBitmap(String packageName, String resourceName, Context context) { PackageManager packageManager = context.getPackageManager(); // the resource @@ -124,7 +149,8 @@ public final class Utilities { if (resources != null) { final int id = resources.getIdentifier(resourceName, null, null); return createIconBitmap( - resources.getDrawableForDensity(id, cache.getFullResIconDpi()), context); + resources.getDrawableForDensity(id, LauncherAppState.getInstance() + .getInvariantDeviceProfile().fillResIconDpi), context); } } catch (Exception e) { // Icon not found. @@ -132,16 +158,16 @@ public final class Utilities { return null; } + private static int getIconBitmapSize() { + return LauncherAppState.getInstance().getInvariantDeviceProfile().iconBitmapSize; + } + /** * Returns a bitmap which is of the appropriate size to be displayed as an icon */ - static Bitmap createIconBitmap(Bitmap icon, Context context) { - synchronized (sCanvas) { // we share the statics :-( - if (sIconWidth == -1) { - initStatics(context); - } - } - if (sIconWidth == icon.getWidth() && sIconHeight == icon.getHeight()) { + public static Bitmap createIconBitmap(Bitmap icon, Context context) { + final int iconBitmapSize = getIconBitmapSize(); + if (iconBitmapSize == icon.getWidth() && iconBitmapSize == icon.getHeight()) { return icon; } return createIconBitmap(new BitmapDrawable(context.getResources(), icon), context); @@ -151,13 +177,11 @@ public final class Utilities { * Returns a bitmap suitable for the all apps view. */ public static Bitmap createIconBitmap(Drawable icon, Context context) { - synchronized (sCanvas) { // we share the statics :-( - if (sIconWidth == -1) { - initStatics(context); - } + synchronized (sCanvas) { + final int iconBitmapSize = getIconBitmapSize(); - int width = sIconWidth; - int height = sIconHeight; + int width = iconBitmapSize; + int height = iconBitmapSize; if (icon instanceof PaintDrawable) { PaintDrawable painter = (PaintDrawable) icon; @@ -184,8 +208,8 @@ public final class Utilities { } // no intrinsic size --> use default size - int textureWidth = sIconWidth; - int textureHeight = sIconHeight; + int textureWidth = iconBitmapSize; + int textureHeight = iconBitmapSize; final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, Bitmap.Config.ARGB_8888); @@ -315,15 +339,6 @@ public final class Utilities { localY < (v.getHeight() + slop); } - private static void initStatics(Context context) { - final Resources resources = context.getResources(); - sIconWidth = sIconHeight = (int) resources.getDimension(R.dimen.app_icon_size); - } - - public static void setIconSize(int widthPx) { - sIconWidth = sIconHeight = widthPx; - } - public static void scaleRect(Rect r, float scale) { if (scale != 1.0f) { r.left = (int) (r.left * scale + 0.5f); @@ -542,4 +557,156 @@ public final class Utilities { } return defaultWidgetForSearchPackage; } + + /** + * Compresses the bitmap to a byte array for serialization. + */ + public static byte[] flattenBitmap(Bitmap bitmap) { + // Try go guesstimate how much space the icon will take when serialized + // to avoid unnecessary allocations/copies during the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + + /** + * Find the first vacant cell, if there is one. + * + * @param vacant Holds the x and y coordinate of the vacant cell + * @param spanX Horizontal cell span. + * @param spanY Vertical cell span. + * + * @return true if a vacant cell was found + */ + public static boolean findVacantCell(int[] vacant, int spanX, int spanY, + int xCount, int yCount, boolean[][] occupied) { + + for (int y = 0; (y + spanY) <= yCount; y++) { + for (int x = 0; (x + spanX) <= xCount; x++) { + boolean available = !occupied[x][y]; + out: for (int i = x; i < x + spanX; i++) { + for (int j = y; j < y + spanY; j++) { + available = available && !occupied[i][j]; + if (!available) break out; + } + } + + if (available) { + vacant[0] = x; + vacant[1] = y; + return true; + } + } + } + + return false; + } + + /** + * Trims the string, removing all whitespace at the beginning and end of the string. + * Non-breaking whitespaces are also removed. + */ + public static String trim(CharSequence s) { + if (s == null) { + return null; + } + + // Just strip any sequence of whitespace or java space characters from the beginning and end + Matcher m = sTrimPattern.matcher(s); + return m.replaceAll("$1"); + } + + /** + * Calculates the height of a given string at a specific text size. + */ + public static float calculateTextHeight(float textSizePx) { + Paint p = new Paint(); + p.setTextSize(textSizePx); + Paint.FontMetrics fm = p.getFontMetrics(); + return -fm.top + fm.bottom; + } + + /** + * Convenience println with multiple args. + */ + public static void println(String key, Object... args) { + StringBuilder b = new StringBuilder(); + b.append(key); + b.append(": "); + boolean isFirstArgument = true; + for (Object arg : args) { + if (isFirstArgument) { + isFirstArgument = false; + } else { + b.append(", "); + } + b.append(arg); + } + System.out.println(b.toString()); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isRtl(Resources res) { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) && + (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + } + + public static void assertWorkerThread() { + if (LauncherAppState.isDogfoodBuild() && + (LauncherModel.sWorkerThread.getThreadId() != Process.myTid())) { + throw new IllegalStateException(); + } + } + + /** + * Returns true if the intent is a valid launch intent for a launcher activity of an app. + * This is used to identify shortcuts which are different from the ones exposed by the + * applications' manifest file. + * + * @param launchIntent The intent that will be launched when the shortcut is clicked. + */ + public static boolean isLauncherAppTarget(Intent launchIntent) { + if (launchIntent != null + && Intent.ACTION_MAIN.equals(launchIntent.getAction()) + && launchIntent.getComponent() != null + && launchIntent.getCategories() != null + && launchIntent.getCategories().size() == 1 + && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER) + && TextUtils.isEmpty(launchIntent.getDataString())) { + // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE. + Bundle extras = launchIntent.getExtras(); + if (extras == null) { + return true; + } else { + Set keys = extras.keySet(); + return keys.size() == 1 && keys.contains(ItemInfo.EXTRA_PROFILE); + } + }; + return false; + } + + public static float dpiFromPx(int size, DisplayMetrics metrics){ + float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; + return (size / densityRatio); + } + public static int pxFromDp(float size, DisplayMetrics metrics) { + return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + size, metrics)); + } + public static int pxFromSp(float size, DisplayMetrics metrics) { + return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + size, metrics)); + } + + public static String createDbSelectionQuery(String columnName, Iterable values) { + return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values)); + } } diff --git a/src/com/android/launcher3/WeightWatcher.java b/src/com/android/launcher3/WeightWatcher.java index 70b8afea83..75684797fa 100644 --- a/src/com/android/launcher3/WeightWatcher.java +++ b/src/com/android/launcher3/WeightWatcher.java @@ -34,6 +34,8 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.launcher3.util.Thunk; + public class WeightWatcher extends LinearLayout { private static final int RAM_GRAPH_RSS_COLOR = 0xFF990000; private static final int RAM_GRAPH_PSS_COLOR = 0xFF99CC00; @@ -81,7 +83,7 @@ public class WeightWatcher extends LinearLayout { } } }; - private MemoryTracker mMemoryService; + @Thunk MemoryTracker mMemoryService; public WeightWatcher(Context context, AttributeSet attrs) { super(context, attrs); @@ -134,7 +136,7 @@ public class WeightWatcher extends LinearLayout { GraphView mRamGraph; TextView mText; int mPid; - private MemoryTracker.ProcessMemInfo mMemInfo; + @Thunk MemoryTracker.ProcessMemInfo mMemInfo; public ProcessWatcher(Context context) { this(context, null); diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index 4e6fe1f886..629387ed0b 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -1,526 +1,353 @@ package com.android.launcher3; -import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; -import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.database.Cursor; -import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDiskIOException; import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteReadOnlyDatabaseException; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.Rect; -import android.graphics.Shader; +import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; -import android.os.Build; +import android.os.Handler; import android.util.Log; -import com.android.launcher3.compat.AppWidgetManagerCompat; +import android.util.LongSparseArray; + +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.widget.WidgetCell; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.lang.ref.SoftReference; -import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; public class WidgetPreviewLoader { - private static abstract class SoftReferenceThreadLocal { - private ThreadLocal> mThreadLocal; - public SoftReferenceThreadLocal() { - mThreadLocal = new ThreadLocal>(); - } - - abstract T initialValue(); - - public void set(T t) { - mThreadLocal.set(new SoftReference(t)); - } - - public T get() { - SoftReference reference = mThreadLocal.get(); - T obj; - if (reference == null) { - obj = initialValue(); - mThreadLocal.set(new SoftReference(obj)); - return obj; - } else { - obj = reference.get(); - if (obj == null) { - obj = initialValue(); - mThreadLocal.set(new SoftReference(obj)); - } - return obj; - } - } - } - - private static class CanvasCache extends SoftReferenceThreadLocal { - @Override - protected Canvas initialValue() { - return new Canvas(); - } - } - - private static class PaintCache extends SoftReferenceThreadLocal { - @Override - protected Paint initialValue() { - return null; - } - } - - private static class BitmapCache extends SoftReferenceThreadLocal { - @Override - protected Bitmap initialValue() { - return null; - } - } - - private static class RectCache extends SoftReferenceThreadLocal { - @Override - protected Rect initialValue() { - return new Rect(); - } - } - - private static class BitmapFactoryOptionsCache extends - SoftReferenceThreadLocal { - @Override - protected BitmapFactory.Options initialValue() { - return new BitmapFactory.Options(); - } - } - private static final String TAG = "WidgetPreviewLoader"; - private static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version"; + private static final boolean DEBUG = false; private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f; - private static final HashSet sInvalidPackages = new HashSet(); - // Used for drawing shortcut previews - private final BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); - private final PaintCache mCachedShortcutPreviewPaint = new PaintCache(); - private final CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); + private final HashMap mPackageVersions = new HashMap<>(); - // Used for drawing widget previews - private final CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); - private final RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); - private final RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); - private final PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); - private final PaintCache mDefaultAppWidgetPreviewPaint = new PaintCache(); - private final BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache(); - - private final HashMap> mLoadedPreviews = new HashMap>(); - private final ArrayList> mUnusedBitmaps = new ArrayList>(); + /** + * Weak reference objects, do not prevent their referents from being made finalizable, + * finalized, and then reclaimed. + * Note: synchronized block used for this variable is expensive and the block should always + * be posted to a background thread. + */ + @Thunk final Set mUnusedBitmaps = + Collections.newSetFromMap(new WeakHashMap()); private final Context mContext; - private final int mAppIconSize; private final IconCache mIconCache; + private final UserManagerCompat mUserManager; private final AppWidgetManagerCompat mManager; - - private int mPreviewBitmapWidth; - private int mPreviewBitmapHeight; - private String mSize; - private PagedViewCellLayout mWidgetSpacingLayout; - - private String mCachedSelectQuery; - - - private CacheDb mDb; + private final CacheDb mDb; + private final int mProfileBadgeMargin; private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); + @Thunk final Handler mWorkerHandler; - public WidgetPreviewLoader(Context context) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); - + public WidgetPreviewLoader(Context context, IconCache iconCache) { mContext = context; - mAppIconSize = grid.iconSizePx; - mIconCache = app.getIconCache(); + mIconCache = iconCache; mManager = AppWidgetManagerCompat.getInstance(context); - - mDb = app.getWidgetPreviewCacheDb(); - - SharedPreferences sp = context.getSharedPreferences( - LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); - final String lastVersionName = sp.getString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, null); - final String versionName = android.os.Build.VERSION.INCREMENTAL; - final boolean isLollipopOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - if (!versionName.equals(lastVersionName)) { - try { - // clear all the previews whenever the system version changes, to ensure that - // previews are up-to-date for any apps that might have been updated with the system - clearDb(); - } catch (SQLiteReadOnlyDatabaseException e) { - if (isLollipopOrGreater) { - // Workaround for Bug. 18554839, if we fail to clear the db due to the read-only - // issue, then ignore this error and leave the old previews - } else { - throw e; - } - } finally { - SharedPreferences.Editor editor = sp.edit(); - editor.putString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, versionName); - editor.commit(); - } - } + mUserManager = UserManagerCompat.getInstance(context); + mDb = new CacheDb(context); + mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); + mProfileBadgeMargin = context.getResources() + .getDimensionPixelSize(R.dimen.profile_badge_margin); } - public void recreateDb() { - LauncherAppState app = LauncherAppState.getInstance(); - app.recreateWidgetPreviewDb(); - mDb = app.getWidgetPreviewCacheDb(); + /** + * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be + * called on UI thread + * + * @param o either {@link LauncherAppWidgetProviderInfo} or {@link ResolveInfo} + * @return a request id which can be used to cancel the request. + */ + public PreviewLoadRequest getPreview(final Object o, int previewWidth, + int previewHeight, WidgetCell caller) { + String size = previewWidth + "x" + previewHeight; + WidgetCacheKey key = getObjectKey(o, size); + + PreviewLoadTask task = new PreviewLoadTask(key, o, previewWidth, previewHeight, caller); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return new PreviewLoadRequest(task); } - public void setPreviewSize(int previewWidth, int previewHeight, - PagedViewCellLayout widgetSpacingLayout) { - mPreviewBitmapWidth = previewWidth; - mPreviewBitmapHeight = previewHeight; - mSize = previewWidth + "x" + previewHeight; - mWidgetSpacingLayout = widgetSpacingLayout; - } + /** + * The DB holds the generated previews for various components. Previews can also have different + * sizes (landscape vs portrait). + */ + private static class CacheDb extends SQLiteOpenHelper { + private static final int DB_VERSION = 3; - public Bitmap getPreview(final Object o) { - final String name = getObjectName(o); - final String packageName = getObjectPackage(o); - // check if the package is valid - synchronized(sInvalidPackages) { - boolean packageValid = !sInvalidPackages.contains(packageName); - if (!packageValid) { - return null; - } - } - synchronized(mLoadedPreviews) { - // check if it exists in our existing cache - if (mLoadedPreviews.containsKey(name)) { - WeakReference bitmapReference = mLoadedPreviews.get(name); - Bitmap bitmap = bitmapReference.get(); - if (bitmap != null) { - return bitmap; - } - } - } - - Bitmap unusedBitmap = null; - synchronized(mUnusedBitmaps) { - // not in cache; we need to load it from the db - while (unusedBitmap == null && mUnusedBitmaps.size() > 0) { - Bitmap candidate = mUnusedBitmaps.remove(0).get(); - if (candidate != null && candidate.isMutable() && - candidate.getWidth() == mPreviewBitmapWidth && - candidate.getHeight() == mPreviewBitmapHeight) { - unusedBitmap = candidate; - } - } - if (unusedBitmap != null) { - final Canvas c = mCachedAppWidgetPreviewCanvas.get(); - c.setBitmap(unusedBitmap); - c.drawColor(0, PorterDuff.Mode.CLEAR); - c.setBitmap(null); - } - } - - if (unusedBitmap == null) { - unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight, - Bitmap.Config.ARGB_8888); - } - Bitmap preview = readFromDb(name, unusedBitmap); - - if (preview != null) { - synchronized(mLoadedPreviews) { - mLoadedPreviews.put(name, new WeakReference(preview)); - } - return preview; - } else { - // it's not in the db... we need to generate it - final Bitmap generatedPreview = generatePreview(o, unusedBitmap); - preview = generatedPreview; - if (preview != unusedBitmap) { - throw new RuntimeException("generatePreview is not recycling the bitmap " + o); - } - - synchronized(mLoadedPreviews) { - mLoadedPreviews.put(name, new WeakReference(preview)); - } - - // write to db on a thread pool... this can be done lazily and improves the performance - // of the first time widget previews are loaded - new AsyncTask() { - public Void doInBackground(Void ... args) { - writeToDb(o, generatedPreview); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - - return preview; - } - } - - public void recycleBitmap(Object o, Bitmap bitmapToRecycle) { - String name = getObjectName(o); - synchronized (mLoadedPreviews) { - if (mLoadedPreviews.containsKey(name)) { - Bitmap b = mLoadedPreviews.get(name).get(); - if (b == bitmapToRecycle) { - mLoadedPreviews.remove(name); - if (bitmapToRecycle.isMutable()) { - synchronized (mUnusedBitmaps) { - mUnusedBitmaps.add(new SoftReference(b)); - } - } - } else { - throw new RuntimeException("Bitmap passed in doesn't match up"); - } - } - } - } - - static class CacheDb extends SQLiteOpenHelper { - final static int DB_VERSION = 2; - final static String TABLE_NAME = "shortcut_and_widget_previews"; - final static String COLUMN_NAME = "name"; - final static String COLUMN_SIZE = "size"; - final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; - Context mContext; + private static final String TABLE_NAME = "shortcut_and_widget_previews"; + private static final String COLUMN_COMPONENT = "componentName"; + private static final String COLUMN_USER = "profileId"; + private static final String COLUMN_SIZE = "size"; + private static final String COLUMN_PACKAGE = "packageName"; + private static final String COLUMN_LAST_UPDATED = "lastUpdated"; + private static final String COLUMN_VERSION = "version"; + private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; public CacheDb(Context context) { - super(context, new File(context.getCacheDir(), - LauncherFiles.WIDGET_PREVIEWS_DB).getPath(), null, DB_VERSION); - // Store the context for later use - mContext = context; + super(context, LauncherFiles.WIDGET_PREVIEWS_DB, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase database) { database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + - COLUMN_NAME + " TEXT NOT NULL, " + + COLUMN_COMPONENT + " TEXT NOT NULL, " + + COLUMN_USER + " INTEGER NOT NULL, " + COLUMN_SIZE + " TEXT NOT NULL, " + - COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " + - "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " + + COLUMN_PACKAGE + " TEXT NOT NULL, " + + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_PREVIEW_BITMAP + " BLOB, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " + ");"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion != newVersion) { - // Delete all the records; they'll be repopulated as this is a cache - db.execSQL("DELETE FROM " + TABLE_NAME); + clearDB(db); } } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } + + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } } - private static final String WIDGET_PREFIX = "Widget:"; - private static final String SHORTCUT_PREFIX = "Shortcut:"; - - private static String getObjectName(Object o) { + private WidgetCacheKey getObjectKey(Object o, String size) { // should cache the string builder - StringBuilder sb = new StringBuilder(); - String output; - if (o instanceof AppWidgetProviderInfo) { - sb.append(WIDGET_PREFIX); - sb.append(((AppWidgetProviderInfo) o).toString()); - output = sb.toString(); - sb.setLength(0); - } else { - sb.append(SHORTCUT_PREFIX); - - ResolveInfo info = (ResolveInfo) o; - sb.append(new ComponentName(info.activityInfo.packageName, - info.activityInfo.name).flattenToString()); - output = sb.toString(); - sb.setLength(0); - } - return output; - } - - private String getObjectPackage(Object o) { - if (o instanceof AppWidgetProviderInfo) { - return ((AppWidgetProviderInfo) o).provider.getPackageName(); + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) o; + return new WidgetCacheKey(info.provider, mManager.getUser(info), size); } else { ResolveInfo info = (ResolveInfo) o; - return info.activityInfo.packageName; + return new WidgetCacheKey( + new ComponentName(info.activityInfo.packageName, info.activityInfo.name), + UserHandleCompat.myUserHandle(), size); } } - private void writeToDb(Object o, Bitmap preview) { - String name = getObjectName(o); - SQLiteDatabase db = mDb.getWritableDatabase(); + @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) { ContentValues values = new ContentValues(); + values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString()); + values.put(CacheDb.COLUMN_USER, mUserManager.getSerialNumberForUser(key.user)); + values.put(CacheDb.COLUMN_SIZE, key.size); + values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName()); + values.put(CacheDb.COLUMN_VERSION, versions[0]); + values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]); + values.put(CacheDb.COLUMN_PREVIEW_BITMAP, Utilities.flattenBitmap(preview)); - values.put(CacheDb.COLUMN_NAME, name); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - preview.compress(Bitmap.CompressFormat.PNG, 100, stream); - values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray()); - values.put(CacheDb.COLUMN_SIZE, mSize); try { - db.insert(CacheDb.TABLE_NAME, null, values); - } catch (SQLiteDiskIOException e) { - recreateDb(); - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + mDb.getWritableDatabase().insertWithOnConflict(CacheDb.TABLE_NAME, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } catch (SQLException e) { + Log.e(TAG, "Error saving image to DB", e); } } - private void clearDb() { - SQLiteDatabase db = mDb.getWritableDatabase(); - // Delete everything + public void removePackage(String packageName, UserHandleCompat user) { + removePackage(packageName, user, mUserManager.getSerialNumberForUser(user)); + } + + private void removePackage(String packageName, UserHandleCompat user, long userSerial) { + synchronized(mPackageVersions) { + mPackageVersions.remove(packageName); + } + try { - db.delete(CacheDb.TABLE_NAME, null, null); - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + mDb.getWritableDatabase().delete(CacheDb.TABLE_NAME, + CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?", + new String[] {packageName, Long.toString(userSerial)}); + } catch (SQLException e) { + Log.e(TAG, "Unable to delete items from DB", e); } } - public static void removePackageFromDb(final CacheDb cacheDb, final String packageName) { - synchronized(sInvalidPackages) { - sInvalidPackages.add(packageName); + /** + * Updates the persistent DB: + * 1. Any preview generated for an old package version is removed + * 2. Any preview for an absent package is removed + * This ensures that we remove entries for packages which changed while the launcher was dead. + */ + public void removeObsoletePreviews(ArrayList list) { + Utilities.assertWorkerThread(); + + LongSparseArray userIdCache = new LongSparseArray<>(); + LongSparseArray> validPackages = new LongSparseArray<>(); + + for (Object obj : list) { + final UserHandleCompat user; + final String pkg; + if (obj instanceof ResolveInfo) { + user = UserHandleCompat.myUserHandle(); + pkg = ((ResolveInfo) obj).activityInfo.packageName; + } else { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) obj; + user = mManager.getUser(info); + pkg = info.provider.getPackageName(); + } + + int userIdIndex = userIdCache.indexOfValue(user); + final long userId; + if (userIdIndex < 0) { + userId = mUserManager.getSerialNumberForUser(user); + userIdCache.put(userId, user); + } else { + userId = userIdCache.keyAt(userIdIndex); + } + + HashSet packages = validPackages.get(userId); + if (packages == null) { + packages = new HashSet<>(); + validPackages.put(userId, packages); + } + packages.add(pkg); } - new AsyncTask() { - public Void doInBackground(Void ... args) { - SQLiteDatabase db = cacheDb.getWritableDatabase(); + + LongSparseArray> packagesToDelete = new LongSparseArray<>(); + Cursor c = null; + try { + c = mDb.getReadableDatabase().query(CacheDb.TABLE_NAME, + new String[] {CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE, + CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION}, + null, null, null, null, null); + while (c.moveToNext()) { + long userId = c.getLong(0); + String pkg = c.getString(1); + long lastUpdated = c.getLong(2); + long version = c.getLong(3); + + HashSet packages = validPackages.get(userId); + if (packages != null && packages.contains(pkg)) { + long[] versions = getPackageVersion(pkg); + if (versions[0] == version && versions[1] == lastUpdated) { + // Every thing checks out + continue; + } + } + + // We need to delete this package. + packages = packagesToDelete.get(userId); + if (packages == null) { + packages = new HashSet<>(); + packagesToDelete.put(userId, packages); + } + packages.add(pkg); + } + + for (int i = 0; i < packagesToDelete.size(); i++) { + long userId = packagesToDelete.keyAt(i); + UserHandleCompat user = mUserManager.getUserForSerialNumber(userId); + for (String pkg : packagesToDelete.valueAt(i)) { + removePackage(pkg, user, userId); + } + } + } catch (SQLException e) { + Log.e(TAG, "Error updatating widget previews", e); + } finally { + if (c != null) { + c.close(); + } + } + } + + /** + * Reads the preview bitmap from the DB or null if the preview is not in the DB. + */ + @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) { + Cursor cursor = null; + try { + cursor = mDb.getReadableDatabase().query( + CacheDb.TABLE_NAME, + new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, + CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND " + CacheDb.COLUMN_SIZE + " = ?", + new String[] { + key.componentName.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(key.user)), + key.size + }, + null, null, null); + // If cancelled, skip getting the blob and decoding it into a bitmap + if (loadTask.isCancelled()) { + return null; + } + if (cursor.moveToNext()) { + byte[] blob = cursor.getBlob(0); + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inBitmap = recycle; try { - db.delete(CacheDb.TABLE_NAME, - CacheDb.COLUMN_NAME + " LIKE ? OR " + - CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query - new String[] { - WIDGET_PREFIX + packageName + "/%", - SHORTCUT_PREFIX + packageName + "/%" - } // args to SELECT query - ); - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; + if (!loadTask.isCancelled()) { + return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); + } + } catch (Exception e) { + return null; } - synchronized(sInvalidPackages) { - sInvalidPackages.remove(packageName); - } - return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + } catch (SQLException e) { + Log.w(TAG, "Error loading preview from DB", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; } - private static void removeItemFromDb(final CacheDb cacheDb, final String objectName) { - new AsyncTask() { - public Void doInBackground(Void ... args) { - SQLiteDatabase db = cacheDb.getWritableDatabase(); - try { - db.delete(CacheDb.TABLE_NAME, - CacheDb.COLUMN_NAME + " = ? ", // SELECT query - new String[] { objectName }); // args to SELECT query - } catch (SQLiteDiskIOException e) { - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; - } - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); - } - - private Bitmap readFromDb(String name, Bitmap b) { - if (mCachedSelectQuery == null) { - mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " + - CacheDb.COLUMN_SIZE + " = ?"; - } - SQLiteDatabase db = mDb.getReadableDatabase(); - Cursor result; - try { - result = db.query(CacheDb.TABLE_NAME, - new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return - mCachedSelectQuery, // select query - new String[] { name, mSize }, // args to select query - null, - null, - null, - null); - } catch (SQLiteDiskIOException e) { - recreateDb(); - return null; - } catch (SQLiteCantOpenDatabaseException e) { - dumpOpenFiles(); - throw e; - } - if (result.getCount() > 0) { - result.moveToFirst(); - byte[] blob = result.getBlob(0); - result.close(); - final BitmapFactory.Options opts = mCachedBitmapFactoryOptions.get(); - opts.inBitmap = b; - opts.inSampleSize = 1; - try { - return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); - } catch (IllegalArgumentException e) { - removeItemFromDb(mDb, name); - return null; - } + @Thunk Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle, + int previewWidth, int previewHeight) { + if (info instanceof LauncherAppWidgetProviderInfo) { + return generateWidgetPreview(launcher, (LauncherAppWidgetProviderInfo) info, + previewWidth, recycle, null); } else { - result.close(); - return null; + return generateShortcutPreview(launcher, + (ResolveInfo) info, previewWidth, previewHeight, recycle); } } - private Bitmap generatePreview(Object info, Bitmap preview) { - if (preview != null && - (preview.getWidth() != mPreviewBitmapWidth || - preview.getHeight() != mPreviewBitmapHeight)) { - throw new RuntimeException("Improperly sized bitmap passed as argument"); - } - if (info instanceof AppWidgetProviderInfo) { - return generateWidgetPreview((AppWidgetProviderInfo) info, preview); - } else { - return generateShortcutPreview( - (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview); - } - } - - public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, Bitmap preview) { - int[] cellSpans = Launcher.getSpanForWidget(mContext, info); - int maxWidth = maxWidthForWidgetPreview(cellSpans[0]); - int maxHeight = maxHeightForWidgetPreview(cellSpans[1]); - return generateWidgetPreview(info, cellSpans[0], cellSpans[1], - maxWidth, maxHeight, preview, null); - } - - public int maxWidthForWidgetPreview(int spanX) { - return Math.min(mPreviewBitmapWidth, - mWidgetSpacingLayout.estimateCellWidth(spanX)); - } - - public int maxHeightForWidgetPreview(int spanY) { - return Math.min(mPreviewBitmapHeight, - mWidgetSpacingLayout.estimateCellHeight(spanY)); - } - - public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, int cellHSpan, int cellVSpan, - int maxPreviewWidth, int maxPreviewHeight, Bitmap preview, int[] preScaledWidthOut) { + public Bitmap generateWidgetPreview(Launcher launcher, LauncherAppWidgetProviderInfo info, + int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) { // Load the preview image if possible if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; - if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; Drawable drawable = null; if (info.previewImage != 0) { @@ -533,61 +360,23 @@ public class WidgetPreviewLoader { } } + final boolean widgetPreviewExists = (drawable != null); + final int spanX = info.getSpanX(launcher) < 1 ? 1 : info.getSpanX(launcher); + final int spanY = info.getSpanY(launcher) < 1 ? 1 : info.getSpanY(launcher); + int previewWidth; int previewHeight; - Bitmap defaultPreview = null; - boolean widgetPreviewExists = (drawable != null); + Bitmap tileBitmap = null; + if (widgetPreviewExists) { previewWidth = drawable.getIntrinsicWidth(); previewHeight = drawable.getIntrinsicHeight(); } else { // Generate a preview image if we couldn't load one - if (cellHSpan < 1) cellHSpan = 1; - if (cellVSpan < 1) cellVSpan = 1; - - // This Drawable is not directly drawn, so there's no need to mutate it. - BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() - .getDrawable(R.drawable.widget_tile); - final int previewDrawableWidth = previewDrawable - .getIntrinsicWidth(); - final int previewDrawableHeight = previewDrawable - .getIntrinsicHeight(); - previewWidth = previewDrawableWidth * cellHSpan; - previewHeight = previewDrawableHeight * cellVSpan; - - defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); - final Canvas c = mCachedAppWidgetPreviewCanvas.get(); - c.setBitmap(defaultPreview); - Paint p = mDefaultAppWidgetPreviewPaint.get(); - if (p == null) { - p = new Paint(); - p.setShader(new BitmapShader(previewDrawable.getBitmap(), - Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); - mDefaultAppWidgetPreviewPaint.set(p); - } - final Rect dest = mCachedAppWidgetPreviewDestRect.get(); - dest.set(0, 0, previewWidth, previewHeight); - c.drawRect(dest, p); - c.setBitmap(null); - - // Draw the icon in the top left corner - int minOffset = (int) (mAppIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); - int smallestSide = Math.min(previewWidth, previewHeight); - float iconScale = Math.min((float) smallestSide - / (mAppIconSize + 2 * minOffset), 1f); - - try { - Drawable icon = mManager.loadIcon(info, mIconCache); - if (icon != null) { - int hoffset = (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); - int yoffset = (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); - icon = mutateOnMainThread(icon); - renderDrawableToBitmap(icon, defaultPreview, hoffset, - yoffset, (int) (mAppIconSize * iconScale), - (int) (mAppIconSize * iconScale)); - } - } catch (Resources.NotFoundException e) { - } + tileBitmap = ((BitmapDrawable) mContext.getResources().getDrawable( + R.drawable.widget_tile)).getBitmap(); + previewWidth = tileBitmap.getWidth() * spanX; + previewHeight = tileBitmap.getHeight() * spanY; } // Scale to fit width only - let the widget preview be clipped in the @@ -605,102 +394,110 @@ public class WidgetPreviewLoader { } // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size + final Canvas c = new Canvas(); if (preview == null) { preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); + c.setBitmap(preview); + } else { + // Reusing bitmap. Clear it. + c.setBitmap(preview); + c.drawColor(0, PorterDuff.Mode.CLEAR); } // Draw the scaled preview into the final bitmap - int x = (preview.getWidth() - previewWidth) / 2; + int x = (preview.getWidth() - previewWidth - mProfileBadgeMargin) / 2; if (widgetPreviewExists) { - renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, - previewHeight); + drawable.setBounds(x, 0, x + previewWidth, previewHeight); + drawable.draw(c); } else { - final Canvas c = mCachedAppWidgetPreviewCanvas.get(); - final Rect src = mCachedAppWidgetPreviewSrcRect.get(); - final Rect dest = mCachedAppWidgetPreviewDestRect.get(); - c.setBitmap(preview); - src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); - dest.set(x, 0, x + previewWidth, previewHeight); + final Paint p = new Paint(); + p.setFilterBitmap(true); + int appIconSize = launcher.getDeviceProfile().iconSizePx; - Paint p = mCachedAppWidgetPreviewPaint.get(); - if (p == null) { - p = new Paint(); - p.setFilterBitmap(true); - mCachedAppWidgetPreviewPaint.set(p); + // draw the spanX x spanY tiles + final Rect src = new Rect(0, 0, tileBitmap.getWidth(), tileBitmap.getHeight()); + + float tileW = scale * tileBitmap.getWidth(); + float tileH = scale * tileBitmap.getHeight(); + final RectF dst = new RectF(0, 0, tileW, tileH); + + float tx = x; + for (int i = 0; i < spanX; i++, tx += tileW) { + float ty = 0; + for (int j = 0; j < spanY; j++, ty += tileH) { + dst.offsetTo(tx, ty); + c.drawBitmap(tileBitmap, src, dst, p); + } } - c.drawBitmap(defaultPreview, src, dest, p); + + // Draw the icon in the top left corner + // TODO: use top right for RTL + int minOffset = (int) (appIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE); + int smallestSide = Math.min(previewWidth, previewHeight); + float iconScale = Math.min((float) smallestSide / (appIconSize + 2 * minOffset), scale); + + try { + Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache)); + if (icon != null) { + int hoffset = (int) ((tileW - appIconSize * iconScale) / 2) + x; + int yoffset = (int) ((tileH - appIconSize * iconScale) / 2); + icon.setBounds(hoffset, yoffset, + hoffset + (int) (appIconSize * iconScale), + yoffset + (int) (appIconSize * iconScale)); + icon.draw(c); + } + } catch (Resources.NotFoundException e) { } c.setBitmap(null); } - return mManager.getBadgeBitmap(info, preview); + int imageHeight = Math.min(preview.getHeight(), previewHeight + mProfileBadgeMargin); + return mManager.getBadgeBitmap(info, preview, imageHeight); } private Bitmap generateShortcutPreview( - ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { - Bitmap tempBitmap = mCachedShortcutPreviewBitmap.get(); - final Canvas c = mCachedShortcutPreviewCanvas.get(); - if (tempBitmap == null || - tempBitmap.getWidth() != maxWidth || - tempBitmap.getHeight() != maxHeight) { - tempBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); - mCachedShortcutPreviewBitmap.set(tempBitmap); - } else { - c.setBitmap(tempBitmap); - c.drawColor(0, PorterDuff.Mode.CLEAR); - c.setBitmap(null); - } - // Render the icon - Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo)); - - int paddingTop = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); - int paddingLeft = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); - int paddingRight = mContext. - getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); - - int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); - - renderDrawableToBitmap( - icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth); - - if (preview != null && - (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight)) { - throw new RuntimeException("Improperly sized bitmap passed as argument"); - } else if (preview == null) { + Launcher launcher, ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { + final Canvas c = new Canvas(); + if (preview == null) { preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); + c.setBitmap(preview); + } else if (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight) { + throw new RuntimeException("Improperly sized bitmap passed as argument"); + } else { + // Reusing bitmap. Clear it. + c.setBitmap(preview); + c.drawColor(0, PorterDuff.Mode.CLEAR); } - c.setBitmap(preview); + Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo)); + icon.setFilterBitmap(true); + // Draw a desaturated/scaled version of the icon in the background as a watermark - Paint p = mCachedShortcutPreviewPaint.get(); - if (p == null) { - p = new Paint(); - ColorMatrix colorMatrix = new ColorMatrix(); - colorMatrix.setSaturation(0); - p.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); - p.setAlpha((int) (255 * 0.06f)); - mCachedShortcutPreviewPaint.set(p); - } - c.drawBitmap(tempBitmap, 0, 0, p); + ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.setSaturation(0); + icon.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); + icon.setAlpha((int) (255 * 0.06f)); + + Resources res = mContext.getResources(); + int paddingTop = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); + int paddingLeft = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); + int paddingRight = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); + int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); + icon.setBounds(paddingLeft, paddingTop, + paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth); + icon.draw(c); + + // Draw the final icon at top left corner. + // TODO: use top right for RTL + int appIconSize = launcher.getDeviceProfile().iconSizePx; + + icon.setAlpha(255); + icon.setColorFilter(null); + icon.setBounds(0, 0, appIconSize, appIconSize); + icon.draw(c); + c.setBitmap(null); - - renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize); - return preview; } - private static void renderDrawableToBitmap( - Drawable d, Bitmap bitmap, int x, int y, int w, int h) { - if (bitmap != null) { - Canvas c = new Canvas(bitmap); - Rect oldBounds = d.copyBounds(); - d.setBounds(x, y, x + w, y + h); - d.draw(c); - d.setBounds(oldBounds); // Restore the bounds - c.setBitmap(null); - } - } - private Drawable mutateOnMainThread(final Drawable drawable) { try { return mMainThreadExecutor.submit(new Callable() { @@ -717,82 +514,195 @@ public class WidgetPreviewLoader { } } - private static final int MAX_OPEN_FILES = 1024; - private static final int SAMPLE_RATE = 23; /** - * Dumps all files that are open in this process without allocating a file descriptor. + * @return an array of containing versionCode and lastUpdatedTime for the package. */ - private static void dumpOpenFiles() { - try { - Log.i(TAG, "DUMP OF OPEN FILES (sample rate: 1 every " + SAMPLE_RATE + "):"); - final String TYPE_APK = "apk"; - final String TYPE_JAR = "jar"; - final String TYPE_PIPE = "pipe"; - final String TYPE_SOCKET = "socket"; - final String TYPE_DB = "db"; - final String TYPE_ANON_INODE = "anon_inode"; - final String TYPE_DEV = "dev"; - final String TYPE_NON_FS = "non-fs"; - final String TYPE_OTHER = "other"; - List types = Arrays.asList(TYPE_APK, TYPE_JAR, TYPE_PIPE, TYPE_SOCKET, TYPE_DB, - TYPE_ANON_INODE, TYPE_DEV, TYPE_NON_FS, TYPE_OTHER); - int[] count = new int[types.size()]; - int[] duplicates = new int[types.size()]; - HashSet files = new HashSet(); - int total = 0; - for (int i = 0; i < MAX_OPEN_FILES; i++) { - // This is a gigantic hack but unfortunately the only way to resolve an fd - // to a file name. Note that we have to loop over all possible fds because - // reading the directory would require allocating a new fd. The kernel is - // currently implemented such that no fd is larger then the current rlimit, - // which is why it's safe to loop over them in such a way. - String fd = "/proc/self/fd/" + i; + @Thunk long[] getPackageVersion(String packageName) { + synchronized (mPackageVersions) { + long[] versions = mPackageVersions.get(packageName); + if (versions == null) { + versions = new long[2]; try { - // getCanonicalPath() uses readlink behind the scene which doesn't require - // a file descriptor. - String resolved = new File(fd).getCanonicalPath(); - int type = types.indexOf(TYPE_OTHER); - if (resolved.startsWith("/dev/")) { - type = types.indexOf(TYPE_DEV); - } else if (resolved.endsWith(".apk")) { - type = types.indexOf(TYPE_APK); - } else if (resolved.endsWith(".jar")) { - type = types.indexOf(TYPE_JAR); - } else if (resolved.contains("/fd/pipe:")) { - type = types.indexOf(TYPE_PIPE); - } else if (resolved.contains("/fd/socket:")) { - type = types.indexOf(TYPE_SOCKET); - } else if (resolved.contains("/fd/anon_inode:")) { - type = types.indexOf(TYPE_ANON_INODE); - } else if (resolved.endsWith(".db") || resolved.contains("/databases/")) { - type = types.indexOf(TYPE_DB); - } else if (resolved.startsWith("/proc/") && resolved.contains("/fd/")) { - // Those are the files that don't point anywhere on the file system. - // getCanonicalPath() wrongly interprets these as relative symlinks and - // resolves them within /proc//fd/. - type = types.indexOf(TYPE_NON_FS); + PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0); + versions[0] = info.versionCode; + versions[1] = info.lastUpdateTime; + } catch (NameNotFoundException e) { + Log.e(TAG, "PackageInfo not found", e); + } + mPackageVersions.put(packageName, versions); + } + return versions; + } + } + + /** + * A request Id which can be used by the client to cancel any request. + */ + public class PreviewLoadRequest { + + @Thunk final PreviewLoadTask mTask; + + public PreviewLoadRequest(PreviewLoadTask task) { + mTask = task; + } + + public void cleanup() { + if (mTask != null) { + mTask.cancel(true); + } + + // This only handles the case where the PreviewLoadTask is cancelled after the task has + // successfully completed (including having written to disk when necessary). In the + // other cases where it is cancelled while the task is running, it will be cleaned up + // in the tasks's onCancelled() call, and if cancelled while the task is writing to + // disk, it will be cancelled in the task's onPostExecute() call. + if (mTask.mBitmapToRecycle != null) { + mWorkerHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mUnusedBitmaps) { + mUnusedBitmaps.add(mTask.mBitmapToRecycle); + } + mTask.mBitmapToRecycle = null; } - count[type]++; - total++; - if (files.contains(resolved)) { - duplicates[type]++; + }); + } + } + } + + public class PreviewLoadTask extends AsyncTask { + @Thunk final WidgetCacheKey mKey; + private final Object mInfo; + private final int mPreviewHeight; + private final int mPreviewWidth; + private final WidgetCell mCaller; + @Thunk long[] mVersions; + @Thunk Bitmap mBitmapToRecycle; + + PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth, + int previewHeight, WidgetCell caller) { + mKey = key; + mInfo = info; + mPreviewHeight = previewHeight; + mPreviewWidth = previewWidth; + mCaller = caller; + if (DEBUG) { + Log.d(TAG, String.format("%s, %s, %d, %d", + mKey, mInfo, mPreviewHeight, mPreviewWidth)); + } + } + + @Override + protected Bitmap doInBackground(Void... params) { + Bitmap unusedBitmap = null; + + // If already cancelled before this gets to run in the background, then return early + if (isCancelled()) { + return null; + } + synchronized (mUnusedBitmaps) { + // Check if we can re-use a bitmap + for (Bitmap candidate : mUnusedBitmaps) { + if (candidate != null && candidate.isMutable() && + candidate.getWidth() == mPreviewWidth && + candidate.getHeight() == mPreviewHeight) { + unusedBitmap = candidate; + mUnusedBitmaps.remove(unusedBitmap); + break; } - files.add(resolved); - if (total % SAMPLE_RATE == 0) { - Log.i(TAG, " fd " + i + ": " + resolved - + " (" + types.get(type) + ")"); - } - } catch (IOException e) { - // Ignoring exceptions for non-existing file descriptors. } } - for (int i = 0; i < types.size(); i++) { - Log.i(TAG, String.format("Open %10s files: %4d total, %4d duplicates", - types.get(i), count[i], duplicates[i])); + + // creating a bitmap is expensive. Do not do this inside synchronized block. + if (unusedBitmap == null) { + unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888); } - } catch (Throwable t) { - // Catch everything. This is called from an exception handler that we shouldn't upset. - Log.e(TAG, "Unable to log open files.", t); + // If cancelled now, don't bother reading the preview from the DB + if (isCancelled()) { + return unusedBitmap; + } + Bitmap preview = readFromDb(mKey, unusedBitmap, this); + // Only consider generating the preview if we have not cancelled the task already + if (!isCancelled() && preview == null) { + // Fetch the version info before we generate the preview, so that, in-case the + // app was updated while we are generating the preview, we use the old version info, + // which would gets re-written next time. + mVersions = getPackageVersion(mKey.componentName.getPackageName()); + + Launcher launcher = (Launcher) mCaller.getContext(); + + // it's not in the db... we need to generate it + preview = generatePreview(launcher, mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight); + } + return preview; + } + + @Override + protected void onPostExecute(final Bitmap preview) { + mCaller.applyPreview(preview); + + // Write the generated preview to the DB in the worker thread + if (mVersions != null) { + mWorkerHandler.post(new Runnable() { + @Override + public void run() { + if (!isCancelled()) { + // If we are still using this preview, then write it to the DB and then + // let the normal clear mechanism recycle the bitmap + writeToDb(mKey, mVersions, preview); + mBitmapToRecycle = preview; + } else { + // If we've already cancelled, then skip writing the bitmap to the DB + // and manually add the bitmap back to the recycled set + synchronized (mUnusedBitmaps) { + mUnusedBitmaps.add(preview); + } + } + } + }); + } else { + // If we don't need to write to disk, then ensure the preview gets recycled by + // the normal clear mechanism + mBitmapToRecycle = preview; + } + } + + @Override + protected void onCancelled(final Bitmap preview) { + // If we've cancelled while the task is running, then can return the bitmap to the + // recycled set immediately. Otherwise, it will be recycled after the preview is written + // to disk. + if (preview != null) { + mWorkerHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mUnusedBitmaps) { + mUnusedBitmaps.add(preview); + } + } + }); + } + } + } + + private static final class WidgetCacheKey extends ComponentKey { + + // TODO: remove dependency on size + @Thunk final String size; + + public WidgetCacheKey(ComponentName componentName, UserHandleCompat user, String size) { + super(componentName, user); + this.size = size; + } + + @Override + public int hashCode() { + return super.hashCode() ^ size.hashCode(); + } + + @Override + public boolean equals(Object o) { + return super.equals(o) && ((WidgetCacheKey) o).size.equals(size); } } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 8bd7991988..4a6b90afe0 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -17,24 +17,18 @@ package com.android.launcher3; import android.animation.Animator; -import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.LayoutTransition; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.WallpaperManager; import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -46,12 +40,12 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region.Op; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Parcelable; -import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -69,16 +63,20 @@ import com.android.launcher3.FolderIcon.FolderRingAnimator; import com.android.launcher3.Launcher.CustomContentCallbacks; import com.android.launcher3.Launcher.LauncherOverlay; import com.android.launcher3.LauncherSettings.Favorites; -import com.android.launcher3.compat.PackageInstallerCompat; -import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; +import com.android.launcher3.UninstallDropTarget.UninstallSource; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource; +import com.android.launcher3.accessibility.OverviewScreenAccessibilityDelegate; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.Thunk; +import com.android.launcher3.util.WallpaperUtils; +import com.android.launcher3.widget.PendingAddShortcutInfo; +import com.android.launcher3.widget.PendingAddWidgetInfo; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** @@ -86,63 +84,45 @@ import java.util.concurrent.atomic.AtomicInteger; * Each page contains a number of icons, folders or widgets the user can * interact with. A workspace is meant to be used with a fixed width only. */ -public class Workspace extends SmoothPagedView +public class Workspace extends PagedView implements DropTarget, DragSource, DragScroller, View.OnTouchListener, DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener, - Insettable { + Insettable, UninstallSource, AccessibilityDragSource, Stats.LaunchSourceProvider { private static final String TAG = "Launcher.Workspace"; - // Y rotation to apply to the workspace screens - private static final float WORKSPACE_OVERSCROLL_ROTATION = 24f; - - private static final int CHILDREN_OUTLINE_FADE_OUT_DELAY = 0; - private static final int CHILDREN_OUTLINE_FADE_OUT_DURATION = 375; - private static final int CHILDREN_OUTLINE_FADE_IN_DURATION = 100; + private static boolean ENFORCE_DRAG_EVENT_ORDER = false; protected static final int SNAP_OFF_EMPTY_SCREEN_DURATION = 400; protected static final int FADE_EMPTY_SCREEN_DURATION = 150; - private static final int BACKGROUND_FADE_OUT_DURATION = 350; private static final int ADJACENT_SCREEN_DROP_DURATION = 300; - private static final int FLING_THRESHOLD_VELOCITY = 500; - - private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f; static final boolean MAP_NO_RECURSE = false; static final boolean MAP_RECURSE = true; - // These animators are used to fade the children's outlines - private ObjectAnimator mChildrenOutlineFadeInAnimation; - private ObjectAnimator mChildrenOutlineFadeOutAnimation; - private float mChildrenOutlineAlpha = 0; - - // These properties refer to the background protection gradient used for AllApps and Customize - private ValueAnimator mBackgroundFadeInAnimation; - private ValueAnimator mBackgroundFadeOutAnimation; - private static final long CUSTOM_CONTENT_GESTURE_DELAY = 200; private long mTouchDownTime = -1; private long mCustomContentShowTime = -1; private LayoutTransition mLayoutTransition; - private final WallpaperManager mWallpaperManager; - private IBinder mWindowToken; + @Thunk final WallpaperManager mWallpaperManager; + @Thunk IBinder mWindowToken; private int mOriginalDefaultPage; private int mDefaultPage; private ShortcutAndWidgetContainer mDragSourceInternal; - private static boolean sAccessibilityEnabled; // The screen id used for the empty screen always present to the right. final static long EXTRA_EMPTY_SCREEN_ID = -201; private final static long CUSTOM_CONTENT_SCREEN_ID = -301; - private HashMap mWorkspaceScreens = new HashMap(); - private ArrayList mScreenOrder = new ArrayList(); + @Thunk LongArrayMap mWorkspaceScreens = new LongArrayMap<>(); + @Thunk ArrayList mScreenOrder = new ArrayList(); - private Runnable mRemoveEmptyScreenRunnable; - private boolean mDeferRemoveExtraEmptyScreen = false; + @Thunk Runnable mRemoveEmptyScreenRunnable; + @Thunk boolean mDeferRemoveExtraEmptyScreen = false; + @Thunk boolean mAddNewPageOnDrag = true; /** * CellInfo for the cell that is currently being dragged @@ -152,7 +132,7 @@ public class Workspace extends SmoothPagedView /** * Target drop area calculated during last acceptDrop call. */ - private int[] mTargetCell = new int[2]; + @Thunk int[] mTargetCell = new int[2]; private int mDragOverX = -1; private int mDragOverY = -1; @@ -167,7 +147,7 @@ public class Workspace extends SmoothPagedView /** * The CellLayout that is currently being dragged over */ - private CellLayout mDragTargetLayout = null; + @Thunk CellLayout mDragTargetLayout = null; /** * The CellLayout that we will show as glowing */ @@ -178,16 +158,16 @@ public class Workspace extends SmoothPagedView */ private CellLayout mDropToLayout = null; - private Launcher mLauncher; - private IconCache mIconCache; - private DragController mDragController; + @Thunk Launcher mLauncher; + @Thunk IconCache mIconCache; + @Thunk DragController mDragController; // These are temporary variables to prevent having to allocate a new object just to // return an (x, y) value from helper functions. Do NOT use them to maintain other state. private int[] mTempCell = new int[2]; private int[] mTempPt = new int[2]; private int[] mTempEstimate = new int[2]; - private float[] mDragViewVisualCenter = new float[2]; + @Thunk float[] mDragViewVisualCenter = new float[2]; private float[] mTempCellLayoutCenterCoordinates = new float[2]; private Matrix mTempInverseMatrix = new Matrix(); @@ -212,34 +192,31 @@ public class Workspace extends SmoothPagedView private boolean mInScrollArea = false; private HolographicOutlineHelper mOutlineHelper; - private Bitmap mDragOutline = null; + @Thunk Bitmap mDragOutline = null; private static final Rect sTempRect = new Rect(); private final int[] mTempXY = new int[2]; private int[] mTempVisiblePagesRange = new int[2]; - private boolean mOverscrollEffectSet; public static final int DRAG_BITMAP_PADDING = 2; private boolean mWorkspaceFadeInAdjacentScreens; WallpaperOffsetInterpolator mWallpaperOffset; - private boolean mWallpaperIsLiveWallpaper; - private int mNumPagesForWallpaperParallax; - private float mLastSetWallpaperOffsetSteps = 0; + @Thunk boolean mWallpaperIsLiveWallpaper; + @Thunk int mNumPagesForWallpaperParallax; + @Thunk float mLastSetWallpaperOffsetSteps = 0; - private Runnable mDelayedResizeRunnable; + @Thunk Runnable mDelayedResizeRunnable; private Runnable mDelayedSnapToPageRunnable; private Point mDisplaySize = new Point(); - private int mCameraDistance; // Variables relating to the creation of user folders by hovering shortcuts over shortcuts private static final int FOLDER_CREATION_TIMEOUT = 0; public static final int REORDER_TIMEOUT = 350; private final Alarm mFolderCreationAlarm = new Alarm(); private final Alarm mReorderAlarm = new Alarm(); - private FolderRingAnimator mDragFolderRingAnimator = null; + @Thunk FolderRingAnimator mDragFolderRingAnimator = null; private FolderIcon mDragOverFolderIcon = null; private boolean mCreateUserFolderOnDrop = false; private boolean mAddToExistingFolderOnDrop = false; - private DropTarget.DragEnforcer mDragEnforcer; private float mMaxDistanceForFolderCreation; private final Canvas mCanvas = new Canvas(); @@ -264,30 +241,16 @@ public class Workspace extends SmoothPagedView private static final int DRAG_MODE_ADD_TO_FOLDER = 2; private static final int DRAG_MODE_REORDER = 3; private int mDragMode = DRAG_MODE_NONE; - private int mLastReorderX = -1; - private int mLastReorderY = -1; + @Thunk int mLastReorderX = -1; + @Thunk int mLastReorderY = -1; private SparseArray mSavedStates; private final ArrayList mRestoredPages = new ArrayList(); - // These variables are used for storing the initial and final values during workspace animations - private int mSavedScrollX; - private float mSavedRotationY; - private float mSavedTranslationX; - private float mCurrentScale; - private float mNewScale; - private float[] mOldBackgroundAlphas; - private float[] mOldAlphas; - private float[] mNewBackgroundAlphas; - private float[] mNewAlphas; - private int mLastChildCount = -1; private float mTransitionProgress; - private Animator mStateAnimator = null; - float mOverScrollEffect = 0f; - - private Runnable mDeferredAction; + @Thunk Runnable mDeferredAction; private boolean mDeferDropAfterUninstall; private boolean mUninstallSuccessful; @@ -298,6 +261,11 @@ public class Workspace extends SmoothPagedView boolean mShouldSendPageSettled; int mLastOverlaySroll = 0; + // Handles workspace state transitions + private WorkspaceStateTransitionAnimation mStateTransitionAnimation; + + private AccessibilityDelegate mPagesAccessibilityDelegate; + private final Runnable mBindPages = new Runnable() { @Override public void run() { @@ -324,29 +292,22 @@ public class Workspace extends SmoothPagedView */ public Workspace(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mContentIsRefreshable = false; mOutlineHelper = HolographicOutlineHelper.obtain(context); - mDragEnforcer = new DropTarget.DragEnforcer(context); - // With workspace, data is available straight from the get-go - setDataIsReady(); - mLauncher = (Launcher) context; + mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this); final Resources res = getResources(); - mWorkspaceFadeInAdjacentScreens = LauncherAppState.getInstance().getDynamicGrid(). - getDeviceProfile().shouldFadeAdjacentWorkspaceScreens(); + DeviceProfile grid = mLauncher.getDeviceProfile(); + mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens(); mFadeInAdjacentScreens = false; mWallpaperManager = WallpaperManager.getInstance(context); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0); mSpringLoadedShrinkFactor = res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; - mOverviewModeShrinkFactor = grid.getOverviewModeScale(); - mCameraDistance = res.getInteger(R.integer.config_cameraDistance); + mOverviewModeShrinkFactor = grid.getOverviewModeScale(mIsRtl); mOriginalDefaultPage = mDefaultPage = a.getInt(R.styleable.Workspace_defaultScreen, 1); a.recycle(); @@ -357,7 +318,6 @@ public class Workspace extends SmoothPagedView // Disable multitouch across the workspace/all apps/customize tray setMotionEventSplittingEnabled(true); - setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } @Override @@ -375,13 +335,12 @@ public class Workspace extends SmoothPagedView // estimate the size of a widget with spans hSpan, vSpan. return MAX_VALUE for each // dimension if unsuccessful - public int[] estimateItemSize(int hSpan, int vSpan, - ItemInfo itemInfo, boolean springLoaded) { + public int[] estimateItemSize(ItemInfo itemInfo, boolean springLoaded) { int[] size = new int[2]; if (getChildCount() > 0) { // Use the first non-custom page to estimate the child position CellLayout cl = (CellLayout) getChildAt(numCustomPages()); - Rect r = estimateItemPosition(cl, itemInfo, 0, 0, hSpan, vSpan); + Rect r = estimateItemPosition(cl, itemInfo, 0, 0, itemInfo.spanX, itemInfo.spanY); size[0] = r.width(); size[1] = r.height(); if (springLoaded) { @@ -403,32 +362,39 @@ public class Workspace extends SmoothPagedView return r; } + @Override public void onDragStart(final DragSource source, Object info, int dragAction) { + if (ENFORCE_DRAG_EVENT_ORDER) { + enfoceDragParity("onDragStart", 0, 0); + } + mIsDragOccuring = true; updateChildrenLayersEnabled(false); mLauncher.lockScreenOrientation(); mLauncher.onInteractionBegin(); - setChildrenBackgroundAlphaMultipliers(1f); // Prevent any Un/InstallShortcutReceivers from updating the db while we are dragging InstallShortcutReceiver.enableInstallQueue(); - UninstallShortcutReceiver.enableUninstallQueue(); - post(new Runnable() { - @Override - public void run() { - if (mIsDragOccuring) { - mDeferRemoveExtraEmptyScreen = false; - addExtraEmptyScreenOnDrag(); - } - } - }); + + if (mAddNewPageOnDrag) { + mDeferRemoveExtraEmptyScreen = false; + addExtraEmptyScreenOnDrag(); + } } + public void setAddNewPageOnDrag(boolean addPage) { + mAddNewPageOnDrag = addPage; + } public void deferRemoveExtraEmptyScreen() { mDeferRemoveExtraEmptyScreen = true; } + @Override public void onDragEnd() { + if (ENFORCE_DRAG_EVENT_ORDER) { + enfoceDragParity("onDragEnd", 0, 0); + } + if (!mDeferRemoveExtraEmptyScreen) { removeExtraEmptyScreen(true, mDragSourceInternal != null); } @@ -439,7 +405,6 @@ public class Workspace extends SmoothPagedView // Re-enable any Un/InstallShortcutReceiver and now process any queued items InstallShortcutReceiver.disableAndFlushInstallQueue(getContext()); - UninstallShortcutReceiver.disableAndFlushUninstallQueue(getContext()); mDragSourceInternal = null; mLauncher.onInteractionEnd(); @@ -450,9 +415,8 @@ public class Workspace extends SmoothPagedView */ protected void initWorkspace() { mCurrentPage = mDefaultPage; - Launcher.setScreen(mCurrentPage); LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); mIconCache = app.getIconCache(); setWillNotDraw(false); setClipChildren(false); @@ -467,10 +431,11 @@ public class Workspace extends SmoothPagedView display.getSize(mDisplaySize); mMaxDistanceForFolderCreation = (0.55f * grid.iconSizePx); - mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); // Set the wallpaper dimensions when Launcher starts up setWallpaperDimension(); + + setEdgeGlowColor(getResources().getColor(R.color.workspace_edge_effect_color)); } private void setupLayoutTransition() { @@ -490,11 +455,6 @@ public class Workspace extends SmoothPagedView setLayoutTransition(null); } - @Override - protected int getScrollMode() { - return SmoothPagedView.X_LARGE_MODE; - } - @Override public void onChildViewAdded(View parent, View child) { if (!(child instanceof CellLayout)) { @@ -503,7 +463,7 @@ public class Workspace extends SmoothPagedView CellLayout cl = ((CellLayout) child); cl.setOnInterceptTouchListener(this); cl.setClickable(true); - cl.setImportantForAccessibility(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO); + cl.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); super.onChildViewAdded(parent, child); } @@ -518,7 +478,7 @@ public class Workspace extends SmoothPagedView /** * @return The open folder on the current screen, or null if there is none */ - Folder getOpenFolder() { + public Folder getOpenFolder() { DragLayer dragLayer = mLauncher.getDragLayer(); int count = dragLayer.getChildCount(); for (int i = 0; i < count; i++) { @@ -572,16 +532,14 @@ public class Workspace extends SmoothPagedView } public long insertNewWorkspaceScreen(long screenId, int insertIndex) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - insertNewWorkspaceScreen(): " + screenId + - " at index: " + insertIndex, true); - if (mWorkspaceScreens.containsKey(screenId)) { throw new RuntimeException("Screen id " + screenId + " already exists!"); } - CellLayout newScreen = (CellLayout) - mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null); + // Inflate the cell layout, but do not add it automatically so that we can get the newly + // created CellLayout. + CellLayout newScreen = (CellLayout) mLauncher.getLayoutInflater().inflate( + R.layout.workspace_screen, this, false /* attachToRoot */); newScreen.setOnLongClickListener(mLongClickListener); newScreen.setOnClickListener(mLauncher); @@ -589,13 +547,18 @@ public class Workspace extends SmoothPagedView mWorkspaceScreens.put(screenId, newScreen); mScreenOrder.add(insertIndex, screenId); addView(newScreen, insertIndex); + + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + if (delegate != null && delegate.isInAccessibleDrag()) { + newScreen.enableAccessibleDrag(true, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG); + } return screenId; } public void createCustomContentContainer() { CellLayout customScreen = (CellLayout) - mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null); - customScreen.disableBackground(); + mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, this, false); customScreen.disableDragTarget(); mWorkspaceScreens.put(CUSTOM_CONTENT_SCREEN_ID, customScreen); @@ -675,9 +638,6 @@ public class Workspace extends SmoothPagedView } public void addExtraEmptyScreenOnDrag() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - addExtraEmptyScreenOnDrag()", true); - boolean lastChildOnScreen = false; boolean childOnFinalScreen = false; @@ -704,9 +664,6 @@ public class Workspace extends SmoothPagedView } public boolean addExtraEmptyScreen() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - addExtraEmptyScreen()", true); - if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) { insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID); return true; @@ -715,9 +672,6 @@ public class Workspace extends SmoothPagedView } private void convertFinalScreenToEmptyScreenIfNecessary() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - convertFinalScreenToEmptyScreenIfNecessary()", true); - if (mLauncher.isWorkspaceLoading()) { // Invalid and dangerous operation if workspace is loading Launcher.addDumpLog(TAG, " - workspace loading, skip", true); @@ -742,7 +696,6 @@ public class Workspace extends SmoothPagedView // Update the model if we have changed any screens mLauncher.getModel().updateWorkspaceScreenOrder(mLauncher, mScreenOrder); - Launcher.addDumpLog(TAG, "11683562 - extra empty screen: " + finalScreenId, true); } } @@ -752,8 +705,6 @@ public class Workspace extends SmoothPagedView public void removeExtraEmptyScreenDelayed(final boolean animate, final Runnable onComplete, final int delay, final boolean stripEmptyScreens) { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - removeExtraEmptyScreen()", true); if (mLauncher.isWorkspaceLoading()) { // Don't strip empty screens if the workspace is still loading Launcher.addDumpLog(TAG, " - workspace loading, skip", true); @@ -778,6 +729,7 @@ public class Workspace extends SmoothPagedView fadeAndRemoveEmptyScreen(SNAP_OFF_EMPTY_SCREEN_DURATION, FADE_EMPTY_SCREEN_DURATION, onComplete, stripEmptyScreens); } else { + snapToPage(getNextPage(), 0); fadeAndRemoveEmptyScreen(0, FADE_EMPTY_SCREEN_DURATION, onComplete, stripEmptyScreens); } @@ -795,9 +747,7 @@ public class Workspace extends SmoothPagedView private void fadeAndRemoveEmptyScreen(int delay, int duration, final Runnable onComplete, final boolean stripEmptyScreens) { - // Log to disk // XXX: Do we need to update LM workspace screens below? - Launcher.addDumpLog(TAG, "11683562 - fadeAndRemoveEmptyScreen()", true); PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f); PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", 0f); @@ -841,8 +791,6 @@ public class Workspace extends SmoothPagedView } public long commitExtraEmptyScreen() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - commitExtraEmptyScreen()", true); if (mLauncher.isWorkspaceLoading()) { // Invalid and dangerous operation if workspace is loading Launcher.addDumpLog(TAG, " - workspace loading, skip", true); @@ -875,12 +823,9 @@ public class Workspace extends SmoothPagedView } public long getIdForScreen(CellLayout layout) { - Iterator iter = mWorkspaceScreens.keySet().iterator(); - while (iter.hasNext()) { - long id = iter.next(); - if (mWorkspaceScreens.get(id) == layout) { - return id; - } + int index = mWorkspaceScreens.indexOfValue(layout); + if (index != -1) { + return mWorkspaceScreens.keyAt(index); } return -1; } @@ -896,14 +841,11 @@ public class Workspace extends SmoothPagedView return -1; } - ArrayList getScreenOrder() { + public ArrayList getScreenOrder() { return mScreenOrder; } public void stripEmptyScreens() { - // Log to disk - Launcher.addDumpLog(TAG, "11683562 - stripEmptyScreens()", true); - if (mLauncher.isWorkspaceLoading()) { // Don't strip empty screens if the workspace is still loading. // This is dangerous and can result in data loss. @@ -918,8 +860,10 @@ public class Workspace extends SmoothPagedView int currentPage = getNextPage(); ArrayList removeScreens = new ArrayList(); - for (Long id: mWorkspaceScreens.keySet()) { - CellLayout cl = mWorkspaceScreens.get(id); + int total = mWorkspaceScreens.size(); + for (int i = 0; i < total; i++) { + long id = mWorkspaceScreens.keyAt(i); + CellLayout cl = mWorkspaceScreens.valueAt(i); if (id >= 0 && cl.getShortcutsAndWidgets().getChildCount() == 0) { removeScreens.add(id); } @@ -931,7 +875,6 @@ public class Workspace extends SmoothPagedView int pageShift = 0; for (Long id: removeScreens) { - Launcher.addDumpLog(TAG, "11683562 - removing id: " + id, true); CellLayout cl = mWorkspaceScreens.get(id); mWorkspaceScreens.remove(id); mScreenOrder.remove(id); @@ -1075,6 +1018,7 @@ public class Workspace extends SmoothPagedView * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout * that it should intercept touch events, which is not something that is normally supported. */ + @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { return (workspaceInModalState() || !isFinishedSwitchingState()) @@ -1116,7 +1060,7 @@ public class Workspace extends SmoothPagedView case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_REST) { final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); - if (currentPage != null && !currentPage.lastDownOnOccupiedCell()) { + if (currentPage != null) { onWallpaperTap(ev); } } @@ -1178,7 +1122,7 @@ public class Workspace extends SmoothPagedView boolean passRightSwipesToCustomContent = (mTouchDownTime - mCustomContentShowTime) > CUSTOM_CONTENT_GESTURE_DELAY; - boolean swipeInIgnoreDirection = isLayoutRtl() ? deltaX < 0 : deltaX > 0; + boolean swipeInIgnoreDirection = mIsRtl ? deltaX < 0 : deltaX > 0; boolean onCustomContentScreen = getScreenIdForPageIndex(getCurrentPage()) == CUSTOM_CONTENT_SCREEN_ID; if (swipeInIgnoreDirection && onCustomContentScreen && passRightSwipesToCustomContent) { @@ -1285,15 +1229,14 @@ public class Workspace extends SmoothPagedView @Override protected void overScroll(float amount) { - boolean isRtl = isLayoutRtl(); - boolean shouldOverScroll = (amount <= 0 && (!hasCustomContent() || isRtl)) || - (amount >= 0 && (!hasCustomContent() || !isRtl)); + boolean shouldOverScroll = (amount <= 0 && (!hasCustomContent() || mIsRtl)) || + (amount >= 0 && (!hasCustomContent() || !mIsRtl)); boolean shouldScrollOverlay = mLauncherOverlay != null && - ((amount <= 0 && !isRtl) || (amount >= 0 && isRtl)); + ((amount <= 0 && !mIsRtl) || (amount >= 0 && mIsRtl)); boolean shouldZeroOverlay = mLauncherOverlay != null && mLastOverlaySroll != 0 && - ((amount >= 0 && !isRtl) || (amount <= 0 && isRtl)); + ((amount >= 0 && !mIsRtl) || (amount <= 0 && mIsRtl)); if (shouldScrollOverlay) { if (!mStartedSendingScrollEvents && mScrollInteractionBegan) { @@ -1307,23 +1250,26 @@ public class Workspace extends SmoothPagedView int progress = (int) Math.abs((f * 100)); mLastOverlaySroll = progress; - mLauncherOverlay.onScrollChange(progress, isRtl); + mLauncherOverlay.onScrollChange(progress, mIsRtl); } else if (shouldOverScroll) { dampedOverScroll(amount); - mOverScrollEffect = acceleratedOverFactor(amount); - } else { - mOverScrollEffect = 0; } if (shouldZeroOverlay) { - mLauncherOverlay.onScrollChange(0, isRtl); + mLauncherOverlay.onScrollChange(0, mIsRtl); } } + @Override + protected void getEdgeVerticalPostion(int[] pos) { + View child = getChildAt(getPageCount() - 1); + pos[0] = child.getTop(); + pos[1] = child.getBottom(); + } + @Override protected void notifyPageSwitchListener() { super.notifyPageSwitchListener(); - Launcher.setScreen(getNextPage()); if (hasCustomContent() && getNextPage() == 0 && !mCustomContentShowing) { mCustomContentShowing = true; @@ -1346,10 +1292,10 @@ public class Workspace extends SmoothPagedView protected void setWallpaperDimension() { new AsyncTask() { public Void doInBackground(Void ... args) { - String spKey = WallpaperCropActivity.getSharedPreferencesKey(); + String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY; SharedPreferences sp = mLauncher.getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); - LauncherWallpaperPickerActivity.suggestWallpaperDimension(mLauncher.getResources(), + WallpaperUtils.suggestWallpaperDimension(mLauncher.getResources(), sp, mLauncher.getWindowManager(), mWallpaperManager, mLauncher.overrideWallpaperDimensions()); return null; @@ -1439,7 +1385,22 @@ public class Workspace extends SmoothPagedView } private float wallpaperOffsetForCurrentScroll() { + // TODO: do different behavior if it's a live wallpaper? + // Don't use up all the wallpaper parallax until you have at least + // MIN_PARALLAX_PAGE_SPAN pages + int numScrollingPages = getNumScreensExcludingEmptyAndCustom(); + int parallaxPageSpan; + if (mWallpaperIsLiveWallpaper) { + parallaxPageSpan = numScrollingPages - 1; + } else { + parallaxPageSpan = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages - 1); + } + mNumPagesForWallpaperParallax = parallaxPageSpan; + if (getChildCount() <= 1) { + if (mIsRtl) { + return 1 - 1.0f/mNumPagesForWallpaperParallax; + } return 0; } @@ -1448,7 +1409,7 @@ public class Workspace extends SmoothPagedView int firstIndex = numCustomPages(); // Exclude the last extra empty screen (if we have > MIN_PARALLAX_PAGE_SPAN pages) int lastIndex = getChildCount() - 1 - emptyExtraPages; - if (isLayoutRtl()) { + if (mIsRtl) { int temp = firstIndex; firstIndex = lastIndex; lastIndex = temp; @@ -1459,28 +1420,20 @@ public class Workspace extends SmoothPagedView if (scrollRange == 0) { return 0; } else { - // TODO: do different behavior if it's a live wallpaper? // Sometimes the left parameter of the pages is animated during a layout transition; // this parameter offsets it to keep the wallpaper from animating as well int adjustedScroll = getScrollX() - firstPageScrollX - getLayoutTransitionOffsetForPage(0); float offset = Math.min(1, adjustedScroll / (float) scrollRange); offset = Math.max(0, offset); - // Don't use up all the wallpaper parallax until you have at least - // MIN_PARALLAX_PAGE_SPAN pages - int numScrollingPages = getNumScreensExcludingEmptyAndCustom(); - int parallaxPageSpan; - if (mWallpaperIsLiveWallpaper) { - parallaxPageSpan = numScrollingPages - 1; - } else { - parallaxPageSpan = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages - 1); - } - mNumPagesForWallpaperParallax = parallaxPageSpan; // On RTL devices, push the wallpaper offset to the right if we don't have enough // pages (ie if numScrollingPages < MIN_PARALLAX_PAGE_SPAN) - int padding = isLayoutRtl() ? parallaxPageSpan - numScrollingPages + 1 : 0; - return offset * (padding + numScrollingPages - 1) / parallaxPageSpan; + if (!mWallpaperIsLiveWallpaper && numScrollingPages < MIN_PARALLAX_PAGE_SPAN + && mIsRtl) { + return offset * (parallaxPageSpan - numScrollingPages + 1) / parallaxPageSpan; + } + return offset * (numScrollingPages - 1) / parallaxPageSpan; } } @@ -1560,81 +1513,17 @@ public class Workspace extends SmoothPagedView @Override public void announceForAccessibility(CharSequence text) { // Don't announce if apps is on top of us. - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { super.announceForAccessibility(text); } } - void showOutlines() { - if (!workspaceInModalState() && !mIsSwitchingState) { - if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); - if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); - mChildrenOutlineFadeInAnimation = LauncherAnimUtils.ofFloat(this, "childrenOutlineAlpha", 1.0f); - mChildrenOutlineFadeInAnimation.setDuration(CHILDREN_OUTLINE_FADE_IN_DURATION); - mChildrenOutlineFadeInAnimation.start(); - } - } - - void hideOutlines() { - if (!workspaceInModalState() && !mIsSwitchingState) { - if (mChildrenOutlineFadeInAnimation != null) mChildrenOutlineFadeInAnimation.cancel(); - if (mChildrenOutlineFadeOutAnimation != null) mChildrenOutlineFadeOutAnimation.cancel(); - mChildrenOutlineFadeOutAnimation = LauncherAnimUtils.ofFloat(this, "childrenOutlineAlpha", 0.0f); - mChildrenOutlineFadeOutAnimation.setDuration(CHILDREN_OUTLINE_FADE_OUT_DURATION); - mChildrenOutlineFadeOutAnimation.setStartDelay(CHILDREN_OUTLINE_FADE_OUT_DELAY); - mChildrenOutlineFadeOutAnimation.start(); - } - } - public void showOutlinesTemporarily() { if (!mIsPageMoving && !isTouchActive()) { snapToPage(mCurrentPage); } } - public void setChildrenOutlineAlpha(float alpha) { - mChildrenOutlineAlpha = alpha; - for (int i = 0; i < getChildCount(); i++) { - CellLayout cl = (CellLayout) getChildAt(i); - cl.setBackgroundAlpha(alpha); - } - } - - public float getChildrenOutlineAlpha() { - return mChildrenOutlineAlpha; - } - - private void animateBackgroundGradient(float finalAlpha, boolean animated) { - final DragLayer dragLayer = mLauncher.getDragLayer(); - - if (mBackgroundFadeInAnimation != null) { - mBackgroundFadeInAnimation.cancel(); - mBackgroundFadeInAnimation = null; - } - if (mBackgroundFadeOutAnimation != null) { - mBackgroundFadeOutAnimation.cancel(); - mBackgroundFadeOutAnimation = null; - } - float startAlpha = dragLayer.getBackgroundAlpha(); - if (finalAlpha != startAlpha) { - if (animated) { - mBackgroundFadeOutAnimation = - LauncherAnimUtils.ofFloat(this, startAlpha, finalAlpha); - mBackgroundFadeOutAnimation.addUpdateListener(new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - dragLayer.setBackgroundAlpha( - ((Float)animation.getAnimatedValue()).floatValue()); - } - }); - mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); - mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION); - mBackgroundFadeOutAnimation.start(); - } else { - dragLayer.setBackgroundAlpha(finalAlpha); - } - } - } - float backgroundAlphaInterpolator(float r) { float pivotA = 0.1f; float pivotB = 0.4f; @@ -1648,28 +1537,38 @@ public class Workspace extends SmoothPagedView } private void updatePageAlphaValues(int screenCenter) { - boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; if (mWorkspaceFadeInAdjacentScreens && !workspaceInModalState() && - !mIsSwitchingState && - !isInOverscroll) { + !mIsSwitchingState) { for (int i = numCustomPages(); i < getChildCount(); i++) { CellLayout child = (CellLayout) getChildAt(i); if (child != null) { float scrollProgress = getScrollProgress(screenCenter, child, i); float alpha = 1 - Math.abs(scrollProgress); child.getShortcutsAndWidgets().setAlpha(alpha); - //child.setBackgroundAlphaMultiplier(1 - alpha); } } } } - private void setChildrenBackgroundAlphaMultipliers(float a) { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void enableAccessibleDrag(boolean enable) { for (int i = 0; i < getChildCount(); i++) { CellLayout child = (CellLayout) getChildAt(i); - child.setBackgroundAlphaMultiplier(a); + child.enableAccessibleDrag(enable, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG); } + + if (enable) { + // We need to allow our individual children to become click handlers in this case + setOnClickListener(null); + } else { + // Reset our click listener + setOnClickListener(mLauncher); + } + mLauncher.getSearchBar().enableAccessibleDrag(enable); + mLauncher.getHotseat().getLayout() + .enableAccessibleDrag(enable, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG); } public boolean hasCustomContent() { @@ -1696,7 +1595,7 @@ public class Workspace extends SmoothPagedView translationX = scrollRange - scrollDelta; progress = (scrollRange - scrollDelta) / scrollRange; - if (isLayoutRtl()) { + if (mIsRtl) { translationX = Math.min(0, translationX); } else { translationX = Math.max(0, translationX); @@ -1713,7 +1612,11 @@ public class Workspace extends SmoothPagedView mLastCustomContentScrollProgress = progress; - mLauncher.getDragLayer().setBackgroundAlpha(progress * 0.8f); + // We should only update the drag layer background alpha if we are not in all apps or the + // widgets tray + if (mState == State.NORMAL) { + mLauncher.getDragLayer().setBackgroundAlpha(progress == 1 ? 0 : progress * 0.8f); + } if (mLauncher.getHotseat() != null) { mLauncher.getHotseat().setTranslationX(translationX); @@ -1738,7 +1641,7 @@ public class Workspace extends SmoothPagedView OnClickListener listener = new OnClickListener() { @Override public void onClick(View arg0) { - enterOverviewMode(); + mLauncher.showOverviewMode(true); } }; return listener; @@ -1746,35 +1649,9 @@ public class Workspace extends SmoothPagedView @Override protected void screenScrolled(int screenCenter) { - final boolean isRtl = isLayoutRtl(); - super.screenScrolled(screenCenter); - updatePageAlphaValues(screenCenter); updateStateForCustomContent(screenCenter); enableHwLayersOnVisiblePages(); - - boolean shouldOverScroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; - - if (shouldOverScroll) { - int index = 0; - final int lowerIndex = 0; - final int upperIndex = getChildCount() - 1; - - final boolean isLeftPage = mOverScrollX < 0; - index = (!isRtl && isLeftPage) || (isRtl && !isLeftPage) ? lowerIndex : upperIndex; - - CellLayout cl = (CellLayout) getChildAt(index); - float effect = Math.abs(mOverScrollEffect); - cl.setOverScrollAmount(Math.abs(effect), isLeftPage); - - mOverscrollEffectSet = true; - } else { - if (mOverscrollEffectSet && getChildCount() > 0) { - mOverscrollEffectSet = false; - ((CellLayout) getChildAt(0)).setOverScrollAmount(0, false); - ((CellLayout) getChildAt(getChildCount() - 1)).setOverScrollAmount(0, false); - } - } } protected void onAttachedToWindow() { @@ -1798,9 +1675,6 @@ public class Workspace extends SmoothPagedView getPageIndicator().setOnClickListener(listener); } } - AccessibilityManager am = (AccessibilityManager) - getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - sAccessibilityEnabled = am.isEnabled(); // Update wallpaper dimensions if they were changed since last onResume // (we also always set the wallpaper dimensions in the constructor) @@ -1832,7 +1706,7 @@ public class Workspace extends SmoothPagedView @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { final Folder openFolder = getOpenFolder(); if (openFolder != null) { return openFolder.requestFocus(direction, previouslyFocusedRect); @@ -1853,7 +1727,7 @@ public class Workspace extends SmoothPagedView @Override public void addFocusables(ArrayList views, int direction, int focusableMode) { - if (!mLauncher.isAllAppsVisible()) { + if (!mLauncher.isAppsViewVisible()) { final Folder openFolder = getOpenFolder(); if (openFolder != null) { openFolder.addFocusables(views, direction); @@ -1898,7 +1772,7 @@ public class Workspace extends SmoothPagedView } } - private void updateChildrenLayersEnabled(boolean force) { + @Thunk void updateChildrenLayersEnabled(boolean force) { boolean small = mState == State.OVERVIEW || mIsSwitchingState; boolean enableChildrenLayers = force || small || mAnimatingViewIntoPlace || isPageMoving(); @@ -1970,64 +1844,6 @@ public class Workspace extends SmoothPagedView position[0], position[1], 0, null); } - /* - * This interpolator emulates the rate at which the perceived scale of an object changes - * as its distance from a camera increases. When this interpolator is applied to a scale - * animation on a view, it evokes the sense that the object is shrinking due to moving away - * from the camera. - */ - static class ZInterpolator implements TimeInterpolator { - private float focalLength; - - public ZInterpolator(float foc) { - focalLength = foc; - } - - public float getInterpolation(float input) { - return (1.0f - focalLength / (focalLength + input)) / - (1.0f - focalLength / (focalLength + 1.0f)); - } - } - - /* - * The exact reverse of ZInterpolator. - */ - static class InverseZInterpolator implements TimeInterpolator { - private ZInterpolator zInterpolator; - public InverseZInterpolator(float foc) { - zInterpolator = new ZInterpolator(foc); - } - public float getInterpolation(float input) { - return 1 - zInterpolator.getInterpolation(1 - input); - } - } - - /* - * ZInterpolator compounded with an ease-out. - */ - static class ZoomOutInterpolator implements TimeInterpolator { - private final DecelerateInterpolator decelerate = new DecelerateInterpolator(0.75f); - private final ZInterpolator zInterpolator = new ZInterpolator(0.13f); - - public float getInterpolation(float input) { - return decelerate.getInterpolation(zInterpolator.getInterpolation(input)); - } - } - - /* - * InvereZInterpolator compounded with an ease-out. - */ - static class ZoomInInterpolator implements TimeInterpolator { - private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f); - private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f); - - public float getInterpolation(float input) { - return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input)); - } - } - - private final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator(); - /* * * We call these methods (onDragStartedWithItemSpans/onDragStartedWithSize) whenever we @@ -2054,16 +1870,14 @@ public class Workspace extends SmoothPagedView public void onExternalDragStartedWithItem(View v) { // Compose a drag bitmap with the view scaled to the icon size - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); int iconSize = grid.iconSizePx; int bmpWidth = v.getMeasuredWidth(); int bmpHeight = v.getMeasuredHeight(); // If this is a text view, use its drawable instead if (v instanceof TextView) { - TextView tv = (TextView) v; - Drawable d = tv.getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); bmpWidth = bounds.width(); bmpHeight = bounds.height(); @@ -2081,7 +1895,7 @@ public class Workspace extends SmoothPagedView } public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, boolean clipAlpha) { - int[] size = estimateItemSize(info.spanX, info.spanY, info, false); + int[] size = estimateItemSize(info, false); // The outline is used to visualize where the item will land if dropped mDragOutline = createDragOutline(b, DRAG_BITMAP_PADDING, size[0], size[1], clipAlpha); @@ -2092,21 +1906,6 @@ public class Workspace extends SmoothPagedView dragLayer.clearAllResizeFrames(); } - private void initAnimationArrays() { - final int childCount = getChildCount(); - if (mLastChildCount == childCount) return; - - mOldBackgroundAlphas = new float[childCount]; - mOldAlphas = new float[childCount]; - mNewBackgroundAlphas = new float[childCount]; - mNewAlphas = new float[childCount]; - } - - Animator getChangeStateAnimation(final State state, boolean animated, - ArrayList layerViews) { - return getChangeStateAnimation(state, animated, 0, -1, layerViews); - } - @Override protected void getFreeScrollPageRange(int[] range) { getOverviewModePages(range); @@ -2120,14 +1919,13 @@ public class Workspace extends SmoothPagedView range[1] = Math.max(0, end); } - protected void onStartReordering() { + public void onStartReordering() { super.onStartReordering(); - showOutlines(); // Reordering handles its own animations, disable the automatic ones. disableLayoutTransitions(); } - protected void onEndReordering() { + public void onEndReordering() { super.onEndReordering(); if (mLauncher.isWorkspaceLoading()) { @@ -2135,7 +1933,6 @@ public class Workspace extends SmoothPagedView return; } - hideOutlines(); mScreenOrder.clear(); int count = getChildCount(); for (int i = 0; i < count; i++) { @@ -2153,44 +1950,8 @@ public class Workspace extends SmoothPagedView return mState == State.OVERVIEW; } - public boolean enterOverviewMode() { - if (mTouchState != TOUCH_STATE_REST) { - return false; - } - enableOverviewMode(true, -1, true); - return true; - } - - public void exitOverviewMode(boolean animated) { - exitOverviewMode(-1, animated); - } - - public void exitOverviewMode(int snapPage, boolean animated) { - enableOverviewMode(false, snapPage, animated); - } - - private void enableOverviewMode(boolean enable, int snapPage, boolean animated) { - State finalState = Workspace.State.OVERVIEW; - if (!enable) { - finalState = Workspace.State.NORMAL; - } - - Animator workspaceAnim = getChangeStateAnimation(finalState, animated, 0, snapPage); - if (workspaceAnim != null) { - onTransitionPrepare(); - workspaceAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator arg0) { - onTransitionEnd(); - } - }); - workspaceAnim.start(); - } - } - int getOverviewModeTranslationY() { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); Rect overviewBar = grid.getOverviewModeButtonBarRect(); int availableHeight = getViewportHeight(); @@ -2202,332 +1963,75 @@ public class Workspace extends SmoothPagedView return -offsetFromTopEdge + mInsets.top + offsetToCenterInOverview; } - public void updateInteractionForState() { - if (mState != State.NORMAL) { - mLauncher.onInteractionBegin(); - } else { - mLauncher.onInteractionEnd(); - } - } + /** + * Sets the current workspace {@link State}, returning an animation transitioning the workspace + * to that new state. + */ + public Animator setStateWithAnimation(State toState, int toPage, boolean animated, + boolean hasOverlaySearchBar, HashMap layerViews) { + // Create the animation to the new state + Animator workspaceAnim = mStateTransitionAnimation.getAnimationToState(mState, + toState, toPage, animated, hasOverlaySearchBar, layerViews); - private void setState(State state) { - mState = state; - updateInteractionForState(); + // Update the current state + mState = toState; updateAccessibilityFlags(); + + return workspaceAnim; } State getState() { return mState; } - private void updateAccessibilityFlags() { - int accessible = mState == State.NORMAL ? - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES : - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; - setImportantForAccessibility(accessible); + public void updateAccessibilityFlags() { + if (Utilities.isLmpOrAbove()) { + int total = getPageCount(); + for (int i = numCustomPages(); i < total; i++) { + updateAccessibilityFlags((CellLayout) getPageAt(i), i); + } + setImportantForAccessibility((mState == State.NORMAL || mState == State.OVERVIEW) + ? IMPORTANT_FOR_ACCESSIBILITY_AUTO + : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } else { + int accessible = mState == State.NORMAL ? + IMPORTANT_FOR_ACCESSIBILITY_AUTO : + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; + setImportantForAccessibility(accessible); + } } - private static final int HIDE_WORKSPACE_DURATION = 100; + private void updateAccessibilityFlags(CellLayout page, int pageNo) { + if (mState == State.OVERVIEW) { + page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + page.getShortcutsAndWidgets().setImportantForAccessibility( + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + page.setContentDescription(getPageDescription(pageNo)); - Animator getChangeStateAnimation(final State state, boolean animated, int delay, int snapPage) { - return getChangeStateAnimation(state, animated, delay, snapPage, null); - } - - Animator getChangeStateAnimation(final State state, boolean animated, int delay, int snapPage, - ArrayList layerViews) { - if (mState == state) { - return null; - } - - // Initialize animation arrays for the first time if necessary - initAnimationArrays(); - - AnimatorSet anim = animated ? LauncherAnimUtils.createAnimatorSet() : null; - - // We only want a single instance of a workspace animation to be running at once, so - // we cancel any incomplete transition. - if (mStateAnimator != null) { - mStateAnimator.cancel(); - } - mStateAnimator = anim; - - final State oldState = mState; - final boolean oldStateIsNormal = (oldState == State.NORMAL); - final boolean oldStateIsSpringLoaded = (oldState == State.SPRING_LOADED); - final boolean oldStateIsNormalHidden = (oldState == State.NORMAL_HIDDEN); - final boolean oldStateIsOverviewHidden = (oldState == State.OVERVIEW_HIDDEN); - final boolean oldStateIsOverview = (oldState == State.OVERVIEW); - setState(state); - final boolean stateIsNormal = (state == State.NORMAL); - final boolean stateIsSpringLoaded = (state == State.SPRING_LOADED); - final boolean stateIsNormalHidden = (state == State.NORMAL_HIDDEN); - final boolean stateIsOverviewHidden = (state == State.OVERVIEW_HIDDEN); - final boolean stateIsOverview = (state == State.OVERVIEW); - float finalBackgroundAlpha = (stateIsSpringLoaded || stateIsOverview) ? 1.0f : 0f; - float finalHotseatAndPageIndicatorAlpha = (stateIsNormal || stateIsSpringLoaded) ? 1f : 0f; - float finalOverviewPanelAlpha = stateIsOverview ? 1f : 0f; - float finalSearchBarAlpha = !stateIsNormal ? 0f : 1f; - float finalWorkspaceTranslationY = stateIsOverview || stateIsOverviewHidden ? - getOverviewModeTranslationY() : 0; - - boolean workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden); - boolean overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden); - boolean allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal); - boolean workspaceToOverview = (oldStateIsNormal && stateIsOverview); - boolean overviewToWorkspace = (oldStateIsOverview && stateIsNormal); - - mNewScale = 1.0f; - - if (oldStateIsOverview) { - disableFreeScroll(); - } else if (stateIsOverview) { - enableFreeScroll(); - } - - if (state != State.NORMAL) { - if (stateIsSpringLoaded) { - mNewScale = mSpringLoadedShrinkFactor; - } else if (stateIsOverview || stateIsOverviewHidden) { - mNewScale = mOverviewModeShrinkFactor; + if (mPagesAccessibilityDelegate == null) { + mPagesAccessibilityDelegate = new OverviewScreenAccessibilityDelegate(this); } - } - - final int duration; - if (workspaceToAllApps || overviewToAllApps) { - duration = HIDE_WORKSPACE_DURATION; //getResources().getInteger(R.integer.config_workspaceUnshrinkTime); - } else if (workspaceToOverview || overviewToWorkspace) { - duration = getResources().getInteger(R.integer.config_overviewTransitionTime); + page.setAccessibilityDelegate(mPagesAccessibilityDelegate); } else { - duration = getResources().getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime); - } - - if (snapPage == -1) { - snapPage = getPageNearestToCenterOfScreen(); - } - snapToPage(snapPage, duration, mZoomInInterpolator); - - for (int i = 0; i < getChildCount(); i++) { - final CellLayout cl = (CellLayout) getChildAt(i); - boolean isCurrentPage = (i == snapPage); - float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); - float finalAlpha; - if (stateIsNormalHidden || stateIsOverviewHidden) { - finalAlpha = 0f; - } else if (stateIsNormal && mWorkspaceFadeInAdjacentScreens) { - finalAlpha = (i == snapPage || i < numCustomPages()) ? 1f : 0f; - } else { - finalAlpha = 1f; - } - - // If we are animating to/from the small state, then hide the side pages and fade the - // current page in - if (!mIsSwitchingState) { - if (workspaceToAllApps || allAppsToWorkspace) { - if (allAppsToWorkspace && isCurrentPage) { - initialAlpha = 0f; - } else if (!isCurrentPage) { - initialAlpha = finalAlpha = 0f; - } - cl.setShortcutAndWidgetAlpha(initialAlpha); - } - } - - mOldAlphas[i] = initialAlpha; - mNewAlphas[i] = finalAlpha; - if (animated) { - mOldBackgroundAlphas[i] = cl.getBackgroundAlpha(); - mNewBackgroundAlphas[i] = finalBackgroundAlpha; - } else { - cl.setBackgroundAlpha(finalBackgroundAlpha); - cl.setShortcutAndWidgetAlpha(finalAlpha); - } - } - - final View searchBar = mLauncher.getQsbBar(); - final View overviewPanel = mLauncher.getOverviewPanel(); - final View hotseat = mLauncher.getHotseat(); - final View pageIndicator = getPageIndicator(); - if (animated) { - LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(this); - scale.scaleX(mNewScale) - .scaleY(mNewScale) - .translationY(finalWorkspaceTranslationY) - .setDuration(duration) - .setInterpolator(mZoomInInterpolator); - anim.play(scale); - for (int index = 0; index < getChildCount(); index++) { - final int i = index; - final CellLayout cl = (CellLayout) getChildAt(i); - float currentAlpha = cl.getShortcutsAndWidgets().getAlpha(); - if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) { - cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); - cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); - } else { - if (layerViews != null) { - layerViews.add(cl); - } - if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) { - LauncherViewPropertyAnimator alphaAnim = - new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets()); - alphaAnim.alpha(mNewAlphas[i]) - .setDuration(duration) - .setInterpolator(mZoomInInterpolator); - anim.play(alphaAnim); - } - if (mOldBackgroundAlphas[i] != 0 || - mNewBackgroundAlphas[i] != 0) { - ValueAnimator bgAnim = - LauncherAnimUtils.ofFloat(cl, 0f, 1f); - bgAnim.setInterpolator(mZoomInInterpolator); - bgAnim.setDuration(duration); - bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { - public void onAnimationUpdate(float a, float b) { - cl.setBackgroundAlpha( - a * mOldBackgroundAlphas[i] + - b * mNewBackgroundAlphas[i]); - } - }); - anim.play(bgAnim); - } - } - } - Animator pageIndicatorAlpha = null; - if (pageIndicator != null) { - pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator) - .alpha(finalHotseatAndPageIndicatorAlpha).withLayer(); - pageIndicatorAlpha.addListener(new AlphaUpdateListener(pageIndicator)); - } else { - // create a dummy animation so we don't need to do null checks later - pageIndicatorAlpha = ValueAnimator.ofFloat(0, 0); - } - - Animator hotseatAlpha = new LauncherViewPropertyAnimator(hotseat) - .alpha(finalHotseatAndPageIndicatorAlpha).withLayer(); - hotseatAlpha.addListener(new AlphaUpdateListener(hotseat)); - - Animator overviewPanelAlpha = new LauncherViewPropertyAnimator(overviewPanel) - .alpha(finalOverviewPanelAlpha).withLayer(); - overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel)); - - // For animation optimations, we may need to provide the Launcher transition - // with a set of views on which to force build layers in certain scenarios. - hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null); - overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null); - if (layerViews != null) { - layerViews.add(hotseat); - layerViews.add(overviewPanel); - } - - if (workspaceToOverview) { - pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2)); - hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); - overviewPanelAlpha.setInterpolator(null); - } else if (overviewToWorkspace) { - pageIndicatorAlpha.setInterpolator(null); - hotseatAlpha.setInterpolator(null); - overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); - } - - overviewPanelAlpha.setDuration(duration); - pageIndicatorAlpha.setDuration(duration); - hotseatAlpha.setDuration(duration); - - if (searchBar != null) { - Animator searchBarAlpha = new LauncherViewPropertyAnimator(searchBar) - .alpha(finalSearchBarAlpha).withLayer(); - searchBarAlpha.addListener(new AlphaUpdateListener(searchBar)); - searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); - if (layerViews != null) { - layerViews.add(searchBar); - } - searchBarAlpha.setDuration(duration); - anim.play(searchBarAlpha); - } - - anim.play(overviewPanelAlpha); - anim.play(hotseatAlpha); - anim.play(pageIndicatorAlpha); - anim.setStartDelay(delay); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mStateAnimator = null; - } - }); - } else { - overviewPanel.setAlpha(finalOverviewPanelAlpha); - AlphaUpdateListener.updateVisibility(overviewPanel); - hotseat.setAlpha(finalHotseatAndPageIndicatorAlpha); - AlphaUpdateListener.updateVisibility(hotseat); - if (pageIndicator != null) { - pageIndicator.setAlpha(finalHotseatAndPageIndicatorAlpha); - AlphaUpdateListener.updateVisibility(pageIndicator); - } - if (searchBar != null) { - searchBar.setAlpha(finalSearchBarAlpha); - AlphaUpdateListener.updateVisibility(searchBar); - } - updateCustomContentVisibility(); - setScaleX(mNewScale); - setScaleY(mNewScale); - setTranslationY(finalWorkspaceTranslationY); - } - - if (stateIsNormal) { - animateBackgroundGradient(0f, animated); - } else { - animateBackgroundGradient(getResources().getInteger( - R.integer.config_workspaceScrimAlpha) / 100f, animated); - } - return anim; - } - - static class AlphaUpdateListener implements AnimatorUpdateListener, AnimatorListener { - View view; - public AlphaUpdateListener(View v) { - view = v; - } - - @Override - public void onAnimationUpdate(ValueAnimator arg0) { - updateVisibility(view); - } - - public static void updateVisibility(View view) { - // We want to avoid the extra layout pass by setting the views to GONE unless - // accessibility is on, in which case not setting them to GONE causes a glitch. - int invisibleState = sAccessibilityEnabled ? GONE : INVISIBLE; - if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) { - view.setVisibility(invisibleState); - } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD - && view.getVisibility() != VISIBLE) { - view.setVisibility(VISIBLE); - } - } - - @Override - public void onAnimationCancel(Animator arg0) { - } - - @Override - public void onAnimationEnd(Animator arg0) { - updateVisibility(view); - } - - @Override - public void onAnimationRepeat(Animator arg0) { - } - - @Override - public void onAnimationStart(Animator arg0) { - // We want the views to be visible for animation, so fade-in/out is visible - view.setVisibility(VISIBLE); + int accessible = mState == State.NORMAL ? + IMPORTANT_FOR_ACCESSIBILITY_AUTO : + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; + page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + page.getShortcutsAndWidgets().setImportantForAccessibility(accessible); + page.setContentDescription(null); + page.setAccessibilityDelegate(null); } } @Override public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { - onTransitionPrepare(); + mIsSwitchingState = true; + + // Invalidate here to ensure that the pages are rendered during the state change transition. + invalidate(); + + updateChildrenLayersEnabled(false); + hideCustomContentIfNecessary(); } @Override @@ -2541,17 +2045,9 @@ public class Workspace extends SmoothPagedView @Override public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { - onTransitionEnd(); - } - - private void onTransitionPrepare() { - mIsSwitchingState = true; - - // Invalidate here to ensure that the pages are rendered during the state change transition. - invalidate(); - + mIsSwitchingState = false; updateChildrenLayersEnabled(false); - hideCustomContentIfNecessary(); + showCustomContentIfNecessary(); } void updateCustomContentVisibility() { @@ -2577,15 +2073,17 @@ public class Workspace extends SmoothPagedView } } - private void onTransitionEnd() { - mIsSwitchingState = false; - updateChildrenLayersEnabled(false); - showCustomContentIfNecessary(); - } - - @Override - public View getContent() { - return this; + /** + * Returns the drawable for the given text view. + */ + public static Drawable getTextViewIcon(TextView tv) { + final Drawable[] drawables = tv.getCompoundDrawables(); + for (int i = 0; i < drawables.length; i++) { + if (drawables[i] != null) { + return drawables[i]; + } + } + return null; } /** @@ -2603,7 +2101,7 @@ public class Workspace extends SmoothPagedView destCanvas.save(); if (v instanceof TextView) { - Drawable d = ((TextView) v).getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); clipRect.set(0, 0, bounds.width() + padding, bounds.height() + padding); destCanvas.translate(padding / 2 - bounds.left, padding / 2 - bounds.top); @@ -2640,7 +2138,7 @@ public class Workspace extends SmoothPagedView int padding = expectedPadding.get(); if (v instanceof TextView) { - Drawable d = ((TextView) v).getCompoundDrawables()[1]; + Drawable d = getTextViewIcon((TextView) v); Rect bounds = getDrawableBounds(d); b = Bitmap.createBitmap(bounds.width() + padding, bounds.height() + padding, Bitmap.Config.ARGB_8888); @@ -2701,7 +2199,12 @@ public class Workspace extends SmoothPagedView return b; } - void startDrag(CellLayout.CellInfo cellInfo) { + public void startDrag(CellLayout.CellInfo cellInfo) { + startDrag(cellInfo, false); + } + + @Override + public void startDrag(CellLayout.CellInfo cellInfo, boolean accessible) { View child = cellInfo.cell; // Make sure the drag was started by a long press as opposed to a long click. @@ -2714,10 +2217,15 @@ public class Workspace extends SmoothPagedView CellLayout layout = (CellLayout) child.getParent().getParent(); layout.prepareChildForDrag(child); - beginDragShared(child, this); + beginDragShared(child, this, accessible); } - public void beginDragShared(View child, DragSource source) { + public void beginDragShared(View child, DragSource source, boolean accessible) { + beginDragShared(child, new Point(), source, accessible); + } + + public void beginDragShared(View child, Point relativeTouchPos, DragSource source, + boolean accessible) { child.clearFocus(); child.setPressed(false); @@ -2737,16 +2245,27 @@ public class Workspace extends SmoothPagedView int dragLayerY = Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 - padding.get() / 2); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); Point dragVisualizeOffset = null; Rect dragRect = null; if (child instanceof BubbleTextView) { + BubbleTextView icon = (BubbleTextView) child; int iconSize = grid.iconSizePx; int top = child.getPaddingTop(); int left = (bmpWidth - iconSize) / 2; int right = left + iconSize; int bottom = top + iconSize; + if (icon.isLayoutHorizontal()) { + // If the layout is horizontal, then if we are just picking up the icon, then just + // use the child position since the icon is top-left aligned. Otherwise, offset + // the drag layer position horizontally so that the icon is under the current + // touch position. + if (icon.getIcon().getBounds().contains(relativeTouchPos.x, relativeTouchPos.y)) { + dragLayerX = Math.round(mTempXY[0]); + } else { + dragLayerX = Math.round(mTempXY[0] + relativeTouchPos.x - (bmpWidth / 2)); + } + } dragLayerY += top; // Note: The drag region is used to calculate drag layer offsets, but the // dragVisualizeOffset in addition to the dragRect (the size) to position the outline. @@ -2771,7 +2290,7 @@ public class Workspace extends SmoothPagedView } DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), - DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible); dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor()); if (child.getParent() instanceof ShortcutAndWidgetContainer) { @@ -2782,8 +2301,7 @@ public class Workspace extends SmoothPagedView } public void beginExternalDragShared(View child, DragSource source) { - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); int iconSize = grid.iconSizePx; // Notify launcher of drag start @@ -2821,25 +2339,13 @@ public class Workspace extends SmoothPagedView // Start the drag DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), - DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, false); dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor()); // Recycle temporary bitmaps tmpB.recycle(); } - void addApplicationShortcut(ShortcutInfo info, CellLayout target, long container, long screenId, - int cellX, int cellY, boolean insertAtFirst, int intersectX, int intersectY) { - View view = mLauncher.createShortcut(R.layout.application, target, (ShortcutInfo) info); - - final int[] cellXY = new int[2]; - target.findCellForSpanThatIntersects(cellXY, 1, 1, intersectX, intersectY); - addInScreen(view, container, screenId, cellXY[0], cellXY[1], 1, 1, insertAtFirst); - - LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screenId, cellXY[0], - cellXY[1]); - } - public boolean transitionStateShouldAllowDrop() { return ((!isSwitchingState() || mTransitionProgress > 0.5f) && (mState == State.NORMAL || mState == State.SPRING_LOADED)); @@ -2858,8 +2364,7 @@ public class Workspace extends SmoothPagedView } if (!transitionStateShouldAllowDrop()) return false; - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, - d.dragView, mDragViewVisualCenter); + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); // We want the point to be mapped to the dragTarget. if (mLauncher.isHotseatLayout(dropTargetLayout)) { @@ -3060,10 +2565,11 @@ public class Workspace extends SmoothPagedView return false; } - public void onDrop(final DragObject d) { - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, - mDragViewVisualCenter); + @Override + public void prepareAccessibilityDrop() { } + public void onDrop(final DragObject d) { + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); CellLayout dropTargetLayout = mDropToLayout; // We want the point to be mapped to the dragTarget. @@ -3075,7 +2581,7 @@ public class Workspace extends SmoothPagedView } } - int snapScreen = -1; + int snapScreen = WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE; boolean resizeOnDrop = false; if (d.dragSource != this) { final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0], @@ -3178,9 +2684,9 @@ public class Workspace extends SmoothPagedView // in its final location final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell; - AppWidgetProviderInfo pinfo = hostView.getAppWidgetInfo(); - if (pinfo != null && - pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) { + AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo(); + if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE + && !d.accessibleDrag) { final Runnable addResizeFrame = new Runnable() { public void run() { DragLayer dragLayer = mLauncher.getDragLayer(); @@ -3228,13 +2734,17 @@ public class Workspace extends SmoothPagedView mAnimatingViewIntoPlace = true; if (d.dragView.hasDrawn()) { final ItemInfo info = (ItemInfo) cell.getTag(); - if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) { + boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET + || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + if (isWidget) { int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE : ANIMATE_INTO_POSITION_AND_DISAPPEAR; animateWidgetDrop(info, parent, d.dragView, onCompleteRunnable, animationType, cell, false); } else { - int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION; + int duration = snapScreen < 0 ? + WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE : + ADJACENT_SCREEN_DROP_DURATION; mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration, onCompleteRunnable, this); } @@ -3246,26 +2756,26 @@ public class Workspace extends SmoothPagedView } } - public void setFinalScrollForPageChange(int pageIndex) { - CellLayout cl = (CellLayout) getChildAt(pageIndex); - if (cl != null) { - mSavedScrollX = getScrollX(); - mSavedTranslationX = cl.getTranslationX(); - mSavedRotationY = cl.getRotationY(); - final int newX = getScrollForPage(pageIndex); - setScrollX(newX); - cl.setTranslationX(0f); - cl.setRotationY(0f); + /** + * Computes the area relative to dragLayer which is used to display a page. + */ + public void getPageAreaRelativeToDragLayer(Rect outArea) { + CellLayout child = (CellLayout) getChildAt(getNextPage()); + if (child == null) { + return; } - } + ShortcutAndWidgetContainer boundingLayout = child.getShortcutsAndWidgets(); - public void resetFinalScrollForPageChange(int pageIndex) { - if (pageIndex >= 0) { - CellLayout cl = (CellLayout) getChildAt(pageIndex); - setScrollX(mSavedScrollX); - cl.setTranslationX(mSavedTranslationX); - cl.setRotationY(mSavedRotationY); - } + // Use the absolute left instead of the child left, as we want the visible area + // irrespective of the visible child. Since the view can only scroll horizontally, the + // top position is not affected. + mTempXY[0] = getViewportOffsetX() + getPaddingLeft() + boundingLayout.getLeft(); + mTempXY[1] = child.getTop() + boundingLayout.getTop(); + + float scale = mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempXY); + outArea.set(mTempXY[0], mTempXY[1], + (int) (mTempXY[0] + scale * boundingLayout.getMeasuredWidth()), + (int) (mTempXY[1] + scale * boundingLayout.getMeasuredHeight())); } public void getViewLocationRelativeToSelf(View v, int[] location) { @@ -3281,8 +2791,12 @@ public class Workspace extends SmoothPagedView location[1] = vY - y; } + @Override public void onDragEnter(DragObject d) { - mDragEnforcer.onDragEnter(); + if (ENFORCE_DRAG_EVENT_ORDER) { + enfoceDragParity("onDragEnter", 1, 1); + } + mCreateUserFolderOnDrop = false; mAddToExistingFolderOnDrop = false; @@ -3300,42 +2814,46 @@ public class Workspace extends SmoothPagedView * widthGap/heightGap (right, bottom) */ static Rect getCellLayoutMetrics(Launcher launcher, int orientation) { LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + InvariantDeviceProfile inv = app.getInvariantDeviceProfile(); Display display = launcher.getWindowManager().getDefaultDisplay(); Point smallestSize = new Point(); Point largestSize = new Point(); display.getCurrentSizeRange(smallestSize, largestSize); - int countX = (int) grid.numColumns; - int countY = (int) grid.numRows; + int countX = (int) inv.numColumns; + int countY = (int) inv.numRows; + boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); if (orientation == CellLayout.LANDSCAPE) { if (mLandscapeCellLayoutMetrics == null) { - Rect padding = grid.getWorkspacePadding(CellLayout.LANDSCAPE); + Rect padding = inv.landscapeProfile.getWorkspacePadding(isLayoutRtl); int width = largestSize.x - padding.left - padding.right; int height = smallestSize.y - padding.top - padding.bottom; mLandscapeCellLayoutMetrics = new Rect(); mLandscapeCellLayoutMetrics.set( - grid.calculateCellWidth(width, countX), - grid.calculateCellHeight(height, countY), 0, 0); + DeviceProfile.calculateCellWidth(width, countX), + DeviceProfile.calculateCellHeight(height, countY), 0, 0); } return mLandscapeCellLayoutMetrics; } else if (orientation == CellLayout.PORTRAIT) { if (mPortraitCellLayoutMetrics == null) { - Rect padding = grid.getWorkspacePadding(CellLayout.PORTRAIT); + Rect padding = inv.portraitProfile.getWorkspacePadding(isLayoutRtl); int width = smallestSize.x - padding.left - padding.right; int height = largestSize.y - padding.top - padding.bottom; mPortraitCellLayoutMetrics = new Rect(); mPortraitCellLayoutMetrics.set( - grid.calculateCellWidth(width, countX), - grid.calculateCellHeight(height, countY), 0, 0); + DeviceProfile.calculateCellWidth(width, countX), + DeviceProfile.calculateCellHeight(height, countY), 0, 0); } return mPortraitCellLayoutMetrics; } return null; } + @Override public void onDragExit(DragObject d) { - mDragEnforcer.onDragExit(); + if (ENFORCE_DRAG_EVENT_ORDER) { + enfoceDragParity("onDragExit", -1, 0); + } // Here we store the final page that will be dropped to, if the workspace in fact // receives the drop @@ -3364,12 +2882,27 @@ public class Workspace extends SmoothPagedView mSpringLoadedDragController.cancel(); - if (!mIsPageMoving) { - hideOutlines(); - } mLauncher.getDragLayer().hidePageHints(); } + private void enfoceDragParity(String event, int update, int expectedValue) { + enfoceDragParity(this, event, update, expectedValue); + for (int i = 0; i < getChildCount(); i++) { + enfoceDragParity(getChildAt(i), event, update, expectedValue); + } + } + + private void enfoceDragParity(View v, String event, int update, int expectedValue) { + Object tag = v.getTag(R.id.drag_event_parity); + int value = tag == null ? 0 : (Integer) tag; + value += update; + v.setTag(R.id.drag_event_parity, value); + + if (value != expectedValue) { + Log.e(TAG, event + ": Drag contract violated: " + value); + } + } + void setCurrentDropLayout(CellLayout layout) { if (mDragTargetLayout != null) { mDragTargetLayout.revertTempState(); @@ -3473,8 +3006,7 @@ public class Workspace extends SmoothPagedView mTempPt[1] = y; mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempPt, true); - LauncherAppState app = LauncherAppState.getInstance(); - DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); + DeviceProfile grid = mLauncher.getDeviceProfile(); r = grid.getHotseatRect(); if (r.contains(mTempPt[0], mTempPt[1])) { return true; @@ -3568,38 +3100,6 @@ public class Workspace extends SmoothPagedView return bestMatchingScreen; } - // This is used to compute the visual center of the dragView. This point is then - // used to visualize drop locations and determine where to drop an item. The idea is that - // the visual center represents the user's interpretation of where the item is, and hence - // is the appropriate point to use when determining drop location. - private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, - DragView dragView, float[] recycle) { - float res[]; - if (recycle == null) { - res = new float[2]; - } else { - res = recycle; - } - - // First off, the drag view has been shifted in a way that is not represented in the - // x and y values or the x/yOffsets. Here we account for that shift. - x += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetX); - y += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY); - - // These represent the visual top and left of drag view if a dragRect was provided. - // If a dragRect was not provided, then they correspond to the actual view left and - // top, as the dragRect is in that case taken to be the entire dragView. - // R.dimen.dragViewOffsetY. - int left = x - xOffset; - int top = y - yOffset; - - // In order to find the visual center, we shift by half the dragRect - res[0] = left + dragView.getDragRegion().width() / 2; - res[1] = top + dragView.getDragRegion().height() / 2; - - return res; - } - private boolean isDragWidget(DragObject d) { return (d.dragInfo instanceof LauncherAppWidgetInfo || d.dragInfo instanceof PendingAddWidgetInfo); @@ -3624,8 +3124,7 @@ public class Workspace extends SmoothPagedView // Ensure that we have proper spans for the item that we are dropping if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found"); - mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, - d.dragView, mDragViewVisualCenter); + mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); final View child = (mDragInfo == null) ? null : mDragInfo.cell; // Identify whether we have dragged over a side page @@ -3700,7 +3199,7 @@ public class Workspace extends SmoothPagedView mTargetCell[1]); manageFolderFeedback(info, mDragTargetLayout, mTargetCell, - targetCellDistance, dragOverView); + targetCellDistance, dragOverView, d.accessibleDrag); boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX, @@ -3738,15 +3237,21 @@ public class Workspace extends SmoothPagedView } private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout, - int[] targetCell, float distance, View dragOverView) { + int[] targetCell, float distance, View dragOverView, boolean accessibleDrag) { boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance, false); - if (mDragMode == DRAG_MODE_NONE && userFolderPending && !mFolderCreationAlarm.alarmPending()) { - mFolderCreationAlarm.setOnAlarmListener(new - FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1])); - mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); + + FolderCreationAlarmListener listener = new + FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]); + + if (!accessibleDrag) { + mFolderCreationAlarm.setOnAlarmListener(listener); + mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT); + } else { + listener.onAlarm(mFolderCreationAlarm); + } return; } @@ -3950,26 +3455,14 @@ public class Workspace extends SmoothPagedView // When dragging and dropping from customization tray, we deal with creating // widgets/shortcuts/folders in a slightly different way - switch (pendingInfo.itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: - int span[] = new int[2]; - span[0] = item.spanX; - span[1] = item.spanY; - mLauncher.addAppWidgetFromDrop((PendingAddWidgetInfo) pendingInfo, - container, screenId, mTargetCell, span, null); - break; - case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: - mLauncher.processShortcutFromDrop(pendingInfo.componentName, - container, screenId, mTargetCell, null); - break; - default: - throw new IllegalStateException("Unknown item type: " + - pendingInfo.itemType); - } + mLauncher.addPendingItem(pendingInfo, container, screenId, mTargetCell, + item.spanX, item.spanY); } }; - View finalView = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET - ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; + boolean isWidget = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET + || pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + + View finalView = isWidget ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; if (finalView instanceof AppWidgetHostView && updateWidgetSize) { AppWidgetHostView awhv = (AppWidgetHostView) finalView; @@ -3978,7 +3471,7 @@ public class Workspace extends SmoothPagedView } int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR; - if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && + if (isWidget && ((PendingAddWidgetInfo) pendingInfo).info != null && ((PendingAddWidgetInfo) pendingInfo).info.configure != null) { animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN; } @@ -3993,10 +3486,9 @@ public class Workspace extends SmoothPagedView case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: if (info.container == NO_ID && info instanceof AppInfo) { // Came from all apps -- make a copy - info = new ShortcutInfo((AppInfo) info); + info = ((AppInfo) info).makeShortcut(); } - view = mLauncher.createShortcut(R.layout.application, cellLayout, - (ShortcutInfo) info); + view = mLauncher.createShortcut(cellLayout, (ShortcutInfo) info); break; case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout, @@ -4055,8 +3547,7 @@ public class Workspace extends SmoothPagedView } public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) { - int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo.spanX, - widgetInfo.spanY, widgetInfo, false); + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo, false); int visibility = layout.getVisibility(); layout.setVisibility(VISIBLE); @@ -4127,14 +3618,16 @@ public class Workspace extends SmoothPagedView // In the case where we've prebound the widget, we remove it from the DragLayer if (finalView instanceof AppWidgetHostView && external) { - Log.d(TAG, "6557954 Animate widget drop, final view is appWidgetHostView"); mLauncher.getDragLayer().removeView(finalView); } + + boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET || + info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external) && finalView != null) { Bitmap crossFadeBitmap = createWidgetBitmap(info, finalView); dragView.setCrossFadeBitmap(crossFadeBitmap); dragView.crossFade((int) (duration * 0.8f)); - } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && external) { + } else if (isWidget && external) { scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]); } @@ -4170,8 +3663,8 @@ public class Workspace extends SmoothPagedView public void setFinalTransitionTransform(CellLayout layout) { if (isSwitchingState()) { mCurrentScale = getScaleX(); - setScaleX(mNewScale); - setScaleY(mNewScale); + setScaleX(mStateTransitionAnimation.getFinalScale()); + setScaleY(mStateTransitionAnimation.getFinalScale()); } } public void resetTransitionTransform(CellLayout layout) { @@ -4208,7 +3701,7 @@ public class Workspace extends SmoothPagedView * * pixelX and pixelY should be in the coordinate system of layout */ - private int[] findNearestArea(int pixelX, int pixelY, + @Thunk int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, CellLayout layout, int[] recycle) { return layout.findNearestArea( pixelX, pixelY, spanX, spanY, recycle); @@ -4242,30 +3735,17 @@ public class Workspace extends SmoothPagedView if (success && !(beingCalledAfterUninstall && !mUninstallSuccessful)) { if (target != this && mDragInfo != null) { - CellLayout parentCell = getParentCellLayoutForView(mDragInfo.cell); - if (parentCell != null) { - parentCell.removeView(mDragInfo.cell); - } else if (LauncherAppState.isDogfoodBuild()) { - throw new NullPointerException("mDragInfo.cell has null parent"); - } - if (mDragInfo.cell instanceof DropTarget) { - mDragController.removeDropTarget((DropTarget) mDragInfo.cell); - } + removeWorkspaceItem(mDragInfo.cell); } } else if (mDragInfo != null) { - CellLayout cellLayout; - if (mLauncher.isHotseatLayout(target)) { - cellLayout = mLauncher.getHotseat().getLayout(); - } else { - cellLayout = getScreenWithId(mDragInfo.screenId); - } - if (cellLayout == null && LauncherAppState.isDogfoodBuild()) { - throw new RuntimeException("Invalid state: cellLayout == null in " - + "Workspace#onDropCompleted. Please file a bug. "); - } + final CellLayout cellLayout = mLauncher.getCellLayout( + mDragInfo.container, mDragInfo.screenId); if (cellLayout != null) { cellLayout.onDropChild(mDragInfo.cell); - } + } else if (LauncherAppState.isDogfoodBuild()) { + throw new RuntimeException("Invalid state: cellLayout == null in " + + "Workspace#onDropCompleted. Please file a bug. "); + }; } if ((d.cancelled || (beingCalledAfterUninstall && !mUninstallSuccessful)) && mDragInfo.cell != null) { @@ -4275,11 +3755,28 @@ public class Workspace extends SmoothPagedView mDragInfo = null; } + /** + * For opposite operation. See {@link #addInScreen}. + */ + public void removeWorkspaceItem(View v) { + CellLayout parentCell = getParentCellLayoutForView(v); + if (parentCell != null) { + parentCell.removeView(v); + } else if (LauncherAppState.isDogfoodBuild()) { + throw new NullPointerException("mDragInfo.cell has null parent"); + } + if (v instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) v); + } + } + + @Override public void deferCompleteDropAfterUninstallActivity() { mDeferDropAfterUninstall = true; } /// maybe move this into a smaller part + @Override public void onUninstallActivityReturned(boolean success) { mDeferDropAfterUninstall = false; mUninstallSuccessful = success; @@ -4311,88 +3808,6 @@ public class Workspace extends SmoothPagedView } } - ArrayList getUniqueComponents(boolean stripDuplicates, ArrayList duplicates) { - ArrayList uniqueIntents = new ArrayList(); - getUniqueIntents((CellLayout) mLauncher.getHotseat().getLayout(), uniqueIntents, duplicates, false); - int count = getChildCount(); - for (int i = 0; i < count; i++) { - CellLayout cl = (CellLayout) getChildAt(i); - getUniqueIntents(cl, uniqueIntents, duplicates, false); - } - return uniqueIntents; - } - - void getUniqueIntents(CellLayout cl, ArrayList uniqueIntents, - ArrayList duplicates, boolean stripDuplicates) { - int count = cl.getShortcutsAndWidgets().getChildCount(); - - ArrayList children = new ArrayList(); - for (int i = 0; i < count; i++) { - View v = cl.getShortcutsAndWidgets().getChildAt(i); - children.add(v); - } - - for (int i = 0; i < count; i++) { - View v = children.get(i); - ItemInfo info = (ItemInfo) v.getTag(); - // Null check required as the AllApps button doesn't have an item info - if (info instanceof ShortcutInfo) { - ShortcutInfo si = (ShortcutInfo) info; - ComponentName cn = si.intent.getComponent(); - - Uri dataUri = si.intent.getData(); - // If dataUri is not null / empty or if this component isn't one that would - // have previously showed up in the AllApps list, then this is a widget-type - // shortcut, so ignore it. - if (dataUri != null && !dataUri.equals(Uri.EMPTY)) { - continue; - } - - if (!uniqueIntents.contains(cn)) { - uniqueIntents.add(cn); - } else { - if (stripDuplicates) { - cl.removeViewInLayout(v); - LauncherModel.deleteItemFromDatabase(mLauncher, si); - } - if (duplicates != null) { - duplicates.add(cn); - } - } - } - if (v instanceof FolderIcon) { - FolderIcon fi = (FolderIcon) v; - ArrayList items = fi.getFolder().getItemsInReadingOrder(); - for (int j = 0; j < items.size(); j++) { - if (items.get(j).getTag() instanceof ShortcutInfo) { - ShortcutInfo si = (ShortcutInfo) items.get(j).getTag(); - ComponentName cn = si.intent.getComponent(); - - Uri dataUri = si.intent.getData(); - // If dataUri is not null / empty or if this component isn't one that would - // have previously showed up in the AllApps list, then this is a widget-type - // shortcut, so ignore it. - if (dataUri != null && !dataUri.equals(Uri.EMPTY)) { - continue; - } - - if (!uniqueIntents.contains(cn)) { - uniqueIntents.add(cn); - } else { - if (stripDuplicates) { - fi.getFolderInfo().remove(si); - LauncherModel.deleteItemFromDatabase(mLauncher, si); - } - if (duplicates != null) { - duplicates.add(cn); - } - } - } - } - } - } - } - void saveWorkspaceToDb() { saveWorkspaceScreenToDb((CellLayout) mLauncher.getHotseat().getLayout()); int count = getChildCount(); @@ -4425,8 +3840,7 @@ public class Workspace extends SmoothPagedView cellX = hotseat.getCellXFromOrder((int) info.screenId); cellY = hotseat.getCellYFromOrder((int) info.screenId); } - LauncherModel.addItemToDatabase(mLauncher, info, container, screenId, cellX, - cellY, false); + LauncherModel.addItemToDatabase(mLauncher, info, container, screenId, cellX, cellY); } if (v instanceof FolderIcon) { FolderIcon fi = (FolderIcon) v; @@ -4456,7 +3870,7 @@ public class Workspace extends SmoothPagedView } @Override - public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + public void onFlingToDelete(DragObject d, PointF vec) { // Do nothing } @@ -4469,12 +3883,6 @@ public class Workspace extends SmoothPagedView return true; } - @Override - protected void onRestoreInstanceState(Parcelable state) { - super.onRestoreInstanceState(state); - Launcher.setScreen(mCurrentPage); - } - @Override protected void dispatchRestoreInstanceState(SparseArray container) { // We don't dispatch restoreInstanceState to our children using this code path. @@ -4529,7 +3937,7 @@ public class Workspace extends SmoothPagedView @Override public boolean onEnterScrollArea(int x, int y, int direction) { // Ignore the scroll area if we are dragging over the hot seat - boolean isPortrait = !LauncherAppState.isScreenLandscape(getContext()); + boolean isPortrait = !mLauncher.getDeviceProfile().isLandscape; if (mLauncher.getHotseat() != null && isPortrait) { Rect r = new Rect(); mLauncher.getHotseat().getHitRect(r); @@ -4706,7 +4114,7 @@ public class Workspace extends SmoothPagedView && packageNames.contains(cn.getPackageName())) { shortcutInfo.isDisabled |= reason; BubbleTextView shortcut = (BubbleTextView) v; - shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache, true, false); + shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache); if (parent != null) { parent.invalidate(); @@ -4890,9 +4298,9 @@ public class Workspace extends SmoothPagedView updates.contains(info)) { ShortcutInfo si = (ShortcutInfo) info; BubbleTextView shortcut = (BubbleTextView) v; - boolean oldPromiseState = shortcut.getCompoundDrawables()[1] + boolean oldPromiseState = getTextViewIcon(shortcut) instanceof PreloadIconDrawable; - shortcut.applyFromShortcutInfo(si, mIconCache, true, + shortcut.applyFromShortcutInfo(si, mIconCache, si.isPromise() != oldPromiseState); if (parent != null) { @@ -4912,31 +4320,17 @@ public class Workspace extends SmoothPagedView removeItemsByPackageName(packages, user); } - public void updatePackageBadge(final String packageName, final UserHandleCompat user) { + public void updateRestoreItems(final HashSet updates) { mapOverItems(MAP_RECURSE, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View v, View parent) { - if (info instanceof ShortcutInfo && v instanceof BubbleTextView) { - ShortcutInfo shortcutInfo = (ShortcutInfo) info; - ComponentName cn = shortcutInfo.getTargetComponent(); - if (user.equals(shortcutInfo.user) && cn != null - && shortcutInfo.isPromise() - && packageName.equals(cn.getPackageName())) { - if (shortcutInfo.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) { - // For auto install apps update the icon as well as label. - mIconCache.getTitleAndIcon(shortcutInfo, - shortcutInfo.promisedIntent, user, true); - } else { - // Only update the icon for restored apps. - shortcutInfo.updateIcon(mIconCache); - } - BubbleTextView shortcut = (BubbleTextView) v; - shortcut.applyFromShortcutInfo(shortcutInfo, mIconCache, true, false); - - if (parent != null) { - parent.invalidate(); - } - } + if (info instanceof ShortcutInfo && v instanceof BubbleTextView + && updates.contains(info)) { + ((BubbleTextView) v).applyState(false); + } else if (v instanceof PendingAppWidgetHostView + && info instanceof LauncherAppWidgetInfo + && updates.contains(info)) { + ((PendingAppWidgetHostView) v).applyState(); } // process all the shortcuts return false; @@ -4944,48 +4338,13 @@ public class Workspace extends SmoothPagedView }); } - public void updatePackageState(ArrayList installInfos) { - for (final PackageInstallInfo installInfo : installInfos) { - if (installInfo.state == PackageInstallerCompat.STATUS_INSTALLED) { - continue; - } - - mapOverItems(MAP_RECURSE, new ItemOperator() { - @Override - public boolean evaluate(ItemInfo info, View v, View parent) { - if (info instanceof ShortcutInfo && v instanceof BubbleTextView) { - ShortcutInfo si = (ShortcutInfo) info; - ComponentName cn = si.getTargetComponent(); - if (si.isPromise() && (cn != null) - && installInfo.packageName.equals(cn.getPackageName())) { - si.setInstallProgress(installInfo.progress); - if (installInfo.state == PackageInstallerCompat.STATUS_FAILED) { - // Mark this info as broken. - si.status &= ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE; - } - ((BubbleTextView)v).applyState(false); - } - } else if (v instanceof PendingAppWidgetHostView - && info instanceof LauncherAppWidgetInfo - && ((LauncherAppWidgetInfo) info).providerName.getPackageName() - .equals(installInfo.packageName)) { - ((LauncherAppWidgetInfo) info).installProgress = installInfo.progress; - ((PendingAppWidgetHostView) v).applyState(); - } - - // process all the shortcuts - return false; - } - }); - } - } - void widgetsRestored(ArrayList changedInfo) { if (!changedInfo.isEmpty()) { DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo, mLauncher.getAppWidgetHost()); - if (LauncherModel.findAppWidgetProviderInfoWithComponent(getContext(), - changedInfo.get(0).providerName) != null) { + if (LauncherModel.getProviderInfo(getContext(), + changedInfo.get(0).providerName, + changedInfo.get(0).user) != null) { // Re-inflate the widgets which have changed status widgetRefresh.run(); } else { @@ -5049,25 +4408,21 @@ public class Workspace extends SmoothPagedView return super.getPageIndicatorMarker(pageIndex); } - @Override - public void syncPages() { - } - - @Override - public void syncPageItems(int page, boolean immediate) { - } - protected String getPageIndicatorDescription() { String settings = getResources().getString(R.string.settings_button_text); return getCurrentPageDescription() + ", " + settings; } protected String getCurrentPageDescription() { - int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; - int delta = numCustomPages(); if (hasCustomContent() && getNextPage() == 0) { return mCustomContentDescription; } + int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + return getPageDescription(page); + } + + private String getPageDescription(int page) { + int delta = numCustomPages(); return String.format(getContext().getString(R.string.workspace_scroll_format), page + 1 - delta, getChildCount() - delta); } @@ -5076,6 +4431,12 @@ public class Workspace extends SmoothPagedView mLauncher.getDragLayer().getLocationInDragLayer(this, loc); } + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_HOMESCREEN); + sourceData.putInt(Stats.SOURCE_EXTRA_CONTAINER_PAGE, getCurrentPage()); + } + /** * Used as a workaround to ensure that the AppWidgetService receives the * PACKAGE_ADDED broadcast before updating widgets. diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java new file mode 100644 index 0000000000..b8916a72bc --- /dev/null +++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.util.Thunk; + +import java.util.HashMap; + +/** + * A convenience class to update a view's visibility state after an alpha animation. + */ +class AlphaUpdateListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener { + private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f; + + private View mView; + private boolean mAccessibilityEnabled; + + public AlphaUpdateListener(View v, boolean accessibilityEnabled) { + mView = v; + mAccessibilityEnabled = accessibilityEnabled; + } + + @Override + public void onAnimationUpdate(ValueAnimator arg0) { + updateVisibility(mView, mAccessibilityEnabled); + } + + public static void updateVisibility(View view, boolean accessibilityEnabled) { + // We want to avoid the extra layout pass by setting the views to GONE unless + // accessibility is on, in which case not setting them to GONE causes a glitch. + int invisibleState = accessibilityEnabled ? View.GONE : View.INVISIBLE; + if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) { + view.setVisibility(invisibleState); + } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD + && view.getVisibility() != View.VISIBLE) { + view.setVisibility(View.VISIBLE); + } + } + + @Override + public void onAnimationEnd(Animator arg0) { + updateVisibility(mView, mAccessibilityEnabled); + } + + @Override + public void onAnimationStart(Animator arg0) { + // We want the views to be visible for animation, so fade-in/out is visible + mView.setVisibility(View.VISIBLE); + } +} + +/** + * This interpolator emulates the rate at which the perceived scale of an object changes + * as its distance from a camera increases. When this interpolator is applied to a scale + * animation on a view, it evokes the sense that the object is shrinking due to moving away + * from the camera. + */ +class ZInterpolator implements TimeInterpolator { + private float focalLength; + + public ZInterpolator(float foc) { + focalLength = foc; + } + + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); + } +} + +/** + * The exact reverse of ZInterpolator. + */ +class InverseZInterpolator implements TimeInterpolator { + private ZInterpolator zInterpolator; + public InverseZInterpolator(float foc) { + zInterpolator = new ZInterpolator(foc); + } + public float getInterpolation(float input) { + return 1 - zInterpolator.getInterpolation(1 - input); + } +} + +/** + * InverseZInterpolator compounded with an ease-out. + */ +class ZoomInInterpolator implements TimeInterpolator { + private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f); + private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f); + + public float getInterpolation(float input) { + return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input)); + } +} + +/** + * Stores the transition states for convenience. + */ +class TransitionStates { + + // Raw states + final boolean oldStateIsNormal; + final boolean oldStateIsSpringLoaded; + final boolean oldStateIsNormalHidden; + final boolean oldStateIsOverviewHidden; + final boolean oldStateIsOverview; + + final boolean stateIsNormal; + final boolean stateIsSpringLoaded; + final boolean stateIsNormalHidden; + final boolean stateIsOverviewHidden; + final boolean stateIsOverview; + + // Convenience members + final boolean workspaceToAllApps; + final boolean overviewToAllApps; + final boolean allAppsToWorkspace; + final boolean workspaceToOverview; + final boolean overviewToWorkspace; + + public TransitionStates(final Workspace.State fromState, final Workspace.State toState) { + oldStateIsNormal = (fromState == Workspace.State.NORMAL); + oldStateIsSpringLoaded = (fromState == Workspace.State.SPRING_LOADED); + oldStateIsNormalHidden = (fromState == Workspace.State.NORMAL_HIDDEN); + oldStateIsOverviewHidden = (fromState == Workspace.State.OVERVIEW_HIDDEN); + oldStateIsOverview = (fromState == Workspace.State.OVERVIEW); + + stateIsNormal = (toState == Workspace.State.NORMAL); + stateIsSpringLoaded = (toState == Workspace.State.SPRING_LOADED); + stateIsNormalHidden = (toState == Workspace.State.NORMAL_HIDDEN); + stateIsOverviewHidden = (toState == Workspace.State.OVERVIEW_HIDDEN); + stateIsOverview = (toState == Workspace.State.OVERVIEW); + + workspaceToOverview = (oldStateIsNormal && stateIsOverview); + workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden); + overviewToWorkspace = (oldStateIsOverview && stateIsNormal); + overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden); + allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal); + } +} + +/** + * Manages the animations between each of the workspace states. + */ +public class WorkspaceStateTransitionAnimation { + + public static final String TAG = "WorkspaceStateTransitionAnimation"; + + public static final int SCROLL_TO_CURRENT_PAGE = -1; + @Thunk static final int BACKGROUND_FADE_OUT_DURATION = 350; + + final @Thunk Launcher mLauncher; + final @Thunk Workspace mWorkspace; + + @Thunk AnimatorSet mStateAnimator; + @Thunk float[] mOldBackgroundAlphas; + @Thunk float[] mOldAlphas; + @Thunk float[] mNewBackgroundAlphas; + @Thunk float[] mNewAlphas; + @Thunk int mLastChildCount = -1; + + @Thunk float mCurrentScale; + @Thunk float mNewScale; + + @Thunk final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator(); + + @Thunk float mSpringLoadedShrinkFactor; + @Thunk float mOverviewModeShrinkFactor; + @Thunk float mWorkspaceScrimAlpha; + @Thunk int mAllAppsTransitionTime; + @Thunk int mOverviewTransitionTime; + @Thunk int mOverlayTransitionTime; + @Thunk boolean mWorkspaceFadeInAdjacentScreens; + + public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) { + mLauncher = launcher; + mWorkspace = workspace; + + DeviceProfile grid = mLauncher.getDeviceProfile(); + Resources res = launcher.getResources(); + mAllAppsTransitionTime = res.getInteger(R.integer.config_allAppsTransitionTime); + mOverviewTransitionTime = res.getInteger(R.integer.config_overviewTransitionTime); + mOverlayTransitionTime = res.getInteger(R.integer.config_overlayTransitionTime); + mSpringLoadedShrinkFactor = + res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100f; + mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f; + mOverviewModeShrinkFactor = grid.getOverviewModeScale(Utilities.isRtl(res)); + mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens(); + } + + public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState, + int toPage, boolean animated, boolean hasOverlaySearchBar, + HashMap layerViews) { + AccessibilityManager am = (AccessibilityManager) + mLauncher.getSystemService(Context.ACCESSIBILITY_SERVICE); + final boolean accessibilityEnabled = am.isEnabled(); + TransitionStates states = new TransitionStates(fromState, toState); + int duration = getAnimationDuration(states); + animateWorkspace(states, toPage, animated, duration, layerViews, + accessibilityEnabled); + animateSearchBar(states, animated, duration, hasOverlaySearchBar, layerViews, + accessibilityEnabled); + animateBackgroundGradient(states, animated, BACKGROUND_FADE_OUT_DURATION); + return mStateAnimator; + } + + public float getFinalScale() { + return mNewScale; + } + + /** + * Reinitializes the arrays that we need for the animations on each page. + */ + private void reinitializeAnimationArrays() { + final int childCount = mWorkspace.getChildCount(); + if (mLastChildCount == childCount) return; + + mOldBackgroundAlphas = new float[childCount]; + mOldAlphas = new float[childCount]; + mNewBackgroundAlphas = new float[childCount]; + mNewAlphas = new float[childCount]; + } + + /** + * Returns the proper animation duration for a transition. + */ + private int getAnimationDuration(TransitionStates states) { + if (states.workspaceToAllApps || states.overviewToAllApps) { + return mAllAppsTransitionTime; + } else if (states.workspaceToOverview || states.overviewToWorkspace) { + return mOverviewTransitionTime; + } else { + return mOverlayTransitionTime; + } + } + + /** + * Starts a transition animation for the workspace. + */ + private void animateWorkspace(final TransitionStates states, int toPage, final boolean animated, + final int duration, final HashMap layerViews, + final boolean accessibilityEnabled) { + // Reinitialize animation arrays for the current workspace state + reinitializeAnimationArrays(); + + // Cancel existing workspace animations and create a new animator set if requested + cancelAnimation(); + if (animated) { + mStateAnimator = LauncherAnimUtils.createAnimatorSet(); + } + + // Update the workspace state + float finalBackgroundAlpha = (states.stateIsSpringLoaded || states.stateIsOverview) ? + 1.0f : 0f; + float finalHotseatAndPageIndicatorAlpha = (states.stateIsNormal || states.stateIsSpringLoaded) ? + 1f : 0f; + float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f; + float finalWorkspaceTranslationY = states.stateIsOverview || states.stateIsOverviewHidden ? + mWorkspace.getOverviewModeTranslationY() : 0; + + final int childCount = mWorkspace.getChildCount(); + final int customPageCount = mWorkspace.numCustomPages(); + + mNewScale = 1.0f; + + if (states.oldStateIsOverview) { + mWorkspace.disableFreeScroll(); + } else if (states.stateIsOverview) { + mWorkspace.enableFreeScroll(); + } + + if (!states.stateIsNormal) { + if (states.stateIsSpringLoaded) { + mNewScale = mSpringLoadedShrinkFactor; + } else if (states.stateIsOverview || states.stateIsOverviewHidden) { + mNewScale = mOverviewModeShrinkFactor; + } + } + + if (toPage == SCROLL_TO_CURRENT_PAGE) { + toPage = mWorkspace.getPageNearestToCenterOfScreen(); + } + mWorkspace.snapToPage(toPage, duration, mZoomInInterpolator); + + for (int i = 0; i < childCount; i++) { + final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); + boolean isCurrentPage = (i == toPage); + float initialAlpha = cl.getShortcutsAndWidgets().getAlpha(); + float finalAlpha; + if (states.stateIsNormalHidden || states.stateIsOverviewHidden) { + finalAlpha = 0f; + } else if (states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) { + finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f; + } else { + finalAlpha = 1f; + } + + // If we are animating to/from the small state, then hide the side pages and fade the + // current page in + if (!mWorkspace.isSwitchingState()) { + if (states.workspaceToAllApps || states.allAppsToWorkspace) { + if (states.allAppsToWorkspace && isCurrentPage) { + initialAlpha = 0f; + } else if (!isCurrentPage) { + initialAlpha = finalAlpha = 0f; + } + cl.setShortcutAndWidgetAlpha(initialAlpha); + } + } + + mOldAlphas[i] = initialAlpha; + mNewAlphas[i] = finalAlpha; + if (animated) { + mOldBackgroundAlphas[i] = cl.getBackgroundAlpha(); + mNewBackgroundAlphas[i] = finalBackgroundAlpha; + } else { + cl.setBackgroundAlpha(finalBackgroundAlpha); + cl.setShortcutAndWidgetAlpha(finalAlpha); + } + } + + final ViewGroup overviewPanel = mLauncher.getOverviewPanel(); + final View hotseat = mLauncher.getHotseat(); + final View pageIndicator = mWorkspace.getPageIndicator(); + if (animated) { + LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mWorkspace); + scale.scaleX(mNewScale) + .scaleY(mNewScale) + .translationY(finalWorkspaceTranslationY) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + mStateAnimator.play(scale); + for (int index = 0; index < childCount; index++) { + final int i = index; + final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i); + float currentAlpha = cl.getShortcutsAndWidgets().getAlpha(); + if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) { + cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); + cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); + } else { + if (layerViews != null) { + layerViews.put(cl, LauncherStateTransitionAnimation.BUILD_LAYER); + } + if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) { + LauncherViewPropertyAnimator alphaAnim = + new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets()); + alphaAnim.alpha(mNewAlphas[i]) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + mStateAnimator.play(alphaAnim); + } + if (mOldBackgroundAlphas[i] != 0 || + mNewBackgroundAlphas[i] != 0) { + ValueAnimator bgAnim = ObjectAnimator.ofFloat(cl, "backgroundAlpha", + mOldBackgroundAlphas[i], mNewBackgroundAlphas[i]); + LauncherAnimUtils.ofFloat(cl, 0f, 1f); + bgAnim.setInterpolator(mZoomInInterpolator); + bgAnim.setDuration(duration); + mStateAnimator.play(bgAnim); + } + } + } + Animator pageIndicatorAlpha; + if (pageIndicator != null) { + pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator) + .alpha(finalHotseatAndPageIndicatorAlpha).withLayer(); + pageIndicatorAlpha.addListener(new AlphaUpdateListener(pageIndicator, + accessibilityEnabled)); + } else { + // create a dummy animation so we don't need to do null checks later + pageIndicatorAlpha = ValueAnimator.ofFloat(0, 0); + } + + LauncherViewPropertyAnimator hotseatAlpha = new LauncherViewPropertyAnimator(hotseat) + .alpha(finalHotseatAndPageIndicatorAlpha); + hotseatAlpha.addListener(new AlphaUpdateListener(hotseat, accessibilityEnabled)); + + LauncherViewPropertyAnimator overviewPanelAlpha = + new LauncherViewPropertyAnimator(overviewPanel).alpha(finalOverviewPanelAlpha); + overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel, + accessibilityEnabled)); + + // For animation optimations, we may need to provide the Launcher transition + // with a set of views on which to force build layers in certain scenarios. + hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null); + overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null); + if (layerViews != null) { + // If layerViews is not null, we add these views, and indicate that + // the caller can manage layer state. + layerViews.put(hotseat, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); + layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); + } else { + // Otherwise let the animator handle layer management. + hotseatAlpha.withLayer(); + overviewPanelAlpha.withLayer(); + } + + if (states.workspaceToOverview) { + pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2)); + hotseatAlpha.setInterpolator(new DecelerateInterpolator(2)); + overviewPanelAlpha.setInterpolator(null); + } else if (states.overviewToWorkspace) { + pageIndicatorAlpha.setInterpolator(null); + hotseatAlpha.setInterpolator(null); + overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2)); + } + + overviewPanelAlpha.setDuration(duration); + pageIndicatorAlpha.setDuration(duration); + hotseatAlpha.setDuration(duration); + + mStateAnimator.play(overviewPanelAlpha); + mStateAnimator.play(hotseatAlpha); + mStateAnimator.play(pageIndicatorAlpha); + mStateAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mStateAnimator = null; + + if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { + overviewPanel.getChildAt(0).performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + } + }); + } else { + overviewPanel.setAlpha(finalOverviewPanelAlpha); + AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled); + hotseat.setAlpha(finalHotseatAndPageIndicatorAlpha); + AlphaUpdateListener.updateVisibility(hotseat, accessibilityEnabled); + if (pageIndicator != null) { + pageIndicator.setAlpha(finalHotseatAndPageIndicatorAlpha); + AlphaUpdateListener.updateVisibility(pageIndicator, accessibilityEnabled); + } + mWorkspace.updateCustomContentVisibility(); + mWorkspace.setScaleX(mNewScale); + mWorkspace.setScaleY(mNewScale); + mWorkspace.setTranslationY(finalWorkspaceTranslationY); + + if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) { + overviewPanel.getChildAt(0).performAccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + } + } + + /** + * Coordinates with the workspace animation to animate the search bar. + * + * TODO: This should really be coordinated with the SearchDropTargetBar, otherwise the + * bar has no idea that it is hidden, and this has no idea what state the bar is + * actually in. + */ + private void animateSearchBar(TransitionStates states, boolean animated, int duration, + boolean hasOverlaySearchBar, final HashMap layerViews, + final boolean accessibilityEnabled) { + + // The search bar is only visible in the workspace + final View searchBar = mLauncher.getOrCreateQsbBar(); + if (searchBar != null) { + final boolean searchBarWillBeShown = states.stateIsNormal; + final float finalSearchBarAlpha = searchBarWillBeShown ? 1f : 0f; + if (animated) { + if (hasOverlaySearchBar) { + // If there is an overlay search bar, then we will coordinate with it. + mStateAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // If we are transitioning to a visible search bar, show it immediately + // and let the overlay search bar has faded out + if (searchBarWillBeShown) { + searchBar.setAlpha(finalSearchBarAlpha); + AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + // If we are transitioning to a hidden search bar, hide it only after + // the overlay search bar has faded in + if (!searchBarWillBeShown) { + searchBar.setAlpha(finalSearchBarAlpha); + AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); + } + } + }); + } else { + // Otherwise, we can just do the normal animation + LauncherViewPropertyAnimator searchBarAlpha = + new LauncherViewPropertyAnimator(searchBar).alpha(finalSearchBarAlpha); + searchBarAlpha.addListener(new AlphaUpdateListener(searchBar, + accessibilityEnabled)); + searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); + if (layerViews != null) { + // If layerViews is not null, we add these views, and indicate that + // the caller can manage layer state. + layerViews.put(searchBar, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); + } else { + // Otherwise let the animator handle layer management. + searchBarAlpha.withLayer(); + } + searchBarAlpha.setDuration(duration); + mStateAnimator.play(searchBarAlpha); + } + } else { + // Set the search bar state immediately + searchBar.setAlpha(finalSearchBarAlpha); + AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); + } + } + } + + /** + * Animates the background scrim. Add to the state animator to prevent jankiness. + * + * @param finalAlpha the final alpha for the background scrim + * @param animated whether or not to set the background alpha immediately + * @duration duration of the animation + */ + private void animateBackgroundGradient(TransitionStates states, + boolean animated, int duration) { + + final DragLayer dragLayer = mLauncher.getDragLayer(); + final float startAlpha = dragLayer.getBackgroundAlpha(); + float finalAlpha = states.stateIsNormal ? 0 : mWorkspaceScrimAlpha; + + if (finalAlpha != startAlpha) { + if (animated) { + // These properties refer to the background protection gradient used for AllApps + // and Widget tray. + ValueAnimator bgFadeOutAnimation = + LauncherAnimUtils.ofFloat(mWorkspace, startAlpha, finalAlpha); + bgFadeOutAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + dragLayer.setBackgroundAlpha( + ((Float)animation.getAnimatedValue()).floatValue()); + } + }); + bgFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); + bgFadeOutAnimation.setDuration(duration); + mStateAnimator.play(bgFadeOutAnimation); + } else { + dragLayer.setBackgroundAlpha(finalAlpha); + } + } + } + + /** + * Cancels the current animation. + */ + private void cancelAnimation() { + if (mStateAnimator != null) { + mStateAnimator.setDuration(0); + mStateAnimator.cancel(); + } + mStateAnimator = null; + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java new file mode 100644 index 0000000000..78accf7201 --- /dev/null +++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.accessibility; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.accessibility.AccessibilityEvent; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; + +import java.util.List; + +/** + * Helper class to make drag-and-drop in a {@link CellLayout} accessible. + */ +public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper + implements OnClickListener { + protected static final int INVALID_POSITION = -1; + + private static final int[] sTempArray = new int[2]; + + protected final CellLayout mView; + protected final Context mContext; + protected final LauncherAccessibilityDelegate mDelegate; + + private final Rect mTempRect = new Rect(); + + public DragAndDropAccessibilityDelegate(CellLayout forView) { + super(forView); + mView = forView; + mContext = mView.getContext(); + mDelegate = LauncherAppState.getInstance().getAccessibilityDelegate(); + } + + @Override + protected int getVirtualViewAt(float x, float y) { + if (x < 0 || y < 0 || x > mView.getMeasuredWidth() || y > mView.getMeasuredHeight()) { + return INVALID_ID; + } + mView.pointToCellExact((int) x, (int) y, sTempArray); + + // Map cell to id + int id = sTempArray[0] + sTempArray[1] * mView.getCountX(); + return intersectsValidDropTarget(id); + } + + /** + * @return the view id of the top left corner of a valid drop region or + * {@link #INVALID_POSITION} if there is no such valid region. + */ + protected abstract int intersectsValidDropTarget(int id); + + @Override + protected void getVisibleVirtualViews(List virtualViews) { + // We create a virtual view for each cell of the grid + // The cell ids correspond to cells in reading order. + int nCells = mView.getCountX() * mView.getCountY(); + + for (int i = 0; i < nCells; i++) { + if (intersectsValidDropTarget(i) == i) { + virtualViews.add(i); + } + } + } + + @Override + protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) { + if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && viewId != INVALID_ID) { + String confirmation = getConfirmationForIconDrop(viewId); + mDelegate.handleAccessibleDrop(mView, getItemBounds(viewId), confirmation); + return true; + } + return false; + } + + @Override + public void onClick(View v) { + onPerformActionForVirtualView(getFocusedVirtualView(), + AccessibilityNodeInfoCompat.ACTION_CLICK, null); + } + + @Override + protected void onPopulateEventForVirtualView(int id, AccessibilityEvent event) { + if (id == INVALID_ID) { + throw new IllegalArgumentException("Invalid virtual view id"); + } + event.setContentDescription(mContext.getString(R.string.action_move_here)); + } + + @Override + protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) { + if (id == INVALID_ID) { + throw new IllegalArgumentException("Invalid virtual view id"); + } + + node.setContentDescription(getLocationDescriptionForIconDrop(id)); + node.setBoundsInParent(getItemBounds(id)); + + node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + node.setClickable(true); + node.setFocusable(true); + } + + protected abstract String getLocationDescriptionForIconDrop(int id); + + protected abstract String getConfirmationForIconDrop(int id); + + private Rect getItemBounds(int id) { + int cellX = id % mView.getCountX(); + int cellY = id / mView.getCountX(); + LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo(); + mView.cellToRect(cellX, cellY, dragInfo.info.spanX, dragInfo.info.spanY, mTempRect); + return mTempRect; + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java b/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java new file mode 100644 index 0000000000..ff99890368 --- /dev/null +++ b/src/com/android/launcher3/accessibility/FolderAccessibilityHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.accessibility; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.FolderPagedView; +import com.android.launcher3.R; + +/** + * Implementation of {@link DragAndDropAccessibilityDelegate} to support DnD in a folder. + */ +public class FolderAccessibilityHelper extends DragAndDropAccessibilityDelegate { + + /** + * 0-index position for the first cell in {@link #mView} in {@link #mParent}. + */ + private final int mStartPosition; + + private final FolderPagedView mParent; + + public FolderAccessibilityHelper(CellLayout layout) { + super(layout); + mParent = (FolderPagedView) layout.getParent(); + + int index = mParent.indexOfChild(layout); + mStartPosition = index * layout.getCountX() * layout.getCountY(); + } + @Override + protected int intersectsValidDropTarget(int id) { + return Math.min(id, mParent.getAllocatedContentSize() - mStartPosition - 1); + } + + @Override + protected String getLocationDescriptionForIconDrop(int id) { + return mContext.getString(R.string.move_to_position, id + mStartPosition + 1); + } + + @Override + protected String getConfirmationForIconDrop(int id) { + return mContext.getString(R.string.item_moved); + } +} diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java new file mode 100644 index 0000000000..fe7b25edd3 --- /dev/null +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -0,0 +1,434 @@ +package com.android.launcher3.accessibility; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.appwidget.AppWidgetProviderInfo; +import android.content.DialogInterface; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.AppWidgetResizeFrame; +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeleteDropTarget; +import com.android.launcher3.DragController.DragListener; +import com.android.launcher3.DragSource; +import com.android.launcher3.Folder; +import com.android.launcher3.FolderInfo; +import com.android.launcher3.InfoDropTarget; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppWidgetHostView; +import com.android.launcher3.LauncherAppWidgetInfo; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.UninstallDropTarget; +import com.android.launcher3.Workspace; +import com.android.launcher3.util.Thunk; + +import java.util.ArrayList; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener { + + private static final String TAG = "LauncherAccessibilityDelegate"; + + private static final int REMOVE = R.id.action_remove; + private static final int INFO = R.id.action_info; + private static final int UNINSTALL = R.id.action_uninstall; + private static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace; + private static final int MOVE = R.id.action_move; + private static final int MOVE_TO_WORKSPACE = R.id.action_move_to_workspace; + private static final int RESIZE = R.id.action_resize; + + public enum DragType { + ICON, + FOLDER, + WIDGET + } + + public static class DragInfo { + public DragType dragType; + public ItemInfo info; + public View item; + } + + private final SparseArray mActions = new SparseArray<>(); + @Thunk final Launcher mLauncher; + + private DragInfo mDragInfo = null; + private AccessibilityDragSource mDragSource = null; + + public LauncherAccessibilityDelegate(Launcher launcher) { + mLauncher = launcher; + + mActions.put(REMOVE, new AccessibilityAction(REMOVE, + launcher.getText(R.string.delete_target_label))); + mActions.put(INFO, new AccessibilityAction(INFO, + launcher.getText(R.string.info_target_label))); + mActions.put(UNINSTALL, new AccessibilityAction(UNINSTALL, + launcher.getText(R.string.delete_target_uninstall_label))); + mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE, + launcher.getText(R.string.action_add_to_workspace))); + mActions.put(MOVE, new AccessibilityAction(MOVE, + launcher.getText(R.string.action_move))); + mActions.put(MOVE_TO_WORKSPACE, new AccessibilityAction(MOVE_TO_WORKSPACE, + launcher.getText(R.string.action_move_to_workspace))); + mActions.put(RESIZE, new AccessibilityAction(RESIZE, + launcher.getText(R.string.action_resize))); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (!(host.getTag() instanceof ItemInfo)) return; + ItemInfo item = (ItemInfo) host.getTag(); + + if (DeleteDropTarget.supportsDrop(item)) { + info.addAction(mActions.get(REMOVE)); + } + if (UninstallDropTarget.supportsDrop(host.getContext(), item)) { + info.addAction(mActions.get(UNINSTALL)); + } + if (InfoDropTarget.supportsDrop(host.getContext(), item)) { + info.addAction(mActions.get(INFO)); + } + + if ((item instanceof ShortcutInfo) + || (item instanceof LauncherAppWidgetInfo) + || (item instanceof FolderInfo)) { + info.addAction(mActions.get(MOVE)); + + if (item.container >= 0) { + info.addAction(mActions.get(MOVE_TO_WORKSPACE)); + } else if (item instanceof LauncherAppWidgetInfo) { + if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) { + info.addAction(mActions.get(RESIZE)); + } + } + } if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) { + info.addAction(mActions.get(ADD_TO_WORKSPACE)); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if ((host.getTag() instanceof ItemInfo) + && performAction(host, (ItemInfo) host.getTag(), action)) { + return true; + } + return super.performAccessibilityAction(host, action, args); + } + + public boolean performAction(final View host, final ItemInfo item, int action) { + if (action == REMOVE) { + if (DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host)) { + announceConfirmation(R.string.item_removed); + return true; + } + return false; + } else if (action == INFO) { + InfoDropTarget.startDetailsActivityForInfo(item, mLauncher); + return true; + } else if (action == UNINSTALL) { + return UninstallDropTarget.startUninstallActivity(mLauncher, item); + } else if (action == MOVE) { + beginAccessibleDrag(host, item); + } else if (action == ADD_TO_WORKSPACE) { + final int[] coordinates = new int[2]; + final long screenId = findSpaceOnWorkspace(item, coordinates); + mLauncher.showWorkspace(true, new Runnable() { + + @Override + public void run() { + if (item instanceof AppInfo) { + ShortcutInfo info = ((AppInfo) item).makeShortcut(); + LauncherModel.addItemToDatabase(mLauncher, info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates[0], coordinates[1]); + + ArrayList itemList = new ArrayList<>(); + itemList.add(info); + mLauncher.bindItems(itemList, 0, itemList.size(), true); + } else if (item instanceof PendingAddItemInfo) { + PendingAddItemInfo info = (PendingAddItemInfo) item; + Workspace workspace = mLauncher.getWorkspace(); + workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)); + mLauncher.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates, info.spanX, info.spanY); + } + announceConfirmation(R.string.item_added_to_workspace); + } + }); + return true; + } else if (action == MOVE_TO_WORKSPACE) { + Folder folder = mLauncher.getWorkspace().getOpenFolder(); + mLauncher.closeFolder(folder); + ShortcutInfo info = (ShortcutInfo) item; + folder.getInfo().remove(info); + + final int[] coordinates = new int[2]; + final long screenId = findSpaceOnWorkspace(item, coordinates); + LauncherModel.moveItemInDatabase(mLauncher, info, + LauncherSettings.Favorites.CONTAINER_DESKTOP, + screenId, coordinates[0], coordinates[1]); + + // Bind the item in next frame so that if a new workspace page was created, + // it will get laid out. + new Handler().post(new Runnable() { + + @Override + public void run() { + ArrayList itemList = new ArrayList<>(); + itemList.add(item); + mLauncher.bindItems(itemList, 0, itemList.size(), true); + announceConfirmation(R.string.item_moved); + } + }); + } else if (action == RESIZE) { + final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item; + final ArrayList actions = getSupportedResizeActions(host, info); + CharSequence[] labels = new CharSequence[actions.size()]; + for (int i = 0; i < actions.size(); i++) { + labels[i] = mLauncher.getText(actions.get(i)); + } + + new AlertDialog.Builder(mLauncher) + .setTitle(R.string.action_resize) + .setItems(labels, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + performResizeAction(actions.get(which), host, info); + dialog.dismiss(); + } + }) + .show(); + } + return false; + } + + private ArrayList getSupportedResizeActions(View host, LauncherAppWidgetInfo info) { + ArrayList actions = new ArrayList<>(); + + AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo(); + if (providerInfo == null) { + return actions; + } + + CellLayout layout = (CellLayout) host.getParent().getParent(); + if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) { + if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) || + layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) { + actions.add(R.string.action_increase_width); + } + + if (info.spanX > info.minSpanX && info.spanX > 1) { + actions.add(R.string.action_decrease_width); + } + } + + if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) { + if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) || + layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) { + actions.add(R.string.action_increase_height); + } + + if (info.spanY > info.minSpanY && info.spanY > 1) { + actions.add(R.string.action_decrease_height); + } + } + return actions; + } + + @Thunk void performResizeAction(int action, View host, LauncherAppWidgetInfo info) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams(); + CellLayout layout = (CellLayout) host.getParent().getParent(); + layout.markCellsAsUnoccupiedForView(host); + + if (action == R.string.action_increase_width) { + if (((host.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) + && layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) + || !layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY)) { + lp.cellX --; + info.cellX --; + } + lp.cellHSpan ++; + info.spanX ++; + } else if (action == R.string.action_decrease_width) { + lp.cellHSpan --; + info.spanX --; + } else if (action == R.string.action_increase_height) { + if (!layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1)) { + lp.cellY --; + info.cellY --; + } + lp.cellVSpan ++; + info.spanY ++; + } else if (action == R.string.action_decrease_height) { + lp.cellVSpan --; + info.spanY --; + } + + layout.markCellsAsOccupiedForView(host); + Rect sizeRange = new Rect(); + AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, sizeRange); + ((LauncherAppWidgetHostView) host).updateAppWidgetSize(null, + sizeRange.left, sizeRange.top, sizeRange.right, sizeRange.bottom); + host.requestLayout(); + LauncherModel.updateItemInDatabase(mLauncher, info); + announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY)); + } + + @Thunk void announceConfirmation(int resId) { + announceConfirmation(mLauncher.getResources().getString(resId)); + } + + @Thunk void announceConfirmation(String confirmation) { + mLauncher.getDragLayer().announceForAccessibility(confirmation); + + } + + public boolean isInAccessibleDrag() { + return mDragInfo != null; + } + + public DragInfo getDragInfo() { + return mDragInfo; + } + + /** + * @param clickedTarget the actual view that was clicked + * @param dropLocation relative to {@param clickedTarget}. If provided, its center is used + * as the actual drop location otherwise the views center is used. + */ + public void handleAccessibleDrop(View clickedTarget, Rect dropLocation, + String confirmation) { + if (!isInAccessibleDrag()) return; + + int[] loc = new int[2]; + if (dropLocation == null) { + loc[0] = clickedTarget.getWidth() / 2; + loc[1] = clickedTarget.getHeight() / 2; + } else { + loc[0] = dropLocation.centerX(); + loc[1] = dropLocation.centerY(); + } + + mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(clickedTarget, loc); + mLauncher.getDragController().completeAccessibleDrag(loc); + + if (!TextUtils.isEmpty(confirmation)) { + announceConfirmation(confirmation); + } + } + + public void beginAccessibleDrag(View item, ItemInfo info) { + mDragInfo = new DragInfo(); + mDragInfo.info = info; + mDragInfo.item = item; + mDragInfo.dragType = DragType.ICON; + if (info instanceof FolderInfo) { + mDragInfo.dragType = DragType.FOLDER; + } else if (info instanceof LauncherAppWidgetInfo) { + mDragInfo.dragType = DragType.WIDGET; + } + + CellLayout.CellInfo cellInfo = new CellLayout.CellInfo(item, info); + + Rect pos = new Rect(); + mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos); + mLauncher.getDragController().prepareAccessibleDrag(pos.centerX(), pos.centerY()); + + Workspace workspace = mLauncher.getWorkspace(); + + Folder folder = workspace.getOpenFolder(); + if (folder != null) { + if (folder.getItemsInReadingOrder().contains(item)) { + mDragSource = folder; + } else { + mLauncher.closeFolder(); + } + } + if (mDragSource == null) { + mDragSource = workspace; + } + mDragSource.enableAccessibleDrag(true); + mDragSource.startDrag(cellInfo, true); + + if (mLauncher.getDragController().isDragging()) { + mLauncher.getDragController().addDragListener(this); + } + } + + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { + // No-op + } + + @Override + public void onDragEnd() { + mLauncher.getDragController().removeDragListener(this); + mDragInfo = null; + if (mDragSource != null) { + mDragSource.enableAccessibleDrag(false); + mDragSource = null; + } + } + + public static interface AccessibilityDragSource { + void startDrag(CellLayout.CellInfo cellInfo, boolean accessible); + + void enableAccessibleDrag(boolean enable); + } + + /** + * Find empty space on the workspace and returns the screenId. + */ + private long findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) { + Workspace workspace = mLauncher.getWorkspace(); + ArrayList workspaceScreens = workspace.getScreenOrder(); + long screenId; + + // First check if there is space on the current screen. + int screenIndex = workspace.getCurrentPage(); + screenId = workspaceScreens.get(screenIndex); + CellLayout layout = (CellLayout) workspace.getPageAt(screenIndex); + + boolean found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); + screenIndex = workspace.hasCustomContent() ? 1 : 0; + while (!found && screenIndex < workspaceScreens.size()) { + screenId = workspaceScreens.get(screenIndex); + layout = (CellLayout) workspace.getPageAt(screenIndex); + found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); + screenIndex++; + } + + if (found) { + return screenId; + } + + workspace.addExtraEmptyScreen(); + screenId = workspace.commitExtraEmptyScreen(); + layout = workspace.getScreenWithId(screenId); + found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); + + if (!found) { + Log.wtf(TAG, "Not enough space on an empty screen"); + } + return screenId; + } +} diff --git a/src/com/android/launcher3/accessibility/OverviewScreenAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/OverviewScreenAccessibilityDelegate.java new file mode 100644 index 0000000000..c5b52de720 --- /dev/null +++ b/src/com/android/launcher3/accessibility/OverviewScreenAccessibilityDelegate.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.accessibility; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.Workspace; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class OverviewScreenAccessibilityDelegate extends AccessibilityDelegate { + + private static final int MOVE_BACKWARD = R.id.action_move_screen_backwards; + private static final int MOVE_FORWARD = R.id.action_move_screen_forwards; + + private final SparseArray mActions = new SparseArray<>(); + private final Workspace mWorkspace; + + public OverviewScreenAccessibilityDelegate(Workspace workspace) { + mWorkspace = workspace; + + Context context = mWorkspace.getContext(); + boolean isRtl = Utilities.isRtl(context.getResources()); + mActions.put(MOVE_BACKWARD, new AccessibilityAction(MOVE_BACKWARD, + context.getText(isRtl ? R.string.action_move_screen_right : + R.string.action_move_screen_left))); + mActions.put(MOVE_FORWARD, new AccessibilityAction(MOVE_FORWARD, + context.getText(isRtl ? R.string.action_move_screen_left : + R.string.action_move_screen_right))); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (host != null) { + if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS ) { + int index = mWorkspace.indexOfChild(host); + mWorkspace.setCurrentPage(index); + } else if (action == MOVE_FORWARD) { + movePage(mWorkspace.indexOfChild(host) + 1, host); + return true; + } else if (action == MOVE_BACKWARD) { + movePage(mWorkspace.indexOfChild(host) - 1, host); + return true; + } + } + + return super.performAccessibilityAction(host, action, args); + } + + private void movePage(int finalIndex, View view) { + mWorkspace.onStartReordering(); + mWorkspace.removeView(view); + mWorkspace.addView(view, finalIndex); + mWorkspace.onEndReordering(); + mWorkspace.announceForAccessibility(mWorkspace.getContext().getText(R.string.screen_moved)); + + mWorkspace.updateAccessibilityFlags(); + view.performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + int index = mWorkspace.indexOfChild(host); + if (index < mWorkspace.getChildCount() - 1) { + info.addAction(mActions.get(MOVE_FORWARD)); + } + if (index > mWorkspace.numCustomPages()) { + info.addAction(mActions.get(MOVE_BACKWARD)); + } + } +} diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java new file mode 100644 index 0000000000..80ddc13b7c --- /dev/null +++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.accessibility; + +import android.text.TextUtils; +import android.view.View; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.CellLayout; +import com.android.launcher3.FolderInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DragType; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; + +/** + * Implementation of {@link DragAndDropAccessibilityDelegate} to support DnD on workspace. + */ +public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelegate { + + public WorkspaceAccessibilityHelper(CellLayout layout) { + super(layout); + } + + /** + * Find the virtual view id corresponding to the top left corner of any drop region by which + * the passed id is contained. For an icon, this is simply + */ + @Override + protected int intersectsValidDropTarget(int id) { + int mCountX = mView.getCountX(); + int mCountY = mView.getCountY(); + + int x = id % mCountX; + int y = id / mCountX; + LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo(); + + if (dragInfo.dragType == DragType.WIDGET && mView.isHotseat()) { + return INVALID_POSITION; + } + + if (dragInfo.dragType == DragType.WIDGET) { + // For a widget, every cell must be vacant. In addition, we will return any valid + // drop target by which the passed id is contained. + boolean fits = false; + + // These represent the amount that we can back off if we hit a problem. They + // get consumed as we move up and to the right, trying new regions. + int spanX = dragInfo.info.spanX; + int spanY = dragInfo.info.spanY; + + for (int m = 0; m < spanX; m++) { + for (int n = 0; n < spanY; n++) { + + fits = true; + int x0 = x - m; + int y0 = y - n; + + if (x0 < 0 || y0 < 0) continue; + + for (int i = x0; i < x0 + spanX; i++) { + if (!fits) break; + for (int j = y0; j < y0 + spanY; j++) { + if (i >= mCountX || j >= mCountY || mView.isOccupied(i, j)) { + fits = false; + break; + } + } + } + if (fits) { + return x0 + mCountX * y0; + } + } + } + return INVALID_POSITION; + } else { + // For an icon, we simply check the view directly below + View child = mView.getChildAt(x, y); + if (child == null || child == dragInfo.item) { + // Empty cell. Good for an icon or folder. + return id; + } else if (dragInfo.dragType != DragType.FOLDER) { + // For icons, we can consider cells that have another icon or a folder. + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof AppInfo || info instanceof FolderInfo || + info instanceof ShortcutInfo) { + return id; + } + } + return INVALID_POSITION; + } + } + + @Override + protected String getConfirmationForIconDrop(int id) { + int x = id % mView.getCountX(); + int y = id / mView.getCountX(); + LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo(); + + View child = mView.getChildAt(x, y); + if (child == null || child == dragInfo.item) { + return mContext.getString(R.string.item_moved); + } else { + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof AppInfo || info instanceof ShortcutInfo) { + return mContext.getString(R.string.folder_created); + + } else if (info instanceof FolderInfo) { + return mContext.getString(R.string.added_to_folder); + } + } + return ""; + } + + @Override + protected String getLocationDescriptionForIconDrop(int id) { + int x = id % mView.getCountX(); + int y = id / mView.getCountX(); + LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo(); + + View child = mView.getChildAt(x, y); + if (child == null || child == dragInfo.item) { + if (mView.isHotseat()) { + return mContext.getString(R.string.move_to_hotseat_position, id + 1); + } else { + return mContext.getString(R.string.move_to_empty_cell, y + 1, x + 1); + } + } else { + ItemInfo info = (ItemInfo) child.getTag(); + if (info instanceof ShortcutInfo) { + return mContext.getString(R.string.create_folder_with, info.title); + } else if (info instanceof FolderInfo) { + if (TextUtils.isEmpty(info.title)) { + // Find the first item in the folder. + FolderInfo folder = (FolderInfo) info; + ShortcutInfo firstItem = null; + for (ShortcutInfo shortcut : folder.contents) { + if (firstItem == null || firstItem.rank > shortcut.rank) { + firstItem = shortcut; + } + } + + if (firstItem != null) { + return mContext.getString(R.string.add_to_folder_with_app, firstItem.title); + } + } + return mContext.getString(R.string.add_to_folder, info.title); + } + } + return ""; + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java new file mode 100644 index 0000000000..67d572819a --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -0,0 +1,638 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.text.Selection; +import android.text.SpannableStringBuilder; +import android.text.method.TextKeyListener; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.BaseContainerView; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.CellLayout; +import com.android.launcher3.CheckLongPressHelper; +import com.android.launcher3.DeleteDropTarget; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget; +import com.android.launcher3.Folder; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherTransitionable; +import com.android.launcher3.R; +import com.android.launcher3.Stats; +import com.android.launcher3.Utilities; +import com.android.launcher3.Workspace; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.Thunk; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.List; + + + +/** + * A merge algorithm that merges every section indiscriminately. + */ +final class FullMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm { + + @Override + public boolean continueMerging(AlphabeticalAppsList.SectionInfo section, + AlphabeticalAppsList.SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount) { + // Don't merge the predicted apps + if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { + return false; + } + // Otherwise, merge every other section + return true; + } +} + +/** + * The logic we use to merge multiple sections. We only merge sections when their final row + * contains less than a certain number of icons, and stop at a specified max number of merges. + * In addition, we will try and not merge sections that identify apps from different scripts. + */ +final class SimpleSectionMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm { + + private int mMinAppsPerRow; + private int mMinRowsInMergedSection; + private int mMaxAllowableMerges; + private CharsetEncoder mAsciiEncoder; + + public SimpleSectionMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) { + mMinAppsPerRow = minAppsPerRow; + mMinRowsInMergedSection = minRowsInMergedSection; + mMaxAllowableMerges = maxNumMerges; + mAsciiEncoder = Charset.forName("US-ASCII").newEncoder(); + } + + @Override + public boolean continueMerging(AlphabeticalAppsList.SectionInfo section, + AlphabeticalAppsList.SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount) { + // Don't merge the predicted apps + if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { + return false; + } + + // Continue merging if the number of hanging apps on the final row is less than some + // fixed number (ragged), the merged rows has yet to exceed some minimum row count, + // and while the number of merged sections is less than some fixed number of merges + int rows = sectionAppCount / numAppsPerRow; + int cols = sectionAppCount % numAppsPerRow; + + // Ensure that we do not merge across scripts, currently we only allow for english and + // native scripts so we can test if both can just be ascii encoded + boolean isCrossScript = false; + if (section.firstAppItem != null && withSection.firstAppItem != null) { + isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) != + mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName); + } + return (0 < cols && cols < mMinAppsPerRow) && + rows < mMinRowsInMergedSection && + mergeCount < mMaxAllowableMerges && + !isCrossScript; + } +} + +/** + * The all apps view container. + */ +public class AllAppsContainerView extends BaseContainerView implements DragSource, + LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener, + AllAppsSearchBarController.Callbacks { + + private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3; + private static final int MAX_NUM_MERGES_PHONE = 2; + + @Thunk Launcher mLauncher; + @Thunk AlphabeticalAppsList mApps; + private AllAppsGridAdapter mAdapter; + private RecyclerView.LayoutManager mLayoutManager; + private RecyclerView.ItemDecoration mItemDecoration; + + @Thunk View mContent; + @Thunk View mContainerView; + @Thunk View mRevealView; + @Thunk AllAppsRecyclerView mAppsRecyclerView; + @Thunk AllAppsSearchBarController mSearchBarController; + private ViewGroup mSearchBarContainerView; + private View mSearchBarView; + + private int mSectionNamesMargin; + private int mNumAppsPerRow; + private int mNumPredictedAppsPerRow; + private int mRecyclerViewTopBottomPadding; + // This coordinate is relative to this container view + private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1); + // This coordinate is relative to its parent + private final Point mIconLastTouchPos = new Point(); + + private SpannableStringBuilder mSearchQueryBuilder = null; + + public AllAppsContainerView(Context context) { + this(context, null); + } + + public AllAppsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + Resources res = context.getResources(); + + mLauncher = (Launcher) context; + mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); + mApps = new AlphabeticalAppsList(context); + mAdapter = new AllAppsGridAdapter(context, mApps, this, mLauncher, this); + mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message)); + mApps.setAdapter(mAdapter); + mLayoutManager = mAdapter.getLayoutManager(); + mItemDecoration = mAdapter.getItemDecoration(); + mRecyclerViewTopBottomPadding = + res.getDimensionPixelSize(R.dimen.all_apps_list_top_bottom_padding); + + mSearchQueryBuilder = new SpannableStringBuilder(); + Selection.setSelection(mSearchQueryBuilder, 0); + } + + /** + * Sets the current set of predicted apps. + */ + public void setPredictedApps(List apps) { + mApps.setPredictedApps(apps); + } + + /** + * Sets the current set of apps. + */ + public void setApps(List apps) { + mApps.setApps(apps); + } + + /** + * Adds new apps to the list. + */ + public void addApps(List apps) { + mApps.addApps(apps); + } + + /** + * Updates existing apps in the list + */ + public void updateApps(List apps) { + mApps.updateApps(apps); + } + + /** + * Removes some apps from the list. + */ + public void removeApps(List apps) { + mApps.removeApps(apps); + } + + /** + * Sets the search bar that shows above the a-z list. + */ + public void setSearchBarController(AllAppsSearchBarController searchController) { + if (mSearchBarController != null) { + throw new RuntimeException("Expected search bar controller to only be set once"); + } + mSearchBarController = searchController; + mSearchBarController.initialize(mApps, this); + + // Add the new search view to the layout + View searchBarView = searchController.getView(mSearchBarContainerView); + mSearchBarContainerView.addView(searchBarView); + mSearchBarContainerView.setVisibility(View.VISIBLE); + mSearchBarView = searchBarView; + setHasSearchBar(); + + updateBackgroundAndPaddings(); + } + + /** + * Scrolls this list view to the top. + */ + public void scrollToTop() { + mAppsRecyclerView.scrollToTop(); + } + + /** + * Returns the content view used for the launcher transitions. + */ + public View getContentView() { + return mContainerView; + } + + /** + * Returns the all apps search view. + */ + public View getSearchBarView() { + return mSearchBarView; + } + + /** + * Returns the reveal view used for the launcher transitions. + */ + public View getRevealView() { + return mRevealView; + } + + /** + * Returns an new instance of the default app search controller. + */ + public AllAppsSearchBarController newDefaultAppSearchController() { + return new DefaultAppSearchController(getContext(), this, mAppsRecyclerView); + } + + /** + * Focuses the search field and begins an app search. + */ + public void startAppsSearch() { + if (mSearchBarController != null) { + mSearchBarController.focusSearchField(); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + boolean isRtl = Utilities.isRtl(getResources()); + mAdapter.setRtl(isRtl); + mContent = findViewById(R.id.content); + + // This is a focus listener that proxies focus from a view into the list view. This is to + // work around the search box from getting first focus and showing the cursor. + View.OnFocusChangeListener focusProxyListener = new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + mAppsRecyclerView.requestFocus(); + } + } + }; + mSearchBarContainerView = (ViewGroup) findViewById(R.id.search_box_container); + mSearchBarContainerView.setOnFocusChangeListener(focusProxyListener); + mContainerView = findViewById(R.id.all_apps_container); + mContainerView.setOnFocusChangeListener(focusProxyListener); + mRevealView = findViewById(R.id.all_apps_reveal); + + // Load the all apps recycler view + mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view); + mAppsRecyclerView.setApps(mApps); + mAppsRecyclerView.setLayoutManager(mLayoutManager); + mAppsRecyclerView.setAdapter(mAdapter); + mAppsRecyclerView.setHasFixedSize(true); + if (mItemDecoration != null) { + mAppsRecyclerView.addItemDecoration(mItemDecoration); + } + + updateBackgroundAndPaddings(); + } + + @Override + public void onBoundsChanged(Rect newBounds) { + mLauncher.updateOverlayBounds(newBounds); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Update the number of items in the grid before we measure the view + int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() : + MeasureSpec.getSize(widthMeasureSpec); + DeviceProfile grid = mLauncher.getDeviceProfile(); + grid.updateAppsViewNumCols(getResources(), availableWidth); + if (mNumAppsPerRow != grid.allAppsNumCols || + mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) { + mNumAppsPerRow = grid.allAppsNumCols; + mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols; + + // If there is a start margin to draw section names, determine how we are going to merge + // app sections + boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone; + AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ? + new FullMergeAlgorithm() : + new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f), + MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE); + + mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow); + mAdapter.setNumAppsPerRow(mNumAppsPerRow); + mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + /** + * Update the background and padding of the Apps view and children. Instead of insetting the + * container view, we inset the background and padding of the recycler view to allow for the + * recycler view to handle touch events (for fast scrolling) all the way to the edge. + */ + @Override + protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { + boolean isRtl = Utilities.isRtl(getResources()); + + // TODO: Use quantum_panel instead of quantum_panel_shape + InsetDrawable background = new InsetDrawable( + getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0, + padding.right, 0); + Rect bgPadding = new Rect(); + background.getPadding(bgPadding); + mContainerView.setBackground(background); + mRevealView.setBackground(background.getConstantState().newDrawable()); + mAppsRecyclerView.updateBackgroundPadding(bgPadding); + mAdapter.updateBackgroundPadding(bgPadding); + + // Hack: We are going to let the recycler view take the full width, so reset the padding on + // the container to zero after setting the background and apply the top-bottom padding to + // the content view instead so that the launcher transition clips correctly. + mContent.setPadding(0, padding.top, 0, padding.bottom); + mContainerView.setPadding(0, 0, 0, 0); + + // Pad the recycler view by the background padding plus the start margin (for the section + // names) + int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth()); + int topBottomPadding = mRecyclerViewTopBottomPadding; + if (isRtl) { + mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(), + topBottomPadding, padding.right + startInset, topBottomPadding); + } else { + mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding, + padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding); + } + + // Inset the search bar to fit its bounds above the container + if (mSearchBarView != null) { + Rect backgroundPadding = new Rect(); + if (mSearchBarView.getBackground() != null) { + mSearchBarView.getBackground().getPadding(backgroundPadding); + } + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + mSearchBarContainerView.getLayoutParams(); + lp.leftMargin = searchBarBounds.left - backgroundPadding.left; + lp.topMargin = searchBarBounds.top - backgroundPadding.top; + lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) - backgroundPadding.right; + mSearchBarContainerView.requestLayout(); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Determine if the key event was actual text, if so, focus the search bar and then dispatch + // the key normally so that it can process this key event + if (!mSearchBarController.isSearchFieldFocused() && + event.getAction() == KeyEvent.ACTION_DOWN) { + final int unicodeChar = event.getUnicodeChar(); + final boolean isKeyNotWhitespace = unicodeChar > 0 && + !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar); + if (isKeyNotWhitespace) { + boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder, + event.getKeyCode(), event); + if (gotKey && mSearchQueryBuilder.length() > 0) { + mSearchBarController.focusSearchField(); + } + } + } + + return super.dispatchKeyEvent(event); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return handleTouchEvent(ev); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + return handleTouchEvent(ev); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); + break; + } + return false; + } + + @Override + public boolean onLongClick(View v) { + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // When we have exited all apps or are in transition, disregard long clicks + if (!mLauncher.isAppsViewVisible() || + mLauncher.getWorkspace().isSwitchingState()) return false; + // Return if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return false; + + // Start the drag + mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false); + // Enter spring loaded mode + mLauncher.enterSpringLoadedDragMode(); + + return false; + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return true; + } + + @Override + public boolean supportsDeleteDropTarget() { + return false; + } + + @Override + public float getIntrinsicIconScaleFactor() { + DeviceProfile grid = mLauncher.getDeviceProfile(); + return (float) grid.allAppsIconSizePx / grid.iconSizePx; + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + mLauncher.unlockScreenOrientation(false); + } + + @Override + public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, + boolean success) { + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && + !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { + // Exit spring loaded mode if we have not successfully dropped or have not handled the + // drop in Workspace + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + } + mLauncher.unlockScreenOrientation(false); + + // Display an error message if the drag failed due to there not being enough space on the + // target layout we were dropping on. + if (!success) { + boolean showOutOfSpaceMessage = false; + if (target instanceof Workspace) { + int currentScreen = mLauncher.getCurrentWorkspaceScreen(); + Workspace workspace = (Workspace) target; + CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); + ItemInfo itemInfo = (ItemInfo) d.dragInfo; + if (layout != null) { + layout.calculateSpans(itemInfo); + showOutOfSpaceMessage = + !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); + } + } + if (showOutOfSpaceMessage) { + mLauncher.showOutOfSpaceMessage(false); + } + + d.deferDragViewCleanupPostAnimation = false; + } + } + + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + // Do nothing + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + // Do nothing + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + // Do nothing + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + if (toWorkspace) { + // Reset the search bar after transitioning home + mSearchBarController.reset(); + } + } + + /** + * Handles the touch events to dismiss all apps when clicking outside the bounds of the + * recycler view. + */ + private boolean handleTouchEvent(MotionEvent ev) { + DeviceProfile grid = mLauncher.getDeviceProfile(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + if (!mContentBounds.isEmpty()) { + // Outset the fixed bounds and check if the touch is outside all apps + Rect tmpRect = new Rect(mContentBounds); + tmpRect.inset(-grid.allAppsIconSizePx / 2, 0); + if (ev.getX() < tmpRect.left || ev.getX() > tmpRect.right) { + mBoundsCheckLastTouchDownPos.set(x, y); + return true; + } + } else { + // Check if the touch is outside all apps + if (ev.getX() < getPaddingLeft() || + ev.getX() > (getWidth() - getPaddingRight())) { + mBoundsCheckLastTouchDownPos.set(x, y); + return true; + } + } + break; + case MotionEvent.ACTION_UP: + if (mBoundsCheckLastTouchDownPos.x > -1) { + ViewConfiguration viewConfig = ViewConfiguration.get(getContext()); + float dx = ev.getX() - mBoundsCheckLastTouchDownPos.x; + float dy = ev.getY() - mBoundsCheckLastTouchDownPos.y; + float distance = (float) Math.hypot(dx, dy); + if (distance < viewConfig.getScaledTouchSlop()) { + // The background was clicked, so just go home + Launcher launcher = (Launcher) getContext(); + launcher.showWorkspace(true); + return true; + } + } + // Fall through + case MotionEvent.ACTION_CANCEL: + mBoundsCheckLastTouchDownPos.set(-1, -1); + break; + } + return false; + } + + @Override + public void onSearchResult(String query, ArrayList apps) { + if (apps != null) { + if (apps.isEmpty()) { + String formatStr = getResources().getString(R.string.all_apps_no_search_results); + mAdapter.setEmptySearchText(String.format(formatStr, query)); + } else { + mAppsRecyclerView.scrollToTop(); + } + mApps.setOrderedFilter(apps); + } + } + + @Override + public void clearSearchResult() { + mApps.setOrderedFilter(null); + + // Clear the search query + mSearchQueryBuilder.clear(); + mSearchQueryBuilder.clearSpans(); + Selection.setSelection(mSearchQueryBuilder, 0); + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java new file mode 100644 index 0000000000..057883cab4 --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Handler; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.widget.TextView; +import com.android.launcher3.AppInfo; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Thunk; + +import java.util.HashMap; +import java.util.List; + + +/** + * The grid view adapter of all the apps. + */ +class AllAppsGridAdapter extends RecyclerView.Adapter { + + public static final String TAG = "AppsGridAdapter"; + private static final boolean DEBUG = false; + + // A section break in the grid + public static final int SECTION_BREAK_VIEW_TYPE = 0; + // A normal icon + public static final int ICON_VIEW_TYPE = 1; + // A prediction icon + public static final int PREDICTION_ICON_VIEW_TYPE = 2; + // The message shown when there are no filtered results + public static final int EMPTY_SEARCH_VIEW_TYPE = 3; + + /** + * ViewHolder for each icon. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + public View mContent; + + public ViewHolder(View v) { + super(v); + mContent = v; + } + } + + /** + * Helper class to size the grid items. + */ + public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { + + public GridSpanSizer() { + super(); + setSpanIndexCacheEnabled(true); + } + + @Override + public int getSpanSize(int position) { + if (mApps.hasNoFilteredResults()) { + // Empty view spans full width + return mAppsPerRow; + } + + switch (mApps.getAdapterItems().get(position).viewType) { + case AllAppsGridAdapter.ICON_VIEW_TYPE: + case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE: + return 1; + default: + // Section breaks span the full width + return mAppsPerRow; + } + } + } + + /** + * Helper class to draw the section headers + */ + public class GridItemDecoration extends RecyclerView.ItemDecoration { + + private static final boolean DEBUG_SECTION_MARGIN = false; + private static final boolean FADE_OUT_SECTIONS = false; + + private HashMap mCachedSectionBounds = new HashMap<>(); + private Rect mTmpBounds = new Rect(); + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (mApps.hasFilter() || mAppsPerRow == 0) { + return; + } + + if (DEBUG_SECTION_MARGIN) { + Paint p = new Paint(); + p.setColor(0x33ff0000); + c.drawRect(mBackgroundPadding.left, 0, mBackgroundPadding.left + mSectionNamesMargin, + parent.getMeasuredHeight(), p); + } + + List items = mApps.getAdapterItems(); + boolean hasDrawnPredictedAppsDivider = false; + boolean showSectionNames = mSectionNamesMargin > 0; + int childCount = parent.getChildCount(); + int lastSectionTop = 0; + int lastSectionHeight = 0; + for (int i = 0; i < childCount; i++) { + View child = parent.getChildAt(i); + ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child); + if (!isValidHolderAndChild(holder, child, items)) { + continue; + } + + if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) { + // Draw the divider under the predicted apps + int top = child.getTop() + child.getHeight() + mPredictionBarDividerOffset; + c.drawLine(mBackgroundPadding.left, top, + parent.getWidth() - mBackgroundPadding.right, top, + mPredictedAppsDividerPaint); + hasDrawnPredictedAppsDivider = true; + + } else if (showSectionNames && shouldDrawItemSection(holder, i, items)) { + // At this point, we only draw sections for each section break; + int viewTopOffset = (2 * child.getPaddingTop()); + int pos = holder.getPosition(); + AlphabeticalAppsList.AdapterItem item = items.get(pos); + AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo; + + // Draw all the sections for this index + String lastSectionName = item.sectionName; + for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) { + AlphabeticalAppsList.AdapterItem nextItem = items.get(pos); + String sectionName = nextItem.sectionName; + if (nextItem.sectionInfo != sectionInfo) { + break; + } + if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) { + continue; + } + + + // Find the section name bounds + PointF sectionBounds = getAndCacheSectionBounds(sectionName); + + // Calculate where to draw the section + int sectionBaseline = (int) (viewTopOffset + sectionBounds.y); + int x = mIsRtl ? + parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin : + mBackgroundPadding.left; + x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f); + int y = child.getTop() + sectionBaseline; + + // Determine whether this is the last row with apps in that section, if + // so, then fix the section to the row allowing it to scroll past the + // baseline, otherwise, bound it to the baseline so it's in the viewport + int appIndexInSection = items.get(pos).sectionAppIndex; + int nextRowPos = Math.min(items.size() - 1, + pos + mAppsPerRow - (appIndexInSection % mAppsPerRow)); + AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos); + boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName); + if (!fixedToRow) { + y = Math.max(sectionBaseline, y); + } + + // In addition, if it overlaps with the last section that was drawn, then + // offset it so that it does not overlap + if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) { + y += lastSectionTop - y + lastSectionHeight; + } + + // Draw the section header + if (FADE_OUT_SECTIONS) { + int alpha = 255; + if (fixedToRow) { + alpha = Math.min(255, + (int) (255 * (Math.max(0, y) / (float) sectionBaseline))); + } + mSectionTextPaint.setAlpha(alpha); + } + c.drawText(sectionName, x, y, mSectionTextPaint); + + lastSectionTop = y; + lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset); + lastSectionName = sectionName; + } + i += (sectionInfo.numApps - item.sectionAppIndex); + } + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + // Do nothing + } + + /** + * Given a section name, return the bounds of the given section name. + */ + private PointF getAndCacheSectionBounds(String sectionName) { + PointF bounds = mCachedSectionBounds.get(sectionName); + if (bounds == null) { + mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds); + bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height()); + mCachedSectionBounds.put(sectionName, bounds); + } + return bounds; + } + + /** + * Returns whether we consider this a valid view holder for us to draw a divider or section for. + */ + private boolean isValidHolderAndChild(ViewHolder holder, View child, + List items) { + // Ensure item is not already removed + GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) + child.getLayoutParams(); + if (lp.isItemRemoved()) { + return false; + } + // Ensure we have a valid holder + if (holder == null) { + return false; + } + // Ensure we have a holder position + int pos = holder.getPosition(); + if (pos < 0 || pos >= items.size()) { + return false; + } + return true; + } + + /** + * Returns whether to draw the divider for a given child. + */ + private boolean shouldDrawItemDivider(ViewHolder holder, + List items) { + int pos = holder.getPosition(); + return items.get(pos).viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE; + } + + /** + * Returns whether to draw the section for the given child. + */ + private boolean shouldDrawItemSection(ViewHolder holder, int childIndex, + List items) { + int pos = holder.getPosition(); + AlphabeticalAppsList.AdapterItem item = items.get(pos); + + // Ensure it's an icon + if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) { + return false; + } + // Draw the section header for the first item in each section + return (childIndex == 0) || + (items.get(pos - 1).viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE); + } + } + + private LayoutInflater mLayoutInflater; + @Thunk AlphabeticalAppsList mApps; + private GridLayoutManager mGridLayoutMgr; + private GridSpanSizer mGridSizer; + private GridItemDecoration mItemDecoration; + private View.OnTouchListener mTouchListener; + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + @Thunk final Rect mBackgroundPadding = new Rect(); + @Thunk int mPredictionBarDividerOffset; + @Thunk int mAppsPerRow; + @Thunk boolean mIsRtl; + private String mEmptySearchText; + + // Section drawing + @Thunk int mSectionNamesMargin; + @Thunk int mSectionHeaderOffset; + @Thunk Paint mSectionTextPaint; + @Thunk Paint mPredictedAppsDividerPaint; + + public AllAppsGridAdapter(Context context, AlphabeticalAppsList apps, + View.OnTouchListener touchListener, View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener) { + Resources res = context.getResources(); + mApps = apps; + mGridSizer = new GridSpanSizer(); + mGridLayoutMgr = new GridLayoutManager(context, 1, GridLayoutManager.VERTICAL, false); + mGridLayoutMgr.setSpanSizeLookup(mGridSizer); + mItemDecoration = new GridItemDecoration(); + mLayoutInflater = LayoutInflater.from(context); + mTouchListener = touchListener; + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); + mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset); + + mSectionTextPaint = new Paint(); + mSectionTextPaint.setTextSize(res.getDimensionPixelSize( + R.dimen.all_apps_grid_section_text_size)); + mSectionTextPaint.setColor(res.getColor(R.color.all_apps_grid_section_text_color)); + mSectionTextPaint.setAntiAlias(true); + + mPredictedAppsDividerPaint = new Paint(); + mPredictedAppsDividerPaint.setStrokeWidth(Utilities.pxFromDp(1f, res.getDisplayMetrics())); + mPredictedAppsDividerPaint.setColor(0x1E000000); + mPredictedAppsDividerPaint.setAntiAlias(true); + mPredictionBarDividerOffset = + (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) + + res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2; + } + + /** + * Sets the number of apps per row. + */ + public void setNumAppsPerRow(int appsPerRow) { + mAppsPerRow = appsPerRow; + mGridLayoutMgr.setSpanCount(appsPerRow); + } + + /** + * Sets whether we are in RTL mode. + */ + public void setRtl(boolean rtl) { + mIsRtl = rtl; + } + + /** + * Sets the text to show when there are no apps. + */ + public void setEmptySearchText(String query) { + mEmptySearchText = query; + } + + /** + * Notifies the adapter of the background padding so that it can draw things correctly in the + * item decorator. + */ + public void updateBackgroundPadding(Rect padding) { + mBackgroundPadding.set(padding); + } + + /** + * Returns the grid layout manager. + */ + public GridLayoutManager getLayoutManager() { + return mGridLayoutMgr; + } + + /** + * Returns the item decoration for the recycler view. + */ + public RecyclerView.ItemDecoration getItemDecoration() { + // We don't draw any headers when we are uncomfortably dense + return mItemDecoration; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case EMPTY_SEARCH_VIEW_TYPE: + return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent, + false)); + case SECTION_BREAK_VIEW_TYPE: + return new ViewHolder(new View(parent.getContext())); + case ICON_VIEW_TYPE: { + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + R.layout.all_apps_icon, parent, false); + icon.setOnTouchListener(mTouchListener); + icon.setOnClickListener(mIconClickListener); + icon.setOnLongClickListener(mIconLongClickListener); + icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) + .getLongPressTimeout()); + icon.setFocusable(true); + return new ViewHolder(icon); + } + case PREDICTION_ICON_VIEW_TYPE: { + BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( + R.layout.all_apps_prediction_bar_icon, parent, false); + icon.setOnTouchListener(mTouchListener); + icon.setOnClickListener(mIconClickListener); + icon.setOnLongClickListener(mIconLongClickListener); + icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) + .getLongPressTimeout()); + icon.setFocusable(true); + return new ViewHolder(icon); + } + default: + throw new RuntimeException("Unexpected view type"); + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case ICON_VIEW_TYPE: { + AppInfo info = mApps.getAdapterItems().get(position).appInfo; + BubbleTextView icon = (BubbleTextView) holder.mContent; + icon.applyFromApplicationInfo(info); + break; + } + case PREDICTION_ICON_VIEW_TYPE: { + AppInfo info = mApps.getAdapterItems().get(position).appInfo; + BubbleTextView icon = (BubbleTextView) holder.mContent; + icon.applyFromApplicationInfo(info); + break; + } + case EMPTY_SEARCH_VIEW_TYPE: + TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text); + emptyViewText.setText(mEmptySearchText); + break; + } + } + + @Override + public int getItemCount() { + if (mApps.hasNoFilteredResults()) { + // For the empty view + return 1; + } + return mApps.getAdapterItems().size(); + } + + @Override + public int getItemViewType(int position) { + if (mApps.hasNoFilteredResults()) { + return EMPTY_SEARCH_VIEW_TYPE; + } + + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); + return item.viewType; + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java new file mode 100644 index 0000000000..730c8d15ac --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.content.Context; +import android.graphics.Canvas; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.View; + +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.BaseRecyclerViewFastScrollBar; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Stats; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Thunk; + +import java.util.List; + +/** + * A RecyclerView with custom fast scroll support for the all apps view. + */ +public class AllAppsRecyclerView extends BaseRecyclerView + implements Stats.LaunchSourceProvider { + + private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0; + private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1; + + private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0; + private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1; + + private AlphabeticalAppsList mApps; + private int mNumAppsPerRow; + + @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView; + @Thunk int mPrevFastScrollFocusedPosition; + @Thunk int mFastScrollFrameIndex; + @Thunk final int[] mFastScrollFrames = new int[10]; + + private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON; + private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW; + + private ScrollPositionState mScrollPosState = new ScrollPositionState(); + + public AllAppsRecyclerView(Context context) { + this(context, null); + } + + public AllAppsRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr); + } + + /** + * Sets the list of apps in this view, used to determine the fastscroll position. + */ + public void setApps(AlphabeticalAppsList apps) { + mApps = apps; + } + + /** + * Sets the number of apps per row in this recycler view. + */ + public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) { + mNumAppsPerRow = numAppsPerRow; + + RecyclerView.RecycledViewPool pool = getRecycledViewPool(); + int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); + pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1); + pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow); + pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow); + pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); + } + + /** + * Scrolls this recycler view to the top. + */ + public void scrollToTop() { + scrollToPosition(0); + } + + /** + * We need to override the draw to ensure that we don't draw the overscroll effect beyond the + * background bounds. + */ + @Override + protected void dispatchDraw(Canvas canvas) { + canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, + getWidth() - mBackgroundPadding.right, + getHeight() - mBackgroundPadding.bottom); + super.dispatchDraw(canvas); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // Bind event handlers + addOnItemTouchListener(this); + } + + @Override + public void fillInLaunchSourceData(Bundle sourceData) { + sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS); + if (mApps.hasFilter()) { + sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, + Stats.SUB_CONTAINER_ALL_APPS_SEARCH); + } else { + sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, + Stats.SUB_CONTAINER_ALL_APPS_A_Z); + } + } + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. + */ + @Override + public String scrollToPositionAtProgress(float touchFraction) { + int rowCount = mApps.getNumAppRows(); + if (rowCount == 0) { + return ""; + } + + // Stop the scroller if it is scrolling + stopScroll(); + + // Find the fastscroll section that maps to this touch fraction + List fastScrollSections = + mApps.getFastScrollerSections(); + AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); + if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) { + for (int i = 1; i < fastScrollSections.size(); i++) { + AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); + if (info.touchFraction > touchFraction) { + break; + } + lastInfo = info; + } + } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){ + lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1))); + } else { + throw new RuntimeException("Unexpected scroll bar mode"); + } + + // Map the touch position back to the scroll of the recycler view + getCurScrollState(mScrollPosState, mApps.getAdapterItems()); + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0); + LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); + if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { + layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); + } + + if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) { + mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; + + // Reset the last focused view + if (mLastFastScrollFocusedView != null) { + mLastFastScrollFocusedView.setFastScrollFocused(false, true); + mLastFastScrollFocusedView = null; + } + + if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) { + smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState); + } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { + final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); + if (vh != null && + vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { + mLastFastScrollFocusedView = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + mLastFastScrollFocusedView.setFastScrollFocused(true, true); + } + } else { + throw new RuntimeException("Unexpected fast scroll mode"); + } + } + return lastInfo.sectionName; + } + + @Override + public void onFastScrollCompleted() { + super.onFastScrollCompleted(); + // Reset and clean up the last focused view + if (mLastFastScrollFocusedView != null) { + mLastFastScrollFocusedView.setFastScrollFocused(false, true); + mLastFastScrollFocusedView = null; + } + mPrevFastScrollFocusedPosition = -1; + } + + /** + * Updates the bounds for the scrollbar. + */ + @Override + public void onUpdateScrollbar() { + List items = mApps.getAdapterItems(); + + // Skip early if there are no items or we haven't been measured + if (items.isEmpty() || mNumAppsPerRow == 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + // Find the index and height of the first visible row (all rows have the same height) + int rowCount = mApps.getNumAppRows(); + getCurScrollState(mScrollPosState, items); + if (mScrollPosState.rowIndex < 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0); + } + + /** + * This runnable runs a single frame of the smooth scroll animation and posts the next frame + * if necessary. + */ + @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() { + @Override + public void run() { + if (mFastScrollFrameIndex < mFastScrollFrames.length) { + scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); + mFastScrollFrameIndex++; + postOnAnimation(mSmoothSnapNextFrameRunnable); + } else { + // Animation completed, set the fast scroll state on the target view + final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); + if (vh != null && + vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView && + mLastFastScrollFocusedView != vh.itemView) { + mLastFastScrollFocusedView = + (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; + mLastFastScrollFocusedView.setFastScrollFocused(true, true); + } + } + } + }; + + /** + * Smoothly snaps to a given position. We do this manually by calculating the keyframes + * ourselves and animating the scroll on the recycler view. + */ + private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) { + removeCallbacks(mSmoothSnapNextFrameRunnable); + + // Calculate the full animation from the current scroll position to the final scroll + // position, and then run the animation for the duration. + int curScrollY = getPaddingTop() + + (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; + int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight); + int numFrames = mFastScrollFrames.length; + for (int i = 0; i < numFrames; i++) { + // TODO(winsonc): We can interpolate this as well. + mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames; + } + mFastScrollFrameIndex = 0; + postOnAnimation(mSmoothSnapNextFrameRunnable); + } + + /** + * Returns the current scroll state of the apps rows. + */ + private void getCurScrollState(ScrollPositionState stateOut, + List items) { + stateOut.rowIndex = -1; + stateOut.rowTopOffset = -1; + stateOut.rowHeight = -1; + + // Return early if there are no items or we haven't been measured + if (items.isEmpty() || mNumAppsPerRow == 0) { + return; + } + + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + int position = getChildPosition(child); + if (position != NO_POSITION) { + AlphabeticalAppsList.AdapterItem item = items.get(position); + if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || + item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + stateOut.rowIndex = item.rowIndex; + stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); + stateOut.rowHeight = child.getHeight(); + break; + } + } + } + } + + /** + * Returns the scrollY for the given position in the adapter. + */ + private int getScrollAtPosition(int position, int rowHeight) { + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); + if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || + item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + int offset = item.rowIndex > 0 ? getPaddingTop() : 0; + return offset + item.rowIndex * rowHeight; + } else { + return 0; + } + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java new file mode 100644 index 0000000000..14e2a1863e --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerViewContainerView.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.BubbleTextView.BubbleTextShadowHandler; +import com.android.launcher3.ClickShadowView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; + +/** + * A container for RecyclerView to allow for the click shadow view to be shown behind an icon that + * is launching. + */ +public class AllAppsRecyclerViewContainerView extends FrameLayout + implements BubbleTextShadowHandler { + + private final ClickShadowView mTouchFeedbackView; + + public AllAppsRecyclerViewContainerView(Context context) { + this(context, null); + } + + public AllAppsRecyclerViewContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsRecyclerViewContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + Launcher launcher = (Launcher) context; + DeviceProfile grid = launcher.getDeviceProfile(); + + mTouchFeedbackView = new ClickShadowView(context); + + // Make the feedback view large enough to hold the blur bitmap. + int size = grid.allAppsIconSizePx + mTouchFeedbackView.getExtraSize(); + addView(mTouchFeedbackView, size, size); + } + + @Override + public void setPressedIcon(BubbleTextView icon, Bitmap background) { + if (icon == null || background == null) { + mTouchFeedbackView.setBitmap(null); + mTouchFeedbackView.animate().cancel(); + } else if (mTouchFeedbackView.setBitmap(background)) { + mTouchFeedbackView.alignWithIconView(icon, (ViewGroup) icon.getParent()); + mTouchFeedbackView.animateShadow(); + } + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java new file mode 100644 index 0000000000..2b363c0cbe --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.content.ComponentName; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; + +/** + * An interface to a search box that AllApps can command. + */ +public abstract class AllAppsSearchBarController { + + protected AlphabeticalAppsList mApps; + protected Callbacks mCb; + + /** + * Sets the references to the apps model and the search result callback. + */ + public final void initialize(AlphabeticalAppsList apps, Callbacks cb) { + mApps = apps; + mCb = cb; + onInitialize(); + } + + /** + * To be overridden by subclasses. This method will get called when the controller is set, + * before getView(). + */ + protected abstract void onInitialize(); + + /** + * Returns the search bar view. + * @param parent the parent to attach the search bar view to. + */ + public abstract View getView(ViewGroup parent); + + /** + * Focuses the search field to handle key events. + */ + public abstract void focusSearchField(); + + /** + * Returns whether the search field is focused. + */ + public abstract boolean isSearchFieldFocused(); + + /** + * Resets the search bar state. + */ + public abstract void reset(); + + /** + * Returns whether the prediction bar should currently be visible depending on the state of + * the search bar. + */ + @Deprecated + public abstract boolean shouldShowPredictionBar(); + + /** + * Callback for getting search results. + */ + public interface Callbacks { + + /** + * Called when the bounds of the search bar has changed. + */ + void onBoundsChanged(Rect newBounds); + + /** + * Called when the search is complete. + * + * @param apps sorted list of matching components or null if in case of failure. + */ + void onSearchResult(String query, ArrayList apps); + + /** + * Called when the search results should be cleared. + */ + void clearSearchResult(); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/allapps/AllAppsSearchEditView.java b/src/com/android/launcher3/allapps/AllAppsSearchEditView.java new file mode 100644 index 0000000000..b7dcd66ed2 --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsSearchEditView.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + + +/** + * The edit text for the search container + */ +public class AllAppsSearchEditView extends EditText { + + /** + * Implemented by listeners of the back key. + */ + public interface OnBackKeyListener { + public void onBackKey(); + } + + private OnBackKeyListener mBackKeyListener; + + public AllAppsSearchEditView(Context context) { + this(context, null); + } + + public AllAppsSearchEditView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AllAppsSearchEditView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setOnBackKeyListener(OnBackKeyListener listener) { + mBackKeyListener = listener; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + // If this is a back key, propagate the key back to the listener + if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { + if (mBackKeyListener != null) { + mBackKeyListener.onBackKey(); + } + return false; + } + return super.onKeyPreIme(keyCode, event); + } +} diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java new file mode 100644 index 0000000000..47241ce5d4 --- /dev/null +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import com.android.launcher3.AppInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.compat.AlphabeticIndexCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.model.AppNameComparator; +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +/** + * The alphabetically sorted list of applications. + */ +public class AlphabeticalAppsList { + + public static final String TAG = "AlphabeticalAppsList"; + private static final boolean DEBUG = false; + private static final boolean DEBUG_PREDICTIONS = false; + + /** + * Info about a section in the alphabetic list + */ + public static class SectionInfo { + // The number of applications in this section + public int numApps; + // The section break AdapterItem for this section + public AdapterItem sectionBreakItem; + // The first app AdapterItem for this section + public AdapterItem firstAppItem; + } + + /** + * Info about a fast scroller section, depending if sections are merged, the fast scroller + * sections will not be the same set as the section headers. + */ + public static class FastScrollSectionInfo { + // The section name + public String sectionName; + // The AdapterItem to scroll to for this section + public AdapterItem fastScrollToItem; + // The touch fraction that should map to this fast scroll section info + public float touchFraction; + + public FastScrollSectionInfo(String sectionName) { + this.sectionName = sectionName; + } + } + + /** + * Info about a particular adapter item (can be either section or app) + */ + public static class AdapterItem { + /** Common properties */ + // The index of this adapter item in the list + public int position; + // The type of this item + public int viewType; + // The row that this item shows up on + public int rowIndex; + + /** Section & App properties */ + // The section for this item + public SectionInfo sectionInfo; + + /** App-only properties */ + // The section name of this app. Note that there can be multiple items with different + // sectionNames in the same section + public String sectionName = null; + // The index of this app in the section + public int sectionAppIndex = -1; + // The index of this app in the row + public int rowAppIndex; + // The associated AppInfo for the app + public AppInfo appInfo = null; + // The index of this app not including sections + public int appIndex = -1; + + public static AdapterItem asSectionBreak(int pos, SectionInfo section) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE; + item.position = pos; + item.sectionInfo = section; + section.sectionBreakItem = item; + return item; + } + + public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName, + int sectionAppIndex, AppInfo appInfo, int appIndex) { + AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex); + item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE; + return item; + } + + public static AdapterItem asApp(int pos, SectionInfo section, String sectionName, + int sectionAppIndex, AppInfo appInfo, int appIndex) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE; + item.position = pos; + item.sectionInfo = section; + item.sectionName = sectionName; + item.sectionAppIndex = sectionAppIndex; + item.appInfo = appInfo; + item.appIndex = appIndex; + return item; + } + } + + /** + * Common interface for different merging strategies. + */ + public interface MergeAlgorithm { + boolean continueMerging(SectionInfo section, SectionInfo withSection, + int sectionAppCount, int numAppsPerRow, int mergeCount); + } + + private Launcher mLauncher; + + // The set of apps from the system not including predictions + private final List mApps = new ArrayList<>(); + private final HashMap mComponentToAppMap = new HashMap<>(); + + // The set of filtered apps with the current filter + private List mFilteredApps = new ArrayList<>(); + // The current set of adapter items + private List mAdapterItems = new ArrayList<>(); + // The set of sections for the apps with the current filter + private List mSections = new ArrayList<>(); + // The set of sections that we allow fast-scrolling to (includes non-merged sections) + private List mFastScrollerSections = new ArrayList<>(); + // The set of predicted app component names + private List mPredictedAppComponents = new ArrayList<>(); + // The set of predicted apps resolved from the component names and the current set of apps + private List mPredictedApps = new ArrayList<>(); + // The of ordered component names as a result of a search query + private ArrayList mSearchResults; + private HashMap mCachedSectionNames = new HashMap<>(); + private RecyclerView.Adapter mAdapter; + private AlphabeticIndexCompat mIndexer; + private AppNameComparator mAppNameComparator; + private MergeAlgorithm mMergeAlgorithm; + private int mNumAppsPerRow; + private int mNumPredictedAppsPerRow; + private int mNumAppRowsInAdapter; + + public AlphabeticalAppsList(Context context) { + mLauncher = (Launcher) context; + mIndexer = new AlphabeticIndexCompat(context); + mAppNameComparator = new AppNameComparator(context); + } + + /** + * Sets the number of apps per row. + */ + public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow, + MergeAlgorithm mergeAlgorithm) { + mNumAppsPerRow = numAppsPerRow; + mNumPredictedAppsPerRow = numPredictedAppsPerRow; + mMergeAlgorithm = mergeAlgorithm; + + updateAdapterItems(); + } + + /** + * Sets the adapter to notify when this dataset changes. + */ + public void setAdapter(RecyclerView.Adapter adapter) { + mAdapter = adapter; + } + + /** + * Returns all the apps. + */ + public List getApps() { + return mApps; + } + + /** + * Returns sections of all the current filtered applications. + */ + public List getSections() { + return mSections; + } + + /** + * Returns fast scroller sections of all the current filtered applications. + */ + public List getFastScrollerSections() { + return mFastScrollerSections; + } + + /** + * Returns the current filtered list of applications broken down into their sections. + */ + public List getAdapterItems() { + return mAdapterItems; + } + + /** + * Returns the number of applications in this list. + */ + public int getSize() { + return mFilteredApps.size(); + } + + /** + * Returns the number of rows of applications (not including predictions) + */ + public int getNumAppRows() { + return mNumAppRowsInAdapter; + } + + /** + * Returns whether there are is a filter set. + */ + public boolean hasFilter() { + return (mSearchResults != null); + } + + /** + * Returns whether there are no filtered results. + */ + public boolean hasNoFilteredResults() { + return (mSearchResults != null) && mFilteredApps.isEmpty(); + } + + /** + * Sets the sorted list of filtered components. + */ + public void setOrderedFilter(ArrayList f) { + if (mSearchResults != f) { + mSearchResults = f; + updateAdapterItems(); + } + } + + /** + * Sets the current set of predicted apps. Since this can be called before we get the full set + * of applications, we should merge the results only in onAppsUpdated() which is idempotent. + */ + public void setPredictedApps(List apps) { + mPredictedAppComponents.clear(); + mPredictedAppComponents.addAll(apps); + onAppsUpdated(); + } + + /** + * Sets the current set of apps. + */ + public void setApps(List apps) { + mComponentToAppMap.clear(); + addApps(apps); + } + + /** + * Adds new apps to the list. + */ + public void addApps(List apps) { + updateApps(apps); + } + + /** + * Updates existing apps in the list + */ + public void updateApps(List apps) { + for (AppInfo app : apps) { + mComponentToAppMap.put(app.toComponentKey(), app); + } + onAppsUpdated(); + } + + /** + * Removes some apps from the list. + */ + public void removeApps(List apps) { + for (AppInfo app : apps) { + mComponentToAppMap.remove(app.toComponentKey()); + } + onAppsUpdated(); + } + + /** + * Updates internals when the set of apps are updated. + */ + private void onAppsUpdated() { + // Sort the list of apps + mApps.clear(); + mApps.addAll(mComponentToAppMap.values()); + Collections.sort(mApps, mAppNameComparator.getAppInfoComparator()); + + // As a special case for some languages (currently only Simplified Chinese), we may need to + // coalesce sections + Locale curLocale = mLauncher.getResources().getConfiguration().locale; + TreeMap> sectionMap = null; + boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); + if (localeRequiresSectionSorting) { + // Compute the section headers. We use a TreeMap with the section name comparator to + // ensure that the sections are ordered when we iterate over it later + sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator()); + for (AppInfo info : mApps) { + // Add the section to the cache + String sectionName = getAndUpdateCachedSectionName(info.title); + + // Add it to the mapping + ArrayList sectionApps = sectionMap.get(sectionName); + if (sectionApps == null) { + sectionApps = new ArrayList<>(); + sectionMap.put(sectionName, sectionApps); + } + sectionApps.add(info); + } + + // Add each of the section apps to the list in order + List allApps = new ArrayList<>(mApps.size()); + for (Map.Entry> entry : sectionMap.entrySet()) { + allApps.addAll(entry.getValue()); + } + + mApps.clear(); + mApps.addAll(allApps); + } else { + // Just compute the section headers for use below + for (AppInfo info : mApps) { + // Add the section to the cache + getAndUpdateCachedSectionName(info.title); + } + } + + // Recompose the set of adapter items from the current set of apps + updateAdapterItems(); + } + + /** + * Updates the set of filtered apps with the current filter. At this point, we expect + * mCachedSectionNames to have been calculated for the set of all apps in mApps. + */ + private void updateAdapterItems() { + SectionInfo lastSectionInfo = null; + String lastSectionName = null; + FastScrollSectionInfo lastFastScrollerSectionInfo = null; + int position = 0; + int appIndex = 0; + + // Prepare to update the list of sections, filtered apps, etc. + mFilteredApps.clear(); + mFastScrollerSections.clear(); + mAdapterItems.clear(); + mSections.clear(); + + if (DEBUG_PREDICTIONS) { + if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) { + mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, + UserHandleCompat.myUserHandle())); + mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, + UserHandleCompat.myUserHandle())); + mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, + UserHandleCompat.myUserHandle())); + mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, + UserHandleCompat.myUserHandle())); + } + } + + // Process the predicted app components + mPredictedApps.clear(); + if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { + for (ComponentKey ck : mPredictedAppComponents) { + AppInfo info = mComponentToAppMap.get(ck); + if (info != null) { + mPredictedApps.add(info); + } else { + if (LauncherAppState.isDogfoodBuild()) { + Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher)); + } + } + // Stop at the number of predicted apps + if (mPredictedApps.size() == mNumPredictedAppsPerRow) { + break; + } + } + + if (!mPredictedApps.isEmpty()) { + // Add a section for the predictions + lastSectionInfo = new SectionInfo(); + lastFastScrollerSectionInfo = new FastScrollSectionInfo(""); + AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); + mSections.add(lastSectionInfo); + mFastScrollerSections.add(lastFastScrollerSectionInfo); + mAdapterItems.add(sectionItem); + + // Add the predicted app items + for (AppInfo info : mPredictedApps) { + AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo, + "", lastSectionInfo.numApps++, info, appIndex++); + if (lastSectionInfo.firstAppItem == null) { + lastSectionInfo.firstAppItem = appItem; + lastFastScrollerSectionInfo.fastScrollToItem = appItem; + } + mAdapterItems.add(appItem); + mFilteredApps.add(info); + } + } + } + + // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the + // ordered set of sections + for (AppInfo info : getFiltersAppInfos()) { + String sectionName = getAndUpdateCachedSectionName(info.title); + + // Create a new section if the section names do not match + if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) { + lastSectionName = sectionName; + lastSectionInfo = new SectionInfo(); + lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); + mSections.add(lastSectionInfo); + mFastScrollerSections.add(lastFastScrollerSectionInfo); + + // Create a new section item to break the flow of items in the list + if (!hasFilter()) { + AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); + mAdapterItems.add(sectionItem); + } + } + + // Create an app item + AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName, + lastSectionInfo.numApps++, info, appIndex++); + if (lastSectionInfo.firstAppItem == null) { + lastSectionInfo.firstAppItem = appItem; + lastFastScrollerSectionInfo.fastScrollToItem = appItem; + } + mAdapterItems.add(appItem); + mFilteredApps.add(info); + } + + // Merge multiple sections together as requested by the merge strategy for this device + mergeSections(); + + if (mNumAppsPerRow != 0) { + // Update the number of rows in the adapter after we do all the merging (otherwise, we + // would have to shift the values again) + int numAppsInSection = 0; + int numAppsInRow = 0; + int rowIndex = -1; + for (AdapterItem item : mAdapterItems) { + item.rowIndex = 0; + if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) { + numAppsInSection = 0; + } else if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || + item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + if (numAppsInSection % mNumAppsPerRow == 0) { + numAppsInRow = 0; + rowIndex++; + } + item.rowIndex = rowIndex; + item.rowAppIndex = numAppsInRow; + numAppsInSection++; + numAppsInRow++; + } + } + mNumAppRowsInAdapter = rowIndex + 1; + + // Pre-calculate all the fast scroller fractions based on the number of rows + float rowFraction = 1f / mNumAppRowsInAdapter; + for (FastScrollSectionInfo info : mFastScrollerSections) { + AdapterItem item = info.fastScrollToItem; + if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && + item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + info.touchFraction = 0f; + continue; + } + + float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); + info.touchFraction = item.rowIndex * rowFraction + subRowFraction; + } + } + + // Refresh the recycler view + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + + private List getFiltersAppInfos() { + if (mSearchResults == null) { + return mApps; + } + + ArrayList result = new ArrayList<>(); + for (ComponentKey key : mSearchResults) { + AppInfo match = mComponentToAppMap.get(key); + if (match != null) { + result.add(match); + } + } + return result; + } + + /** + * Merges multiple sections to reduce visual raggedness. + */ + private void mergeSections() { + // Ignore merging until we have an algorithm and a valid row size + if (mMergeAlgorithm == null || mNumAppsPerRow == 0) { + return; + } + + // Go through each section and try and merge some of the sections + if (!hasFilter()) { + int sectionAppCount = 0; + for (int i = 0; i < mSections.size() - 1; i++) { + SectionInfo section = mSections.get(i); + sectionAppCount = section.numApps; + int mergeCount = 1; + + // Merge rows based on the current strategy + while (i < (mSections.size() - 1) && + mMergeAlgorithm.continueMerging(section, mSections.get(i + 1), + sectionAppCount, mNumAppsPerRow, mergeCount)) { + SectionInfo nextSection = mSections.remove(i + 1); + + // Remove the next section break + mAdapterItems.remove(nextSection.sectionBreakItem); + int pos = mAdapterItems.indexOf(section.firstAppItem); + + // Point the section for these new apps to the merged section + int nextPos = pos + section.numApps; + for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) { + AdapterItem item = mAdapterItems.get(j); + item.sectionInfo = section; + item.sectionAppIndex += section.numApps; + } + + // Update the following adapter items of the removed section item + pos = mAdapterItems.indexOf(nextSection.firstAppItem); + for (int j = pos; j < mAdapterItems.size(); j++) { + AdapterItem item = mAdapterItems.get(j); + item.position--; + } + section.numApps += nextSection.numApps; + sectionAppCount += nextSection.numApps; + + if (DEBUG) { + Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName + + " to " + section.firstAppItem.sectionName + + " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow)); + } + mergeCount++; + } + } + } + } + + /** + * Returns the cached section name for the given title, recomputing and updating the cache if + * the title has no cached section name. + */ + private String getAndUpdateCachedSectionName(CharSequence title) { + String sectionName = mCachedSectionNames.get(title); + if (sectionName == null) { + sectionName = mIndexer.computeSectionName(title); + mCachedSectionNames.put(title, sectionName); + } + return sectionName; + } +} diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java new file mode 100644 index 0000000000..10740ec778 --- /dev/null +++ b/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.os.Handler; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.util.ComponentKey; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * The default search implementation. + */ +public class DefaultAppSearchAlgorithm { + + private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+"); + + private final List mApps; + protected final Handler mResultHandler; + + public DefaultAppSearchAlgorithm(List apps) { + mApps = apps; + mResultHandler = new Handler(); + } + + public void cancel(boolean interruptActiveRequests) { + if (interruptActiveRequests) { + mResultHandler.removeCallbacksAndMessages(null); + } + } + + public void doSearch(final String query, + final AllAppsSearchBarController.Callbacks callback) { + final ArrayList result = getTitleMatchResult(query); + mResultHandler.post(new Runnable() { + + @Override + public void run() { + callback.onSearchResult(query, result); + } + }); + } + + protected ArrayList getTitleMatchResult(String query) { + // Do an intersection of the words in the query and each title, and filter out all the + // apps that don't match all of the words in the query. + final String queryTextLower = query.toLowerCase(); + final String[] queryWords = SPLIT_PATTERN.split(queryTextLower); + + final ArrayList result = new ArrayList<>(); + for (AppInfo info : mApps) { + if (matches(info, queryWords)) { + result.add(info.toComponentKey()); + } + } + return result; + } + + protected boolean matches(AppInfo info, String[] queryWords) { + String title = info.title.toString(); + String[] words = SPLIT_PATTERN.split(title.toLowerCase()); + for (int qi = 0; qi < queryWords.length; qi++) { + boolean foundMatch = false; + for (int i = 0; i < words.length; i++) { + if (words[i].startsWith(queryWords[qi])) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + // If there is a word in the query that does not match any words in this + // title, so skip it. + return false; + } + } + return true; + } +} diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchController.java b/src/com/android/launcher3/allapps/DefaultAppSearchController.java new file mode 100644 index 0000000000..83b920589b --- /dev/null +++ b/src/com/android/launcher3/allapps/DefaultAppSearchController.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.allapps; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Thunk; + +import java.util.List; + + +/** + * The default search controller. + */ +final class DefaultAppSearchController extends AllAppsSearchBarController + implements TextWatcher, TextView.OnEditorActionListener, View.OnClickListener { + + private static final boolean ALLOW_SINGLE_APP_LAUNCH = true; + + private static final int FADE_IN_DURATION = 175; + private static final int FADE_OUT_DURATION = 100; + private static final int SEARCH_TRANSLATION_X_DP = 18; + + private final Context mContext; + @Thunk final InputMethodManager mInputMethodManager; + + private DefaultAppSearchAlgorithm mSearchManager; + + private ViewGroup mContainerView; + private View mSearchView; + @Thunk View mSearchBarContainerView; + private View mSearchButtonView; + private View mDismissSearchButtonView; + @Thunk AllAppsSearchEditView mSearchBarEditView; + @Thunk AllAppsRecyclerView mAppsRecyclerView; + @Thunk Runnable mFocusRecyclerViewRunnable = new Runnable() { + @Override + public void run() { + mAppsRecyclerView.requestFocus(); + } + }; + + public DefaultAppSearchController(Context context, ViewGroup containerView, + AllAppsRecyclerView appsRecyclerView) { + mContext = context; + mInputMethodManager = (InputMethodManager) + mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + mContainerView = containerView; + mAppsRecyclerView = appsRecyclerView; + } + + @Override + public View getView(ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + mSearchView = inflater.inflate(R.layout.all_apps_search_bar, parent, false); + mSearchView.setOnClickListener(this); + + mSearchButtonView = mSearchView.findViewById(R.id.search_button); + mSearchBarContainerView = mSearchView.findViewById(R.id.search_container); + mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); + mDismissSearchButtonView.setOnClickListener(this); + mSearchBarEditView = (AllAppsSearchEditView) + mSearchBarContainerView.findViewById(R.id.search_box_input); + mSearchBarEditView.addTextChangedListener(this); + mSearchBarEditView.setOnEditorActionListener(this); + mSearchBarEditView.setOnBackKeyListener( + new AllAppsSearchEditView.OnBackKeyListener() { + @Override + public void onBackKey() { + // Only hide the search field if there is no query, or if there + // are no filtered results + String query = Utilities.trim( + mSearchBarEditView.getEditableText().toString()); + if (query.isEmpty() || mApps.hasNoFilteredResults()) { + hideSearchField(true, mFocusRecyclerViewRunnable); + } + } + }); + return mSearchView; + } + + @Override + public void focusSearchField() { + mSearchBarEditView.requestFocus(); + showSearchField(); + } + + @Override + public boolean isSearchFieldFocused() { + return mSearchBarEditView.isFocused(); + } + + @Override + protected void onInitialize() { + mSearchManager = new DefaultAppSearchAlgorithm(mApps.getApps()); + } + + @Override + public void reset() { + hideSearchField(false, null); + } + + @Override + public boolean shouldShowPredictionBar() { + return false; + } + + @Override + public void onClick(View v) { + if (v == mSearchView) { + showSearchField(); + } else if (v == mDismissSearchButtonView) { + hideSearchField(true, mFocusRecyclerViewRunnable); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + @Override + public void afterTextChanged(final Editable s) { + String query = s.toString(); + if (query.isEmpty()) { + mSearchManager.cancel(true); + mCb.clearSearchResult(); + } else { + mSearchManager.cancel(false); + mSearchManager.doSearch(query, mCb); + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Skip if we disallow app-launch-on-enter + if (!ALLOW_SINGLE_APP_LAUNCH) { + return false; + } + // Skip if it's not the right action + if (actionId != EditorInfo.IME_ACTION_DONE) { + return false; + } + // Skip if there isn't exactly one item + if (mApps.getSize() != 1) { + return false; + } + // If there is exactly one icon, then quick-launch it + List items = mApps.getAdapterItems(); + for (int i = 0; i < items.size(); i++) { + AlphabeticalAppsList.AdapterItem item = items.get(i); + if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) { + mAppsRecyclerView.getChildAt(i).performClick(); + mInputMethodManager.hideSoftInputFromWindow( + mContainerView.getWindowToken(), 0); + return true; + } + } + return false; + } + + /** + * Focuses the search field. + */ + private void showSearchField() { + // Show the search bar and focus the search + final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, + mContext.getResources().getDisplayMetrics()); + mSearchBarContainerView.setVisibility(View.VISIBLE); + mSearchBarContainerView.setAlpha(0f); + mSearchBarContainerView.setTranslationX(translationX); + mSearchBarContainerView.animate() + .alpha(1f) + .translationX(0) + .setDuration(FADE_IN_DURATION) + .withLayer() + .withEndAction(new Runnable() { + @Override + public void run() { + mSearchBarEditView.requestFocus(); + mInputMethodManager.showSoftInput(mSearchBarEditView, + InputMethodManager.SHOW_IMPLICIT); + } + }); + mSearchButtonView.animate() + .alpha(0f) + .translationX(-translationX) + .setDuration(FADE_OUT_DURATION) + .withLayer(); + } + + /** + * Unfocuses the search field. + */ + @Thunk void hideSearchField(boolean animated, final Runnable postAnimationRunnable) { + mSearchManager.cancel(true); + + final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0; + final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP, + mContext.getResources().getDisplayMetrics()); + if (animated) { + // Hide the search bar and focus the recycler view + mSearchBarContainerView.animate() + .alpha(0f) + .translationX(0) + .setDuration(FADE_IN_DURATION) + .withLayer() + .withEndAction(new Runnable() { + @Override + public void run() { + mSearchBarContainerView.setVisibility(View.INVISIBLE); + if (resetTextField) { + mSearchBarEditView.setText(""); + } + mCb.clearSearchResult(); + if (postAnimationRunnable != null) { + postAnimationRunnable.run(); + } + } + }); + mSearchButtonView.setTranslationX(-translationX); + mSearchButtonView.animate() + .alpha(1f) + .translationX(0) + .setDuration(FADE_OUT_DURATION) + .withLayer(); + } else { + mSearchBarContainerView.setVisibility(View.INVISIBLE); + if (resetTextField) { + mSearchBarEditView.setText(""); + } + mCb.clearSearchResult(); + mSearchButtonView.setAlpha(1f); + mSearchButtonView.setTranslationX(0f); + if (postAnimationRunnable != null) { + postAnimationRunnable.run(); + } + } + mInputMethodManager.hideSoftInputFromWindow(mContainerView.getWindowToken(), 0); + } +} diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java new file mode 100644 index 0000000000..ec1fb669fb --- /dev/null +++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java @@ -0,0 +1,170 @@ +package com.android.launcher3.compat; + +import android.content.Context; +import com.android.launcher3.Utilities; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * Fallback class to support Alphabetic indexing if not supported by the framework. + * TODO(winsonc): disable for non-english locales + */ +class BaseAlphabeticIndex { + + private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-"; + private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1; + + public BaseAlphabeticIndex() {} + + /** + * Sets the max number of the label buckets in this index. + */ + public void setMaxLabelCount(int count) { + // Not currently supported + } + + /** + * Returns the index of the bucket in which the given string should appear. + */ + protected int getBucketIndex(String s) { + if (s.isEmpty()) { + return UNKNOWN_BUCKET_INDEX; + } + int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase()); + if (index != -1) { + return index; + } + return UNKNOWN_BUCKET_INDEX; + } + + /** + * Returns the label for the bucket at the given index (as returned by getBucketIndex). + */ + protected String getBucketLabel(int index) { + return BUCKETS.substring(index, index + 1); + } +} + +/** + * Reflected libcore.icu.AlphabeticIndex implementation, falls back to the base alphabetic index. + */ +public class AlphabeticIndexCompat extends BaseAlphabeticIndex { + + private static final String MID_DOT = "\u2219"; + + private Object mAlphabeticIndex; + private Method mAddLabelsMethod; + private Method mSetMaxLabelCountMethod; + private Method mGetBucketIndexMethod; + private Method mGetBucketLabelMethod; + private boolean mHasValidAlphabeticIndex; + private String mDefaultMiscLabel; + + public AlphabeticIndexCompat(Context context) { + super(); + try { + Locale curLocale = context.getResources().getConfiguration().locale; + Class clazz = Class.forName("libcore.icu.AlphabeticIndex"); + Constructor ctor = clazz.getConstructor(Locale.class); + mAddLabelsMethod = clazz.getDeclaredMethod("addLabels", Locale.class); + mSetMaxLabelCountMethod = clazz.getDeclaredMethod("setMaxLabelCount", int.class); + mGetBucketIndexMethod = clazz.getDeclaredMethod("getBucketIndex", String.class); + mGetBucketLabelMethod = clazz.getDeclaredMethod("getBucketLabel", int.class); + mAlphabeticIndex = ctor.newInstance(curLocale); + try { + // Ensure we always have some base English locale buckets + if (!curLocale.getLanguage().equals(Locale.ENGLISH.getLanguage())) { + mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH); + } + } catch (Exception e) { + e.printStackTrace(); + } + if (curLocale.getLanguage().equals(Locale.JAPANESE.getLanguage())) { + // Japanese character 他 ("misc") + mDefaultMiscLabel = "\u4ed6"; + // TODO(winsonc, omakoto): We need to handle Japanese sections better, especially the kanji + } else { + // Dot + mDefaultMiscLabel = MID_DOT; + } + mHasValidAlphabeticIndex = true; + } catch (Exception e) { + mHasValidAlphabeticIndex = false; + } + } + + /** + * Sets the max number of the label buckets in this index. + * (ICU 51 default is 99) + */ + public void setMaxLabelCount(int count) { + if (mHasValidAlphabeticIndex) { + try { + mSetMaxLabelCountMethod.invoke(mAlphabeticIndex, count); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + super.setMaxLabelCount(count); + } + } + + /** + * Computes the section name for an given string {@param s}. + */ + public String computeSectionName(CharSequence cs) { + String s = Utilities.trim(cs); + String sectionName = getBucketLabel(getBucketIndex(s)); + if (Utilities.trim(sectionName).isEmpty() && s.length() > 0) { + int c = s.codePointAt(0); + boolean startsWithDigit = Character.isDigit(c); + if (startsWithDigit) { + // Digit section + return "#"; + } else { + boolean startsWithLetter = Character.isLetter(c); + if (startsWithLetter) { + return mDefaultMiscLabel; + } else { + // In languages where these differ, this ensures that we differentiate + // between the misc section in the native language and a misc section + // for everything else. + return MID_DOT; + } + } + } + return sectionName; + } + + /** + * Returns the index of the bucket in which {@param s} should appear. + * Function is synchronized because underlying routine walks an iterator + * whose state is maintained inside the index object. + */ + protected int getBucketIndex(String s) { + if (mHasValidAlphabeticIndex) { + try { + return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s); + } catch (Exception e) { + e.printStackTrace(); + } + } + return super.getBucketIndex(s); + } + + /** + * Returns the label for the bucket at the given index (as returned by getBucketIndex). + */ + protected String getBucketLabel(int index) { + if (mHasValidAlphabeticIndex) { + try { + return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index); + } catch (Exception e) { + e.printStackTrace(); + } + } + return super.getBucketLabel(index); + } +} diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java index 6512d427e9..7aa36d447b 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java @@ -26,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Utilities; import java.util.List; @@ -63,20 +64,21 @@ public abstract class AppWidgetManagerCompat { public abstract List getAllProviders(); - public abstract String loadLabel(AppWidgetProviderInfo info); + public abstract String loadLabel(LauncherAppWidgetProviderInfo info); public abstract boolean bindAppWidgetIdIfAllowed( int appWidgetId, AppWidgetProviderInfo info, Bundle options); - public abstract UserHandleCompat getUser(AppWidgetProviderInfo info); + public abstract UserHandleCompat getUser(LauncherAppWidgetProviderInfo info); public abstract void startConfigActivity(AppWidgetProviderInfo info, int widgetId, Activity activity, AppWidgetHost host, int requestCode); public abstract Drawable loadPreview(AppWidgetProviderInfo info); - public abstract Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache); + public abstract Drawable loadIcon(LauncherAppWidgetProviderInfo info, IconCache cache); - public abstract Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap); + public abstract Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, + int imageHeight); } diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java index f599f4303c..f7f4b7e4f2 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java @@ -16,6 +16,7 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.app.Activity; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; @@ -28,6 +29,7 @@ import android.os.Build; import android.os.Bundle; import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Utilities; import java.util.List; @@ -44,10 +46,11 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { } @Override - public String loadLabel(AppWidgetProviderInfo info) { - return info.label.trim(); + public String loadLabel(LauncherAppWidgetProviderInfo info) { + return Utilities.trim(info.label); } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override public boolean bindAppWidgetIdIfAllowed(int appWidgetId, AppWidgetProviderInfo info, Bundle options) { @@ -59,7 +62,7 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { } @Override - public UserHandleCompat getUser(AppWidgetProviderInfo info) { + public UserHandleCompat getUser(LauncherAppWidgetProviderInfo info) { return UserHandleCompat.myUserHandle(); } @@ -79,12 +82,13 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { } @Override - public Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache) { + public Drawable loadIcon(LauncherAppWidgetProviderInfo info, IconCache cache) { return cache.getFullResIcon(info.provider.getPackageName(), info.icon); } @Override - public Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap) { + public Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, + int imageHeight) { return bitmap; } } diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java index 03d43a6f21..13712d8c71 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java @@ -38,6 +38,7 @@ import android.view.View; import android.widget.Toast; import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.R; import java.util.ArrayList; @@ -65,8 +66,8 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { } @Override - public String loadLabel(AppWidgetProviderInfo info) { - return info.loadLabel(mPm); + public String loadLabel(LauncherAppWidgetProviderInfo info) { + return info.getLabel(mPm); } @Override @@ -77,7 +78,10 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { } @Override - public UserHandleCompat getUser(AppWidgetProviderInfo info) { + public UserHandleCompat getUser(LauncherAppWidgetProviderInfo info) { + if (info.isCustomWidget) { + return UserHandleCompat.myUserHandle(); + } return UserHandleCompat.fromUser(info.getProfile()); } @@ -99,27 +103,28 @@ class AppWidgetManagerCompatVL extends AppWidgetManagerCompat { } @Override - public Drawable loadIcon(AppWidgetProviderInfo info, IconCache cache) { - return info.loadIcon(mContext, cache.getFullResIconDpi()); + public Drawable loadIcon(LauncherAppWidgetProviderInfo info, IconCache cache) { + return info.getIcon(mContext, cache); } @Override - public Bitmap getBadgeBitmap(AppWidgetProviderInfo info, Bitmap bitmap) { - if (info.getProfile().equals(android.os.Process.myUserHandle())) { + public Bitmap getBadgeBitmap(LauncherAppWidgetProviderInfo info, Bitmap bitmap, + int imageHeight) { + if (info.isCustomWidget || info.getProfile().equals(android.os.Process.myUserHandle())) { return bitmap; } // Add a user badge in the bottom right of the image. final Resources res = mContext.getResources(); final int badgeSize = res.getDimensionPixelSize(R.dimen.profile_badge_size); - final int badgeMargin = res.getDimensionPixelSize(R.dimen.profile_badge_margin); + final int badgeMinTop = res.getDimensionPixelSize(R.dimen.profile_badge_minimum_top); final Rect badgeLocation = new Rect(0, 0, badgeSize, badgeSize); - final int top = bitmap.getHeight() - badgeSize - badgeMargin; + final int top = Math.max(imageHeight - badgeSize, badgeMinTop); if (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { - badgeLocation.offset(badgeMargin, top); + badgeLocation.offset(0, top); } else { - badgeLocation.offset(bitmap.getWidth() - badgeSize - badgeMargin, top); + badgeLocation.offset(bitmap.getWidth() - badgeSize, top); } Drawable drawable = mPm.getUserBadgedDrawableForDensity( diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java index 90a4d1a1f7..07ef0efb71 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java @@ -17,7 +17,9 @@ package com.android.launcher3.compat; import android.content.ComponentName; +import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; public abstract class LauncherActivityInfoCompat { @@ -32,4 +34,11 @@ public abstract class LauncherActivityInfoCompat { public abstract ApplicationInfo getApplicationInfo(); public abstract long getFirstInstallTime(); public abstract Drawable getBadgedIcon(int density); + + /** + * Creates a LauncherActivityInfoCompat for the primary user. + */ + public static LauncherActivityInfoCompat fromResolveInfo(ResolveInfo info, Context context) { + return new LauncherActivityInfoCompatV16(context, info); + } } diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java index 1d41a6ff6b..ea51aace87 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java @@ -29,13 +29,15 @@ import android.graphics.drawable.Drawable; public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat { - private ActivityInfo mActivityInfo; - private ComponentName mComponentName; - private PackageManager mPm; + private final ResolveInfo mResolveInfo; + private final ActivityInfo mActivityInfo; + private final ComponentName mComponentName; + private final PackageManager mPm; LauncherActivityInfoCompatV16(Context context, ResolveInfo info) { super(); - this.mActivityInfo = info.activityInfo; + mResolveInfo = info; + mActivityInfo = info.activityInfo; mComponentName = new ComponentName(mActivityInfo.packageName, mActivityInfo.name); mPm = context.getPackageManager(); } @@ -49,31 +51,30 @@ public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat { } public CharSequence getLabel() { - return mActivityInfo.loadLabel(mPm); + return mResolveInfo.loadLabel(mPm); } public Drawable getIcon(int density) { - Drawable d = null; - if (mActivityInfo.getIconResource() != 0) { - Resources resources; + int iconRes = mResolveInfo.getIconResource(); + Resources resources = null; + Drawable icon = null; + // Get the preferred density icon from the app's resources + if (density != 0 && iconRes != 0) { try { - resources = mPm.getResourcesForApplication(mActivityInfo.packageName); - } catch (PackageManager.NameNotFoundException e) { - resources = null; - } - if (resources != null) { - try { - d = resources.getDrawableForDensity(mActivityInfo.getIconResource(), density); - } catch (Resources.NotFoundException e) { - // Return default icon below. - } + resources = mPm.getResourcesForApplication(mActivityInfo.applicationInfo); + icon = resources.getDrawableForDensity(iconRes, density); + } catch (NameNotFoundException | Resources.NotFoundException exc) { } } - if (d == null) { - Resources resources = Resources.getSystem(); - d = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density); + // Get the default density icon + if (icon == null) { + icon = mResolveInfo.loadIcon(mPm); } - return d; + if (icon == null) { + resources = Resources.getSystem(); + icon = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density); + } + return icon; } public ApplicationInfo getApplicationInfo() { diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java index b52cf1de2b..4448758e7a 100644 --- a/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java +++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatVL.java @@ -16,12 +16,14 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.graphics.drawable.Drawable; -import android.os.UserHandle; +import android.os.Build; +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class LauncherActivityInfoCompatVL extends LauncherActivityInfoCompat { private LauncherActivityInfo mLauncherActivityInfo; diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java index e47b9a58da..ac3d252f57 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java @@ -31,6 +31,8 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import com.android.launcher3.util.Thunk; + import java.util.ArrayList; import java.util.List; @@ -139,11 +141,11 @@ public class LauncherAppsCompatV16 extends LauncherAppsCompat { mContext.registerReceiver(mPackageMonitor, filter); } - private synchronized List getCallbacks() { + @Thunk synchronized List getCallbacks() { return new ArrayList(mCallbacks); } - private class PackageMonitor extends BroadcastReceiver { + @Thunk class PackageMonitor extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final UserHandleCompat user = UserHandleCompat.myUserHandle(); diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java index e0d28b5660..fbf91b548c 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java @@ -16,6 +16,7 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -32,6 +33,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class LauncherAppsCompatVL extends LauncherAppsCompat { private LauncherApps mLauncherApps; @@ -49,7 +51,7 @@ public class LauncherAppsCompatVL extends LauncherAppsCompat { List list = mLauncherApps.getActivityList(packageName, user.getUser()); if (list.size() == 0) { - return Collections.EMPTY_LIST; + return Collections.emptyList(); } ArrayList compatList = new ArrayList(list.size()); diff --git a/src/com/android/launcher3/compat/PackageInstallerCompat.java b/src/com/android/launcher3/compat/PackageInstallerCompat.java index 0eb8754e88..c499083283 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompat.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompat.java @@ -20,7 +20,7 @@ import android.content.Context; import com.android.launcher3.Utilities; -import java.util.HashSet; +import java.util.HashMap; public abstract class PackageInstallerCompat { @@ -37,25 +37,20 @@ public abstract class PackageInstallerCompat { if (Utilities.isLmpOrAbove()) { sInstance = new PackageInstallerCompatVL(context); } else { - sInstance = new PackageInstallerCompatV16(context) { }; + sInstance = new PackageInstallerCompatV16(); } } return sInstance; } } - public abstract HashSet updateAndGetActiveSessionCache(); - - public abstract void onPause(); - - public abstract void onResume(); - - public abstract void onFinishBind(); + /** + * @return a map of active installs to their progress + */ + public abstract HashMap updateAndGetActiveSessionCache(); public abstract void onStop(); - public abstract void recordPackageUpdate(String packageName, int state, int progress); - public static final class PackageInstallInfo { public final String packageName; diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatV16.java b/src/com/android/launcher3/compat/PackageInstallerCompatV16.java index 1910d22ae5..654e349685 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompatV16.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompatV16.java @@ -16,160 +16,17 @@ package com.android.launcher3.compat; -import android.content.Context; -import android.content.SharedPreferences; -import android.text.TextUtils; -import android.util.Log; - -import com.android.launcher3.LauncherAppState; - -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONStringer; -import org.json.JSONTokener; - -import java.util.ArrayList; -import java.util.HashSet; +import java.util.HashMap; public class PackageInstallerCompatV16 extends PackageInstallerCompat { - private static final String TAG = "PackageInstallerCompatV16"; - private static final boolean DEBUG = false; - - private static final String KEY_PROGRESS = "progress"; - private static final String KEY_STATE = "state"; - - private static final String PREFS = - "com.android.launcher3.compat.PackageInstallerCompatV16.queue"; - - protected final SharedPreferences mPrefs; - - boolean mUseQueue; - boolean mFinishedBind; - boolean mReplayPending; - - PackageInstallerCompatV16(Context context) { - mPrefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); - } - - @Override - public void onPause() { - mUseQueue = true; - if (DEBUG) Log.d(TAG, "updates paused"); - } - - @Override - public void onResume() { - mUseQueue = false; - if (mFinishedBind) { - replayUpdates(); - } - } - - @Override - public void onFinishBind() { - mFinishedBind = true; - if (!mUseQueue) { - replayUpdates(); - } - } + PackageInstallerCompatV16() { } @Override public void onStop() { } - private void replayUpdates() { - if (DEBUG) Log.d(TAG, "updates resumed"); - LauncherAppState app = LauncherAppState.getInstanceNoCreate(); - if (app == null) { - mReplayPending = true; // try again later - if (DEBUG) Log.d(TAG, "app is null, delaying send"); - return; - } - mReplayPending = false; - ArrayList updates = new ArrayList(); - for (String packageName: mPrefs.getAll().keySet()) { - final String json = mPrefs.getString(packageName, null); - if (!TextUtils.isEmpty(json)) { - updates.add(infoFromJson(packageName, json)); - } - } - if (!updates.isEmpty()) { - sendUpdate(app, updates); - } - } - - /** - * This should be called by the implementations to register a package update. - */ @Override - public synchronized void recordPackageUpdate(String packageName, int state, int progress) { - SharedPreferences.Editor editor = mPrefs.edit(); - PackageInstallInfo installInfo = new PackageInstallInfo(packageName); - installInfo.progress = progress; - installInfo.state = state; - if (state == STATUS_INSTALLED) { - // no longer necessary to track this package - editor.remove(packageName); - if (DEBUG) Log.d(TAG, "no longer tracking " + packageName); - } else { - editor.putString(packageName, infoToJson(installInfo)); - if (DEBUG) - Log.d(TAG, "saved state: " + infoToJson(installInfo) - + " for package: " + packageName); - - } - editor.commit(); - - if (!mUseQueue) { - if (mReplayPending) { - replayUpdates(); - } else if (state != STATUS_INSTALLED) { - LauncherAppState app = LauncherAppState.getInstanceNoCreate(); - ArrayList update = new ArrayList(); - update.add(installInfo); - sendUpdate(app, update); - } - } - } - - private void sendUpdate(LauncherAppState app, ArrayList updates) { - if (app == null) { - mReplayPending = true; // try again later - if (DEBUG) Log.d(TAG, "app is null, delaying send"); - } else { - app.setPackageState(updates); - } - } - - private static PackageInstallInfo infoFromJson(String packageName, String json) { - PackageInstallInfo info = new PackageInstallInfo(packageName); - try { - JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); - info.state = object.getInt(KEY_STATE); - info.progress = object.getInt(KEY_PROGRESS); - } catch (JSONException e) { - Log.e(TAG, "failed to deserialize app state update", e); - } - return info; - } - - private static String infoToJson(PackageInstallInfo info) { - String value = null; - try { - JSONStringer json = new JSONStringer() - .object() - .key(KEY_STATE).value(info.state) - .key(KEY_PROGRESS).value(info.progress) - .endObject(); - value = json.toString(); - } catch (JSONException e) { - Log.e(TAG, "failed to serialize app state update", e); - } - return value; - } - - @Override - public HashSet updateAndGetActiveSessionCache() { - return new HashSet(); + public HashMap updateAndGetActiveSessionCache() { + return new HashMap<>(); } } diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java index 601f04ceaf..3ad51017d2 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java @@ -16,73 +16,55 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.Context; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionCallback; import android.content.pm.PackageInstaller.SessionInfo; +import android.os.Build; import android.os.Handler; -import android.util.Log; import android.util.SparseArray; import com.android.launcher3.IconCache; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.util.Thunk; -import java.util.ArrayList; -import java.util.HashSet; +import java.util.HashMap; -public class PackageInstallerCompatVL extends PackageInstallerCompat implements Runnable { +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class PackageInstallerCompatVL extends PackageInstallerCompat { - private static final String TAG = "PackageInstallerCompatVL"; - private static final boolean DEBUG = false; + @Thunk final SparseArray mActiveSessions = new SparseArray<>(); - // All updates to these sets must happen on the {@link #mWorker} thread. - private final SparseArray mPendingReplays = new SparseArray(); - private final HashSet mPendingBadgeUpdates = new HashSet(); - - private final PackageInstaller mInstaller; + @Thunk final PackageInstaller mInstaller; private final IconCache mCache; private final Handler mWorker; - private boolean mResumed; - private boolean mBound; - PackageInstallerCompatVL(Context context) { mInstaller = context.getPackageManager().getPackageInstaller(); LauncherAppState.setApplicationContext(context.getApplicationContext()); mCache = LauncherAppState.getInstance().getIconCache(); - mWorker = new Handler(); - - mResumed = false; - mBound = false; + mWorker = new Handler(LauncherModel.getWorkerLooper()); mInstaller.registerSessionCallback(mCallback, mWorker); - - // On start, send updates for all active sessions - mWorker.post(new Runnable() { - - @Override - public void run() { - for (SessionInfo info : mInstaller.getAllSessions()) { - mPendingReplays.append(info.getSessionId(), info); - } - } - }); } @Override - public HashSet updateAndGetActiveSessionCache() { - HashSet activePackages = new HashSet(); + public HashMap updateAndGetActiveSessionCache() { + HashMap activePackages = new HashMap<>(); UserHandleCompat user = UserHandleCompat.myUserHandle(); for (SessionInfo info : mInstaller.getAllSessions()) { addSessionInfoToCahce(info, user); if (info.getAppPackageName() != null) { - activePackages.add(info.getAppPackageName()); + activePackages.put(info.getAppPackageName(), (int) (info.getProgress() * 100)); + mActiveSessions.put(info.getSessionId(), info.getAppPackageName()); } } return activePackages; } - private void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) { + @Thunk void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) { String packageName = info.getAppPackageName(); if (packageName != null) { mCache.cachePackageInstallInfo(packageName, user, info.getAppIcon(), @@ -95,74 +77,10 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements mInstaller.unregisterSessionCallback(mCallback); } - @Override - public void onFinishBind() { - mBound = true; - mWorker.post(this); - } - - @Override - public void onPause() { - mResumed = false; - } - - @Override - public void onResume() { - mResumed = true; - mWorker.post(this); - } - - @Override - public void recordPackageUpdate(String packageName, int state, int progress) { - // No op - } - - @Override - public void run() { - // Called on mWorker thread. - replayUpdates(null); - } - - private void replayUpdates(PackageInstallInfo newInfo) { - if (DEBUG) Log.d(TAG, "updates resumed"); - if (!mResumed || !mBound) { - // Not yet ready - return; - } - if ((mPendingReplays.size() == 0) && (newInfo == null)) { - // Nothing to update - return; - } - + @Thunk void sendUpdate(PackageInstallInfo info) { LauncherAppState app = LauncherAppState.getInstanceNoCreate(); - if (app == null) { - // Try again later - if (DEBUG) Log.d(TAG, "app is null, delaying send"); - return; - } - - ArrayList updates = new ArrayList(); - if ((newInfo != null) && (newInfo.state != STATUS_INSTALLED)) { - updates.add(newInfo); - } - for (int i = mPendingReplays.size() - 1; i >= 0; i--) { - SessionInfo session = mPendingReplays.valueAt(i); - if (session.getAppPackageName() != null) { - updates.add(new PackageInstallInfo(session.getAppPackageName(), - STATUS_INSTALLING, - (int) (session.getProgress() * 100))); - } - } - mPendingReplays.clear(); - if (!updates.isEmpty()) { - app.setPackageState(updates); - } - - if (!mPendingBadgeUpdates.isEmpty()) { - for (String pkg : mPendingBadgeUpdates) { - app.updatePackageBadge(pkg); - } - mPendingBadgeUpdates.clear(); + if (app != null) { + app.getModel().setPackageState(info); } } @@ -170,19 +88,18 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements @Override public void onCreated(int sessionId) { - pushSessionBadgeToLauncher(sessionId); + pushSessionDisplayToLauncher(sessionId); } @Override public void onFinished(int sessionId, boolean success) { - mPendingReplays.remove(sessionId); - SessionInfo session = mInstaller.getSessionInfo(sessionId); - if ((session != null) && (session.getAppPackageName() != null)) { - mPendingBadgeUpdates.remove(session.getAppPackageName()); - // Replay all updates with a one time update for this installed package. No - // need to store this record for future updates, as the app list will get - // refreshed on resume. - replayUpdates(new PackageInstallInfo(session.getAppPackageName(), + // For a finished session, we can't get the session info. So use the + // packageName from our local cache. + String packageName = mActiveSessions.get(sessionId); + mActiveSessions.remove(sessionId); + + if (packageName != null) { + sendUpdate(new PackageInstallInfo(packageName, success ? STATUS_INSTALLED : STATUS_FAILED, 0)); } } @@ -191,8 +108,9 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements public void onProgressChanged(int sessionId, float progress) { SessionInfo session = mInstaller.getSessionInfo(sessionId); if (session != null) { - mPendingReplays.put(sessionId, session); - replayUpdates(null); + sendUpdate(new PackageInstallInfo(session.getAppPackageName(), + STATUS_INSTALLING, + (int) (session.getProgress() * 100))); } } @@ -201,18 +119,18 @@ public class PackageInstallerCompatVL extends PackageInstallerCompat implements @Override public void onBadgingChanged(int sessionId) { - pushSessionBadgeToLauncher(sessionId); + pushSessionDisplayToLauncher(sessionId); } - private void pushSessionBadgeToLauncher(int sessionId) { + private void pushSessionDisplayToLauncher(int sessionId) { SessionInfo session = mInstaller.getSessionInfo(sessionId); if (session != null) { addSessionInfoToCahce(session, UserHandleCompat.myUserHandle()); - if (session.getAppPackageName() != null) { - mPendingBadgeUpdates.add(session.getAppPackageName()); + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + + if (app != null) { + app.getModel().updateSessionDisplayInfo(session.getAppPackageName()); } - mPendingReplays.put(sessionId, session); - replayUpdates(null); } } }; diff --git a/src/com/android/launcher3/compat/UserHandleCompat.java b/src/com/android/launcher3/compat/UserHandleCompat.java index 2ae6731715..ab4b7216b3 100644 --- a/src/com/android/launcher3/compat/UserHandleCompat.java +++ b/src/com/android/launcher3/compat/UserHandleCompat.java @@ -16,10 +16,10 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.Intent; import android.os.Build; import android.os.UserHandle; - import com.android.launcher3.Utilities; public class UserHandleCompat { @@ -32,6 +32,7 @@ public class UserHandleCompat { private UserHandleCompat() { } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static UserHandleCompat myUserHandle() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return new UserHandleCompat(android.os.Process.myUserHandle()); @@ -40,7 +41,7 @@ public class UserHandleCompat { } } - static UserHandleCompat fromUser(UserHandle user) { + public static UserHandleCompat fromUser(UserHandle user) { if (user == null) { return null; } else { diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java index 1374b4e494..a79d94646d 100644 --- a/src/com/android/launcher3/compat/UserManagerCompat.java +++ b/src/com/android/launcher3/compat/UserManagerCompat.java @@ -43,4 +43,5 @@ public abstract class UserManagerCompat { public abstract UserHandleCompat getUserForSerialNumber(long serialNumber); public abstract Drawable getBadgedDrawableForUser(Drawable unbadged, UserHandleCompat user); public abstract CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user); + public abstract long getUserCreationTime(UserHandleCompat user); } diff --git a/src/com/android/launcher3/compat/UserManagerCompatV16.java b/src/com/android/launcher3/compat/UserManagerCompatV16.java index 32f972e85a..ffe698c8b5 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatV16.java +++ b/src/com/android/launcher3/compat/UserManagerCompatV16.java @@ -48,4 +48,9 @@ public class UserManagerCompatV16 extends UserManagerCompat { public CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user) { return label; } + + @Override + public long getUserCreationTime(UserHandleCompat user) { + return 0; + } } diff --git a/src/com/android/launcher3/compat/UserManagerCompatV17.java b/src/com/android/launcher3/compat/UserManagerCompatV17.java index 055359afe3..c42c00c7d3 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatV17.java +++ b/src/com/android/launcher3/compat/UserManagerCompatV17.java @@ -16,14 +16,12 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.UserHandle; +import android.os.Build; import android.os.UserManager; -import java.util.ArrayList; -import java.util.List; - +@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public class UserManagerCompatV17 extends UserManagerCompatV16 { protected UserManager mUserManager; diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java index 19eeabdcf3..dd7a726175 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatVL.java +++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java @@ -17,29 +17,36 @@ package com.android.launcher3.compat; +import android.annotation.TargetApi; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.UserHandle; -import android.os.UserManager; - +import com.android.launcher3.LauncherAppState; import java.util.ArrayList; import java.util.Collections; import java.util.List; +@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class UserManagerCompatVL extends UserManagerCompatV17 { + private static final String USER_CREATION_TIME_KEY = "user_creation_time_"; + private final PackageManager mPm; + private final Context mContext; UserManagerCompatVL(Context context) { super(context); mPm = context.getPackageManager(); + mContext = context; } @Override public List getUserProfiles() { List users = mUserManager.getUserProfiles(); if (users == null) { - return Collections.EMPTY_LIST; + return Collections.emptyList(); } ArrayList compatUsers = new ArrayList( users.size()); @@ -61,5 +68,17 @@ public class UserManagerCompatVL extends UserManagerCompatV17 { } return mPm.getUserBadgedLabel(label, user.getUser()); } + + @Override + public long getUserCreationTime(UserHandleCompat user) { + // TODO: Use system API once available. + SharedPreferences prefs = mContext.getSharedPreferences( + LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); + String key = USER_CREATION_TIME_KEY + getSerialNumberForUser(user); + if (!prefs.contains(key)) { + prefs.edit().putLong(key, System.currentTimeMillis()).apply(); + } + return prefs.getLong(key, 0); + } } diff --git a/src/com/android/launcher3/config/ProviderConfig.java b/src/com/android/launcher3/config/ProviderConfig.java index db25076ead..e8930d0636 100644 --- a/src/com/android/launcher3/config/ProviderConfig.java +++ b/src/com/android/launcher3/config/ProviderConfig.java @@ -18,5 +18,5 @@ package com.android.launcher3.config; public class ProviderConfig { - public static final String AUTHORITY = "com.android.launcher3.settings"; + public static final String AUTHORITY = "com.android.launcher3.settings".intern(); } diff --git a/src/com/android/launcher3/model/AbstractUserComparator.java b/src/com/android/launcher3/model/AbstractUserComparator.java new file mode 100644 index 0000000000..cf47ce648d --- /dev/null +++ b/src/com/android/launcher3/model/AbstractUserComparator.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.model; + +import android.content.Context; + +import com.android.launcher3.ItemInfo; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.Comparator; +import java.util.HashMap; + +/** + * A comparator to arrange items based on user profiles. + */ +public abstract class AbstractUserComparator implements Comparator { + + private HashMap mUserSerialCache = new HashMap<>(); + private final UserManagerCompat mUserManager; + private final UserHandleCompat mMyUser; + + public AbstractUserComparator(Context context) { + mUserManager = UserManagerCompat.getInstance(context); + mMyUser = UserHandleCompat.myUserHandle(); + } + + @Override + public int compare(T lhs, T rhs) { + if (mMyUser.equals(lhs.user)) { + return -1; + } else { + Long aUserSerial = getAndCacheUserSerial(lhs.user); + Long bUserSerial = getAndCacheUserSerial(rhs.user); + return aUserSerial.compareTo(bUserSerial); + } + } + + /** + * Returns the user serial for this user, using a cached serial if possible. + */ + private Long getAndCacheUserSerial(UserHandleCompat user) { + Long userSerial = mUserSerialCache.get(user); + if (userSerial == null) { + userSerial = mUserManager.getSerialNumberForUser(user); + mUserSerialCache.put(user, userSerial); + } + return userSerial; + } + + public void clearUserCache() { + mUserSerialCache.clear(); + } +} diff --git a/src/com/android/launcher3/model/AppNameComparator.java b/src/com/android/launcher3/model/AppNameComparator.java new file mode 100644 index 0000000000..c4b74d4dc4 --- /dev/null +++ b/src/com/android/launcher3/model/AppNameComparator.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.model; + +import android.content.Context; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.util.Thunk; + +import java.text.Collator; +import java.util.Comparator; + +/** + * Class to manage access to an app name comparator. + *

+ * Used to sort application name in all apps view and widget tray view. + */ +public class AppNameComparator { + private final Collator mCollator; + private final AbstractUserComparator mAppInfoComparator; + private final Comparator mSectionNameComparator; + + public AppNameComparator(Context context) { + mCollator = Collator.getInstance(); + mAppInfoComparator = new AbstractUserComparator(context) { + + @Override + public final int compare(ItemInfo a, ItemInfo b) { + // Order by the title in the current locale + int result = compareTitles(a.title.toString(), b.title.toString()); + if (result == 0 && a instanceof AppInfo && b instanceof AppInfo) { + AppInfo aAppInfo = (AppInfo) a; + AppInfo bAppInfo = (AppInfo) b; + // If two apps have the same title, then order by the component name + result = aAppInfo.componentName.compareTo(bAppInfo.componentName); + if (result == 0) { + // If the two apps are the same component, then prioritize by the order that + // the app user was created (prioritizing the main user's apps) + return super.compare(a, b); + } + } + return result; + } + }; + mSectionNameComparator = new Comparator() { + @Override + public int compare(String o1, String o2) { + return compareTitles(o1, o2); + } + }; + } + + /** + * Returns a locale-aware comparator that will alphabetically order a list of applications. + */ + public Comparator getAppInfoComparator() { + // Clear the user serial cache so that we get serials as needed in the comparator + mAppInfoComparator.clearUserCache(); + return mAppInfoComparator; + } + + /** + * Returns a locale-aware comparator that will alphabetically order a list of section names. + */ + public Comparator getSectionNameComparator() { + return mSectionNameComparator; + } + + /** + * Compares two titles with the same return value semantics as Comparator. + */ + @Thunk int compareTitles(String titleA, String titleB) { + // Ensure that we de-prioritize any titles that don't start with a linguistic letter or digit + boolean aStartsWithLetter = (titleA.length() > 0) && + Character.isLetterOrDigit(titleA.codePointAt(0)); + boolean bStartsWithLetter = (titleB.length() > 0) && + Character.isLetterOrDigit(titleB.codePointAt(0)); + if (aStartsWithLetter && !bStartsWithLetter) { + return -1; + } else if (!aStartsWithLetter && bStartsWithLetter) { + return 1; + } + + // Order by the title in the current locale + return mCollator.compare(titleA, titleB); + } +} diff --git a/src/com/android/launcher3/model/PackageItemInfo.java b/src/com/android/launcher3/model/PackageItemInfo.java new file mode 100644 index 0000000000..30f228c682 --- /dev/null +++ b/src/com/android/launcher3/model/PackageItemInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.model; + +import android.graphics.Bitmap; + +import com.android.launcher3.ItemInfo; + +import java.util.Arrays; + +/** + * Represents a {@link Package} in the widget tray section. + */ +public class PackageItemInfo extends ItemInfo { + + /** + * A bitmap version of the application icon. + */ + public Bitmap iconBitmap; + + /** + * Indicates whether we're using a low res icon. + */ + public boolean usingLowResIcon; + + /** + * Package name of the {@link ItemInfo}. + */ + public String packageName; + + /** + * Character that is used as a section name for the {@link ItemInfo#title}. + * (e.g., "G" will be stored if title is "Google") + */ + public String titleSectionName; + + int flags = 0; + + PackageItemInfo(String packageName) { + this.packageName = packageName; + } + + @Override + public String toString() { + return "PackageItemInfo(title=" + title + " id=" + this.id + + " type=" + this.itemType + " container=" + this.container + + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY + + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + + " user=" + user + ")"; + } +} diff --git a/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java b/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java new file mode 100644 index 0000000000..61e8952831 --- /dev/null +++ b/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java @@ -0,0 +1,68 @@ +package com.android.launcher3.model; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; + +import java.text.Collator; +import java.util.Comparator; +import java.util.HashMap; + +public class WidgetsAndShortcutNameComparator implements Comparator { + private final AppWidgetManagerCompat mManager; + private final PackageManager mPackageManager; + private final HashMap mLabelCache; + private final Collator mCollator; + private final UserHandleCompat mMainHandle; + + public WidgetsAndShortcutNameComparator(Context context) { + mManager = AppWidgetManagerCompat.getInstance(context); + mPackageManager = context.getPackageManager(); + mLabelCache = new HashMap(); + mCollator = Collator.getInstance(); + mMainHandle = UserHandleCompat.myUserHandle(); + } + + @Override + public final int compare(Object a, Object b) { + String labelA, labelB; + if (mLabelCache.containsKey(a)) { + labelA = mLabelCache.get(a); + } else { + labelA = (a instanceof LauncherAppWidgetProviderInfo) + ? Utilities.trim(mManager.loadLabel((LauncherAppWidgetProviderInfo) a)) + : Utilities.trim(((ResolveInfo) a).loadLabel(mPackageManager)); + mLabelCache.put(a, labelA); + } + if (mLabelCache.containsKey(b)) { + labelB = mLabelCache.get(b); + } else { + labelB = (b instanceof LauncherAppWidgetProviderInfo) + ? Utilities.trim(mManager.loadLabel((LauncherAppWidgetProviderInfo) b)) + : Utilities.trim(((ResolveInfo) b).loadLabel(mPackageManager)); + mLabelCache.put(b, labelB); + } + + // Currently, there is no work profile shortcuts, hence only considering the widget cases. + + boolean aWorkProfile = (a instanceof LauncherAppWidgetProviderInfo) && + !mMainHandle.equals(mManager.getUser((LauncherAppWidgetProviderInfo) a)); + boolean bWorkProfile = (b instanceof LauncherAppWidgetProviderInfo) && + !mMainHandle.equals(mManager.getUser((LauncherAppWidgetProviderInfo) b)); + + // Independent of how the labels compare, if only one of the two widget info belongs to + // work profile, put that one in the back. + if (aWorkProfile && !bWorkProfile) { + return 1; + } + if (!aWorkProfile && bWorkProfile) { + return -1; + } + return mCollator.compare(labelA, labelB); + } +}; diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java new file mode 100644 index 0000000000..185dfcae3a --- /dev/null +++ b/src/com/android/launcher3/model/WidgetsModel.java @@ -0,0 +1,170 @@ + +package com.android.launcher3.model; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.util.Log; + +import com.android.launcher3.AppFilter; +import com.android.launcher3.IconCache; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.AlphabeticIndexCompat; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.compat.UserHandleCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +/** + * Widgets data model that is used by the adapters of the widget views and controllers. + * + *

The widgets and shortcuts are organized using package name as its index. + */ +public class WidgetsModel { + + private static final String TAG = "WidgetsModel"; + private static final boolean DEBUG = false; + + /* List of packages that is tracked by this model. */ + private ArrayList mPackageItemInfos = new ArrayList<>(); + + /* Map of widgets and shortcuts that are tracked per package. */ + private HashMap> mWidgetsList = new HashMap<>(); + + private ArrayList mRawList; + + private final AppWidgetManagerCompat mAppWidgetMgr; + private final Comparator mWidgetAndShortcutNameComparator; + private final Comparator mAppNameComparator; + private final IconCache mIconCache; + private final AppFilter mAppFilter; + private AlphabeticIndexCompat mIndexer; + + public WidgetsModel(Context context, IconCache iconCache, AppFilter appFilter) { + mAppWidgetMgr = AppWidgetManagerCompat.getInstance(context); + mWidgetAndShortcutNameComparator = new WidgetsAndShortcutNameComparator(context); + mAppNameComparator = (new AppNameComparator(context)).getAppInfoComparator(); + mIconCache = iconCache; + mAppFilter = appFilter; + mIndexer = new AlphabeticIndexCompat(context); + } + + private WidgetsModel(WidgetsModel model) { + mAppWidgetMgr = model.mAppWidgetMgr; + mPackageItemInfos = (ArrayList) model.mPackageItemInfos.clone(); + mWidgetsList = (HashMap>) model.mWidgetsList.clone(); + mRawList = (ArrayList) model.mRawList.clone(); + mWidgetAndShortcutNameComparator = model.mWidgetAndShortcutNameComparator; + mAppNameComparator = model.mAppNameComparator; + mIconCache = model.mIconCache; + mAppFilter = model.mAppFilter; + } + + // Access methods that may be deleted if the private fields are made package-private. + public int getPackageSize() { + if (mPackageItemInfos == null) { + return 0; + } + return mPackageItemInfos.size(); + } + + // Access methods that may be deleted if the private fields are made package-private. + public PackageItemInfo getPackageItemInfo(int pos) { + if (pos >= mPackageItemInfos.size() || pos < 0) { + return null; + } + return mPackageItemInfos.get(pos); + } + + public List getSortedWidgets(int pos) { + return mWidgetsList.get(mPackageItemInfos.get(pos)); + } + + public ArrayList getRawList() { + return mRawList; + } + + public void setWidgetsAndShortcuts(ArrayList rawWidgetsShortcuts) { + Utilities.assertWorkerThread(); + mRawList = rawWidgetsShortcuts; + if (DEBUG) { + Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size()); + } + + // Temporary list for {@link PackageItemInfos} to avoid having to go through + // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList} + HashMap tmpPackageItemInfos = new HashMap<>(); + + // clear the lists. + mWidgetsList.clear(); + mPackageItemInfos.clear(); + + // add and update. + for (Object o: rawWidgetsShortcuts) { + String packageName = ""; + UserHandleCompat userHandle = null; + ComponentName componentName = null; + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; + componentName = widgetInfo.provider; + packageName = widgetInfo.provider.getPackageName(); + userHandle = mAppWidgetMgr.getUser(widgetInfo); + } else if (o instanceof ResolveInfo) { + ResolveInfo resolveInfo = (ResolveInfo) o; + componentName = new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + packageName = resolveInfo.activityInfo.packageName; + userHandle = UserHandleCompat.myUserHandle(); + } + + if (componentName == null || userHandle == null) { + Log.e(TAG, String.format("Widget cannot be set for %s.", o.getClass().toString())); + continue; + } + if (mAppFilter != null && !mAppFilter.shouldShowApp(componentName)) { + if (DEBUG) { + Log.d(TAG, String.format("%s is filtered and not added to the widget tray.", + packageName)); + } + continue; + } + + PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName); + ArrayList widgetsShortcutsList = mWidgetsList.get(pInfo); + if (widgetsShortcutsList != null) { + widgetsShortcutsList.add(o); + } else { + widgetsShortcutsList = new ArrayList(); + widgetsShortcutsList.add(o); + pInfo = new PackageItemInfo(packageName); + mIconCache.getTitleAndIconForApp(packageName, userHandle, + true /* userLowResIcon */, pInfo); + pInfo.titleSectionName = mIndexer.computeSectionName(pInfo.title); + mWidgetsList.put(pInfo, widgetsShortcutsList); + tmpPackageItemInfos.put(packageName, pInfo); + mPackageItemInfos.add(pInfo); + } + } + + // sort. + Collections.sort(mPackageItemInfos, mAppNameComparator); + for (PackageItemInfo p: mPackageItemInfos) { + Collections.sort(mWidgetsList.get(p), mWidgetAndShortcutNameComparator); + } + } + + /** + * Create a snapshot of the widgets model. + *

+ * Usage case: view binding without being modified from package updates. + */ + @Override + public WidgetsModel clone(){ + return new WidgetsModel(this); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java similarity index 90% rename from src/com/android/launcher3/LauncherExtension.java rename to src/com/android/launcher3/testing/LauncherExtension.java index b264042cfd..34492e4cae 100644 --- a/src/com/android/launcher3/LauncherExtension.java +++ b/src/com/android/launcher3/testing/LauncherExtension.java @@ -1,7 +1,6 @@ -package com.android.launcher3; +package com.android.launcher3.testing; import android.animation.Animator; -import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.ComponentName; @@ -12,9 +11,19 @@ import android.view.Menu; import android.view.View; import android.view.ViewGroup; +import com.android.launcher3.AppInfo; +import com.android.launcher3.InsettableFrameLayout; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.LauncherCallbacks; +import com.android.launcher3.R; +import com.android.launcher3.allapps.AllAppsSearchBarController; +import com.android.launcher3.util.ComponentKey; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; /** * This class represents a very trivial LauncherExtension. It primarily serves as a simple @@ -81,6 +90,11 @@ public class LauncherExtension extends Launcher { public void onActivityResult(int requestCode, int resultCode, Intent data) { } + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + } + @Override public void onWindowFocusChanged(boolean hasFocus) { } @@ -107,6 +121,10 @@ public class LauncherExtension extends Launcher { return false; } + @Override + public void onTrimMemory(int level) { + } + @Override public void onLauncherProviderChange() { } @@ -241,6 +259,16 @@ public class LauncherExtension extends Launcher { return false; } + @Override + public AllAppsSearchBarController getAllAppsSearchBarController() { + return null; + } + + @Override + public List getPredictedApps() { + return new ArrayList<>(); + } + @Override public boolean isLauncherPreinstalled() { return false; @@ -261,6 +289,11 @@ public class LauncherExtension extends Launcher { return mLauncherOverlay; } + @Override + public void setLauncherSearchCallback(Object callbacks) { + // Do nothing + } + class LauncherExtensionOverlay implements LauncherOverlay { LauncherOverlayCallbacks mLauncherOverlayCallbacks; ViewGroup mOverlayView; diff --git a/src/com/android/launcher3/util/ComponentKey.java b/src/com/android/launcher3/util/ComponentKey.java new file mode 100644 index 0000000000..6a7df43186 --- /dev/null +++ b/src/com/android/launcher3/util/ComponentKey.java @@ -0,0 +1,81 @@ +package com.android.launcher3.util; + +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.ComponentName; +import android.content.Context; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.Arrays; + +public class ComponentKey { + + public final ComponentName componentName; + public final UserHandleCompat user; + + private final int mHashCode; + + public ComponentKey(ComponentName componentName, UserHandleCompat user) { + assert (componentName != null); + assert (user != null); + this.componentName = componentName; + this.user = user; + mHashCode = Arrays.hashCode(new Object[] {componentName, user}); + + } + + /** + * Creates a new component key from an encoded component key string in the form of + * [flattenedComponentString#userId]. If the userId is not present, then it defaults + * to the current user. + */ + public ComponentKey(Context context, String componentKeyStr) { + int userDelimiterIndex = componentKeyStr.indexOf("#"); + if (userDelimiterIndex != -1) { + String componentStr = componentKeyStr.substring(0, userDelimiterIndex); + Long componentUser = Long.valueOf(componentKeyStr.substring(userDelimiterIndex + 1)); + componentName = ComponentName.unflattenFromString(componentStr); + user = UserManagerCompat.getInstance(context) + .getUserForSerialNumber(componentUser.longValue()); + } else { + // No user provided, default to the current user + componentName = ComponentName.unflattenFromString(componentKeyStr); + user = UserHandleCompat.myUserHandle(); + } + mHashCode = Arrays.hashCode(new Object[] {componentName, user}); + } + + /** + * Encodes a component key as a string of the form [flattenedComponentString#userId]. + */ + public String flattenToString(Context context) { + return componentName.flattenToString() + "#" + + UserManagerCompat.getInstance(context).getSerialNumberForUser(user); + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(Object o) { + ComponentKey other = (ComponentKey) o; + return other.componentName.equals(componentName) && other.user.equals(user); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/util/CursorIconInfo.java b/src/com/android/launcher3/util/CursorIconInfo.java new file mode 100644 index 0000000000..cdf9e3c60a --- /dev/null +++ b/src/com/android/launcher3/util/CursorIconInfo.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.util; + +import android.content.Context; +import android.content.Intent.ShortcutIconResource; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.text.TextUtils; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.Utilities; + +/** + * Utility class to load icon from a cursor. + */ +public class CursorIconInfo { + public final int iconTypeIndex; + public final int iconPackageIndex; + public final int iconResourceIndex; + public final int iconIndex; + + public CursorIconInfo(Cursor c) { + iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); + iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); + iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); + iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); + } + + public Bitmap loadIcon(Cursor c, ShortcutInfo info, Context context) { + Bitmap icon = null; + int iconType = c.getInt(iconTypeIndex); + switch (iconType) { + case LauncherSettings.Favorites.ICON_TYPE_RESOURCE: + String packageName = c.getString(iconPackageIndex); + String resourceName = c.getString(iconResourceIndex); + if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) { + info.iconResource = new ShortcutIconResource(); + info.iconResource.packageName = packageName; + info.iconResource.resourceName = resourceName; + icon = Utilities.createIconBitmap(packageName, resourceName, context); + } + if (icon == null) { + // Failed to load from resource, try loading from DB. + icon = Utilities.createIconBitmap(c, iconIndex, context); + } + break; + case LauncherSettings.Favorites.ICON_TYPE_BITMAP: + icon = Utilities.createIconBitmap(c, iconIndex, context); + info.customIcon = icon != null; + break; + } + return icon; + } +} diff --git a/src/com/android/launcher3/util/FlingAnimation.java b/src/com/android/launcher3/util/FlingAnimation.java new file mode 100644 index 0000000000..55c5d7dc25 --- /dev/null +++ b/src/com/android/launcher3/util/FlingAnimation.java @@ -0,0 +1,104 @@ +package com.android.launcher3.util; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.graphics.PointF; +import android.graphics.Rect; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.DragLayer; +import com.android.launcher3.DragView; +import com.android.launcher3.DropTarget.DragObject; + +public class FlingAnimation implements AnimatorUpdateListener { + + /** + * Maximum acceleration in one dimension (pixels per milliseconds) + */ + private static final float MAX_ACCELERATION = 0.5f; + private static final int DRAG_END_DELAY = 300; + + protected final DragObject mDragObject; + protected final Rect mIconRect; + protected final DragLayer mDragLayer; + protected final Rect mFrom; + protected final int mDuration; + protected final float mUX, mUY; + protected final float mAnimationTimeFraction; + protected final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); + + protected float mAX, mAY; + + /** + * @param vel initial fling velocity in pixels per second. + */ + public FlingAnimation(DragObject d, PointF vel, Rect iconRect, DragLayer dragLayer) { + mDragObject = d; + mUX = vel.x / 1000; + mUY = vel.y / 1000; + mIconRect = iconRect; + + mDragLayer = dragLayer; + mFrom = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, mFrom); + + float scale = d.dragView.getScaleX(); + float xOffset = ((scale - 1f) * d.dragView.getMeasuredWidth()) / 2f; + float yOffset = ((scale - 1f) * d.dragView.getMeasuredHeight()) / 2f; + mFrom.left += xOffset; + mFrom.right -= xOffset; + mFrom.top += yOffset; + mFrom.bottom -= yOffset; + + mDuration = initDuration(); + mAnimationTimeFraction = ((float) mDuration) / (mDuration + DRAG_END_DELAY); + } + + /** + * The fling animation is based on the following system + * - Apply a constant force in the y direction to causing the fling to decelerate. + * - The animation runs for the time taken by the object to go out of the screen. + * - Calculate a constant acceleration in x direction such that the object reaches + * {@link #mIconRect} in the given time. + */ + protected int initDuration() { + float sY = -mFrom.bottom; + + float d = mUY * mUY + 2 * sY * MAX_ACCELERATION; + if (d >= 0) { + // sY can be reached under the MAX_ACCELERATION. Use MAX_ACCELERATION for y direction. + mAY = MAX_ACCELERATION; + } else { + // sY is not reachable, decrease the acceleration so that sY is almost reached. + d = 0; + mAY = mUY * mUY / (2 * -sY); + } + double t = (-mUY - Math.sqrt(d)) / mAY; + + float sX = -mFrom.exactCenterX() + mIconRect.exactCenterX(); + + // Find horizontal acceleration such that: u*t + a*t*t/2 = s + mAX = (float) ((sX - t * mUX) * 2 / (t * t)); + return (int) Math.round(t); + } + + public final int getDuration() { + return mDuration + DRAG_END_DELAY; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = animation.getAnimatedFraction(); + if (t > mAnimationTimeFraction) { + t = 1; + } else { + t = t / mAnimationTimeFraction; + } + final DragView dragView = (DragView) mDragLayer.getAnimatedView(); + final float time = t * mDuration; + dragView.setTranslationX(time * mUX + mFrom.left + mAX * time * time / 2); + dragView.setTranslationY(time * mUY + mFrom.top + mAY * time * time / 2); + dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); + } +} diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java new file mode 100644 index 0000000000..696eabe002 --- /dev/null +++ b/src/com/android/launcher3/util/FocusLogic.java @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.util; + +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.ShortcutAndWidgetContainer; + +import java.util.Arrays; + +/** + * Calculates the next item that a {@link KeyEvent} should change the focus to. + *

+ * Note, this utility class calculates everything regards to icon index and its (x,y) coordinates. + * Currently supports: + *

    + *
  • full matrix of cells that are 1x1 + *
  • sparse matrix of cells that are 1x1 + * [ 1][ ][ 2][ ] + * [ ][ ][ 3][ ] + * [ ][ 4][ ][ ] + * [ ][ 5][ 6][ 7] + *
+ * *

+ * For testing, one can use a BT keyboard, or use following adb command. + * ex. $ adb shell input keyevent 20 // KEYCODE_DPAD_LEFT + */ +public class FocusLogic { + + private static final String TAG = "FocusLogic"; + private static final boolean DEBUG = false; + + // Item and page index related constant used by {@link #handleKeyEvent}. + public static final int NOOP = -1; + + public static final int PREVIOUS_PAGE_RIGHT_COLUMN = -2; + public static final int PREVIOUS_PAGE_FIRST_ITEM = -3; + public static final int PREVIOUS_PAGE_LAST_ITEM = -4; + public static final int PREVIOUS_PAGE_LEFT_COLUMN = -5; + + public static final int CURRENT_PAGE_FIRST_ITEM = -6; + public static final int CURRENT_PAGE_LAST_ITEM = -7; + + public static final int NEXT_PAGE_FIRST_ITEM = -8; + public static final int NEXT_PAGE_LEFT_COLUMN = -9; + public static final int NEXT_PAGE_RIGHT_COLUMN = -10; + + // Matrix related constant. + public static final int EMPTY = -1; + public static final int PIVOT = 100; + + /** + * Returns true only if this utility class handles the key code. + */ + public static boolean shouldConsume(int keyCode) { + return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || + keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || + keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END || + keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || + keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL); + } + + public static int handleKeyEvent(int keyCode, int cntX, int cntY, + int [][] map, int iconIdx, int pageIndex, int pageCount, boolean isRtl) { + + if (DEBUG) { + Log.v(TAG, String.format( + "handleKeyEvent START: cntX=%d, cntY=%d, iconIdx=%d, pageIdx=%d, pageCnt=%d", + cntX, cntY, iconIdx, pageIndex, pageCount)); + } + + int newIndex = NOOP; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/); + if (isRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_RIGHT_COLUMN; + } else if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_PAGE_RIGHT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/); + if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_PAGE_LEFT_COLUMN; + } else if (isRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_LEFT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, 1 /*increment*/); + break; + case KeyEvent.KEYCODE_DPAD_UP: + newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, -1 /*increment*/); + break; + case KeyEvent.KEYCODE_MOVE_HOME: + newIndex = handleMoveHome(); + break; + case KeyEvent.KEYCODE_MOVE_END: + newIndex = handleMoveEnd(); + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + newIndex = handlePageDown(pageIndex, pageCount); + break; + case KeyEvent.KEYCODE_PAGE_UP: + newIndex = handlePageUp(pageIndex); + break; + default: + break; + } + + if (DEBUG) { + Log.v(TAG, String.format("handleKeyEvent FINISH: index [%d -> %s]", + iconIdx, getStringIndex(newIndex))); + } + return newIndex; + } + + /** + * Returns a matrix of size (m x n) that has been initialized with {@link #EMPTY}. + * + * @param m number of columns in the matrix + * @param n number of rows in the matrix + */ + // TODO: get rid of dynamic matrix creation. + private static int[][] createFullMatrix(int m, int n) { + int[][] matrix = new int [m][n]; + + for (int i=0; i < m;i++) { + Arrays.fill(matrix[i], EMPTY); + } + return matrix; + } + + /** + * Returns a matrix of size same as the {@link CellLayout} dimension that is initialized with the + * index of the child view. + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout layout) { + ShortcutAndWidgetContainer parent = layout.getShortcutsAndWidgets(); + final int m = layout.getCountX(); + final int n = layout.getCountY(); + final boolean invert = parent.invertLayoutHorizontally(); + + int[][] matrix = createFullMatrix(m, n); + + // Iterate thru the children. + for (int i = 0; i < parent.getChildCount(); i++ ) { + int cx = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellY; + matrix[invert ? (m - cx - 1) : cx][cy] = i; + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + /** + * Creates a sparse matrix that merges the icon and hotseat view group using the cell layout. + * The size of the returning matrix is [icon column count x (icon + hotseat row count)] + * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)] + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout, + boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + + int m, n; + if (isHorizontal) { + m = iconLayout.getCountX(); + n = iconLayout.getCountY() + hotseatLayout.getCountY(); + } else { + m = iconLayout.getCountX() + hotseatLayout.getCountX(); + n = iconLayout.getCountY(); + } + int[][] matrix = createFullMatrix(m, n); + + // Iterate thru the children of the top parent. + for (int i = 0; i < iconParent.getChildCount(); i++) { + int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + matrix[cx][cy] = i; + } + + // Iterate thru the children of the bottom parent + // The hotseat view group contains one more item than iconLayout column count. + // If {@param allappsiconRank} not negative, then the last icon in the hotseat + // is truncated. If it is negative, then all apps icon index is not inserted. + for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) { + int delta = 0; + if (isHorizontal) { + int cx = ((CellLayout.LayoutParams) + hotseatParent.getChildAt(i).getLayoutParams()).cellX; + if ((includeAllappsicon && cx >= allappsiconRank) || + (!includeAllappsicon && cx > allappsiconRank)) { + delta = -1; + } + matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i; + } else { + int cy = ((CellLayout.LayoutParams) + hotseatParent.getChildAt(i).getLayoutParams()).cellY; + if ((includeAllappsicon && cy >= allappsiconRank) || + (!includeAllappsicon && cy > allappsiconRank)) { + delta = -1; + } + matrix[iconLayout.getCountX()][cy + delta] = iconParent.getChildCount() + i; + } + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + /** + * Creates a sparse matrix that merges the icon of previous/next page and last column of + * current page. When left key is triggered on the leftmost column, sparse matrix is created + * that combines previous page matrix and an extra column on the right. Likewise, when right + * key is triggered on the rightmost column, sparse matrix is created that combines this column + * on the 0th column and the next page matrix. + * + * @param pivotX x coordinate of the focused item in the current page + * @param pivotY y coordinate of the focused item in the current page + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout iconLayout, int pivotX, int pivotY) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + + int[][] matrix = createFullMatrix(iconLayout.getCountX() + 1, iconLayout.getCountY()); + + // Iterate thru the children of the top parent. + for (int i = 0; i < iconParent.getChildCount(); i++) { + int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + if (pivotX < 0) { + matrix[cx - pivotX][cy] = i; + } else { + matrix[cx][cy] = i; + } + } + + if (pivotX < 0) { + matrix[0][pivotY] = PIVOT; + } else { + matrix[pivotX][pivotY] = PIVOT; + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + // + // key event handling methods. + // + + /** + * Calculates icon that has is closest to the horizontal axis in reference to the cur icon. + * + * Example of the check order for KEYCODE_DPAD_RIGHT: + * [ ][ ][13][14][15] + * [ ][ 6][ 8][10][12] + * [ X][ 1][ 2][ 3][ 4] + * [ ][ 5][ 7][ 9][11] + */ + // TODO: add unit tests to verify all permutation. + private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY, + int[][] matrix, int increment) { + if(matrix == null) { + throw new IllegalStateException("Dpad navigation requires a matrix."); + } + int newIconIndex = NOOP; + + int xPos = -1; + int yPos = -1; + // Figure out the location of the icon. + for (int i = 0; i < cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIdx) { + xPos = i; + yPos = j; + } + } + } + if (DEBUG) { + Log.v(TAG, String.format("\thandleDpadHorizontal: \t[x, y]=[%d, %d] iconIndex=%d", + xPos, yPos, iconIdx)); + } + + // Rule1: check first in the horizontal direction + for (int i = xPos + increment; 0 <= i && i < cntX; i = i + increment) { + if ((newIconIndex = inspectMatrix(i, yPos, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + + // Rule2: check (x1-n, yPos + increment), (x1-n, yPos - increment) + // (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment) + int nextYPos1; + int nextYPos2; + int i = -1; + for (int coeff = 1; coeff < cntY; coeff++) { + nextYPos1 = yPos + coeff * increment; + nextYPos2 = yPos - coeff * increment; + for (i = xPos + increment * coeff; 0 <= i && i < cntX; i = i + increment) { + if ((newIconIndex = inspectMatrix(i, nextYPos1, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + if ((newIconIndex = inspectMatrix(i, nextYPos2, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + } + return newIconIndex; + } + + /** + * Calculates icon that is closest to the vertical axis in reference to the current icon. + * + * Example of the check order for KEYCODE_DPAD_DOWN: + * [ ][ ][ ][ X][ ][ ][ ] + * [ ][ ][ 5][ 1][ 4][ ][ ] + * [ ][10][ 7][ 2][ 6][ 9][ ] + * [14][12][ 9][ 3][ 8][11][13] + */ + // TODO: add unit tests to verify all permutation. + private static int handleDpadVertical(int iconIndex, int cntX, int cntY, + int [][] matrix, int increment) { + int newIconIndex = NOOP; + if(matrix == null) { + throw new IllegalStateException("Dpad navigation requires a matrix."); + } + + int xPos = -1; + int yPos = -1; + // Figure out the location of the icon. + for (int i = 0; i< cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIndex) { + xPos = i; + yPos = j; + } + } + } + + if (DEBUG) { + Log.v(TAG, String.format("\thandleDpadVertical: \t[x, y]=[%d, %d] iconIndex=%d", + xPos, yPos, iconIndex)); + } + + // Rule1: check first in the dpad direction + for (int j = yPos + increment; 0 <= j && j 0) { + return PREVIOUS_PAGE_FIRST_ITEM; + } else { + return CURRENT_PAGE_FIRST_ITEM; + } + } + + // + // Helper methods. + // + + private static boolean isValid(int xPos, int yPos, int countX, int countY) { + return (0 <= xPos && xPos < countX && 0 <= yPos && yPos < countY); + } + + private static int inspectMatrix(int x, int y, int cntX, int cntY, int[][] matrix) { + int newIconIndex = NOOP; + if (isValid(x, y, cntX, cntY)) { + if (matrix[x][y] != -1) { + newIconIndex = matrix[x][y]; + if (DEBUG) { + Log.v(TAG, String.format("\t\tinspect: \t[x, y]=[%d, %d] %d", + x, y, matrix[x][y])); + } + return newIconIndex; + } + } + return newIconIndex; + } + + /** + * Only used for debugging. + */ + private static String getStringIndex(int index) { + switch(index) { + case NOOP: return "NOOP"; + case PREVIOUS_PAGE_FIRST_ITEM: return "PREVIOUS_PAGE_FIRST"; + case PREVIOUS_PAGE_LAST_ITEM: return "PREVIOUS_PAGE_LAST"; + case PREVIOUS_PAGE_RIGHT_COLUMN:return "PREVIOUS_PAGE_RIGHT_COLUMN"; + case CURRENT_PAGE_FIRST_ITEM: return "CURRENT_PAGE_FIRST"; + case CURRENT_PAGE_LAST_ITEM: return "CURRENT_PAGE_LAST"; + case NEXT_PAGE_FIRST_ITEM: return "NEXT_PAGE_FIRST"; + case NEXT_PAGE_LEFT_COLUMN: return "NEXT_PAGE_LEFT_COLUMN"; + default: + return Integer.toString(index); + } + } + + /** + * Only used for debugging. + */ + private static void printMatrix(int[][] matrix) { + Log.v(TAG, "\tprintMap:"); + int m = matrix.length; + int n = matrix[0].length; + + for (int j=0; j < n; j++) { + String colY = "\t\t"; + for (int i=0; i < m; i++) { + colY += String.format("%3d",matrix[i][j]); + } + Log.v(TAG, colY); + } + } + + /** + * @param edgeColumn the column of the new icon. either {@link #NEXT_PAGE_LEFT_COLUMN} or + * {@link #NEXT_PAGE_RIGHT_COLUMN} + * @return the view adjacent to {@param oldView} in the {@param nextPage}. + */ + public static View getAdjacentChildInNextPage( + ShortcutAndWidgetContainer nextPage, View oldView, int edgeColumn) { + final int newRow = ((CellLayout.LayoutParams) oldView.getLayoutParams()).cellY; + + int column = (edgeColumn == NEXT_PAGE_LEFT_COLUMN) ^ nextPage.invertLayoutHorizontally() + ? 0 : (((CellLayout) nextPage.getParent()).getCountX() - 1); + + for (; column >= 0; column--) { + for (int row = newRow; row >= 0; row--) { + View newView = nextPage.getChildAt(column, row); + if (newView != null) { + return newView; + } + } + } + return null; + } +} diff --git a/src/com/android/launcher3/util/LauncherEdgeEffect.java b/src/com/android/launcher3/util/LauncherEdgeEffect.java new file mode 100644 index 0000000000..3e3b255add --- /dev/null +++ b/src/com/android/launcher3/util/LauncherEdgeEffect.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.util; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +/** + * This class differs from the framework {@link android.widget.EdgeEffect}: + * 1) It does not use PorterDuffXfermode + * 2) The width to radius factor is smaller (0.5 instead of 0.75) + */ +public class LauncherEdgeEffect { + + // Time it will take the effect to fully recede in ms + private static final int RECEDE_TIME = 600; + + // Time it will take before a pulled glow begins receding in ms + private static final int PULL_TIME = 167; + + // Time it will take in ms for a pulled glow to decay to partial strength before release + private static final int PULL_DECAY_TIME = 2000; + + private static final float MAX_ALPHA = 0.5f; + + private static final float MAX_GLOW_SCALE = 2.f; + + private static final float PULL_GLOW_BEGIN = 0.f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 100; + // Maximum velocity, clamps at this value + private static final int MAX_VELOCITY = 10000; + + private static final float EPSILON = 0.001f; + + private static final double ANGLE = Math.PI / 6; + private static final float SIN = (float) Math.sin(ANGLE); + private static final float COS = (float) Math.cos(ANGLE); + + private float mGlowAlpha; + private float mGlowScaleY; + + private float mGlowAlphaStart; + private float mGlowAlphaFinish; + private float mGlowScaleYStart; + private float mGlowScaleYFinish; + + private long mStartTime; + private float mDuration; + + private final Interpolator mInterpolator; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RECEDE = 3; + private static final int STATE_PULL_DECAY = 4; + + private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; + + private static final int VELOCITY_GLOW_FACTOR = 6; + + private int mState = STATE_IDLE; + + private float mPullDistance; + + private final Rect mBounds = new Rect(); + private final Paint mPaint = new Paint(); + private float mRadius; + private float mBaseGlowScale; + private float mDisplacement = 0.5f; + private float mTargetDisplacement = 0.5f; + + /** + * Construct a new EdgeEffect with a theme appropriate for the provided context. + */ + public LauncherEdgeEffect() { + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.FILL); + mInterpolator = new DecelerateInterpolator(); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + final float r = width * 0.5f / SIN; + final float y = COS * r; + final float h = r - y; + final float or = height * 0.75f / SIN; + final float oy = COS * or; + final float oh = or - oy; + + mRadius = r; + mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f; + + mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h)); + } + + /** + * Reports if this EdgeEffect's animation is finished. If this method returns false + * after a call to {@link #draw(Canvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return mState == STATE_IDLE; + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + mState = STATE_IDLE; + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + *

Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement + * of the pull point is known.

+ * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + */ + public void onPull(float deltaDistance) { + onPull(deltaDistance, 0.5f); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @param displacement The displacement from the starting side of the effect of the point + * initiating the pull. In the case of touch this is the finger position. + * Values may be from 0-1. + */ + public void onPull(float deltaDistance, float displacement) { + final long now = AnimationUtils.currentAnimationTimeMillis(); + mTargetDisplacement = displacement; + if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { + return; + } + if (mState != STATE_PULL) { + mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY); + } + mState = STATE_PULL; + + mStartTime = now; + mDuration = PULL_TIME; + + mPullDistance += deltaDistance; + + final float absdd = Math.abs(deltaDistance); + mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, + mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + + if (mPullDistance == 0) { + mGlowScaleY = mGlowScaleYStart = 0; + } else { + final float scale = (float) (Math.max(0, 1 - 1 / + Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d); + + mGlowScaleY = mGlowScaleYStart = scale; + } + + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} and thereby + * draw the results accordingly. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + + mState = STATE_RECEDE; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + *

When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, + * the method getCurrVelocity will provide a reasonable approximation + * to use here.

+ * + * @param velocity Velocity at impact in pixels per second. + */ + public void onAbsorb(int velocity) { + mState = STATE_ABSORB; + velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY); + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0.15f + (velocity * 0.02f); + + // The glow depends more on the velocity, and therefore starts out + // nearly invisible. + mGlowAlphaStart = 0.3f; + mGlowScaleYStart = Math.max(mGlowScaleY, 0.f); + + + // Growth for the size of the glow should be quadratic to properly + // respond + // to a user's scrolling speed. The faster the scrolling speed, the more + // intense the effect should be for both the size and the saturation. + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f); + // Alpha should change for the glow as well as size. + mGlowAlphaFinish = Math.max( + mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + mTargetDisplacement = 0.5f; + } + + /** + * Set the color of this edge effect in argb. + * + * @param color Color in argb + */ + public void setColor(int color) { + mPaint.setColor(color); + } + + /** + * Return the color of this edge effect in argb. + * @return The color of this edge effect in argb + */ + public int getColor() { + return mPaint.getColor(); + } + + /** + * Draw into the provided canvas. Assumes that the canvas has been rotated + * accordingly and the size has been set. The effect will be drawn the full + * width of X=0 to X=width, beginning from Y=0 and extending to some factor < + * 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the + * animation + */ + public boolean draw(Canvas canvas) { + update(); + + final float centerX = mBounds.centerX(); + final float centerY = mBounds.height() - mRadius; + + canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0); + + final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f; + float translateX = mBounds.width() * displacement / 2; + mPaint.setAlpha((int) (0xff * mGlowAlpha)); + canvas.drawCircle(centerX + translateX, centerY, mRadius, mPaint); + + boolean oneLastFrame = false; + if (mState == STATE_RECEDE && mGlowScaleY == 0) { + mState = STATE_IDLE; + oneLastFrame = true; + } + + return mState != STATE_IDLE || oneLastFrame; + } + + /** + * Return the maximum height that the edge effect will be drawn at given the original + * {@link #setSize(int, int) input size}. + * @return The maximum height of the edge effect + */ + public int getMaxHeight() { + return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f); + } + + private void update() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final float t = Math.min((time - mStartTime) / mDuration, 1.f); + + final float interp = mInterpolator.getInterpolation(t); + + mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; + mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; + mDisplacement = (mDisplacement + mTargetDisplacement) / 2; + + if (t >= 1.f - EPSILON) { + switch (mState) { + case STATE_ABSORB: + mState = STATE_RECEDE; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After absorb, the glow should fade to nothing. + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL: + mState = STATE_PULL_DECAY; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = PULL_DECAY_TIME; + + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After pull, the glow should fade to nothing. + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL_DECAY: + mState = STATE_RECEDE; + break; + case STATE_RECEDE: + mState = STATE_IDLE; + break; + } + } + } +} diff --git a/src/com/android/launcher3/util/LongArrayMap.java b/src/com/android/launcher3/util/LongArrayMap.java new file mode 100644 index 0000000000..a337e85bd8 --- /dev/null +++ b/src/com/android/launcher3/util/LongArrayMap.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.util; + +import android.util.LongSparseArray; + +import java.util.Iterator; + +/** + * Extension of {@link LongSparseArray} with some utility methods. + */ +public class LongArrayMap extends LongSparseArray implements Iterable { + + public boolean containsKey(long key) { + return indexOfKey(key) >= 0; + } + + public boolean isEmpty() { + return size() <= 0; + } + + @Override + public LongArrayMap clone() { + return (LongArrayMap) super.clone(); + } + + @Override + public Iterator iterator() { + return new ValueIterator(); + } + + @Thunk class ValueIterator implements Iterator { + + private int mNextIndex = 0; + + @Override + public boolean hasNext() { + return mNextIndex < size(); + } + + @Override + public E next() { + return valueAt(mNextIndex ++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java new file mode 100644 index 0000000000..b37f44719d --- /dev/null +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.util.Log; + +import com.android.launcher3.FolderInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherFiles; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.LauncherActivityInfoCompat; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Handles addition of app shortcuts for managed profiles. + * Methods of class should only be called on {@link LauncherModel#sWorkerThread}. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class ManagedProfileHeuristic { + + private static final String TAG = "ManagedProfileHeuristic"; + + /** + * Maintain a set of packages installed per user. + */ + private static final String INSTALLED_PACKAGES_PREFIX = "installed_packages_for_user_"; + + private static final String USER_FOLDER_ID_PREFIX = "user_folder_"; + + /** + * Duration (in milliseconds) for which app shortcuts will be added to work folder. + */ + private static final long AUTO_ADD_TO_FOLDER_DURATION = 8 * 60 * 60 * 1000; + + public static ManagedProfileHeuristic get(Context context, UserHandleCompat user) { + if (Utilities.isLmpOrAbove() && !UserHandleCompat.myUserHandle().equals(user)) { + return new ManagedProfileHeuristic(context, user); + } + return null; + } + + private final Context mContext; + private final UserHandleCompat mUser; + private final LauncherModel mModel; + + private final SharedPreferences mPrefs; + private final long mUserSerial; + private final long mUserCreationTime; + private final String mPackageSetKey; + + private ArrayList mHomescreenApps; + private ArrayList mWorkFolderApps; + + private ManagedProfileHeuristic(Context context, UserHandleCompat user) { + mContext = context; + mUser = user; + mModel = LauncherAppState.getInstance().getModel(); + + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + mUserSerial = userManager.getSerialNumberForUser(user); + mUserCreationTime = userManager.getUserCreationTime(user); + mPackageSetKey = INSTALLED_PACKAGES_PREFIX + mUserSerial; + + mPrefs = mContext.getSharedPreferences(LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + } + + /** + * Checks the list of user apps and adds icons for newly installed apps on the homescreen or + * workfolder. + */ + public void processUserApps(List apps) { + mHomescreenApps = new ArrayList<>(); + mWorkFolderApps = new ArrayList<>(); + + HashSet packageSet = new HashSet<>(); + final boolean userAppsExisted = getUserApps(packageSet); + + boolean newPackageAdded = false; + + for (LauncherActivityInfoCompat info : apps) { + String packageName = info.getComponentName().getPackageName(); + if (!packageSet.contains(packageName)) { + packageSet.add(packageName); + newPackageAdded = true; + + try { + PackageInfo pkgInfo = mContext.getPackageManager() + .getPackageInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + markForAddition(info, pkgInfo.firstInstallTime); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unknown package " + packageName, e); + } + } + } + + if (newPackageAdded) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + // Do not add shortcuts on the homescreen for the first time. This prevents the launcher + // getting filled with the managed user apps, when it start with a fresh DB (or after + // a very long time). + finalizeAdditions(userAppsExisted); + } + } + + private void markForAddition(LauncherActivityInfoCompat info, long installTime) { + ArrayList targetList = + (installTime <= mUserCreationTime + AUTO_ADD_TO_FOLDER_DURATION) ? + mWorkFolderApps : mHomescreenApps; + targetList.add(ShortcutInfo.fromActivityInfo(info, mContext)); + } + + /** + * Adds and binds shortcuts marked to be added to the work folder. + */ + private void finalizeWorkFolder() { + if (mWorkFolderApps.isEmpty()) { + return; + } + Collections.sort(mWorkFolderApps, new Comparator() { + + @Override + public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { + return Long.compare(lhs.firstInstallTime, rhs.firstInstallTime); + } + }); + + // Try to get a work folder. + String folderIdKey = USER_FOLDER_ID_PREFIX + mUserSerial; + if (mPrefs.contains(folderIdKey)) { + long folderId = mPrefs.getLong(folderIdKey, 0); + final FolderInfo workFolder = mModel.findFolderById(folderId); + + if (workFolder == null || !workFolder.hasOption(FolderInfo.FLAG_WORK_FOLDER)) { + // Could not get a work folder. Add all the icons to homescreen. + mHomescreenApps.addAll(mWorkFolderApps); + return; + } + saveWorkFolderShortcuts(folderId, workFolder.contents.size()); + + final ArrayList shortcuts = mWorkFolderApps; + // FolderInfo could already be bound. We need to add shortcuts on the UI thread. + new MainThreadExecutor().execute(new Runnable() { + + @Override + public void run() { + for (ShortcutInfo info : shortcuts) { + workFolder.add(info); + } + } + }); + } else { + // Create a new folder. + final FolderInfo workFolder = new FolderInfo(); + workFolder.title = mContext.getText(R.string.work_folder_name); + workFolder.setOption(FolderInfo.FLAG_WORK_FOLDER, true, null); + + // Add all shortcuts before adding it to the UI, as an empty folder might get deleted. + for (ShortcutInfo info : mWorkFolderApps) { + workFolder.add(info); + } + + // Add the item to home screen and DB. This also generates an item id synchronously. + ArrayList itemList = new ArrayList(1); + itemList.add(workFolder); + mModel.addAndBindAddedWorkspaceItems(mContext, itemList); + mPrefs.edit().putLong(USER_FOLDER_ID_PREFIX + mUserSerial, workFolder.id).apply(); + + saveWorkFolderShortcuts(workFolder.id, 0); + } + } + + /** + * Add work folder shortcuts to the DB. + */ + private void saveWorkFolderShortcuts(long workFolderId, int startingRank) { + for (ItemInfo info : mWorkFolderApps) { + info.rank = startingRank++; + LauncherModel.addItemToDatabase(mContext, info, workFolderId, 0, 0, 0); + } + } + + /** + * Adds and binds all shortcuts marked for addition. + */ + private void finalizeAdditions(boolean addHomeScreenShortcuts) { + finalizeWorkFolder(); + + if (addHomeScreenShortcuts && !mHomescreenApps.isEmpty()) { + mModel.addAndBindAddedWorkspaceItems(mContext, mHomescreenApps); + } + } + + /** + * Updates the list of installed apps and adds any new icons on homescreen or work folder. + */ + public void processPackageAdd(String[] packages) { + mHomescreenApps = new ArrayList<>(); + mWorkFolderApps = new ArrayList<>(); + + HashSet packageSet = new HashSet<>(); + final boolean userAppsExisted = getUserApps(packageSet); + + boolean newPackageAdded = false; + long installTime = System.currentTimeMillis(); + LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mContext); + + for (String packageName : packages) { + if (!packageSet.contains(packageName)) { + packageSet.add(packageName); + newPackageAdded = true; + + List activities = + launcherApps.getActivityList(packageName, mUser); + if (!activities.isEmpty()) { + markForAddition(activities.get(0), installTime); + } + } + } + + if (newPackageAdded) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + finalizeAdditions(userAppsExisted); + } + } + + /** + * Updates the list of installed packages for the user. + */ + public void processPackageRemoved(String[] packages) { + HashSet packageSet = new HashSet(); + getUserApps(packageSet); + boolean packageRemoved = false; + + for (String packageName : packages) { + if (packageSet.remove(packageName)) { + packageRemoved = true; + } + } + + if (packageRemoved) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + } + } + + /** + * Reads the list of user apps which have already been processed. + * @return false if the list didn't exist, true otherwise + */ + private boolean getUserApps(HashSet outExistingApps) { + Set userApps = mPrefs.getStringSet(mPackageSetKey, null); + if (userApps == null) { + return false; + } else { + outExistingApps.addAll(userApps); + return true; + } + } + + /** + * Verifies that entries corresponding to {@param users} exist and removes all invalid entries. + */ + public static void processAllUsers(List users, Context context) { + if (!Utilities.isLmpOrAbove()) { + return; + } + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + HashSet validKeys = new HashSet(); + for (UserHandleCompat user : users) { + addAllUserKeys(userManager.getSerialNumberForUser(user), validKeys); + } + + SharedPreferences prefs = context.getSharedPreferences( + LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + for (String key : prefs.getAll().keySet()) { + if (!validKeys.contains(key)) { + editor.remove(key); + } + } + editor.apply(); + } + + private static void addAllUserKeys(long userSerial, HashSet keysOut) { + keysOut.add(INSTALLED_PACKAGES_PREFIX + userSerial); + keysOut.add(USER_FOLDER_ID_PREFIX + userSerial); + } + + /** + * For each user, if a work folder has not been created, mark it such that the folder will + * never get created. + */ + public static void markExistingUsersForNoFolderCreation(Context context) { + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + UserHandleCompat myUser = UserHandleCompat.myUserHandle(); + + SharedPreferences prefs = null; + for (UserHandleCompat user : userManager.getUserProfiles()) { + if (myUser.equals(user)) { + continue; + } + + if (prefs == null) { + prefs = context.getSharedPreferences( + LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + } + String folderIdKey = USER_FOLDER_ID_PREFIX + userManager.getSerialNumberForUser(user); + if (!prefs.contains(folderIdKey)) { + prefs.edit().putLong(folderIdKey, ItemInfo.NO_ID).apply(); + } + } + } +} diff --git a/src/com/android/launcher3/util/RevealOutlineProvider.java b/src/com/android/launcher3/util/RevealOutlineProvider.java new file mode 100644 index 0000000000..0db3984f81 --- /dev/null +++ b/src/com/android/launcher3/util/RevealOutlineProvider.java @@ -0,0 +1,49 @@ +package com.android.launcher3.util; + +import android.annotation.TargetApi; +import android.graphics.Outline; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.view.ViewOutlineProvider; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class RevealOutlineProvider extends ViewOutlineProvider { + + private int mCenterX; + private int mCenterY; + private float mRadius0; + private float mRadius1; + private int mCurrentRadius; + + private final Rect mOval; + + /** + * @param x reveal center x + * @param y reveal center y + * @param r0 initial radius + * @param r1 final radius + */ + public RevealOutlineProvider(int x, int y, float r0, float r1) { + mCenterX = x; + mCenterY = y; + mRadius0 = r0; + mRadius1 = r1; + + mOval = new Rect(); + } + + public void setProgress(float progress) { + mCurrentRadius = (int) ((1 - progress) * mRadius0 + progress * mRadius1); + + mOval.left = mCenterX - mCurrentRadius; + mOval.top = mCenterY - mCurrentRadius; + mOval.right = mCenterX + mCurrentRadius; + mOval.bottom = mCenterY + mCurrentRadius; + } + + @Override + public void getOutline(View v, Outline outline) { + outline.setRoundRect(mOval, mCurrentRadius); + } +} diff --git a/src/com/android/launcher3/util/Thunk.java b/src/com/android/launcher3/util/Thunk.java new file mode 100644 index 0000000000..de350b0686 --- /dev/null +++ b/src/com/android/launcher3/util/Thunk.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the given field or method has package visibility solely to prevent the creation + * of a synthetic method. In practice, you should treat this field/method as if it were private. + *

+ * + * When a private method is called from an inner class, the Java compiler generates a simple + * package private shim method that the class generated from the inner class can call. This results + * in unnecessary bloat and runtime method call overhead. It also gets us closer to the dex method + * count limit. + *

+ * + * If you'd like to see warnings for these synthetic methods in eclipse, turn on: + * Window > Preferences > Java > Compiler > Errors/Warnings > "Access to a non-accessible member + * of an enclosing type". + *

+ * + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface Thunk { } \ No newline at end of file diff --git a/src/com/android/launcher3/util/UiThreadCircularReveal.java b/src/com/android/launcher3/util/UiThreadCircularReveal.java new file mode 100644 index 0000000000..3ca3aeeeec --- /dev/null +++ b/src/com/android/launcher3/util/UiThreadCircularReveal.java @@ -0,0 +1,57 @@ +package com.android.launcher3.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.View; +import android.view.ViewOutlineProvider; + +import com.android.launcher3.Utilities; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class UiThreadCircularReveal { + + public static ValueAnimator createCircularReveal(View v, int x, int y, float r0, float r1) { + return createCircularReveal(v, x, y, r0, r1, ViewOutlineProvider.BACKGROUND); + } + + public static ValueAnimator createCircularReveal(View v, int x, int y, float r0, float r1, + final ViewOutlineProvider originalProvider) { + ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + + final View revealView = v; + final RevealOutlineProvider outlineProvider = new RevealOutlineProvider(x, y, r0, r1); + final float elevation = v.getElevation(); + + va.addListener(new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + revealView.setOutlineProvider(outlineProvider); + revealView.setClipToOutline(true); + revealView.setTranslationZ(-elevation); + } + + public void onAnimationEnd(Animator animation) { + revealView.setOutlineProvider(originalProvider); + revealView.setClipToOutline(false); + revealView.setTranslationZ(0); + } + + }); + + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator arg0) { + float progress = arg0.getAnimatedFraction(); + outlineProvider.setProgress(progress); + revealView.invalidateOutline(); + if (!Utilities.isLmpMR1OrAbove()) { + revealView.invalidate(); + } + } + }); + return va; + } +} diff --git a/src/com/android/launcher3/util/WallpaperUtils.java b/src/com/android/launcher3/util/WallpaperUtils.java new file mode 100644 index 0000000000..53b2acd840 --- /dev/null +++ b/src/com/android/launcher3/util/WallpaperUtils.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.util; + +import android.annotation.TargetApi; +import android.app.WallpaperManager; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Point; +import android.os.Build; +import android.view.WindowManager; + +/** + * Utility methods for wallpaper management. + */ +public final class WallpaperUtils { + + public static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; + public static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; + public static final float WALLPAPER_SCREENS_SPAN = 2f; + + public static void suggestWallpaperDimension(Resources res, + final SharedPreferences sharedPrefs, + WindowManager windowManager, + final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { + final Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(res, windowManager); + // If we have saved a wallpaper width/height, use that instead + + int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); + int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1); + + if (savedWidth == -1 || savedHeight == -1) { + if (!fallBackToDefaults) { + return; + } else { + savedWidth = defaultWallpaperSize.x; + savedHeight = defaultWallpaperSize.y; + } + } + + if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || + savedHeight != wallpaperManager.getDesiredMinimumHeight()) { + wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); + } + } + + /** + * As a ratio of screen height, the total distance we want the parallax effect to span + * horizontally + */ + public static float wallpaperTravelToScreenWidthRatio(int width, int height) { + float aspectRatio = width / (float) height; + + // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width + // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width + // We will use these two data points to extrapolate how much the wallpaper parallax effect + // to span (ie travel) at any aspect ratio: + + final float ASPECT_RATIO_LANDSCAPE = 16/10f; + final float ASPECT_RATIO_PORTRAIT = 10/16f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; + + // To find out the desired width at different aspect ratios, we use the following two + // formulas, where the coefficient on x is the aspect ratio (width/height): + // (16/10)x + y = 1.5 + // (10/16)x + y = 1.2 + // We solve for x and y and end up with a final formula: + final float x = + (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / + (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); + final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; + return x * aspectRatio + y; + } + + private static Point sDefaultWallpaperSize; + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) { + if (sDefaultWallpaperSize == null) { + Point minDims = new Point(); + Point maxDims = new Point(); + windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + int maxDim = Math.max(maxDims.x, maxDims.y); + int minDim = Math.max(minDims.x, minDims.y); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + Point realSize = new Point(); + windowManager.getDefaultDisplay().getRealSize(realSize); + maxDim = Math.max(realSize.x, realSize.y); + minDim = Math.min(realSize.x, realSize.y); + } + + // We need to ensure that there is enough extra space in the wallpaper + // for the intended parallax effects + final int defaultWidth, defaultHeight; + if (res.getConfiguration().smallestScreenWidthDp >= 720) { + defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); + defaultHeight = maxDim; + } else { + defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); + defaultHeight = maxDim; + } + sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); + } + return sDefaultWallpaperSize; + } +} diff --git a/src/com/android/launcher3/widget/PendingAddShortcutInfo.java b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java new file mode 100644 index 0000000000..a56985083c --- /dev/null +++ b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.widget; + +import android.content.ComponentName; +import android.content.pm.ActivityInfo; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; + +/** + * Meta data used for late binding of the short cuts. + * + * @see {@link PendingAddItemInfo} + */ +public class PendingAddShortcutInfo extends PendingAddItemInfo { + + ActivityInfo activityInfo; + + public PendingAddShortcutInfo(ActivityInfo activityInfo) { + this.activityInfo = activityInfo; + componentName = new ComponentName(activityInfo.packageName, activityInfo.name); + itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + } + + @Override + public String toString() { + return String.format("PendingAddShortcutInfo package=%s, name=%s", + activityInfo.packageName, activityInfo.name); + } +} diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java new file mode 100644 index 0000000000..758287af3a --- /dev/null +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.widget; + +import android.appwidget.AppWidgetHostView; +import android.os.Bundle; +import android.os.Parcelable; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.PendingAddItemInfo; +import com.android.launcher3.compat.AppWidgetManagerCompat; + +/** + * Meta data used for late binding of {@link LauncherAppWidgetProviderInfo}. + * + * @see {@link PendingAddItemInfo} + */ +public class PendingAddWidgetInfo extends PendingAddItemInfo { + public int minWidth; + public int minHeight; + public int minResizeWidth; + public int minResizeHeight; + public int previewImage; + public int icon; + public LauncherAppWidgetProviderInfo info; + public AppWidgetHostView boundWidget; + public Bundle bindOptions = null; + + public PendingAddWidgetInfo(Launcher launcher, LauncherAppWidgetProviderInfo i, Parcelable data) { + if (i.isCustomWidget) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } else { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + } + this.info = i; + user = AppWidgetManagerCompat.getInstance(launcher).getUser(i); + componentName = i.provider; + minWidth = i.minWidth; + minHeight = i.minHeight; + minResizeWidth = i.minResizeWidth; + minResizeHeight = i.minResizeHeight; + previewImage = i.previewImage; + icon = i.icon; + + spanX = i.getSpanX(launcher); + spanY = i.getSpanY(launcher); + minSpanX = i.getMinSpanX(launcher); + minSpanY = i.getMinSpanY(launcher); + } + + public boolean isCustomWidget() { + return itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; + } + + @Override + public String toString() { + return String.format("PendingAddWidgetInfo package=%s, name=%s", + componentName.getPackageName(), componentName.getShortClassName()); + } +} diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java new file mode 100644 index 0000000000..7496ea2eff --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.widget; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewPropertyAnimator; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.StylusEventHelper; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.WidgetPreviewLoader.PreviewLoadRequest; +import com.android.launcher3.compat.AppWidgetManagerCompat; + +/** + * Represents the individual cell of the widget inside the widget tray. The preview is drawn + * horizontally centered, and scaled down if needed. + * + * This view does not support padding. Since the image is scaled down to fit the view, padding will + * further decrease the scaling factor. Drag-n-drop uses the view bounds for showing a smooth + * transition from the view to drag view, so when adding padding support, DnD would need to + * consider the appropriate scaling factor. + */ +public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { + + private static final String TAG = "WidgetCell"; + private static final boolean DEBUG = false; + + private static final int FADE_IN_DURATION_MS = 90; + + /** Widget cell width is calculated by multiplying this factor to grid cell width. */ + private static final float WIDTH_SCALE = 2.6f; + + /** Widget preview width is calculated by multiplying this factor to the widget cell width. */ + private static final float PREVIEW_SCALE = 0.8f; + + private int mPresetPreviewSize; + int cellSize; + + private WidgetImageView mWidgetImage; + private TextView mWidgetName; + private TextView mWidgetDims; + + private String mDimensionsFormatString; + private Object mInfo; + + private WidgetPreviewLoader mWidgetPreviewLoader; + private PreviewLoadRequest mActiveRequest; + private StylusEventHelper mStylusEventHelper; + + private Launcher mLauncher; + + public WidgetCell(Context context) { + this(context, null); + } + + public WidgetCell(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetCell(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final Resources r = context.getResources(); + mLauncher = (Launcher) context; + mStylusEventHelper = new StylusEventHelper(this); + + mDimensionsFormatString = r.getString(R.string.widget_dims_format); + setContainerWidth(); + setWillNotDraw(false); + setClipToPadding(false); + setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate()); + } + + private void setContainerWidth() { + DeviceProfile profile = mLauncher.getDeviceProfile(); + cellSize = (int) (profile.cellWidthPx * WIDTH_SCALE); + mPresetPreviewSize = (int) (cellSize * PREVIEW_SCALE); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mWidgetImage = (WidgetImageView) findViewById(R.id.widget_preview); + mWidgetName = ((TextView) findViewById(R.id.widget_name)); + mWidgetDims = ((TextView) findViewById(R.id.widget_dims)); + } + + /** + * Called to clear the view and free attached resources. (e.g., {@link Bitmap} + */ + public void clear() { + if (DEBUG) { + Log.d(TAG, "reset called on:" + mWidgetName.getText()); + } + mWidgetImage.animate().cancel(); + mWidgetImage.setBitmap(null); + mWidgetName.setText(null); + mWidgetDims.setText(null); + + if (mActiveRequest != null) { + mActiveRequest.cleanup(); + mActiveRequest = null; + } + } + + /** + * Apply the widget provider info to the view. + */ + public void applyFromAppWidgetProviderInfo(LauncherAppWidgetProviderInfo info, + WidgetPreviewLoader loader) { + + InvariantDeviceProfile profile = + LauncherAppState.getInstance().getInvariantDeviceProfile(); + mInfo = info; + // TODO(hyunyoungs): setup a cache for these labels. + mWidgetName.setText(AppWidgetManagerCompat.getInstance(getContext()).loadLabel(info)); + int hSpan = Math.min(info.getSpanX(mLauncher), profile.numColumns); + int vSpan = Math.min(info.getSpanY(mLauncher), profile.numRows); + mWidgetDims.setText(String.format(mDimensionsFormatString, hSpan, vSpan)); + mWidgetPreviewLoader = loader; + } + + /** + * Apply the resolve info to the view. + */ + public void applyFromResolveInfo( + PackageManager pm, ResolveInfo info, WidgetPreviewLoader loader) { + mInfo = info; + CharSequence label = info.loadLabel(pm); + mWidgetName.setText(label); + mWidgetDims.setText(String.format(mDimensionsFormatString, 1, 1)); + mWidgetPreviewLoader = loader; + } + + public int[] getPreviewSize() { + int[] maxSize = new int[2]; + + maxSize[0] = mPresetPreviewSize; + maxSize[1] = mPresetPreviewSize; + return maxSize; + } + + public void applyPreview(Bitmap bitmap) { + if (bitmap != null) { + mWidgetImage.setBitmap(bitmap); + mWidgetImage.setAlpha(0f); + ViewPropertyAnimator anim = mWidgetImage.animate(); + anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS); + } + } + + public void ensurePreview() { + if (mActiveRequest != null) { + return; + } + int[] size = getPreviewSize(); + if (DEBUG) { + Log.d(TAG, String.format("[tag=%s] ensurePreview (%d, %d):", + getTagToString(), size[0], size[1])); + } + mActiveRequest = mWidgetPreviewLoader.getPreview(mInfo, size[0], size[1], this); + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, + int oldTop, int oldRight, int oldBottom) { + removeOnLayoutChangeListener(this); + ensurePreview(); + } + + public int getActualItemWidth() { + ItemInfo info = (ItemInfo) getTag(); + int[] size = getPreviewSize(); + int cellWidth = mLauncher.getDeviceProfile().cellWidthPx; + + return Math.min(size[0], info.spanX * cellWidth); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean handled = super.onTouchEvent(ev); + if (mStylusEventHelper.checkAndPerformStylusEvent(ev)) { + return true; + } + return handled; + } + + /** + * Helper method to get the string info of the tag. + */ + private String getTagToString() { + if (getTag() instanceof PendingAddWidgetInfo || + getTag() instanceof PendingAddShortcutInfo) { + return getTag().toString(); + } + return ""; + } +} diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java new file mode 100644 index 0000000000..30b3d581a9 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java @@ -0,0 +1,155 @@ +package com.android.launcher3.widget; + +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; + +import com.android.launcher3.AppWidgetResizeFrame; +import com.android.launcher3.DragController.DragListener; +import com.android.launcher3.DragLayer; +import com.android.launcher3.DragSource; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.util.Thunk; + +public class WidgetHostViewLoader implements DragListener { + + /* Runnables to handle inflation and binding. */ + @Thunk Runnable mInflateWidgetRunnable = null; + private Runnable mBindWidgetRunnable = null; + + // TODO: technically, this class should not have to know the existence of the launcher. + @Thunk Launcher mLauncher; + @Thunk Handler mHandler; + @Thunk final View mView; + @Thunk final PendingAddWidgetInfo mInfo; + + // Widget id generated for binding a widget host view or -1 for invalid id. The id is + // not is use as long as it is stored here and can be deleted safely. Once its used, this value + // to be set back to -1. + @Thunk int mWidgetLoadingId = -1; + + public WidgetHostViewLoader(Launcher launcher, View view) { + mLauncher = launcher; + mHandler = new Handler(); + mView = view; + mInfo = (PendingAddWidgetInfo) view.getTag(); + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { } + + @Override + public void onDragEnd() { + // Cleanup up preloading state. + mLauncher.getDragController().removeDragListener(this); + + mHandler.removeCallbacks(mBindWidgetRunnable); + mHandler.removeCallbacks(mInflateWidgetRunnable); + + // Cleanup widget id + if (mWidgetLoadingId != -1) { + mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId); + mWidgetLoadingId = -1; + } + + // The widget was inflated and added to the DragLayer -- remove it. + if (mInfo.boundWidget != null) { + mLauncher.getDragLayer().removeView(mInfo.boundWidget); + mLauncher.getAppWidgetHost().deleteAppWidgetId(mInfo.boundWidget.getAppWidgetId()); + mInfo.boundWidget = null; + } + } + + /** + * Start preloading the widget. + */ + public boolean preloadWidget() { + final LauncherAppWidgetProviderInfo pInfo = mInfo.info; + + if (pInfo.isCustomWidget) { + return false; + } + final Bundle options = getDefaultOptionsForWidget(mLauncher, mInfo); + + // If there is a configuration activity, do not follow thru bound and inflate. + if (pInfo.configure != null) { + mInfo.bindOptions = options; + return false; + } + + mBindWidgetRunnable = new Runnable() { + @Override + public void run() { + mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId(); + if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed( + mWidgetLoadingId, pInfo, options)) { + + // Widget id bound. Inflate the widget. + mHandler.post(mInflateWidgetRunnable); + } + } + }; + + mInflateWidgetRunnable = new Runnable() { + @Override + public void run() { + if (mWidgetLoadingId == -1) { + return; + } + AppWidgetHostView hostView = mLauncher.getAppWidgetHost().createView( + (Context) mLauncher, mWidgetLoadingId, pInfo); + mInfo.boundWidget = hostView; + + // We used up the widget Id in binding the above view. + mWidgetLoadingId = -1; + + hostView.setVisibility(View.INVISIBLE); + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(mInfo, false); + // We want the first widget layout to be the correct size. This will be important + // for width size reporting to the AppWidgetManager. + DragLayer.LayoutParams lp = new DragLayer.LayoutParams(unScaledSize[0], + unScaledSize[1]); + lp.x = lp.y = 0; + lp.customPosition = true; + hostView.setLayoutParams(lp); + mLauncher.getDragLayer().addView(hostView); + mView.setTag(mInfo); + } + }; + + mHandler.post(mBindWidgetRunnable); + return true; + } + + public static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { + Bundle options = null; + Rect rect = new Rect(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, rect); + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher, + info.componentName, null); + + float density = launcher.getResources().getDisplayMetrics().density; + int xPaddingDips = (int) ((padding.left + padding.right) / density); + int yPaddingDips = (int) ((padding.top + padding.bottom) / density); + + options = new Bundle(); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, + rect.left - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, + rect.top - yPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, + rect.right - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, + rect.bottom - yPaddingDips); + } + return options; + } +} diff --git a/src/com/android/launcher3/widget/WidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java new file mode 100644 index 0000000000..b0fbe1ed9f --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetImageView.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +/** + * View that draws a bitmap horizontally centered. If the image width is greater than the view + * width, the image is scaled down appropriately. + */ +public class WidgetImageView extends View { + + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + private final RectF mDstRectF = new RectF(); + private Bitmap mBitmap; + + public WidgetImageView(Context context) { + super(context); + } + + public WidgetImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WidgetImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + invalidate(); + } + + public Bitmap getBitmap() { + return mBitmap; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBitmap != null) { + updateDstRectF(); + canvas.drawBitmap(mBitmap, null, mDstRectF, mPaint); + } + } + + /** + * Prevents the inefficient alpha view rendering. + */ + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private void updateDstRectF() { + if (mBitmap.getWidth() > getWidth()) { + float scale = ((float) getWidth()) / mBitmap.getWidth(); + mDstRectF.set(0, 0, getWidth(), scale * mBitmap.getHeight()); + } else { + mDstRectF.set( + (getWidth() - mBitmap.getWidth()) * 0.5f, + 0, + (getWidth() + mBitmap.getWidth()) * 0.5f, + mBitmap.getHeight()); + } + } + + /** + * @return the bounds where the image was drawn. + */ + public Rect getBitmapBounds() { + updateDstRectF(); + Rect rect = new Rect(); + mDstRectF.round(rect); + return rect; + } +} diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java new file mode 100644 index 0000000000..5afd7c4933 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView.State; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.android.launcher3.BaseContainerView; +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeleteDropTarget; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DragController; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget.DragObject; +import com.android.launcher3.Folder; +import com.android.launcher3.IconCache; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.PendingAddItemInfo; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.Workspace; +import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.util.Thunk; + +/** + * The widgets list view container. + */ +public class WidgetsContainerView extends BaseContainerView + implements View.OnLongClickListener, View.OnClickListener, DragSource{ + + private static final String TAG = "WidgetsContainerView"; + private static final boolean DEBUG = false; + + /* Coefficient multiplied to the screen height for preloading widgets. */ + private static final int PRELOAD_SCREEN_HEIGHT_MULTIPLE = 1; + + /* Global instances that are used inside this container. */ + @Thunk Launcher mLauncher; + private DragController mDragController; + private IconCache mIconCache; + + /* Recycler view related member variables */ + private View mContent; + private WidgetsRecyclerView mView; + private WidgetsListAdapter mAdapter; + + /* Touch handling related member variables. */ + private Toast mWidgetInstructionToast; + + /* Rendering related. */ + private WidgetPreviewLoader mWidgetPreviewLoader; + + private Rect mPadding = new Rect(); + + public WidgetsContainerView(Context context) { + this(context, null); + } + + public WidgetsContainerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = (Launcher) context; + mDragController = mLauncher.getDragController(); + mAdapter = new WidgetsListAdapter(context, this, this, mLauncher); + mIconCache = (LauncherAppState.getInstance()).getIconCache(); + if (DEBUG) { + Log.d(TAG, "WidgetsContainerView constructor"); + } + } + + @Override + protected void onFinishInflate() { + mContent = findViewById(R.id.content); + mView = (WidgetsRecyclerView) findViewById(R.id.widgets_list_view); + mView.setAdapter(mAdapter); + + // This extends the layout space so that preloading happen for the {@link RecyclerView} + mView.setLayoutManager(new LinearLayoutManager(getContext()) { + @Override + protected int getExtraLayoutSpace(State state) { + DeviceProfile grid = mLauncher.getDeviceProfile(); + return super.getExtraLayoutSpace(state) + + grid.availableHeightPx * PRELOAD_SCREEN_HEIGHT_MULTIPLE; + } + }); + mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), + getPaddingBottom()); + } + + // + // Returns views used for launcher transitions. + // + + public View getContentView() { + return mView; + } + + public View getRevealView() { + // TODO(hyunyoungs): temporarily use apps view transition. + return findViewById(R.id.widgets_reveal_view); + } + + public void scrollToTop() { + mView.scrollToPosition(0); + } + + // + // Touch related handling. + // + + @Override + public void onClick(View v) { + // When we have exited widget tray or are in transition, disregard clicks + if (!mLauncher.isWidgetsViewVisible() + || mLauncher.getWorkspace().isSwitchingState() + || !(v instanceof WidgetCell)) return; + + // Let the user know that they have to long press to add a widget + if (mWidgetInstructionToast != null) { + mWidgetInstructionToast.cancel(); + } + mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add, + Toast.LENGTH_SHORT); + mWidgetInstructionToast.show(); + } + + @Override + public boolean onLongClick(View v) { + if (DEBUG) { + Log.d(TAG, String.format("onLonglick [v=%s]", v)); + } + // Return early if this is not initiated from a touch + if (!v.isInTouchMode()) return false; + // When we have exited all apps or are in transition, disregard long clicks + if (!mLauncher.isWidgetsViewVisible() || + mLauncher.getWorkspace().isSwitchingState()) return false; + // Return if global dragging is not enabled + Log.d(TAG, String.format("onLonglick dragging enabled?.", v)); + if (!mLauncher.isDraggingEnabled()) return false; + + boolean status = beginDragging(v); + if (status && v.getTag() instanceof PendingAddWidgetInfo) { + WidgetHostViewLoader hostLoader = new WidgetHostViewLoader(mLauncher, v); + boolean preloadStatus = hostLoader.preloadWidget(); + if (DEBUG) { + Log.d(TAG, String.format("preloading widget [status=%s]", preloadStatus)); + } + mLauncher.getDragController().addDragListener(hostLoader); + } + return status; + } + + private boolean beginDragging(View v) { + if (v instanceof WidgetCell) { + if (!beginDraggingWidget((WidgetCell) v)) { + return false; + } + } else { + Log.e(TAG, "Unexpected dragging view: " + v); + } + + // We don't enter spring-loaded mode if the drag has been cancelled + if (mLauncher.getDragController().isDragging()) { + // Go into spring loaded mode (must happen before we startDrag()) + mLauncher.enterSpringLoadedDragMode(); + } + + return true; + } + + private boolean beginDraggingWidget(WidgetCell v) { + // Get the widget preview as the drag representation + WidgetImageView image = (WidgetImageView) v.findViewById(R.id.widget_preview); + PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag(); + + // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and + // we abort the drag. + if (image.getBitmap() == null) { + return false; + } + + // Compose the drag image + Bitmap preview; + float scale = 1f; + final Rect bounds = image.getBitmapBounds(); + + if (createItemInfo instanceof PendingAddWidgetInfo) { + // This can happen in some weird cases involving multi-touch. We can't start dragging + // the widget if this is null, so we break out. + + PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) createItemInfo; + int[] size = mLauncher.getWorkspace().estimateItemSize(createWidgetInfo, true); + + Bitmap icon = image.getBitmap(); + float minScale = 1.25f; + int maxWidth = Math.min((int) (icon.getWidth() * minScale), size[0]); + + int[] previewSizeBeforeScale = new int[1]; + preview = getWidgetPreviewLoader().generateWidgetPreview(mLauncher, + createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale); + + if (previewSizeBeforeScale[0] < icon.getWidth()) { + // The icon has extra padding around it. + int padding = (icon.getWidth() - previewSizeBeforeScale[0]) / 2; + if (icon.getWidth() > image.getWidth()) { + padding = padding * image.getWidth() / icon.getWidth(); + } + + bounds.left += padding; + bounds.right -= padding; + } + scale = bounds.width() / (float) preview.getWidth(); + } else { + PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag(); + Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo); + preview = Utilities.createIconBitmap(icon, mLauncher); + createItemInfo.spanX = createItemInfo.spanY = 1; + scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / preview.getWidth(); + } + + // Don't clip alpha values for the drag outline if we're using the default widget preview + boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo && + (((PendingAddWidgetInfo) createItemInfo).previewImage == 0)); + + // Start the drag + mLauncher.lockScreenOrientation(); + mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, preview, clipAlpha); + mDragController.startDrag(image, preview, this, createItemInfo, + bounds, DragController.DRAG_ACTION_COPY, scale); + + preview.recycle(); + return true; + } + + // + // Drag related handling methods that implement {@link DragSource} interface. + // + + @Override + public boolean supportsFlingToDelete() { + return false; + } + + @Override + public boolean supportsAppInfoDropTarget() { + return true; + } + + /* + * Both this method and {@link #supportsFlingToDelete} has to return {@code false} for the + * {@link DeleteDropTarget} to be invisible.) + */ + @Override + public boolean supportsDeleteDropTarget() { + return false; + } + + @Override + public float getIntrinsicIconScaleFactor() { + return 0; + } + + @Override + public void onFlingToDeleteCompleted() { + // We just dismiss the drag when we fling, so cleanup here + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + mLauncher.unlockScreenOrientation(false); + } + + @Override + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && + !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { + // Exit spring loaded mode if we have not successfully dropped or have not handled the + // drop in Workspace + mLauncher.exitSpringLoadedDragModeDelayed(true, + Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); + } + mLauncher.unlockScreenOrientation(false); + + // Display an error message if the drag failed due to there not being enough space on the + // target layout we were dropping on. + if (!success) { + boolean showOutOfSpaceMessage = false; + if (target instanceof Workspace) { + int currentScreen = mLauncher.getCurrentWorkspaceScreen(); + Workspace workspace = (Workspace) target; + CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); + ItemInfo itemInfo = (ItemInfo) d.dragInfo; + if (layout != null) { + layout.calculateSpans(itemInfo); + showOutOfSpaceMessage = + !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); + } + } + if (showOutOfSpaceMessage) { + mLauncher.showOutOfSpaceMessage(false); + } + d.deferDragViewCleanupPostAnimation = false; + } + } + + // + // Container rendering related. + // + + @Override + protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) { + // Apply the top-bottom padding to the content itself so that the launcher transition is + // clipped correctly + mContent.setPadding(0, padding.top, 0, padding.bottom); + + // TODO: Use quantum_panel_dark instead of quantum_panel_shape_dark. + InsetDrawable background = new InsetDrawable( + getResources().getDrawable(R.drawable.quantum_panel_shape_dark), padding.left, 0, + padding.right, 0); + Rect bgPadding = new Rect(); + background.getPadding(bgPadding); + mView.setBackground(background); + getRevealView().setBackground(background.getConstantState().newDrawable()); + mView.updateBackgroundPadding(bgPadding); + } + + /** + * Initialize the widget data model. + */ + public void addWidgets(WidgetsModel model) { + mView.setWidgets(model); + mAdapter.setWidgetsModel(model); + mAdapter.notifyDataSetChanged(); + } + + private WidgetPreviewLoader getWidgetPreviewLoader() { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); + } + return mWidgetPreviewLoader; + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java new file mode 100644 index 0000000000..d2ea252302 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.Adapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup.MarginLayoutParams; +import android.widget.LinearLayout; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.model.WidgetsModel; + +import java.util.List; + +/** + * List view adapter for the widget tray. + * + *

Memory vs. Performance: + * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling + * happens and less memory is consumed. {@link #getItemViewType} was not overridden as there is + * only a single type of view. + */ +public class WidgetsListAdapter extends Adapter { + + private static final String TAG = "WidgetsListAdapter"; + private static final boolean DEBUG = false; + + private Launcher mLauncher; + private LayoutInflater mLayoutInflater; + + private WidgetsModel mWidgetsModel; + private WidgetPreviewLoader mWidgetPreviewLoader; + + private View.OnClickListener mIconClickListener; + private View.OnLongClickListener mIconLongClickListener; + + private static final int PRESET_INDENT_SIZE_TABLET = 56; + private int mIndent = 0; + + public WidgetsListAdapter(Context context, + View.OnClickListener iconClickListener, + View.OnLongClickListener iconLongClickListener, + Launcher launcher) { + mLayoutInflater = LayoutInflater.from(context); + + mIconClickListener = iconClickListener; + mIconLongClickListener = iconLongClickListener; + mLauncher = launcher; + + setContainerHeight(); + } + + public void setWidgetsModel(WidgetsModel w) { + mWidgetsModel = w; + } + + @Override + public int getItemCount() { + return mWidgetsModel.getPackageSize(); + } + + @Override + public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) { + List infoList = mWidgetsModel.getSortedWidgets(pos); + + ViewGroup row = ((ViewGroup) holder.getContent().findViewById(R.id.widgets_cell_list)); + if (DEBUG) { + Log.d(TAG, String.format( + "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]", + pos, infoList.size(), row.getChildCount())); + } + + // Add more views. + // if there are too many, hide them. + int diff = infoList.size() - row.getChildCount(); + + if (diff > 0) { + for (int i = 0; i < diff; i++) { + WidgetCell widget = (WidgetCell) mLayoutInflater.inflate( + R.layout.widget_cell, row, false); + + // set up touch. + widget.setOnClickListener(mIconClickListener); + widget.setOnLongClickListener(mIconLongClickListener); + LayoutParams lp = widget.getLayoutParams(); + lp.height = widget.cellSize; + lp.width = widget.cellSize; + widget.setLayoutParams(lp); + + row.addView(widget); + } + } else if (diff < 0) { + for (int i=infoList.size() ; i < row.getChildCount(); i++) { + row.getChildAt(i).setVisibility(View.GONE); + } + } + + // Bind the views in the application info section. + PackageItemInfo infoOut = mWidgetsModel.getPackageItemInfo(pos); + BubbleTextView tv = ((BubbleTextView) holder.getContent().findViewById(R.id.section)); + tv.applyFromPackageItemInfo(infoOut); + + // Bind the view in the widget horizontal tray region. + if (getWidgetPreviewLoader() == null) { + return; + } + for (int i=0; i < infoList.size(); i++) { + WidgetCell widget = (WidgetCell) row.getChildAt(i); + if (infoList.get(i) instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) infoList.get(i); + PendingAddWidgetInfo pawi = new PendingAddWidgetInfo(mLauncher, info, null); + widget.setTag(pawi); + widget.applyFromAppWidgetProviderInfo(info, mWidgetPreviewLoader); + } else if (infoList.get(i) instanceof ResolveInfo) { + ResolveInfo info = (ResolveInfo) infoList.get(i); + PendingAddShortcutInfo pasi = new PendingAddShortcutInfo(info.activityInfo); + widget.setTag(pasi); + widget.applyFromResolveInfo(mLauncher.getPackageManager(), info, mWidgetPreviewLoader); + } + widget.ensurePreview(); + widget.setVisibility(View.VISIBLE); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @Override + public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (DEBUG) { + Log.v(TAG, "\nonCreateViewHolder"); + } + + ViewGroup container = (ViewGroup) mLayoutInflater.inflate( + R.layout.widgets_list_row_view, parent, false); + LinearLayout cellList = (LinearLayout) container.findViewById(R.id.widgets_cell_list); + + // if the end padding is 0, then container view (horizontal scroll view) doesn't respect + // the end of the linear layout width + the start padding and doesn't allow scrolling. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + cellList.setPaddingRelative(mIndent, 0, 1, 0); + } else { + cellList.setPadding(mIndent, 0, 1, 0); + } + + return new WidgetsRowViewHolder(container); + } + + @Override + public void onViewRecycled(WidgetsRowViewHolder holder) { + ViewGroup row = ((ViewGroup) holder.getContent().findViewById(R.id.widgets_cell_list)); + + for (int i = 0; i < row.getChildCount(); i++) { + WidgetCell widget = (WidgetCell) row.getChildAt(i); + widget.clear(); + } + } + + public boolean onFailedToRecycleView(WidgetsRowViewHolder holder) { + // If child views are animating, then the RecyclerView may choose not to recycle the view, + // causing extraneous onCreateViewHolder() calls. It is safe in this case to continue + // recycling this view, and take care in onViewRecycled() to cancel any existing + // animations. + return true; + } + + @Override + public long getItemId(int pos) { + return pos; + } + + private WidgetPreviewLoader getWidgetPreviewLoader() { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache(); + } + return mWidgetPreviewLoader; + } + + private void setContainerHeight() { + Resources r = mLauncher.getResources(); + DeviceProfile profile = mLauncher.getDeviceProfile(); + if (profile.isLargeTablet || profile.isTablet) { + mIndent = Utilities.pxFromDp(PRESET_INDENT_SIZE_TABLET, r.getDisplayMetrics()); + } + } +} diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java new file mode 100644 index 0000000000..61e63cdb76 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.launcher3.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.support.v7.widget.LinearLayoutManager; +import android.util.AttributeSet; +import android.view.View; +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.R; +import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.model.WidgetsModel; + +/** + * The widgets recycler view. + */ +public class WidgetsRecyclerView extends BaseRecyclerView { + + private static final String TAG = "WidgetsRecyclerView"; + private WidgetsModel mWidgets; + private ScrollPositionState mScrollPosState = new ScrollPositionState(); + + public WidgetsRecyclerView(Context context) { + this(context, null); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + // API 21 and below only support 3 parameter ctor. + super(context, attrs, defStyleAttr); + } + + public WidgetsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + this(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + public int getFastScrollerTrackColor(int defaultTrackColor) { + return Color.WHITE; + } + + public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { + return getResources().getColor(R.color.widgets_view_fastscroll_thumb_inactive_color); + } + + /** + * Sets the widget model in this view, used to determine the fast scroll position. + */ + public void setWidgets(WidgetsModel widgets) { + mWidgets = widgets; + } + + /** + * We need to override the draw to ensure that we don't draw the overscroll effect beyond the + * background bounds. + */ + @Override + protected void dispatchDraw(Canvas canvas) { + canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, + getWidth() - mBackgroundPadding.right, + getHeight() - mBackgroundPadding.bottom); + super.dispatchDraw(canvas); + } + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. + */ + @Override + public String scrollToPositionAtProgress(float touchFraction) { + int rowCount = mWidgets.getPackageSize(); + if (rowCount == 0) { + return ""; + } + + // Stop the scroller if it is scrolling + stopScroll(); + + getCurScrollState(mScrollPosState); + float pos = rowCount * touchFraction; + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0); + LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); + layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); + + int posInt = (int) ((touchFraction == 1)? pos -1 : pos); + PackageItemInfo p = mWidgets.getPackageItemInfo(posInt); + return p.titleSectionName; + } + + /** + * Updates the bounds for the scrollbar. + */ + @Override + public void onUpdateScrollbar() { + int rowCount = mWidgets.getPackageSize(); + + // Skip early if, there are no items. + if (rowCount == 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + // Skip early if, there no child laid out in the container. + getCurScrollState(mScrollPosState); + if (mScrollPosState.rowIndex < 0) { + mScrollbar.setScrollbarThumbOffset(-1, -1); + return; + } + + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0); + } + + /** + * Returns the current scroll state. + */ + private void getCurScrollState(ScrollPositionState stateOut) { + stateOut.rowIndex = -1; + stateOut.rowTopOffset = -1; + stateOut.rowHeight = -1; + + int rowCount = mWidgets.getPackageSize(); + + // Return early if there are no items + if (rowCount == 0) { + return; + } + View child = getChildAt(0); + int position = getChildPosition(child); + + stateOut.rowIndex = position; + stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); + stateOut.rowHeight = child.getHeight(); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/LauncherAnimatorUpdateListener.java b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java similarity index 52% rename from src/com/android/launcher3/LauncherAnimatorUpdateListener.java rename to src/com/android/launcher3/widget/WidgetsRowViewHolder.java index ec9fd4d16e..249559ab9f 100644 --- a/src/com/android/launcher3/LauncherAnimatorUpdateListener.java +++ b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.launcher3.widget; -package com.android.launcher3; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; +public class WidgetsRowViewHolder extends ViewHolder { -abstract class LauncherAnimatorUpdateListener implements AnimatorUpdateListener { - public void onAnimationUpdate(ValueAnimator animation) { - final float b = (Float) animation.getAnimatedValue(); - final float a = 1f - b; - onAnimationUpdate(a, b); + ViewGroup mContent; + + public WidgetsRowViewHolder(ViewGroup v) { + super(v); + mContent = v; } - abstract void onAnimationUpdate(float a, float b); -} \ No newline at end of file + ViewGroup getContent() { + return mContent; + } +} diff --git a/tests/Android.mk b/tests/Android.mk index 762a52b8d0..eba4ade486 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -1,4 +1,4 @@ -# Copyright (C) 2011 The Android Open Source Project +# Copyright (C) 2015 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,5 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. # -#LOCAL_PATH := $(call my-dir) -#include $(call all-makefiles-under,$(LOCAL_PATH)) + +LOCAL_PATH := $(call my-dir) + +src_dirs := src +res_dirs := res + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_STATIC_JAVA_LIBRARIES := android-support-test + +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) +LOCAL_AAPT_FLAGS := --auto-add-overlay + +LOCAL_SDK_VERSION := 21 + +LOCAL_PACKAGE_NAME := Launcher3Tests + +LOCAL_INSTRUMENTATION_FOR := Launcher3 + +include $(BUILD_PACKAGE) diff --git a/tests/stress/AndroidManifest.xml b/tests/AndroidManifest.xml similarity index 84% rename from tests/stress/AndroidManifest.xml rename to tests/AndroidManifest.xml index bcca1ff535..42ae5a38a7 100644 --- a/tests/stress/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -1,5 +1,5 @@ - + package="com.android.launcher3.tests"> @@ -24,6 +24,6 @@ + android:label="Unit tests for Launcher3"> diff --git a/tests/res/values/string.xml b/tests/res/values/string.xml new file mode 100644 index 0000000000..3c1ec5c610 --- /dev/null +++ b/tests/res/values/string.xml @@ -0,0 +1,21 @@ + + + + + + Dummy string for tests. + + diff --git a/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java b/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java new file mode 100644 index 0000000000..1bc7c1190f --- /dev/null +++ b/tests/src/com/android/launcher3/InvariantDeviceProfileTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES 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.launcher3; + +import android.graphics.PointF; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.util.FocusLogic; + +import java.util.ArrayList; + +/** + * Tests the {@link DeviceProfile} and {@link InvariantDeviceProfile}. + */ +@SmallTest +public class InvariantDeviceProfileTest extends AndroidTestCase { + + private static final String TAG = "DeviceProfileTest"; + private static final boolean DEBUG = false; + + private InvariantDeviceProfile mInvariantProfile; + private ArrayList mPredefinedDeviceProfiles; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mInvariantProfile = new InvariantDeviceProfile(getContext()); + mPredefinedDeviceProfiles = mInvariantProfile.getPredefinedDeviceProfiles(); + } + + @Override + protected void tearDown() throws Exception { + // Nothing to tear down as this class only tests static methods. + } + + public void testFindClosestDeviceProfile2() { + for (InvariantDeviceProfile idf: mPredefinedDeviceProfiles) { + ArrayList closestProfiles = + mInvariantProfile.findClosestDeviceProfiles( + idf.minWidthDps, idf.minHeightDps, mPredefinedDeviceProfiles); + assertTrue(closestProfiles.get(0).equals(idf)); + } + } + + /** + * Used to print out how the invDistWeightedInterpolate works between device profiles to + * tweak the two constants that control how the interpolation curve is shaped. + */ + public void testInvInterpolation() { + + InvariantDeviceProfile p1 = mPredefinedDeviceProfiles.get(7); // e.g., Large Phone + InvariantDeviceProfile p2 = mPredefinedDeviceProfiles.get(8); // e.g., Nexus 7 + + ArrayList pts = createInterpolatedPoints( + new PointF(p1.minWidthDps, p1.minHeightDps), + new PointF(p2.minWidthDps, p2.minHeightDps), + 20f); + + for (int i = 0; i < pts.size(); i++) { + ArrayList closestProfiles = + mInvariantProfile.findClosestDeviceProfiles( + pts.get(i).x, pts.get(i).y, mPredefinedDeviceProfiles); + InvariantDeviceProfile result = + mInvariantProfile.invDistWeightedInterpolate( + pts.get(i).x, pts.get(i).y, closestProfiles); + if (DEBUG) { + Log.d(TAG, String.format("width x height = (%f, %f)] iconSize = %f", + pts.get(i).x, pts.get(i).y, result.iconSize)); + } + } + } + + private ArrayList createInterpolatedPoints(PointF a, PointF b, float numPts) { + ArrayList result = new ArrayList(); + result.add(a); + for (float i = 1; i < numPts; i = i + 1.0f) { + result.add(new PointF((b.x * i + a.x * (numPts - i)) / numPts, + (b.y * i + a.y * (numPts - i)) / numPts)); + } + result.add(b); + return result; + } + + /** + * Ensures that system calls (e.g., WindowManager, DisplayMetrics) that require contexts are + * properly working to generate minimum width and height of the display. + */ + public void test_hammerhead() { + if (!android.os.Build.DEVICE.equals("hammerhead")) { + return; + } + assertEquals(mInvariantProfile.numRows, 4); + assertEquals(mInvariantProfile.numColumns, 4); + assertEquals((int) mInvariantProfile.numHotseatIcons, 5); + + DeviceProfile landscapeProfile = mInvariantProfile.landscapeProfile; + DeviceProfile portraitProfile = mInvariantProfile.portraitProfile; + + assertEquals(portraitProfile.allAppsNumCols, 3); + assertEquals(landscapeProfile.allAppsNumCols, 5); // not used + } + + // Add more tests for other devices, however, running them once on a single device is enough + // for verifying that for a platform version, the WindowManager and DisplayMetrics is + // working as intended. +} diff --git a/tests/src/com/android/launcher3/util/FocusLogicTest.java b/tests/src/com/android/launcher3/util/FocusLogicTest.java new file mode 100644 index 0000000000..ea87014e96 --- /dev/null +++ b/tests/src/com/android/launcher3/util/FocusLogicTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES 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.launcher3; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.KeyEvent; + +import com.android.launcher3.util.FocusLogic; + +/** + * Tests the {@link FocusLogic} class that handles key event based focus handling. + */ +@SmallTest +public final class FocusLogicTest extends AndroidTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + // Nothing to set up as this class only tests static methods. + } + + @Override + protected void tearDown() throws Exception { + // Nothing to tear down as this class only tests static methods. + } + + public void testShouldConsume() { + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_LEFT)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_RIGHT)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_UP)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_DOWN)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_MOVE_HOME)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_MOVE_END)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_PAGE_UP)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_PAGE_DOWN)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DEL)); + assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_FORWARD_DEL)); + } + + public void testCreateSparseMatrix() { + // Either, 1) create a helper method to generate/instantiate all possible cell layout that + // may get created in real world to test this method. OR 2) Move all the matrix + // management routine to celllayout and write tests for them. + } +} diff --git a/tests/stress/Android.mk b/tests/stress/Android.mk deleted file mode 100644 index 68289bd3ea..0000000000 --- a/tests/stress/Android.mk +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (C) 2011 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -LOCAL_PATH := $(call my-dir) -include $(CLEAR_VARS) - -# We only want this apk build for tests. -LOCAL_MODULE_TAGS := tests - -LOCAL_JAVA_LIBRARIES := android.test.runner - -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_PACKAGE_NAME := LauncherRotationStressTest - -LOCAL_CERTIFICATE := shared - -LOCAL_INSTRUMENTATION_FOR := Launcher2 - -include $(BUILD_PACKAGE) diff --git a/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java b/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java deleted file mode 100644 index a5b85eb19c..0000000000 --- a/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.stress; - - -import com.android.launcher3.Launcher; - -import android.content.pm.ActivityInfo; -import android.os.SystemClock; -import android.test.ActivityInstrumentationTestCase2; -import android.test.RepetitiveTest; -import android.util.Log; - -/** - * Run rotation stress test using Launcher2 for 50 iterations. - */ -public class LauncherRotationStressTest extends ActivityInstrumentationTestCase2 { - - private static final int NUM_ITERATIONS = 50; - private static final int WAIT_TIME_MS = 500; - private static final String LOG_TAG = "LauncherRotationStressTest"; - - public LauncherRotationStressTest() { - super(Launcher.class); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - } - - @Override - protected void tearDown() throws Exception { - super.tearDown(); - } - - @RepetitiveTest(numIterations=NUM_ITERATIONS) - public void testLauncherRotationStress() throws Exception { - Launcher launcher = getActivity(); - getInstrumentation().waitForIdleSync(); - SystemClock.sleep(WAIT_TIME_MS); - launcher.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - getInstrumentation().waitForIdleSync(); - SystemClock.sleep(WAIT_TIME_MS); - launcher.setRequestedOrientation( - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } -} diff --git a/util/com/android/launcher3/DecoderRing.java b/util/com/android/launcher3/DecoderRing.java index 86431d9b7b..0732fe8893 100644 --- a/util/com/android/launcher3/DecoderRing.java +++ b/util/com/android/launcher3/DecoderRing.java @@ -220,9 +220,6 @@ class DecoderRing { if (widget.icon != null) { writeImageData(widget.icon.data, prefix + "_icon.png"); } - if (widget.preview != null) { - writeImageData(widget.preview.data, prefix + "_preview.png"); - } } } }