From e80eb380b7093081298ddf3bae7d9347089378f5 Mon Sep 17 00:00:00 2001 From: Katana Date: Thu, 26 Oct 2023 23:37:14 +0800 Subject: [PATCH] First commit --- app/.gitignore | 5 + app/build.gradle | 79 + app/libs/android_common.jar | Bin 0 -> 138520 bytes app/proguard-rules.pro | 19 + .../launcher22/ExampleInstrumentedTest.java | 26 + app/src/main/AndroidManifest.xml | 165 + .../android/launcher2/AccessibleTabView.java | 51 + .../com/android/launcher2/AddAdapter.java | 103 + .../java/com/android/launcher2/Alarm.java | 84 + .../com/android/launcher2/AllAppsList.java | 221 + .../launcher2/AppWidgetResizeFrame.java | 471 ++ .../android/launcher2/ApplicationInfo.java | 133 + .../launcher2/AppsCustomizePagedView.java | 1729 +++++++ .../launcher2/AppsCustomizeTabHost.java | 482 ++ .../com/android/launcher2/BubbleTextView.java | 342 ++ .../android/launcher2/ButtonDropTarget.java | 166 + .../com/android/launcher2/CellLayout.java | 3338 ++++++++++++++ .../launcher2/CheckLongPressHelper.java | 62 + .../java/com/android/launcher2/Cling.java | 271 ++ .../android/launcher2/DeferredHandler.java | 146 + .../android/launcher2/DeleteDropTarget.java | 437 ++ .../com/android/launcher2/DragController.java | 821 ++++ .../java/com/android/launcher2/DragLayer.java | 804 ++++ .../com/android/launcher2/DragScroller.java | 40 + .../com/android/launcher2/DragSource.java | 45 + .../java/com/android/launcher2/DragView.java | 295 ++ .../launcher2/DrawableStateProxyView.java | 69 + .../com/android/launcher2/DropTarget.java | 184 + .../android/launcher2/FastBitmapDrawable.java | 109 + .../launcher2/FirstFrameAnimatorHelper.java | 141 + .../com/android/launcher2/FocusHelper.java | 898 ++++ .../android/launcher2/FocusOnlyTabWidget.java | 86 + .../java/com/android/launcher2/Folder.java | 1114 +++++ .../com/android/launcher2/FolderEditText.java | 36 + .../com/android/launcher2/FolderIcon.java | 667 +++ .../com/android/launcher2/FolderInfo.java | 111 + .../com/android/launcher2/HandleView.java | 76 + .../HideFromAccessibilityHelper.java | 113 + .../launcher2/HolographicImageView.java | 54 + .../launcher2/HolographicLinearLayout.java | 85 + .../launcher2/HolographicOutlineHelper.java | 221 + .../launcher2/HolographicViewHelper.java | 104 + .../java/com/android/launcher2/Hotseat.java | 147 + .../java/com/android/launcher2/IconCache.java | 229 + .../com/android/launcher2/InfoDropTarget.java | 129 + .../launcher2/InstallShortcutReceiver.java | 363 ++ .../launcher2/InstallWidgetReceiver.java | 195 + .../launcher2/InterruptibleInOutAnimator.java | 131 + .../java/com/android/launcher2/ItemInfo.java | 194 + .../java/com/android/launcher2/Launcher.java | 4059 +++++++++++++++++ .../android/launcher2/LauncherAnimUtils.java | 129 + .../LauncherAnimatorUpdateListener.java | 30 + .../launcher2/LauncherAppWidgetHost.java | 55 + .../launcher2/LauncherAppWidgetHostView.java | 101 + .../launcher2/LauncherAppWidgetInfo.java | 98 + .../launcher2/LauncherApplication.java | 166 + .../com/android/launcher2/LauncherModel.java | 2623 +++++++++++ .../android/launcher2/LauncherProvider.java | 1193 +++++ .../android/launcher2/LauncherSettings.java | 236 + .../com/android/launcher2/LauncherUtils.java | 20 + .../LauncherViewPropertyAnimator.java | 266 ++ .../launcher2/PackageChangedReceiver.java | 19 + .../java/com/android/launcher2/PagedView.java | 1981 ++++++++ .../launcher2/PagedViewCellLayout.java | 505 ++ .../PagedViewCellLayoutChildren.java | 160 + .../launcher2/PagedViewGridLayout.java | 133 + .../com/android/launcher2/PagedViewIcon.java | 92 + .../android/launcher2/PagedViewIconCache.java | 133 + .../android/launcher2/PagedViewWidget.java | 246 + .../launcher2/PagedViewWidgetImageView.java | 49 + .../PagedViewWithDraggableItems.java | 178 + .../android/launcher2/PendingAddItemInfo.java | 107 + .../android/launcher2/PreloadReceiver.java | 51 + .../launcher2/SearchDropTargetBar.java | 239 + .../launcher2/ShortcutAndWidgetContainer.java | 204 + .../com/android/launcher2/ShortcutInfo.java | 162 + .../android/launcher2/SmoothPagedView.java | 188 + .../launcher2/SpringLoadedDragController.java | 62 + .../launcher2/UninstallShortcutReceiver.java | 166 + .../launcher2/UserInitializeReceiver.java | 70 + .../java/com/android/launcher2/Utilities.java | 275 ++ .../android/launcher2/WallpaperChooser.java | 47 + .../WallpaperChooserDialogFragment.java | 360 ++ .../launcher2/WidgetPreviewLoader.java | 610 +++ .../java/com/android/launcher2/Workspace.java | 3893 ++++++++++++++++ app/src/main/res/anim/fade_in_fast.xml | 23 + app/src/main/res/anim/fade_out_fast.xml | 23 + .../res/drawable/all_apps_button_icon.xml | 21 + .../main/res/drawable/apps_customize_bg.png | Bin 0 -> 119 bytes .../res/drawable/bg_appwidget_error.9.png | Bin 0 -> 794 bytes app/src/main/res/drawable/bg_cling1.png | Bin 0 -> 535 bytes app/src/main/res/drawable/bg_cling2.png | Bin 0 -> 517 bytes app/src/main/res/drawable/bg_cling3.png | Bin 0 -> 617 bytes app/src/main/res/drawable/bg_cling4.png | Bin 0 -> 537 bytes app/src/main/res/drawable/bg_cling5.png | Bin 0 -> 228 bytes .../main/res/drawable/btn_cling_normal.9.png | Bin 0 -> 295 bytes .../main/res/drawable/btn_cling_pressed.9.png | Bin 0 -> 420 bytes app/src/main/res/drawable/cling.png | Bin 0 -> 32299 bytes app/src/main/res/drawable/cling_button_bg.xml | 20 + .../res/drawable/divider_launcher_holo.9.png | Bin 0 -> 179 bytes app/src/main/res/drawable/flying_icon_bg.xml | 20 + .../res/drawable/flying_icon_bg_pressed.9.png | Bin 0 -> 1749 bytes .../main/res/drawable/focusable_view_bg.xml | 19 + app/src/main/res/drawable/focused_bg.9.png | Bin 0 -> 230 bytes app/src/main/res/drawable/grid_focused.9.png | Bin 0 -> 213 bytes app/src/main/res/drawable/grid_pressed.9.png | Bin 0 -> 210 bytes app/src/main/res/drawable/grid_selected.9.png | Bin 0 -> 206 bytes app/src/main/res/drawable/hand.png | Bin 0 -> 9752 bytes app/src/main/res/drawable/home_press.9.png | Bin 0 -> 179 bytes .../homescreen_blue_normal_holo.9.png | Bin 0 -> 2936 bytes .../homescreen_blue_strong_holo.9.png | Bin 0 -> 2945 bytes .../res/drawable/hotseat_scrubber_holo.9.png | Bin 0 -> 194 bytes .../res/drawable/hotseat_track_holo.9.png | Bin 0 -> 209 bytes app/src/main/res/drawable/ic_allapps.png | Bin 0 -> 3234 bytes .../main/res/drawable/ic_allapps_pressed.png | Bin 0 -> 2455 bytes .../drawable/ic_home_all_apps_holo_dark.png | Bin 0 -> 889 bytes .../drawable/ic_home_search_normal_holo.png | Bin 0 -> 1071 bytes .../drawable/ic_home_voice_search_holo.png | Bin 0 -> 865 bytes .../ic_launcher_clear_active_holo.png | Bin 0 -> 949 bytes .../ic_launcher_clear_normal_holo.png | Bin 0 -> 802 bytes .../drawable/ic_launcher_info_active_holo.png | Bin 0 -> 2736 bytes .../drawable/ic_launcher_info_normal_holo.png | Bin 0 -> 1518 bytes .../res/drawable/ic_launcher_market_holo.png | Bin 0 -> 1124 bytes .../ic_launcher_trashcan_active_holo.png | Bin 0 -> 1783 bytes .../ic_launcher_trashcan_normal_holo.png | Bin 0 -> 1109 bytes .../res/drawable/info_target_selector.xml | 24 + .../res/drawable/overscroll_glow_left.9.png | Bin 0 -> 552 bytes .../res/drawable/overscroll_glow_right.9.png | Bin 0 -> 525 bytes .../res/drawable/page_hover_left_holo.9.png | Bin 0 -> 256 bytes .../res/drawable/page_hover_right_holo.9.png | Bin 0 -> 254 bytes .../res/drawable/paged_view_indicator.9.png | Bin 0 -> 314 bytes .../res/drawable/portal_container_holo.9.png | Bin 0 -> 864 bytes .../res/drawable/portal_ring_inner_holo.png | Bin 0 -> 4991 bytes .../drawable/portal_ring_inner_nolip_holo.png | Bin 0 -> 1772 bytes .../res/drawable/portal_ring_outer_holo.png | Bin 0 -> 4798 bytes .../main/res/drawable/portal_ring_rest.png | Bin 0 -> 1551 bytes .../res/drawable/remove_target_selector.xml | 24 + app/src/main/res/drawable/search_frame.9.png | Bin 0 -> 342 bytes .../drawable/tab_selected_focused_holo.9.png | Bin 0 -> 198 bytes .../main/res/drawable/tab_selected_holo.9.png | Bin 0 -> 201 bytes .../tab_selected_pressed_focused_holo.9.png | Bin 0 -> 249 bytes .../drawable/tab_selected_pressed_holo.9.png | Bin 0 -> 210 bytes .../tab_unselected_focused_holo.9.png | Bin 0 -> 200 bytes .../res/drawable/tab_unselected_holo.9.png | Bin 0 -> 207 bytes .../tab_unselected_pressed_focused_holo.9.png | Bin 0 -> 252 bytes .../tab_unselected_pressed_holo.9.png | Bin 0 -> 220 bytes .../tab_widget_indicator_selector.xml | 33 + .../drawable/uninstall_target_selector.xml | 24 + .../drawable/wallpaper_gallery_background.xml | 20 + .../res/drawable/wallpaper_gallery_item.xml | 22 + .../res/drawable/widget_container_holo.9.png | Bin 0 -> 335 bytes .../main/res/drawable/widget_preview_tile.png | Bin 0 -> 406 bytes .../drawable/widget_resize_frame_holo.9.png | Bin 0 -> 619 bytes .../drawable/widget_resize_handle_bottom.png | Bin 0 -> 745 bytes .../drawable/widget_resize_handle_left.png | Bin 0 -> 762 bytes .../drawable/widget_resize_handle_right.png | Bin 0 -> 772 bytes .../res/drawable/widget_resize_handle_top.png | Bin 0 -> 750 bytes app/src/main/res/drawable/workspace_bg.9.png | Bin 0 -> 263 bytes app/src/main/res/layout/add_list_item.xml | 25 + app/src/main/res/layout/all_apps_cling.xml | 51 + app/src/main/res/layout/application.xml | 20 + .../res/layout/apps_customize_application.xml | 29 + .../main/res/layout/apps_customize_pane.xml | 99 + .../res/layout/apps_customize_progressbar.xml | 22 + .../main/res/layout/apps_customize_widget.xml | 78 + app/src/main/res/layout/appwidget_error.xml | 29 + .../res/layout/custom_workspace_cling.xml | 28 + app/src/main/res/layout/drop_target_bar.xml | 39 + .../layout/external_widget_drop_list_item.xml | 39 + app/src/main/res/layout/folder_cling.xml | 52 + app/src/main/res/layout/folder_icon.xml | 33 + app/src/main/res/layout/hotseat.xml | 35 + app/src/main/res/layout/launcher.xml | 134 + app/src/main/res/layout/market_button.xml | 30 + app/src/main/res/layout/qsb_bar.xml | 33 + app/src/main/res/layout/rename_folder.xml | 42 + app/src/main/res/layout/scroll_indicator.xml | 21 + app/src/main/res/layout/search_bar.xml | 73 + .../main/res/layout/tab_widget_indicator.xml | 19 + app/src/main/res/layout/user_folder.xml | 53 + app/src/main/res/layout/wallpaper_chooser.xml | 44 + .../res/layout/wallpaper_chooser_base.xml | 28 + app/src/main/res/layout/wallpaper_item.xml | 23 + app/src/main/res/layout/workspace_cling.xml | 66 + app/src/main/res/layout/workspace_divider.xml | 23 + app/src/main/res/layout/workspace_screen.xml | 31 + app/src/main/res/mipmap/ic_launcher.png | Bin 0 -> 2206 bytes .../res/mipmap/ic_launcher_application.png | Bin 0 -> 4953 bytes app/src/main/res/mipmap/ic_launcher_home.png | Bin 0 -> 3116 bytes app/src/main/res/mipmap/ic_launcher_round.png | Bin 0 -> 2555 bytes .../main/res/mipmap/ic_launcher_wallpaper.png | Bin 0 -> 2871 bytes app/src/main/res/values-zh/strings.xml | 108 + app/src/main/res/values/attrs.xml | 166 + app/src/main/res/values/colors.xml | 33 + app/src/main/res/values/config.xml | 89 + app/src/main/res/values/dimens.xml | 194 + app/src/main/res/values/extra_wallpapers.xml | 21 + app/src/main/res/values/strings.xml | 272 ++ app/src/main/res/values/styles.xml | 197 + app/src/main/res/values/wallpapers.xml | 33 + app/src/main/res/xml/default_workspace.xml | 90 + app/src/main/res/xml/update_workspace.xml | 49 + .../main/res/xml/wallpaper_picker_preview.xml | 19 + .../test/qqy/launcher22/ExampleUnitTest.java | 17 + build.gradle | 27 + gradle.properties | 25 + settings.gradle | 1 + 207 files changed, 37510 insertions(+) create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/libs/android_common.jar create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/test/qqy/launcher22/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/android/launcher2/AccessibleTabView.java create mode 100644 app/src/main/java/com/android/launcher2/AddAdapter.java create mode 100644 app/src/main/java/com/android/launcher2/Alarm.java create mode 100644 app/src/main/java/com/android/launcher2/AllAppsList.java create mode 100644 app/src/main/java/com/android/launcher2/AppWidgetResizeFrame.java create mode 100644 app/src/main/java/com/android/launcher2/ApplicationInfo.java create mode 100644 app/src/main/java/com/android/launcher2/AppsCustomizePagedView.java create mode 100644 app/src/main/java/com/android/launcher2/AppsCustomizeTabHost.java create mode 100644 app/src/main/java/com/android/launcher2/BubbleTextView.java create mode 100644 app/src/main/java/com/android/launcher2/ButtonDropTarget.java create mode 100644 app/src/main/java/com/android/launcher2/CellLayout.java create mode 100644 app/src/main/java/com/android/launcher2/CheckLongPressHelper.java create mode 100644 app/src/main/java/com/android/launcher2/Cling.java create mode 100644 app/src/main/java/com/android/launcher2/DeferredHandler.java create mode 100644 app/src/main/java/com/android/launcher2/DeleteDropTarget.java create mode 100644 app/src/main/java/com/android/launcher2/DragController.java create mode 100644 app/src/main/java/com/android/launcher2/DragLayer.java create mode 100644 app/src/main/java/com/android/launcher2/DragScroller.java create mode 100644 app/src/main/java/com/android/launcher2/DragSource.java create mode 100644 app/src/main/java/com/android/launcher2/DragView.java create mode 100644 app/src/main/java/com/android/launcher2/DrawableStateProxyView.java create mode 100644 app/src/main/java/com/android/launcher2/DropTarget.java create mode 100644 app/src/main/java/com/android/launcher2/FastBitmapDrawable.java create mode 100644 app/src/main/java/com/android/launcher2/FirstFrameAnimatorHelper.java create mode 100644 app/src/main/java/com/android/launcher2/FocusHelper.java create mode 100644 app/src/main/java/com/android/launcher2/FocusOnlyTabWidget.java create mode 100644 app/src/main/java/com/android/launcher2/Folder.java create mode 100644 app/src/main/java/com/android/launcher2/FolderEditText.java create mode 100644 app/src/main/java/com/android/launcher2/FolderIcon.java create mode 100644 app/src/main/java/com/android/launcher2/FolderInfo.java create mode 100644 app/src/main/java/com/android/launcher2/HandleView.java create mode 100644 app/src/main/java/com/android/launcher2/HideFromAccessibilityHelper.java create mode 100644 app/src/main/java/com/android/launcher2/HolographicImageView.java create mode 100644 app/src/main/java/com/android/launcher2/HolographicLinearLayout.java create mode 100644 app/src/main/java/com/android/launcher2/HolographicOutlineHelper.java create mode 100644 app/src/main/java/com/android/launcher2/HolographicViewHelper.java create mode 100644 app/src/main/java/com/android/launcher2/Hotseat.java create mode 100644 app/src/main/java/com/android/launcher2/IconCache.java create mode 100644 app/src/main/java/com/android/launcher2/InfoDropTarget.java create mode 100644 app/src/main/java/com/android/launcher2/InstallShortcutReceiver.java create mode 100644 app/src/main/java/com/android/launcher2/InstallWidgetReceiver.java create mode 100644 app/src/main/java/com/android/launcher2/InterruptibleInOutAnimator.java create mode 100644 app/src/main/java/com/android/launcher2/ItemInfo.java create mode 100644 app/src/main/java/com/android/launcher2/Launcher.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherAnimUtils.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherAnimatorUpdateListener.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherAppWidgetHost.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherAppWidgetHostView.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherAppWidgetInfo.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherApplication.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherModel.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherProvider.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherSettings.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherUtils.java create mode 100644 app/src/main/java/com/android/launcher2/LauncherViewPropertyAnimator.java create mode 100644 app/src/main/java/com/android/launcher2/PackageChangedReceiver.java create mode 100644 app/src/main/java/com/android/launcher2/PagedView.java create mode 100644 app/src/main/java/com/android/launcher2/PagedViewCellLayout.java create mode 100644 app/src/main/java/com/android/launcher2/PagedViewCellLayoutChildren.java create mode 100644 app/src/main/java/com/android/launcher2/PagedViewGridLayout.java create mode 100644 app/src/main/java/com/android/launcher2/PagedViewIcon.java create mode 100644 app/src/main/java/com/android/launcher2/PagedViewIconCache.java create mode 100644 app/src/main/java/com/android/launcher2/PagedViewWidget.java create mode 100644 app/src/main/java/com/android/launcher2/PagedViewWidgetImageView.java create mode 100644 app/src/main/java/com/android/launcher2/PagedViewWithDraggableItems.java create mode 100644 app/src/main/java/com/android/launcher2/PendingAddItemInfo.java create mode 100644 app/src/main/java/com/android/launcher2/PreloadReceiver.java create mode 100644 app/src/main/java/com/android/launcher2/SearchDropTargetBar.java create mode 100644 app/src/main/java/com/android/launcher2/ShortcutAndWidgetContainer.java create mode 100644 app/src/main/java/com/android/launcher2/ShortcutInfo.java create mode 100644 app/src/main/java/com/android/launcher2/SmoothPagedView.java create mode 100644 app/src/main/java/com/android/launcher2/SpringLoadedDragController.java create mode 100644 app/src/main/java/com/android/launcher2/UninstallShortcutReceiver.java create mode 100644 app/src/main/java/com/android/launcher2/UserInitializeReceiver.java create mode 100644 app/src/main/java/com/android/launcher2/Utilities.java create mode 100644 app/src/main/java/com/android/launcher2/WallpaperChooser.java create mode 100644 app/src/main/java/com/android/launcher2/WallpaperChooserDialogFragment.java create mode 100644 app/src/main/java/com/android/launcher2/WidgetPreviewLoader.java create mode 100644 app/src/main/java/com/android/launcher2/Workspace.java create mode 100644 app/src/main/res/anim/fade_in_fast.xml create mode 100644 app/src/main/res/anim/fade_out_fast.xml create mode 100644 app/src/main/res/drawable/all_apps_button_icon.xml create mode 100644 app/src/main/res/drawable/apps_customize_bg.png create mode 100644 app/src/main/res/drawable/bg_appwidget_error.9.png create mode 100644 app/src/main/res/drawable/bg_cling1.png create mode 100644 app/src/main/res/drawable/bg_cling2.png create mode 100644 app/src/main/res/drawable/bg_cling3.png create mode 100644 app/src/main/res/drawable/bg_cling4.png create mode 100644 app/src/main/res/drawable/bg_cling5.png create mode 100644 app/src/main/res/drawable/btn_cling_normal.9.png create mode 100644 app/src/main/res/drawable/btn_cling_pressed.9.png create mode 100644 app/src/main/res/drawable/cling.png create mode 100644 app/src/main/res/drawable/cling_button_bg.xml create mode 100644 app/src/main/res/drawable/divider_launcher_holo.9.png create mode 100644 app/src/main/res/drawable/flying_icon_bg.xml create mode 100644 app/src/main/res/drawable/flying_icon_bg_pressed.9.png create mode 100644 app/src/main/res/drawable/focusable_view_bg.xml create mode 100644 app/src/main/res/drawable/focused_bg.9.png create mode 100644 app/src/main/res/drawable/grid_focused.9.png create mode 100644 app/src/main/res/drawable/grid_pressed.9.png create mode 100644 app/src/main/res/drawable/grid_selected.9.png create mode 100644 app/src/main/res/drawable/hand.png create mode 100644 app/src/main/res/drawable/home_press.9.png create mode 100644 app/src/main/res/drawable/homescreen_blue_normal_holo.9.png create mode 100644 app/src/main/res/drawable/homescreen_blue_strong_holo.9.png create mode 100644 app/src/main/res/drawable/hotseat_scrubber_holo.9.png create mode 100644 app/src/main/res/drawable/hotseat_track_holo.9.png create mode 100644 app/src/main/res/drawable/ic_allapps.png create mode 100644 app/src/main/res/drawable/ic_allapps_pressed.png create mode 100644 app/src/main/res/drawable/ic_home_all_apps_holo_dark.png create mode 100644 app/src/main/res/drawable/ic_home_search_normal_holo.png create mode 100644 app/src/main/res/drawable/ic_home_voice_search_holo.png create mode 100644 app/src/main/res/drawable/ic_launcher_clear_active_holo.png create mode 100644 app/src/main/res/drawable/ic_launcher_clear_normal_holo.png create mode 100644 app/src/main/res/drawable/ic_launcher_info_active_holo.png create mode 100644 app/src/main/res/drawable/ic_launcher_info_normal_holo.png create mode 100644 app/src/main/res/drawable/ic_launcher_market_holo.png create mode 100644 app/src/main/res/drawable/ic_launcher_trashcan_active_holo.png create mode 100644 app/src/main/res/drawable/ic_launcher_trashcan_normal_holo.png create mode 100644 app/src/main/res/drawable/info_target_selector.xml create mode 100644 app/src/main/res/drawable/overscroll_glow_left.9.png create mode 100644 app/src/main/res/drawable/overscroll_glow_right.9.png create mode 100644 app/src/main/res/drawable/page_hover_left_holo.9.png create mode 100644 app/src/main/res/drawable/page_hover_right_holo.9.png create mode 100644 app/src/main/res/drawable/paged_view_indicator.9.png create mode 100644 app/src/main/res/drawable/portal_container_holo.9.png create mode 100644 app/src/main/res/drawable/portal_ring_inner_holo.png create mode 100644 app/src/main/res/drawable/portal_ring_inner_nolip_holo.png create mode 100644 app/src/main/res/drawable/portal_ring_outer_holo.png create mode 100644 app/src/main/res/drawable/portal_ring_rest.png create mode 100644 app/src/main/res/drawable/remove_target_selector.xml create mode 100644 app/src/main/res/drawable/search_frame.9.png create mode 100644 app/src/main/res/drawable/tab_selected_focused_holo.9.png create mode 100644 app/src/main/res/drawable/tab_selected_holo.9.png create mode 100644 app/src/main/res/drawable/tab_selected_pressed_focused_holo.9.png create mode 100644 app/src/main/res/drawable/tab_selected_pressed_holo.9.png create mode 100644 app/src/main/res/drawable/tab_unselected_focused_holo.9.png create mode 100644 app/src/main/res/drawable/tab_unselected_holo.9.png create mode 100644 app/src/main/res/drawable/tab_unselected_pressed_focused_holo.9.png create mode 100644 app/src/main/res/drawable/tab_unselected_pressed_holo.9.png create mode 100644 app/src/main/res/drawable/tab_widget_indicator_selector.xml create mode 100644 app/src/main/res/drawable/uninstall_target_selector.xml create mode 100644 app/src/main/res/drawable/wallpaper_gallery_background.xml create mode 100644 app/src/main/res/drawable/wallpaper_gallery_item.xml create mode 100644 app/src/main/res/drawable/widget_container_holo.9.png create mode 100644 app/src/main/res/drawable/widget_preview_tile.png create mode 100644 app/src/main/res/drawable/widget_resize_frame_holo.9.png create mode 100644 app/src/main/res/drawable/widget_resize_handle_bottom.png create mode 100644 app/src/main/res/drawable/widget_resize_handle_left.png create mode 100644 app/src/main/res/drawable/widget_resize_handle_right.png create mode 100644 app/src/main/res/drawable/widget_resize_handle_top.png create mode 100644 app/src/main/res/drawable/workspace_bg.9.png create mode 100644 app/src/main/res/layout/add_list_item.xml create mode 100644 app/src/main/res/layout/all_apps_cling.xml create mode 100644 app/src/main/res/layout/application.xml create mode 100644 app/src/main/res/layout/apps_customize_application.xml create mode 100644 app/src/main/res/layout/apps_customize_pane.xml create mode 100644 app/src/main/res/layout/apps_customize_progressbar.xml create mode 100644 app/src/main/res/layout/apps_customize_widget.xml create mode 100644 app/src/main/res/layout/appwidget_error.xml create mode 100644 app/src/main/res/layout/custom_workspace_cling.xml create mode 100644 app/src/main/res/layout/drop_target_bar.xml create mode 100644 app/src/main/res/layout/external_widget_drop_list_item.xml create mode 100644 app/src/main/res/layout/folder_cling.xml create mode 100644 app/src/main/res/layout/folder_icon.xml create mode 100644 app/src/main/res/layout/hotseat.xml create mode 100644 app/src/main/res/layout/launcher.xml create mode 100644 app/src/main/res/layout/market_button.xml create mode 100644 app/src/main/res/layout/qsb_bar.xml create mode 100644 app/src/main/res/layout/rename_folder.xml create mode 100644 app/src/main/res/layout/scroll_indicator.xml create mode 100644 app/src/main/res/layout/search_bar.xml create mode 100644 app/src/main/res/layout/tab_widget_indicator.xml create mode 100644 app/src/main/res/layout/user_folder.xml create mode 100644 app/src/main/res/layout/wallpaper_chooser.xml create mode 100644 app/src/main/res/layout/wallpaper_chooser_base.xml create mode 100644 app/src/main/res/layout/wallpaper_item.xml create mode 100644 app/src/main/res/layout/workspace_cling.xml create mode 100644 app/src/main/res/layout/workspace_divider.xml create mode 100644 app/src/main/res/layout/workspace_screen.xml create mode 100644 app/src/main/res/mipmap/ic_launcher.png create mode 100644 app/src/main/res/mipmap/ic_launcher_application.png create mode 100644 app/src/main/res/mipmap/ic_launcher_home.png create mode 100644 app/src/main/res/mipmap/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap/ic_launcher_wallpaper.png create mode 100644 app/src/main/res/values-zh/strings.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/config.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/extra_wallpapers.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/wallpapers.xml create mode 100644 app/src/main/res/xml/default_workspace.xml create mode 100644 app/src/main/res/xml/update_workspace.xml create mode 100644 app/src/main/res/xml/wallpaper_picker_preview.xml create mode 100644 app/src/test/java/com/test/qqy/launcher22/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 settings.gradle diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..33ae131 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,5 @@ +/build +/release +*.apk +/libs/foza-release.aar +/debug/ diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..bb905bb --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,79 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdk 34 + defaultConfig { + applicationId "net_62v.launcher" + minSdkVersion 21 + targetSdk 34 + versionCode 1 + versionName "1.0" + android { + defaultConfig { + ndk { + abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" + } + } + } + splits { + abi { + enable true + reset() + include 'x86', 'x86_64', 'armeabi-v7a', "arm64-v8a" + universalApk true + } + } + } + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + res.srcDirs = [ + 'src/main/res2', + 'src/main/res' + ] + } + } + buildTypes { + debug { + debuggable false + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + } + lint { + abortOnError false + checkReleaseBuilds false + } + kotlinOptions { + jvmTarget = '1.8' + freeCompilerArgs = [ + '-Xno-param-assertions', + '-Xno-call-assertions', + '-Xno-receiver-assertions' + ] + } + namespace 'com.android.launcher' +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation fileTree(include: ['*.aar'], dir: 'libs') + implementation "androidx.annotation:annotation:1.7.0" + implementation 'com.jakewharton.android.repackaged:dalvik-dx:11.0.0_r3' +} diff --git a/app/libs/android_common.jar b/app/libs/android_common.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1cb003c0a0b4a64732f190e674de276394e154e GIT binary patch literal 138520 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
    `2@bE%`Y%@pTV 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

    elv!EjRak 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 Ogr8e5QVp@{H|hUtkBkEV literal 0 HcmV?d00001 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..c2356f8 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,19 @@ +-ignorewarnings +-optimizationpasses 7 +-allowaccessmodification +-keep class android.** { *; } +-keepclassmembers class * { + native ; +} +#-keepclasseswithmembernames,includedescriptorclasses class * { +# native ; +#} +-keepclasseswithmembers,allowshrinking class * { + native ; +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} +-dontwarn * +-repackageclasses \ No newline at end of file diff --git a/app/src/androidTest/java/com/test/qqy/launcher22/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/test/qqy/launcher22/ExampleInstrumentedTest.java new file mode 100644 index 0000000..236a012 --- /dev/null +++ b/app/src/androidTest/java/com/test/qqy/launcher22/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.test.qqy.launcher22; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.test.qqy.launcher22", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dc00d23 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/android/launcher2/AccessibleTabView.java b/app/src/main/java/com/android/launcher2/AccessibleTabView.java new file mode 100644 index 0000000..101f139 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/AccessibleTabView.java @@ -0,0 +1,51 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.TextView; + +/** + * We use a custom tab view to process our own focus traversals. + */ +public class AccessibleTabView extends TextView { + public AccessibleTabView(Context context) { + super(context); + } + + public AccessibleTabView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AccessibleTabView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return FocusHelper.handleTabKeyEvent(this, keyCode, event) + || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return FocusHelper.handleTabKeyEvent(this, keyCode, event) + || super.onKeyUp(keyCode, event); + } +} diff --git a/app/src/main/java/com/android/launcher2/AddAdapter.java b/app/src/main/java/com/android/launcher2/AddAdapter.java new file mode 100644 index 0000000..c0bb17b --- /dev/null +++ b/app/src/main/java/com/android/launcher2/AddAdapter.java @@ -0,0 +1,103 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import java.util.ArrayList; + +import com.android.launcher.R; + +/** + * Adapter showing the types of items that can be added to a {@link Workspace}. + */ +public class AddAdapter extends BaseAdapter { + + private final LayoutInflater mInflater; + + private final ArrayList mItems = new ArrayList(); + + public static final int ITEM_SHORTCUT = 0; + public static final int ITEM_APPWIDGET = 1; + public static final int ITEM_APPLICATION = 2; + public static final int ITEM_WALLPAPER = 3; + + /** + * Specific item in our list. + */ + public class ListItem { + public final CharSequence text; + public final Drawable image; + public final int actionTag; + + public ListItem(Resources res, int textResourceId, int imageResourceId, int actionTag) { + text = res.getString(textResourceId); + if (imageResourceId != -1) { + image = res.getDrawable(imageResourceId); + } else { + image = null; + } + this.actionTag = actionTag; + } + } + + public AddAdapter(Launcher launcher) { + super(); + + mInflater = (LayoutInflater) launcher.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + // Create default actions + Resources res = launcher.getResources(); + + mItems.add(new ListItem(res, R.string.group_wallpapers, + R.mipmap.ic_launcher_wallpaper, ITEM_WALLPAPER)); + } + + public View getView(int position, View convertView, ViewGroup parent) { + ListItem item = (ListItem) getItem(position); + + if (convertView == null) { + convertView = mInflater.inflate(R.layout.add_list_item, parent, false); + } + + TextView textView = (TextView) convertView; + textView.setTag(item); + textView.setText(item.text); + textView.setCompoundDrawablesWithIntrinsicBounds(item.image, null, null, null); + + return convertView; + } + + public int getCount() { + return mItems.size(); + } + + public Object getItem(int position) { + return mItems.get(position); + } + + public long getItemId(int position) { + return position; + } +} diff --git a/app/src/main/java/com/android/launcher2/Alarm.java b/app/src/main/java/com/android/launcher2/Alarm.java new file mode 100644 index 0000000..7cd21c3 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/Alarm.java @@ -0,0 +1,84 @@ +/* + * 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.launcher2; + +import android.os.Handler; + +public class Alarm implements Runnable{ + // if we reach this time and the alarm hasn't been cancelled, call the listener + private long mAlarmTriggerTime; + + // if we've scheduled a call to run() (ie called mHandler.postDelayed), this variable is true. + // We use this to avoid having multiple pending callbacks + private boolean mWaitingForCallback; + + private Handler mHandler; + private OnAlarmListener mAlarmListener; + private boolean mAlarmPending = false; + + public Alarm() { + mHandler = new Handler(); + } + + public void setOnAlarmListener(OnAlarmListener alarmListener) { + mAlarmListener = alarmListener; + } + + // Sets the alarm to go off in a certain number of milliseconds. If the alarm is already set, + // it's overwritten and only the new alarm setting is used + public void setAlarm(long millisecondsInFuture) { + long currentTime = System.currentTimeMillis(); + mAlarmPending = true; + mAlarmTriggerTime = currentTime + millisecondsInFuture; + if (!mWaitingForCallback) { + mHandler.postDelayed(this, mAlarmTriggerTime - currentTime); + mWaitingForCallback = true; + } + } + + public void cancelAlarm() { + mAlarmTriggerTime = 0; + mAlarmPending = false; + } + + // this is called when our timer runs out + public void run() { + mWaitingForCallback = false; + if (mAlarmTriggerTime != 0) { + long currentTime = System.currentTimeMillis(); + if (mAlarmTriggerTime > currentTime) { + // We still need to wait some time to trigger spring loaded mode-- + // post a new callback + mHandler.postDelayed(this, Math.max(0, mAlarmTriggerTime - currentTime)); + mWaitingForCallback = true; + } else { + mAlarmPending = false; + if (mAlarmListener != null) { + mAlarmListener.onAlarm(this); + } + } + } + } + + public boolean alarmPending() { + return mAlarmPending; + } +} + +interface OnAlarmListener { + public void onAlarm(Alarm alarm); +} diff --git a/app/src/main/java/com/android/launcher2/AllAppsList.java b/app/src/main/java/com/android/launcher2/AllAppsList.java new file mode 100644 index 0000000..051b0bd --- /dev/null +++ b/app/src/main/java/com/android/launcher2/AllAppsList.java @@ -0,0 +1,221 @@ +/* + * 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.launcher2; + +import java.util.ArrayList; +import java.util.List; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + + +/** + * Stores the list of all applications for the all apps view. + */ +class AllAppsList { + public static final int DEFAULT_APPLICATIONS_NUMBER = 42; + + /** The list off all apps. */ + public ArrayList data = + new ArrayList(DEFAULT_APPLICATIONS_NUMBER); + /** The list of apps that have been added since the last notify() call. */ + public ArrayList added = + new ArrayList(DEFAULT_APPLICATIONS_NUMBER); + /** The list of apps that have been removed since the last notify() call. */ + public ArrayList removed = new ArrayList(); + /** The list of apps that have been modified since the last notify() call. */ + public ArrayList modified = new ArrayList(); + + private IconCache mIconCache; + + /** + * Boring constructor. + */ + public AllAppsList(IconCache iconCache) { + mIconCache = iconCache; + } + + /** + * Add the supplied ApplicationInfo objects to the list, and enqueue it into the + * list to broadcast when notify() is called. + * + * If the app is already in the list, doesn't add it. + */ + public void add(ApplicationInfo info) { + if (findActivity(data, info.componentName)) { + return; + } + data.add(info); + added.add(info); + } + + public void clear() { + data.clear(); + // TODO: do we clear these too? + added.clear(); + removed.clear(); + modified.clear(); + } + + public int size() { + return data.size(); + } + + public ApplicationInfo get(int index) { + return data.get(index); + } + + /** + * Add the icons for the supplied apk called packageName. + */ + public void addPackage(Context context, String packageName) { + final List matches = findActivitiesForPackage(context, packageName); + + if (matches.size() > 0) { + for (ResolveInfo info : matches) { + add(new ApplicationInfo(context.getPackageManager(), info, mIconCache, null)); + } + } + } + + /** + * Remove the apps for the given apk identified by packageName. + */ + public void removePackage(String packageName) { + final List data = this.data; + for (int i = data.size() - 1; i >= 0; i--) { + ApplicationInfo info = data.get(i); + final ComponentName component = info.intent.getComponent(); + if (packageName.equals(component.getPackageName())) { + removed.add(info); + data.remove(i); + } + } + // This is more aggressive than it needs to be. + mIconCache.flush(); + } + + /** + * Add and remove icons for this package which has been updated. + */ + public void updatePackage(Context context, String packageName) { + final List matches = findActivitiesForPackage(context, packageName); + if (matches.size() > 0) { + // Find disabled/removed activities and remove them from data and add them + // to the removed list. + for (int i = data.size() - 1; i >= 0; i--) { + final ApplicationInfo applicationInfo = data.get(i); + final ComponentName component = applicationInfo.intent.getComponent(); + if (packageName.equals(component.getPackageName())) { + if (!findActivity(matches, component)) { + removed.add(applicationInfo); + mIconCache.remove(component); + data.remove(i); + } + } + } + + // Find enabled activities and add them to the adapter + // Also updates existing activities with new labels/icons + int count = matches.size(); + for (int i = 0; i < count; i++) { + final ResolveInfo info = matches.get(i); + ApplicationInfo applicationInfo = findApplicationInfoLocked( + info.activityInfo.applicationInfo.packageName, + info.activityInfo.name); + if (applicationInfo == null) { + add(new ApplicationInfo(context.getPackageManager(), info, mIconCache, null)); + } else { + mIconCache.remove(applicationInfo.componentName); + mIconCache.getTitleAndIcon(applicationInfo, info, null); + modified.add(applicationInfo); + } + } + } else { + // Remove all data for this package. + for (int i = data.size() - 1; i >= 0; i--) { + final ApplicationInfo applicationInfo = data.get(i); + final ComponentName component = applicationInfo.intent.getComponent(); + if (packageName.equals(component.getPackageName())) { + removed.add(applicationInfo); + mIconCache.remove(component); + data.remove(i); + } + } + } + } + + /** + * Query the package manager for MAIN/LAUNCHER activities in the supplied package. + */ + private static List findActivitiesForPackage(Context context, String packageName) { + final PackageManager packageManager = context.getPackageManager(); + + final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + mainIntent.setPackage(packageName); + + final List apps = packageManager.queryIntentActivities(mainIntent, 0); + return apps != null ? apps : new ArrayList(); + } + + /** + * Returns whether apps contains component. + */ + private static boolean findActivity(List apps, ComponentName component) { + final String className = component.getClassName(); + for (ResolveInfo info : apps) { + final ActivityInfo activityInfo = info.activityInfo; + if (activityInfo.name.equals(className)) { + return true; + } + } + return false; + } + + /** + * Returns whether apps contains component. + */ + private static boolean findActivity(ArrayList apps, ComponentName component) { + final int N = apps.size(); + for (int i=0; i getWidth() - mTouchTargetWidth) && horizontalActive; + mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive; + mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment) + && verticalActive; + + boolean anyBordersActive = mLeftBorderActive || mRightBorderActive + || mTopBorderActive || mBottomBorderActive; + + mBaselineWidth = getMeasuredWidth(); + mBaselineHeight = getMeasuredHeight(); + mBaselineX = getLeft(); + mBaselineY = getTop(); + + if (anyBordersActive) { + mLeftHandle.setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); + mRightHandle.setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA); + mTopHandle.setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); + mBottomHandle.setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA); + } + return anyBordersActive; + } + + /** + * Here we bound the deltas such that the frame cannot be stretched beyond the extents + * of the CellLayout, and such that the frame's borders can't cross. + */ + public void updateDeltas(int deltaX, int deltaY) { + if (mLeftBorderActive) { + mDeltaX = Math.max(-mBaselineX, deltaX); + mDeltaX = Math.min(mBaselineWidth - 2 * mTouchTargetWidth, mDeltaX); + } else if (mRightBorderActive) { + mDeltaX = Math.min(mDragLayer.getWidth() - (mBaselineX + mBaselineWidth), deltaX); + mDeltaX = Math.max(-mBaselineWidth + 2 * mTouchTargetWidth, mDeltaX); + } + + if (mTopBorderActive) { + mDeltaY = Math.max(-mBaselineY, deltaY); + mDeltaY = Math.min(mBaselineHeight - 2 * mTouchTargetWidth, mDeltaY); + } else if (mBottomBorderActive) { + mDeltaY = Math.min(mDragLayer.getHeight() - (mBaselineY + mBaselineHeight), deltaY); + mDeltaY = Math.max(-mBaselineHeight + 2 * mTouchTargetWidth, mDeltaY); + } + } + + public void visualizeResizeForDelta(int deltaX, int deltaY) { + visualizeResizeForDelta(deltaX, deltaY, false); + } + + /** + * Based on the deltas, we resize the frame, and, if needed, we resize the widget. + */ + private void visualizeResizeForDelta(int deltaX, int deltaY, boolean onDismiss) { + updateDeltas(deltaX, deltaY); + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + + if (mLeftBorderActive) { + lp.x = mBaselineX + mDeltaX; + lp.width = mBaselineWidth - mDeltaX; + } else if (mRightBorderActive) { + lp.width = mBaselineWidth + mDeltaX; + } + + if (mTopBorderActive) { + lp.y = mBaselineY + mDeltaY; + lp.height = mBaselineHeight - mDeltaY; + } else if (mBottomBorderActive) { + lp.height = mBaselineHeight + mDeltaY; + } + + resizeWidgetIfNeeded(onDismiss); + requestLayout(); + } + + /** + * Based on the current deltas, we determine if and how to resize the widget. + */ + private void resizeWidgetIfNeeded(boolean onDismiss) { + int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap(); + int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap(); + + int deltaX = mDeltaX + mDeltaXAddOn; + int deltaY = mDeltaY + mDeltaYAddOn; + + float hSpanIncF = 1.0f * deltaX / xThreshold - mRunningHInc; + float vSpanIncF = 1.0f * deltaY / yThreshold - mRunningVInc; + + int hSpanInc = 0; + int vSpanInc = 0; + int cellXInc = 0; + int cellYInc = 0; + + int countX = mCellLayout.getCountX(); + int countY = mCellLayout.getCountY(); + + if (Math.abs(hSpanIncF) > RESIZE_THRESHOLD) { + hSpanInc = Math.round(hSpanIncF); + } + if (Math.abs(vSpanIncF) > RESIZE_THRESHOLD) { + vSpanInc = Math.round(vSpanIncF); + } + + if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return; + + + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams(); + + int spanX = lp.cellHSpan; + int spanY = lp.cellVSpan; + int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX; + int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY; + + int hSpanDelta = 0; + int vSpanDelta = 0; + + // For each border, we bound the resizing based on the minimum width, and the maximum + // expandability. + if (mLeftBorderActive) { + cellXInc = Math.max(-cellX, hSpanInc); + cellXInc = Math.min(lp.cellHSpan - mMinHSpan, cellXInc); + hSpanInc *= -1; + hSpanInc = Math.min(cellX, hSpanInc); + hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc); + hSpanDelta = -hSpanInc; + + } else if (mRightBorderActive) { + hSpanInc = Math.min(countX - (cellX + spanX), hSpanInc); + hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc); + hSpanDelta = hSpanInc; + } + + if (mTopBorderActive) { + cellYInc = Math.max(-cellY, vSpanInc); + cellYInc = Math.min(lp.cellVSpan - mMinVSpan, cellYInc); + vSpanInc *= -1; + vSpanInc = Math.min(cellY, vSpanInc); + vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc); + vSpanDelta = -vSpanInc; + } else if (mBottomBorderActive) { + vSpanInc = Math.min(countY - (cellY + spanY), vSpanInc); + vSpanInc = Math.max(-(lp.cellVSpan - mMinVSpan), vSpanInc); + vSpanDelta = vSpanInc; + } + + mDirectionVector[0] = 0; + mDirectionVector[1] = 0; + // Update the widget's dimensions and position according to the deltas computed above + if (mLeftBorderActive || mRightBorderActive) { + spanX += hSpanInc; + cellX += cellXInc; + if (hSpanDelta != 0) { + mDirectionVector[0] = mLeftBorderActive ? -1 : 1; + } + } + + if (mTopBorderActive || mBottomBorderActive) { + spanY += vSpanInc; + cellY += cellYInc; + if (vSpanDelta != 0) { + mDirectionVector[1] = mTopBorderActive ? -1 : 1; + } + } + + if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return; + + // We always want the final commit to match the feedback, so we make sure to use the + // last used direction vector when committing the resize / reorder. + if (onDismiss) { + mDirectionVector[0] = mLastDirectionVector[0]; + mDirectionVector[1] = mLastDirectionVector[1]; + } else { + mLastDirectionVector[0] = mDirectionVector[0]; + mLastDirectionVector[1] = mDirectionVector[1]; + } + + if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView, + mDirectionVector, onDismiss)) { + lp.tmpCellX = cellX; + lp.tmpCellY = cellY; + lp.cellHSpan = spanX; + lp.cellVSpan = spanY; + mRunningVInc += vSpanDelta; + mRunningHInc += hSpanDelta; + if (!onDismiss) { + updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY); + } + } + mWidgetView.requestLayout(); + } + + 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); + } + + static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) { + if (rect == null) { + rect = new Rect(); + } + Rect landMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.LANDSCAPE); + Rect portMetrics = Workspace.getCellLayoutMetrics(launcher, CellLayout.PORTRAIT); + final float density = launcher.getResources().getDisplayMetrics().density; + + // Compute landscape size + int cellWidth = landMetrics.left; + int cellHeight = landMetrics.top; + int widthGap = landMetrics.right; + int heightGap = landMetrics.bottom; + int landWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density); + int landHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density); + + // Compute portrait size + cellWidth = portMetrics.left; + cellHeight = portMetrics.top; + widthGap = portMetrics.right; + heightGap = portMetrics.bottom; + int portWidth = (int) ((spanX * cellWidth + (spanX - 1) * widthGap) / density); + int portHeight = (int) ((spanY * cellHeight + (spanY - 1) * heightGap) / density); + rect.set(portWidth, landHeight, landWidth, portHeight); + return rect; + } + + /** + * This is the final step of the resize. Here we save the new widget size and position + * to LauncherModel and animate the resize frame. + */ + public void commitResize() { + resizeWidgetIfNeeded(true); + requestLayout(); + } + + public void onTouchUp() { + int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap(); + int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap(); + + mDeltaXAddOn = mRunningHInc * xThreshold; + mDeltaYAddOn = mRunningVInc * yThreshold; + mDeltaX = 0; + mDeltaY = 0; + + post(new Runnable() { + @Override + public void run() { + snapToWidget(true); + } + }); + } + + public void snapToWidget(boolean animate) { + final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + int xOffset = mCellLayout.getLeft() + mCellLayout.getPaddingLeft() + + mDragLayer.getPaddingLeft() - mWorkspace.getScrollX(); + int yOffset = mCellLayout.getTop() + mCellLayout.getPaddingTop() + + mDragLayer.getPaddingTop() - mWorkspace.getScrollY(); + + int newWidth = mWidgetView.getWidth() + 2 * mBackgroundPadding - mWidgetPaddingLeft - + mWidgetPaddingRight; + int newHeight = mWidgetView.getHeight() + 2 * mBackgroundPadding - mWidgetPaddingTop - + mWidgetPaddingBottom; + + int newX = mWidgetView.getLeft() - mBackgroundPadding + xOffset + mWidgetPaddingLeft; + int newY = mWidgetView.getTop() - mBackgroundPadding + yOffset + mWidgetPaddingTop; + + // 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) { + // In this case we shift the touch region down to start at the top of the DragLayer + mTopTouchRegionAdjustment = -newY; + } else { + mTopTouchRegionAdjustment = 0; + } + if (newY + newHeight > mDragLayer.getHeight()) { + // In this case we shift the touch region up to end at the bottom of the DragLayer + mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight()); + } else { + mBottomTouchRegionAdjustment = 0; + } + + if (!animate) { + lp.width = newWidth; + lp.height = newHeight; + lp.x = newX; + lp.y = newY; + mLeftHandle.setAlpha(1.0f); + mRightHandle.setAlpha(1.0f); + mTopHandle.setAlpha(1.0f); + mBottomHandle.setAlpha(1.0f); + requestLayout(); + } else { + PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth); + PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height, + newHeight); + PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX); + PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY); + ObjectAnimator oa = + LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y); + ObjectAnimator leftOa = LauncherAnimUtils.ofFloat(mLeftHandle, "alpha", 1.0f); + ObjectAnimator rightOa = LauncherAnimUtils.ofFloat(mRightHandle, "alpha", 1.0f); + ObjectAnimator topOa = LauncherAnimUtils.ofFloat(mTopHandle, "alpha", 1.0f); + ObjectAnimator bottomOa = LauncherAnimUtils.ofFloat(mBottomHandle, "alpha", 1.0f); + oa.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + requestLayout(); + } + }); + AnimatorSet set = LauncherAnimUtils.createAnimatorSet(); + if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) { + set.playTogether(oa, topOa, bottomOa); + } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) { + set.playTogether(oa, leftOa, rightOa); + } else { + set.playTogether(oa, leftOa, rightOa, topOa, bottomOa); + } + + set.setDuration(SNAP_DURATION); + set.start(); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/ApplicationInfo.java b/app/src/main/java/com/android/launcher2/ApplicationInfo.java new file mode 100644 index 0000000..eda8c25 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/ApplicationInfo.java @@ -0,0 +1,133 @@ +/* + * 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.launcher2; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Represents an app in AllAppsView. + */ +class ApplicationInfo extends ItemInfo { + private static final String TAG = "Launcher2.ApplicationInfo"; + + /** + * The intent used to start the application. + */ + Intent intent; + + /** + * A bitmap version of the application icon. + */ + Bitmap iconBitmap; + + /** + * The time at which the app was first installed. + */ + long firstInstallTime; + + ComponentName componentName; + + static final int DOWNLOADED_FLAG = 1; + static final int UPDATED_SYSTEM_APP_FLAG = 2; + + int flags = 0; + + ApplicationInfo() { + itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; + } + + /** + * Must not hold the Context. + */ + public ApplicationInfo(PackageManager pm, ResolveInfo info, IconCache iconCache, + HashMap labelCache) { + final String packageName = info.activityInfo.applicationInfo.packageName; + + this.componentName = new ComponentName(packageName, info.activityInfo.name); + this.container = ItemInfo.NO_ID; + this.setActivity(componentName, + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + + try { + int appFlags = pm.getApplicationInfo(packageName, 0).flags; + if ((appFlags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) == 0) { + flags |= DOWNLOADED_FLAG; + + if ((appFlags & android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + flags |= UPDATED_SYSTEM_APP_FLAG; + } + } + firstInstallTime = pm.getPackageInfo(packageName, 0).firstInstallTime; + } catch (NameNotFoundException e) { + Log.d(TAG, "PackageManager.getApplicationInfo failed for " + packageName); + } + + iconCache.getTitleAndIcon(this, info, labelCache); + } + + public ApplicationInfo(ApplicationInfo info) { + super(info); + componentName = info.componentName; + title = info.title.toString(); + intent = new Intent(info.intent); + flags = info.flags; + firstInstallTime = info.firstInstallTime; + } + + /** + * Creates the application intent based on a component name and various launch flags. + * Sets {@link #itemType} to {@link LauncherSettings.BaseLauncherColumns#ITEM_TYPE_APPLICATION}. + * + * @param className the class name of the component representing the intent + * @param launchFlags the launch flags + */ + final void setActivity(ComponentName className, int launchFlags) { + intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(className); + intent.setFlags(launchFlags); + itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION; + } + + @Override + public String toString() { + return "ApplicationInfo(title=" + title.toString() + ")"; + } + + public static void dumpApplicationInfoList(String tag, String label, + ArrayList list) { + Log.d(tag, label + " size=" + list.size()); + for (ApplicationInfo info: list) { + Log.d(tag, " title=\"" + info.title + "\" iconBitmap=" + + info.iconBitmap + " firstInstallTime=" + + info.firstInstallTime); + } + } + + public ShortcutInfo makeShortcut() { + return new ShortcutInfo(this); + } +} diff --git a/app/src/main/java/com/android/launcher2/AppsCustomizePagedView.java b/app/src/main/java/com/android/launcher2/AppsCustomizePagedView.java new file mode 100644 index 0000000..fd49f0c --- /dev/null +++ b/app/src/main/java/com/android/launcher2/AppsCustomizePagedView.java @@ -0,0 +1,1729 @@ +/* + * 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.launcher2; + +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.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +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.view.animation.DecelerateInterpolator; +import android.widget.GridLayout; +import android.widget.ImageView; +import android.widget.Toast; + +import com.android.launcher.R; +import com.android.launcher2.DropTarget.DragObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import lu.die.fozacompatibility.FozaActivityManager; + +/** + * 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, + PagedViewIcon.PressedCallback, PagedViewWidget.ShortPressListener, + LauncherTransitionable { + static final String TAG = "AppsCustomizePagedView"; + + /** + * The different content types that this paged view can show. + */ + public enum ContentType { + Applications, + Widgets + } + + // Refs + private Launcher mLauncher; + private DragController mDragController; + private final LayoutInflater mLayoutInflater; + private final PackageManager mPackageManager; + + // Save and Restore + private int mSaveInstanceStateItemIndex = -1; + private PagedViewIcon mPressedIcon; + + // Content + private ArrayList mApps; + private ArrayList mWidgets; + + // Cling + private boolean mHasShownAllAppsCling; + private int mClingFocusedX; + private int mClingFocusedY; + + // Caching + private Canvas mCanvas; + private IconCache mIconCache; + + // Dimens + private int mContentWidth; + private int mMaxAppCellCountX, mMaxAppCellCountY; + private int mWidgetCountX, mWidgetCountY; + private int mWidgetWidthGap, mWidgetHeightGap; + private PagedViewCellLayout mWidgetSpacingLayout; + private int mNumAppsPages; + private int mNumWidgetPages; + + // Relating to the scroll and overscroll effects + Workspace.ZInterpolator mZInterpolator = new Workspace.ZInterpolator(0.5f); + private static float CAMERA_DISTANCE = 6500; + private static float TRANSITION_SCALE_FACTOR = 0.74f; + private static float TRANSITION_PIVOT = 0.65f; + private static float TRANSITION_MAX_ROTATION = 22; + private static final boolean PERFORM_OVERSCROLL_ROTATION = true; + private AccelerateInterpolator mAlphaInterpolator = new AccelerateInterpolator(0.9f); + private DecelerateInterpolator mLeftScreenAlphaInterpolator = new DecelerateInterpolator(4); + + // 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; + + private Toast mWidgetInstructionToast; + + // Deferral of loading widget previews during launcher transitions + private boolean mInTransition; + private ArrayList mDeferredSyncWidgetPageItems = + new ArrayList(); + private ArrayList mDeferredPrepareLoadWidgetPreviewsTasks = + new ArrayList(); + + private Rect mTmpRect = new Rect(); + + // Used for drawing shortcut previews + BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); + PaintCache mCachedShortcutPreviewPaint = new PaintCache(); + CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); + + // Used for drawing widget previews + CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); + RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); + RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); + PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); + + 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 = ((LauncherApplication) context.getApplicationContext()).getIconCache(); + mCanvas = new Canvas(); + mRunningTasks = new ArrayList(); + + // Save the default widget preview background + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0); + mMaxAppCellCountX = a.getInt(R.styleable.AppsCustomizePagedView_maxAppCellCountX, -1); + mMaxAppCellCountY = a.getInt(R.styleable.AppsCustomizePagedView_maxAppCellCountY, -1); + mWidgetWidthGap = + a.getDimensionPixelSize(R.styleable.AppsCustomizePagedView_widgetCellWidthGap, 0); + mWidgetHeightGap = + a.getDimensionPixelSize(R.styleable.AppsCustomizePagedView_widgetCellHeightGap, 0); + mWidgetCountX = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountX, 2); + mWidgetCountY = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountY, 2); + mClingFocusedX = a.getInt(R.styleable.AppsCustomizePagedView_clingFocusedX, 0); + mClingFocusedY = a.getInt(R.styleable.AppsCustomizePagedView_clingFocusedY, 0); + 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); + } + } + + @Override + protected void init() { + super.init(); + mCenterPagesVertically = false; + + Context context = getContext(); + Resources r = context.getResources(); + setDragSlopeThreshold(r.getInteger(R.integer.config_appsCustomizeDragSlopeThreshold)/100f); + } + + /** 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 (currentPage < mNumAppsPages) { + PagedViewCellLayout layout = (PagedViewCellLayout) getPageAt(currentPage); + PagedViewCellLayoutChildren childrenLayout = layout.getChildrenLayout(); + int numItemsPerPage = mCellCountX * mCellCountY; + int childCount = childrenLayout.getChildCount(); + if (childCount > 0) { + i = (currentPage * numItemsPerPage) + (childCount / 2); + } + } else { + int numApps = mApps.size(); + PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(currentPage); + int numItemsPerPage = mWidgetCountX * mWidgetCountY; + int childCount = layout.getChildCount(); + if (childCount > 0) { + i = numApps + + ((currentPage - mNumAppsPages) * numItemsPerPage) + (childCount / 2); + } + } + } + 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 mNumAppsPages + ((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) { + if (mWidgetPreviewLoader == null) { + mWidgetPreviewLoader = new WidgetPreviewLoader(mLauncher); + } + + // Note that we transpose the counts in portrait so that we get a similar layout + boolean isLandscape = getResources().getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE; + int maxCellCountX = Integer.MAX_VALUE; + int maxCellCountY = Integer.MAX_VALUE; + if (LauncherApplication.isScreenLarge()) { + maxCellCountX = (isLandscape ? LauncherModel.getCellCountX() : + LauncherModel.getCellCountY()); + maxCellCountY = (isLandscape ? LauncherModel.getCellCountY() : + LauncherModel.getCellCountX()); + } + if (mMaxAppCellCountX > -1) { + maxCellCountX = Math.min(maxCellCountX, mMaxAppCellCountX); + } + // Temp hack for now: only use the max cell count Y for widget layout + int maxWidgetCellCountY = maxCellCountY; + if (mMaxAppCellCountY > -1) { + maxWidgetCellCountY = Math.min(maxWidgetCellCountY, mMaxAppCellCountY); + } + + // Now that the data is ready, we can calculate the content width, the number of cells to + // use for each page + mWidgetSpacingLayout.setGap(mPageLayoutWidthGap, mPageLayoutHeightGap); + mWidgetSpacingLayout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + mWidgetSpacingLayout.calculateCellCount(width, height, maxCellCountX, maxCellCountY); + mCellCountX = mWidgetSpacingLayout.getCellCountX(); + mCellCountY = mWidgetSpacingLayout.getCellCountY(); + updatePageCounts(); + + // Force a measure to update recalculate the gaps + int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST); + mWidgetSpacingLayout.calculateCellCount(width, height, maxCellCountX, maxWidgetCellCountY); + mWidgetSpacingLayout.measure(widthSpec, heightSpec); + mContentWidth = mWidgetSpacingLayout.getContentWidth(); + + AppsCustomizeTabHost host = (AppsCustomizeTabHost) getTabHost(); + final boolean hostIsTransitioning = host.isTransitioning(); + + // Restore the page + int page = getPageForComponent(mSaveInstanceStateItemIndex); + invalidatePageData(Math.max(0, page), hostIsTransitioning); + + // Show All Apps cling if we are finished transitioning, otherwise, we will try again when + // the transition completes in AppsCustomizeTabHost (otherwise the wrong offsets will be + // returned while animating) + if (!hostIsTransitioning) { + post(new Runnable() { + @Override + public void run() { + showAllAppsCling(); + } + }); + } + } + + void showAllAppsCling() { + if (!mHasShownAllAppsCling && isDataReady()) { + mHasShownAllAppsCling = true; + // Calculate the position for the cling punch through + int[] offset = new int[2]; + int[] pos = mWidgetSpacingLayout.estimateCellPosition(mClingFocusedX, mClingFocusedY); + mLauncher.getDragLayer().getLocationInDragLayer(this, offset); + // PagedViews are centered horizontally but top aligned + // Note we have to shift the items up now that Launcher sits under the status bar + pos[0] += (getMeasuredWidth() - mWidgetSpacingLayout.getMeasuredWidth()) / 2 + + offset[0]; + pos[1] += offset[1] - mLauncher.getDragLayer().getPaddingTop(); + mLauncher.showFirstRunAllAppsCling(pos); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + if (!isDataReady()) { + if (!mApps.isEmpty() && !mWidgets.isEmpty()) { + setDataIsReady(); + setMeasuredDimension(width, height); + onDataReady(width, height); + } + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public void onPackagesUpdated(ArrayList widgetsAndShortcuts) { + // Get the list of widgets and shortcuts + mWidgets.clear(); + for (Object o : widgetsAndShortcuts) { + if (o instanceof AppWidgetProviderInfo) { + AppWidgetProviderInfo widget = (AppWidgetProviderInfo) o; + widget.label = widget.label.trim(); + 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 <= LauncherModel.getCellCountX() && + minSpanY <= LauncherModel.getCellCountY()) { + 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()) return; + + if (v instanceof PagedViewIcon) { + // Animate some feedback to the click + final ApplicationInfo appInfo = (ApplicationInfo) v.getTag(); + + // Lock the drawable state to pressed until we return to Launcher + if (mPressedIcon != null) { + mPressedIcon.lockDrawableState(); + } + + // NOTE: We want all transitions from launcher to act as if the wallpaper were enabled + // to be consistent. So re-enable the flag here, and we will re-disable it as necessary + // when Launcher resumes and we are still in AllApps. + mLauncher.updateWallpaperVisibility(true); + if(LauncherUtils.isSystemApplication( + mLauncher, + appInfo.componentName.getPackageName() + )) + { + mLauncher.startActivitySafely(v, appInfo.intent, appInfo); + } + else try{ + Intent reserver = FozaActivityManager.get().obtainSplashLaunchIntent( + 0, + appInfo.componentName.getPackageName(), + mLauncher + ); + if(reserver != null) + mLauncher.startActivity(reserver); + }catch (Exception e) + { + } + + } else if (v instanceof PagedViewWidget) { + // 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().onDragStartedWithItem(v); + mLauncher.getWorkspace().beginDragShared(v, this); + } + + Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { + Bundle options = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, mTmpRect); + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(mLauncher, + info.componentName, null); + + float density = 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, + mTmpRect.left - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, + mTmpRect.top - yPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, + mTmpRect.right - xPaddingDips); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, + mTmpRect.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(); + // Options will be null for platforms with JB or lower, so this serves as an + // SDK level check. + if (options == null) { + if (AppWidgetManager.getInstance(mLauncher).bindAppWidgetIdIfAllowed( + mWidgetLoadingId, info.componentName)) { + mWidgetCleanupState = WIDGET_BOUND; + } + } else { + if (AppWidgetManager.getInstance(mLauncher).bindAppWidgetIdIfAllowed( + mWidgetLoadingId, info.componentName, 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 = mWidgetPreviewLoader.generateWidgetPreview(createWidgetInfo.componentName, + createWidgetInfo.previewImage, createWidgetInfo.icon, 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], + mWidgetPreviewLoader.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 = Bitmap.createBitmap(icon.getIntrinsicWidth(), + icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + + mCanvas.setBitmap(preview); + mCanvas.save(); + WidgetPreviewLoader.renderDrawableToBitmap(icon, preview, 0, 0, + icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + mCanvas.restore(); + mCanvas.setBitmap(null); + 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 PagedViewIcon) { + 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()) { + // Dismiss the cling + mLauncher.dismissAllAppsCling(null); + + // Reset the alpha on the dragged icon before we drag + resetDrawableState(); + + // 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))) { + // Exit spring loaded mode if we have not successfully dropped or have not handled the + // drop in Workspace + mLauncher.exitSpringLoadedDragMode(); + } + mLauncher.unlockScreenOrientation(false); + } + + @Override + public View getContent() { + 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); + } + 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 + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + cancelAllTasks(); + } + + 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) { + if (type == ContentType.Widgets) { + invalidatePageData(mNumAppsPages, true); + } else if (type == ContentType.Applications) { + invalidatePageData(0, true); + } + } + + protected void snapToPage(int whichPage, int delta, int duration) { + super.snapToPage(whichPage, delta, duration); + updateCurrentTab(whichPage); + + // 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); + } + } + } + + private void updateCurrentTab(int currentPage) { + AppsCustomizeTabHost tabHost = getTabHost(); + if (tabHost != null) { + String tag = tabHost.getCurrentTabTag(); + if (tag != null) { + if (currentPage >= mNumAppsPages && + !tag.equals(tabHost.getTabTagForContentType(ContentType.Widgets))) { + tabHost.setCurrentTabFromContent(ContentType.Widgets); + } else if (currentPage < mNumAppsPages && + !tag.equals(tabHost.getTabTagForContentType(ContentType.Applications))) { + tabHost.setCurrentTabFromContent(ContentType.Applications); + } + } + } + } + + /* + * 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(PagedViewCellLayout layout) { + layout.setCellCount(mCellCountX, mCellCountY); + layout.setGap(mPageLayoutWidthGap, mPageLayoutHeightGap); + layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + + // 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(getMeasuredWidth(), MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST); + layout.setMinimumWidth(getPageContentWidth()); + layout.measure(widthSpec, heightSpec); + setVisibilityOnChildren(layout, View.VISIBLE); + } + + 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()); + PagedViewCellLayout layout = (PagedViewCellLayout) getPageAt(page); + + layout.removeAllViewsOnPage(); + ArrayList items = new ArrayList(); + ArrayList images = new ArrayList(); + for (int i = startIndex; i < endIndex; ++i) { + ApplicationInfo info = mApps.get(i); + PagedViewIcon icon = (PagedViewIcon) mLayoutInflater.inflate( + R.layout.apps_customize_application, layout, false); + icon.applyFromApplicationInfo(info, true, this); + icon.setOnClickListener(this); + icon.setOnLongClickListener(this); + icon.setOnTouchListener(this); + icon.setOnKeyListener(this); + + int index = i - startIndex; + int x = index % mCellCountX; + int y = index / mCellCountX; + if (isRtl) { + x = mCellCountX - x - 1; + } + layout.addViewToCellLayout(icon, -1, i, new PagedViewCellLayout.LayoutParams(x,y, 1,1)); + + 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); + } + }, mWidgetPreviewLoader); + + // 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) { + layout.setPadding(mPageLayoutPaddingLeft, mPageLayoutPaddingTop, + mPageLayoutPaddingRight, mPageLayoutPaddingBottom); + + // 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(getMeasuredWidth(), MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST); + layout.setMinimumWidth(getPageContentWidth()); + layout.measure(widthSpec, heightSpec); + } + + public void syncWidgetPageItems(final int page, final boolean immediate) { + int numItemsPerPage = mWidgetCountX * mWidgetCountY; + + // Calculate the dimensions of each cell we are giving to each widget + final ArrayList items = new ArrayList(); + int contentWidth = mWidgetSpacingLayout.getContentWidth(); + final int cellWidth = ((contentWidth - mPageLayoutPaddingLeft - mPageLayoutPaddingRight + - ((mWidgetCountX - 1) * mWidgetWidthGap)) / mWidgetCountX); + int contentHeight = mWidgetSpacingLayout.getContentHeight(); + final int cellHeight = ((contentHeight - mPageLayoutPaddingTop - mPageLayoutPaddingBottom + - ((mWidgetCountY - 1) * mWidgetHeightGap)) / mWidgetCountY); + + // Prepare the set of widgets to load previews for in the background + int offset = (page - mNumAppsPages) * 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 + final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page); + 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, mWidgetPreviewLoader); + 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, mWidgetPreviewLoader); + 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; + 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); + if (ix > 0) lp.leftMargin = mWidgetWidthGap; + if (iy > 0) lp.topMargin = mWidgetHeightGap; + 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]; + } + + mWidgetPreviewLoader.setPreviewSize( + maxPreviewWidth, maxPreviewHeight, mWidgetSpacingLayout); + if (immediate) { + AsyncTaskPageData data = new AsyncTaskPageData(page, items, + maxPreviewWidth, maxPreviewHeight, null, null, mWidgetPreviewLoader); + loadWidgetPreviewsInBackground(null, data); + onSyncWidgetPageItems(data); + } 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(mWidgetPreviewLoader.getPreview(items.get(i))); + } + } + + private void onSyncWidgetPageItems(AsyncTaskPageData data) { + if (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() { + removeAllViews(); + cancelAllTasks(); + + Context context = getContext(); + 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)); + } + + for (int i = 0; i < mNumAppsPages; ++i) { + PagedViewCellLayout layout = new PagedViewCellLayout(context); + setupPage(layout); + addView(layout); + } + } + + @Override + public void syncPageItems(int page, boolean immediate) { + if (page < mNumAppsPages) { + syncAppsPageItems(page, immediate); + } else { + syncWidgetPageItems(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) { + final boolean isRtl = isLayoutRtl(); + super.screenScrolled(screenCenter); + + for (int i = 0; i < getChildCount(); i++) { + View v = getPageAt(i); + if (v != null) { + float scrollProgress = getScrollProgress(screenCenter, v, i); + + float interpolatedProgress; + float translationX; + float maxScrollProgress = Math.max(0, scrollProgress); + float minScrollProgress = Math.min(0, scrollProgress); + + if (isRtl) { + translationX = maxScrollProgress * v.getMeasuredWidth(); + interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(maxScrollProgress)); + } else { + translationX = minScrollProgress * v.getMeasuredWidth(); + interpolatedProgress = mZInterpolator.getInterpolation(Math.abs(minScrollProgress)); + } + float scale = (1 - interpolatedProgress) + + interpolatedProgress * TRANSITION_SCALE_FACTOR; + + float alpha; + if (isRtl && (scrollProgress > 0)) { + alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(maxScrollProgress)); + } else if (!isRtl && (scrollProgress < 0)) { + alpha = mAlphaInterpolator.getInterpolation(1 - Math.abs(scrollProgress)); + } else { + // On large screens we need to fade the page as it nears its leftmost position + alpha = mLeftScreenAlphaInterpolator.getInterpolation(1 - scrollProgress); + } + + v.setCameraDistance(mDensity * CAMERA_DISTANCE); + int pageWidth = v.getMeasuredWidth(); + int pageHeight = v.getMeasuredHeight(); + + if (PERFORM_OVERSCROLL_ROTATION) { + float xPivot = isRtl ? 1f - TRANSITION_PIVOT : TRANSITION_PIVOT; + boolean isOverscrollingFirstPage = isRtl ? scrollProgress > 0 : scrollProgress < 0; + boolean isOverscrollingLastPage = isRtl ? scrollProgress < 0 : scrollProgress > 0; + + if (i == 0 && isOverscrollingFirstPage) { + // Overscroll to the left + v.setPivotX(xPivot * pageWidth); + v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress); + scale = 1.0f; + alpha = 1.0f; + // On the first page, we don't want the page to have any lateral motion + translationX = 0; + } else if (i == getChildCount() - 1 && isOverscrollingLastPage) { + // Overscroll to the right + v.setPivotX((1 - xPivot) * pageWidth); + v.setRotationY(-TRANSITION_MAX_ROTATION * scrollProgress); + scale = 1.0f; + alpha = 1.0f; + // On the last page, we don't want the page to have any lateral motion. + translationX = 0; + } else { + v.setPivotY(pageHeight / 2.0f); + v.setPivotX(pageWidth / 2.0f); + v.setRotationY(0f); + } + } + + v.setTranslationX(translationX); + v.setScaleX(scale); + v.setScaleY(scale); + v.setAlpha(alpha); + + // If the view has 0 alpha, we set it to be invisible so as to prevent + // it from accepting touches + if (alpha == 0) { + v.setVisibility(INVISIBLE); + } else if (v.getVisibility() != VISIBLE) { + v.setVisibility(VISIBLE); + } + } + } + + 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) { + acceleratedOverScroll(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) { + 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) { + ApplicationInfo 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) { + addAppsWithoutInvalidate(list); + updatePageCountsAndInvalidateData(); + } + private int findAppByComponent(List list, ApplicationInfo item) { + ComponentName removeComponent = item.intent.getComponent(); + int length = list.size(); + for (int i = 0; i < length; ++i) { + ApplicationInfo info = list.get(i); + if (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) { + ApplicationInfo info = list.get(i); + int removeIndex = findAppByComponent(mApps, info); + if (removeIndex > -1) { + mApps.remove(removeIndex); + } + } + } + public void removeApps(ArrayList appInfos) { + 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. + removeAppsWithoutInvalidate(list); + addAppsWithoutInvalidate(list); + updatePageCountsAndInvalidateData(); + } + + public void reset() { + // If we have reset, then we should not continue to restore the previous state + mSaveInstanceStateItemIndex = -1; + + AppsCustomizeTabHost tabHost = getTabHost(); + String tag = tabHost.getCurrentTabTag(); + if (tag != null) { + if (!tag.equals(tabHost.getTabTagForContentType(ContentType.Applications))) { + tabHost.setCurrentTabFromContent(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. + ApplicationInfo.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(); + } + + @Override + public void iconPressed(PagedViewIcon icon) { + // Reset the previously pressed icon and store a reference to the pressed icon so that + // we can reset it on return to Launcher (in Launcher.onResume()) + if (mPressedIcon != null) { + mPressedIcon.resetDrawableState(); + } + mPressedIcon = icon; + } + + public void resetDrawableState() { + if (mPressedIcon != null) { + mPressedIcon.resetDrawableState(); + mPressedIcon = null; + } + } + + /* + * 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; + } + + @Override + protected String getCurrentPageDescription() { + int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + int stringId = R.string.default_scroll_format; + int count = 0; + + if (page < mNumAppsPages) { + stringId = R.string.apps_customize_apps_scroll_format; + count = mNumAppsPages; + } else { + page -= mNumAppsPages; + stringId = R.string.apps_customize_widgets_scroll_format; + count = mNumWidgetPages; + } + + return String.format(getContext().getString(stringId), page + 1, count); + } +} diff --git a/app/src/main/java/com/android/launcher2/AppsCustomizeTabHost.java b/app/src/main/java/com/android/launcher2/AppsCustomizeTabHost.java new file mode 100644 index 0000000..225b056 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/AppsCustomizeTabHost.java @@ -0,0 +1,482 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TabHost; +import android.widget.TabWidget; +import android.widget.TextView; + +import com.android.launcher.R; + +import java.util.ArrayList; + +public class AppsCustomizeTabHost extends TabHost implements LauncherTransitionable, + TabHost.OnTabChangeListener { + static final String LOG_TAG = "AppsCustomizeTabHost"; + + private static final String APPS_TAB_TAG = "APPS"; + private static final String WIDGETS_TAB_TAG = "WIDGETS"; + + private final LayoutInflater mLayoutInflater; + private ViewGroup mTabs; + private ViewGroup mTabsContainer; + private AppsCustomizePagedView mAppsCustomizePane; + private FrameLayout mAnimationBuffer; + private LinearLayout mContent; + + private boolean mInTransition; + private boolean mTransitioningToWorkspace; + private boolean mResetAfterTransition; + private Runnable mRelayoutAndMakeVisible; + + public AppsCustomizeTabHost(Context context, AttributeSet attrs) { + super(context, attrs); + mLayoutInflater = LayoutInflater.from(context); + mRelayoutAndMakeVisible = new Runnable() { + public void run() { + mTabs.requestLayout(); + mTabsContainer.setAlpha(1f); + } + }; + } + + /** + * 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) { + setOnTabChangedListener(null); + onTabChangedStart(); + onTabChangedEnd(type); + setCurrentTabByTag(getTabTagForContentType(type)); + setOnTabChangedListener(this); + } + void selectAppsTab() { + setContentTypeImmediate(AppsCustomizePagedView.ContentType.Applications); + } + void selectWidgetsTab() { + setContentTypeImmediate(AppsCustomizePagedView.ContentType.Widgets); + } + + /** + * Setup the tab host and create all necessary tabs. + */ + @Override + protected void onFinishInflate() { + // Setup the tab host + setup(); + + final ViewGroup tabsContainer = (ViewGroup) findViewById(R.id.tabs_container); + final TabWidget tabs = getTabWidget(); + final AppsCustomizePagedView appsCustomizePane = (AppsCustomizePagedView) + findViewById(R.id.apps_customize_pane_content); + mTabs = tabs; + mTabsContainer = tabsContainer; + mAppsCustomizePane = appsCustomizePane; + mAnimationBuffer = (FrameLayout) findViewById(R.id.animation_buffer); + mContent = (LinearLayout) findViewById(R.id.apps_customize_content); + if (tabs == null || mAppsCustomizePane == null) throw new Resources.NotFoundException(); + + // Configure the tabs content factory to return the same paged view (that we change the + // content filter on) + TabContentFactory contentFactory = new TabContentFactory() { + public View createTabContent(String tag) { + return appsCustomizePane; + } + }; + + // Create the tabs + TextView tabView; + String label; + label = getContext().getString(R.string.all_apps_button_label); + tabView = (TextView) mLayoutInflater.inflate(R.layout.tab_widget_indicator, tabs, false); + tabView.setText(label); + tabView.setContentDescription(label); + addTab(newTabSpec(APPS_TAB_TAG).setIndicator(tabView).setContent(contentFactory)); + label = getContext().getString(R.string.widgets_tab_label); + tabView = (TextView) mLayoutInflater.inflate(R.layout.tab_widget_indicator, tabs, false); + tabView.setText(label); + tabView.setContentDescription(label); + addTab(newTabSpec(WIDGETS_TAB_TAG).setIndicator(tabView).setContent(contentFactory)); + setOnTabChangedListener(this); + + // Setup the key listener to jump between the last tab view and the market icon + AppsCustomizeTabKeyEventListener keyListener = new AppsCustomizeTabKeyEventListener(); + View lastTab = tabs.getChildTabViewAt(tabs.getTabCount() - 1); + lastTab.setOnKeyListener(keyListener); + View shopButton = findViewById(R.id.market_button); + shopButton.setOnKeyListener(keyListener); + + // Hide the tab bar until we measure + mTabsContainer.setAlpha(0f); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + boolean remeasureTabWidth = (mTabs.getLayoutParams().width <= 0); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Set the width of the tab list to the content width + if (remeasureTabWidth) { + int contentWidth = mAppsCustomizePane.getPageContentWidth(); + if (contentWidth > 0 && mTabs.getLayoutParams().width != contentWidth) { + // Set the width and show the tab bar + mTabs.getLayoutParams().width = contentWidth; + mRelayoutAndMakeVisible.run(); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + // If we are mid transitioning to the workspace, then intercept touch events here so we + // can ignore them, otherwise we just let all apps handle the touch events. + if (mInTransition && mTransitioningToWorkspace) { + return true; + } + return super.onInterceptTouchEvent(ev); + }; + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Allow touch events to fall through to the workspace if we are transitioning there + if (mInTransition && mTransitioningToWorkspace) { + return super.onTouchEvent(event); + } + + // Intercept all touch events up to the bottom of the AppsCustomizePane so they do not fall + // through to the workspace and trigger showWorkspace() + if (event.getY() < mAppsCustomizePane.getBottom()) { + return true; + } + return super.onTouchEvent(event); + } + + private void onTabChangedStart() { + mAppsCustomizePane.hideScrollingIndicator(false); + } + + private void reloadCurrentPage() { + if (!LauncherApplication.isScreenLarge()) { + mAppsCustomizePane.flashScrollingIndicator(true); + } + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); + mAppsCustomizePane.requestFocus(); + } + + private void onTabChangedEnd(AppsCustomizePagedView.ContentType type) { + mAppsCustomizePane.setContentType(type); + } + + @Override + public void onTabChanged(String tabId) { + final AppsCustomizePagedView.ContentType type = getContentTypeForTabTag(tabId); + + // Animate the changing of the tab content by fading pages in and out + final Resources res = getResources(); + final int duration = res.getInteger(R.integer.config_tabTransitionDuration); + + // We post a runnable here because there is a delay while the first page is loading and + // the feedback from having changed the tab almost feels better than having it stick + post(new Runnable() { + @Override + public void run() { + if (mAppsCustomizePane.getMeasuredWidth() <= 0 || + mAppsCustomizePane.getMeasuredHeight() <= 0) { + reloadCurrentPage(); + return; + } + + // Take the visible pages and re-parent them temporarily to mAnimatorBuffer + // and then cross fade to the new pages + int[] visiblePageRange = new int[2]; + mAppsCustomizePane.getVisiblePages(visiblePageRange); + if (visiblePageRange[0] == -1 && visiblePageRange[1] == -1) { + // If we can't get the visible page ranges, then just skip the animation + reloadCurrentPage(); + return; + } + ArrayList visiblePages = new ArrayList(); + for (int i = visiblePageRange[0]; i <= visiblePageRange[1]; i++) { + visiblePages.add(mAppsCustomizePane.getPageAt(i)); + } + + // We want the pages to be rendered in exactly the same way as they were when + // their parent was mAppsCustomizePane -- so set the scroll on mAnimationBuffer + // to be exactly the same as mAppsCustomizePane, and below, set the left/top + // parameters to be correct for each of the pages + mAnimationBuffer.scrollTo(mAppsCustomizePane.getScrollX(), 0); + + // mAppsCustomizePane renders its children in reverse order, so + // add the pages to mAnimationBuffer in reverse order to match that behavior + for (int i = visiblePages.size() - 1; i >= 0; i--) { + View child = visiblePages.get(i); + if (child instanceof PagedViewCellLayout) { + ((PagedViewCellLayout) child).resetChildrenOnKeyListeners(); + } else if (child instanceof PagedViewGridLayout) { + ((PagedViewGridLayout) child).resetChildrenOnKeyListeners(); + } + PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(false); + mAppsCustomizePane.removeView(child); + PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(true); + mAnimationBuffer.setAlpha(1f); + mAnimationBuffer.setVisibility(View.VISIBLE); + LayoutParams p = new FrameLayout.LayoutParams(child.getMeasuredWidth(), + child.getMeasuredHeight()); + p.setMargins((int) child.getLeft(), (int) child.getTop(), 0, 0); + mAnimationBuffer.addView(child, p); + } + + // Toggle the new content + onTabChangedStart(); + onTabChangedEnd(type); + + // Animate the transition + ObjectAnimator outAnim = LauncherAnimUtils.ofFloat(mAnimationBuffer, "alpha", 0f); + outAnim.addListener(new AnimatorListenerAdapter() { + private void clearAnimationBuffer() { + mAnimationBuffer.setVisibility(View.GONE); + PagedViewWidget.setRecyclePreviewsWhenDetachedFromWindow(false); + mAnimationBuffer.removeAllViews(); + PagedViewWidget.setRecyclePreviewsWhenDetachedFromWindow(true); + } + @Override + public void onAnimationEnd(Animator animation) { + clearAnimationBuffer(); + } + @Override + public void onAnimationCancel(Animator animation) { + clearAnimationBuffer(); + } + }); + ObjectAnimator inAnim = LauncherAnimUtils.ofFloat(mAppsCustomizePane, "alpha", 1f); + inAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + reloadCurrentPage(); + } + }); + + final AnimatorSet animSet = LauncherAnimUtils.createAnimatorSet(); + animSet.playTogether(outAnim, inAnim); + animSet.setDuration(duration); + animSet.start(); + } + }); + } + + public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) { + setOnTabChangedListener(null); + setCurrentTabByTag(getTabTagForContentType(type)); + setOnTabChangedListener(this); + } + + /** + * 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() { + if (mInTransition) { + // Defer to after the transition to reset + mResetAfterTransition = true; + } else { + // Reset immediately + mAppsCustomizePane.reset(); + } + } + + private void enableAndBuildHardwareLayer() { + // isHardwareAccelerated() checks if we're attached to a window and if that + // window is HW accelerated-- we were sometimes not attached to a window + // and buildLayer was throwing an IllegalStateException + if (isHardwareAccelerated()) { + // Turn on hardware layers for performance + setLayerType(LAYER_TYPE_HARDWARE, null); + + // force building the layer, so you don't get a blip early in an animation + // when the layer is created layer + buildLayer(); + } + } + + @Override + public View getContent() { + return mContent; + } + + /* LauncherTransitionable overrides */ + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + mAppsCustomizePane.onLauncherTransitionPrepare(l, animated, toWorkspace); + mInTransition = true; + mTransitioningToWorkspace = toWorkspace; + + if (toWorkspace) { + // Going from All Apps -> Workspace + setVisibilityOfSiblingsWithLowerZOrder(VISIBLE); + // Stop the scrolling indicator - we don't want All Apps to be invalidating itself + // during the transition, especially since it has a hardware layer set on it + mAppsCustomizePane.cancelScrollingIndicatorAnimations(); + } 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) + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true); + + if (!LauncherApplication.isScreenLarge()) { + mAppsCustomizePane.showScrollingIndicator(true); + } + } + + if (mResetAfterTransition) { + mAppsCustomizePane.reset(); + mResetAfterTransition = false; + } + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + if (animated) { + enableAndBuildHardwareLayer(); + } + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + // Do nothing + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + mAppsCustomizePane.onLauncherTransitionEnd(l, animated, toWorkspace); + mInTransition = false; + if (animated) { + setLayerType(LAYER_TYPE_NONE, null); + } + + if (!toWorkspace) { + // Dismiss the workspace cling + l.dismissWorkspaceCling(null); + // Show the all apps cling (if not already shown) + mAppsCustomizePane.showAllAppsCling(); + // Make sure adjacent pages are loaded (we wait until after the transition to + // prevent slowing down the animation) + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); + + if (!LauncherApplication.isScreenLarge()) { + mAppsCustomizePane.hideScrollingIndicator(false); + } + + // 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; + + 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) { + continue; + } + child.setVisibility(visibility); + } + } + } else { + throw new RuntimeException("Failed; can't get z-order of views"); + } + } + + 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 + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true); + mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); + } + } + + public void onTrimMemory() { + mContent.setVisibility(GONE); + // Clear the widget pages of all their subviews - this will trigger the widget previews + // to delete their bitmaps + mAppsCustomizePane.clearAllWidgetPages(); + } + + boolean isTransitioning() { + return mInTransition; + } +} diff --git a/app/src/main/java/com/android/launcher2/BubbleTextView.java b/app/src/main/java/com/android/launcher2/BubbleTextView.java new file mode 100644 index 0000000..725eb6c --- /dev/null +++ b/app/src/main/java/com/android/launcher2/BubbleTextView.java @@ -0,0 +1,342 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.TextView; + +/** + * 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 { + static final float CORNER_RADIUS = 4.0f; + static final float SHADOW_LARGE_RADIUS = 4.0f; + static final float SHADOW_SMALL_RADIUS = 1.75f; + static final float SHADOW_Y_OFFSET = 2.0f; + static final int SHADOW_LARGE_COLOUR = 0xDD000000; + static final int SHADOW_SMALL_COLOUR = 0xCC000000; + static final float PADDING_H = 8.0f; + static final float PADDING_V = 3.0f; + + private int mPrevAlpha = -1; + + private final HolographicOutlineHelper mOutlineHelper = new HolographicOutlineHelper(); + private final Canvas mTempCanvas = new Canvas(); + private final Rect mTempRect = new Rect(); + private boolean mDidInvalidateForPressedState; + private Bitmap mPressedOrFocusedBackground; + private int mFocusedOutlineColor; + private int mFocusedGlowColor; + private int mPressedOutlineColor; + private int mPressedGlowColor; + + private boolean mBackgroundSizeChanged; + private Drawable mBackground; + + private boolean mStayPressed; + private CheckLongPressHelper mLongPressHelper; + + public BubbleTextView(Context context) { + super(context); + init(); + } + + public BubbleTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + mLongPressHelper = new CheckLongPressHelper(this); + mBackground = getBackground(); + + final Resources res = getContext().getResources(); + mFocusedOutlineColor = mFocusedGlowColor = mPressedOutlineColor = mPressedGlowColor = + res.getColor(android.R.color.white); + + setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); + } + + public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) { + Bitmap b = info.getIcon(iconCache); + + setCompoundDrawablesWithIntrinsicBounds(null, + new FastBitmapDrawable(b), + null, null); + setText(info.title); + setTag(info); + } + + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + if (getLeft() != left || getRight() != right || getTop() != top || getBottom() != bottom) { + mBackgroundSizeChanged = true; + } + return super.setFrame(left, top, right, bottom); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mBackground || super.verifyDrawable(who); + } + + @Override + public void setTag(Object tag) { + if (tag != null) { + LauncherModel.checkItemInfo((ItemInfo) tag); + } + super.setTag(tag); + } + + @Override + protected void drawableStateChanged() { + if (isPressed()) { + // In this case, we have already created the pressed outline on ACTION_DOWN, + // so we just need to do an invalidate to trigger draw + if (!mDidInvalidateForPressedState) { + setCellLayoutPressedOrFocusedIcon(); + } + } else { + // Otherwise, either clear the pressed/focused background, or create a background + // for the focused state + final boolean backgroundEmptyBefore = mPressedOrFocusedBackground == null; + if (!mStayPressed) { + mPressedOrFocusedBackground = null; + } + if (isFocused()) { + if (getLayout() == null) { + // In some cases, we get focus before we have been layed out. Set the + // background to null so that it will get created when the view is drawn. + mPressedOrFocusedBackground = null; + } else { + mPressedOrFocusedBackground = createGlowingOutline( + mTempCanvas, mFocusedGlowColor, mFocusedOutlineColor); + } + mStayPressed = false; + setCellLayoutPressedOrFocusedIcon(); + } + final boolean backgroundEmptyNow = mPressedOrFocusedBackground == null; + if (!backgroundEmptyBefore && backgroundEmptyNow) { + setCellLayoutPressedOrFocusedIcon(); + } + } + + Drawable d = mBackground; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + super.drawableStateChanged(); + } + + /** + * Draw this BubbleTextView into the given Canvas. + * + * @param destCanvas the canvas to draw on + * @param padding the horizontal and vertical padding to use when drawing + */ + private void drawWithPadding(Canvas destCanvas, int padding) { + final Rect clipRect = mTempRect; + getDrawingRect(clipRect); + + // adjust the clip rect so that we don't include the text label + clipRect.bottom = + getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V + getLayout().getLineTop(0); + + // 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 + destCanvas.save(); + destCanvas.scale(getScaleX(), getScaleY(), + (getWidth() + padding) / 2, (getHeight() + padding) / 2); + destCanvas.translate(-getScrollX() + padding / 2, -getScrollY() + padding / 2); + destCanvas.clipRect(clipRect); + draw(destCanvas); + destCanvas.restore(); + } + + /** + * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createGlowingOutline(Canvas canvas, int outlineColor, int glowColor) { + final int padding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS; + final Bitmap b = Bitmap.createBitmap( + getWidth() + padding, getHeight() + padding, Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + drawWithPadding(canvas, padding); + mOutlineHelper.applyExtraThickExpensiveOutlineWithBlur(b, canvas, glowColor, outlineColor); + canvas.setBitmap(null); + + return b; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Call the superclass onTouchEvent first, because sometimes it changes the state to + // isPressed() on an ACTION_UP + boolean result = super.onTouchEvent(event); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // So that the pressed outline is visible immediately when isPressed() is true, + // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time + // to create it) + if (mPressedOrFocusedBackground == null) { + mPressedOrFocusedBackground = createGlowingOutline( + mTempCanvas, mPressedGlowColor, mPressedOutlineColor); + } + // Invalidate so the pressed state is visible, or set a flag so we know that we + // have to call invalidate as soon as the state is "pressed" + if (isPressed()) { + mDidInvalidateForPressedState = true; + setCellLayoutPressedOrFocusedIcon(); + } else { + mDidInvalidateForPressedState = false; + } + + mLongPressHelper.postCheckForLongPress(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + // If we've touched down and up on an item, and it's still not "pressed", then + // destroy the pressed outline + if (!isPressed()) { + mPressedOrFocusedBackground = null; + } + + mLongPressHelper.cancelLongPress(); + break; + } + return result; + } + + void setStayPressed(boolean stayPressed) { + mStayPressed = stayPressed; + if (!stayPressed) { + mPressedOrFocusedBackground = null; + } + setCellLayoutPressedOrFocusedIcon(); + } + + void setCellLayoutPressedOrFocusedIcon() { + if (getParent() instanceof ShortcutAndWidgetContainer) { + ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) getParent(); + if (parent != null) { + CellLayout layout = (CellLayout) parent.getParent(); + layout.setPressedOrFocusedIcon((mPressedOrFocusedBackground != null) ? this : null); + } + } + } + + void clearPressedOrFocusedBackground() { + mPressedOrFocusedBackground = null; + setCellLayoutPressedOrFocusedIcon(); + } + + Bitmap getPressedOrFocusedBackground() { + return mPressedOrFocusedBackground; + } + + int getPressedOrFocusedBackgroundPadding() { + return HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS / 2; + } + + @Override + public void draw(Canvas canvas) { + final Drawable background = mBackground; + if (background != null) { + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + + if (mBackgroundSizeChanged) { + background.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); + mBackgroundSizeChanged = false; + } + + if ((scrollX | scrollY) == 0) { + background.draw(canvas); + } else { + canvas.translate(scrollX, scrollY); + background.draw(canvas); + canvas.translate(-scrollX, -scrollY); + } + } + + // If text is transparent, don't draw any shadow + if (getCurrentTextColor() == getResources().getColor(android.R.color.transparent)) { + getPaint().clearShadowLayer(); + super.draw(canvas); + return; + } + + // We enhance the shadow by drawing the shadow twice + getPaint().setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR); + super.draw(canvas); + canvas.save(); + canvas.clipRect(getScrollX(), getScrollY() + getExtendedPaddingTop(), + getScrollX() + getWidth(), + getScrollY() + getHeight(), Region.Op.INTERSECT); + getPaint().setShadowLayer(SHADOW_SMALL_RADIUS, 0.0f, 0.0f, SHADOW_SMALL_COLOUR); + super.draw(canvas); + canvas.restore(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mBackground != null) mBackground.setCallback(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mBackground != null) mBackground.setCallback(null); + } + + @Override + protected boolean onSetAlpha(int alpha) { + if (mPrevAlpha != alpha) { + mPrevAlpha = alpha; + super.onSetAlpha(alpha); + } + return true; + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + mLongPressHelper.cancelLongPress(); + } +} diff --git a/app/src/main/java/com/android/launcher2/ButtonDropTarget.java b/app/src/main/java/com/android/launcher2/ButtonDropTarget.java new file mode 100644 index 0000000..ff0813a --- /dev/null +++ b/app/src/main/java/com/android/launcher2/ButtonDropTarget.java @@ -0,0 +1,166 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import com.android.launcher.R; + + +/** + * Implements a DropTarget. + */ +public class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener { + + protected final int mTransitionDuration; + + protected Launcher mLauncher; + private int mBottomDragPadding; + protected TextView mText; + protected SearchDropTargetBar mSearchDropTargetBar; + + /** Whether this drop target is active for the current drag */ + protected boolean mActive; + + /** The paint applied to the drag view on hover */ + protected int mHoverColor = 0; + + 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); + } + + 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; + } + + public void onDrop(DragObject d) { + } + + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + public void onDragEnter(DragObject d) { + d.dragView.setColor(mHoverColor); + } + + public void onDragOver(DragObject d) { + // Do nothing + } + + public void onDragExit(DragObject d) { + d.dragView.setColor(0); + } + + public void onDragStart(DragSource source, Object info, int dragAction) { + // Do nothing + } + + public boolean isDropEnabled() { + return mActive; + } + + public void onDragEnd() { + // Do nothing + } + + @Override + public void getHitRect(android.graphics.Rect outRect) { + super.getHitRect(outRect); + outRect.bottom += mBottomDragPadding; + } + + private boolean isRtl() { + return (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + } + + 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) + Rect to = new Rect(); + dragLayer.getViewRectRelativeToSelf(this, to); + + final int width = drawableWidth; + final int height = drawableHeight; + + final int left; + final int right; + + if (isRtl()) { + right = to.right - getPaddingRight(); + left = right - width; + } else { + left = to.left + getPaddingLeft(); + right = left + width; + } + + final int top = to.top + (getMeasuredHeight() - height) / 2; + final int bottom = top + height; + + to.set(left, top, right, bottom); + + // Center the destination rect about the trash icon + final int xOffset = (int) -(viewWidth - width) / 2; + final int yOffset = (int) -(viewHeight - height) / 2; + to.offset(xOffset, yOffset); + + return to; + } + + @Override + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + public void getLocationInDragLayer(int[] loc) { + mLauncher.getDragLayer().getLocationInDragLayer(this, loc); + } +} diff --git a/app/src/main/java/com/android/launcher2/CellLayout.java b/app/src/main/java/com/android/launcher2/CellLayout.java new file mode 100644 index 0000000..024bb37 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/CellLayout.java @@ -0,0 +1,3338 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LayoutAnimationController; + +import com.android.launcher.R; +import com.android.launcher2.FolderIcon.FolderRingAnimator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Stack; + +public class CellLayout extends ViewGroup { + static final String TAG = "CellLayout"; + + private Launcher mLauncher; + private int mCellWidth; + private int mCellHeight; + + private int mCountX; + private int mCountY; + + private int mOriginalWidthGap; + private int mOriginalHeightGap; + private int mWidthGap; + private int mHeightGap; + private int mMaxGap; + private boolean mScrollingTransformsDirty = false; + + private final Rect mRect = new Rect(); + private final CellInfo mCellInfo = new CellInfo(); + + // 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]; + + boolean[][] mOccupied; + boolean[][] mTmpOccupied; + private boolean mLastDownOnOccupiedCell = false; + + private OnTouchListener mInterceptTouchListener; + + private ArrayList mFolderOuterRings = new ArrayList(); + private int[] mFolderLeaveBehindCell = {-1, -1}; + + private int mForegroundAlpha = 0; + private float mBackgroundAlpha; + private float mBackgroundAlphaMultiplier = 1.0f; + + private Drawable mNormalBackground; + private Drawable mActiveGlowBackground; + private Drawable mOverScrollForegroundDrawable; + private Drawable mOverScrollLeft; + private Drawable mOverScrollRight; + private Rect mBackgroundRect; + private Rect mForegroundRect; + private int mForegroundPadding; + + // If we're actively dragging something over this screen, mIsDragOverlapping is true + private boolean mIsDragOverlapping = false; + private final Point mDragCenter = new Point(); + + // 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]; + private InterruptibleInOutAnimator[] mDragOutlineAnims = + new InterruptibleInOutAnimator[mDragOutlines.length]; + + // Used as an index into the above 3 arrays; indicates which is the most current value. + private int mDragOutlineCurrent = 0; + private final Paint mDragOutlinePaint = new Paint(); + + private BubbleTextView mPressedOrFocusedIcon; + + private HashMap mReorderAnimators = new + HashMap(); + private HashMap + mShakeAnimators = new HashMap(); + + private boolean mItemPlacementDirty = false; + + // When a drag operation is in progress, holds the nearest cell to the touch point + private final int[] mDragCell = new int[2]; + + private boolean mDragging = false; + + private TimeInterpolator mEaseOutInterpolator; + private ShortcutAndWidgetContainer mShortcutsAndWidgets; + + private boolean mIsHotseat = false; + private float mHotseatScale = 1f; + + public static final int MODE_DRAG_OVER = 0; + public static final int MODE_ON_DROP = 1; + public static final int MODE_ON_DROP_EXTERNAL = 2; + public static final int MODE_ACCEPT_DROP = 3; + private static final boolean DESTRUCTIVE_REORDER = false; + private static final boolean DEBUG_VISUALIZE_OCCUPIED = false; + + static final int LANDSCAPE = 0; + static final int PORTRAIT = 1; + + private static final float REORDER_HINT_MAGNITUDE = 0.12f; + private static final int REORDER_ANIMATION_DURATION = 150; + private float mReorderHintAnimationMagnitude; + + 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 final static PorterDuffXfermode sAddBlendMode = + new PorterDuffXfermode(PorterDuff.Mode.ADD); + private final static Paint sPaint = new Paint(); + + public CellLayout(Context context) { + this(context, null); + } + + public CellLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + 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. + setWillNotDraw(false); + setClipToPadding(false); + mLauncher = (Launcher) context; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); + + mCellWidth = a.getDimensionPixelSize(R.styleable.CellLayout_cellWidth, 10); + mCellHeight = a.getDimensionPixelSize(R.styleable.CellLayout_cellHeight, 10); + mWidthGap = mOriginalWidthGap = a.getDimensionPixelSize(R.styleable.CellLayout_widthGap, 0); + mHeightGap = mOriginalHeightGap = a.getDimensionPixelSize(R.styleable.CellLayout_heightGap, 0); + mMaxGap = a.getDimensionPixelSize(R.styleable.CellLayout_maxGap, 0); + mCountX = LauncherModel.getCellCountX(); + mCountY = LauncherModel.getCellCountY(); + mOccupied = new boolean[mCountX][mCountY]; + mTmpOccupied = new boolean[mCountX][mCountY]; + mPreviousReorderDirection[0] = INVALID_DIRECTION; + mPreviousReorderDirection[1] = INVALID_DIRECTION; + + a.recycle(); + + setAlwaysDrawnWithCacheEnabled(false); + + final Resources res = getResources(); + mHotseatScale = (res.getInteger(R.integer.hotseat_item_scale_percentage) / 100f); + + mNormalBackground = res.getDrawable(R.drawable.homescreen_blue_normal_holo); + mActiveGlowBackground = res.getDrawable(R.drawable.homescreen_blue_strong_holo); + + 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); + + mReorderHintAnimationMagnitude = (REORDER_HINT_MAGNITUDE * + res.getDimensionPixelSize(R.dimen.app_icon_size)); + + 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; + for (int i = 0; i < mDragOutlines.length; i++) { + mDragOutlines[i] = new Rect(-1, -1, -1, -1); + } + + // When dragging things around the home screens, we show a green outline of + // where the item will land. The outlines gradually fade out, leaving a trail + // behind the drag path. + // Set up all the animations that are used to implement this fading. + final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime); + final float fromAlphaValue = 0; + final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha); + + Arrays.fill(mDragOutlineAlphas, fromAlphaValue); + + for (int i = 0; i < mDragOutlineAnims.length; i++) { + final InterruptibleInOutAnimator anim = + new InterruptibleInOutAnimator(this, duration, fromAlphaValue, toAlphaValue); + anim.getAnimator().setInterpolator(mEaseOutInterpolator); + final int thisIndex = i; + anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final Bitmap outline = (Bitmap)anim.getTag(); + + // If an animation is started and then stopped very quickly, we can still + // get spurious updates we've cleared the tag. Guard against this. + if (outline == null) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + Object val = animation.getAnimatedValue(); + Log.d(TAG, "anim " + thisIndex + " update: " + val + + ", isStopped " + anim.isStopped()); + } + // Try to prevent it from continuing to run + animation.cancel(); + } else { + mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue(); + CellLayout.this.invalidate(mDragOutlines[thisIndex]); + } + } + }); + // The animation holds a reference to the drag outline bitmap as long is it's + // running. This way the bitmap can be GCed when the animations are complete. + anim.getAnimator().addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if ((Float) ((ValueAnimator) animation).getAnimatedValue() == 0f) { + anim.setTag(null); + } + } + }); + mDragOutlineAnims[i] = anim; + } + + mBackgroundRect = new Rect(); + mForegroundRect = new Rect(); + + mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context); + mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, + mCountX); + + addView(mShortcutsAndWidgets); + } + + static int widthInPortrait(Resources r, int numCells) { + // We use this method from Workspace to figure out how many rows/columns Launcher should + // have. We ignore the left/right padding on CellLayout because it turns out in our design + // the padding extends outside the visible screen size, but it looked fine anyway. + int cellWidth = r.getDimensionPixelSize(R.dimen.workspace_cell_width); + int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap), + r.getDimensionPixelSize(R.dimen.workspace_height_gap)); + + return minGap * (numCells - 1) + cellWidth * numCells; + } + + static int heightInLandscape(Resources r, int numCells) { + // We use this method from Workspace to figure out how many rows/columns Launcher should + // have. We ignore the left/right padding on CellLayout because it turns out in our design + // the padding extends outside the visible screen size, but it looked fine anyway. + int cellHeight = r.getDimensionPixelSize(R.dimen.workspace_cell_height); + int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap), + r.getDimensionPixelSize(R.dimen.workspace_height_gap)); + + return minGap * (numCells - 1) + cellHeight * numCells; + } + + public void enableHardwareLayers() { + mShortcutsAndWidgets.setLayerType(LAYER_TYPE_HARDWARE, sPaint); + } + + public void disableHardwareLayers() { + mShortcutsAndWidgets.setLayerType(LAYER_TYPE_NONE, sPaint); + } + + public void buildHardwareLayer() { + mShortcutsAndWidgets.buildLayer(); + } + + public float getChildrenScale() { + return mIsHotseat ? mHotseatScale : 1.0f; + } + + public void setGridSize(int x, int y) { + mCountX = x; + mCountY = y; + mOccupied = new boolean[mCountX][mCountY]; + mTmpOccupied = new boolean[mCountX][mCountY]; + mTempRectStack.clear(); + mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, + mCountX); + requestLayout(); + } + + // Set whether or not to invert the layout horizontally if the layout is in RTL mode. + public void setInvertIfRtl(boolean invert) { + mShortcutsAndWidgets.setInvertIfRtl(invert); + } + + private void invalidateBubbleTextView(BubbleTextView icon) { + final int padding = icon.getPressedOrFocusedBackgroundPadding(); + invalidate(icon.getLeft() + getPaddingLeft() - padding, + icon.getTop() + getPaddingTop() - padding, + icon.getRight() + getPaddingLeft() + padding, + icon.getBottom() + getPaddingTop() + padding); + } + + void setOverScrollAmount(float r, boolean left) { + if (left && mOverScrollForegroundDrawable != mOverScrollLeft) { + mOverScrollForegroundDrawable = mOverScrollLeft; + } else if (!left && mOverScrollForegroundDrawable != mOverScrollRight) { + mOverScrollForegroundDrawable = mOverScrollRight; + } + + mForegroundAlpha = (int) Math.round((r * 255)); + mOverScrollForegroundDrawable.setAlpha(mForegroundAlpha); + invalidate(); + } + + void setPressedOrFocusedIcon(BubbleTextView icon) { + // We draw the pressed or focused BubbleTextView's background in CellLayout because it + // requires an expanded clip rect (due to the glow's blur radius) + BubbleTextView oldIcon = mPressedOrFocusedIcon; + mPressedOrFocusedIcon = icon; + if (oldIcon != null) { + invalidateBubbleTextView(oldIcon); + } + if (mPressedOrFocusedIcon != null) { + invalidateBubbleTextView(mPressedOrFocusedIcon); + } + } + + void setIsDragOverlapping(boolean isDragOverlapping) { + if (mIsDragOverlapping != isDragOverlapping) { + mIsDragOverlapping = isDragOverlapping; + invalidate(); + } + } + + boolean getIsDragOverlapping() { + return mIsDragOverlapping; + } + + protected void setOverscrollTransformsDirty(boolean dirty) { + mScrollingTransformsDirty = dirty; + } + + protected void resetOverscrollTransforms() { + if (mScrollingTransformsDirty) { + setOverscrollTransformsDirty(false); + setTranslationX(0); + setRotationY(0); + // It doesn't matter if we pass true or false here, the important thing is that we + // pass 0, which results in the overscroll drawable not being drawn any more. + setOverScrollAmount(0, false); + setPivotX(getMeasuredWidth() / 2); + setPivotY(getMeasuredHeight() / 2); + } + } + + public void scaleRect(Rect r, float scale) { + if (scale != 1.0f) { + r.left = (int) (r.left * scale + 0.5f); + r.top = (int) (r.top * scale + 0.5f); + r.right = (int) (r.right * scale + 0.5f); + r.bottom = (int) (r.bottom * scale + 0.5f); + } + } + + Rect temp = new Rect(); + void scaleRectAboutCenter(Rect in, Rect out, float scale) { + int cx = in.centerX(); + int cy = in.centerY(); + out.set(in); + out.offset(-cx, -cy); + scaleRect(out, scale); + out.offset(cx, cy); + } + + @Override + protected void onDraw(Canvas canvas) { + // 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 (mBackgroundAlpha > 0.0f) { + Drawable bg; + + if (mIsDragOverlapping) { + // 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); + } + + final Paint paint = mDragOutlinePaint; + for (int i = 0; i < mDragOutlines.length; i++) { + final float alpha = mDragOutlineAlphas[i]; + if (alpha > 0) { + final Rect r = mDragOutlines[i]; + scaleRectAboutCenter(r, temp, getChildrenScale()); + final Bitmap b = (Bitmap) mDragOutlineAnims[i].getTag(); + paint.setAlpha((int)(alpha + .5f)); + canvas.drawBitmap(b, null, temp, paint); + } + } + + // We draw the pressed or focused BubbleTextView's background in CellLayout because it + // requires an expanded clip rect (due to the glow's blur radius) + if (mPressedOrFocusedIcon != null) { + final int padding = mPressedOrFocusedIcon.getPressedOrFocusedBackgroundPadding(); + final Bitmap b = mPressedOrFocusedIcon.getPressedOrFocusedBackground(); + if (b != null) { + canvas.drawBitmap(b, + mPressedOrFocusedIcon.getLeft() + getPaddingLeft() - padding, + mPressedOrFocusedIcon.getTop() + getPaddingTop() - padding, + null); + } + } + + if (DEBUG_VISUALIZE_OCCUPIED) { + int[] pt = new int[2]; + ColorDrawable cd = new ColorDrawable(Color.RED); + cd.setBounds(0, 0, mCellWidth, mCellHeight); + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + if (mOccupied[i][j]) { + cellToPoint(i, j, pt); + canvas.save(); + canvas.translate(pt[0], pt[1]); + cd.draw(canvas); + canvas.restore(); + } + } + } + } + + int previewOffset = FolderRingAnimator.sPreviewSize; + + // The folder outer / inner ring image(s) + for (int i = 0; i < mFolderOuterRings.size(); i++) { + FolderRingAnimator fra = mFolderOuterRings.get(i); + + // Draw outer ring + Drawable d = FolderRingAnimator.sSharedOuterRingDrawable; + int width = (int) fra.getOuterRingSize(); + int height = width; + cellToPoint(fra.mCellX, fra.mCellY, mTempLocation); + + int centerX = mTempLocation[0] + mCellWidth / 2; + int centerY = mTempLocation[1] + previewOffset / 2; + + canvas.save(); + canvas.translate(centerX - width / 2, centerY - height / 2); + d.setBounds(0, 0, width, height); + d.draw(canvas); + canvas.restore(); + + // Draw inner ring + d = FolderRingAnimator.sSharedInnerRingDrawable; + width = (int) fra.getInnerRingSize(); + height = width; + cellToPoint(fra.mCellX, fra.mCellY, mTempLocation); + + centerX = mTempLocation[0] + mCellWidth / 2; + centerY = mTempLocation[1] + previewOffset / 2; + canvas.save(); + canvas.translate(centerX - width / 2, centerY - width / 2); + d.setBounds(0, 0, width, height); + d.draw(canvas); + canvas.restore(); + } + + if (mFolderLeaveBehindCell[0] >= 0 && mFolderLeaveBehindCell[1] >= 0) { + Drawable d = FolderIcon.sSharedFolderLeaveBehind; + int width = d.getIntrinsicWidth(); + int height = d.getIntrinsicHeight(); + + cellToPoint(mFolderLeaveBehindCell[0], mFolderLeaveBehindCell[1], mTempLocation); + int centerX = mTempLocation[0] + mCellWidth / 2; + int centerY = mTempLocation[1] + previewOffset / 2; + + canvas.save(); + canvas.translate(centerX - width / 2, centerY - width / 2); + d.setBounds(0, 0, width, height); + d.draw(canvas); + canvas.restore(); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mForegroundAlpha > 0) { + mOverScrollForegroundDrawable.setBounds(mForegroundRect); + Paint p = ((NinePatchDrawable) mOverScrollForegroundDrawable).getPaint(); + p.setXfermode(sAddBlendMode); + mOverScrollForegroundDrawable.draw(canvas); + p.setXfermode(null); + } + } + + public void showFolderAccept(FolderRingAnimator fra) { + mFolderOuterRings.add(fra); + } + + public void hideFolderAccept(FolderRingAnimator fra) { + if (mFolderOuterRings.contains(fra)) { + mFolderOuterRings.remove(fra); + } + invalidate(); + } + + public void setFolderLeaveBehindCell(int x, int y) { + mFolderLeaveBehindCell[0] = x; + mFolderLeaveBehindCell[1] = y; + invalidate(); + } + + public void clearFolderLeaveBehind() { + mFolderLeaveBehindCell[0] = -1; + mFolderLeaveBehindCell[1] = -1; + invalidate(); + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + public void restoreInstanceState(SparseArray states) { + dispatchRestoreInstanceState(states); + } + + @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 setOnInterceptTouchListener(View.OnTouchListener listener) { + mInterceptTouchListener = listener; + } + + int getCountX() { + return mCountX; + } + + int getCountY() { + return mCountY; + } + + public void setIsHotseat(boolean isHotseat) { + mIsHotseat = isHotseat; + } + + public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params, + boolean markCells) { + final LayoutParams lp = params; + + // Hotseat icons - remove text + if (child instanceof BubbleTextView) { + BubbleTextView bubbleChild = (BubbleTextView) child; + + Resources res = getResources(); + if (mIsHotseat) { + bubbleChild.setTextColor(res.getColor(android.R.color.transparent)); + } else { + bubbleChild.setTextColor(res.getColor(R.color.workspace_icon_text_color)); + } + } + + child.setScaleX(getChildrenScale()); + child.setScaleY(getChildrenScale()); + + // Generate an id for each view, this assumes we have at most 256x256 cells + // per workspace screen + if (lp.cellX >= 0 && lp.cellX <= mCountX - 1 && lp.cellY >= 0 && lp.cellY <= mCountY - 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 = mCountX; + if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; + + child.setId(childId); + + mShortcutsAndWidgets.addView(child, index, lp); + + if (markCells) markCellsAsOccupiedForView(child); + + return true; + } + return false; + } + + @Override + public void removeAllViews() { + clearOccupiedCells(); + mShortcutsAndWidgets.removeAllViews(); + } + + @Override + public void removeAllViewsInLayout() { + if (mShortcutsAndWidgets.getChildCount() > 0) { + clearOccupiedCells(); + mShortcutsAndWidgets.removeAllViewsInLayout(); + } + } + + public void removeViewWithoutMarkingCells(View view) { + mShortcutsAndWidgets.removeView(view); + } + + @Override + public void removeView(View view) { + markCellsAsUnoccupiedForView(view); + mShortcutsAndWidgets.removeView(view); + } + + @Override + public void removeViewAt(int index) { + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index)); + mShortcutsAndWidgets.removeViewAt(index); + } + + @Override + public void removeViewInLayout(View view) { + markCellsAsUnoccupiedForView(view); + mShortcutsAndWidgets.removeViewInLayout(view); + } + + @Override + public void removeViews(int start, int count) { + for (int i = start; i < start + count; i++) { + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); + } + mShortcutsAndWidgets.removeViews(start, count); + } + + @Override + public void removeViewsInLayout(int start, int count) { + for (int i = start; i < start + count; i++) { + markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); + } + mShortcutsAndWidgets.removeViewsInLayout(start, count); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mCellInfo.screen = ((ViewGroup) getParent()).indexOfChild(this); + } + + public void setTagToCellInfoForPoint(int touchX, int touchY) { + final CellInfo cellInfo = mCellInfo; + Rect frame = mRect; + final int x = touchX + getScrollX(); + final int y = touchY + getScrollY(); + final int count = mShortcutsAndWidgets.getChildCount(); + + boolean found = false; + for (int i = count - 1; i >= 0; i--) { + final View child = mShortcutsAndWidgets.getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if ((child.getVisibility() == VISIBLE || child.getAnimation() != null) && + lp.isLockedToGrid) { + child.getHitRect(frame); + + float scale = child.getScaleX(); + frame = new Rect(child.getLeft(), child.getTop(), child.getRight(), + child.getBottom()); + // The child hit rect is relative to the CellLayoutChildren parent, so we need to + // offset that by this CellLayout's padding to test an (x,y) point that is relative + // to this view. + frame.offset(getPaddingLeft(), getPaddingTop()); + frame.inset((int) (frame.width() * (1f - scale) / 2), + (int) (frame.height() * (1f - scale) / 2)); + + if (frame.contains(x, y)) { + cellInfo.cell = child; + cellInfo.cellX = lp.cellX; + cellInfo.cellY = lp.cellY; + cellInfo.spanX = lp.cellHSpan; + cellInfo.spanY = lp.cellVSpan; + found = true; + break; + } + } + } + + mLastDownOnOccupiedCell = found; + + if (!found) { + final int cellXY[] = mTmpXY; + pointToCellExact(x, y, cellXY); + + cellInfo.cell = null; + cellInfo.cellX = cellXY[0]; + cellInfo.cellY = cellXY[1]; + cellInfo.spanX = 1; + cellInfo.spanY = 1; + } + setTag(cellInfo); + } + + @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. + final int action = ev.getAction(); + + if (action == MotionEvent.ACTION_DOWN) { + clearTagCellInfo(); + } + + if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) { + return true; + } + + if (action == MotionEvent.ACTION_DOWN) { + setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY()); + } + + return false; + } + + private void clearTagCellInfo() { + final CellInfo cellInfo = mCellInfo; + cellInfo.cell = null; + cellInfo.cellX = -1; + cellInfo.cellY = -1; + cellInfo.spanX = 0; + cellInfo.spanY = 0; + setTag(cellInfo); + } + + public CellInfo getTag() { + return (CellInfo) super.getTag(); + } + + /** + * 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) { + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + + result[0] = (x - hStartPadding) / (mCellWidth + mWidthGap); + result[1] = (y - vStartPadding) / (mCellHeight + mHeightGap); + + final int xAxis = mCountX; + final int yAxis = mCountY; + + if (result[0] < 0) result[0] = 0; + if (result[0] >= xAxis) result[0] = xAxis - 1; + if (result[1] < 0) result[1] = 0; + if (result[1] >= yAxis) result[1] = yAxis - 1; + } + + /** + * Given a point, return the cell that most closely 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 pointToCellRounded(int x, int y, int[] result) { + pointToCellExact(x + (mCellWidth / 2), y + (mCellHeight / 2), result); + } + + /** + * Given a cell coordinate, return the point that represents the upper left corner of that cell + * + * @param cellX X coordinate of the cell + * @param cellY Y coordinate of the cell + * + * @param result Array of 2 ints to hold the x and y coordinate of the point + */ + void cellToPoint(int cellX, int cellY, int[] result) { + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + + result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap); + result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap); + } + + /** + * Given a cell coordinate, return the point that represents the center of the cell + * + * @param cellX X coordinate of the cell + * @param cellY Y coordinate of the cell + * + * @param result Array of 2 ints to hold the x and y coordinate of the point + */ + void cellToCenterPoint(int cellX, int cellY, int[] result) { + regionToCenterPoint(cellX, cellY, 1, 1, result); + } + + /** + * Given a cell coordinate and span return the point that represents the center of the regio + * + * @param cellX X coordinate of the cell + * @param cellY Y coordinate of the cell + * + * @param result Array of 2 ints to hold the x and y coordinate of the point + */ + void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) { + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap) + + (spanX * mCellWidth + (spanX - 1) * mWidthGap) / 2; + result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap) + + (spanY * mCellHeight + (spanY - 1) * mHeightGap) / 2; + } + + /** + * Given a cell coordinate and span fills out a corresponding pixel rect + * + * @param cellX X coordinate of the cell + * @param cellY Y coordinate of the cell + * @param result Rect in which to write the result + */ + void regionToRect(int cellX, int cellY, int spanX, int spanY, Rect result) { + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + final int left = hStartPadding + cellX * (mCellWidth + mWidthGap); + final int top = vStartPadding + cellY * (mCellHeight + mHeightGap); + result.set(left, top, left + (spanX * mCellWidth + (spanX - 1) * mWidthGap), + top + (spanY * mCellHeight + (spanY - 1) * mHeightGap)); + } + + 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; + } + + int getCellWidth() { + return mCellWidth; + } + + int getCellHeight() { + return mCellHeight; + } + + int getWidthGap() { + return mWidthGap; + } + + int getHeightGap() { + 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; + } + + static void getMetrics(Rect metrics, Resources res, int measureWidth, int measureHeight, + int countX, int countY, int orientation) { + int numWidthGaps = countX - 1; + int numHeightGaps = countY - 1; + + int widthGap; + int heightGap; + int cellWidth; + int cellHeight; + int paddingLeft; + int paddingRight; + int paddingTop; + int paddingBottom; + + int maxGap = res.getDimensionPixelSize(R.dimen.workspace_max_gap); + if (orientation == LANDSCAPE) { + cellWidth = res.getDimensionPixelSize(R.dimen.workspace_cell_width_land); + cellHeight = res.getDimensionPixelSize(R.dimen.workspace_cell_height_land); + widthGap = res.getDimensionPixelSize(R.dimen.workspace_width_gap_land); + heightGap = res.getDimensionPixelSize(R.dimen.workspace_height_gap_land); + paddingLeft = res.getDimensionPixelSize(R.dimen.cell_layout_left_padding_land); + paddingRight = res.getDimensionPixelSize(R.dimen.cell_layout_right_padding_land); + paddingTop = res.getDimensionPixelSize(R.dimen.cell_layout_top_padding_land); + paddingBottom = res.getDimensionPixelSize(R.dimen.cell_layout_bottom_padding_land); + } else { + // PORTRAIT + cellWidth = res.getDimensionPixelSize(R.dimen.workspace_cell_width_port); + cellHeight = res.getDimensionPixelSize(R.dimen.workspace_cell_height_port); + widthGap = res.getDimensionPixelSize(R.dimen.workspace_width_gap_port); + heightGap = res.getDimensionPixelSize(R.dimen.workspace_height_gap_port); + paddingLeft = res.getDimensionPixelSize(R.dimen.cell_layout_left_padding_port); + paddingRight = res.getDimensionPixelSize(R.dimen.cell_layout_right_padding_port); + paddingTop = res.getDimensionPixelSize(R.dimen.cell_layout_top_padding_port); + paddingBottom = res.getDimensionPixelSize(R.dimen.cell_layout_bottom_padding_port); + } + + if (widthGap < 0 || heightGap < 0) { + int hSpace = measureWidth - paddingLeft - paddingRight; + int vSpace = measureHeight - paddingTop - paddingBottom; + int hFreeSpace = hSpace - (countX * cellWidth); + int vFreeSpace = vSpace - (countY * cellHeight); + widthGap = Math.min(maxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0); + heightGap = Math.min(maxGap, numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0); + } + metrics.set(cellWidth, cellHeight, widthGap, heightGap); + } + + @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"); + } + + int numWidthGaps = mCountX - 1; + int numHeightGaps = mCountY - 1; + + if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) { + int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight(); + int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom(); + int hFreeSpace = hSpace - (mCountX * mCellWidth); + int vFreeSpace = vSpace - (mCountY * mCellHeight); + mWidthGap = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0); + mHeightGap = Math.min(mMaxGap,numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0); + mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap, + mCountX); + } 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() + (mCountX * mCellWidth) + + ((mCountX - 1) * mWidthGap); + newHeight = getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) + + ((mCountY - 1) * mHeightGap); + setMeasuredDimension(newWidth, newHeight); + } + + 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); + } + + @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 + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mBackgroundRect.set(0, 0, w, h); + mForegroundRect.set(mForegroundPadding, mForegroundPadding, + w - mForegroundPadding, h - mForegroundPadding); + } + + @Override + protected void setChildrenDrawingCacheEnabled(boolean enabled) { + mShortcutsAndWidgets.setChildrenDrawingCacheEnabled(enabled); + } + + @Override + protected void setChildrenDrawnWithCacheEnabled(boolean enabled) { + mShortcutsAndWidgets.setChildrenDrawnWithCacheEnabled(enabled); + } + + public float getBackgroundAlpha() { + 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(); + } + } + + public void setShortcutAndWidgetAlpha(float alpha) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).setAlpha(alpha); + } + } + + public ShortcutAndWidgetContainer getShortcutsAndWidgets() { + if (getChildCount() > 0) { + return (ShortcutAndWidgetContainer) getChildAt(0); + } + return null; + } + + public View getChildAt(int x, int y) { + return mShortcutsAndWidgets.getChildAt(x, y); + } + + public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration, + int delay, boolean permanent, boolean adjustOccupied) { + ShortcutAndWidgetContainer clc = getShortcutsAndWidgets(); + boolean[][] occupied = mOccupied; + if (!permanent) { + occupied = mTmpOccupied; + } + + if (clc.indexOfChild(child) != -1) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final ItemInfo info = (ItemInfo) child.getTag(); + + // We cancel any existing animations + if (mReorderAnimators.containsKey(lp)) { + mReorderAnimators.get(lp).cancel(); + mReorderAnimators.remove(lp); + } + + final int oldX = lp.x; + final int oldY = lp.y; + if (adjustOccupied) { + occupied[lp.cellX][lp.cellY] = false; + occupied[cellX][cellY] = true; + } + lp.isLockedToGrid = true; + if (permanent) { + lp.cellX = info.cellX = cellX; + lp.cellY = info.cellY = cellY; + } else { + lp.tmpCellX = cellX; + lp.tmpCellY = cellY; + } + clc.setupLp(lp); + lp.isLockedToGrid = false; + final int newX = lp.x; + final int newY = lp.y; + + lp.x = oldX; + lp.y = oldY; + + // Exit early if we're not actually moving the view + if (oldX == newX && oldY == newY) { + lp.isLockedToGrid = true; + return true; + } + + ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f); + va.setDuration(duration); + mReorderAnimators.put(lp, va); + + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float r = ((Float) animation.getAnimatedValue()).floatValue(); + lp.x = (int) ((1 - r) * oldX + r * newX); + lp.y = (int) ((1 - r) * oldY + r * newY); + child.requestLayout(); + } + }); + va.addListener(new AnimatorListenerAdapter() { + boolean cancelled = false; + public void onAnimationEnd(Animator animation) { + // If the animation was cancelled, it means that another animation + // has interrupted this one, and we don't want to lock the item into + // place just yet. + if (!cancelled) { + lp.isLockedToGrid = true; + child.requestLayout(); + } + if (mReorderAnimators.containsKey(lp)) { + mReorderAnimators.remove(lp); + } + } + public void onAnimationCancel(Animator animation) { + cancelled = true; + } + }); + va.setStartDelay(delay); + va.start(); + return true; + } + 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]; + final int oldDragCellY = mDragCell[1]; + + if (v != null && dragOffset == null) { + mDragCenter.set(originX + (v.getWidth() / 2), originY + (v.getHeight() / 2)); + } else { + mDragCenter.set(originX, originY); + } + + if (dragOutline == null && v == null) { + return; + } + + if (cellX != oldDragCellX || cellY != oldDragCellY) { + mDragCell[0] = cellX; + mDragCell[1] = cellY; + // Find the top left corner of the rect the object will occupy + final int[] topLeft = mTmpPoint; + cellToPoint(cellX, cellY, topLeft); + + int left = topLeft[0]; + int top = topLeft[1]; + + if (v != null && dragOffset == null) { + // When drawing the drag outline, it did not account for margin offsets + // added by the view's parent. + MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams(); + left += lp.leftMargin; + top += lp.topMargin; + + // Offsets due to the size difference between the View and the dragOutline. + // There is a size difference to account for the outer blur, which may lie + // outside the bounds of the view. + top += (v.getHeight() - dragOutline.getHeight()) / 2; + // We center about the x axis + left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) + - dragOutline.getWidth()) / 2; + } else { + if (dragOffset != null && dragRegion != null) { + // Center the drag region *horizontally* in the cell and apply a drag + // outline offset + left += dragOffset.x + ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) + - dragRegion.width()) / 2; + top += dragOffset.y; + } else { + // Center the drag outline in the cell + left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) + - dragOutline.getWidth()) / 2; + top += ((mCellHeight * spanY) + ((spanY - 1) * mHeightGap) + - dragOutline.getHeight()) / 2; + } + } + final int oldIndex = mDragOutlineCurrent; + mDragOutlineAnims[oldIndex].animateOut(); + mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length; + Rect r = mDragOutlines[mDragOutlineCurrent]; + r.set(left, top, left + dragOutline.getWidth(), top + dragOutline.getHeight()); + if (resize) { + cellToRect(cellX, cellY, spanX, spanY, r); + } + + mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline); + mDragOutlineAnims[mDragOutlineCurrent].animateIn(); + } + } + + public void clearDragOutlines() { + final int oldIndex = mDragOutlineCurrent; + mDragOutlineAnims[oldIndex].animateOut(); + mDragCell[0] = mDragCell[1] = -1; + } + + /** + * 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 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[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, + int[] result) { + return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, 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 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[] 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, + 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()) { + for (int i = 0; i < mCountX * mCountY; i++) { + mTempRectStack.push(new Rect()); + } + } + } + + private void recycleTempRects(Stack used) { + while (!used.isEmpty()) { + mTempRectStack.push(used.pop()); + } + } + + /** + * 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 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 minSpanX, int minSpanY, int spanX, int spanY, + View ignoreView, boolean ignoreOccupied, int[] result, int[] resultSpan, + boolean[][] occupied) { + 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 + // we translate the point over to correspond to the top-left. + pixelX -= (mCellWidth + mWidthGap) * (spanX - 1) / 2f; + pixelY -= (mCellHeight + mHeightGap) * (spanY - 1) / 2f; + + // Keep track of best-scoring drop area + final int[] bestXY = result != null ? result : new int[2]; + double bestDistance = Double.MAX_VALUE; + final Rect bestRect = new Rect(-1, -1, -1, -1); + final Stack validRegions = new Stack(); + + final int countX = mCountX; + final int countY = mCountY; + + if (minSpanX <= 0 || minSpanY <= 0 || spanX <= 0 || spanY <= 0 || + spanX < minSpanX || spanY < minSpanY) { + return bestXY; + } + + for (int y = 0; y < countY - (minSpanY - 1); y++) { + inner: + for (int x = 0; x < countX - (minSpanX - 1); x++) { + int ySize = -1; + int xSize = -1; + if (ignoreOccupied) { + // 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]) { + continue inner; + } + } + } + xSize = minSpanX; + ySize = minSpanY; + + // We know that the item will fit at _some_ acceptable size, now let's see + // how big we can make it. We'll alternate between incrementing x and y spans + // until we hit a limit. + boolean incX = true; + boolean hitMaxX = xSize >= spanX; + boolean hitMaxY = ySize >= spanY; + while (!(hitMaxX && hitMaxY)) { + if (incX && !hitMaxX) { + for (int j = 0; j < ySize; j++) { + if (x + xSize > countX -1 || occupied[x + xSize][y + j]) { + // We can't move out horizontally + hitMaxX = true; + } + } + if (!hitMaxX) { + xSize++; + } + } else if (!hitMaxY) { + for (int i = 0; i < xSize; i++) { + if (y + ySize > countY - 1 || occupied[x + i][y + ySize]) { + // We can't move out vertically + hitMaxY = true; + } + } + if (!hitMaxY) { + ySize++; + } + } + hitMaxX |= xSize >= spanX; + hitMaxY |= ySize >= spanY; + incX = !incX; + } + incX = true; + hitMaxX = xSize >= spanX; + hitMaxY = ySize >= spanY; + } + final int[] cellXY = mTmpXY; + cellToCenterPoint(x, y, cellXY); + + // We verify that the current rect is not a sub-rect of any of our previous + // candidates. In this case, the current rect is disqualified in favour of the + // containing rect. + Rect currentRect = mTempRectStack.pop(); + currentRect.set(x, y, x + xSize, y + ySize); + boolean contained = false; + for (Rect r : validRegions) { + if (r.contains(currentRect)) { + contained = true; + break; + } + } + validRegions.push(currentRect); + double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2) + + Math.pow(cellXY[1] - pixelY, 2)); + + if ((distance <= bestDistance && !contained) || + currentRect.contains(bestRect)) { + bestDistance = distance; + bestXY[0] = x; + bestXY[1] = y; + if (resultSpan != null) { + resultSpan[0] = xSize; + resultSpan[1] = ySize; + } + bestRect.set(currentRect); + } + } + } + // re-mark space taken by ignoreView as occupied + markCellsAsOccupiedForView(ignoreView, occupied); + + // Return -1, -1 if no suitable location found + if (bestDistance == Double.MAX_VALUE) { + bestXY[0] = -1; + bestXY[1] = -1; + } + recycleTempRects(validRegions); + return bestXY; + } + + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location, and will also weigh in a suggested direction vector of the + * desired location. This method computers distance based on unit grid distances, + * not pixel distances. + * + * @param cellX The X cell nearest to which you want to search for a vacant area. + * @param cellY The Y cell nearest which you want to search for a vacant area. + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param direction The favored direction in which the views should move from x, y + * @param exactDirectionOnly If this parameter is true, then only solutions where the direction + * matches exactly. Otherwise we find the best matching direction. + * @param occoupied The array which represents which cells in the CellLayout are occupied + * @param blockOccupied The array which represents which cells in the specified block (cellX, + * cellY, spanX, spanY) are occupied. This is used when try to move a group of views. + * @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. + */ + private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction, + boolean[][] occupied, boolean blockOccupied[][], int[] result) { + // Keep track of best-scoring drop area + final int[] bestXY = result != null ? result : new int[2]; + float bestDistance = Float.MAX_VALUE; + int bestDirectionScore = Integer.MIN_VALUE; + + final int countX = mCountX; + final int countY = mCountY; + + for (int y = 0; y < countY - (spanY - 1); y++) { + inner: + for (int x = 0; x < countX - (spanX - 1); x++) { + // First, let's see if this thing fits anywhere + for (int i = 0; i < spanX; i++) { + for (int j = 0; j < spanY; j++) { + if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) { + continue inner; + } + } + } + + float distance = (float) + Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY)); + int[] curDirection = mTmpPoint; + computeDirectionVector(x - cellX, y - cellY, curDirection); + // The direction score is just the dot product of the two candidate direction + // and that passed in. + int curDirectionScore = direction[0] * curDirection[0] + + direction[1] * curDirection[1]; + boolean exactDirectionOnly = false; + boolean directionMatches = direction[0] == curDirection[0] && + direction[0] == curDirection[0]; + if ((directionMatches || !exactDirectionOnly) && + Float.compare(distance, bestDistance) < 0 || (Float.compare(distance, + bestDistance) == 0 && curDirectionScore > bestDirectionScore)) { + bestDistance = distance; + bestDirectionScore = curDirectionScore; + bestXY[0] = x; + bestXY[1] = y; + } + } + } + + // Return -1, -1 if no suitable location found + if (bestDistance == Float.MAX_VALUE) { + bestXY[0] = -1; + bestXY[1] = -1; + } + return bestXY; + } + + private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop, + int[] direction, ItemConfiguration currentState) { + CellAndSpan c = currentState.map.get(v); + boolean success = false; + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); + markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true); + + findNearestArea(c.x, c.y, c.spanX, c.spanY, direction, mTmpOccupied, null, mTempLocation); + + if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { + c.x = mTempLocation[0]; + c.y = mTempLocation[1]; + success = true; + } + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); + return success; + } + + /** + * This helper class defines a cluster of views. It helps with defining complex edges + * of the cluster and determining how those edges interact with other views. The edges + * essentially define a fine-grained boundary around the cluster of views -- like a more + * precise version of a bounding box. + */ + private class ViewCluster { + final static int LEFT = 0; + final static int TOP = 1; + final static int RIGHT = 2; + final static int BOTTOM = 3; + + ArrayList views; + ItemConfiguration config; + Rect boundingRect = new Rect(); + + int[] leftEdge = new int[mCountY]; + int[] rightEdge = new int[mCountY]; + int[] topEdge = new int[mCountX]; + int[] bottomEdge = new int[mCountX]; + boolean leftEdgeDirty, rightEdgeDirty, topEdgeDirty, bottomEdgeDirty, boundingRectDirty; + + @SuppressWarnings("unchecked") + public ViewCluster(ArrayList views, ItemConfiguration config) { + this.views = (ArrayList) views.clone(); + this.config = config; + resetEdges(); + } + + void resetEdges() { + for (int i = 0; i < mCountX; i++) { + topEdge[i] = -1; + bottomEdge[i] = -1; + } + for (int i = 0; i < mCountY; i++) { + leftEdge[i] = -1; + rightEdge[i] = -1; + } + leftEdgeDirty = true; + rightEdgeDirty = true; + bottomEdgeDirty = true; + topEdgeDirty = true; + boundingRectDirty = true; + } + + void computeEdge(int which, int[] edge) { + int count = views.size(); + for (int i = 0; i < count; i++) { + CellAndSpan cs = config.map.get(views.get(i)); + switch (which) { + case LEFT: + int left = cs.x; + for (int j = cs.y; j < cs.y + cs.spanY; j++) { + if (left < edge[j] || edge[j] < 0) { + edge[j] = left; + } + } + break; + case RIGHT: + int right = cs.x + cs.spanX; + for (int j = cs.y; j < cs.y + cs.spanY; j++) { + if (right > edge[j]) { + edge[j] = right; + } + } + break; + case TOP: + int top = cs.y; + for (int j = cs.x; j < cs.x + cs.spanX; j++) { + if (top < edge[j] || edge[j] < 0) { + edge[j] = top; + } + } + break; + case BOTTOM: + int bottom = cs.y + cs.spanY; + for (int j = cs.x; j < cs.x + cs.spanX; j++) { + if (bottom > edge[j]) { + edge[j] = bottom; + } + } + break; + } + } + } + + boolean isViewTouchingEdge(View v, int whichEdge) { + CellAndSpan cs = config.map.get(v); + + int[] edge = getEdge(whichEdge); + + switch (whichEdge) { + case LEFT: + for (int i = cs.y; i < cs.y + cs.spanY; i++) { + if (edge[i] == cs.x + cs.spanX) { + return true; + } + } + break; + case RIGHT: + for (int i = cs.y; i < cs.y + cs.spanY; i++) { + if (edge[i] == cs.x) { + return true; + } + } + break; + case TOP: + for (int i = cs.x; i < cs.x + cs.spanX; i++) { + if (edge[i] == cs.y + cs.spanY) { + return true; + } + } + break; + case BOTTOM: + for (int i = cs.x; i < cs.x + cs.spanX; i++) { + if (edge[i] == cs.y) { + return true; + } + } + break; + } + return false; + } + + void shift(int whichEdge, int delta) { + for (View v: views) { + CellAndSpan c = config.map.get(v); + switch (whichEdge) { + case LEFT: + c.x -= delta; + break; + case RIGHT: + c.x += delta; + break; + case TOP: + c.y -= delta; + break; + case BOTTOM: + default: + c.y += delta; + break; + } + } + resetEdges(); + } + + public void addView(View v) { + views.add(v); + resetEdges(); + } + + public Rect getBoundingRect() { + if (boundingRectDirty) { + boolean first = true; + for (View v: views) { + CellAndSpan c = config.map.get(v); + if (first) { + boundingRect.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + first = false; + } else { + boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + } + } + } + return boundingRect; + } + + public int[] getEdge(int which) { + switch (which) { + case LEFT: + return getLeftEdge(); + case RIGHT: + return getRightEdge(); + case TOP: + return getTopEdge(); + case BOTTOM: + default: + return getBottomEdge(); + } + } + + public int[] getLeftEdge() { + if (leftEdgeDirty) { + computeEdge(LEFT, leftEdge); + } + return leftEdge; + } + + public int[] getRightEdge() { + if (rightEdgeDirty) { + computeEdge(RIGHT, rightEdge); + } + return rightEdge; + } + + public int[] getTopEdge() { + if (topEdgeDirty) { + computeEdge(TOP, topEdge); + } + return topEdge; + } + + public int[] getBottomEdge() { + if (bottomEdgeDirty) { + computeEdge(BOTTOM, bottomEdge); + } + return bottomEdge; + } + + PositionComparator comparator = new PositionComparator(); + class PositionComparator implements Comparator { + int whichEdge = 0; + public int compare(View left, View right) { + CellAndSpan l = config.map.get(left); + CellAndSpan r = config.map.get(right); + switch (whichEdge) { + case LEFT: + return (r.x + r.spanX) - (l.x + l.spanX); + case RIGHT: + return l.x - r.x; + case TOP: + return (r.y + r.spanY) - (l.y + l.spanY); + case BOTTOM: + default: + return l.y - r.y; + } + } + } + + public void sortConfigurationForEdgePush(int edge) { + comparator.whichEdge = edge; + Collections.sort(config.sortedViews, comparator); + } + } + + private boolean pushViewsToTempLocation(ArrayList views, Rect rectOccupiedByPotentialDrop, + int[] direction, View dragView, ItemConfiguration currentState) { + + ViewCluster cluster = new ViewCluster(views, currentState); + Rect clusterRect = cluster.getBoundingRect(); + int whichEdge; + int pushDistance; + boolean fail = false; + + // Determine the edge of the cluster that will be leading the push and how far + // the cluster must be shifted. + if (direction[0] < 0) { + whichEdge = ViewCluster.LEFT; + pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left; + } else if (direction[0] > 0) { + whichEdge = ViewCluster.RIGHT; + pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left; + } else if (direction[1] < 0) { + whichEdge = ViewCluster.TOP; + pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top; + } else { + whichEdge = ViewCluster.BOTTOM; + pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top; + } + + // Break early for invalid push distance. + if (pushDistance <= 0) { + return false; + } + + // Mark the occupied state as false for the group of views we want to move. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); + } + + // We save the current configuration -- if we fail to find a solution we will revert + // to the initial state. The process of finding a solution modifies the configuration + // in place, hence the need for revert in the failure case. + currentState.save(); + + // The pushing algorithm is simplified by considering the views in the order in which + // they would be pushed by the cluster. For example, if the cluster is leading with its + // left edge, we consider sort the views by their right edge, from right to left. + cluster.sortConfigurationForEdgePush(whichEdge); + + while (pushDistance > 0 && !fail) { + for (View v: currentState.sortedViews) { + // For each view that isn't in the cluster, we see if the leading edge of the + // cluster is contacting the edge of that view. If so, we add that view to the + // cluster. + if (!cluster.views.contains(v) && v != dragView) { + if (cluster.isViewTouchingEdge(v, whichEdge)) { + LayoutParams lp = (LayoutParams) v.getLayoutParams(); + if (!lp.canReorder) { + // The push solution includes the all apps button, this is not viable. + fail = true; + break; + } + cluster.addView(v); + CellAndSpan c = currentState.map.get(v); + + // Adding view to cluster, mark it as not occupied. + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); + } + } + } + pushDistance--; + + // The cluster has been completed, now we move the whole thing over in the appropriate + // direction. + cluster.shift(whichEdge, 1); + } + + boolean foundSolution = false; + clusterRect = cluster.getBoundingRect(); + + // Due to the nature of the algorithm, the only check required to verify a valid solution + // is to ensure that completed shifted cluster lies completely within the cell layout. + if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 && + clusterRect.bottom <= mCountY) { + foundSolution = true; + } else { + currentState.restore(); + } + + // In either case, we set the occupied array as marked for the location of the views + for (View v: cluster.views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); + } + + return foundSolution; + } + + private boolean addViewsToTempLocation(ArrayList views, Rect rectOccupiedByPotentialDrop, + int[] direction, View dragView, ItemConfiguration currentState) { + if (views.size() == 0) return true; + + boolean success = false; + Rect boundingRect = null; + // We construct a rect which represents the entire group of views passed in + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + if (boundingRect == null) { + boundingRect = new Rect(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + } else { + boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + } + } + + // Mark the occupied state as false for the group of views we want to move. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); + } + + boolean[][] blockOccupied = new boolean[boundingRect.width()][boundingRect.height()]; + int top = boundingRect.top; + int left = boundingRect.left; + // We mark more precisely which parts of the bounding rect are truly occupied, allowing + // for interlocking. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x - left, c.y - top, c.spanX, c.spanY, blockOccupied, true); + } + + markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true); + + findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(), + boundingRect.height(), direction, mTmpOccupied, blockOccupied, mTempLocation); + + // If we successfuly found a location by pushing the block of views, we commit it + if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { + int deltaX = mTempLocation[0] - boundingRect.left; + int deltaY = mTempLocation[1] - boundingRect.top; + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + c.x += deltaX; + c.y += deltaY; + } + success = true; + } + + // In either case, we set the occupied array as marked for the location of the views + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); + } + return success; + } + + private void markCellsForRect(Rect r, boolean[][] occupied, boolean value) { + markCellsForView(r.left, r.top, r.width(), r.height(), occupied, value); + } + + // This method tries to find a reordering solution which satisfies the push mechanic by trying + // to push items in each of the cardinal directions, in an order based on the direction vector + // passed. + private boolean attemptPushInDirection(ArrayList intersectingViews, Rect occupied, + int[] direction, View ignoreView, ItemConfiguration solution) { + if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) { + // If the direction vector has two non-zero components, we try pushing + // separately in each of the components. + int temp = direction[1]; + direction[1] = 0; + + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + direction[1] = temp; + temp = direction[0]; + direction[0] = 0; + + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Revert the direction + direction[0] = temp; + + // Now we try pushing in each component of the opposite direction + direction[0] *= -1; + direction[1] *= -1; + temp = direction[1]; + direction[1] = 0; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + + direction[1] = temp; + temp = direction[0]; + direction[0] = 0; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // revert the direction + direction[0] = temp; + direction[0] *= -1; + direction[1] *= -1; + + } else { + // If the direction vector has a single non-zero component, we push first in the + // direction of the vector + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Then we try the opposite direction + direction[0] *= -1; + direction[1] *= -1; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Switch the direction back + direction[0] *= -1; + direction[1] *= -1; + + // If we have failed to find a push solution with the above, then we try + // to find a solution by pushing along the perpendicular axis. + + // Swap the components + int temp = direction[1]; + direction[1] = direction[0]; + direction[0] = temp; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + + // Then we try the opposite direction + direction[0] *= -1; + direction[1] *= -1; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Switch the direction back + direction[0] *= -1; + direction[1] *= -1; + + // Swap the components back + temp = direction[1]; + direction[1] = direction[0]; + direction[0] = temp; + } + return false; + } + + private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction, + View ignoreView, ItemConfiguration solution) { + // Return early if get invalid cell positions + if (cellX < 0 || cellY < 0) return false; + + mIntersectingViews.clear(); + mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY); + + // Mark the desired location of the view currently being dragged. + if (ignoreView != null) { + CellAndSpan c = solution.map.get(ignoreView); + if (c != null) { + c.x = cellX; + c.y = cellY; + } + } + Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY); + Rect r1 = new Rect(); + for (View child: solution.map.keySet()) { + if (child == ignoreView) continue; + CellAndSpan c = solution.map.get(child); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + r1.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY); + if (Rect.intersects(r0, r1)) { + if (!lp.canReorder) { + return false; + } + mIntersectingViews.add(child); + } + } + + // First we try to find a solution which respects the push mechanic. That is, + // we try to find a solution such that no displaced item travels through another item + // without also displacing that item. + if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView, + solution)) { + return true; + } + + // Next we try moving the views as a block, but without requiring the push mechanic. + if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView, + solution)) { + return true; + } + + // Ok, they couldn't move as a block, let's move them individually + for (View v : mIntersectingViews) { + if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) { + return false; + } + } + return true; + } + + /* + * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between + * the provided point and the provided cell + */ + private void computeDirectionVector(float deltaX, float deltaY, int[] result) { + double angle = Math.atan(((float) deltaY) / deltaX); + + result[0] = 0; + result[1] = 0; + if (Math.abs(Math.cos(angle)) > 0.5f) { + result[0] = (int) Math.signum(deltaX); + } + if (Math.abs(Math.sin(angle)) > 0.5f) { + result[1] = (int) Math.signum(deltaY); + } + } + + private void copyOccupiedArray(boolean[][] occupied) { + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + occupied[i][j] = mOccupied[i][j]; + } + } + } + + ItemConfiguration simpleSwap(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. + copyCurrentStateToSolution(solution, false); + // Copy the current occupied array into the temporary occupied array. This array will be + // manipulated as necessary to find a solution. + copyOccupiedArray(mTmpOccupied); + + // We find the nearest cell into which we would place the dragged item, assuming there's + // nothing in its way. + int result[] = new int[2]; + result = findNearestArea(pixelX, pixelY, spanX, spanY, result); + + boolean success = false; + // First we try the exact nearest position of the item being dragged, + // we will then want to try to move this around to other neighbouring positions + success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView, + solution); + + if (!success) { + // We try shrinking the widget down to size in an alternating pattern, shrink 1 in + // x, then 1 in y etc. + if (spanX > minSpanX && (minSpanY == spanY || decX)) { + return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY, direction, + dragView, false, solution); + } else if (spanY > minSpanY) { + return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1, direction, + dragView, true, solution); + } + solution.isSolution = false; + } else { + solution.isSolution = true; + solution.dragViewX = result[0]; + solution.dragViewY = result[1]; + solution.dragViewSpanX = spanX; + solution.dragViewSpanY = spanY; + } + return solution; + } + + private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) { + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + CellAndSpan c; + if (temp) { + c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan); + } else { + c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan); + } + solution.add(child, c); + } + } + + private void copySolutionToTempState(ItemConfiguration solution, View dragView) { + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + mTmpOccupied[i][j] = false; + } + } + + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (child == dragView) continue; + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + CellAndSpan c = solution.map.get(child); + if (c != null) { + lp.tmpCellX = c.x; + lp.tmpCellY = c.y; + lp.cellHSpan = c.spanX; + lp.cellVSpan = c.spanY; + markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); + } + } + markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX, + solution.dragViewSpanY, mTmpOccupied, true); + } + + private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean + commitDragView) { + + boolean[][] occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied; + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + occupied[i][j] = false; + } + } + + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (child == dragView) continue; + CellAndSpan c = solution.map.get(child); + if (c != null) { + animateChildToPosition(child, c.x, c.y, REORDER_ANIMATION_DURATION, 0, + DESTRUCTIVE_REORDER, false); + markCellsForView(c.x, c.y, c.spanX, c.spanY, occupied, true); + } + } + if (commitDragView) { + markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX, + solution.dragViewSpanY, occupied, true); + } + } + + // This method starts or changes the reorder hint animations + private void beginOrAdjustHintAnimations(ItemConfiguration solution, View dragView, int delay) { + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (child == dragView) continue; + CellAndSpan c = solution.map.get(child); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (c != null) { + ReorderHintAnimation rha = new ReorderHintAnimation(child, lp.cellX, lp.cellY, + c.x, c.y, c.spanX, c.spanY); + rha.animate(); + } + } + } + + // Class which represents the reorder hint animations. These animations show that an item is + // in a temporary state, and hint at where the item will return to. + class ReorderHintAnimation { + View child; + float finalDeltaX; + float finalDeltaY; + float initDeltaX; + float initDeltaY; + float finalScale; + float initScale; + private static final int DURATION = 300; + Animator a; + + public ReorderHintAnimation(View child, int cellX0, int cellY0, int cellX1, int cellY1, + int spanX, int spanY) { + regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint); + final int x0 = mTmpPoint[0]; + final int y0 = mTmpPoint[1]; + regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint); + final int x1 = mTmpPoint[0]; + final int y1 = mTmpPoint[1]; + final int dX = x1 - x0; + final int dY = y1 - y0; + finalDeltaX = 0; + finalDeltaY = 0; + if (dX == dY && dX == 0) { + } else { + if (dY == 0) { + finalDeltaX = - Math.signum(dX) * mReorderHintAnimationMagnitude; + } else if (dX == 0) { + finalDeltaY = - Math.signum(dY) * mReorderHintAnimationMagnitude; + } else { + double angle = Math.atan( (float) (dY) / dX); + finalDeltaX = (int) (- Math.signum(dX) * + Math.abs(Math.cos(angle) * mReorderHintAnimationMagnitude)); + finalDeltaY = (int) (- Math.signum(dY) * + Math.abs(Math.sin(angle) * mReorderHintAnimationMagnitude)); + } + } + initDeltaX = child.getTranslationX(); + initDeltaY = child.getTranslationY(); + finalScale = getChildrenScale() - 4.0f / child.getWidth(); + initScale = child.getScaleX(); + this.child = child; + } + + void animate() { + if (mShakeAnimators.containsKey(child)) { + ReorderHintAnimation oldAnimation = mShakeAnimators.get(child); + oldAnimation.cancel(); + mShakeAnimators.remove(child); + if (finalDeltaX == 0 && finalDeltaY == 0) { + completeAnimationImmediately(); + return; + } + } + if (finalDeltaX == 0 && finalDeltaY == 0) { + return; + } + ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f); + a = va; + va.setRepeatMode(ValueAnimator.REVERSE); + va.setRepeatCount(ValueAnimator.INFINITE); + va.setDuration(DURATION); + va.setStartDelay((int) (Math.random() * 60)); + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float r = ((Float) animation.getAnimatedValue()).floatValue(); + float x = r * finalDeltaX + (1 - r) * initDeltaX; + float y = r * finalDeltaY + (1 - r) * initDeltaY; + child.setTranslationX(x); + child.setTranslationY(y); + float s = r * finalScale + (1 - r) * initScale; + child.setScaleX(s); + child.setScaleY(s); + } + }); + va.addListener(new AnimatorListenerAdapter() { + public void onAnimationRepeat(Animator animation) { + // We make sure to end only after a full period + initDeltaX = 0; + initDeltaY = 0; + initScale = getChildrenScale(); + } + }); + mShakeAnimators.put(child, this); + va.start(); + } + + private void cancel() { + if (a != null) { + a.cancel(); + } + } + + private void completeAnimationImmediately() { + if (a != null) { + a.cancel(); + } + + AnimatorSet s = LauncherAnimUtils.createAnimatorSet(); + a = s; + s.playTogether( + LauncherAnimUtils.ofFloat(child, "scaleX", getChildrenScale()), + LauncherAnimUtils.ofFloat(child, "scaleY", getChildrenScale()), + LauncherAnimUtils.ofFloat(child, "translationX", 0f), + LauncherAnimUtils.ofFloat(child, "translationY", 0f) + ); + s.setDuration(REORDER_ANIMATION_DURATION); + s.setInterpolator(new android.view.animation.DecelerateInterpolator(1.5f)); + s.start(); + } + } + + private void completeAndClearReorderHintAnimations() { + for (ReorderHintAnimation a: mShakeAnimators.values()) { + a.completeAnimationImmediately(); + } + mShakeAnimators.clear(); + } + + private void commitTempPlacement() { + for (int i = 0; i < mCountX; i++) { + for (int j = 0; j < mCountY; j++) { + mOccupied[i][j] = mTmpOccupied[i][j]; + } + } + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo info = (ItemInfo) child.getTag(); + // We do a null check here because the item info can be null in the case of the + // AllApps button in the hotseat. + if (info != null) { + if (info.cellX != lp.tmpCellX || info.cellY != lp.tmpCellY || + info.spanX != lp.cellHSpan || info.spanY != lp.cellVSpan) { + info.requiresDbUpdate = true; + } + info.cellX = lp.cellX = lp.tmpCellX; + info.cellY = lp.cellY = lp.tmpCellY; + info.spanX = lp.cellHSpan; + info.spanY = lp.cellVSpan; + } + } + mLauncher.getWorkspace().updateItemLocationsInDatabase(this); + } + + public void setUseTempCoords(boolean useTempCoords) { + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams(); + lp.useTmpCoords = useTempCoords; + } + } + + 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, + resultSpan); + if (result[0] >= 0 && result[1] >= 0) { + copyCurrentStateToSolution(solution, false); + solution.dragViewX = result[0]; + solution.dragViewY = result[1]; + solution.dragViewSpanX = resultSpan[0]; + solution.dragViewSpanY = resultSpan[1]; + solution.isSolution = true; + } else { + solution.isSolution = false; + } + return solution; + } + + public void prepareChildForDrag(View child) { + markCellsAsUnoccupiedForView(child); + } + + /* This seems like it should be obvious and straight-forward, but when the direction vector + needs to match with the notion of the dragView pushing other views, we have to employ + a slightly more subtle notion of the direction vector. The question is what two points is + the vector between? The center of the dragView and its desired destination? Not quite, as + this doesn't necessarily coincide with the interaction of the dragView and items occupying + those cells. Instead we use some heuristics to often lock the vector to up, down, left + or right, which helps make pushing feel right. + */ + private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX, + int spanY, View dragView, int[] resultDirection) { + int[] targetDestination = new int[2]; + + findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination); + Rect dragRect = new Rect(); + regionToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect); + dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY()); + + Rect dropRegionRect = new Rect(); + getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY, + dragView, dropRegionRect, mIntersectingViews); + + int dropRegionSpanX = dropRegionRect.width(); + int dropRegionSpanY = dropRegionRect.height(); + + regionToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(), + dropRegionRect.height(), dropRegionRect); + + int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX; + int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY; + + if (dropRegionSpanX == mCountX || spanX == mCountX) { + deltaX = 0; + } + if (dropRegionSpanY == mCountY || spanY == mCountY) { + deltaY = 0; + } + + if (deltaX == 0 && deltaY == 0) { + // No idea what to do, give a random direction. + resultDirection[0] = 1; + resultDirection[1] = 0; + } else { + computeDirectionVector(deltaX, deltaY, resultDirection); + } + } + + // For a given cell and span, fetch the set of views intersecting the region. + private void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY, + View dragView, Rect boundingRect, ArrayList intersectingViews) { + if (boundingRect != null) { + boundingRect.set(cellX, cellY, cellX + spanX, cellY + spanY); + } + intersectingViews.clear(); + Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY); + Rect r1 = new Rect(); + final int count = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < count; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + if (child == dragView) continue; + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + r1.set(lp.cellX, lp.cellY, lp.cellX + lp.cellHSpan, lp.cellY + lp.cellVSpan); + if (Rect.intersects(r0, r1)) { + mIntersectingViews.add(child); + if (boundingRect != null) { + boundingRect.union(r1); + } + } + } + } + + boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY, + View dragView, int[] result) { + result = findNearestArea(pixelX, pixelY, spanX, spanY, result); + getViewsIntersectingRegion(result[0], result[1], spanX, spanY, dragView, null, + mIntersectingViews); + return !mIntersectingViews.isEmpty(); + } + + void revertTempState() { + if (!isItemPlacementDirty() || DESTRUCTIVE_REORDER) return; + final int count = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < count; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY) { + lp.tmpCellX = lp.cellX; + lp.tmpCellY = lp.cellY; + animateChildToPosition(child, lp.cellX, lp.cellY, REORDER_ANIMATION_DURATION, + 0, false, false); + } + } + completeAndClearReorderHintAnimations(); + setItemPlacementDirty(false); + } + + boolean createAreaForResize(int cellX, int cellY, int spanX, int spanY, + View dragView, int[] direction, boolean commit) { + int[] pixelXY = new int[2]; + regionToCenterPoint(cellX, cellY, spanX, spanY, pixelXY); + + // First we determine if things have moved enough to cause a different layout + ItemConfiguration swapSolution = simpleSwap(pixelXY[0], pixelXY[1], spanX, spanY, + spanX, spanY, direction, dragView, true, new ItemConfiguration()); + + setUseTempCoords(true); + if (swapSolution != null && swapSolution.isSolution) { + // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother + // committing anything or animating anything as we just want to determine if a solution + // exists + copySolutionToTempState(swapSolution, dragView); + setItemPlacementDirty(true); + animateItemsToSolution(swapSolution, dragView, commit); + + if (commit) { + commitTempPlacement(); + completeAndClearReorderHintAnimations(); + setItemPlacementDirty(false); + } else { + beginOrAdjustHintAnimations(swapSolution, dragView, + REORDER_ANIMATION_DURATION); + } + mShortcutsAndWidgets.requestLayout(); + } + return swapSolution.isSolution; + } + + int[] createArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, + View dragView, int[] result, int resultSpan[], int mode) { + // First we determine if things have moved enough to cause a different layout + result = findNearestArea(pixelX, pixelY, spanX, spanY, result); + + if (resultSpan == null) { + resultSpan = new int[2]; + } + + // When we are checking drop validity or actually dropping, we don't recompute the + // direction vector, since we want the solution to match the preview, and it's possible + // that the exact position of the item has changed to result in a new reordering outcome. + if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP) + && mPreviousReorderDirection[0] != INVALID_DIRECTION) { + mDirectionVector[0] = mPreviousReorderDirection[0]; + mDirectionVector[1] = mPreviousReorderDirection[1]; + // We reset this vector after drop + if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { + mPreviousReorderDirection[0] = INVALID_DIRECTION; + mPreviousReorderDirection[1] = INVALID_DIRECTION; + } + } else { + getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector); + mPreviousReorderDirection[0] = mDirectionVector[0]; + mPreviousReorderDirection[1] = mDirectionVector[1]; + } + + ItemConfiguration swapSolution = simpleSwap(pixelX, pixelY, minSpanX, minSpanY, + spanX, spanY, mDirectionVector, dragView, true, new ItemConfiguration()); + + // We attempt the approach which doesn't shuffle views at all + ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX, + minSpanY, spanX, spanY, dragView, new ItemConfiguration()); + + ItemConfiguration finalSolution = null; + if (swapSolution.isSolution && swapSolution.area() >= noShuffleSolution.area()) { + finalSolution = swapSolution; + } else if (noShuffleSolution.isSolution) { + finalSolution = noShuffleSolution; + } + + boolean foundSolution = true; + if (!DESTRUCTIVE_REORDER) { + setUseTempCoords(true); + } + + if (finalSolution != null) { + result[0] = finalSolution.dragViewX; + result[1] = finalSolution.dragViewY; + resultSpan[0] = finalSolution.dragViewSpanX; + resultSpan[1] = finalSolution.dragViewSpanY; + + // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother + // committing anything or animating anything as we just want to determine if a solution + // exists + if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { + if (!DESTRUCTIVE_REORDER) { + copySolutionToTempState(finalSolution, dragView); + } + setItemPlacementDirty(true); + animateItemsToSolution(finalSolution, dragView, mode == MODE_ON_DROP); + + if (!DESTRUCTIVE_REORDER && + (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) { + commitTempPlacement(); + completeAndClearReorderHintAnimations(); + setItemPlacementDirty(false); + } else { + beginOrAdjustHintAnimations(finalSolution, dragView, + REORDER_ANIMATION_DURATION); + } + } + } else { + foundSolution = false; + result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1; + } + + if ((mode == MODE_ON_DROP || !foundSolution) && !DESTRUCTIVE_REORDER) { + setUseTempCoords(false); + } + + mShortcutsAndWidgets.requestLayout(); + return result; + } + + void setItemPlacementDirty(boolean dirty) { + mItemPlacementDirty = dirty; + } + boolean isItemPlacementDirty() { + return mItemPlacementDirty; + } + + private class ItemConfiguration { + HashMap map = new HashMap(); + private HashMap savedMap = new HashMap(); + ArrayList sortedViews = new ArrayList(); + boolean isSolution = false; + int dragViewX, dragViewY, dragViewSpanX, dragViewSpanY; + + void save() { + // Copy current state into savedMap + for (View v: map.keySet()) { + map.get(v).copy(savedMap.get(v)); + } + } + + void restore() { + // Restore current state from savedMap + for (View v: savedMap.keySet()) { + savedMap.get(v).copy(map.get(v)); + } + } + + void add(View v, CellAndSpan cs) { + map.put(v, cs); + savedMap.put(v, new CellAndSpan()); + sortedViews.add(v); + } + + int area() { + return dragViewSpanX * dragViewSpanY; + } + } + + private class CellAndSpan { + int x, y; + int spanX, spanY; + + public CellAndSpan() { + } + + public void copy(CellAndSpan copy) { + copy.x = x; + copy.y = y; + copy.spanX = spanX; + copy.spanY = spanY; + } + + public CellAndSpan(int x, int y, int spanX, int spanY) { + this.x = x; + this.y = y; + this.spanX = spanX; + this.spanY = spanY; + } + + public String toString() { + return "(" + x + ", " + y + ": " + spanX + ", " + spanY + ")"; + } + + } + + /** + * 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. + * + * @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[] findNearestArea( + int pixelX, int pixelY, int spanX, int spanY, int[] result) { + return findNearestArea(pixelX, pixelY, spanX, spanY, null, false, result); + } + + boolean existsEmptyCell() { + return findCellForSpan(null, 1, 1); + } + + /** + * Finds the upper-left coordinate of the first rectangle in the grid that can + * hold a cell of the specified dimensions. If intersectX and intersectY are not -1, + * then this method will only return coordinates for rectangles that contain the cell + * (intersectX, intersectY) + * + * @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. + * + * @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); + + 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)); + } + + 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; + } + } + } + if (cellXY != null) { + cellXY[0] = x; + cellXY[1] = y; + } + foundCell = true; + break; + } + } + if (intersectX == -1 && intersectY == -1) { + 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; + } + + /** + * A drag event has begun over this layout. + * It may have begun over this layout (in which case onDragChild is called first), + * or it may have begun on another layout. + */ + void onDragEnter() { + mDragEnforcer.onDragEnter(); + mDragging = true; + } + + /** + * 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. + if (mDragging) { + mDragging = false; + } + + // Invalidate the drag data + mDragCell[0] = mDragCell[1] = -1; + mDragOutlineAnims[mDragOutlineCurrent].animateOut(); + mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length; + revertTempState(); + setIsDragOverlapping(false); + } + + /** + * Mark a child as having been dropped. + * At the beginning of the drag operation, the child may have been on another + * screen, but it is re-parented before this method is called. + * + * @param child The child that is being dropped + */ + void onDropChild(View child) { + if (child != null) { + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.dropped = true; + child.requestLayout(); + } + } + + /** + * Computes a bounding rectangle for a range of cells + * + * @param cellX X coordinate of upper left corner expressed as a cell position + * @param cellY Y coordinate of upper left corner expressed as a cell position + * @param cellHSpan Width in cells + * @param cellVSpan Height in cells + * @param resultRect Rect into which to put the results + */ + public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect) { + final int cellWidth = mCellWidth; + final int cellHeight = mCellHeight; + final int widthGap = mWidthGap; + final int heightGap = mHeightGap; + + final int hStartPadding = getPaddingLeft(); + final int vStartPadding = getPaddingTop(); + + int width = cellHSpan * cellWidth + ((cellHSpan - 1) * widthGap); + int height = cellVSpan * cellHeight + ((cellVSpan - 1) * heightGap); + + int x = hStartPadding + cellX * (cellWidth + widthGap); + int y = vStartPadding + cellY * (cellHeight + heightGap); + + resultRect.set(x, y, x + width, y + height); + } + + /** + * Computes the required horizontal and vertical cell spans to always + * fit the given rectangle. + * + * @param width Width in pixels + * @param height Height in pixels + * @param result An array of length 2 in which to store the result (may be null). + */ + public int[] rectToCell(int width, int height, int[] result) { + return rectToCell(getResources(), width, height, result); + } + + public static int[] rectToCell(Resources resources, int width, int height, int[] result) { + // Always assume we're working with the smallest span to make sure we + // reserve enough space in both orientations. + int actualWidth = resources.getDimensionPixelSize(R.dimen.workspace_cell_width); + int actualHeight = resources.getDimensionPixelSize(R.dimen.workspace_cell_height); + int smallerSize = Math.min(actualWidth, actualHeight); + + // Always round up to next largest cell + int spanX = (int) Math.ceil(width / (float) smallerSize); + int spanY = (int) Math.ceil(height / (float) smallerSize); + + if (result == null) { + return new int[] { spanX, spanY }; + } + result[0] = spanX; + result[1] = spanY; + 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 + */ + public void calculateSpans(ItemInfo info) { + final int minWidth; + final int minHeight; + + if (info instanceof LauncherAppWidgetInfo) { + minWidth = ((LauncherAppWidgetInfo) info).minWidth; + minHeight = ((LauncherAppWidgetInfo) info).minHeight; + } else if (info instanceof PendingAddWidgetInfo) { + minWidth = ((PendingAddWidgetInfo) info).minWidth; + minHeight = ((PendingAddWidgetInfo) info).minHeight; + } else { + // It's not a widget, so it must be 1x1 + info.spanX = info.spanY = 1; + return; + } + int[] spans = rectToCell(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++) { + mOccupied[x][y] = false; + } + } + } + + 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); + } + + 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); + } + + private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean[][] occupied, + boolean value) { + if (cellX < 0 || cellY < 0) return; + for (int x = cellX; x < cellX + spanX && x < mCountX; x++) { + for (int y = cellY; y < cellY + spanY && y < mCountY; y++) { + occupied[x][y] = value; + } + } + } + + public int getDesiredWidth() { + return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth) + + (Math.max((mCountX - 1), 0) * mWidthGap); + } + + public int getDesiredHeight() { + return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) + + (Math.max((mCountY - 1), 0) * mHeightGap); + } + + public boolean isOccupied(int x, int y) { + if (x < mCountX && y < mCountY) { + return mOccupied[x][y]; + } else { + throw new RuntimeException("Position exceeds the bound of this CellLayout"); + } + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new CellLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof CellLayout.LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + 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. + */ + @ViewDebug.ExportedProperty + public int cellX; + + /** + * Vertical location of the item in the grid. + */ + @ViewDebug.ExportedProperty + public int cellY; + + /** + * Temporary horizontal location of the item in the grid during reorder + */ + public int tmpCellX; + + /** + * Temporary vertical location of the item in the grid during reorder + */ + public int tmpCellY; + + /** + * Indicates that the temporary coordinates should be used to layout the items + */ + public boolean useTmpCoords; + + /** + * 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; + + /** + * Indicates whether the item will set its x, y, width and height parameters freely, + * or whether these will be computed based on cellX, cellY, cellHSpan and cellVSpan. + */ + public boolean isLockedToGrid = true; + + /** + * Indicates whether this item can be reordered. Always true except in the case of the + * the AllApps button. + */ + public boolean canReorder = true; + + // X coordinate of the view in the layout. + @ViewDebug.ExportedProperty + int x; + // Y coordinate of the view in the layout. + @ViewDebug.ExportedProperty + int y; + + boolean dropped; + + 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(int cellWidth, int cellHeight, int widthGap, int heightGap, + boolean invertHorizontally, int colCount) { + if (isLockedToGrid) { + final int myCellHSpan = cellHSpan; + final int myCellVSpan = cellVSpan; + int myCellX = useTmpCoords ? tmpCellX : cellX; + int myCellY = useTmpCoords ? tmpCellY : cellY; + + if (invertHorizontally) { + myCellX = colCount - myCellX - cellHSpan; + } + + width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - + leftMargin - rightMargin; + height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - + topMargin - bottomMargin; + x = (int) (myCellX * (cellWidth + widthGap) + leftMargin); + y = (int) (myCellY * (cellHeight + heightGap) + topMargin); + } + } + + public String toString() { + return "(" + this.cellX + ", " + this.cellY + ")"; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getWidth() { + return width; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getHeight() { + return height; + } + + public void setX(int x) { + this.x = x; + } + + public int getX() { + return x; + } + + public void setY(int y) { + this.y = y; + } + + public int getY() { + return y; + } + } + + // This class stores info for two purposes: + // 1. When dragging items (mDragInfo in Workspace), we store the View, its cellX & cellY, + // its spanX, spanY, and the screen it is on + // 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 { + View cell; + int cellX = -1; + int cellY = -1; + int spanX; + int spanY; + int screen; + long container; + + @Override + public String toString() { + return "Cell[view=" + (cell == null ? "null" : cell.getClass()) + + ", x=" + cellX + ", y=" + cellY + "]"; + } + } + + public boolean lastDownOnOccupiedCell() { + return mLastDownOnOccupiedCell; + } +} diff --git a/app/src/main/java/com/android/launcher2/CheckLongPressHelper.java b/app/src/main/java/com/android/launcher2/CheckLongPressHelper.java new file mode 100644 index 0000000..5c3752a --- /dev/null +++ b/app/src/main/java/com/android/launcher2/CheckLongPressHelper.java @@ -0,0 +1,62 @@ +/* + * 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.launcher2; + +import android.view.View; + +public class CheckLongPressHelper { + private View mView; + private boolean mHasPerformedLongPress; + private CheckForLongPress mPendingCheckForLongPress; + + class CheckForLongPress implements Runnable { + public void run() { + if ((mView.getParent() != null) && mView.hasWindowFocus() + && !mHasPerformedLongPress) { + if (mView.performLongClick()) { + mView.setPressed(false); + mHasPerformedLongPress = true; + } + } + } + } + + public CheckLongPressHelper(View v) { + mView = v; + } + + public void postCheckForLongPress() { + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + mView.postDelayed(mPendingCheckForLongPress, LauncherApplication.getLongPressTimeout()); + } + + public void cancelLongPress() { + mHasPerformedLongPress = false; + if (mPendingCheckForLongPress != null) { + mView.removeCallbacks(mPendingCheckForLongPress); + mPendingCheckForLongPress = null; + } + } + + public boolean hasPerformedLongPress() { + return mHasPerformedLongPress; + } +} diff --git a/app/src/main/java/com/android/launcher2/Cling.java b/app/src/main/java/com/android/launcher2/Cling.java new file mode 100644 index 0000000..971d9ff --- /dev/null +++ b/app/src/main/java/com/android/launcher2/Cling.java @@ -0,0 +1,271 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.FocusFinder; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.launcher.R; + +public class Cling extends FrameLayout { + + static final String WORKSPACE_CLING_DISMISSED_KEY = "cling.workspace.dismissed"; + static final String ALLAPPS_CLING_DISMISSED_KEY = "cling.allapps.dismissed"; + static final String FOLDER_CLING_DISMISSED_KEY = "cling.folder.dismissed"; + + private static String WORKSPACE_PORTRAIT = "workspace_portrait"; + private static String WORKSPACE_LANDSCAPE = "workspace_landscape"; + private static String WORKSPACE_LARGE = "workspace_large"; + private static String WORKSPACE_CUSTOM = "workspace_custom"; + + private static String ALLAPPS_PORTRAIT = "all_apps_portrait"; + private static String ALLAPPS_LANDSCAPE = "all_apps_landscape"; + private static String ALLAPPS_LARGE = "all_apps_large"; + + private static String FOLDER_PORTRAIT = "folder_portrait"; + private static String FOLDER_LANDSCAPE = "folder_landscape"; + private static String FOLDER_LARGE = "folder_large"; + + private Launcher mLauncher; + private boolean mIsInitialized; + private String mDrawIdentifier; + private Drawable mBackground; + private Drawable mPunchThroughGraphic; + private Drawable mHandTouchGraphic; + private int mPunchThroughGraphicCenterRadius; + private int mAppIconSize; + private int mButtonBarHeight; + private float mRevealRadius; + private int[] mPositionData; + + private Paint mErasePaint; + + public Cling(Context context) { + this(context, null, 0); + } + + public Cling(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Cling(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Cling, defStyle, 0); + mDrawIdentifier = a.getString(R.styleable.Cling_drawIdentifier); + a.recycle(); + + setClickable(true); + } + + void init(Launcher l, int[] positionData) { + if (!mIsInitialized) { + mLauncher = l; + mPositionData = positionData; + + Resources r = getContext().getResources(); + + mPunchThroughGraphic = r.getDrawable(R.drawable.cling); + mPunchThroughGraphicCenterRadius = + r.getDimensionPixelSize(R.dimen.clingPunchThroughGraphicCenterRadius); + mAppIconSize = r.getDimensionPixelSize(R.dimen.app_icon_size); + mRevealRadius = r.getDimensionPixelSize(R.dimen.reveal_radius) * 1f; + mButtonBarHeight = r.getDimensionPixelSize(R.dimen.button_bar_height); + + mErasePaint = new Paint(); + mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); + mErasePaint.setColor(0xFFFFFF); + mErasePaint.setAlpha(0); + + mIsInitialized = true; + } + } + + void cleanup() { + mBackground = null; + mPunchThroughGraphic = null; + mHandTouchGraphic = null; + mIsInitialized = false; + } + + public String getDrawIdentifier() { + return mDrawIdentifier; + } + + private int[] getPunchThroughPositions() { + if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT)) { + return new int[]{getMeasuredWidth() / 2, getMeasuredHeight() - (mButtonBarHeight / 2)}; + } else if (mDrawIdentifier.equals(WORKSPACE_LANDSCAPE)) { + return new int[]{getMeasuredWidth() - (mButtonBarHeight / 2), getMeasuredHeight() / 2}; + } else if (mDrawIdentifier.equals(WORKSPACE_LARGE)) { + final float scale = LauncherApplication.getScreenDensity(); + final int cornerXOffset = (int) (scale * 15); + final int cornerYOffset = (int) (scale * 10); + return new int[]{getMeasuredWidth() - cornerXOffset, cornerYOffset}; + } else if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) || + mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) || + mDrawIdentifier.equals(ALLAPPS_LARGE)) { + return mPositionData; + } + return new int[]{-1, -1}; + } + + @Override + public View focusSearch(int direction) { + return this.focusSearch(this, direction); + } + + @Override + public View focusSearch(View focused, int direction) { + return FocusFinder.getInstance().findNextFocus(this, focused, direction); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + return (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) + || mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) + || mDrawIdentifier.equals(WORKSPACE_LARGE) + || mDrawIdentifier.equals(ALLAPPS_PORTRAIT) + || mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) + || mDrawIdentifier.equals(ALLAPPS_LARGE) + || mDrawIdentifier.equals(WORKSPACE_CUSTOM)); + } + + @Override + public boolean onTouchEvent(android.view.MotionEvent event) { + if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || + mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || + mDrawIdentifier.equals(WORKSPACE_LARGE) || + mDrawIdentifier.equals(ALLAPPS_PORTRAIT) || + mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) || + mDrawIdentifier.equals(ALLAPPS_LARGE)) { + + int[] positions = getPunchThroughPositions(); + for (int i = 0; i < positions.length; i += 2) { + double diff = Math.sqrt(Math.pow(event.getX() - positions[i], 2) + + Math.pow(event.getY() - positions[i + 1], 2)); + if (diff < mRevealRadius) { + return false; + } + } + } else if (mDrawIdentifier.equals(FOLDER_PORTRAIT) || + mDrawIdentifier.equals(FOLDER_LANDSCAPE) || + mDrawIdentifier.equals(FOLDER_LARGE)) { + Folder f = mLauncher.getWorkspace().getOpenFolder(); + if (f != null) { + Rect r = new Rect(); + f.getHitRect(r); + if (r.contains((int) event.getX(), (int) event.getY())) { + return false; + } + } + } + return true; + }; + + @Override + protected void dispatchDraw(Canvas canvas) { + if (mIsInitialized) { + DisplayMetrics metrics = new DisplayMetrics(); + mLauncher.getWindowManager().getDefaultDisplay().getMetrics(metrics); + + // Initialize the draw buffer (to allow punching through) + Bitmap b = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + + // Draw the background + if (mBackground == null) { + if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || + mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || + mDrawIdentifier.equals(WORKSPACE_LARGE)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling1); + } else if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) || + mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) || + mDrawIdentifier.equals(ALLAPPS_LARGE)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling2); + } else if (mDrawIdentifier.equals(FOLDER_PORTRAIT) || + mDrawIdentifier.equals(FOLDER_LANDSCAPE)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling3); + } else if (mDrawIdentifier.equals(FOLDER_LARGE)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling4); + } else if (mDrawIdentifier.equals(WORKSPACE_CUSTOM)) { + mBackground = getResources().getDrawable(R.drawable.bg_cling5); + } + } + if (mBackground != null) { + mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); + mBackground.draw(c); + } else { + c.drawColor(0x99000000); + } + + int cx = -1; + int cy = -1; + float scale = mRevealRadius / mPunchThroughGraphicCenterRadius; + int dw = (int) (scale * mPunchThroughGraphic.getIntrinsicWidth()); + int dh = (int) (scale * mPunchThroughGraphic.getIntrinsicHeight()); + + // Determine where to draw the punch through graphic + int[] positions = getPunchThroughPositions(); + for (int i = 0; i < positions.length; i += 2) { + cx = positions[i]; + cy = positions[i + 1]; + if (cx > -1 && cy > -1) { + c.drawCircle(cx, cy, mRevealRadius, mErasePaint); + mPunchThroughGraphic.setBounds(cx - dw/2, cy - dh/2, cx + dw/2, cy + dh/2); + mPunchThroughGraphic.draw(c); + } + } + + // Draw the hand graphic in All Apps + if (mDrawIdentifier.equals(ALLAPPS_PORTRAIT) || + mDrawIdentifier.equals(ALLAPPS_LANDSCAPE) || + mDrawIdentifier.equals(ALLAPPS_LARGE)) { + if (mHandTouchGraphic == null) { + mHandTouchGraphic = getResources().getDrawable(R.drawable.hand); + } + int offset = mAppIconSize / 4; + mHandTouchGraphic.setBounds(cx + offset, cy + offset, + cx + mHandTouchGraphic.getIntrinsicWidth() + offset, + cy + mHandTouchGraphic.getIntrinsicHeight() + offset); + mHandTouchGraphic.draw(c); + } + + canvas.drawBitmap(b, 0, 0, null); + c.setBitmap(null); + b = null; + } + + // Draw the rest of the cling + super.dispatchDraw(canvas); + }; +} diff --git a/app/src/main/java/com/android/launcher2/DeferredHandler.java b/app/src/main/java/com/android/launcher2/DeferredHandler.java new file mode 100644 index 0000000..cee27df --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DeferredHandler.java @@ -0,0 +1,146 @@ +/* + * 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.launcher2; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue; +import android.util.Pair; +import java.util.LinkedList; +import java.util.ListIterator; + +/** + * Queue of things to run on a looper thread. Items posted with {@link #post} will not + * be actually enqued on the handler until after the last one has run, to keep from + * starving the thread. + * + * This class is fifo. + */ +public class DeferredHandler { + private LinkedList> mQueue = new LinkedList>(); + private MessageQueue mMessageQueue = Looper.myQueue(); + private Impl mHandler = new Impl(); + + private 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.run(); + synchronized (mQueue) { + scheduleNextLocked(); + } + } + + public boolean queueIdle() { + handleMessage(null); + return false; + } + } + + private class IdleRunnable implements Runnable { + Runnable mRunnable; + + IdleRunnable(Runnable r) { + mRunnable = r; + } + + public void run() { + mRunnable.run(); + } + } + + public 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)); + if (mQueue.size() == 1) { + scheduleNextLocked(); + } + } + } + + /** 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); + } + + 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() { + synchronized (mQueue) { + mQueue.clear(); + } + } + + /** Runs all queued Runnables from the calling thread. */ + public void flush() { + LinkedList> queue = new LinkedList>(); + synchronized (mQueue) { + queue.addAll(mQueue); + mQueue.clear(); + } + for (Pair p : queue) { + p.first.run(); + } + } + + void scheduleNextLocked() { + if (mQueue.size() > 0) { + Pair p = mQueue.getFirst(); + Runnable peek = p.first; + if (peek instanceof IdleRunnable) { + mMessageQueue.addIdleHandler(mHandler); + } else { + mHandler.sendEmptyMessage(1); + } + } + } +} + diff --git a/app/src/main/java/com/android/launcher2/DeleteDropTarget.java b/app/src/main/java/com/android/launcher2/DeleteDropTarget.java new file mode 100644 index 0000000..d575b8f --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DeleteDropTarget.java @@ -0,0 +1,437 @@ +/* + * 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.launcher2; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +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.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.launcher.R; + +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; + + public DeleteDropTarget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DeleteDropTarget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @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); + + 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 (!LauncherApplication.isScreenLarge()) { + setText(""); + } + } + } + + private boolean isAllAppsApplication(DragSource source, Object info) { + return (source instanceof AppsCustomizePagedView) && (info instanceof ApplicationInfo); + } + 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() { + mCurrentDrawable.startTransition(mTransitionDuration); + setTextColor(mHoverColor); + } + private void resetHoverColor() { + mCurrentDrawable.resetTransition(); + setTextColor(mOriginalTextColor); + } + + @Override + public boolean acceptDrop(DragObject d) { + // We can remove everything including App shortcuts, folders, widgets, etc. + return true; + } + + @Override + public void onDragStart(DragSource source, Object info, int dragAction) { + boolean isVisible = true; + boolean isUninstall = false; + + // If we are dragging a widget from AppsCustomize, hide the delete target + if (isAllAppsWidget(source, info)) { + isVisible = false; + } + + // 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 + if (isAllAppsApplication(source, info)) { + ApplicationInfo appInfo = (ApplicationInfo) info; + if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) != 0) { + isUninstall = true; + } else { + isVisible = false; + } + } + + if (isUninstall) { + setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null); + } else { + setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null); + } + mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); + + mActive = isVisible; + resetHoverColor(); + ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); + if (getText().length() > 0) { + setText(isUninstall ? 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) { + DragLayer dragLayer = mLauncher.getDragLayer(); + Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, from); + Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); + float scale = (float) to.width() / from.width(); + + mSearchDropTargetBar.deferOnDragEnd(); + Runnable onAnimationEndRunnable = new Runnable() { + @Override + public void run() { + mSearchDropTargetBar.onDragEnd(); + mLauncher.exitSpringLoadedDragMode(); + completeDrop(d); + } + }; + 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 completeDrop(DragObject d) { + ItemInfo item = (ItemInfo) d.dragInfo; + + if (isAllAppsApplication(d.dragSource, item)) { + // Uninstall the application if it is being dragged from AppsCustomize + mLauncher.startApplicationUninstallActivity((ApplicationInfo) item); + } 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); + + final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; + final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); + if (appWidgetHost != null) { + // Deleting an app widget ID is a void call but writes to disk before returning + // to the caller... + new Thread("deleteAppWidgetId") { + public void run() { + appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId); + } + }.start(); + } + } + } + + 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) { + final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), + mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); + 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); + } + + @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; + } + }; + 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); + } + + public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) { + final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView; + + // 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; + 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 float mOffset = 0f; + + @Override + public float getInterpolation(float t) { + if (mCount < 0) { + mCount++; + } else if (mCount == 0) { + mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - + startTime) / duration); + mCount++; + } + 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); + } + Runnable onAnimationEndRunnable = new Runnable() { + @Override + public void run() { + mSearchDropTargetBar.onDragEnd(); + + // 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.getDragController().onDeferredEndFling(d); + } + }; + dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable, + DragLayer.ANIMATION_END_DISAPPEAR, null); + } +} diff --git a/app/src/main/java/com/android/launcher2/DragController.java b/app/src/main/java/com/android/launcher2/DragController.java new file mode 100644 index 0000000..d15cb6e --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DragController.java @@ -0,0 +1,821 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Handler; +import android.os.IBinder; +import android.os.Vibrator; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.inputmethod.InputMethodManager; + +import com.android.launcher.R; + +import java.util.ArrayList; + +/** + * Class for initiating a drag within a view or across multiple views. + */ +public class DragController { + private static final String TAG = "Launcher.DragController"; + + /** Indicates the drag is a move. */ + public static int DRAG_ACTION_MOVE = 0; + + /** 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 = 750; + private static final int VIBRATE_DURATION = 15; + + private static final boolean PROFILE_DRAWING_DURING_DRAG = false; + + private static final int SCROLL_OUTSIDE_ZONE = 0; + private static final int SCROLL_WAITING_IN_ZONE = 1; + + static final int SCROLL_NONE = -1; + static final int SCROLL_LEFT = 0; + static final int SCROLL_RIGHT = 1; + + private static final float MAX_FLING_DEGREES = 35f; + + private Launcher mLauncher; + private Handler mHandler; + private final Vibrator mVibrator; + + // temporaries to avoid gc thrash + private Rect mRectTemp = new Rect(); + private final int[] mCoordinatesTemp = new int[2]; + + /** Whether or not we're dragging. */ + private boolean mDragging; + + /** X coordinate of the down event. */ + private int mMotionDownX; + + /** Y coordinate of the down event. */ + private int mMotionDownY; + + /** the area at the edge of the screen that makes the workspace go left + * or right while you're dragging. + */ + private int mScrollZone; + + private DropTarget.DragObject mDragObject; + + /** Who can receive drop events */ + private ArrayList mDropTargets = new ArrayList(); + private ArrayList mListeners = new ArrayList(); + private DropTarget mFlingToDeleteDropTarget; + + /** The window token used as the parent for the DragView. */ + private IBinder mWindowToken; + + /** The view that will be scrolled when dragging to the left and right edges of the screen. */ + private View mScrollView; + + private View mMoveTarget; + + private DragScroller mDragScroller; + private 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; + + private int mTmpPoint[] = new int[2]; + private Rect mDragLayerRect = new Rect(); + + protected int mFlingToDeleteThresholdVelocity; + private VelocityTracker mVelocityTracker; + + /** + * Interface to receive notifications when a drag starts or stops + */ + interface DragListener { + + /** + * A drag has begun + * + * @param source An object representing where the drag originated + * @param info The data associated with the object that is being dragged + * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE} + * or {@link DragController#DRAG_ACTION_COPY} + */ + void onDragStart(DragSource source, Object info, int dragAction); + + /** + * The drag has ended + */ + void onDragEnd(); + } + + /** + * Used to create a new DragLayer from XML. + * + * @param context The application's context. + */ + public DragController(Launcher launcher) { + Resources r = launcher.getResources(); + mLauncher = launcher; + mHandler = new Handler(); + mScrollZone = r.getDimensionPixelSize(R.dimen.scroll_zone); + mVelocityTracker = VelocityTracker.obtain(); + mVibrator = (Vibrator) launcher.getSystemService(Context.VIBRATOR_SERVICE); + + float density = r.getDisplayMetrics().density; + mFlingToDeleteThresholdVelocity = + (int) (r.getInteger(R.integer.config_flingToDeleteMinVelocity) * density); + } + + public boolean dragging() { + return mDragging; + } + + /** + * Starts a drag. + * + * @param v The view that is being dragged + * @param bmp The bitmap that represents the view being dragged + * @param source An object representing where the drag originated + * @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 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) { + 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); + + startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null, + null, initialDragViewScale); + + if (dragAction == DRAG_ACTION_MOVE) { + v.setVisibility(View.GONE); + } + } + + /** + * Starts a drag. + * + * @param b The bitmap to display as the drag image. It will be re-scaled to the + * enlarged size. + * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. + * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. + * @param source An object representing where the drag originated + * @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 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(Bitmap b, int dragLayerX, int dragLayerY, + DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion, + float initialDragViewScale) { + if (PROFILE_DRAWING_DURING_DRAG) { + android.os.Debug.startMethodTracing("Launcher"); + } + + // Hide soft keyboard, if visible + if (mInputMethodManager == null) { + mInputMethodManager = (InputMethodManager) + mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE); + } + mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); + + for (DragListener listener : mListeners) { + listener.onDragStart(source, dragInfo, dragAction); + } + + final int registrationX = mMotionDownX - dragLayerX; + final int registrationY = mMotionDownY - dragLayerY; + + final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; + final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; + + mDragging = true; + + mDragObject = new DropTarget.DragObject(); + + mDragObject.dragComplete = false; + mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); + mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); + mDragObject.dragSource = source; + mDragObject.dragInfo = dragInfo; + + mVibrator.vibrate(VIBRATE_DURATION); + + final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, + registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale); + + if (dragOffset != null) { + dragView.setDragVisualizeOffset(new Point(dragOffset)); + } + if (dragRegion != null) { + dragView.setDragRegion(new Rect(dragRegion)); + } + + dragView.show(mMotionDownX, mMotionDownY); + handleMoveEvent(mMotionDownX, mMotionDownY); + } + + /** + * Draw the view into a bitmap. + */ + Bitmap getViewBitmap(View v) { + v.clearFocus(); + v.setPressed(false); + + boolean willNotCache = v.willNotCacheDrawing(); + v.setWillNotCacheDrawing(false); + + // Reset the drawing cache background color to fully transparent + // for the duration of this operation + int color = v.getDrawingCacheBackgroundColor(); + v.setDrawingCacheBackgroundColor(0); + float alpha = v.getAlpha(); + v.setAlpha(1.0f); + + if (color != 0) { + v.destroyDrawingCache(); + } + v.buildDrawingCache(); + Bitmap cacheBitmap = v.getDrawingCache(); + if (cacheBitmap == null) { + Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); + return null; + } + + Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); + + // Restore the view + v.destroyDrawingCache(); + v.setAlpha(alpha); + v.setWillNotCacheDrawing(willNotCache); + v.setDrawingCacheBackgroundColor(color); + + return bitmap; + } + + /** + * Call this from a drag source view like this: + * + *
    +     *  @Override
    +     *  public boolean dispatchKeyEvent(KeyEvent event) {
    +     *      return mDragController.dispatchKeyEvent(this, event)
    +     *              || super.dispatchKeyEvent(event);
    +     * 
    + */ + public boolean dispatchKeyEvent(KeyEvent event) { + return mDragging; + } + + public boolean isDragging() { + return mDragging; + } + + /** + * Stop dragging without dropping. + */ + public void cancelDrag() { + if (mDragging) { + if (mLastDropTarget != null) { + mLastDropTarget.onDragExit(mDragObject); + } + mDragObject.deferDragViewCleanupPostAnimation = false; + mDragObject.cancelled = true; + mDragObject.dragComplete = true; + mDragObject.dragSource.onDropCompleted(null, mDragObject, false, false); + } + endDrag(); + } + public void onAppsRemoved(ArrayList appInfos, Context context) { + // Cancel the current drag if we are removing an app that we are dragging + if (mDragObject != null) { + Object rawDragInfo = mDragObject.dragInfo; + if (rawDragInfo instanceof ShortcutInfo) { + ShortcutInfo dragInfo = (ShortcutInfo) rawDragInfo; + for (ApplicationInfo info : appInfos) { + // Added null checks to prevent NPE we've seen in the wild + if (dragInfo != null && + dragInfo.intent != null) { + boolean isSameComponent = + dragInfo.intent.getComponent().equals(info.componentName); + if (isSameComponent) { + cancelDrag(); + return; + } + } + } + } + } + } + + private void endDrag() { + if (mDragging) { + mDragging = false; + clearScrollRunnable(); + boolean isDeferred = false; + if (mDragObject.dragView != null) { + isDeferred = mDragObject.deferDragViewCleanupPostAnimation; + if (!isDeferred) { + mDragObject.dragView.remove(); + } + mDragObject.dragView = null; + } + + // Only end the drag if we are not deferred + if (!isDeferred) { + for (DragListener listener : mListeners) { + listener.onDragEnd(); + } + } + } + + releaseVelocityTracker(); + } + + /** + * This only gets called as a result of drag view cleanup being deferred in endDrag(); + */ + void onDeferredEndDrag(DragView dragView) { + dragView.remove(); + + // If we skipped calling onDragEnd() before, do it now + for (DragListener listener : mListeners) { + listener.onDragEnd(); + } + } + + void onDeferredEndFling(DropTarget.DragObject d) { + d.dragSource.onFlingToDeleteCompleted(); + } + + /** + * Clamps the position to the drag layer bounds. + */ + private int[] getClampedDragLayerPos(float x, float y) { + mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect); + mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1)); + mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1)); + return mTmpPoint; + } + + long getLastGestureUpTime() { + if (mDragging) { + return System.currentTimeMillis(); + } else { + return mLastTouchUpTime; + } + } + + void resetLastGestureUpTime() { + mLastTouchUpTime = -1; + } + + /** + * Call this from a drag source view. + */ + public boolean onInterceptTouchEvent(MotionEvent ev) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" + + mDragging); + } + + // Update the velocity tracker + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); + final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); + final int dragLayerX = dragLayerPos[0]; + final int dragLayerY = dragLayerPos[1]; + + switch (action) { + case MotionEvent.ACTION_MOVE: + break; + case MotionEvent.ACTION_DOWN: + // Remember location of down touch + mMotionDownX = dragLayerX; + mMotionDownY = dragLayerY; + mLastDropTarget = null; + break; + case MotionEvent.ACTION_UP: + mLastTouchUpTime = System.currentTimeMillis(); + if (mDragging) { + PointF vec = isFlingingToDelete(mDragObject.dragSource); + if (vec != null) { + dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec); + } else { + drop(dragLayerX, dragLayerY); + } + } + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + cancelDrag(); + break; + } + + return mDragging; + } + + /** + * Sets the view that should handle move events. + */ + void setMoveTarget(View view) { + mMoveTarget = view; + } + + public boolean dispatchUnhandledMove(View focused, int direction) { + return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); + } + + private void clearScrollRunnable() { + mHandler.removeCallbacks(mScrollRunnable); + if (mScrollState == SCROLL_WAITING_IN_ZONE) { + mScrollState = SCROLL_OUTSIDE_ZONE; + mScrollRunnable.setDirection(SCROLL_RIGHT); + mDragScroller.onExitScrollArea(); + mLauncher.getDragLayer().onExitScrollArea(); + } + } + + private void handleMoveEvent(int x, int y) { + mDragObject.dragView.move(x, y); + + // Drop on someone? + final int[] coordinates = mCoordinatesTemp; + DropTarget dropTarget = findDropTarget(x, y, coordinates); + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + 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)); + mLastTouch[0] = x; + mLastTouch[1] = y; + checkScrollState(x, y); + } + + public void forceTouchMove() { + int[] dummyCoordinates = mCoordinatesTemp; + DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates); + checkTouchMove(dropTarget); + } + + private void checkTouchMove(DropTarget dropTarget) { + if (dropTarget != null) { + DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject); + if (delegate != null) { + dropTarget = delegate; + } + + if (mLastDropTarget != dropTarget) { + if (mLastDropTarget != null) { + mLastDropTarget.onDragExit(mDragObject); + } + dropTarget.onDragEnter(mDragObject); + } + dropTarget.onDragOver(mDragObject); + } else { + if (mLastDropTarget != null) { + mLastDropTarget.onDragExit(mDragObject); + } + } + mLastDropTarget = dropTarget; + } + + private 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; + + if (x < mScrollZone) { + if (mScrollState == SCROLL_OUTSIDE_ZONE) { + mScrollState = SCROLL_WAITING_IN_ZONE; + if (mDragScroller.onEnterScrollArea(x, y, forwardDirection)) { + dragLayer.onEnterScrollArea(forwardDirection); + mScrollRunnable.setDirection(forwardDirection); + mHandler.postDelayed(mScrollRunnable, delay); + } + } + } else if (x > mScrollView.getWidth() - mScrollZone) { + if (mScrollState == SCROLL_OUTSIDE_ZONE) { + mScrollState = SCROLL_WAITING_IN_ZONE; + if (mDragScroller.onEnterScrollArea(x, y, backwardsDirection)) { + dragLayer.onEnterScrollArea(backwardsDirection); + mScrollRunnable.setDirection(backwardsDirection); + mHandler.postDelayed(mScrollRunnable, delay); + } + } + } else { + clearScrollRunnable(); + } + } + + /** + * Call this from a drag source view. + */ + public boolean onTouchEvent(MotionEvent ev) { + if (!mDragging) { + return false; + } + + // Update the velocity tracker + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); + final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); + final int dragLayerX = dragLayerPos[0]; + final int dragLayerY = dragLayerPos[1]; + + switch (action) { + case MotionEvent.ACTION_DOWN: + // Remember where the motion event started + mMotionDownX = dragLayerX; + mMotionDownY = dragLayerY; + + if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) { + mScrollState = SCROLL_WAITING_IN_ZONE; + mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); + } else { + mScrollState = SCROLL_OUTSIDE_ZONE; + } + break; + case MotionEvent.ACTION_MOVE: + handleMoveEvent(dragLayerX, dragLayerY); + break; + case MotionEvent.ACTION_UP: + // Ensure that we've processed a move event at the current pointer location. + handleMoveEvent(dragLayerX, dragLayerY); + mHandler.removeCallbacks(mScrollRunnable); + + if (mDragging) { + PointF vec = isFlingingToDelete(mDragObject.dragSource); + if (vec != null) { + dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec); + } else { + drop(dragLayerX, dragLayerY); + } + } + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + mHandler.removeCallbacks(mScrollRunnable); + cancelDrag(); + break; + } + + return true; + } + + /** + * Determines whether the user flung the current item to delete it. + * + * @return the vector at which the item was flung, or null if no fling was detected. + */ + private PointF isFlingingToDelete(DragSource source) { + if (mFlingToDeleteDropTarget == null) return null; + if (!source.supportsFlingToDelete()) return null; + + ViewConfiguration config = ViewConfiguration.get(mLauncher); + 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(MAX_FLING_DEGREES)) { + return vel; + } + } + return null; + } + + private void dropOnFlingToDeleteTarget(float x, float y, PointF vel) { + final int[] coordinates = mCoordinatesTemp; + + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + + // Clean up dragging on the target if it's not the current fling delete target otherwise, + // start dragging to it. + if (mLastDropTarget != null && mFlingToDeleteDropTarget != mLastDropTarget) { + mLastDropTarget.onDragExit(mDragObject); + } + + // Drop onto the fling-to-delete target + boolean accepted = false; + mFlingToDeleteDropTarget.onDragEnter(mDragObject); + // We must set dragComplete to true _only_ after we "enter" the fling-to-delete target for + // "drop" + mDragObject.dragComplete = true; + mFlingToDeleteDropTarget.onDragExit(mDragObject); + if (mFlingToDeleteDropTarget.acceptDrop(mDragObject)) { + mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, mDragObject.x, mDragObject.y, + vel); + accepted = true; + } + mDragObject.dragSource.onDropCompleted((View) mFlingToDeleteDropTarget, mDragObject, true, + accepted); + } + + private void drop(float x, float y) { + final int[] coordinates = mCoordinatesTemp; + final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); + + mDragObject.x = coordinates[0]; + mDragObject.y = coordinates[1]; + boolean accepted = false; + if (dropTarget != null) { + mDragObject.dragComplete = true; + dropTarget.onDragExit(mDragObject); + if (dropTarget.acceptDrop(mDragObject)) { + dropTarget.onDrop(mDragObject); + accepted = true; + } + } + mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted); + } + + private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { + final Rect r = mRectTemp; + + final ArrayList dropTargets = mDropTargets; + final int count = dropTargets.size(); + for (int i=count-1; i>=0; i--) { + DropTarget target = dropTargets.get(i); + if (!target.isDropEnabled()) + continue; + + target.getHitRect(r); + + // Convert the hit rect to DragLayer coordinates + target.getLocationInDragLayer(dropCoordinates); + r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); + + mDragObject.x = x; + mDragObject.y = y; + if (r.contains(x, y)) { + DropTarget delegate = target.getDropTargetDelegate(mDragObject); + if (delegate != null) { + target = delegate; + target.getLocationInDragLayer(dropCoordinates); + } + + // Make dropCoordinates relative to the DropTarget + dropCoordinates[0] = x - dropCoordinates[0]; + dropCoordinates[1] = y - dropCoordinates[1]; + + return target; + } + } + return null; + } + + public void setDragScoller(DragScroller scroller) { + mDragScroller = scroller; + } + + public void setWindowToken(IBinder token) { + mWindowToken = token; + } + + /** + * Sets the drag listner which will be notified when a drag starts or ends. + */ + public void addDragListener(DragListener l) { + mListeners.add(l); + } + + /** + * Remove a previously installed drag listener. + */ + public void removeDragListener(DragListener l) { + mListeners.remove(l); + } + + /** + * Add a DropTarget to the list of potential places to receive drop events. + */ + public void addDropTarget(DropTarget target) { + mDropTargets.add(target); + } + + /** + * Don't send drop events to target any more. + */ + public void removeDropTarget(DropTarget target) { + mDropTargets.remove(target); + } + + /** + * Sets the current fling-to-delete drop target. + */ + public void setFlingToDeleteDropTarget(DropTarget target) { + mFlingToDeleteDropTarget = target; + } + + private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * Set which view scrolls for touch events near the edge of the screen. + */ + public void setScrollView(View v) { + mScrollView = v; + } + + DragView getDragView() { + return mDragObject.dragView; + } + + private class ScrollRunnable implements Runnable { + private int mDirection; + + ScrollRunnable() { + } + + public void run() { + if (mDragScroller != null) { + if (mDirection == SCROLL_LEFT) { + mDragScroller.scrollLeft(); + } else { + mDragScroller.scrollRight(); + } + mScrollState = SCROLL_OUTSIDE_ZONE; + mDistanceSinceScroll = 0; + mDragScroller.onExitScrollArea(); + mLauncher.getDragLayer().onExitScrollArea(); + + if (isDragging()) { + // Check the scroll again so that we can requeue the scroller if necessary + checkScrollState(mLastTouch[0], mLastTouch[1]); + } + } + } + + void setDirection(int direction) { + mDirection = direction; + } + } +} diff --git a/app/src/main/java/com/android/launcher2/DragLayer.java b/app/src/main/java/com/android/launcher2/DragLayer.java new file mode 100644 index 0000000..c3f442b --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DragLayer.java @@ -0,0 +1,804 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.launcher.R; + +import java.util.ArrayList; + +/** + * A ViewGroup that coordinates dragging across its descendants + */ +public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChangeListener { + private DragController mDragController; + private int[] mTmpXY = new int[2]; + + private int mXDown, mYDown; + private Launcher mLauncher; + + // Variables relating to resizing widgets + private final ArrayList mResizeFrames = + new ArrayList(); + 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 boolean mHoverPointClosesFolder = false; + private Rect mHitRect = new Rect(); + private int mWorkspaceIndex = -1; + private int mQsbIndex = -1; + 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; + + /** + * Used to create a new DragLayer from XML. + * + * @param context The application's context. + * @param attrs The attributes set containing the Workspace's customization values. + */ + public DragLayer(Context context, AttributeSet attrs) { + super(context, attrs); + + // Disable multitouch across the workspace/all apps/customize tray + setMotionEventSplittingEnabled(false); + setChildrenDrawingOrderEnabled(true); + setOnHierarchyChangeListener(this); + + mLeftHoverDrawable = getResources().getDrawable(R.drawable.page_hover_left_holo); + mRightHoverDrawable = getResources().getDrawable(R.drawable.page_hover_right_holo); + } + + public void setup(Launcher launcher, DragController controller) { + mLauncher = launcher; + mDragController = controller; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); + } + + private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) { + getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect); + if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { + return true; + } + return false; + } + + private boolean isEventOverFolder(Folder folder, MotionEvent ev) { + getDescendantRectRelativeToSelf(folder, mHitRect); + if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { + return true; + } + return false; + } + + private boolean handleTouchDown(MotionEvent ev, boolean intercept) { + Rect hitRect = new Rect(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + + for (AppWidgetResizeFrame child: mResizeFrames) { + child.getHitRect(hitRect); + if (hitRect.contains(x, y)) { + if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) { + mCurrentResizeFrame = child; + mXDown = x; + mYDown = y; + requestDisallowInterceptTouchEvent(true); + return true; + } + } + } + + Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); + if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) { + if (currentFolder.isEditingName()) { + if (!isEventOverFolderTextRegion(currentFolder, ev)) { + currentFolder.dismissEditingName(); + return true; + } + } + + getDescendantRectRelativeToSelf(currentFolder, hitRect); + if (!isEventOverFolder(currentFolder, ev)) { + mLauncher.closeFolder(); + return true; + } + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (handleTouchDown(ev, true)) { + return true; + } + } + clearAllResizeFrames(); + return mDragController.onInterceptTouchEvent(ev); + } + + @Override + public boolean onInterceptHoverEvent(MotionEvent ev) { + if (mLauncher == null || mLauncher.getWorkspace() == null) { + return false; + } + Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); + if (currentFolder == null) { + return false; + } else { + AccessibilityManager accessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isTouchExplorationEnabled()) { + final int action = ev.getAction(); + boolean isOverFolder; + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: + isOverFolder = isEventOverFolder(currentFolder, ev); + if (!isOverFolder) { + sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); + mHoverPointClosesFolder = true; + return true; + } else if (isOverFolder) { + mHoverPointClosesFolder = false; + } else { + return true; + } + case MotionEvent.ACTION_HOVER_MOVE: + isOverFolder = isEventOverFolder(currentFolder, ev); + if (!isOverFolder && !mHoverPointClosesFolder) { + sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName()); + mHoverPointClosesFolder = true; + return true; + } else if (isOverFolder) { + mHoverPointClosesFolder = false; + } else { + return true; + } + } + } + } + return false; + } + + private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) { + AccessibilityManager accessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isEnabled()) { + int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close; + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_VIEW_FOCUSED); + onInitializeAccessibilityEvent(event); + event.getText().add(getContext().getString(stringId)); + accessibilityManager.sendAccessibilityEvent(event); + } + } + + @Override + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); + if (currentFolder != null) { + if (child == currentFolder) { + return super.onRequestSendAccessibilityEvent(child, event); + } + // Skip propagating onRequestSendAccessibilityEvent all for other children + // when a folder is open + return false; + } + return super.onRequestSendAccessibilityEvent(child, event); + } + + @Override + public void addChildrenForAccessibility(ArrayList childrenForAccessibility) { + Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); + if (currentFolder != null) { + // Only add the folder as a child for accessibility when it is open + childrenForAccessibility.add(currentFolder); + } else { + super.addChildrenForAccessibility(childrenForAccessibility); + } + } + + @Override + public boolean onHoverEvent(MotionEvent ev) { + // If we've received this, we've already done the necessary handling + // in onInterceptHoverEvent. Return true to consume the event. + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean handled = false; + int action = ev.getAction(); + + int x = (int) ev.getX(); + int y = (int) ev.getY(); + + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (handleTouchDown(ev, false)) { + return true; + } + } + } + + if (mCurrentResizeFrame != null) { + handled = true; + switch (action) { + case MotionEvent.ACTION_MOVE: + mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown); + mCurrentResizeFrame.onTouchUp(); + mCurrentResizeFrame = null; + } + } + if (handled) return true; + return mDragController.onTouchEvent(ev); + } + + /** + * Determine the rect of the descendant in this DragLayer's coordinates + * + * @param descendant The descendant whose coordinates we want to find. + * @param r The rect into which to place the results. + * @return The factor by which this descendant is scaled relative to this DragLayer. + */ + public float getDescendantRectRelativeToSelf(View descendant, Rect r) { + mTmpXY[0] = 0; + mTmpXY[1] = 0; + float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY); + r.set(mTmpXY[0], mTmpXY[1], + mTmpXY[0] + descendant.getWidth(), mTmpXY[1] + descendant.getHeight()); + return scale; + } + + public float getLocationInDragLayer(View child, int[] loc) { + loc[0] = 0; + loc[1] = 0; + return getDescendantCoordRelativeToSelf(child, loc); + } + + /** + * Given a coordinate relative to the descendant, find the coordinate in this DragLayer's + * coordinates. + * + * @param descendant The descendant to which the passed coordinate is relative. + * @param coord The coordinate that we want mapped. + * @return The factor by which this descendant is scaled relative to this DragLayer. Caution + * this scale factor is assumed to be equal in X and Y, and so if at any point this + * assumption fails, we will need to return a pair of scale factors. + */ + public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) { + float scale = 1.0f; + float[] pt = {coord[0], coord[1]}; + descendant.getMatrix().mapPoints(pt); + scale *= descendant.getScaleX(); + pt[0] += descendant.getLeft(); + pt[1] += descendant.getTop(); + ViewParent viewParent = descendant.getParent(); + while (viewParent instanceof View && viewParent != this) { + final View view = (View)viewParent; + view.getMatrix().mapPoints(pt); + scale *= view.getScaleX(); + pt[0] += view.getLeft() - view.getScrollX(); + pt[1] += view.getTop() - view.getScrollY(); + viewParent = view.getParent(); + } + coord[0] = (int) Math.round(pt[0]); + coord[1] = (int) Math.round(pt[1]); + return scale; + } + + public void getViewRectRelativeToSelf(View v, Rect r) { + int[] loc = new int[2]; + getLocationInWindow(loc); + int x = loc[0]; + int y = loc[1]; + + v.getLocationInWindow(loc); + int vX = loc[0]; + int vY = loc[1]; + + int left = vX - x; + int top = vY - y; + r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight()); + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + return mDragController.dispatchUnhandledMove(focused, direction); + } + + public static class LayoutParams extends FrameLayout.LayoutParams { + public int x, y; + public boolean customPosition = false; + + /** + * {@inheritDoc} + */ + public LayoutParams(int width, int height) { + super(width, height); + } + + public void setWidth(int width) { + this.width = width; + } + + public int getWidth() { + return width; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getHeight() { + return height; + } + + public void setX(int x) { + this.x = x; + } + + public int getX() { + return x; + } + + public void setY(int y) { + this.y = y; + } + + public int getY() { + return y; + } + } + + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams(); + if (flp instanceof LayoutParams) { + final LayoutParams lp = (LayoutParams) flp; + if (lp.customPosition) { + child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height); + } + } + } + } + + public void clearAllResizeFrames() { + if (mResizeFrames.size() > 0) { + for (AppWidgetResizeFrame frame: mResizeFrames) { + frame.commitResize(); + removeView(frame); + } + mResizeFrames.clear(); + } + } + + public boolean hasResizeFrames() { + return mResizeFrames.size() > 0; + } + + public boolean isWidgetBeingResized() { + return mCurrentResizeFrame != null; + } + + public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget, + CellLayout cellLayout) { + AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(), + widget, cellLayout, this); + + LayoutParams lp = new LayoutParams(-1, -1); + lp.customPosition = true; + + addView(resizeFrame, lp); + mResizeFrames.add(resizeFrame); + + resizeFrame.snapToWidget(false); + } + + public void animateViewIntoPosition(DragView dragView, final View child) { + animateViewIntoPosition(dragView, child, null); + } + + public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, + float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, + int duration) { + Rect r = new Rect(); + getViewRectRelativeToSelf(dragView, r); + final int fromX = r.left; + final int fromY = r.top; + + animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY, + onFinishRunnable, animationEndStyle, duration, null); + } + + public void animateViewIntoPosition(DragView dragView, final View child, + final Runnable onFinishAnimationRunnable) { + animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable, null); + } + + public void animateViewIntoPosition(DragView dragView, final View child, int duration, + final Runnable onFinishAnimationRunnable, View anchorView) { + ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent(); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + parentChildren.measureChild(child); + + Rect r = new Rect(); + getViewRectRelativeToSelf(dragView, r); + + int coord[] = new int[2]; + float childScale = child.getScaleX(); + coord[0] = lp.x + (int) (child.getMeasuredWidth() * (1 - childScale) / 2); + coord[1] = lp.y + (int) (child.getMeasuredHeight() * (1 - childScale) / 2); + + // Since the child hasn't necessarily been laid out, we force the lp to be updated with + // the correct coordinates (above) and use these to determine the final location + float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord); + // We need to account for the scale of the child itself, as the above only accounts for + // for the scale in parents. + scale *= childScale; + int toX = coord[0]; + int toY = coord[1]; + if (child instanceof TextView) { + TextView tv = (TextView) child; + + // The child may be scaled (always about the center of the view) so to account for it, + // we have to offset the position by the scaled size. Once we do that, we can center + // the drag view about the scaled child view. + toY += Math.round(scale * tv.getPaddingTop()); + toY -= dragView.getMeasuredHeight() * (1 - scale) / 2; + toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; + } else if (child instanceof FolderIcon) { + // Account for holographic blur padding on the drag view + toY -= scale * Workspace.DRAG_BITMAP_PADDING / 2; + toY -= (1 - scale) * dragView.getMeasuredHeight() / 2; + // Center in the x coordinate about the target's drawable + toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2; + } else { + toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2; + toX -= (Math.round(scale * (dragView.getMeasuredWidth() + - child.getMeasuredWidth()))) / 2; + } + + final int fromX = r.left; + final int fromY = r.top; + child.setVisibility(INVISIBLE); + Runnable onCompleteRunnable = new Runnable() { + public void run() { + child.setVisibility(VISIBLE); + if (onFinishAnimationRunnable != null) { + onFinishAnimationRunnable.run(); + } + } + }; + animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, scale, scale, + onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView); + } + + public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY, + final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY, + float finalScaleX, float finalScaleY, Runnable onCompleteRunnable, + int animationEndStyle, int duration, View anchorView) { + Rect from = new Rect(fromX, fromY, fromX + + view.getMeasuredWidth(), fromY + view.getMeasuredHeight()); + Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight()); + animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration, + null, null, onCompleteRunnable, animationEndStyle, anchorView); + } + + /** + * This method animates a view at the end of a drag and drop animation. + * + * @param view The view to be animated. This view is drawn directly into DragLayer, and so + * doesn't need to be a child of DragLayer. + * @param from The initial location of the view. Only the left and top parameters are used. + * @param to The final location of the view. Only the left and top parameters are used. This + * location doesn't account for scaling, and so should be centered about the desired + * final location (including scaling). + * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates. + * @param finalScale The final scale of the view. The view is scaled about its center. + * @param duration The duration of the animation. + * @param motionInterpolator The interpolator to use for the location of the view. + * @param alphaInterpolator The interpolator to use for the alpha of the view. + * @param onCompleteRunnable Optional runnable to run on animation completion. + * @param fadeOut Whether or not to fade out the view once the animation completes. If true, + * the runnable will execute after the view is faded out. + * @param anchorView If not null, this represents the view which the animated view stays + * anchored to in case scrolling is currently taking place. Note: currently this is + * only used for the X dimension for the case of the workspace. + */ + public void animateView(final DragView view, final Rect from, final Rect to, + final float finalAlpha, final float initScaleX, final float initScaleY, + final float finalScaleX, final float finalScaleY, int duration, + final Interpolator motionInterpolator, final Interpolator alphaInterpolator, + 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 Resources res = getResources(); + final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist); + + // If duration < 0, this is a cue to compute the duration based on the distance + if (duration < 0) { + duration = res.getInteger(R.integer.config_dropAnimMaxDuration); + if (dist < maxDist) { + duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist); + } + duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration)); + } + + // Fall back to cubic ease out interpolator for the animation if none is specified + TimeInterpolator interpolator = null; + if (alphaInterpolator == null || motionInterpolator == null) { + interpolator = mCubicEaseOutInterpolator; + } + + // Animate the view + final float initAlpha = view.getAlpha(); + final float dropViewScale = view.getScaleX(); + AnimatorUpdateListener updateCb = new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float percent = (Float) animation.getAnimatedValue(); + final int width = view.getMeasuredWidth(); + final int height = view.getMeasuredHeight(); + + float alphaPercent = alphaInterpolator == null ? percent : + alphaInterpolator.getInterpolation(percent); + float motionPercent = motionInterpolator == null ? percent : + motionInterpolator.getInterpolation(percent); + + float initialScaleX = initScaleX * dropViewScale; + float initialScaleY = initScaleY * dropViewScale; + float scaleX = finalScaleX * percent + initialScaleX * (1 - percent); + float scaleY = finalScaleY * percent + initialScaleY * (1 - percent); + float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent); + + float fromLeft = from.left + (initialScaleX - 1f) * width / 2; + float fromTop = from.top + (initialScaleY - 1f) * height / 2; + + int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent))); + int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent))); + + int xPos = x - mDropView.getScrollX() + (mAnchorView != null + ? (mAnchorViewInitialScrollX - mAnchorView.getScrollX()) : 0); + int yPos = y - mDropView.getScrollY(); + + mDropView.setTranslationX(xPos); + mDropView.setTranslationY(yPos); + mDropView.setScaleX(scaleX); + mDropView.setScaleY(scaleY); + mDropView.setAlpha(alpha); + } + }; + animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle, + anchorView); + } + + public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration, + TimeInterpolator interpolator, final Runnable onCompleteRunnable, + 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; + mDropView.cancelAnimation(); + mDropView.resetLayoutParams(); + + // Set the anchor view if the page is scrolling + if (anchorView != null) { + mAnchorViewInitialScrollX = anchorView.getScrollX(); + } + mAnchorView = anchorView; + + // Create and start the animation + mDropAnim = new ValueAnimator(); + mDropAnim.setInterpolator(interpolator); + mDropAnim.setDuration(duration); + mDropAnim.setFloatValues(0f, 1f); + mDropAnim.addUpdateListener(updateCb); + mDropAnim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + switch (animationEndStyle) { + case ANIMATION_END_DISAPPEAR: + clearAnimatedView(); + break; + case ANIMATION_END_FADE_OUT: + fadeOutDragView(); + break; + case ANIMATION_END_REMAIN_VISIBLE: + break; + } + } + }); + mDropAnim.start(); + } + + public void clearAnimatedView() { + if (mDropAnim != null) { + mDropAnim.cancel(); + } + if (mDropView != null) { + mDragController.onDeferredEndDrag(mDropView); + } + mDropView = null; + invalidate(); + } + + public View getAnimatedView() { + 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) { + updateChildIndices(); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + updateChildIndices(); + } + + private void updateChildIndices() { + if (mLauncher != null) { + mWorkspaceIndex = indexOfChild(mLauncher.getWorkspace()); + mQsbIndex = indexOfChild(mLauncher.getSearchBar()); + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + // TODO: We have turned off this custom drawing order because it now effects touch + // dispatch order. We need to sort that issue out and then decide how to go about this. + if (true || LauncherApplication.isScreenLandscape(getContext()) || + mWorkspaceIndex == -1 || mQsbIndex == -1 || + mLauncher.getWorkspace().isDrawingBackgroundGradient()) { + return i; + } + + // This ensures that the workspace is drawn above the hotseat and qsb, + // except when the workspace is drawing a background gradient, in which + // case we want the workspace to stay behind these elements. + if (i == mQsbIndex) { + return mWorkspaceIndex; + } else if (i == mWorkspaceIndex) { + return mQsbIndex; + } else { + return i; + } + } + + private boolean mInScrollArea; + private Drawable mLeftHoverDrawable; + private Drawable mRightHoverDrawable; + + void onEnterScrollArea(int direction) { + mInScrollArea = true; + invalidate(); + } + + void onExitScrollArea() { + mInScrollArea = false; + invalidate(); + } + + /** + * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. + */ + private boolean isLayoutDirectionRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mInScrollArea && !LauncherApplication.isScreenLarge()) { + Workspace workspace = mLauncher.getWorkspace(); + int width = workspace.getWidth(); + Rect childRect = new Rect(); + getDescendantRectRelativeToSelf(workspace.getChildAt(0), childRect); + + int page = workspace.getNextPage(); + final boolean isRtl = isLayoutDirectionRtl(); + CellLayout leftPage = (CellLayout) workspace.getChildAt(isRtl ? page + 1 : page - 1); + CellLayout rightPage = (CellLayout) workspace.getChildAt(isRtl ? page - 1 : page + 1); + + if (leftPage != null && leftPage.getIsDragOverlapping()) { + mLeftHoverDrawable.setBounds(0, childRect.top, + mLeftHoverDrawable.getIntrinsicWidth(), childRect.bottom); + mLeftHoverDrawable.draw(canvas); + } else if (rightPage != null && rightPage.getIsDragOverlapping()) { + mRightHoverDrawable.setBounds(width - mRightHoverDrawable.getIntrinsicWidth(), + childRect.top, width, childRect.bottom); + mRightHoverDrawable.draw(canvas); + } + } + } +} diff --git a/app/src/main/java/com/android/launcher2/DragScroller.java b/app/src/main/java/com/android/launcher2/DragScroller.java new file mode 100644 index 0000000..a3ee6c2 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DragScroller.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher2; + +/** + * Handles scrolling while dragging + * + */ +public interface DragScroller { + void scrollLeft(); + void scrollRight(); + + /** + * The touch point has entered the scroll area; a scroll is imminent. + * This event will only occur while a drag is active. + * + * @param direction The scroll direction + */ + boolean onEnterScrollArea(int x, int y, int direction); + + /** + * The touch point has left the scroll area. + * NOTE: This may not be called, if a drop occurs inside the scroll area. + */ + boolean onExitScrollArea(); +} diff --git a/app/src/main/java/com/android/launcher2/DragSource.java b/app/src/main/java/com/android/launcher2/DragSource.java new file mode 100644 index 0000000..5440477 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DragSource.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher2; + +import android.view.View; + +import com.android.launcher2.DropTarget.DragObject; + +/** + * Interface defining an object that can originate a drag. + * + */ +public interface DragSource { + /** + * @return whether items dragged from this source supports + */ + boolean supportsFlingToDelete(); + + /** + * A callback specifically made back to the source after an item from this source has been flung + * to be deleted on a DropTarget. In such a situation, this method will be called after + * onDropCompleted, and more importantly, after the fling animation has completed. + */ + void onFlingToDeleteCompleted(); + + /** + * A callback made back to the source after an item from this source has been dropped on a + * DropTarget. + */ + void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, boolean success); +} diff --git a/app/src/main/java/com/android/launcher2/DragView.java b/app/src/main/java/com/android/launcher2/DragView.java new file mode 100644 index 0000000..b25ae61 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DragView.java @@ -0,0 +1,295 @@ +/* + * 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.launcher2; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher.R; + +public class DragView extends View { + private static float sDragAlpha = 1f; + + private Bitmap mBitmap; + private Bitmap mCrossFadeBitmap; + private Paint mPaint; + private int mRegistrationX; + private int mRegistrationY; + + private Point mDragVisualizeOffset = null; + private Rect mDragRegion = null; + private DragLayer mDragLayer = null; + private boolean mHasDrawn = false; + private float mCrossFadeProgress = 0f; + + ValueAnimator mAnim; + private float mOffsetX = 0.0f; + private float mOffsetY = 0.0f; + private float mInitialScale = 1f; + + /** + * Construct the drag view. + *

    + * The registration point is the point inside our view that the touch events should + * be centered upon. + * + * @param launcher The Launcher instance + * @param bitmap The view that we're dragging around. We scale it up when we draw it. + * @param registrationX The x coordinate of the registration point. + * @param registrationY The y coordinate of the registration point. + */ + public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, + int left, int top, int width, int height, final float initialScale) { + super(launcher); + mDragLayer = launcher.getDragLayer(); + 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; + + // Set the initial scale to avoid any jumps + setScaleX(initialScale); + setScaleY(initialScale); + + // Animate the view into the correct position + mAnim = LauncherAnimUtils.ofFloat(this, 0f, 1f); + mAnim.setDuration(150); + mAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + 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); + + mOffsetX += deltaX; + mOffsetY += deltaY; + setScaleX(initialScale + (value * (scale - initialScale))); + setScaleY(initialScale + (value * (scale - initialScale))); + if (sDragAlpha != 1f) { + setAlpha(sDragAlpha * value + (1f - value)); + } + + if (getParent() == null) { + animation.cancel(); + } else { + setTranslationX(getTranslationX() + deltaX); + setTranslationY(getTranslationY() + deltaY); + } + } + }); + + mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height); + setDragRegion(new Rect(0, 0, width, height)); + + // The point in our scaled bitmap that the touch events are located + mRegistrationX = registrationX; + mRegistrationY = registrationY; + + // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass + int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + measure(ms, ms); + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + } + + public float getOffsetY() { + return mOffsetY; + } + + public int getDragRegionLeft() { + return mDragRegion.left; + } + + public int getDragRegionTop() { + return mDragRegion.top; + } + + public int getDragRegionWidth() { + return mDragRegion.width(); + } + + public int getDragRegionHeight() { + return mDragRegion.height(); + } + + public void setDragVisualizeOffset(Point p) { + mDragVisualizeOffset = p; + } + + public Point getDragVisualizeOffset() { + return mDragVisualizeOffset; + } + + public void setDragRegion(Rect r) { + mDragRegion = r; + } + + public Rect getDragRegion() { + return mDragRegion; + } + + public float getInitialScale() { + return mInitialScale; + } + + public void updateInitialScaleToCurrentScale() { + mInitialScale = getScaleX(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight()); + } + + @Override + protected void onDraw(Canvas canvas) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + Paint p = new Paint(); + p.setStyle(Paint.Style.FILL); + p.setColor(0x66ffffff); + canvas.drawRect(0, 0, getWidth(), getHeight(), p); + } + + mHasDrawn = true; + boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null; + if (crossFade) { + int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255; + mPaint.setAlpha(alpha); + } + canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint); + if (crossFade) { + mPaint.setAlpha((int) (255 * mCrossFadeProgress)); + canvas.save(); + float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth(); + float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight(); + canvas.scale(sX, sY); + canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint); + canvas.restore(); + } + } + + public void setCrossFadeBitmap(Bitmap crossFadeBitmap) { + mCrossFadeBitmap = crossFadeBitmap; + } + + public void crossFade(int duration) { + ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1f); + va.setDuration(duration); + va.setInterpolator(new DecelerateInterpolator(1.5f)); + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mCrossFadeProgress = animation.getAnimatedFraction(); + } + }); + va.start(); + } + + public void setColor(int color) { + if (mPaint == null) { + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + } + if (color != 0) { + mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + } else { + mPaint.setColorFilter(null); + } + invalidate(); + } + + public boolean hasDrawn() { + return mHasDrawn; + } + + @Override + public void setAlpha(float alpha) { + super.setAlpha(alpha); + mPaint.setAlpha((int) (255 * alpha)); + invalidate(); + } + + /** + * Create a window containing this view and show it. + * + * @param windowToken obtained from v.getWindowToken() from one of your views + * @param touchX the x coordinate the user touched in DragLayer coordinates + * @param touchY the y coordinate the user touched in DragLayer coordinates + */ + public void show(int touchX, int touchY) { + mDragLayer.addView(this); + + // Start the pick-up animation + DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0); + lp.width = mBitmap.getWidth(); + lp.height = mBitmap.getHeight(); + lp.customPosition = true; + setLayoutParams(lp); + setTranslationX(touchX - mRegistrationX); + setTranslationY(touchY - mRegistrationY); + // Post the animation to skip other expensive work happening on the first frame + post(new Runnable() { + public void run() { + mAnim.start(); + } + }); + } + + public void cancelAnimation() { + if (mAnim != null && mAnim.isRunning()) { + mAnim.cancel(); + } + } + + public void resetLayoutParams() { + mOffsetX = mOffsetY = 0; + requestLayout(); + } + + /** + * Move the window containing this view. + * + * @param touchX the x coordinate the user touched in DragLayer coordinates + * @param touchY the y coordinate the user touched in DragLayer coordinates + */ + void move(int touchX, int touchY) { + setTranslationX(touchX - mRegistrationX + (int) mOffsetX); + setTranslationY(touchY - mRegistrationY + (int) mOffsetY); + } + + void remove() { + if (getParent() != null) { + mDragLayer.removeView(DragView.this); + } + } +} + diff --git a/app/src/main/java/com/android/launcher2/DrawableStateProxyView.java b/app/src/main/java/com/android/launcher2/DrawableStateProxyView.java new file mode 100644 index 0000000..5d2f6e0 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DrawableStateProxyView.java @@ -0,0 +1,69 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.launcher.R; + +public class DrawableStateProxyView extends LinearLayout { + + private View mView; + private int mViewId; + + public DrawableStateProxyView(Context context) { + this(context, null); + } + + public DrawableStateProxyView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + + public DrawableStateProxyView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DrawableStateProxyView, + defStyle, 0); + mViewId = a.getResourceId(R.styleable.DrawableStateProxyView_sourceViewId, -1); + a.recycle(); + + setFocusable(false); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (mView == null) { + View parent = (View) getParent(); + mView = parent.findViewById(mViewId); + } + mView.setPressed(isPressed()); + mView.setHovered(isHovered()); + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + return false; + } +} diff --git a/app/src/main/java/com/android/launcher2/DropTarget.java b/app/src/main/java/com/android/launcher2/DropTarget.java new file mode 100644 index 0000000..d627a4c --- /dev/null +++ b/app/src/main/java/com/android/launcher2/DropTarget.java @@ -0,0 +1,184 @@ +/* + * 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.launcher2; + +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. + * + */ +public interface DropTarget { + + public static final String TAG = "DropTarget"; + + class DragObject { + public int x = -1; + public int y = -1; + + /** X offset from the upper-left corner of the cell to where we touched. */ + public int xOffset = -1; + + /** Y offset from the upper-left corner of the cell to where we touched. */ + public int yOffset = -1; + + /** This indicates whether a drag is in final stages, either drop or cancel. It + * differentiates onDragExit, since this is called when the drag is ending, above + * the current drag target, or when the drag moves off the current drag object. + */ + public boolean dragComplete = false; + + /** The view that moves around while you drag. */ + public DragView dragView = null; + + /** The data associated with the object being dragged */ + public Object dragInfo = null; + + /** Where the drag originated */ + public DragSource dragSource = null; + + /** Post drag animation runnable */ + public Runnable postAnimationRunnable = null; + + /** Indicates that the drag operation was cancelled */ + public boolean cancelled = false; + + /** Defers removing the DragView from the DragLayer until after the drop animation. */ + public boolean deferDragViewCleanupPostAnimation = true; + + public DragObject() { + } + } + + public static class DragEnforcer implements DragController.DragListener { + int dragParity = 0; + + public DragEnforcer(Context context) { + Launcher launcher = (Launcher) context; + launcher.getDragController().addDragListener(this); + } + + void onDragEnter() { + dragParity++; + if (dragParity != 1) { + Log.e(TAG, "onDragEnter: Drag contract violated: " + dragParity); + } + } + + 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); + } + } + } + + /** + * Used to temporarily disable certain drop targets + * + * @return boolean specifying whether this drop target is currently enabled + */ + boolean isDropEnabled(); + + /** + * 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 + * @param xOffset Horizontal offset with the object being dragged where the original + * touch happened + * @param yOffset Vertical offset with the object being dragged where the original + * touch happened + * @param dragView The DragView that's being dragged around on screen. + * @param dragInfo Data associated with the object being dragged + * + */ + void onDrop(DragObject dragObject); + + void onDragEnter(DragObject dragObject); + + void onDragOver(DragObject dragObject); + + void onDragExit(DragObject dragObject); + + /** + * Handle an object being dropped as a result of flinging to delete and will be called in place + * 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); + + /** + * Allows a DropTarget to delegate drag and drop events to another object. + * + * Most subclasses will should just return null from this method. + * + * @param source DragSource where the drag started + * @param x X coordinate of the drop location + * @param y Y coordinate of the drop location + * @param xOffset Horizontal offset with the object being dragged where the original + * touch happened + * @param yOffset Vertical offset with the object being dragged where the original + * touch happened + * @param dragView The DragView that's being dragged around on screen. + * @param dragInfo Data associated with the object being dragged + * + * @return The DropTarget to delegate to, or null to not delegate to another object. + */ + DropTarget getDropTargetDelegate(DragObject dragObject); + + /** + * 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 + * @param xOffset Horizontal offset with the object being dragged where the + * original touch happened + * @param yOffset Vertical offset with the object being dragged where the + * original touch happened + * @param dragView The DragView that's being dragged around on screen. + * @param dragInfo Data associated with the object being dragged + * @return True if the drop will be accepted, false otherwise. + */ + boolean acceptDrop(DragObject dragObject); + + // These methods are implemented in Views + void getHitRect(Rect outRect); + void getLocationInDragLayer(int[] loc); + int getLeft(); + int getTop(); +} diff --git a/app/src/main/java/com/android/launcher2/FastBitmapDrawable.java b/app/src/main/java/com/android/launcher2/FastBitmapDrawable.java new file mode 100644 index 0000000..3c39d27 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/FastBitmapDrawable.java @@ -0,0 +1,109 @@ +/* + * 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.launcher2; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +class FastBitmapDrawable extends Drawable { + private Bitmap mBitmap; + private int mAlpha; + private int mWidth; + private int mHeight; + private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + + FastBitmapDrawable(Bitmap b) { + mAlpha = 255; + mBitmap = b; + if (b != null) { + mWidth = mBitmap.getWidth(); + mHeight = mBitmap.getHeight(); + } else { + mWidth = mHeight = 0; + } + } + + @Override + public void draw(Canvas canvas) { + final Rect r = getBounds(); + // Draw the bitmap into the bounding rect + canvas.drawBitmap(mBitmap, null, r, mPaint); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + mAlpha = alpha; + mPaint.setAlpha(alpha); + } + + public void setFilterBitmap(boolean filterBitmap) { + mPaint.setFilterBitmap(filterBitmap); + } + + public int getAlpha() { + return mAlpha; + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + + @Override + public int getMinimumWidth() { + return mWidth; + } + + @Override + public int getMinimumHeight() { + return mHeight; + } + + public void setBitmap(Bitmap b) { + mBitmap = b; + if (b != null) { + mWidth = mBitmap.getWidth(); + mHeight = mBitmap.getHeight(); + } else { + mWidth = mHeight = 0; + } + } + + public Bitmap getBitmap() { + return mBitmap; + } +} diff --git a/app/src/main/java/com/android/launcher2/FirstFrameAnimatorHelper.java b/app/src/main/java/com/android/launcher2/FirstFrameAnimatorHelper.java new file mode 100644 index 0000000..3815486 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/FirstFrameAnimatorHelper.java @@ -0,0 +1,141 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.util.Log; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver; + +/* + * 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 + * prevent jank at the beginning of the animation + */ +public class FirstFrameAnimatorHelper extends AnimatorListenerAdapter + implements ValueAnimator.AnimatorUpdateListener { + private static final boolean DEBUG = false; + private static final int MAX_DELAY = 1000; + private static final int IDEAL_FRAME_DURATION = 16; + private View mTarget; + private long mStartFrame; + private long mStartTime = -1; + private boolean mHandlingOnAnimationUpdate; + private boolean mAdjustedSecondFrameTime; + + private static ViewTreeObserver.OnDrawListener sGlobalDrawListener; + private static long sGlobalFrameCounter; + private static boolean sVisible; + + public FirstFrameAnimatorHelper(ValueAnimator animator, View target) { + mTarget = target; + animator.addUpdateListener(this); + } + + public FirstFrameAnimatorHelper(ViewPropertyAnimator vpa, View target) { + mTarget = target; + vpa.setListener(this); + } + + // only used for ViewPropertyAnimators + public void onAnimationStart(Animator animation) { + final ValueAnimator va = (ValueAnimator) animation; + va.addUpdateListener(FirstFrameAnimatorHelper.this); + onAnimationUpdate(va); + } + + public static void setIsVisible(boolean visible) { + sVisible = visible; + } + + public static void initializeDrawListener(View view) { + if (sGlobalDrawListener != null) { + view.getViewTreeObserver().removeOnDrawListener(sGlobalDrawListener); + } + sGlobalDrawListener = new ViewTreeObserver.OnDrawListener() { + private long mTime = System.currentTimeMillis(); + public void onDraw() { + sGlobalFrameCounter++; + if (DEBUG) { + long newTime = System.currentTimeMillis(); + Log.d("FirstFrameAnimatorHelper", "TICK " + (newTime - mTime)); + mTime = newTime; + } + } + }; + view.getViewTreeObserver().addOnDrawListener(sGlobalDrawListener); + sVisible = true; + } + + public void onAnimationUpdate(final ValueAnimator animation) { + final long currentTime = System.currentTimeMillis(); + if (mStartTime == -1) { + mStartFrame = sGlobalFrameCounter; + mStartTime = currentTime; + } + + if (!mHandlingOnAnimationUpdate && + sVisible && + // If the current play time exceeds the duration, the animation + // will get finished, even if we call setCurrentPlayTime -- therefore + // don't adjust the animation in that case + animation.getCurrentPlayTime() < animation.getDuration()) { + mHandlingOnAnimationUpdate = true; + long frameNum = sGlobalFrameCounter - mStartFrame; + // If we haven't drawn our first frame, reset the time to t = 0 + // (give up after MAX_DELAY ms of waiting though - might happen, for example, if we + // are no longer in the foreground and no frames are being rendered ever) + if (frameNum == 0 && currentTime < mStartTime + MAX_DELAY) { + // The first frame on animations doesn't always trigger an invalidate... + // force an invalidate here to make sure the animation continues to advance + mTarget.getRootView().invalidate(); + animation.setCurrentPlayTime(0); + + // For the second frame, if the first frame took more than 16ms, + // adjust the start time and pretend it took only 16ms anyway. This + // prevents a large jump in the animation due to an expensive first frame + } else if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY && + !mAdjustedSecondFrameTime && + currentTime > mStartTime + IDEAL_FRAME_DURATION) { + animation.setCurrentPlayTime(IDEAL_FRAME_DURATION); + mAdjustedSecondFrameTime = true; + } else { + if (frameNum > 1) { + mTarget.post(new Runnable() { + public void run() { + animation.removeUpdateListener(FirstFrameAnimatorHelper.this); + } + }); + } + if (DEBUG) print(animation); + } + mHandlingOnAnimationUpdate = false; + } else { + if (DEBUG) print(animation); + } + } + + public void print(ValueAnimator animation) { + float flatFraction = animation.getCurrentPlayTime() / (float) animation.getDuration(); + Log.d("FirstFrameAnimatorHelper", sGlobalFrameCounter + + "(" + (sGlobalFrameCounter - mStartFrame) + ") " + mTarget + " dirty? " + + mTarget.isDirty() + " " + flatFraction + " " + this + " " + animation); + } +} diff --git a/app/src/main/java/com/android/launcher2/FocusHelper.java b/app/src/main/java/com/android/launcher2/FocusHelper.java new file mode 100644 index 0000000..e9f986d --- /dev/null +++ b/app/src/main/java/com/android/launcher2/FocusHelper.java @@ -0,0 +1,898 @@ +/* + * 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.launcher2; + +import android.content.res.Configuration; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.TabHost; +import android.widget.TabWidget; + +import com.android.launcher.R; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * A keyboard listener we set on all the workspace icons. + */ +class IconKeyEventListener implements View.OnKeyListener { + 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 { + public boolean onKey(View v, int keyCode, KeyEvent event) { + final Configuration configuration = v.getResources().getConfiguration(); + return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation); + } +} + +/** + * A keyboard listener we set on the last tab button in AppsCustomize to jump to then + * market icon and vice versa. + */ +class AppsCustomizeTabKeyEventListener implements View.OnKeyListener { + public boolean onKey(View v, int keyCode, KeyEvent event) { + return FocusHelper.handleAppsCustomizeTabKeyEvent(v, keyCode, event); + } +} + +public class FocusHelper { + /** + * Private helper to get the parent TabHost in the view hiearchy. + */ + private static TabHost findTabHostParent(View v) { + ViewParent p = v.getParent(); + while (p != null && !(p instanceof TabHost)) { + p = p.getParent(); + } + return (TabHost) p; + } + + /** + * Handles key events in a AppsCustomize tab between the last tab view and the shop button. + */ + static boolean handleAppsCustomizeTabKeyEvent(View v, int keyCode, KeyEvent e) { + final TabHost tabHost = findTabHostParent(v); + final ViewGroup contents = tabHost.getTabContentView(); + final View shop = tabHost.findViewById(R.id.market_button); + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the shop button if we aren't on it + if (v != shop) { + shop.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the content view (down is handled by the tab key handler otherwise) + if (v == shop) { + contents.requestFocus(); + wasHandled = true; + } + } + break; + default: break; + } + return wasHandled; + } + + /** + * Returns the Viewgroup containing page contents for the page at the index specified. + */ + private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) { + ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index); + if (page instanceof PagedViewCellLayout) { + // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren + page = (ViewGroup) page.getChildAt(0); + } + return page; + } + + /** + * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets. + */ + static boolean handlePagedViewGridLayoutWidgetKeyEvent(PagedViewWidget w, int keyCode, + KeyEvent e) { + + final PagedViewGridLayout parent = (PagedViewGridLayout) w.getParent(); + final PagedView container = (PagedView) parent.getParent(); + final TabHost tabHost = findTabHostParent(container); + final TabWidget tabs = tabHost.getTabWidget(); + final int widgetIndex = parent.indexOfChild(w); + final int widgetCount = parent.getChildCount(); + final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parent)); + final int pageCount = container.getChildCount(); + final int cellCountX = parent.getCellCountX(); + final int cellCountY = parent.getCellCountY(); + final int x = widgetIndex % cellCountX; + final int y = widgetIndex / cellCountX; + + final int action = e.getAction(); + final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP); + ViewGroup newParent = null; + // Now that we load items in the bg asynchronously, we can't just focus + // child siblings willy-nilly + View child = null; + boolean wasHandled = false; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (handleKeyEvent) { + // Select the previous widget or the last widget on the previous page + if (widgetIndex > 0) { + parent.getChildAt(widgetIndex - 1).requestFocus(); + } else { + if (pageIndex > 0) { + newParent = getAppsCustomizePage(container, pageIndex - 1); + if (newParent != null) { + child = newParent.getChildAt(newParent.getChildCount() - 1); + if (child != null) child.requestFocus(); + } + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next widget or the first widget on the next page + if (widgetIndex < (widgetCount - 1)) { + parent.getChildAt(widgetIndex + 1).requestFocus(); + } else { + if (pageIndex < (pageCount - 1)) { + newParent = getAppsCustomizePage(container, pageIndex + 1); + if (newParent != null) { + child = newParent.getChildAt(0); + if (child != null) child.requestFocus(); + } + } + } + } + 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 newWidgetIndex = ((y - 1) * cellCountX) + x; + child = parent.getChildAt(newWidgetIndex); + if (child != null) child.requestFocus(); + } else { + tabs.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the closest icon in the previous row, otherwise do nothing + if (y < (cellCountY - 1)) { + int newWidgetIndex = Math.min(widgetCount - 1, ((y + 1) * cellCountX) + x); + child = parent.getChildAt(newWidgetIndex); + if (child != null) child.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (handleKeyEvent) { + // Simulate a click on the widget + View.OnClickListener clickListener = (View.OnClickListener) container; + clickListener.onClick(w); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_UP: + if (handleKeyEvent) { + // Select the first item on the previous page, or the first item on this page + // if there is no previous page + if (pageIndex > 0) { + newParent = getAppsCustomizePage(container, pageIndex - 1); + if (newParent != null) { + child = newParent.getChildAt(0); + } + } else { + child = parent.getChildAt(0); + } + if (child != null) child.requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + if (handleKeyEvent) { + // Select the first item on the next page, or the last item on this page + // if there is no next page + if (pageIndex < (pageCount - 1)) { + newParent = getAppsCustomizePage(container, pageIndex + 1); + if (newParent != null) { + child = newParent.getChildAt(0); + } + } else { + child = parent.getChildAt(widgetCount - 1); + } + if (child != null) child.requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first item on this page + child = parent.getChildAt(0); + if (child != null) child.requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last item on this page + parent.getChildAt(widgetCount - 1).requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events in a PageViewCellLayout containing PagedViewIcons. + */ + static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) { + ViewGroup parentLayout; + ViewGroup itemContainer; + int countX; + int countY; + if (v.getParent() instanceof PagedViewCellLayoutChildren) { + itemContainer = (ViewGroup) v.getParent(); + parentLayout = (ViewGroup) itemContainer.getParent(); + countX = ((PagedViewCellLayout) parentLayout).getCellCountX(); + countY = ((PagedViewCellLayout) parentLayout).getCellCountY(); + } else { + itemContainer = parentLayout = (ViewGroup) v.getParent(); + countX = ((PagedViewGridLayout) parentLayout).getCellCountX(); + countY = ((PagedViewGridLayout) parentLayout).getCellCountY(); + } + + // Note we have an extra parent because of the + // PagedViewCellLayout/PagedViewCellLayoutChildren relationship + final PagedView container = (PagedView) parentLayout.getParent(); + final TabHost tabHost = findTabHostParent(container); + final TabWidget tabs = tabHost.getTabWidget(); + 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(); + + 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(); + } 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(); + } + } + } + } + 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(); + } 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(); + } + } + } + } + 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(); + } else { + tabs.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the closest icon in the previous row, otherwise do nothing + if (y < (countY - 1)) { + int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x); + itemContainer.getChildAt(newiconIndex).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (handleKeyEvent) { + // Simulate a click on the icon + View.OnClickListener clickListener = (View.OnClickListener) container; + clickListener.onClick(v); + } + 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(); + } + } else { + itemContainer.getChildAt(0).requestFocus(); + } + } + 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(); + } + } else { + itemContainer.getChildAt(itemCount - 1).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_HOME: + if (handleKeyEvent) { + // Select the first icon on this page + itemContainer.getChildAt(0).requestFocus(); + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_MOVE_END: + if (handleKeyEvent) { + // Select the last icon on this page + itemContainer.getChildAt(itemCount - 1).requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events in the tab widget. + */ + static boolean handleTabKeyEvent(AccessibleTabView v, int keyCode, KeyEvent e) { + if (!LauncherApplication.isScreenLarge()) return false; + + final FocusOnlyTabWidget parent = (FocusOnlyTabWidget) v.getParent(); + final TabHost tabHost = findTabHostParent(parent); + final ViewGroup contents = tabHost.getTabContentView(); + final int tabCount = parent.getTabCount(); + final int tabIndex = parent.getChildTabIndex(v); + + 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 tab + if (tabIndex > 0) { + parent.getChildTabViewAt(tabIndex - 1).requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next tab, or if the last tab has a focus right id, select that + if (tabIndex < (tabCount - 1)) { + parent.getChildTabViewAt(tabIndex + 1).requestFocus(); + } else { + if (v.getNextFocusRightId() != View.NO_ID) { + tabHost.findViewById(v.getNextFocusRightId()).requestFocus(); + } + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + // Do nothing + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (handleKeyEvent) { + // Select the content view + contents.requestFocus(); + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Handles key events in the workspace hotseat (bottom of the screen). + */ + static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) { + final ViewGroup parent = (ViewGroup) v.getParent(); + final ViewGroup launcher = (ViewGroup) parent.getParent(); + final Workspace workspace = (Workspace) launcher.findViewById(R.id.workspace); + final int buttonIndex = parent.indexOfChild(v); + final int buttonCount = parent.getChildCount(); + final int pageIndex = workspace.getCurrentPage(); + + // 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) { + // Select the previous button, otherwise snap to the previous page + if (buttonIndex > 0) { + parent.getChildAt(buttonIndex - 1).requestFocus(); + } else { + workspace.snapToPage(pageIndex - 1); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (handleKeyEvent) { + // Select the next button, otherwise snap to the next page + if (buttonIndex < (buttonCount - 1)) { + parent.getChildAt(buttonIndex + 1).requestFocus(); + } else { + workspace.snapToPage(pageIndex + 1); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (handleKeyEvent) { + // Select the first bubble text view in the current page of the workspace + final CellLayout layout = (CellLayout) workspace.getChildAt(pageIndex); + final ShortcutAndWidgetContainer children = layout.getShortcutsAndWidgets(); + final View newIcon = getIconInDirection(layout, children, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } else { + workspace.requestFocus(); + } + } + wasHandled = true; + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + // Do nothing + wasHandled = true; + break; + default: break; + } + return wasHandled; + } + + /** + * Private helper method to get the CellLayoutChildren given a CellLayout index. + */ + private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( + ViewGroup container, int i) { + ViewGroup parent = (ViewGroup) container.getChildAt(i); + return (ShortcutAndWidgetContainer) parent.getChildAt(0); + } + + /** + * Private helper method to sort all the CellLayout children in order of their (x,y) spatially + * from top left to bottom right. + */ + 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.qsb_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; + 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(); + } 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); + } + } + } + } + wasHandled = true; + 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(); + } 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); + } + } + } + } + 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(); + } + } + 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(); + wasHandled = true; + } else if (hotseat != null) { + hotseat.requestFocus(); + } + } + 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); + } + } else { + View newIcon = getIconInDirection(layout, parent, -1, 1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + } + 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); + } + } else { + View newIcon = getIconInDirection(layout, parent, + parent.getChildCount(), -1); + if (newIcon != null) { + newIcon.requestFocus(); + } + } + } + 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(); + } + } + 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(); + } + } + 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 Folder folder = (Folder) layout.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(); + } + } + 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(); + } + } + wasHandled = true; + 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(); + } + } + 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(); + } + } + 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(); + } + } + 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(); + } + } + wasHandled = true; + break; + default: break; + } + return wasHandled; + } +} diff --git a/app/src/main/java/com/android/launcher2/FocusOnlyTabWidget.java b/app/src/main/java/com/android/launcher2/FocusOnlyTabWidget.java new file mode 100644 index 0000000..8e9f58c --- /dev/null +++ b/app/src/main/java/com/android/launcher2/FocusOnlyTabWidget.java @@ -0,0 +1,86 @@ +/* + * 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.launcher2; + +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/app/src/main/java/com/android/launcher2/Folder.java b/app/src/main/java/com/android/launcher2/Folder.java new file mode 100644 index 0000000..1692b0e --- /dev/null +++ b/app/src/main/java/com/android/launcher2/Folder.java @@ -0,0 +1,1114 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.InputType; +import android.text.Selection; +import android.text.Spannable; +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.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher.R; +import com.android.launcher2.FolderInfo.FolderListener; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * Represents a set of icons chosen by the user or generated by the system. + */ +public class Folder extends LinearLayout implements DragSource, View.OnClickListener, + View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, + View.OnFocusChangeListener { + private static final String TAG = "Launcher.Folder"; + + protected DragController mDragController; + protected Launcher mLauncher; + protected FolderInfo mInfo; + + 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 int mExpandDuration; + protected CellLayout mContent; + 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 ON_EXIT_CLOSE_DELAY = 800; + private boolean mRearrangeOnClose = false; + private FolderIcon mFolderIcon; + private int mMaxCountX; + private int mMaxCountY; + private int mMaxNumItems; + private ArrayList mItemsInReadingOrder = new ArrayList(); + private Drawable mIconDrawable; + boolean mItemsInvalidated = false; + private ShortcutInfo mCurrentDragInfo; + private View mCurrentDragView; + 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; + + private boolean mIsEditingName = false; + private InputMethodManager mInputMethodManager; + + private static String sDefaultFolderName; + private static String sHintText; + + private boolean mDestroyed; + + /** + * Used to inflate the Workspace from XML. + * + * @param context The application's context. + * @param attrs The attribtues set containing the Workspace's customization values. + */ + public Folder(Context context, AttributeSet attrs) { + super(context, attrs); + setAlwaysDrawnWithCacheEnabled(false); + mInflater = LayoutInflater.from(context); + mIconCache = ((LauncherApplication)context.getApplicationContext()).getIconCache(); + + Resources res = getResources(); + mMaxCountX = res.getInteger(R.integer.folder_max_count_x); + mMaxCountY = res.getInteger(R.integer.folder_max_count_y); + mMaxNumItems = res.getInteger(R.integer.folder_max_num_items); + if (mMaxCountX < 0 || mMaxCountY < 0 || mMaxNumItems < 0) { + mMaxCountX = LauncherModel.getCellCountX(); + mMaxCountY = LauncherModel.getCellCountY(); + mMaxNumItems = mMaxCountX * mMaxCountY; + } + + mInputMethodManager = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + mExpandDuration = res.getInteger(R.integer.config_folderAnimDuration); + + if (sDefaultFolderName == null) { + sDefaultFolderName = ""; + } + if (sHintText == null) { + sHintText = res.getString(R.string.folder_hint_text); + } + 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). + setFocusableInTouchMode(true); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mContent = (CellLayout) findViewById(R.id.folder_content); + 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); + } + + private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + public void onDestroyActionMode(ActionMode mode) { + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + }; + + public void onClick(View v) { + Object tag = v.getTag(); + if (tag instanceof ShortcutInfo) { + // refactor this code from Folder + ShortcutInfo item = (ShortcutInfo) tag; + int[] pos = new int[2]; + v.getLocationOnScreen(pos); + item.intent.setSourceBounds(new Rect(pos[0], pos[1], + pos[0] + v.getWidth(), pos[1] + v.getHeight())); + + mLauncher.startActivitySafely(v, item.intent, item); + } + } + + public boolean onLongClick(View v) { + // Return if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return true; + + Object tag = v.getTag(); + if (tag instanceof ShortcutInfo) { + ShortcutInfo item = (ShortcutInfo) tag; + if (!v.isInTouchMode()) { + return false; + } + + mLauncher.dismissFolderCling(null); + + mLauncher.getWorkspace().onDragStartedWithItem(v); + mLauncher.getWorkspace().beginDragShared(v, this); + mIconDrawable = ((TextView) v).getCompoundDrawables()[1]; + + mCurrentDragInfo = item; + mEmptyCell[0] = item.cellX; + mEmptyCell[1] = item.cellY; + mCurrentDragView = v; + + mContent.removeView(mCurrentDragView); + mInfo.remove(mCurrentDragInfo); + mDragInProgress = true; + mItemAddedBackToSelfViaIcon = false; + } + return true; + } + + public boolean isEditingName() { + return mIsEditingName; + } + + public void startEditingFolderName() { + mFolderName.setHint(""); + mIsEditingName = true; + } + + public void dismissEditingName() { + mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + doneEditingFolderName(true); + } + + public void doneEditingFolderName(boolean commit) { + mFolderName.setHint(sHintText); + // Convert to a string here to ensure that no other state associated with the text field + // gets saved. + String newTitle = mFolderName.getText().toString(); + mInfo.setTitle(newTitle); + LauncherModel.updateItemInDatabase(mLauncher, mInfo); + + if (commit) { + sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + String.format(getContext().getString(R.string.folder_renamed), newTitle)); + } + // In order to clear the focus from the text field, we set the focus on ourself. This + // ensures that every time the field is clicked, focus is gained, giving reliable behavior. + requestFocus(); + + Selection.setSelection((Spannable) mFolderName.getText(), 0, 0); + mIsEditingName = false; + } + + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + dismissEditingName(); + return true; + } + return false; + } + + public View getEditTextRegion() { + return mFolderName; + } + + public Drawable getDragDrawable() { + return mIconDrawable; + } + + /** + * We need to handle touch events to prevent them from falling through to the workspace below. + */ + @Override + public boolean onTouchEvent(MotionEvent ev) { + return true; + } + + public void setDragController(DragController dragController) { + mDragController = dragController; + } + + void setFolderIcon(FolderIcon icon) { + mFolderIcon = icon; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // When the folder gets focus, we don't want to announce the list of items. + return true; + } + + /** + * @return the FolderInfo object associated with this folder + */ + 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)) { + overflow.add(child); + } else { + count++; + } + } + + // We rearrange the items in case there are any empty gaps + setupContentForNumItems(count); + + // 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 + // number of items. + for (ShortcutInfo item: overflow) { + mInfo.remove(item); + LauncherModel.deleteItemFromDatabase(mLauncher, item); + } + + mItemsInvalidated = true; + updateTextViewFocus(); + mInfo.addListener(this); + + if (!sDefaultFolderName.contentEquals(mInfo.title)) { + mFolderName.setText(mInfo.title); + } else { + mFolderName.setText(""); + } + updateItemLocationsInDatabase(); + } + + /** + * Creates a new UserFolder, inflated from R.layout.user_folder. + * + * @param context The application's context. + * + * @return A new UserFolder. + */ + static Folder fromXml(Context context) { + return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null); + } + + /** + * This method is intended to make the UserFolder to be visually identical in size and position + * to its associated FolderIcon. This allows for a seamless transition into the expanded state. + */ + private void positionAndSizeAsIcon() { + if (!(getParent() instanceof DragLayer)) return; + setScaleX(0.8f); + setScaleY(0.8f); + setAlpha(0f); + mState = STATE_SMALL; + } + + public void animateOpen() { + positionAndSizeAsIcon(); + + if (!(getParent() instanceof DragLayer)) return; + centerAboutIcon(); + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); + final ObjectAnimator oa = + LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); + + oa.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + String.format(getContext().getString(R.string.folder_opened), + mContent.getCountX(), mContent.getCountY())); + mState = STATE_ANIMATING; + } + @Override + public void onAnimationEnd(Animator animation) { + mState = STATE_OPEN; + setLayerType(LAYER_TYPE_NONE, null); + Cling cling = mLauncher.showFirstRunFoldersCling(); + if (cling != null) { + cling.bringToFront(); + } + setFocusOnFirstChild(); + } + }); + oa.setDuration(mExpandDuration); + setLayerType(LAYER_TYPE_HARDWARE, null); + oa.start(); + } + + private void sendCustomAccessibilityEvent(int type, String text) { + AccessibilityManager accessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(type); + onInitializeAccessibilityEvent(event); + event.getText().add(text); + accessibilityManager.sendAccessibilityEvent(event); + } + } + + 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); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); + final ObjectAnimator oa = + LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); + + oa.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onCloseComplete(); + setLayerType(LAYER_TYPE_NONE, null); + mState = STATE_SMALL; + } + @Override + public void onAnimationStart(Animator animation) { + sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + getContext().getString(R.string.folder_closed)); + mState = STATE_ANIMATING; + } + }); + oa.setDuration(mExpandDuration); + setLayerType(LAYER_TYPE_HARDWARE, null); + oa.start(); + } + + void notifyDataSetChanged() { + // recreate all the children if the data set changes under us. We may want to do this more + // intelligently (ie just removing the views that should no longer exist) + mContent.removeAllViewsInLayout(); + bind(mInfo); + } + + public boolean acceptDrop(DragObject d) { + final ItemInfo item = (ItemInfo) d.dragInfo; + final int itemType = item.itemType; + return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || + itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && + !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 boolean createAndAddShortcut(ShortcutInfo item) { + final TextView textView = + (TextView) mInflater.inflate(R.layout.application, this, false); + textView.setCompoundDrawablesWithIntrinsicBounds(null, + new FastBitmapDrawable(item.getIcon(mIconCache)), null, null); + textView.setText(item.title); + textView.setTag(item); + + textView.setOnClickListener(this); + textView.setOnLongClickListener(this); + + // 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 false; + } + } + + 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 true; + } + + public void onDragEnter(DragObject d) { + mPreviousTargetCell[0] = -1; + mPreviousTargetCell[1] = -1; + mOnExitAlarm.cancelAlarm(); + } + + OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { + public void onAlarm(Alarm alarm) { + realTimeReorder(mEmptyCell, mTargetCell); + } + }; + + 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; + } + } + } + } + } + + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + public void onDragOver(DragObject d) { + float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null); + mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1], 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(150); + mPreviousTargetCell[0] = mTargetCell[0]; + mPreviousTargetCell[1] = mTargetCell[1]; + } + } + + // 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; + } + + // 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; + } + + OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { + public void onAlarm(Alarm alarm) { + completeDragExit(); + } + }; + + public void completeDragExit() { + mLauncher.closeFolder(); + mCurrentDragInfo = null; + mCurrentDragView = null; + mSuppressOnAdd = false; + mRearrangeOnClose = true; + } + + public void onDragExit(DragObject d) { + // 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) { + mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener); + mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); + } + mReorderAlarm.cancelAlarm(); + } + + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + if (success) { + if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon) { + replaceFolderWithFinalItem(); + } + } else { + setupContentForNumItems(getItemCount()); + // The drag failed, we need to return the item to the folder + mFolderIcon.onDrop(d); + } + + if (target != this) { + if (mOnExitAlarm.alarmPending()) { + mOnExitAlarm.cancelAlarm(); + if (!success) { + mSuppressFolderDeletion = true; + } + completeDragExit(); + } + } + + mDeleteFolderOnDropCompleted = false; + mDragInProgress = false; + mItemAddedBackToSelfViaIcon = false; + mCurrentDragInfo = null; + mCurrentDragView = null; + mSuppressOnAdd = false; + + // 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. + updateItemLocationsInDatabase(); + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + @Override + public void onFlingToDeleteCompleted() { + // 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); + } + } + + public void notifyDrop() { + if (mDragInProgress) { + mItemAddedBackToSelfViaIcon = true; + } + } + + public boolean isDropEnabled() { + return true; + } + + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + 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; + } + + private void centerAboutIcon() { + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + + int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight() + + mFolderNameHeight; + DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer); + + float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect); + + int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2); + int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2); + int centeredLeft = centerX - width / 2; + int centeredTop = centerY - height / 2; + + int currentPage = mLauncher.getWorkspace().getCurrentPage(); + // 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 the folder doesn't fit within the bounds, center it about the desired bounds + if (width >= bounds.width()) { + left = bounds.left + (bounds.width() - width) / 2; + } + if (height >= bounds.height()) { + top = bounds.top + (bounds.height() - height) / 2; + } + + int folderPivotX = width / 2 + (centeredLeft - left); + int folderPivotY = height / 2 + (centeredTop - top); + setPivotX(folderPivotX); + setPivotY(folderPivotY); + mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() * + (1.0f * folderPivotX / width)); + mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * + (1.0f * folderPivotY / height)); + + lp.width = width; + lp.height = height; + lp.x = left; + lp.y = top; + } + + float getPivotXForIconAnimation() { + return mFolderIconPivotX; + } + float getPivotYForIconAnimation() { + 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(); + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight() + + mFolderNameHeight; + + int contentWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(), + MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredHeight(), + MeasureSpec.EXACTLY); + mContent.measure(contentWidthSpec, contentHeightSpec); + + mFolderName.measure(contentWidthSpec, + MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); + setMeasuredDimension(width, height); + } + + private void arrangeChildren(ArrayList list) { + int[] vacant = new int[2]; + if (list == null) { + list = getItemsInReadingOrder(); + } + mContent.removeAllViews(); + + 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); + } + mItemsInvalidated = true; + } + + public int getItemCount() { + return mContent.getShortcutsAndWidgets().getChildCount(); + } + + public View getItemAt(int index) { + return mContent.getShortcutsAndWidgets().getChildAt(index); + } + + private void onCloseComplete() { + DragLayer parent = (DragLayer) getParent(); + if (parent != null) { + parent.removeView(this); + } + mDragController.removeDropTarget((DropTarget) this); + clearFocus(); + mFolderIcon.requestFocus(); + + if (mRearrangeOnClose) { + setupContentForNumItems(getItemCount()); + mRearrangeOnClose = false; + } + if (getItemCount() <= 1) { + if (!mDragInProgress && !mSuppressFolderDeletion) { + replaceFolderWithFinalItem(); + } else if (mDragInProgress) { + mDeleteFolderOnDropCompleted = true; + } + } + mSuppressFolderDeletion = false; + } + + private void replaceFolderWithFinalItem() { + // Add the last remaining child to the workspace in place of the folder + Runnable onCompleteRunnable = new Runnable() { + @Override + public void run() { + CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screen); + + View child = null; + // 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); + LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, + mInfo.screen, mInfo.cellX, mInfo.cellY); + } + if (getItemCount() <= 1) { + // Remove the folder + LauncherModel.deleteItemFromDatabase(mLauncher, mInfo); + cellLayout.removeView(mFolderIcon); + if (mFolderIcon instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) mFolderIcon); + } + mLauncher.removeFolder(mInfo); + } + // We add the child after removing the folder to prevent both from existing at + // the same time in the CellLayout. + if (child != null) { + mLauncher.getWorkspace().addInScreen(child, mInfo.container, mInfo.screen, + mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY); + } + } + }; + View finalChild = getItemAt(0); + if (finalChild != null) { + mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); + } + mDestroyed = true; + } + + boolean isDestroyed() { + return mDestroyed; + } + + // 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); + if (lastChild != null) { + mFolderName.setNextFocusDownId(lastChild.getId()); + mFolderName.setNextFocusRightId(lastChild.getId()); + mFolderName.setNextFocusLeftId(lastChild.getId()); + mFolderName.setNextFocusUpId(lastChild.getId()); + } + } + + public void onDrop(DragObject d) { + ShortcutInfo item; + if (d.dragInfo instanceof ApplicationInfo) { + // Came from all apps -- make a copy + item = ((ApplicationInfo) d.dragInfo).makeShortcut(); + item.spanX = 1; + item.spanY = 1; + } else { + item = (ShortcutInfo) d.dragInfo; + } + // Dragged from self onto self, currently this is the only path possible, however + // we keep this as a distinct code path. + if (item == mCurrentDragInfo) { + ShortcutInfo si = (ShortcutInfo) mCurrentDragView.getTag(); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mCurrentDragView.getLayoutParams(); + si.cellX = lp.cellX = mEmptyCell[0]; + si.cellX = lp.cellY = mEmptyCell[1]; + mContent.addViewToCellLayout(mCurrentDragView, -1, (int)item.id, lp, true); + if (d.dragView.hasDrawn()) { + mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, mCurrentDragView); + } else { + d.deferDragViewCleanupPostAnimation = false; + mCurrentDragView.setVisibility(VISIBLE); + } + mItemsInvalidated = true; + setupContentDimensions(getItemCount()); + mSuppressOnAdd = true; + } + mInfo.add(item); + } + + // This is used so the item doesn't immediately appear in the folder when added. In one case + // we need to create the illusion that the item isn't added back to the folder yet, to + // to correspond to the animation of the icon back into the folder. This is + public void hideItem(ShortcutInfo info) { + View v = getViewForInfo(info); + v.setVisibility(INVISIBLE); + } + public void showItem(ShortcutInfo info) { + View v = getViewForInfo(info); + v.setVisibility(VISIBLE); + } + + 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); + LauncherModel.addOrMoveItemInDatabase( + mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); + } + + public void onRemove(ShortcutInfo item) { + mItemsInvalidated = true; + // If this item is being dragged from this open folder, we have already handled + // 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); + if (mState == STATE_ANIMATING) { + mRearrangeOnClose = true; + } else { + setupContentForNumItems(getItemCount()); + } + 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; + } + } + } + return null; + } + + public void onItemsChanged() { + updateTextViewFocus(); + } + + public void onTitleChanged(CharSequence title) { + } + + 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); + } + } + } + mItemsInvalidated = false; + } + return mItemsInReadingOrder; + } + + public void getLocationInDragLayer(int[] loc) { + mLauncher.getDragLayer().getLocationInDragLayer(this, loc); + } + + public void onFocusChange(View v, boolean hasFocus) { + if (v == mFolderName && hasFocus) { + startEditingFolderName(); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/FolderEditText.java b/app/src/main/java/com/android/launcher2/FolderEditText.java new file mode 100644 index 0000000..13169bd --- /dev/null +++ b/app/src/main/java/com/android/launcher2/FolderEditText.java @@ -0,0 +1,36 @@ +package com.android.launcher2; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + +public class FolderEditText extends EditText { + + private Folder mFolder; + + public FolderEditText(Context context) { + super(context); + } + + public FolderEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FolderEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setFolder(Folder folder) { + mFolder = folder; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + // Catch the back button on the soft keyboard so that we can just close the activity + if (event.getKeyCode() == android.view.KeyEvent.KEYCODE_BACK) { + mFolder.doneEditingFolderName(true); + } + return super.onKeyPreIme(keyCode, event); + } +} diff --git a/app/src/main/java/com/android/launcher2/FolderIcon.java b/app/src/main/java/com/android/launcher2/FolderIcon.java new file mode 100644 index 0000000..33650c5 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/FolderIcon.java @@ -0,0 +1,667 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +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.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher.R; +import com.android.launcher2.DropTarget.DragObject; +import com.android.launcher2.FolderInfo.FolderListener; + +import java.util.ArrayList; + +/** + * An icon that can appear on in the workspace representing an {@link UserFolder}. + */ +public class FolderIcon extends LinearLayout implements FolderListener { + private Launcher mLauncher; + private Folder mFolder; + private FolderInfo mInfo; + private static boolean sStaticValuesDirty = true; + + private CheckLongPressHelper mLongPressHelper; + + // The number of icons to display in the + private 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; + private static final int FINAL_ITEM_ANIMATION_DURATION = 200; + + // The degree to which the inner ring grows when accepting drop + private static final float INNER_RING_GROWTH_FACTOR = 0.15f; + + // The degree to which the outer ring is scaled in its natural state + private static final float OUTER_RING_GROWTH_FACTOR = 0.3f; + + // The amount of vertical spread between items in the stack [0...1] + private static final float PERSPECTIVE_SHIFT_FACTOR = 0.24f; + + // The degree to which the item in the back of the stack is scaled [0...1] + // (0 means it's not scaled at all, 1 means it's scaled to nothing) + private static final float PERSPECTIVE_SCALE_FACTOR = 0.35f; + + public static Drawable sSharedFolderLeaveBehind = null; + + private ImageView mPreviewBackground; + private BubbleTextView mFolderName; + + FolderRingAnimator mFolderRingAnimator = null; + + // These variables are all associated with the drawing of the preview; they are stored + // as member variables for shared usage and to avoid computation on each frame + private int mIntrinsicIconSize; + private float mBaselineIconScale; + private int mBaselineIconSize; + private int mAvailableSpaceInPreview; + private int mTotalWidth = -1; + private int mPreviewOffsetX; + private int mPreviewOffsetY; + private float mMaxPerspectiveShift; + boolean mAnimating = false; + + private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0); + private PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0); + private ArrayList mHiddenItems = new ArrayList(); + + public FolderIcon(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public FolderIcon(Context context) { + super(context); + init(); + } + + private void init() { + mLongPressHelper = new CheckLongPressHelper(this); + } + + public boolean isDropEnabled() { + final ViewGroup cellLayoutChildren = (ViewGroup) getParent(); + final ViewGroup cellLayout = (ViewGroup) cellLayoutChildren.getParent(); + final Workspace workspace = (Workspace) cellLayout.getParent(); + return !workspace.isSmall(); + } + + static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group, + FolderInfo folderInfo, IconCache iconCache) { + @SuppressWarnings("all") // suppress dead code warning + final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION; + if (error) { + throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " + + "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + + "is dependent on this"); + } + + FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false); + + icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name); + icon.mFolderName.setText(folderInfo.title); + icon.mPreviewBackground = (ImageView) icon.findViewById(R.id.preview_background); + + icon.setTag(folderInfo); + icon.setOnClickListener(launcher); + icon.mInfo = folderInfo; + icon.mLauncher = launcher; + icon.setContentDescription(String.format(launcher.getString(R.string.folder_name_format), + folderInfo.title)); + Folder folder = Folder.fromXml(launcher); + folder.setDragController(launcher.getDragController()); + folder.setFolderIcon(icon); + folder.bind(folderInfo); + icon.mFolder = folder; + + icon.mFolderRingAnimator = new FolderRingAnimator(launcher, icon); + folderInfo.addListener(icon); + + return icon; + } + + @Override + protected Parcelable onSaveInstanceState() { + sStaticValuesDirty = true; + return super.onSaveInstanceState(); + } + + public static class FolderRingAnimator { + public int mCellX; + public int mCellY; + private CellLayout mCellLayout; + public float mOuterRingSize; + public float mInnerRingSize; + public FolderIcon mFolderIcon = null; + public Drawable mOuterRingDrawable = null; + public Drawable mInnerRingDrawable = null; + public static Drawable sSharedOuterRingDrawable = null; + public static Drawable sSharedInnerRingDrawable = null; + public static int sPreviewSize = -1; + public static int sPreviewPadding = -1; + + private ValueAnimator mAcceptAnimator; + private ValueAnimator mNeutralAnimator; + + public FolderRingAnimator(Launcher launcher, FolderIcon folderIcon) { + mFolderIcon = folderIcon; + Resources res = launcher.getResources(); + mOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo); + mInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo); + + // We need to reload the static values when configuration changes in case they are + // different in another configuration + if (sStaticValuesDirty) { + sPreviewSize = res.getDimensionPixelSize(R.dimen.folder_preview_size); + 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_holo); + sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest); + sStaticValuesDirty = false; + } + } + + public void animateToAcceptState() { + if (mNeutralAnimator != null) { + mNeutralAnimator.cancel(); + } + mAcceptAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f); + mAcceptAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); + + final int previewSize = sPreviewSize; + mAcceptAnimator.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final float percent = (Float) animation.getAnimatedValue(); + mOuterRingSize = (1 + percent * OUTER_RING_GROWTH_FACTOR) * previewSize; + mInnerRingSize = (1 + percent * INNER_RING_GROWTH_FACTOR) * previewSize; + if (mCellLayout != null) { + mCellLayout.invalidate(); + } + } + }); + mAcceptAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (mFolderIcon != null) { + mFolderIcon.mPreviewBackground.setVisibility(INVISIBLE); + } + } + }); + mAcceptAnimator.start(); + } + + public void animateToNaturalState() { + if (mAcceptAnimator != null) { + mAcceptAnimator.cancel(); + } + mNeutralAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f); + mNeutralAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); + + final int previewSize = sPreviewSize; + mNeutralAnimator.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + final float percent = (Float) animation.getAnimatedValue(); + mOuterRingSize = (1 + (1 - percent) * OUTER_RING_GROWTH_FACTOR) * previewSize; + mInnerRingSize = (1 + (1 - percent) * INNER_RING_GROWTH_FACTOR) * previewSize; + if (mCellLayout != null) { + mCellLayout.invalidate(); + } + } + }); + mNeutralAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mCellLayout != null) { + mCellLayout.hideFolderAccept(FolderRingAnimator.this); + } + if (mFolderIcon != null) { + mFolderIcon.mPreviewBackground.setVisibility(VISIBLE); + } + } + }); + mNeutralAnimator.start(); + } + + // Location is expressed in window coordinates + public void getCell(int[] loc) { + loc[0] = mCellX; + loc[1] = mCellY; + } + + // Location is expressed in window coordinates + public void setCell(int x, int y) { + mCellX = x; + mCellY = y; + } + + public void setCellLayout(CellLayout layout) { + mCellLayout = layout; + } + + public float getOuterRingSize() { + return mOuterRingSize; + } + + public float getInnerRingSize() { + return mInnerRingSize; + } + } + + Folder getFolder() { + return mFolder; + } + + FolderInfo getFolderInfo() { + return mInfo; + } + + private boolean willAcceptItem(ItemInfo item) { + final int itemType = item.itemType; + return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || + itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && + !mFolder.isFull() && item != mInfo && !mInfo.opened); + } + + public boolean acceptDrop(Object dragInfo) { + final ItemInfo item = (ItemInfo) dragInfo; + return !mFolder.isDestroyed() && willAcceptItem(item); + } + + public void addItem(ShortcutInfo item) { + mInfo.add(item); + } + + public void onDragEnter(Object dragInfo) { + if (mFolder.isDestroyed() || !willAcceptItem((ItemInfo) dragInfo)) return; + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); + CellLayout layout = (CellLayout) getParent().getParent(); + mFolderRingAnimator.setCell(lp.cellX, lp.cellY); + mFolderRingAnimator.setCellLayout(layout); + mFolderRingAnimator.animateToAcceptState(); + layout.showFolderAccept(mFolderRingAnimator); + } + + public void onDragOver(Object dragInfo) { + } + + public void performCreateAnimation(final ShortcutInfo destInfo, final View destView, + final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect, + float scaleRelativeToDragLayer, Runnable postAnimationRunnable) { + + // These correspond two the drawable and view that the icon was dropped _onto_ + Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1]; + computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), + destView.getMeasuredWidth()); + + // This will animate the first item from it's position as an icon into its + // position as the first item in the preview + animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null); + addItem(destInfo); + + // This will animate the dragView (srcView) into the new folder + onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable, null); + } + + public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) { + Drawable animateDrawable = ((TextView) finalView).getCompoundDrawables()[1]; + computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), + finalView.getMeasuredWidth()); + + // This will animate the first item from it's position as an icon into its + // position as the first item in the preview + animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true, + onCompleteRunnable); + } + + public void onDragExit(Object dragInfo) { + onDragExit(); + } + + public void onDragExit() { + mFolderRingAnimator.animateToNaturalState(); + } + + private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect, + float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable, + DragObject d) { + item.cellX = -1; + item.cellY = -1; + + // Typically, the animateView corresponds to the DragView; however, if this is being done + // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we + // will not have a view to animate + if (animateView != null) { + DragLayer dragLayer = mLauncher.getDragLayer(); + Rect from = new Rect(); + dragLayer.getViewRectRelativeToSelf(animateView, from); + Rect to = finalRect; + if (to == null) { + to = new Rect(); + Workspace workspace = mLauncher.getWorkspace(); + // Set cellLayout and this to it's final state to compute final animation locations + workspace.setFinalTransitionTransform((CellLayout) getParent().getParent()); + float scaleX = getScaleX(); + float scaleY = getScaleY(); + setScaleX(1.0f); + setScaleY(1.0f); + scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to); + // Finished computing final animation locations, restore current state + setScaleX(scaleX); + setScaleY(scaleY); + workspace.resetTransitionTransform((CellLayout) getParent().getParent()); + } + + int[] center = new int[2]; + float scale = getLocalCenterForIndex(index, center); + center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]); + center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]); + + to.offset(center[0] - animateView.getMeasuredWidth() / 2, + center[1] - animateView.getMeasuredHeight() / 2); + + float finalAlpha = index < NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f; + + float finalScale = scale * scaleRelativeToDragLayer; + dragLayer.animateView(animateView, from, to, finalAlpha, + 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION, + new DecelerateInterpolator(2), new AccelerateInterpolator(2), + postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); + addItem(item); + mHiddenItems.add(item); + mFolder.hideItem(item); + postDelayed(new Runnable() { + public void run() { + mHiddenItems.remove(item); + mFolder.showItem(item); + invalidate(); + } + }, DROP_IN_ANIMATION_DURATION); + } else { + addItem(item); + } + } + + public void onDrop(DragObject d) { + ShortcutInfo item; + if (d.dragInfo instanceof ApplicationInfo) { + // Came from all apps -- make a copy + item = ((ApplicationInfo) d.dragInfo).makeShortcut(); + } else { + item = (ShortcutInfo) d.dragInfo; + } + mFolder.notifyDrop(); + onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d); + } + + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + private void computePreviewDrawingParams(int drawableSize, int totalSize) { + if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) { + mIntrinsicIconSize = drawableSize; + mTotalWidth = totalSize; + + final int previewSize = FolderRingAnimator.sPreviewSize; + final int previewPadding = FolderRingAnimator.sPreviewPadding; + + mAvailableSpaceInPreview = (previewSize - 2 * previewPadding); + // cos(45) = 0.707 + ~= 0.1) = 0.8f + int adjustedAvailableSpace = (int) ((mAvailableSpaceInPreview / 2) * (1 + 0.8f)); + + int unscaledHeight = (int) (mIntrinsicIconSize * (1 + PERSPECTIVE_SHIFT_FACTOR)); + mBaselineIconScale = (1.0f * adjustedAvailableSpace / unscaledHeight); + + mBaselineIconSize = (int) (mIntrinsicIconSize * mBaselineIconScale); + mMaxPerspectiveShift = mBaselineIconSize * PERSPECTIVE_SHIFT_FACTOR; + + mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2; + mPreviewOffsetY = previewPadding; + } + } + + private void computePreviewDrawingParams(Drawable d) { + computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth()); + } + + class PreviewItemDrawingParams { + PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) { + this.transX = transX; + this.transY = transY; + this.scale = scale; + this.overlayAlpha = overlayAlpha; + } + float transX; + float transY; + float scale; + int overlayAlpha; + Drawable drawable; + } + + private float getLocalCenterForIndex(int index, int[] center) { + mParams = computePreviewItemDrawingParams(Math.min(NUM_ITEMS_IN_PREVIEW, index), mParams); + + mParams.transX += mPreviewOffsetX; + mParams.transY += mPreviewOffsetY; + float offsetX = mParams.transX + (mParams.scale * mIntrinsicIconSize) / 2; + float offsetY = mParams.transY + (mParams.scale * mIntrinsicIconSize) / 2; + + center[0] = (int) Math.round(offsetX); + center[1] = (int) Math.round(offsetY); + return mParams.scale; + } + + private PreviewItemDrawingParams computePreviewItemDrawingParams(int index, + PreviewItemDrawingParams params) { + index = NUM_ITEMS_IN_PREVIEW - index - 1; + float r = (index * 1.0f) / (NUM_ITEMS_IN_PREVIEW - 1); + float scale = (1 - PERSPECTIVE_SCALE_FACTOR * (1 - r)); + + float offset = (1 - r) * mMaxPerspectiveShift; + float scaledSize = scale * mBaselineIconSize; + float scaleOffsetCorrection = (1 - scale) * mBaselineIconSize; + + // We want to imagine our coordinates from the bottom left, growing up and to the + // right. This is natural for the x-axis, but for the y-axis, we have to invert things. + float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection); + float transX = offset + scaleOffsetCorrection; + float totalScale = mBaselineIconScale * scale; + final int overlayAlpha = (int) (80 * (1 - r)); + + if (params == null) { + params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha); + } else { + params.transX = transX; + params.transY = transY; + params.scale = totalScale; + params.overlayAlpha = overlayAlpha; + } + return params; + } + + private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) { + canvas.save(); + canvas.translate(params.transX + mPreviewOffsetX, params.transY + mPreviewOffsetY); + canvas.scale(params.scale, params.scale); + Drawable d = params.drawable; + + if (d != null) { + d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize); + d.setFilterBitmap(true); + d.setColorFilter(Color.argb(params.overlayAlpha, 0, 0, 0), PorterDuff.Mode.SRC_ATOP); + d.draw(canvas); + d.clearColorFilter(); + d.setFilterBitmap(false); + } + canvas.restore(); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mFolder == null) return; + if (mFolder.getItemCount() == 0 && !mAnimating) return; + + ArrayList items = mFolder.getItemsInReadingOrder(); + Drawable d; + TextView v; + + // Update our drawing parameters if necessary + if (mAnimating) { + computePreviewDrawingParams(mAnimParams.drawable); + } else { + v = (TextView) items.get(0); + d = v.getCompoundDrawables()[1]; + computePreviewDrawingParams(d); + } + + int nItemsInPreview = Math.min(items.size(), NUM_ITEMS_IN_PREVIEW); + if (!mAnimating) { + for (int i = nItemsInPreview - 1; i >= 0; i--) { + v = (TextView) items.get(i); + if (!mHiddenItems.contains(v.getTag())) { + d = v.getCompoundDrawables()[1]; + mParams = computePreviewItemDrawingParams(i, mParams); + mParams.drawable = d; + drawPreviewItem(canvas, mParams); + } + } + } else { + drawPreviewItem(canvas, mAnimParams); + } + } + + private void animateFirstItem(final Drawable d, int duration, final boolean reverse, + 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; + mAnimParams.drawable = d; + + ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f); + va.addUpdateListener(new AnimatorUpdateListener(){ + public void onAnimationUpdate(ValueAnimator animation) { + float progress = (Float) animation.getAnimatedValue(); + if (reverse) { + progress = 1 - progress; + mPreviewBackground.setAlpha(progress); + } + + mAnimParams.transX = transX0 + progress * (finalParams.transX - transX0); + mAnimParams.transY = transY0 + progress * (finalParams.transY - transY0); + mAnimParams.scale = scale0 + progress * (finalParams.scale - scale0); + invalidate(); + } + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mAnimating = true; + } + @Override + public void onAnimationEnd(Animator animation) { + mAnimating = false; + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + }); + va.setDuration(duration); + va.start(); + } + + public void setTextVisible(boolean visible) { + if (visible) { + mFolderName.setVisibility(VISIBLE); + } else { + mFolderName.setVisibility(INVISIBLE); + } + } + + public boolean getTextVisible() { + return mFolderName.getVisibility() == VISIBLE; + } + + public void onItemsChanged() { + invalidate(); + requestLayout(); + } + + public void onAdd(ShortcutInfo item) { + invalidate(); + requestLayout(); + } + + public void onRemove(ShortcutInfo item) { + invalidate(); + requestLayout(); + } + + public void onTitleChanged(CharSequence title) { + mFolderName.setText(title.toString()); + setContentDescription(String.format(getContext().getString(R.string.folder_name_format), + title)); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + // Call the superclass onTouchEvent first, because sometimes it changes the state to + // isPressed() on an ACTION_UP + boolean result = super.onTouchEvent(event); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mLongPressHelper.postCheckForLongPress(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mLongPressHelper.cancelLongPress(); + break; + } + return result; + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + mLongPressHelper.cancelLongPress(); + } +} diff --git a/app/src/main/java/com/android/launcher2/FolderInfo.java b/app/src/main/java/com/android/launcher2/FolderInfo.java new file mode 100644 index 0000000..dbac90e --- /dev/null +++ b/app/src/main/java/com/android/launcher2/FolderInfo.java @@ -0,0 +1,111 @@ +/* + * 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.launcher2; + +import java.util.ArrayList; + +import android.content.ContentValues; + +/** + * Represents a folder containing shortcuts or apps. + */ +class FolderInfo extends ItemInfo { + + /** + * Whether this folder has been opened + */ + boolean opened; + + /** + * The apps and shortcuts + */ + ArrayList contents = new ArrayList(); + + ArrayList listeners = new ArrayList(); + + FolderInfo() { + itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; + } + + /** + * Add an app or shortcut + * + * @param item + */ + public void add(ShortcutInfo item) { + contents.add(item); + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onAdd(item); + } + itemsChanged(); + } + + /** + * Remove an app or shortcut. Does not change the DB. + * + * @param item + */ + public void remove(ShortcutInfo item) { + contents.remove(item); + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onRemove(item); + } + itemsChanged(); + } + + public void setTitle(CharSequence title) { + this.title = title; + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onTitleChanged(title); + } + } + + @Override + void onAddToDatabase(ContentValues values) { + super.onAddToDatabase(values); + values.put(LauncherSettings.Favorites.TITLE, title.toString()); + } + + void addListener(FolderListener listener) { + listeners.add(listener); + } + + void removeListener(FolderListener listener) { + if (listeners.contains(listener)) { + listeners.remove(listener); + } + } + + void itemsChanged() { + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onItemsChanged(); + } + } + + @Override + void unbind() { + super.unbind(); + listeners.clear(); + } + + interface FolderListener { + public void onAdd(ShortcutInfo item); + public void onRemove(ShortcutInfo item); + public void onTitleChanged(CharSequence title); + public void onItemsChanged(); + } +} diff --git a/app/src/main/java/com/android/launcher2/HandleView.java b/app/src/main/java/com/android/launcher2/HandleView.java new file mode 100644 index 0000000..d77138b --- /dev/null +++ b/app/src/main/java/com/android/launcher2/HandleView.java @@ -0,0 +1,76 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; + +import com.android.launcher.R; + +public class HandleView extends ImageView { + private static final int ORIENTATION_HORIZONTAL = 1; + + private Launcher mLauncher; + private int mOrientation = ORIENTATION_HORIZONTAL; + + public HandleView(Context context) { + super(context); + } + + public HandleView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HandleView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HandleView, defStyle, 0); + mOrientation = a.getInt(R.styleable.HandleView_direction, ORIENTATION_HORIZONTAL); + a.recycle(); + + setContentDescription(context.getString(R.string.all_apps_button_label)); + } + + @Override + public View focusSearch(int direction) { + View newFocus = super.focusSearch(direction); + if (newFocus == null && !mLauncher.isAllAppsVisible()) { + final Workspace workspace = mLauncher.getWorkspace(); + workspace.dispatchUnhandledMove(null, direction); + return (mOrientation == ORIENTATION_HORIZONTAL && direction == FOCUS_DOWN) ? + this : workspace; + } + return newFocus; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN && mLauncher.isAllAppsVisible()) { + return false; + } + return super.onTouchEvent(ev); + } + + void setLauncher(Launcher launcher) { + mLauncher = launcher; + } +} diff --git a/app/src/main/java/com/android/launcher2/HideFromAccessibilityHelper.java b/app/src/main/java/com/android/launcher2/HideFromAccessibilityHelper.java new file mode 100644 index 0000000..0b2ce5b --- /dev/null +++ b/app/src/main/java/com/android/launcher2/HideFromAccessibilityHelper.java @@ -0,0 +1,113 @@ +/* + * 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.launcher2; + +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.OnHierarchyChangeListener; + +import java.util.HashMap; + +public class HideFromAccessibilityHelper implements OnHierarchyChangeListener { + private HashMap mPreviousValues; + boolean mHide; + boolean mOnlyAllApps; + + public HideFromAccessibilityHelper() { + mPreviousValues = new HashMap(); + mHide = false; + } + + public void setImportantForAccessibilityToNo(View v, boolean onlyAllApps) { + mOnlyAllApps = onlyAllApps; + setImportantForAccessibilityToNoHelper(v); + mHide = true; + } + + private void setImportantForAccessibilityToNoHelper(View v) { + mPreviousValues.put(v, v.getImportantForAccessibility()); + v.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + + // Call method on children recursively + if (v instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) v; + vg.setOnHierarchyChangeListener(this); + for (int i = 0; i < vg.getChildCount(); i++) { + View child = vg.getChildAt(i); + + if (includeView(child)) { + setImportantForAccessibilityToNoHelper(child); + } + } + } + } + + public void restoreImportantForAccessibility(View v) { + if (mHide) { + restoreImportantForAccessibilityHelper(v); + } + mHide = false; + } + + private void restoreImportantForAccessibilityHelper(View v) { + v.setImportantForAccessibility(mPreviousValues.get(v)); + mPreviousValues.remove(v); + + // Call method on children recursively + if (v instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) v; + + // We assume if a class implements OnHierarchyChangeListener, it listens + // to changes to any of its children (happens to be the case in Launcher) + if (vg instanceof OnHierarchyChangeListener) { + vg.setOnHierarchyChangeListener((OnHierarchyChangeListener) vg); + } else { + vg.setOnHierarchyChangeListener(null); + } + for (int i = 0; i < vg.getChildCount(); i++) { + View child = vg.getChildAt(i); + if (includeView(child)) { + restoreImportantForAccessibilityHelper(child); + } + } + } + } + + public void onChildViewAdded(View parent, View child) { + if (mHide && includeView(child)) { + setImportantForAccessibilityToNoHelper(child); + } + } + + public void onChildViewRemoved(View parent, View child) { + if (mHide && includeView(child)) { + restoreImportantForAccessibilityHelper(child); + } + } + + private boolean includeView(View v) { + return !hasAncestorOfType(v, Cling.class) && + (!mOnlyAllApps || hasAncestorOfType(v, AppsCustomizeTabHost.class)); + } + + private boolean hasAncestorOfType(View v, Class c) { + return v != null && + (v.getClass().equals(c) || + (v.getParent() instanceof ViewGroup && + hasAncestorOfType((ViewGroup) v.getParent(), c))); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/android/launcher2/HolographicImageView.java b/app/src/main/java/com/android/launcher2/HolographicImageView.java new file mode 100644 index 0000000..9e551e0 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/HolographicImageView.java @@ -0,0 +1,54 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class HolographicImageView extends ImageView { + + private final HolographicViewHelper mHolographicHelper; + + public HolographicImageView(Context context) { + this(context, null); + } + + public HolographicImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HolographicImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mHolographicHelper = new HolographicViewHelper(context); + } + + void invalidatePressedFocusedStates() { + mHolographicHelper.invalidatePressedFocusedStates(this); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // One time call to generate the pressed/focused state -- must be called after + // measure/layout + mHolographicHelper.generatePressedFocusedStates(this); + } +} diff --git a/app/src/main/java/com/android/launcher2/HolographicLinearLayout.java b/app/src/main/java/com/android/launcher2/HolographicLinearLayout.java new file mode 100644 index 0000000..0f997d5 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/HolographicLinearLayout.java @@ -0,0 +1,85 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.launcher.R; + +public class HolographicLinearLayout extends LinearLayout { + + private final HolographicViewHelper mHolographicHelper; + private ImageView mImageView; + private int mImageViewId; + + public HolographicLinearLayout(Context context) { + this(context, null); + } + + public HolographicLinearLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HolographicLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HolographicLinearLayout, + defStyle, 0); + mImageViewId = a.getResourceId(R.styleable.HolographicLinearLayout_sourceImageViewId, -1); + a.recycle(); + + setWillNotDraw(false); + mHolographicHelper = new HolographicViewHelper(context); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + if (mImageView != null) { + Drawable d = mImageView.getDrawable(); + if (d instanceof StateListDrawable) { + StateListDrawable sld = (StateListDrawable) d; + sld.setState(getDrawableState()); + } + } + } + + void invalidatePressedFocusedStates() { + mHolographicHelper.invalidatePressedFocusedStates(mImageView); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // One time call to generate the pressed/focused state -- must be called after + // measure/layout + if (mImageView == null) { + mImageView = (ImageView) findViewById(mImageViewId); + } + mHolographicHelper.generatePressedFocusedStates(mImageView); + } +} diff --git a/app/src/main/java/com/android/launcher2/HolographicOutlineHelper.java b/app/src/main/java/com/android/launcher2/HolographicOutlineHelper.java new file mode 100644 index 0000000..1e990dc --- /dev/null +++ b/app/src/main/java/com/android/launcher2/HolographicOutlineHelper.java @@ -0,0 +1,221 @@ +/* + * 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.launcher2; + +import android.graphics.Bitmap; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; + +public class HolographicOutlineHelper { + private final Paint mHolographicPaint = new Paint(); + private final Paint mBlurPaint = new Paint(); + private final Paint mErasePaint = new Paint(); + + public static final int MAX_OUTER_BLUR_RADIUS; + public static final int MIN_OUTER_BLUR_RADIUS; + + private static final BlurMaskFilter sExtraThickOuterBlurMaskFilter; + private static final BlurMaskFilter sThickOuterBlurMaskFilter; + private static final BlurMaskFilter sMediumOuterBlurMaskFilter; + private static final BlurMaskFilter sThinOuterBlurMaskFilter; + private static final BlurMaskFilter sThickInnerBlurMaskFilter; + private static final BlurMaskFilter sExtraThickInnerBlurMaskFilter; + private static final BlurMaskFilter sMediumInnerBlurMaskFilter; + + private static final int THICK = 0; + private static final int MEDIUM = 1; + private static final int EXTRA_THICK = 2; + + static { + final float scale = LauncherApplication.getScreenDensity(); + + MIN_OUTER_BLUR_RADIUS = (int) (scale * 1.0f); + MAX_OUTER_BLUR_RADIUS = (int) (scale * 12.0f); + + sExtraThickOuterBlurMaskFilter = new BlurMaskFilter(scale * 12.0f, BlurMaskFilter.Blur.OUTER); + sThickOuterBlurMaskFilter = new BlurMaskFilter(scale * 6.0f, BlurMaskFilter.Blur.OUTER); + sMediumOuterBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.OUTER); + sThinOuterBlurMaskFilter = new BlurMaskFilter(scale * 1.0f, BlurMaskFilter.Blur.OUTER); + sExtraThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 6.0f, BlurMaskFilter.Blur.NORMAL); + sThickInnerBlurMaskFilter = new BlurMaskFilter(scale * 4.0f, BlurMaskFilter.Blur.NORMAL); + sMediumInnerBlurMaskFilter = new BlurMaskFilter(scale * 2.0f, BlurMaskFilter.Blur.NORMAL); + } + + HolographicOutlineHelper() { + mHolographicPaint.setFilterBitmap(true); + mHolographicPaint.setAntiAlias(true); + mBlurPaint.setFilterBitmap(true); + mBlurPaint.setAntiAlias(true); + mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + mErasePaint.setFilterBitmap(true); + mErasePaint.setAntiAlias(true); + } + + /** + * Returns the interpolated holographic highlight alpha for the effect we want when scrolling + * pages. + */ + public static float highlightAlphaInterpolator(float r) { + float maxAlpha = 0.6f; + return (float) Math.pow(maxAlpha * (1.0f - r), 1.5f); + } + + /** + * Returns the interpolated view alpha for the effect we want when scrolling pages. + */ + public static float viewAlphaInterpolator(float r) { + final float pivot = 0.95f; + if (r < pivot) { + return (float) Math.pow(r / pivot, 1.5f); + } else { + return 1.0f; + } + } + + /** + * Applies a more expensive and accurate outline to whatever is currently drawn in a specified + * bitmap. + */ + void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor, int thickness) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, true, + thickness); + } + void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor, boolean clipAlpha, int thickness) { + + // We start by removing most of the alpha channel so as to ignore shadows, and + // other types of partial transparency when defining the shape of the object + if (clipAlpha) { + int[] srcBuffer = new int[srcDst.getWidth() * srcDst.getHeight()]; + srcDst.getPixels(srcBuffer, + 0, srcDst.getWidth(), 0, 0, srcDst.getWidth(), srcDst.getHeight()); + for (int i = 0; i < srcBuffer.length; i++) { + final int alpha = srcBuffer[i] >>> 24; + if (alpha < 188) { + srcBuffer[i] = 0; + } + } + srcDst.setPixels(srcBuffer, + 0, srcDst.getWidth(), 0, 0, srcDst.getWidth(), srcDst.getHeight()); + } + Bitmap glowShape = srcDst.extractAlpha(); + + // calculate the outer blur first + BlurMaskFilter outerBlurMaskFilter; + switch (thickness) { + case EXTRA_THICK: + outerBlurMaskFilter = sExtraThickOuterBlurMaskFilter; + break; + case THICK: + outerBlurMaskFilter = sThickOuterBlurMaskFilter; + break; + case MEDIUM: + outerBlurMaskFilter = sMediumOuterBlurMaskFilter; + break; + default: + throw new RuntimeException("Invalid blur thickness"); + } + mBlurPaint.setMaskFilter(outerBlurMaskFilter); + int[] outerBlurOffset = new int[2]; + Bitmap thickOuterBlur = glowShape.extractAlpha(mBlurPaint, outerBlurOffset); + if (thickness == EXTRA_THICK) { + mBlurPaint.setMaskFilter(sMediumOuterBlurMaskFilter); + } else { + mBlurPaint.setMaskFilter(sThinOuterBlurMaskFilter); + } + + int[] brightOutlineOffset = new int[2]; + Bitmap brightOutline = glowShape.extractAlpha(mBlurPaint, brightOutlineOffset); + + // calculate the inner blur + srcDstCanvas.setBitmap(glowShape); + srcDstCanvas.drawColor(0xFF000000, PorterDuff.Mode.SRC_OUT); + BlurMaskFilter innerBlurMaskFilter; + switch (thickness) { + case EXTRA_THICK: + innerBlurMaskFilter = sExtraThickInnerBlurMaskFilter; + break; + case THICK: + innerBlurMaskFilter = sThickInnerBlurMaskFilter; + break; + case MEDIUM: + innerBlurMaskFilter = sMediumInnerBlurMaskFilter; + break; + default: + throw new RuntimeException("Invalid blur thickness"); + } + mBlurPaint.setMaskFilter(innerBlurMaskFilter); + int[] thickInnerBlurOffset = new int[2]; + Bitmap thickInnerBlur = glowShape.extractAlpha(mBlurPaint, thickInnerBlurOffset); + + // mask out the inner blur + srcDstCanvas.setBitmap(thickInnerBlur); + srcDstCanvas.drawBitmap(glowShape, -thickInnerBlurOffset[0], + -thickInnerBlurOffset[1], mErasePaint); + srcDstCanvas.drawRect(0, 0, -thickInnerBlurOffset[0], thickInnerBlur.getHeight(), + mErasePaint); + srcDstCanvas.drawRect(0, 0, thickInnerBlur.getWidth(), -thickInnerBlurOffset[1], + mErasePaint); + + // draw the inner and outer blur + srcDstCanvas.setBitmap(srcDst); + srcDstCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + mHolographicPaint.setColor(color); + srcDstCanvas.drawBitmap(thickInnerBlur, thickInnerBlurOffset[0], thickInnerBlurOffset[1], + mHolographicPaint); + srcDstCanvas.drawBitmap(thickOuterBlur, outerBlurOffset[0], outerBlurOffset[1], + mHolographicPaint); + + // draw the bright outline + mHolographicPaint.setColor(outlineColor); + srcDstCanvas.drawBitmap(brightOutline, brightOutlineOffset[0], brightOutlineOffset[1], + mHolographicPaint); + + // cleanup + srcDstCanvas.setBitmap(null); + brightOutline.recycle(); + thickOuterBlur.recycle(); + thickInnerBlur.recycle(); + glowShape.recycle(); + } + + void applyExtraThickExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, EXTRA_THICK); + } + + void applyThickExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, THICK); + } + + void applyMediumExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor, boolean clipAlpha) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, clipAlpha, + MEDIUM); + } + + void applyMediumExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas, int color, + int outlineColor) { + applyExpensiveOutlineWithBlur(srcDst, srcDstCanvas, color, outlineColor, MEDIUM); + } + +} diff --git a/app/src/main/java/com/android/launcher2/HolographicViewHelper.java b/app/src/main/java/com/android/launcher2/HolographicViewHelper.java new file mode 100644 index 0000000..3d509ca --- /dev/null +++ b/app/src/main/java/com/android/launcher2/HolographicViewHelper.java @@ -0,0 +1,104 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.widget.ImageView; + +public class HolographicViewHelper { + + private final Canvas mTempCanvas = new Canvas(); + + private boolean mStatesUpdated; + private int mHighlightColor; + + public HolographicViewHelper(Context context) { + Resources res = context.getResources(); + mHighlightColor = res.getColor(android.R.color.white); + } + + /** + * Generate the pressed/focused states if necessary. + */ + void generatePressedFocusedStates(ImageView v) { + if (!mStatesUpdated && v != null) { + mStatesUpdated = true; + Bitmap original = createOriginalImage(v, mTempCanvas); + Bitmap outline = createPressImage(v, mTempCanvas); + FastBitmapDrawable originalD = new FastBitmapDrawable(original); + FastBitmapDrawable outlineD = new FastBitmapDrawable(outline); + + StateListDrawable states = new StateListDrawable(); + states.addState(new int[] {android.R.attr.state_pressed}, outlineD); + states.addState(new int[] {android.R.attr.state_focused}, outlineD); + states.addState(new int[] {}, originalD); + v.setImageDrawable(states); + } + } + + /** + * Invalidates the pressed/focused states. + */ + void invalidatePressedFocusedStates(ImageView v) { + mStatesUpdated = false; + if (v != null) { + v.invalidate(); + } + } + + /** + * Creates a copy of the original image. + */ + private Bitmap createOriginalImage(ImageView v, Canvas canvas) { + final Drawable d = v.getDrawable(); + final Bitmap b = Bitmap.createBitmap( + d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + canvas.save(); + d.draw(canvas); + canvas.restore(); + canvas.setBitmap(null); + + return b; + } + + /** + * Creates a new press state image which is the old image with a blue overlay. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createPressImage(ImageView v, Canvas canvas) { + final Drawable d = v.getDrawable(); + final Bitmap b = Bitmap.createBitmap( + d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + canvas.save(); + d.draw(canvas); + canvas.restore(); + canvas.drawColor(mHighlightColor, PorterDuff.Mode.SRC_IN); + canvas.setBitmap(null); + + return b; + } +} diff --git a/app/src/main/java/com/android/launcher2/Hotseat.java b/app/src/main/java/com/android/launcher2/Hotseat.java new file mode 100644 index 0000000..c122695 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/Hotseat.java @@ -0,0 +1,147 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.launcher.R; + +public class Hotseat extends FrameLayout { + @SuppressWarnings("unused") + private static final String TAG = "Hotseat"; + + private Launcher mLauncher; + private CellLayout mContent; + + private int mCellCountX; + private int mCellCountY; + private int mAllAppsButtonRank; + + private boolean mTransposeLayoutWithOrientation; + private boolean mIsLandscape; + + public Hotseat(Context context) { + this(context, null); + } + + public Hotseat(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Hotseat(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.Hotseat, defStyle, 0); + Resources r = context.getResources(); + mCellCountX = a.getInt(R.styleable.Hotseat_cellCountX, -1); + mCellCountY = a.getInt(R.styleable.Hotseat_cellCountY, -1); + mAllAppsButtonRank = r.getInteger(R.integer.hotseat_all_apps_index); + 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; + setOnKeyListener(new HotseatIconKeyEventListener()); + } + + CellLayout getLayout() { + return mContent; + } + + 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; + } + /* Get the orientation specific coordinates given an invariant order in the hotseat. */ + int getCellXFromOrder(int rank) { + return hasVerticalHotseat() ? 0 : rank; + } + int getCellYFromOrder(int rank) { + return hasVerticalHotseat() ? (mContent.getCountY() - (rank + 1)) : 0; + } + public boolean isAllAppsButtonRank(int rank) { + return rank == mAllAppsButtonRank; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (mCellCountX < 0) mCellCountX = LauncherModel.getCellCountX(); + if (mCellCountY < 0) mCellCountY = LauncherModel.getCellCountY(); + mContent = (CellLayout) findViewById(R.id.layout); + mContent.setGridSize(mCellCountX, mCellCountY); + mContent.setIsHotseat(true); + + resetLayout(); + } + + void resetLayout() { + mContent.removeAllViewsInLayout(); + + // Add the Apps button + Context context = getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + BubbleTextView allAppsButton = (BubbleTextView) + inflater.inflate(R.layout.application, mContent, false); + allAppsButton.setCompoundDrawablesWithIntrinsicBounds(null, + context.getResources().getDrawable(R.drawable.all_apps_button_icon), null, null); + allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label)); + allAppsButton.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mLauncher != null && + (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { + mLauncher.onTouchDownAllAppsButton(v); + } + return false; + } + }); + + allAppsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(android.view.View v) { + if (mLauncher != null) { + mLauncher.onClickAllAppsButton(v); + } + } + }); + + // 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, 0, lp, true); + } +} diff --git a/app/src/main/java/com/android/launcher2/IconCache.java b/app/src/main/java/com/android/launcher2/IconCache.java new file mode 100644 index 0000000..aa19545 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/IconCache.java @@ -0,0 +1,229 @@ +/* + * 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.launcher2; + +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; + +import java.util.HashMap; + +/** + * Cache of application icons. Icons can be made from any thread. + */ +public class IconCache { + @SuppressWarnings("unused") + private static final String TAG = "Launcher.IconCache"; + + private static final int INITIAL_ICON_CACHE_CAPACITY = 50; + + private static class CacheEntry { + public Bitmap icon; + public String title; + } + + private final Bitmap mDefaultIcon; + private final LauncherApplication mContext; + private final PackageManager mPackageManager; + private final HashMap mCache = + new HashMap(INITIAL_ICON_CACHE_CAPACITY); + private int mIconDpi; + + public IconCache(LauncherApplication context) { + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + mContext = context; + mPackageManager = context.getPackageManager(); + mIconDpi = activityManager.getLauncherLargeIconDensity(); + + // need to set mIconDpi before getting default icon + mDefaultIcon = makeDefaultIcon(); + } + + public Drawable getFullResDefaultActivityIcon() { + return getFullResIcon(Resources.getSystem(), + android.R.mipmap.sym_def_app_icon); + } + + public Drawable getFullResIcon(Resources resources, int iconId) { + Drawable d; + try { + d = resources.getDrawableForDensity(iconId, mIconDpi); + } catch (Resources.NotFoundException e) { + d = null; + } + + return (d != null) ? d : getFullResDefaultActivityIcon(); + } + + public Drawable getFullResIcon(String packageName, int iconId) { + Resources resources; + try { + resources = mPackageManager.getResourcesForApplication(packageName); + } catch (PackageManager.NameNotFoundException e) { + resources = null; + } + if (resources != null) { + if (iconId != 0) { + return getFullResIcon(resources, iconId); + } + } + return getFullResDefaultActivityIcon(); + } + + public Drawable getFullResIcon(ResolveInfo info) { + return getFullResIcon(info.activityInfo); + } + + public Drawable getFullResIcon(ActivityInfo info) { + + Resources resources; + try { + resources = mPackageManager.getResourcesForApplication( + info.applicationInfo); + } catch (PackageManager.NameNotFoundException e) { + resources = null; + } + if (resources != null) { + int iconId = info.getIconResource(); + if (iconId != 0) { + return getFullResIcon(resources, iconId); + } + } + return getFullResDefaultActivityIcon(); + } + + private Bitmap makeDefaultIcon() { + Drawable d = getFullResDefaultActivityIcon(); + Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1), + Math.max(d.getIntrinsicHeight(), 1), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + d.setBounds(0, 0, b.getWidth(), b.getHeight()); + d.draw(c); + c.setBitmap(null); + return b; + } + + /** + * Remove any records for the supplied ComponentName. + */ + public void remove(ComponentName componentName) { + synchronized (mCache) { + mCache.remove(componentName); + } + } + + /** + * Empty out the cache. + */ + public void flush() { + synchronized (mCache) { + mCache.clear(); + } + } + + /** + * Fill in "application" with the icon and label for "info." + */ + public void getTitleAndIcon(ApplicationInfo application, ResolveInfo info, + HashMap labelCache) { + synchronized (mCache) { + CacheEntry entry = cacheLocked(application.componentName, info, labelCache); + + application.title = entry.title; + application.iconBitmap = entry.icon; + } + } + + public Bitmap getIcon(Intent intent) { + synchronized (mCache) { + final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, 0); + ComponentName component = intent.getComponent(); + + if (resolveInfo == null || component == null) { + return mDefaultIcon; + } + + CacheEntry entry = cacheLocked(component, resolveInfo, null); + return entry.icon; + } + } + + public Bitmap getIcon(ComponentName component, ResolveInfo resolveInfo, + HashMap labelCache) { + synchronized (mCache) { + if (resolveInfo == null || component == null) { + return null; + } + + CacheEntry entry = cacheLocked(component, resolveInfo, labelCache); + return entry.icon; + } + } + + public boolean isDefaultIcon(Bitmap icon) { + return mDefaultIcon == icon; + } + + private CacheEntry cacheLocked(ComponentName componentName, ResolveInfo info, + HashMap labelCache) { + CacheEntry entry = mCache.get(componentName); + if (entry == null) { + entry = new CacheEntry(); + + mCache.put(componentName, entry); + + ComponentName key = LauncherModel.getComponentNameFromResolveInfo(info); + if (labelCache != null && labelCache.containsKey(key)) { + entry.title = labelCache.get(key).toString(); + } else { + entry.title = info.loadLabel(mPackageManager).toString(); + if (labelCache != null) { + labelCache.put(key, entry.title); + } + } + if (entry.title == null) { + entry.title = info.activityInfo.name; + } + + entry.icon = Utilities.createIconBitmap( + getFullResIcon(info), mContext); + } + return entry; + } + + public HashMap getAllIcons() { + synchronized (mCache) { + HashMap set = new HashMap(); + for (ComponentName cn : mCache.keySet()) { + final CacheEntry e = mCache.get(cn); + set.put(cn, e.icon); + } + return set; + } + } +} diff --git a/app/src/main/java/com/android/launcher2/InfoDropTarget.java b/app/src/main/java/com/android/launcher2/InfoDropTarget.java new file mode 100644 index 0000000..850cc1f --- /dev/null +++ b/app/src/main/java/com/android/launcher2/InfoDropTarget.java @@ -0,0 +1,129 @@ +/* + * 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.launcher2; + +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.launcher.R; + +public class InfoDropTarget extends ButtonDropTarget { + + private ColorStateList mOriginalTextColor; + private TransitionDrawable mDrawable; + + public InfoDropTarget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public InfoDropTarget(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @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(); + 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 (!LauncherApplication.isScreenLarge()) { + setText(""); + } + } + } + + private boolean isFromAllApps(DragSource source) { + return (source instanceof AppsCustomizePagedView); + } + + @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. + ComponentName componentName = null; + if (d.dragInfo instanceof ApplicationInfo) { + componentName = ((ApplicationInfo) 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 (componentName != null) { + mLauncher.startApplicationDetailsActivity(componentName); + } + + // 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; + + // Hide this button unless we are dragging something from AllApps + if (!isFromAllApps(source)) { + isVisible = false; + } + + mActive = isVisible; + mDrawable.resetTransition(); + setTextColor(mOriginalTextColor); + ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); + } + + @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); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/InstallShortcutReceiver.java b/app/src/main/java/com/android/launcher2/InstallShortcutReceiver.java new file mode 100644 index 0000000..2e86a7f --- /dev/null +++ b/app/src/main/java/com/android/launcher2/InstallShortcutReceiver.java @@ -0,0 +1,363 @@ +/* + * 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.launcher2; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Base64; +import android.util.Log; +import android.widget.Toast; + +import com.android.launcher.R; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.json.*; + +public class InstallShortcutReceiver extends BroadcastReceiver { + public static final String ACTION_INSTALL_SHORTCUT = + "com.android.launcher.action.INSTALL_SHORTCUT"; + public static final String NEW_APPS_PAGE_KEY = "apps.new.page"; + public static final String NEW_APPS_LIST_KEY = "apps.new.list"; + + public static final String DATA_INTENT_KEY = "intent.data"; + public static final String LAUNCH_INTENT_KEY = "intent.launch"; + public static final String NAME_KEY = "name"; + public static final String ICON_KEY = "icon"; + public static final String ICON_RESOURCE_NAME_KEY = "iconResource"; + public static final String ICON_RESOURCE_PACKAGE_NAME_KEY = "iconResourcePackage"; + // The set of shortcuts that are pending install + public static final String APPS_PENDING_INSTALL = "apps_to_install"; + + public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450; + public static final int NEW_SHORTCUT_STAGGER_DELAY = 75; + + private static final int INSTALL_SHORTCUT_SUCCESSFUL = 0; + private static final int INSTALL_SHORTCUT_IS_DUPLICATE = -1; + private static final int INSTALL_SHORTCUT_NO_SPACE = -2; + + // A mime-type representing shortcut data + public static final String SHORTCUT_MIMETYPE = + "com.android.launcher/shortcut"; + + private static Object sLock = new Object(); + + private static void addToStringSet(SharedPreferences sharedPrefs, + SharedPreferences.Editor editor, String key, String value) { + Set strings = sharedPrefs.getStringSet(key, null); + if (strings == null) { + strings = new HashSet(0); + } else { + strings = new HashSet(strings); + } + strings.add(value); + editor.putStringSet(key, strings); + } + + private static void addToInstallQueue( + SharedPreferences sharedPrefs, PendingInstallShortcutInfo info) { + synchronized(sLock) { + try { + JSONStringer json = new JSONStringer() + .object() + .key(DATA_INTENT_KEY).value(info.data.toUri(0)) + .key(LAUNCH_INTENT_KEY).value(info.launchIntent.toUri(0)) + .key(NAME_KEY).value(info.name); + if (info.icon != null) { + byte[] iconByteArray = ItemInfo.flattenBitmap(info.icon); + json = json.key(ICON_KEY).value( + Base64.encodeToString( + iconByteArray, 0, iconByteArray.length, Base64.DEFAULT)); + } + if (info.iconResource != null) { + json = json.key(ICON_RESOURCE_NAME_KEY).value(info.iconResource.resourceName); + json = json.key(ICON_RESOURCE_PACKAGE_NAME_KEY) + .value(info.iconResource.packageName); + } + json = json.endObject(); + SharedPreferences.Editor editor = sharedPrefs.edit(); + addToStringSet(sharedPrefs, editor, APPS_PENDING_INSTALL, json.toString()); + editor.commit(); + } catch (org.json.JSONException e) { + Log.d("InstallShortcutReceiver", "Exception when adding shortcut: " + e); + } + } + } + + private static ArrayList getAndClearInstallQueue( + SharedPreferences sharedPrefs) { + synchronized(sLock) { + Set strings = sharedPrefs.getStringSet(APPS_PENDING_INSTALL, null); + if (strings == null) { + return new ArrayList(); + } + ArrayList infos = + new ArrayList(); + for (String json : strings) { + try { + JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); + Intent data = Intent.parseUri(object.getString(DATA_INTENT_KEY), 0); + Intent launchIntent = Intent.parseUri(object.getString(LAUNCH_INTENT_KEY), 0); + String name = object.getString(NAME_KEY); + String iconBase64 = object.optString(ICON_KEY); + String iconResourceName = object.optString(ICON_RESOURCE_NAME_KEY); + String iconResourcePackageName = + object.optString(ICON_RESOURCE_PACKAGE_NAME_KEY); + if (iconBase64 != null && !iconBase64.isEmpty()) { + byte[] iconArray = Base64.decode(iconBase64, Base64.DEFAULT); + Bitmap b = BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length); + data.putExtra(Intent.EXTRA_SHORTCUT_ICON, b); + } else if (iconResourceName != null && !iconResourceName.isEmpty()) { + Intent.ShortcutIconResource iconResource = + new Intent.ShortcutIconResource(); + iconResource.resourceName = iconResourceName; + iconResource.packageName = iconResourcePackageName; + data.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource); + } + data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent); + PendingInstallShortcutInfo info = + new PendingInstallShortcutInfo(data, name, launchIntent); + infos.add(info); + } catch (org.json.JSONException e) { + Log.d("InstallShortcutReceiver", "Exception reading shortcut to add: " + e); + } catch (java.net.URISyntaxException e) { + Log.d("InstallShortcutReceiver", "Exception reading shortcut to add: " + e); + } + } + sharedPrefs.edit().putStringSet(APPS_PENDING_INSTALL, new HashSet()).commit(); + return infos; + } + } + + // Determines whether to defer installing shortcuts immediately until + // processAllPendingInstalls() is called. + private static boolean mUseInstallQueue = false; + + private static class PendingInstallShortcutInfo { + Intent data; + Intent launchIntent; + String name; + Bitmap icon; + Intent.ShortcutIconResource iconResource; + + public PendingInstallShortcutInfo(Intent rawData, String shortcutName, + Intent shortcutIntent) { + data = rawData; + name = shortcutName; + launchIntent = shortcutIntent; + } + } + + public void onReceive(Context context, Intent data) { + if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) { + return; + } + + Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); + if (intent == null) { + return; + } + // This name is only used for comparisons and notifications, so fall back to activity name + // if not supplied + String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + if (name == null) { + try { + PackageManager pm = context.getPackageManager(); + ActivityInfo info = pm.getActivityInfo(intent.getComponent(), 0); + name = info.loadLabel(pm).toString(); + } catch (PackageManager.NameNotFoundException nnfe) { + return; + } + } + Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); + Intent.ShortcutIconResource iconResource = + data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE); + + // Queue the item up for adding if launcher has not loaded properly yet + boolean launcherNotLoaded = LauncherModel.getCellCountX() <= 0 || + LauncherModel.getCellCountY() <= 0; + + PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, name, intent); + info.icon = icon; + info.iconResource = iconResource; + if (mUseInstallQueue || launcherNotLoaded) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + addToInstallQueue(sp, info); + } else { + processInstallShortcut(context, info); + } + } + + static void enableInstallQueue() { + mUseInstallQueue = true; + } + static void disableAndFlushInstallQueue(Context context) { + mUseInstallQueue = false; + flushInstallQueue(context); + } + static void flushInstallQueue(Context context) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + ArrayList installQueue = getAndClearInstallQueue(sp); + Iterator iter = installQueue.iterator(); + while (iter.hasNext()) { + processInstallShortcut(context, iter.next()); + } + } + + private static void processInstallShortcut(Context context, + PendingInstallShortcutInfo pendingInfo) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + + final Intent data = pendingInfo.data; + final Intent intent = pendingInfo.launchIntent; + final String name = pendingInfo.name; + + // Lock on the app so that we don't try and get the items while apps are being added + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + final int[] result = {INSTALL_SHORTCUT_SUCCESSFUL}; + boolean found = false; + synchronized (app) { + // 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) + app.getModel().flushWorkerThread(); + final ArrayList items = LauncherModel.getItemsInLocalCoordinates(context); + final boolean exists = LauncherModel.shortcutExists(context, name, intent); + + // 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)) + final int screen = Launcher.DEFAULT_SCREEN; + for (int i = 0; i < (2 * Launcher.SCREEN_COUNT) + 1 && !found; ++i) { + int si = screen + (int) ((i / 2f) + 0.5f) * ((i % 2 == 1) ? 1 : -1); + if (0 <= si && si < Launcher.SCREEN_COUNT) { + found = installShortcut(context, data, items, name, intent, si, exists, sp, + result); + } + } + } + + // We only report error messages (duplicate shortcut or out of space) as the add-animation + // will provide feedback otherwise + if (!found) { + if (result[0] == INSTALL_SHORTCUT_NO_SPACE) { + Toast.makeText(context, context.getString(R.string.completely_out_of_space), + Toast.LENGTH_SHORT).show(); + } else if (result[0] == INSTALL_SHORTCUT_IS_DUPLICATE) { + Toast.makeText(context, context.getString(R.string.shortcut_duplicate, name), + Toast.LENGTH_SHORT).show(); + } + } + } + + private static boolean installShortcut(Context context, Intent data, ArrayList items, + String name, final Intent intent, final int screen, boolean shortcutExists, + final SharedPreferences sharedPrefs, int[] result) { + int[] tmpCoordinates = new int[2]; + if (findEmptyCell(context, items, tmpCoordinates, screen)) { + if (intent != null) { + if (intent.getAction() == null) { + intent.setAction(Intent.ACTION_VIEW); + } else if (intent.getAction().equals(Intent.ACTION_MAIN) && + intent.getCategories() != null && + intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + } + + // By default, we allow for duplicate entries (located in + // different places) + boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true); + if (duplicate || !shortcutExists) { + new Thread("setNewAppsThread") { + public void run() { + synchronized (sLock) { + // If the new app is going to fall into the same page as before, + // then just continue adding to the current page + final int newAppsScreen = sharedPrefs.getInt( + NEW_APPS_PAGE_KEY, screen); + SharedPreferences.Editor editor = sharedPrefs.edit(); + if (newAppsScreen == -1 || newAppsScreen == screen) { + addToStringSet(sharedPrefs, + editor, NEW_APPS_LIST_KEY, intent.toUri(0)); + } + editor.putInt(NEW_APPS_PAGE_KEY, screen); + editor.commit(); + } + } + }.start(); + + // Update the Launcher db + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + ShortcutInfo info = app.getModel().addShortcut(context, data, + LauncherSettings.Favorites.CONTAINER_DESKTOP, screen, + tmpCoordinates[0], tmpCoordinates[1], true); + if (info == null) { + return false; + } + } else { + result[0] = INSTALL_SHORTCUT_IS_DUPLICATE; + } + + return true; + } + } else { + result[0] = INSTALL_SHORTCUT_NO_SPACE; + } + + return false; + } + + private static boolean findEmptyCell(Context context, ArrayList items, int[] xy, + int screen) { + final int xCount = LauncherModel.getCellCountX(); + final int yCount = LauncherModel.getCellCountY(); + boolean[][] occupied = new boolean[xCount][yCount]; + + ItemInfo item = null; + int cellX, cellY, spanX, spanY; + for (int i = 0; i < items.size(); ++i) { + item = items.get(i); + if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + if (item.screen == 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; + } + } + } + } + } + + return CellLayout.findVacantCell(xy, 1, 1, xCount, yCount, occupied); + } +} diff --git a/app/src/main/java/com/android/launcher2/InstallWidgetReceiver.java b/app/src/main/java/com/android/launcher2/InstallWidgetReceiver.java new file mode 100644 index 0000000..a1e9b11 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/InstallWidgetReceiver.java @@ -0,0 +1,195 @@ +/* + * 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.launcher2; + +import java.util.List; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import com.android.launcher.R; + + +/** + * We will likely flesh this out later, to handle allow external apps to place widgets, but for now, + * we just want to expose the action around for checking elsewhere. + */ +public class InstallWidgetReceiver { + public static final String ACTION_INSTALL_WIDGET = + "com.android.launcher.action.INSTALL_WIDGET"; + public static final String ACTION_SUPPORTS_CLIPDATA_MIMETYPE = + "com.android.launcher.action.SUPPORTS_CLIPDATA_MIMETYPE"; + + // Currently not exposed. Put into Intent when we want to make it public. + // TEMP: Should we call this "EXTRA_APPWIDGET_PROVIDER"? + public static final String EXTRA_APPWIDGET_COMPONENT = + "com.android.launcher.extra.widget.COMPONENT"; + public static final String EXTRA_APPWIDGET_CONFIGURATION_DATA_MIME_TYPE = + "com.android.launcher.extra.widget.CONFIGURATION_DATA_MIME_TYPE"; + public static final String EXTRA_APPWIDGET_CONFIGURATION_DATA = + "com.android.launcher.extra.widget.CONFIGURATION_DATA"; + + /** + * A simple data class that contains per-item information that the adapter below can reference. + */ + public static class WidgetMimeTypeHandlerData { + public ResolveInfo resolveInfo; + public AppWidgetProviderInfo widgetInfo; + + public WidgetMimeTypeHandlerData(ResolveInfo rInfo, AppWidgetProviderInfo wInfo) { + resolveInfo = rInfo; + widgetInfo = wInfo; + } + } + + /** + * The ListAdapter which presents all the valid widgets that can be created for a given drop. + */ + public static class WidgetListAdapter implements ListAdapter, DialogInterface.OnClickListener { + private LayoutInflater mInflater; + private Launcher mLauncher; + private String mMimeType; + private ClipData mClipData; + private List mActivities; + private CellLayout mTargetLayout; + private int mTargetLayoutScreen; + private int[] mTargetLayoutPos; + + public WidgetListAdapter(Launcher l, String mimeType, ClipData data, + List list, CellLayout target, + int targetScreen, int[] targetPos) { + mLauncher = l; + mMimeType = mimeType; + mClipData = data; + mActivities = list; + mTargetLayout = target; + mTargetLayoutScreen = targetScreen; + mTargetLayoutPos = targetPos; + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + } + + @Override + public int getCount() { + return mActivities.size(); + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final Context context = parent.getContext(); + final PackageManager packageManager = context.getPackageManager(); + + // Lazy-create inflater + if (mInflater == null) { + mInflater = LayoutInflater.from(context); + } + + // Use the convert-view where possible + if (convertView == null) { + convertView = mInflater.inflate(R.layout.external_widget_drop_list_item, parent, + false); + } + + final WidgetMimeTypeHandlerData data = mActivities.get(position); + final ResolveInfo resolveInfo = data.resolveInfo; + final AppWidgetProviderInfo widgetInfo = data.widgetInfo; + + // Set the icon + Drawable d = resolveInfo.loadIcon(packageManager); + ImageView i = (ImageView) convertView.findViewById(R.id.provider_icon); + i.setImageDrawable(d); + + // Set the text + final CharSequence component = resolveInfo.loadLabel(packageManager); + final int[] widgetSpan = new int[2]; + mTargetLayout.rectToCell(widgetInfo.minWidth, widgetInfo.minHeight, widgetSpan); + TextView t = (TextView) convertView.findViewById(R.id.provider); + t.setText(context.getString(R.string.external_drop_widget_pick_format, + component, widgetSpan[0], widgetSpan[1])); + + return convertView; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return mActivities.isEmpty(); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final AppWidgetProviderInfo widgetInfo = mActivities.get(which).widgetInfo; + + final PendingAddWidgetInfo createInfo = new PendingAddWidgetInfo(widgetInfo, mMimeType, + mClipData); + mLauncher.addAppWidgetFromDrop(createInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, + mTargetLayoutScreen, null, null, mTargetLayoutPos); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/InterruptibleInOutAnimator.java b/app/src/main/java/com/android/launcher2/InterruptibleInOutAnimator.java new file mode 100644 index 0000000..375fddc --- /dev/null +++ b/app/src/main/java/com/android/launcher2/InterruptibleInOutAnimator.java @@ -0,0 +1,131 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.View; + +/** + * 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 + * a frame-by-frame mirror of the 'in' animation -- i.e., the interpolated values will + * be exactly reversed. Using this class, both the 'in' and the 'out' animation use the + * interpolator in the same direction. + */ +public class InterruptibleInOutAnimator { + private long mOriginalDuration; + private float mOriginalFromValue; + private float mOriginalToValue; + private ValueAnimator mAnimator; + + private boolean mFirstRun = true; + + private Object mTag = null; + + private static final int STOPPED = 0; + private static final int IN = 1; + 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; + + public InterruptibleInOutAnimator(View view, long duration, float fromValue, float toValue) { + mAnimator = LauncherAnimUtils.ofFloat(view, fromValue, toValue).setDuration(duration); + mOriginalDuration = duration; + mOriginalFromValue = fromValue; + mOriginalToValue = toValue; + + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDirection = STOPPED; + } + }); + } + + private void animate(int direction) { + final long currentPlayTime = mAnimator.getCurrentPlayTime(); + final float toValue = (direction == IN) ? mOriginalToValue : mOriginalFromValue; + final float startValue = mFirstRun ? mOriginalFromValue : + ((Float) mAnimator.getAnimatedValue()).floatValue(); + + // Make sure it's stopped before we modify any values + cancel(); + + // TODO: We don't really need to do the animation if startValue == toValue, but + // somehow that doesn't seem to work, possibly a quirk of the animation framework + mDirection = direction; + + // Ensure we don't calculate a non-sensical duration + long duration = mOriginalDuration - currentPlayTime; + mAnimator.setDuration(Math.max(0, Math.min(duration, mOriginalDuration))); + + mAnimator.setFloatValues(startValue, toValue); + mAnimator.start(); + mFirstRun = false; + } + + public void cancel() { + mAnimator.cancel(); + mDirection = STOPPED; + } + + public void end() { + mAnimator.end(); + mDirection = STOPPED; + } + + /** + * Return true when the animation is not running and it hasn't even been started. + */ + public boolean isStopped() { + return mDirection == STOPPED; + } + + /** + * This is the equivalent of calling Animator.start(), except that it can be called when + * the animation is running in the opposite direction, in which case we reverse + * direction and animate for a correspondingly shorter duration. + */ + public void animateIn() { + animate(IN); + } + + /** + * This is the roughly the equivalent of calling Animator.reverse(), except that it uses the + * same interpolation curve as animateIn(), rather than mirroring it. Also, like animateIn(), + * if the animation is currently running in the opposite direction, we reverse + * direction and animate for a correspondingly shorter duration. + */ + public void animateOut() { + animate(OUT); + } + + public void setTag(Object tag) { + mTag = tag; + } + + public Object getTag() { + return mTag; + } + + public ValueAnimator getAnimator() { + return mAnimator; + } +} diff --git a/app/src/main/java/com/android/launcher2/ItemInfo.java b/app/src/main/java/com/android/launcher2/ItemInfo.java new file mode 100644 index 0000000..165c07b --- /dev/null +++ b/app/src/main/java/com/android/launcher2/ItemInfo.java @@ -0,0 +1,194 @@ +/* + * 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.launcher2; + +import android.content.ContentValues; +import android.content.Intent; +import android.graphics.Bitmap; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Represents an item in the launcher. + */ +class ItemInfo { + + static final int NO_ID = -1; + + /** + * The id in the settings database for this item + */ + long id = NO_ID; + + /** + * One of {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION}, + * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT}, + * {@link LauncherSettings.Favorites#ITEM_TYPE_FOLDER}, or + * {@link LauncherSettings.Favorites#ITEM_TYPE_APPWIDGET}. + */ + int itemType; + + /** + * The id of the container that holds this item. For the desktop, this will be + * {@link LauncherSettings.Favorites#CONTAINER_DESKTOP}. For the all applications folder it + * will be {@link #NO_ID} (since it is not stored in the settings DB). For user folders + * it will be the id of the folder. + */ + long container = NO_ID; + + /** + * Iindicates the screen in which the shortcut appears. + */ + int screen = -1; + + /** + * Indicates the X position of the associated cell. + */ + int cellX = -1; + + /** + * Indicates the Y position of the associated cell. + */ + int cellY = -1; + + /** + * Indicates the X cell span. + */ + int spanX = 1; + + /** + * Indicates the Y cell span. + */ + int spanY = 1; + + /** + * Indicates the minimum X cell span. + */ + int minSpanX = 1; + + /** + * Indicates the minimum Y cell span. + */ + int minSpanY = 1; + + /** + * Indicates that this item needs to be updated in the db + */ + boolean requiresDbUpdate = false; + + /** + * Title of the item + */ + CharSequence title; + + /** + * The position of the item in a drag-and-drop operation. + */ + int[] dropPos = null; + + ItemInfo() { + } + + ItemInfo(ItemInfo info) { + id = info.id; + cellX = info.cellX; + cellY = info.cellY; + spanX = info.spanX; + spanY = info.spanY; + screen = info.screen; + itemType = info.itemType; + container = info.container; + // tempdebug: + LauncherModel.checkItemInfo(this); + } + + /** Returns the package name that the intent will resolve to, or an empty string if + * none exists. */ + static String getPackageName(Intent intent) { + if (intent != null) { + String packageName = intent.getPackage(); + if (packageName == null && intent.getComponent() != null) { + packageName = intent.getComponent().getPackageName(); + } + if (packageName != null) { + return packageName; + } + } + return ""; + } + + /** + * Write the fields of this item to the DB + * + * @param values + */ + void onAddToDatabase(ContentValues values) { + values.put(LauncherSettings.BaseLauncherColumns.ITEM_TYPE, itemType); + values.put(LauncherSettings.Favorites.CONTAINER, container); + values.put(LauncherSettings.Favorites.SCREEN, screen); + values.put(LauncherSettings.Favorites.CELLX, cellX); + values.put(LauncherSettings.Favorites.CELLY, cellY); + values.put(LauncherSettings.Favorites.SPANX, spanX); + values.put(LauncherSettings.Favorites.SPANY, spanY); + } + + 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); + values.put(LauncherSettings.Favorites.ICON, data); + } + } + + /** + * It is very important that sub-classes implement this if they contain any references + * to the activity (anything in the view hierarchy etc.). If not, leaks can result since + * ItemInfo objects persist across rotation and can hence leak by holding stale references + * to the old view hierarchy / activity. + */ + void unbind() { + } + + @Override + public String toString() { + return "Item(id=" + this.id + " type=" + this.itemType + " container=" + this.container + + " screen=" + screen + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + + " spanY=" + spanY + " dropPos=" + dropPos + ")"; + } +} diff --git a/app/src/main/java/com/android/launcher2/Launcher.java b/app/src/main/java/com/android/launcher2/Launcher.java new file mode 100644 index 0000000..c1eb4cb --- /dev/null +++ b/app/src/main/java/com/android/launcher2/Launcher.java @@ -0,0 +1,4059 @@ + +/* + * 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.launcher2; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.SearchManager; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +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.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +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.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Debug; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.os.StrictMode; +import android.os.SystemClock; +import android.os.UserManager; +import android.provider.Settings; +import android.speech.RecognizerIntent; +import android.text.Selection; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.method.TextKeyListener; +import android.util.Log; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.inputmethod.InputMethodManager; +import android.widget.Advanceable; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.common.Search; +import com.android.launcher.R; +import com.android.launcher2.DropTarget.DragObject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import lu.die.foza.SuperAPI.FozaCore; +import lu.die.fozacompatibility.FozaActivityManager; +import lu.die.fozacompatibility.FozaPackageManager; + +/** + * Default launcher application. + */ +public final class Launcher extends Activity + implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, + View.OnTouchListener { + 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_STRICT_MODE = false; + static final boolean DEBUG_RESUME_TIME = false; + + private static final int MENU_GROUP_WALLPAPER = 1; + private static final int MENU_WALLPAPER_SETTINGS = Menu.FIRST + 1; + private static final int MENU_MANAGE_APPS = MENU_WALLPAPER_SETTINGS + 1; + private static final int MENU_SYSTEM_SETTINGS = MENU_MANAGE_APPS + 1; + private static final int MENU_HELP = MENU_SYSTEM_SETTINGS + 1; + + private static final int REQUEST_CREATE_SHORTCUT = 1; + private static final int REQUEST_CREATE_APPWIDGET = 5; + private static final int REQUEST_PICK_APPLICATION = 6; + 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; + + static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate"; + + static final int SCREEN_COUNT = 5; + static final int DEFAULT_SCREEN = 2; + + private static final String PREFERENCES = "launcher.preferences"; + // 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"; + static final String DUMP_STATE_PROPERTY = "launcher_dump_state"; + + // The Intent extra that defines whether to ignore the launch animation + static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = + "com.android.launcher.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION"; + + // Type: int + private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen"; + // Type: int + private static final String RUNTIME_STATE = "launcher.state"; + // Type: int + private static final String RUNTIME_STATE_PENDING_ADD_CONTAINER = "launcher.add_container"; + // Type: int + private static final String RUNTIME_STATE_PENDING_ADD_SCREEN = "launcher.add_screen"; + // Type: int + 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 + private static final String RUNTIME_STATE_PENDING_ADD_SPAN_Y = "launcher.add_span_y"; + // Type: parcelable + private static final String RUNTIME_STATE_PENDING_ADD_WIDGET_INFO = "launcher.add_widget_info"; + + private static final String TOOLBAR_ICON_METADATA_NAME = "com.android.launcher.toolbar_icon"; + private static final String TOOLBAR_SEARCH_ICON_METADATA_NAME = + "com.android.launcher.toolbar_search_icon"; + private static final String TOOLBAR_VOICE_SEARCH_ICON_METADATA_NAME = + "com.android.launcher.toolbar_voice_search_icon"; + + /** 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; + private AnimatorSet mDividerAnimator; + + static final int APPWIDGET_HOST_ID = 1024; + private static final int EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT = 300; + private static final int EXIT_SPRINGLOADED_MODE_LONG_TIMEOUT = 600; + private static final int SHOW_CLING_DURATION = 550; + private static final int DISMISS_CLING_DURATION = 250; + + private static final Object sLock = new Object(); + private static int sScreen = DEFAULT_SCREEN; + + // How long to wait before the new-shortcut animation automatically pans the workspace + private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 10; + + private final BroadcastReceiver mCloseSystemDialogsReceiver + = new CloseSystemDialogsIntentReceiver(); + private final ContentObserver mWidgetObserver = new AppWidgetResetObserver(); + + private LayoutInflater mInflater; + + private Workspace mWorkspace; + private View mDockDivider; + private View mLauncherView; + private DragLayer mDragLayer; + private DragController mDragController; + + private AppWidgetManager mAppWidgetManager; + private LauncherAppWidgetHost mAppWidgetHost; + + private ItemInfo mPendingAddInfo = new ItemInfo(); + private AppWidgetProviderInfo mPendingAddWidgetInfo; + + private int[] mTmpAddItemCellCoordinates = new int[2]; + + private FolderInfo mFolderInfo; + + private Hotseat mHotseat; + private View mAllAppsButton; + + private SearchDropTargetBar mSearchDropTargetBar; + private AppsCustomizeTabHost mAppsCustomizeTabHost; + private AppsCustomizePagedView mAppsCustomizeContent; + private boolean mAutoAdvanceRunning = false; + + private Bundle mSavedState; + // We set the state in both onCreate and then onNewIntent in some cases, which causes both + // scroll issues (because the workspace may not have been measured yet) and extra work. + // Instead, just save the state that we need to restore Launcher to, and commit it in onResume. + private State mOnResumeState = State.NONE; + + private SpannableStringBuilder mDefaultKeySsb = null; + + private boolean mWorkspaceLoading = true; + + private boolean mPaused = true; + private boolean mRestoring; + private boolean mWaitingForResult; + private boolean mOnResumeNeedsLoad; + + private ArrayList mOnResumeCallbacks = new ArrayList(); + + // Keep track of whether the user has left launcher + private static boolean sPausedFromUserAction = false; + + private Bundle mSavedInstanceState; + + private LauncherModel mModel; + private IconCache mIconCache; + private boolean mUserPresent = true; + private boolean mVisible = false; + private boolean mAttached = false; + + private static LocaleConfiguration sLocaleConfiguration = null; + + private static HashMap sFolders = new HashMap(); + + private Intent mAppMarketIntent = null; + + // Related to the auto-advancing of widgets + private final int ADVANCE_MSG = 1; + private final int mAdvanceInterval = 20000; + private final int mAdvanceStagger = 250; + private long mAutoAdvanceSentTime; + private long mAutoAdvanceTimeLeft = -1; + private 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; + + // External icons saved in case of resource changes, orientation, etc. + private static Drawable.ConstantState[] sGlobalSearchIcon = new Drawable.ConstantState[2]; + private static Drawable.ConstantState[] sVoiceSearchIcon = new Drawable.ConstantState[2]; + private static Drawable.ConstantState[] sAppMarketIcon = new Drawable.ConstantState[2]; + + private Drawable mWorkspaceBackgroundDrawable; + + private final ArrayList mSynchronouslyBoundPages = new ArrayList(); + + static final ArrayList sDumpLogs = new ArrayList(); + + // We only want to get the SharedPreferences once since it does an FS stat each time we get + // it from the context. + private SharedPreferences mSharedPrefs; + + // 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 int mNewShortcutAnimatePage = -1; + private ArrayList mNewShortcutAnimateViews = new ArrayList(); + private ImageView mFolderIconImageView; + private Bitmap mFolderIconBitmap; + private Canvas mFolderIconCanvas; + private Rect mRectForFolderAnimation = new Rect(); + + private BubbleTextView mWaitingForResume; + + private HideFromAccessibilityHelper mHideFromAccessibilityHelper + = new HideFromAccessibilityHelper(); + + private Runnable mBuildLayersRunnable = new Runnable() { + public void run() { + if (mWorkspace != null) { + mWorkspace.buildPageHardwareLayers(); + } + } + }; + + private static ArrayList sPendingAddList + = new ArrayList(); + + private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); + + private static class PendingAddArguments { + int requestCode; + Intent intent; + long container; + int screen; + int cellX; + int cellY; + } + + private static boolean isPropertyEnabled(String propertyName) { + return Log.isLoggable(propertyName, Log.VERBOSE); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (DEBUG_STRICT_MODE) { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() // or .detectAll() for all detectable problems + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build()); + } + + super.onCreate(savedInstanceState); + LauncherApplication app = ((LauncherApplication)getApplication()); + mSharedPrefs = getSharedPreferences(LauncherApplication.getSharedPreferencesKey(), + Context.MODE_PRIVATE); + mModel = app.setLauncher(this); + mIconCache = app.getIconCache(); + mDragController = new DragController(this); + mInflater = getLayoutInflater(); + + mAppWidgetManager = AppWidgetManager.getInstance(this); + mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID); + mAppWidgetHost.startListening(); + + // If we are getting an onCreate, we can actually preempt onResume and unset mPaused here, + // this also ensures that any synchronous binding below doesn't re-trigger another + // LauncherModel load. + mPaused = false; + + if (PROFILE_STARTUP) { + Debug.startMethodTracing( + Environment.getExternalStorageDirectory() + "/launcher"); + } + + checkForLocaleChange(); + setContentView(R.layout.launcher); + setupViews(); + showFirstRunWorkspaceCling(); + + registerContentObservers(); + + lockAllApps(); + + mSavedState = savedInstanceState; + restoreState(mSavedState); + + // Update customization drawer _after_ restoring the states + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.onPackagesUpdated( + LauncherModel.getSortedWidgetsAndShortcuts(this)); + } + + if (PROFILE_STARTUP) { + Debug.stopMethodTracing(); + } + + if (!mRestoring) { + if (sPausedFromUserAction) { + // If the user leaves launcher, then we should just load items asynchronously when + // they return. + mModel.startLoader(true, -1); + } 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.getCurrentPage()); + } + } + + if (!mModel.isAllAppsLoaded()) { + ViewGroup appsCustomizeContentParent = (ViewGroup) mAppsCustomizeContent.getParent(); + mInflater.inflate(R.layout.apps_customize_progressbar, appsCustomizeContentParent); + } + + // For handling default keys + mDefaultKeySsb = new SpannableStringBuilder(); + Selection.setSelection(mDefaultKeySsb, 0); + + IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(mCloseSystemDialogsReceiver, filter, RECEIVER_EXPORTED); + } else registerReceiver(mCloseSystemDialogsReceiver, filter); + + updateGlobalIcons(); + + // On large interfaces, we want the screen to auto-rotate based on the current orientation + unlockScreenOrientation(true); + FozaCore.registerCoreCallback(() -> FozaPackageManager.get().acquireObtainAppSplash()); + } + + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + sPausedFromUserAction = true; + } + + private void updateGlobalIcons() { + boolean searchVisible = false; + boolean voiceVisible = false; + // If we have a saved version of these external icons, we load them up immediately + int coi = getCurrentOrientationIndexForGlobalIcons(); + if (sGlobalSearchIcon[coi] == null || sVoiceSearchIcon[coi] == null || + sAppMarketIcon[coi] == null) { + updateAppMarketIcon(); + searchVisible = updateGlobalSearchIcon(); + voiceVisible = updateVoiceSearchIcon(searchVisible); + } + if (sGlobalSearchIcon[coi] != null) { + updateGlobalSearchIcon(sGlobalSearchIcon[coi]); + searchVisible = true; + } + if (sVoiceSearchIcon[coi] != null) { + updateVoiceSearchIcon(sVoiceSearchIcon[coi]); + voiceVisible = true; + } + if (sAppMarketIcon[coi] != null) { + updateAppMarketIcon(sAppMarketIcon[coi]); + } + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.onSearchPackagesChanged(searchVisible, voiceVisible); + } + } + + 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 Thread("WriteLocaleConfiguration") { + @Override + public void run() { + writeConfiguration(Launcher.this, localeConfiguration); + } + }.start(); + } + } + + 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(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(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(PREFERENCES).delete(); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + // Ignore + } + } + } + } + + public DragLayer getDragLayer() { + return mDragLayer; + } + + 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; + } + } + + /** + * Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have + * a configuration step, this allows the proper animations to run after other transitions. + */ + private boolean completeAdd(PendingAddArguments args) { + boolean result = false; + switch (args.requestCode) { + case REQUEST_PICK_APPLICATION: + completeAddApplication(args.intent, args.container, args.screen, args.cellX, + args.cellY); + break; + case REQUEST_PICK_SHORTCUT: + processShortcut(args.intent); + break; + case REQUEST_CREATE_SHORTCUT: + completeAddShortcut(args.intent, args.container, args.screen, args.cellX, + args.cellY); + result = true; + break; + case REQUEST_CREATE_APPWIDGET: + int appWidgetId = args.intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); + completeAddAppWidget(appWidgetId, args.container, args.screen, null, null); + result = true; + break; + case REQUEST_PICK_WALLPAPER: + // We just wanted the activity result here so we can clear mWaitingForResult + break; + } + // Before adding this resetAddInfo(), after a shortcut was added to a workspace screen, + // if you turned the screen off and then back while in All Apps, Launcher would not + // return to the workspace. Clearing mAddInfo.container here fixes this issue + resetAddInfo(); + return result; + } + + @Override + protected void onActivityResult( + final int requestCode, final int resultCode, final Intent data) { + if (requestCode == REQUEST_BIND_APPWIDGET) { + int appWidgetId = data != null ? + data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1; + if (resultCode == RESULT_CANCELED) { + completeTwoStageWidgetDrop(RESULT_CANCELED, appWidgetId); + } else if (resultCode == RESULT_OK) { + addAppWidgetImpl(appWidgetId, mPendingAddInfo, null, mPendingAddWidgetInfo); + } + return; + } + boolean delayExitSpringLoadedMode = false; + boolean isWidgetDrop = (requestCode == REQUEST_PICK_APPWIDGET || + requestCode == REQUEST_CREATE_APPWIDGET); + mWaitingForResult = false; + + // We have special handling for widgets + if (isWidgetDrop) { + int appWidgetId = data != null ? + data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1; + if (appWidgetId < 0) { + Log.e(TAG, "Error: appWidgetId (EXTRA_APPWIDGET_ID) was not returned from the \\" + + "widget configuration activity."); + completeTwoStageWidgetDrop(RESULT_CANCELED, appWidgetId); + } else { + completeTwoStageWidgetDrop(resultCode, appWidgetId); + } + return; + } + + // The pattern used here is that a user PICKs a specific application, + // which, depending on the target, might need to CREATE the actual target. + + // For example, the user would PICK_SHORTCUT for "Music playlist", and we + // launch over to the Music app to actually CREATE_SHORTCUT. + if (resultCode == RESULT_OK && mPendingAddInfo.container != ItemInfo.NO_ID) { + final PendingAddArguments args = new PendingAddArguments(); + args.requestCode = requestCode; + args.intent = data; + args.container = mPendingAddInfo.container; + args.screen = mPendingAddInfo.screen; + args.cellX = mPendingAddInfo.cellX; + args.cellY = mPendingAddInfo.cellY; + if (isWorkspaceLocked()) { + sPendingAddList.add(args); + } else { + delayExitSpringLoadedMode = completeAdd(args); + } + } + mDragLayer.clearAnimatedView(); + // Exit spring loaded mode if necessary after cancelling the configuration of a widget + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), delayExitSpringLoadedMode, + null); + } + + private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) { + CellLayout cellLayout = + (CellLayout) mWorkspace.getChildAt(mPendingAddInfo.screen); + Runnable onCompleteRunnable = null; + int animationType = 0; + + AppWidgetHostView boundWidget = null; + if (resultCode == RESULT_OK) { + animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION; + final AppWidgetHostView layout = mAppWidgetHost.createView(this, appWidgetId, + mPendingAddWidgetInfo); + boundWidget = layout; + onCompleteRunnable = new Runnable() { + @Override + public void run() { + completeAddAppWidget(appWidgetId, mPendingAddInfo.container, + mPendingAddInfo.screen, layout, null); + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), false, + null); + } + }; + } else if (resultCode == RESULT_CANCELED) { + animationType = Workspace.CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION; + onCompleteRunnable = new Runnable() { + @Override + public void run() { + exitSpringLoadedDragModeDelayed((resultCode != RESULT_CANCELED), false, + null); + } + }; + } + if (mDragLayer.getAnimatedView() != null) { + mWorkspace.animateWidgetDrop(mPendingAddInfo, cellLayout, + (DragView) mDragLayer.getAnimatedView(), onCompleteRunnable, + animationType, boundWidget, true); + } else { + // The animated view may be null in the case of a rotation during widget configuration + onCompleteRunnable.run(); + } + } + + @Override + protected void onStop() { + super.onStop(); + FirstFrameAnimatorHelper.setIsVisible(false); + } + + @Override + protected void onStart() { + super.onStart(); + FirstFrameAnimatorHelper.setIsVisible(true); + } + + @Override + protected void onResume() { + long startTime = 0; + if (DEBUG_RESUME_TIME) { + startTime = System.currentTimeMillis(); + } + super.onResume(); + + // Restore the previous launcher state + if (mOnResumeState == State.WORKSPACE) { + showWorkspace(false); + } else if (mOnResumeState == State.APPS_CUSTOMIZE) { + showAllApps(false); + } + mOnResumeState = State.NONE; + + // Background was set to gradient in onPause(), restore to black if in all apps. + setWorkspaceBackground(mState == State.WORKSPACE); + + // Process any items that were added while Launcher was away + InstallShortcutReceiver.flushInstallQueue(this); + + mPaused = false; + sPausedFromUserAction = false; + if (mRestoring || mOnResumeNeedsLoad) { + mWorkspaceLoading = true; + mModel.startLoader(true, -1); + mRestoring = false; + mOnResumeNeedsLoad = false; + } + if (mOnResumeCallbacks.size() > 0) { + // We might have postponed some bind calls until onResume (see waitUntilResume) -- + // execute them here + long startTimeCallbacks = 0; + if (DEBUG_RESUME_TIME) { + startTimeCallbacks = System.currentTimeMillis(); + } + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.setBulkBind(true); + } + for (int i = 0; i < mOnResumeCallbacks.size(); i++) { + mOnResumeCallbacks.get(i).run(); + } + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.setBulkBind(false); + } + mOnResumeCallbacks.clear(); + if (DEBUG_RESUME_TIME) { + Log.d(TAG, "Time spent processing callbacks in onResume: " + + (System.currentTimeMillis() - startTimeCallbacks)); + } + } + + // Reset the pressed state of icons that were locked in the press state while activities + // were launching + if (mWaitingForResume != null) { + // Resets the previous workspace icon press state + mWaitingForResume.setStayPressed(false); + } + if (mAppsCustomizeContent != null) { + // Resets the previous all apps icon press state + mAppsCustomizeContent.resetDrawableState(); + } + // It is possible that widgets can receive updates while launcher is not in the foreground. + // Consequently, the widgets will be inflated in the orientation of the foreground activity + // (framework issue). On resuming, we ensure that any widgets are inflated for the current + // orientation. + getWorkspace().reinflateWidgetsIfNecessary(); + + // Again, as with the above scenario, it's possible that one or more of the global icons + // were updated in the wrong orientation. + updateGlobalIcons(); + if (DEBUG_RESUME_TIME) { + Log.d(TAG, "Time spent in onResume: " + (System.currentTimeMillis() - startTime)); + } + } + + @Override + protected void onPause() { + // NOTE: We want all transitions from launcher to act as if the wallpaper were enabled + // to be consistent. So re-enable the flag here, and we will re-disable it as necessary + // when Launcher resumes and we are still in AllApps. + updateWallpaperVisibility(true); + + super.onPause(); + mPaused = true; + mDragController.cancelDrag(); + mDragController.resetLastGestureUpTime(); + } + + @Override + public Object onRetainNonConfigurationInstance() { + // Flag the loader to stop early before switching + mModel.stopLoader(); + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.surrender(); + } + return Boolean.TRUE; + } + + // We can't hide the IME if it was forced open. So don't bother + /* + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + if (hasFocus) { + final InputMethodManager inputManager = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + WindowManager.LayoutParams lp = getWindow().getAttributes(); + inputManager.hideSoftInputFromWindow(lp.token, 0, new android.os.ResultReceiver(new + android.os.Handler()) { + protected void onReceiveResult(int resultCode, Bundle resultData) { + Log.d(TAG, "ResultReceiver got resultCode=" + resultCode); + } + }); + Log.d(TAG, "called hideSoftInputFromWindow from onWindowFocusChanged"); + } + } + */ + + private boolean acceptFilter() { + final InputMethodManager inputManager = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + return !inputManager.isFullscreenMode(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + final int uniChar = event.getUnicodeChar(); + final boolean handled = super.onKeyDown(keyCode, event); + final boolean isKeyNotWhitespace = uniChar > 0 && !Character.isWhitespace(uniChar); + if (!handled && acceptFilter() && isKeyNotWhitespace) { + boolean gotKey = TextKeyListener.getInstance().onKeyDown(mWorkspace, mDefaultKeySsb, + keyCode, event); + if (gotKey && mDefaultKeySsb != null && mDefaultKeySsb.length() > 0) { + // something usable has been typed - start a search + // the typed text will be retrieved and cleared by + // showSearchDialog() + // If there are multiple keystrokes before the search dialog takes focus, + // onSearchRequested() will be called for every keystroke, + // but it is idempotent, so it's fine. + return onSearchRequested(); + } + } + + // Eat the long press event so the keyboard doesn't come up. + if (keyCode == KeyEvent.KEYCODE_MENU && event.isLongPress()) { + return true; + } + + return handled; + } + + private String getTypedText() { + return mDefaultKeySsb.toString(); + } + + private void clearTypedText() { + mDefaultKeySsb.clear(); + mDefaultKeySsb.clearSpans(); + Selection.setSelection(mDefaultKeySsb, 0); + } + + /** + * Given the integer (ordinal) value of a State enum instance, convert it to a variable of type + * State + */ + private static State intToState(int stateOrdinal) { + State state = State.WORKSPACE; + final State[] stateValues = State.values(); + for (int i = 0; i < stateValues.length; i++) { + if (stateValues[i].ordinal() == stateOrdinal) { + state = stateValues[i]; + break; + } + } + return state; + } + + /** + * Restores the previous state, if it exists. + * + * @param savedState The previous state. + */ + private void restoreState(Bundle savedState) { + if (savedState == null) { + return; + } + + State state = intToState(savedState.getInt(RUNTIME_STATE, State.WORKSPACE.ordinal())); + if (state == State.APPS_CUSTOMIZE) { + mOnResumeState = State.APPS_CUSTOMIZE; + } + + int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN, -1); + if (currentScreen > -1) { + mWorkspace.setCurrentPage(currentScreen); + } + + final long pendingAddContainer = savedState.getLong(RUNTIME_STATE_PENDING_ADD_CONTAINER, -1); + final int pendingAddScreen = savedState.getInt(RUNTIME_STATE_PENDING_ADD_SCREEN, -1); + + if (pendingAddContainer != ItemInfo.NO_ID && pendingAddScreen > -1) { + mPendingAddInfo.container = pendingAddContainer; + mPendingAddInfo.screen = pendingAddScreen; + mPendingAddInfo.cellX = savedState.getInt(RUNTIME_STATE_PENDING_ADD_CELL_X); + 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); + mWaitingForResult = 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); + } + } + + /** + * Finds all the views we need and configure them properly. + */ + private void setupViews() { + final DragController dragController = mDragController; + + mLauncherView = findViewById(R.id.launcher); + mDragLayer = (DragLayer) findViewById(R.id.drag_layer); + mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace); + mDockDivider = findViewById(R.id.dock_divider); + + mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + mWorkspaceBackgroundDrawable = getResources().getDrawable(R.drawable.workspace_bg); + + // Setup the drag layer + mDragLayer.setup(this, dragController); + + // Setup the hotseat + mHotseat = (Hotseat) findViewById(R.id.hotseat); + if (mHotseat != null) { + mHotseat.setup(this); + } + + // Setup the workspace + mWorkspace.setHapticFeedbackEnabled(false); + mWorkspace.setOnLongClickListener(this); + mWorkspace.setup(dragController); + dragController.addDragListener(mWorkspace); + + // Get the search/delete bar + mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.qsb_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 the drag controller (drop targets have to be added in reverse order in priority) + dragController.setDragScoller(mWorkspace); + dragController.setScrollView(mDragLayer); + dragController.setMoveTarget(mWorkspace); + dragController.addDropTarget(mWorkspace); + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.setup(this, dragController); + } + } + + /** + * 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); + } + + /** + * 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); + favorite.setOnClickListener(this); + return favorite; + } + + /** + * Add an application shortcut to the workspace. + * + * @param data The intent describing the application. + */ + void completeAddApplication(Intent data, long container, int screen, int cellX, int cellY) { + final int[] cellXY = mTmpAddItemCellCoordinates; + final CellLayout layout = getCellLayout(container, screen); + + // 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; + cellXY[1] = cellY; + } else if (!layout.findCellForSpan(cellXY, 1, 1)) { + showOutOfSpaceMessage(isHotseatLayout(layout)); + return; + } + + final ShortcutInfo info = mModel.getShortcutInfo(getPackageManager(), data, this); + + if (info != null) { + info.setActivity(data.getComponent(), Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + info.container = ItemInfo.NO_ID; + mWorkspace.addApplicationShortcut(info, layout, container, screen, cellXY[0], cellXY[1], + isWorkspaceLocked(), cellX, cellY); + } else { + Log.e(TAG, "Couldn't find ActivityInfo for selected application: " + data); + } + } + + /** + * Add a shortcut to the workspace. + * + * @param data The intent describing the shortcut. + */ + private void completeAddShortcut(Intent data, long container, int screen, int cellX, + int cellY) { + int[] cellXY = mTmpAddItemCellCoordinates; + int[] touchXY = mPendingAddInfo.dropPos; + CellLayout layout = getCellLayout(container, screen); + + boolean foundCellSpan = false; + + ShortcutInfo info = mModel.infoFromShortcutIntent(this, data, null); + if (info == null) { + return; + } + final View view = createShortcut(info); + + // 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; + cellXY[1] = cellY; + foundCellSpan = true; + + // If appropriate, either create a folder or add to an existing folder + if (mWorkspace.createUserFolderIfNecessary(view, container, layout, cellXY, 0, + true, null,null)) { + return; + } + DragObject dragObject = new DragObject(); + dragObject.dragInfo = info; + if (mWorkspace.addToExistingFolderIfNecessary(view, layout, cellXY, 0, dragObject, + true)) { + return; + } + } else if (touchXY != null) { + // when dragging and dropping, just find the closest free spot + int[] result = layout.findNearestVacantArea(touchXY[0], touchXY[1], 1, 1, cellXY); + foundCellSpan = (result != null); + } else { + foundCellSpan = layout.findCellForSpan(cellXY, 1, 1); + } + + if (!foundCellSpan) { + showOutOfSpaceMessage(isHotseatLayout(layout)); + return; + } + + LauncherModel.addItemToDatabase(this, info, container, screen, cellXY[0], cellXY[1], false); + + if (!mRestoring) { + mWorkspace.addInScreen(view, container, screen, cellXY[0], cellXY[1], 1, 1, + isWorkspaceLocked()); + } + } + + static int[] getSpanForWidget(Context context, ComponentName component, int minWidth, + int minHeight) { + Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context, 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(context.getResources(), requiredWidth, requiredHeight, null); + } + + static int[] getSpanForWidget(Context context, AppWidgetProviderInfo info) { + return getSpanForWidget(context, 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); + } + + /** + * Add a widget to the workspace. + * + * @param appWidgetId The app widget id + */ + private void completeAddAppWidget(final int appWidgetId, long container, int screen, + AppWidgetHostView hostView, AppWidgetProviderInfo appWidgetInfo) { + if (appWidgetInfo == null) { + appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); + } + + // Calculate the grid spans needed to fit this widget + CellLayout layout = getCellLayout(container, screen); + + 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 (!foundCellSpan) { + if (appWidgetId != -1) { + // Deleting an app widget ID is a void call but writes to disk before returning + // to the caller... + new Thread("deleteAppWidgetId") { + public void run() { + mAppWidgetHost.deleteAppWidgetId(appWidgetId); + } + }.start(); + } + 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; + + LauncherModel.addItemToDatabase(this, launcherInfo, + container, screen, cellXY[0], cellXY[1], false); + + if (!mRestoring) { + if (hostView == null) { + // Perform actual inflation because we're live + launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + launcherInfo.hostView.setAppWidget(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, screen, cellXY[0], cellXY[1], + launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); + + addWidgetToAutoAdvanceIfNeeded(launcherInfo.hostView, appWidgetInfo); + } + resetAddInfo(); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_SCREEN_OFF.equals(action)) { + mUserPresent = false; + mDragLayer.clearAllResizeFrames(); + updateRunning(); + + // 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) { + mAppsCustomizeTabHost.reset(); + showWorkspace(false); + } + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + mUserPresent = true; + updateRunning(); + } + } + }; + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Listen for broadcasts related to user-presence + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(mReceiver, filter, RECEIVER_EXPORTED); + } else registerReceiver(mReceiver, filter); + FirstFrameAnimatorHelper.initializeDrawListener(getWindow().getDecorView()); + mAttached = true; + mVisible = true; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mVisible = false; + + if (mAttached) { + unregisterReceiver(mReceiver); + mAttached = false; + } + updateRunning(); + } + + public void onWindowVisibilityChanged(int visibility) { + mVisible = visibility == View.VISIBLE; + updateRunning(); + // 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 + // layers on all the workspace pages, so that transitioning to Launcher from other + // apps is nice and speedy. + observer.addOnDrawListener(new ViewTreeObserver.OnDrawListener() { + private boolean mStarted = false; + public void onDraw() { + if (mStarted) return; + mStarted = true; + // We delay the layer building a bit in order to give + // other message processing a time to run. In particular + // this avoids a delay in hiding the IME if it was + // currently shown, because doing that may involve + // some communication back with the app. + mWorkspace.postDelayed(mBuildLayersRunnable, 500); + final ViewTreeObserver.OnDrawListener listener = this; + mWorkspace.post(new Runnable() { + public void run() { + if (mWorkspace != null && + mWorkspace.getViewTreeObserver() != null) { + mWorkspace.getViewTreeObserver(). + removeOnDrawListener(listener); + } + } + }); + return; + } + }); + } + // When Launcher comes back to foreground, a different Activity might be responsible for + // the app market intent, so refresh the icon + updateAppMarketIcon(); + clearTypedText(); + } + } + + private void sendAdvanceMessage(long delay) { + mHandler.removeMessages(ADVANCE_MSG); + Message msg = mHandler.obtainMessage(ADVANCE_MSG); + mHandler.sendMessageDelayed(msg, delay); + mAutoAdvanceSentTime = System.currentTimeMillis(); + } + + private void updateRunning() { + boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty(); + if (autoAdvanceRunning != mAutoAdvanceRunning) { + mAutoAdvanceRunning = autoAdvanceRunning; + if (autoAdvanceRunning) { + long delay = mAutoAdvanceTimeLeft == -1 ? mAdvanceInterval : mAutoAdvanceTimeLeft; + sendAdvanceMessage(delay); + } else { + if (!mWidgetsToAdvance.isEmpty()) { + mAutoAdvanceTimeLeft = Math.max(0, mAdvanceInterval - + (System.currentTimeMillis() - mAutoAdvanceSentTime)); + } + mHandler.removeMessages(ADVANCE_MSG); + mHandler.removeMessages(0); // Remove messages sent using postDelayed() + } + } + } + + private final Handler mHandler = new Handler() { + @Override + public void 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() { + public void run() { + ((Advanceable) v).advance(); + } + }, delay); + } + i++; + } + sendAdvanceMessage(mAdvanceInterval); + } + } + }; + + void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) { + if (appWidgetInfo == null || appWidgetInfo.autoAdvanceViewId == -1) return; + View v = hostView.findViewById(appWidgetInfo.autoAdvanceViewId); + if (v instanceof Advanceable) { + mWidgetsToAdvance.put(hostView, appWidgetInfo); + ((Advanceable) v).fyiWillBeAdvancedByHostKThx(); + updateRunning(); + } + } + + void removeWidgetToAutoAdvance(View hostView) { + if (mWidgetsToAdvance.containsKey(hostView)) { + mWidgetsToAdvance.remove(hostView); + updateRunning(); + } + } + + public void removeAppWidget(LauncherAppWidgetInfo launcherInfo) { + removeWidgetToAutoAdvance(launcherInfo.hostView); + launcherInfo.hostView = null; + } + + 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(); + } + + public LauncherAppWidgetHost getAppWidgetHost() { + return mAppWidgetHost; + } + + public LauncherModel getModel() { + return mModel; + } + + void closeSystemDialogs() { + getWindow().closeAllPanels(); + + // Whatever we were doing is hereby canceled. + mWaitingForResult = false; + } + + @Override + protected void onNewIntent(Intent intent) { + long startTime = 0; + if (DEBUG_RESUME_TIME) { + startTime = System.currentTimeMillis(); + } + super.onNewIntent(intent); + + // Close the menu + if (Intent.ACTION_MAIN.equals(intent.getAction())) { + // also will cancel mWaitingForResult. + closeSystemDialogs(); + + final boolean alreadyOnHome = + ((intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) + != Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); + + Runnable processIntent = new Runnable() { + public void run() { + if (mWorkspace == null) { + // Can be cases where mWorkspace is null, this prevents a NPE + return; + } + Folder openFolder = mWorkspace.getOpenFolder(); + // In all these cases, only animate if we're already on home + mWorkspace.exitWidgetResizeMode(); + if (alreadyOnHome && mState == State.WORKSPACE && !mWorkspace.isTouchActive() && + openFolder == null) { + mWorkspace.moveToDefaultScreen(true); + } + + closeFolder(); + exitSpringLoadedDragMode(); + + // If we are already on home, then just animate back to the workspace, + // otherwise, just wait until onResume to set the state back to Workspace + if (alreadyOnHome) { + showWorkspace(true); + } else { + mOnResumeState = State.WORKSPACE; + } + + final View v = getWindow().peekDecorView(); + if (v != null && v.getWindowToken() != null) { + InputMethodManager imm = (InputMethodManager)getSystemService( + INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + + // Reset AllApps to its initial state + if (!alreadyOnHome && mAppsCustomizeTabHost != null) { + mAppsCustomizeTabHost.reset(); + } + } + }; + + if (alreadyOnHome && !mWorkspace.hasWindowFocus()) { + // Delay processing of the intent to allow the status bar animation to finish + // first in order to avoid janky animations. + mWorkspace.postDelayed(processIntent, 350); + } else { + // Process the intent immediately. + processIntent.run(); + } + + } + if (DEBUG_RESUME_TIME) { + Log.d(TAG, "Time spent in onNewIntent: " + (System.currentTimeMillis() - startTime)); + } + } + + @Override + public void onRestoreInstanceState(Bundle state) { + super.onRestoreInstanceState(state); + for (int page: mSynchronouslyBoundPages) { + mWorkspace.restoreInstanceStateForChild(page); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putInt(RUNTIME_STATE_CURRENT_SCREEN, mWorkspace.getNextPage()); + super.onSaveInstanceState(outState); + + outState.putInt(RUNTIME_STATE, mState.ordinal()); + // We close any open folder since it will not be re-opened, and we need to make sure + // this state is reflected. + closeFolder(); + + if (mPendingAddInfo.container != ItemInfo.NO_ID && mPendingAddInfo.screen > -1 && + mWaitingForResult) { + outState.putLong(RUNTIME_STATE_PENDING_ADD_CONTAINER, mPendingAddInfo.container); + outState.putInt(RUNTIME_STATE_PENDING_ADD_SCREEN, mPendingAddInfo.screen); + outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_X, mPendingAddInfo.cellX); + outState.putInt(RUNTIME_STATE_PENDING_ADD_CELL_Y, mPendingAddInfo.cellY); + outState.putInt(RUNTIME_STATE_PENDING_ADD_SPAN_X, mPendingAddInfo.spanX); + outState.putInt(RUNTIME_STATE_PENDING_ADD_SPAN_Y, mPendingAddInfo.spanY); + outState.putParcelable(RUNTIME_STATE_PENDING_ADD_WIDGET_INFO, mPendingAddWidgetInfo); + } + + 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) { + String currentTabTag = mAppsCustomizeTabHost.getCurrentTabTag(); + if (currentTabTag != null) { + outState.putString("apps_customize_currentTab", currentTabTag); + } + int currentIndex = mAppsCustomizeContent.getSaveInstanceStateIndex(); + outState.putInt("apps_customize_currentIndex", currentIndex); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + // Remove all pending runnables + mHandler.removeMessages(ADVANCE_MSG); + mHandler.removeMessages(0); + mWorkspace.removeCallbacks(mBuildLayersRunnable); + + // Stop callbacks from LauncherModel + LauncherApplication app = ((LauncherApplication) getApplication()); + mModel.stopLoader(); + app.setLauncher(null); + + try { + mAppWidgetHost.stopListening(); + } catch (NullPointerException ex) { + Log.w(TAG, "problem while stopping AppWidgetHost during Launcher destruction", ex); + } + mAppWidgetHost = null; + + mWidgetsToAdvance.clear(); + + 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(); + ((ViewGroup) mWorkspace.getParent()).removeAllViews(); + mWorkspace.removeAllViews(); + mWorkspace = null; + mDragController = null; + + LauncherAnimUtils.onDestroyActivity(); + } + + public DragController getDragController() { + return mDragController; + } + + @Override + public void startActivityForResult(Intent intent, int requestCode) { + if (requestCode >= 0) mWaitingForResult = true; + super.startActivityForResult(intent, requestCode); + } + + /** + * Indicates that we want global search for this activity by setting the globalSearch + * argument for {@link #startSearch} to true. + */ + @Override + 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(); + } + if (appSearchData == null) { + appSearchData = new Bundle(); + appSearchData.putString(Search.SOURCE, "launcher-search"); + } + Rect sourceBounds = new Rect(); + if (mSearchDropTargetBar != null) { + sourceBounds = mSearchDropTargetBar.getSearchBarBounds(); + } + + startGlobalSearch(initialQuery, selectInitialQuery, + appSearchData, sourceBounds); + } + + /** + * Starts the global search activity. This code is a copied from SearchManager + */ + public void startGlobalSearch(String initialQuery, + boolean selectInitialQuery, Bundle appSearchData, Rect sourceBounds) { + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName globalSearchActivity = searchManager.getGlobalSearchActivity(); + if (globalSearchActivity == null) { + Log.w(TAG, "No global search activity found."); + return; + } + Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setComponent(globalSearchActivity); + // Make sure that we have a Bundle to put source in + if (appSearchData == null) { + appSearchData = new Bundle(); + } else { + appSearchData = new Bundle(appSearchData); + } + // Set source to package name of app that starts global search, if not set already. + if (!appSearchData.containsKey("source")) { + appSearchData.putString("source", getPackageName()); + } + intent.putExtra(SearchManager.APP_DATA, appSearchData); + if (!TextUtils.isEmpty(initialQuery)) { + intent.putExtra(SearchManager.QUERY, initialQuery); + } + if (selectInitialQuery) { + intent.putExtra(SearchManager.EXTRA_SELECT_QUERY, selectInitialQuery); + } + intent.setSourceBounds(sourceBounds); + try { + startActivity(intent); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "Global search activity not found: " + globalSearchActivity); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (isWorkspaceLocked()) { + return false; + } + + super.onCreateOptionsMenu(menu); + + Intent manageApps = new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS); + manageApps.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + Intent settings = new Intent(android.provider.Settings.ACTION_SETTINGS); + settings.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + String helpUrl = getString(R.string.help_url); + Intent help = new Intent(Intent.ACTION_VIEW, Uri.parse(helpUrl)); + help.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + + menu.add(MENU_GROUP_WALLPAPER, MENU_WALLPAPER_SETTINGS, 0, R.string.menu_wallpaper) + .setIcon(android.R.drawable.ic_menu_gallery) + .setAlphabeticShortcut('W'); + menu.add(0, MENU_MANAGE_APPS, 0, R.string.menu_manage_apps) + .setIcon(android.R.drawable.ic_menu_manage) + .setIntent(manageApps) + .setAlphabeticShortcut('M'); + menu.add(0, MENU_SYSTEM_SETTINGS, 0, R.string.menu_settings) + .setIcon(android.R.drawable.ic_menu_preferences) + .setIntent(settings) + .setAlphabeticShortcut('P'); + if (!helpUrl.isEmpty()) { + menu.add(0, MENU_HELP, 0, R.string.menu_help) + .setIcon(android.R.drawable.ic_menu_help) + .setIntent(help) + .setAlphabeticShortcut('H'); + } + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + if (mAppsCustomizeTabHost.isTransitioning()) { + return false; + } + boolean allAppsVisible = (mAppsCustomizeTabHost.getVisibility() == View.VISIBLE); + menu.setGroupVisible(MENU_GROUP_WALLPAPER, !allAppsVisible); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_WALLPAPER_SETTINGS: + startWallpaper(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onSearchRequested() { + startSearch(null, false, null, true); + // Use a custom animation for launching search + return true; + } + + public boolean isWorkspaceLocked() { + return mWorkspaceLoading || mWaitingForResult; + } + + private void resetAddInfo() { + mPendingAddInfo.container = ItemInfo.NO_ID; + mPendingAddInfo.screen = -1; + mPendingAddInfo.cellX = mPendingAddInfo.cellY = -1; + mPendingAddInfo.spanX = mPendingAddInfo.spanY = -1; + mPendingAddInfo.minSpanX = mPendingAddInfo.minSpanY = -1; + mPendingAddInfo.dropPos = null; + } + + void addAppWidgetImpl(final int appWidgetId, ItemInfo info, AppWidgetHostView boundWidget, + AppWidgetProviderInfo appWidgetInfo) { + if (appWidgetInfo.configure != null) { + mPendingAddWidgetInfo = appWidgetInfo; + + // Launch over to configure widget, if needed + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); + intent.setComponent(appWidgetInfo.configure); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET); + } else { + // Otherwise just add it + completeAddAppWidget(appWidgetId, info.container, info.screen, boundWidget, + appWidgetInfo); + // Exit spring loaded mode if necessary after adding the widget + exitSpringLoadedDragModeDelayed(true, false, null); + } + } + + /** + * Process a shortcut drop. + * + * @param componentName The name of the component + * @param screen The screen where it should be added + * @param cell The cell it should be added to, optional + */ + void processShortcutFromDrop(ComponentName componentName, long container, int screen, + int[] cell, int[] loc) { + resetAddInfo(); + mPendingAddInfo.container = container; + mPendingAddInfo.screen = screen; + mPendingAddInfo.dropPos = loc; + + if (cell != null) { + mPendingAddInfo.cellX = cell[0]; + mPendingAddInfo.cellY = cell[1]; + } + + Intent createShortcutIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); + createShortcutIntent.setComponent(componentName); + processShortcut(createShortcutIntent); + } + + /** + * Process a widget drop. + * + * @param info The PendingAppWidgetInfo of the widget being added. + * @param screen The screen where it should be added + * @param cell The cell it should be added to, optional + */ + void addAppWidgetFromDrop(PendingAddWidgetInfo info, long container, int screen, + int[] cell, int[] span, int[] loc) { + resetAddInfo(); + mPendingAddInfo.container = info.container = container; + mPendingAddInfo.screen = info.screen = screen; + mPendingAddInfo.dropPos = loc; + mPendingAddInfo.minSpanX = info.minSpanX; + mPendingAddInfo.minSpanY = info.minSpanY; + + if (cell != null) { + mPendingAddInfo.cellX = cell[0]; + mPendingAddInfo.cellY = cell[1]; + } + if (span != null) { + mPendingAddInfo.spanX = span[0]; + mPendingAddInfo.spanY = span[1]; + } + + AppWidgetHostView hostView = info.boundWidget; + int appWidgetId; + if (hostView != null) { + appWidgetId = hostView.getAppWidgetId(); + addAppWidgetImpl(appWidgetId, info, hostView, info.info); + } 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. + appWidgetId = getAppWidgetHost().allocateAppWidgetId(); + Bundle options = info.bindOptions; + + boolean success = false; + if (options != null) { + success = mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, + info.componentName, options); + } else { + success = mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, + info.componentName); + } + if (success) { + addAppWidgetImpl(appWidgetId, info, null, info.info); + } else { + mPendingAddWidgetInfo = info.info; + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.componentName); + // TODO: we need to make sure that this accounts for the options bundle. + // intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options); + startActivityForResult(intent, REQUEST_BIND_APPWIDGET); + } + } + } + + void processShortcut(Intent intent) { + // Handle case where user selected "Applications" + String applicationName = getResources().getString(R.string.group_applications); + String shortcutName = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + + if (applicationName != null && applicationName.equals(shortcutName)) { + Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + Intent pickIntent = new Intent(Intent.ACTION_PICK_ACTIVITY); + pickIntent.putExtra(Intent.EXTRA_INTENT, mainIntent); + pickIntent.putExtra(Intent.EXTRA_TITLE, getText(R.string.title_select_application)); + startActivityForResultSafely(pickIntent, REQUEST_PICK_APPLICATION); + } else { + startActivityForResultSafely(intent, REQUEST_CREATE_SHORTCUT); + } + } + + void processWallpaper(Intent intent) { + startActivityForResult(intent, REQUEST_PICK_WALLPAPER); + } + + FolderIcon addFolder(CellLayout layout, long container, final int screen, int cellX, + int cellY) { + final FolderInfo folderInfo = new FolderInfo(); + folderInfo.title = ""; + + // Update the model + LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screen, cellX, cellY, + false); + sFolders.put(folderInfo.id, folderInfo); + + // Create the view + FolderIcon newFolder = + FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo, mIconCache); + mWorkspace.addInScreen(newFolder, container, screen, cellX, cellY, 1, 1, + isWorkspaceLocked()); + return newFolder; + } + + void removeFolder(FolderInfo folder) { + sFolders.remove(folder.id); + } + + private void startWallpaper() { + showWorkspace(true); + final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER); + Intent chooser = Intent.createChooser(pickWallpaper, + getText(R.string.chooser_wallpaper)); + // NOTE: Adds a configure option to the chooser if the wallpaper supports it + // Removed in Eclair MR1 +// WallpaperManager wm = (WallpaperManager) +// getSystemService(Context.WALLPAPER_SERVICE); +// WallpaperInfo wi = wm.getWallpaperInfo(); +// if (wi != null && wi.getSettingsActivity() != null) { +// LabeledIntent li = new LabeledIntent(getPackageName(), +// R.string.configure_wallpaper, 0); +// li.setClassName(wi.getPackageName(), wi.getSettingsActivity()); +// chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { li }); +// } + startActivityForResult(chooser, REQUEST_PICK_WALLPAPER); + } + + /** + * 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) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_HOME: + return true; + case KeyEvent.KEYCODE_VOLUME_DOWN: + if (isPropertyEnabled(DUMP_STATE_PROPERTY)) { + dumpState(); + return true; + } + break; + } + } else if (event.getAction() == KeyEvent.ACTION_UP) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_HOME: + return true; + } + } + + return super.dispatchKeyEvent(event); + } + + @Override + public void onBackPressed() { + if (isAllAppsVisible()) { + showWorkspace(true); + } else if (mWorkspace.getOpenFolder() != null) { + Folder openFolder = mWorkspace.getOpenFolder(); + if (openFolder.isEditingName()) { + openFolder.dismissEditingName(); + } else { + closeFolder(); + } + } else if(getDragLayer().hasResizeFrames()) { + mWorkspace.exitWidgetResizeMode(); + + // Back button is a no-op here, but give at least some feedback for the button press + mWorkspace.showOutlinesTemporarily(); + } else super.onBackPressed(); + } + + /** + * Re-listen when widgets are reset. + */ + private void onAppWidgetReset() { + if (mAppWidgetHost != null) { + mAppWidgetHost.startListening(); + } + } + + /** + * Launches the intent referred by the clicked shortcut. + * + * @param v The view representing the clicked shortcut. + */ + public void onClick(View v) { + // Make sure that rogue clicks don't get through while allapps is launching, or after the + // view has detached (it's possible for this to happen if the view is removed mid touch). + if (v.getWindowToken() == null) { + return; + } + + if (!mWorkspace.isFinishedSwitchingState()) { + return; + } + + Object tag = v.getTag(); + if (tag instanceof ShortcutInfo) { + // Open shortcut +// final Intent intent = ((ShortcutInfo) tag).intent; + int[] pos = new int[2]; + v.getLocationOnScreen(pos); + if(LauncherUtils.isSystemApplication( + this, + ((ShortcutInfo) tag).intent.getComponent().getPackageName() + )) + { + ((ShortcutInfo) tag).intent.setSourceBounds(new Rect(pos[0], pos[1], + pos[0] + v.getWidth(), pos[1] + v.getHeight())); + + boolean success = startActivitySafely(v, ((ShortcutInfo) tag).intent, tag); + } + else + { + try{ + Intent reserveIntent = FozaActivityManager.get().obtainSplashLaunchIntent( + 0, + ((ShortcutInfo) tag).intent.getComponent().getPackageName(), + this + ); + if(reserveIntent != null) startActivity(reserveIntent); + }catch (Exception e) + { + } + } + + if (v instanceof BubbleTextView) { + mWaitingForResume = (BubbleTextView) v; + mWaitingForResume.setStayPressed(true); + } + } else if (tag instanceof FolderInfo) { + if (v instanceof FolderIcon) { + FolderIcon fi = (FolderIcon) v; + handleFolderClick(fi); + } + } else if (v == mAllAppsButton) { + if (isAllAppsVisible()) { + showWorkspace(true); + } else { + onClickAllAppsButton(v); + } + } + } + + public boolean onTouch(View v, MotionEvent event) { + // this is an intercepted event being forwarded from mWorkspace; + // clicking anywhere on the workspace causes the customization drawer to slide down + showWorkspace(true); + return false; + } + + /** + * Event handler for the search button + * + * @param v The view that was clicked. + */ + public void onClickSearchButton(View v) { + v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + + onSearchRequested(); + } + + /** + * Event handler for the voice button + * + * @param v The view that was clicked. + */ + public void onClickVoiceButton(View v) { + v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + + try { + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName activityName = searchManager.getGlobalSearchActivity(); + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (activityName != null) { + intent.setPackage(activityName.getPackageName()); + } + startActivity(null, intent, "onClickVoiceButton"); + } catch (ActivityNotFoundException e) { + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivitySafely(null, intent, "onClickVoiceButton"); + } + } + + /** + * Event handler for the "grid" button that appears on the home screen, which + * enters all apps mode. + * + * @param v The view that was clicked. + */ + public void onClickAllAppsButton(View v) { + showAllApps(true); + } + + public void onTouchDownAllAppsButton(View v) { + // Provide the same haptic feedback that the system offers for virtual keys. + v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + } + + public void onClickAppMarketButton(View v) { + if (mAppMarketIntent != null) { + startActivitySafely(v, mAppMarketIntent, "app market"); + } else { + Log.e(TAG, "Invalid app market intent."); + } + } + + void startApplicationDetailsActivity(ComponentName componentName) { + String packageName = componentName.getPackageName(); + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", packageName, null)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + startActivitySafely(null, intent, "startApplicationDetailsActivity"); + } + + void startApplicationUninstallActivity(ApplicationInfo appInfo) { + if(FozaPackageManager.get().isInnerAppInstalled(appInfo.componentName.getPackageName())) + { + FozaPackageManager.get().uninstallAppFully(appInfo.componentName.getPackageName()); + int messageId = R.string.cling_dismiss; + Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show(); + return; + } + if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) == 0) { + // System applications cannot be installed. For now, show a toast explaining that. + // We may give them the option of disabling apps this way. + int messageId = R.string.uninstall_system_app_text; + Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show(); + } else { + String packageName = appInfo.componentName.getPackageName(); + String className = appInfo.componentName.getClassName(); + Intent intent = new Intent( + Intent.ACTION_DELETE, Uri.fromParts("package", packageName, className)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + startActivity(intent); + } + } + + 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 + // private contract between launcher and may be ignored in the future). + boolean useLaunchAnimation = (v != null) && + !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION); + if (useLaunchAnimation) { + ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, + v.getMeasuredWidth(), v.getMeasuredHeight()); + + startActivity(intent, opts.toBundle()); + } else { + startActivity(intent); + } + return true; + } catch (SecurityException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Launcher does not have the permission to launch " + intent + + ". Make sure to create a MAIN intent-filter for the corresponding activity " + + "or use the exported attribute for this activity. " + + "tag="+ tag + " intent=" + intent, e); + } + return false; + } + + boolean startActivitySafely(View v, Intent intent, Object tag) { + boolean success = false; + try { + success = startActivity(v, intent, tag); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Unable to launch. tag=" + tag + " intent=" + intent, e); + } + return success; + } + + void startActivityForResultSafely(Intent intent, int requestCode) { + try { + startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + } catch (SecurityException e) { + Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Launcher does not have the permission to launch " + intent + + ". Make sure to create a MAIN intent-filter for the corresponding activity " + + "or use the exported attribute for this activity.", e); + } + } + + private void handleFolderClick(FolderIcon folderIcon) { + final FolderInfo info = folderIcon.getFolderInfo(); + Folder openFolder = mWorkspace.getFolderForTag(info); + + // If the folder info reports that the associated folder is open, then verify that + // it is actually opened. There have been a few instances where this gets out of sync. + if (info.opened && openFolder == null) { + Log.d(TAG, "Folder info marked as open, but associated folder is not open. Screen: " + + info.screen + " (" + info.cellX + ", " + info.cellY + ")"); + info.opened = false; + } + + if (!info.opened && !folderIcon.getFolder().isDestroyed()) { + // Close any open folder + closeFolder(); + // Open the requested folder + openFolder(folderIcon); + } else { + // Find the open folder... + int folderScreen; + if (openFolder != null) { + folderScreen = mWorkspace.getPageForView(openFolder); + // .. and close it + closeFolder(openFolder); + if (folderScreen != mWorkspace.getCurrentPage()) { + // Close any folder open on the current screen + closeFolder(); + // Pull the folder onto this screen + openFolder(folderIcon); + } + } + } + } + + /** + * This method draws the FolderIcon to an ImageView and then adds and positions that ImageView + * in the DragLayer in the exact absolute location of the original FolderIcon. + */ + private void copyFolderIconToImage(FolderIcon fi) { + final int width = fi.getMeasuredWidth(); + final int height = fi.getMeasuredHeight(); + + // Lazy load ImageView, Bitmap and Canvas + if (mFolderIconImageView == null) { + mFolderIconImageView = new ImageView(this); + } + if (mFolderIconBitmap == null || mFolderIconBitmap.getWidth() != width || + mFolderIconBitmap.getHeight() != height) { + mFolderIconBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + mFolderIconCanvas = new Canvas(mFolderIconBitmap); + } + + DragLayer.LayoutParams lp; + if (mFolderIconImageView.getLayoutParams() instanceof DragLayer.LayoutParams) { + lp = (DragLayer.LayoutParams) mFolderIconImageView.getLayoutParams(); + } else { + lp = new DragLayer.LayoutParams(width, height); + } + + // The layout from which the folder is being opened may be scaled, adjust the starting + // view size by this scale factor. + float scale = mDragLayer.getDescendantRectRelativeToSelf(fi, mRectForFolderAnimation); + lp.customPosition = true; + lp.x = mRectForFolderAnimation.left; + lp.y = mRectForFolderAnimation.top; + lp.width = (int) (scale * width); + lp.height = (int) (scale * height); + + mFolderIconCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + fi.draw(mFolderIconCanvas); + mFolderIconImageView.setImageBitmap(mFolderIconBitmap); + if (fi.getFolder() != null) { + mFolderIconImageView.setPivotX(fi.getFolder().getPivotXForIconAnimation()); + mFolderIconImageView.setPivotY(fi.getFolder().getPivotYForIconAnimation()); + } + // Just in case this image view is still in the drag layer from a previous animation, + // we remove it and re-add it. + if (mDragLayer.indexOfChild(mFolderIconImageView) != -1) { + mDragLayer.removeView(mFolderIconImageView); + } + mDragLayer.addView(mFolderIconImageView, lp); + if (fi.getFolder() != null) { + fi.getFolder().bringToFront(); + } + } + + private void growAndFadeOutFolderIcon(FolderIcon fi) { + if (fi == null) return; + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.5f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.5f); + + FolderInfo info = (FolderInfo) fi.getTag(); + if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + CellLayout cl = (CellLayout) fi.getParent().getParent(); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) fi.getLayoutParams(); + cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY); + } + + // Push an ImageView copy of the FolderIcon into the DragLayer and hide the original + copyFolderIconToImage(fi); + fi.setVisibility(View.INVISIBLE); + + ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha, + scaleX, scaleY); + oa.setDuration(getResources().getInteger(R.integer.config_folderAnimDuration)); + oa.start(); + } + + private void shrinkAndFadeInFolderIcon(final FolderIcon fi) { + if (fi == null) return; + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); + + final CellLayout cl = (CellLayout) fi.getParent().getParent(); + + // We remove and re-draw the FolderIcon in-case it has changed + mDragLayer.removeView(mFolderIconImageView); + copyFolderIconToImage(fi); + ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha, + scaleX, scaleY); + oa.setDuration(getResources().getInteger(R.integer.config_folderAnimDuration)); + oa.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (cl != null) { + cl.clearFolderLeaveBehind(); + // Remove the ImageView copy of the FolderIcon and make the original visible. + mDragLayer.removeView(mFolderIconImageView); + fi.setVisibility(View.VISIBLE); + } + } + }); + oa.start(); + } + + /** + * Opens the user folder described by the specified tag. The opening of the folder + * is animated relative to the specified View. If the View is null, no animation + * is played. + */ + public void openFolder(FolderIcon folderIcon) { + Folder folder = folderIcon.getFolder(); + FolderInfo info = folder.mInfo; + + info.opened = true; + + // 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) { + mDragLayer.addView(folder); + mDragController.addDropTarget((DropTarget) folder); + } else { + Log.w(TAG, "Opening folder (" + folder + ") which already has a parent (" + + folder.getParent() + ")."); + } + folder.animateOpen(); + growAndFadeOutFolderIcon(folderIcon); + + // Notify the accessibility manager that this folder "window" has appeared and occluded + // the workspace items + folder.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + public void closeFolder() { + Folder folder = mWorkspace.getOpenFolder(); + if (folder != null) { + if (folder.isEditingName()) { + folder.dismissEditingName(); + } + closeFolder(folder); + + // Dismiss the folder cling + dismissFolderCling(null); + } + } + + 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); + } + folder.animateClosed(); + + // Notify the accessibility manager that this folder "window" has disappeard and no + // longer occludeds the workspace items + getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + public boolean onLongClick(View v) { + if (!isDraggingEnabled()) return false; + if (isWorkspaceLocked()) return false; + if (mState != State.WORKSPACE) return false; + + if (!(v instanceof CellLayout)) { + v = (View) v.getParent().getParent(); + } + + resetAddInfo(); + CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag(); + // This happens when long clicking an item with the dpad/trackball + if (longClickCellInfo == null) { + return true; + } + + // The hotseat touch handling does not go through Workspace, and we always allow long press + // on hotseat items. + final View itemUnderLongClick = longClickCellInfo.cell; + boolean allowLongPress = isHotseatLayout(v) || mWorkspace.allowLongPress(); + if (allowLongPress && !mDragController.isDragging()) { + if (itemUnderLongClick == null) { + // User long pressed on empty space + mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + startWallpaper(); + } else { + if (!(itemUnderLongClick instanceof Folder)) { + // User long pressed on an item + mWorkspace.startDrag(longClickCellInfo); + } + } + } + return true; + } + + boolean isHotseatLayout(View layout) { + return mHotseat != null && layout != null && + (layout instanceof CellLayout) && (layout == mHotseat.getLayout()); + } + Hotseat getHotseat() { + return mHotseat; + } + SearchDropTargetBar getSearchBar() { + return mSearchDropTargetBar; + } + + /** + * Returns the CellLayout of the specified container at the specified screen. + */ + CellLayout getCellLayout(long container, int screen) { + if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + if (mHotseat != null) { + return mHotseat.getLayout(); + } else { + return null; + } + } else { + return (CellLayout) mWorkspace.getChildAt(screen); + } + } + + Workspace getWorkspace() { + return mWorkspace; + } + + // Now a part of LauncherModel.Callbacks. Used to reorder loading steps. + @Override + public boolean isAllAppsVisible() { + return (mState == State.APPS_CUSTOMIZE) || (mOnResumeState == State.APPS_CUSTOMIZE); + } + + @Override + public boolean isAllAppsButtonRank(int rank) { + return mHotseat.isAllAppsButtonRank(rank); + } + + /** + * Helper method for the cameraZoomIn/cameraZoomOut animations + * @param view The view being animated + * @param scaleFactor The scale factor used for the zoom + */ + private void setPivotsForZoom(View view, float scaleFactor) { + view.setPivotX(view.getWidth() / 2.0f); + view.setPivotY(view.getHeight() / 2.0f); + } + + void disableWallpaperIfInAllApps() { + // Only disable it if we are in all apps + if (isAllAppsVisible()) { + if (mAppsCustomizeTabHost != null && + !mAppsCustomizeTabHost.isTransitioning()) { + updateWallpaperVisibility(false); + } + } + } + + private void setWorkspaceBackground(boolean workspace) { + mLauncherView.setBackground(workspace ? + mWorkspaceBackgroundDrawable : null); + } + + void updateWallpaperVisibility(boolean visible) { + int wpflags = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0; + int curflags = getWindow().getAttributes().flags + & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + 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) { + if (mStateAnimation != null) { + mStateAnimation.setDuration(0); + mStateAnimation.cancel(); + mStateAnimation = null; + } + final Resources res = getResources(); + + final int duration = res.getInteger(R.integer.config_appsCustomizeZoomInTime); + final int fadeDuration = res.getInteger(R.integer.config_appsCustomizeFadeInTime); + final float scale = (float) res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); + final View fromView = mWorkspace; + final AppsCustomizeTabHost toView = mAppsCustomizeTabHost; + final int startDelay = + res.getInteger(R.integer.config_workspaceAppsCustomizeAnimationStagger); + + setPivotsForZoom(toView, scale); + + // Shrink workspaces away if going to AppsCustomize from workspace + Animator workspaceAnim = + mWorkspace.getChangeStateAnimation(Workspace.State.SMALL, animated); + + if (animated) { + toView.setScaleX(scale); + toView.setScaleY(scale); + final LauncherViewPropertyAnimator scaleAnim = new LauncherViewPropertyAnimator(toView); + scaleAnim. + scaleX(1f).scaleY(1f). + setDuration(duration). + setInterpolator(new Workspace.ZoomOutInterpolator()); + + toView.setVisibility(View.VISIBLE); + toView.setAlpha(0f); + final ObjectAnimator alphaAnim = LauncherAnimUtils + .ofFloat(toView, "alpha", 0f, 1f) + .setDuration(fadeDuration); + alphaAnim.setInterpolator(new DecelerateInterpolator(1.5f)); + alphaAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (animation == null) { + throw new RuntimeException("animation is null"); + } + float t = (Float) animation.getAnimatedValue(); + dispatchOnLauncherTransitionStep(fromView, t); + dispatchOnLauncherTransitionStep(toView, t); + } + }); + + // toView should appear right at the end of the workspace shrink + // animation + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + mStateAnimation.play(scaleAnim).after(startDelay); + mStateAnimation.play(alphaAnim).after(startDelay); + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + boolean animationCancelled = false; + + @Override + public void onAnimationStart(Animator animation) { + updateWallpaperVisibility(true); + // Prepare the position + toView.setTranslationX(0.0f); + toView.setTranslationY(0.0f); + toView.setVisibility(View.VISIBLE); + toView.bringToFront(); + } + @Override + public void onAnimationEnd(Animator animation) { + dispatchOnLauncherTransitionEnd(fromView, animated, false); + dispatchOnLauncherTransitionEnd(toView, animated, false); + + if (mWorkspace != null && !springLoaded && !LauncherApplication.isScreenLarge()) { + // Hide the workspace scrollbar + mWorkspace.hideScrollingIndicator(true); + } + if (!animationCancelled) { + updateWallpaperVisibility(false); + } + + // Hide the search bar + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.hideSearchBar(false); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + animationCancelled = true; + } + }); + + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + + boolean delayAnim = false; + + dispatchOnLauncherTransitionPrepare(fromView, animated, false); + dispatchOnLauncherTransitionPrepare(toView, animated, false); + + // If any of the objects being animated haven't been measured/laid out + // yet, delay the animation until we get a layout pass + if ((((LauncherTransitionable) toView).getContent().getMeasuredWidth() == 0) || + (mWorkspace.getMeasuredWidth() == 0) || + (toView.getMeasuredWidth() == 0)) { + delayAnim = true; + } + + 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; + setPivotsForZoom(toView, scale); + dispatchOnLauncherTransitionStart(fromView, animated, false); + dispatchOnLauncherTransitionStart(toView, animated, false); + LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView); + } + }; + if (delayAnim) { + final ViewTreeObserver observer = toView.getViewTreeObserver(); + observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + public void onGlobalLayout() { + startAnimRunnable.run(); + toView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + } else { + startAnimRunnable.run(); + } + } 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 && !LauncherApplication.isScreenLarge()) { + // Hide the workspace scrollbar + mWorkspace.hideScrollingIndicator(true); + + // 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); + updateWallpaperVisibility(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(State toState, final boolean animated, + final boolean springLoaded, final Runnable onCompleteRunnable) { + + if (mStateAnimation != null) { + mStateAnimation.setDuration(0); + mStateAnimation.cancel(); + mStateAnimation = null; + } + Resources res = getResources(); + + final int duration = res.getInteger(R.integer.config_appsCustomizeZoomOutTime); + final int fadeOutDuration = + res.getInteger(R.integer.config_appsCustomizeFadeOutTime); + final float scaleFactor = (float) + res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor); + final View fromView = mAppsCustomizeTabHost; + final View toView = mWorkspace; + Animator workspaceAnim = null; + + if (toState == State.WORKSPACE) { + int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger); + workspaceAnim = mWorkspace.getChangeStateAnimation( + Workspace.State.NORMAL, animated, stagger); + } else if (toState == State.APPS_CUSTOMIZE_SPRING_LOADED) { + workspaceAnim = mWorkspace.getChangeStateAnimation( + Workspace.State.SPRING_LOADED, animated); + } + + setPivotsForZoom(fromView, scaleFactor); + updateWallpaperVisibility(true); + showHotseat(animated); + if (animated) { + final LauncherViewPropertyAnimator scaleAnim = + new LauncherViewPropertyAnimator(fromView); + scaleAnim. + scaleX(scaleFactor).scaleY(scaleFactor). + setDuration(duration). + setInterpolator(new Workspace.ZoomInInterpolator()); + + final ObjectAnimator alphaAnim = LauncherAnimUtils + .ofFloat(fromView, "alpha", 1f, 0f) + .setDuration(fadeOutDuration); + alphaAnim.setInterpolator(new AccelerateDecelerateInterpolator()); + alphaAnim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = 1f - (Float) animation.getAnimatedValue(); + dispatchOnLauncherTransitionStep(fromView, t); + dispatchOnLauncherTransitionStep(toView, t); + } + }); + + mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + + dispatchOnLauncherTransitionPrepare(fromView, animated, true); + dispatchOnLauncherTransitionPrepare(toView, animated, true); + mAppsCustomizeContent.pauseScrolling(); + + mStateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + updateWallpaperVisibility(true); + fromView.setVisibility(View.GONE); + dispatchOnLauncherTransitionEnd(fromView, animated, true); + dispatchOnLauncherTransitionEnd(toView, animated, true); + if (mWorkspace != null) { + mWorkspace.hideScrollingIndicator(false); + } + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + mAppsCustomizeContent.updateCurrentPageScroll(); + mAppsCustomizeContent.resumeScrolling(); + } + }); + + mStateAnimation.playTogether(scaleAnim, alphaAnim); + if (workspaceAnim != null) { + mStateAnimation.play(workspaceAnim); + } + dispatchOnLauncherTransitionStart(fromView, animated, true); + dispatchOnLauncherTransitionStart(toView, animated, true); + LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView); + } 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); + mWorkspace.hideScrollingIndicator(false); + } + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + mAppsCustomizeTabHost.onTrimMemory(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (!hasFocus) { + // When another window occludes launcher (like the notification shade, or recents), + // ensure that we enable the wallpaper flag so that transitions are done correctly. + updateWallpaperVisibility(true); + } else { + // When launcher has focus again, disable the wallpaper if we are in AllApps + mWorkspace.postDelayed(new Runnable() { + @Override + public void run() { + disableWallpaperIfInAllApps(); + } + }, 500); + } + } + + void showWorkspace(boolean animated) { + showWorkspace(animated, null); + } + + void showWorkspace(boolean animated, Runnable onCompleteRunnable) { + if (mState != State.WORKSPACE) { + boolean wasInSpringLoadedMode = (mState == State.APPS_CUSTOMIZE_SPRING_LOADED); + mWorkspace.setVisibility(View.VISIBLE); + hideAppsCustomizeHelper(State.WORKSPACE, animated, false, onCompleteRunnable); + + // Show the search bar (only animate if we were showing the drop target bar in spring + // loaded mode) + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.showSearchBar(wasInSpringLoadedMode); + } + + // Set focus to the AppsCustomize button + if (mAllAppsButton != null) { + mAllAppsButton.requestFocus(); + } + } + + mWorkspace.flashScrollingIndicator(animated); + + // Change the state *after* we've called all the transition code + mState = State.WORKSPACE; + + // Resume the auto-advance of widgets + mUserPresent = true; + updateRunning(); + + // Send an accessibility event to announce the context change + getWindow().getDecorView() + .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void showAllApps(boolean animated) { + if (mState != State.WORKSPACE) return; + + showAppsCustomizeHelper(animated, false); + mAppsCustomizeTabHost.requestFocus(); + + // Change the state *after* we've called all the transition code + mState = State.APPS_CUSTOMIZE; + + // Pause the auto-advance of widgets until we are out of AllApps + mUserPresent = false; + updateRunning(); + closeFolder(); + + // Send an accessibility event to announce the context change + getWindow().getDecorView() + .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void enterSpringLoadedDragMode() { + if (isAllAppsVisible()) { + hideAppsCustomizeHelper(State.APPS_CUSTOMIZE_SPRING_LOADED, true, true, null); + mState = State.APPS_CUSTOMIZE_SPRING_LOADED; + } + } + + void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, boolean extendedDelay, + final Runnable onCompleteRunnable) { + if (mState != State.APPS_CUSTOMIZE_SPRING_LOADED) return; + + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (successfulDrop) { + // 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); + showWorkspace(true, onCompleteRunnable); + } else { + exitSpringLoadedDragMode(); + } + } + }, (extendedDelay ? + EXIT_SPRINGLOADED_MODE_LONG_TIMEOUT : + EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT)); + } + + void exitSpringLoadedDragMode() { + if (mState == State.APPS_CUSTOMIZE_SPRING_LOADED) { + final boolean animated = true; + final boolean springLoaded = true; + showAppsCustomizeHelper(animated, springLoaded); + mState = State.APPS_CUSTOMIZE; + } + // Otherwise, we are not in spring loaded mode, so don't do anything. + } + + void lockAllApps() { + // TODO + } + + void unlockAllApps() { + // TODO + } + + /** + * Shows the hotseat area. + */ + void showHotseat(boolean animated) { + if (!LauncherApplication.isScreenLarge()) { + if (animated) { + if (mHotseat.getAlpha() != 1f) { + int duration = 0; + if (mSearchDropTargetBar != null) { + duration = mSearchDropTargetBar.getTransitionInDuration(); + } + mHotseat.animate().alpha(1f).setDuration(duration); + } + } else { + mHotseat.setAlpha(1f); + } + } + } + + /** + * Hides the hotseat area. + */ + void hideHotseat(boolean animated) { + if (!LauncherApplication.isScreenLarge()) { + if (animated) { + if (mHotseat.getAlpha() != 0f) { + int duration = 0; + if (mSearchDropTargetBar != null) { + duration = mSearchDropTargetBar.getTransitionOutDuration(); + } + mHotseat.animate().alpha(0f).setDuration(duration); + } + } else { + mHotseat.setAlpha(0f); + } + } + } + + /** + * Add an item from all apps or customize onto the given workspace screen. + * If layout is null, add to the current screen. + */ + void addExternalItemToScreen(ItemInfo itemInfo, final CellLayout layout) { + if (!mWorkspace.addExternalItemToScreen(itemInfo, layout)) { + showOutOfSpaceMessage(isHotseatLayout(layout)); + } + } + + /** Maps the current orientation to an index for referencing orientation correct global icons */ + private int getCurrentOrientationIndexForGlobalIcons() { + // default - 0, landscape - 1 + switch (getResources().getConfiguration().orientation) { + case Configuration.ORIENTATION_LANDSCAPE: + return 1; + default: + return 0; + } + } + + private Drawable getExternalPackageToolbarIcon(ComponentName activityName, String resourceName) { + try { + PackageManager packageManager = getPackageManager(); + // Look for the toolbar icon specified in the activity meta-data + Bundle metaData = packageManager.getActivityInfo( + activityName, PackageManager.GET_META_DATA).metaData; + if (metaData != null) { + int iconResId = metaData.getInt(resourceName); + if (iconResId != 0) { + Resources res = packageManager.getResourcesForActivity(activityName); + return res.getDrawable(iconResId); + } + } + } catch (NameNotFoundException e) { + // This can happen if the activity defines an invalid drawable + Log.w(TAG, "Failed to load toolbar icon; " + activityName.flattenToShortString() + + " not found", e); + } catch (Resources.NotFoundException nfe) { + // This can happen if the activity defines an invalid drawable + Log.w(TAG, "Failed to load toolbar icon from " + activityName.flattenToShortString(), + nfe); + } + return null; + } + + // if successful in getting icon, return it; otherwise, set button to use default drawable + private Drawable.ConstantState updateTextButtonWithIconFromExternalActivity( + int buttonId, ComponentName activityName, int fallbackDrawableId, + String toolbarResourceName) { + Drawable toolbarIcon = getExternalPackageToolbarIcon(activityName, toolbarResourceName); + Resources r = getResources(); + int w = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_width); + int h = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_height); + + TextView button = (TextView) findViewById(buttonId); + // If we were unable to find the icon via the meta-data, use a generic one + if (toolbarIcon == null) { + toolbarIcon = r.getDrawable(fallbackDrawableId); + toolbarIcon.setBounds(0, 0, w, h); + if (button != null) { + button.setCompoundDrawables(toolbarIcon, null, null, null); + } + return null; + } else { + toolbarIcon.setBounds(0, 0, w, h); + if (button != null) { + button.setCompoundDrawables(toolbarIcon, null, null, null); + } + return toolbarIcon.getConstantState(); + } + } + + // if successful in getting icon, return it; otherwise, set button to use default drawable + private Drawable.ConstantState updateButtonWithIconFromExternalActivity( + int buttonId, ComponentName activityName, int fallbackDrawableId, + String toolbarResourceName) { + ImageView button = (ImageView) findViewById(buttonId); + Drawable toolbarIcon = getExternalPackageToolbarIcon(activityName, toolbarResourceName); + + if (button != null) { + // If we were unable to find the icon via the meta-data, use a + // generic one + if (toolbarIcon == null) { + button.setImageResource(fallbackDrawableId); + } else { + button.setImageDrawable(toolbarIcon); + } + } + + return toolbarIcon != null ? toolbarIcon.getConstantState() : null; + + } + + private void updateTextButtonWithDrawable(int buttonId, Drawable d) { + TextView button = (TextView) findViewById(buttonId); + button.setCompoundDrawables(d, null, null, null); + } + + private void updateButtonWithDrawable(int buttonId, Drawable.ConstantState d) { + ImageView button = (ImageView) findViewById(buttonId); + button.setImageDrawable(d.newDrawable(getResources())); + } + + private void invalidatePressedFocusedStates(View container, View button) { + if (container instanceof HolographicLinearLayout) { + HolographicLinearLayout layout = (HolographicLinearLayout) container; + layout.invalidatePressedFocusedStates(); + } else if (button instanceof HolographicImageView) { + HolographicImageView view = (HolographicImageView) button; + view.invalidatePressedFocusedStates(); + } + } + + private boolean updateGlobalSearchIcon() { + final View searchButtonContainer = findViewById(R.id.search_button_container); + final ImageView searchButton = (ImageView) findViewById(R.id.search_button); + final View voiceButtonContainer = findViewById(R.id.voice_button_container); + final View voiceButton = findViewById(R.id.voice_button); + final View voiceButtonProxy = findViewById(R.id.voice_button_proxy); + + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName activityName = searchManager.getGlobalSearchActivity(); + if (activityName != null) { + int coi = getCurrentOrientationIndexForGlobalIcons(); + sGlobalSearchIcon[coi] = updateButtonWithIconFromExternalActivity( + R.id.search_button, activityName, R.drawable.ic_home_search_normal_holo, + TOOLBAR_SEARCH_ICON_METADATA_NAME); + if (sGlobalSearchIcon[coi] == null) { + sGlobalSearchIcon[coi] = updateButtonWithIconFromExternalActivity( + R.id.search_button, activityName, R.drawable.ic_home_search_normal_holo, + TOOLBAR_ICON_METADATA_NAME); + } + + if (searchButtonContainer != null) searchButtonContainer.setVisibility(View.VISIBLE); + searchButton.setVisibility(View.VISIBLE); + invalidatePressedFocusedStates(searchButtonContainer, searchButton); + return true; + } else { + // We disable both search and voice search when there is no global search provider + if (searchButtonContainer != null) searchButtonContainer.setVisibility(View.GONE); + if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.GONE); + searchButton.setVisibility(View.GONE); + voiceButton.setVisibility(View.GONE); + if (voiceButtonProxy != null) { + voiceButtonProxy.setVisibility(View.GONE); + } + return false; + } + } + + private void updateGlobalSearchIcon(Drawable.ConstantState d) { + final View searchButtonContainer = findViewById(R.id.search_button_container); + final View searchButton = (ImageView) findViewById(R.id.search_button); + updateButtonWithDrawable(R.id.search_button, d); + invalidatePressedFocusedStates(searchButtonContainer, searchButton); + } + + private boolean updateVoiceSearchIcon(boolean searchVisible) { + final View voiceButtonContainer = findViewById(R.id.voice_button_container); + final View voiceButton = findViewById(R.id.voice_button); + final View voiceButtonProxy = findViewById(R.id.voice_button_proxy); + + // We only show/update the voice search icon if the search icon is enabled as well + final SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + ComponentName globalSearchActivity = searchManager.getGlobalSearchActivity(); + + ComponentName activityName = null; + if (globalSearchActivity != null) { + // Check if the global search activity handles voice search + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + intent.setPackage(globalSearchActivity.getPackageName()); + activityName = intent.resolveActivity(getPackageManager()); + } + + if (activityName == null) { + // Fallback: check if an activity other than the global search activity + // resolves this + Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + activityName = intent.resolveActivity(getPackageManager()); + } + if (searchVisible && activityName != null) { + int coi = getCurrentOrientationIndexForGlobalIcons(); + sVoiceSearchIcon[coi] = updateButtonWithIconFromExternalActivity( + R.id.voice_button, activityName, R.drawable.ic_home_voice_search_holo, + TOOLBAR_VOICE_SEARCH_ICON_METADATA_NAME); + if (sVoiceSearchIcon[coi] == null) { + sVoiceSearchIcon[coi] = updateButtonWithIconFromExternalActivity( + R.id.voice_button, activityName, R.drawable.ic_home_voice_search_holo, + TOOLBAR_ICON_METADATA_NAME); + } + if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.VISIBLE); + voiceButton.setVisibility(View.VISIBLE); + if (voiceButtonProxy != null) { + voiceButtonProxy.setVisibility(View.VISIBLE); + } + invalidatePressedFocusedStates(voiceButtonContainer, voiceButton); + return true; + } else { + if (voiceButtonContainer != null) voiceButtonContainer.setVisibility(View.GONE); + voiceButton.setVisibility(View.GONE); + if (voiceButtonProxy != null) { + voiceButtonProxy.setVisibility(View.GONE); + } + return false; + } + } + + private void updateVoiceSearchIcon(Drawable.ConstantState d) { + final View voiceButtonContainer = findViewById(R.id.voice_button_container); + final View voiceButton = findViewById(R.id.voice_button); + updateButtonWithDrawable(R.id.voice_button, d); + invalidatePressedFocusedStates(voiceButtonContainer, voiceButton); + } + + /** + * Sets the app market icon + */ + private void updateAppMarketIcon() { + final View marketButton = findViewById(R.id.market_button); + Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_APP_MARKET); + // Find the app market activity by resolving an intent. + // (If multiple app markets are installed, it will return the ResolverActivity.) + ComponentName activityName = intent.resolveActivity(getPackageManager()); + if (activityName != null) { + int coi = getCurrentOrientationIndexForGlobalIcons(); + mAppMarketIntent = intent; + sAppMarketIcon[coi] = updateTextButtonWithIconFromExternalActivity( + R.id.market_button, activityName, R.drawable.ic_launcher_market_holo, + TOOLBAR_ICON_METADATA_NAME); + marketButton.setVisibility(View.VISIBLE); + } else { + // We should hide and disable the view so that we don't try and restore the visibility + // of it when we swap between drag & normal states from IconDropTarget subclasses. + marketButton.setVisibility(View.GONE); + marketButton.setEnabled(false); + } + } + + private void updateAppMarketIcon(Drawable.ConstantState d) { + // Ensure that the new drawable we are creating has the approprate toolbar icon bounds + Resources r = getResources(); + Drawable marketIconDrawable = d.newDrawable(r); + int w = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_width); + int h = r.getDimensionPixelSize(R.dimen.toolbar_external_icon_height); + marketIconDrawable.setBounds(0, 0, w, h); + + updateTextButtonWithDrawable(R.id.market_button, marketIconDrawable); + } + + @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(getString(R.string.all_apps_button_label)); + } else { + text.add(getString(R.string.all_apps_home_button_label)); + } + return result; + } + + /** + * Receives notifications when system dialogs are to be closed. + */ + private 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. + * + * 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. + */ + private boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) { + if (mPaused) { + Log.i(TAG, "Deferring update until onResume"); + if (deletePreviousRunnables) { + while (mOnResumeCallbacks.remove(run)) { + } + } + mOnResumeCallbacks.add(run); + return true; + } else { + return false; + } + } + + private boolean waitUntilResume(Runnable run) { + return waitUntilResume(run, false); + } + + /** + * If the activity is currently paused, signal that we need to re-run the loader + * 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. + * + * 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. + */ + public boolean setLoadOnResume() { + if (mPaused) { + Log.i(TAG, "setLoadOnResume"); + mOnResumeNeedsLoad = true; + return true; + } else { + return false; + } + } + + /** + * Implementation of the method from LauncherModel.Callbacks. + */ + public int getCurrentWorkspaceScreen() { + if (mWorkspace != null) { + return mWorkspace.getCurrentPage(); + } else { + return SCREEN_COUNT / 2; + } + } + + /** + * Refreshes the shortcuts shown on the workspace. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void startBinding() { + // If we're starting binding all over again, clear any bind calls we'd postponed in + // the past (see waitUntilResume) -- we don't need them since we're starting binding + // from scratch again + mOnResumeCallbacks.clear(); + + final Workspace workspace = mWorkspace; + mNewShortcutAnimatePage = -1; + mNewShortcutAnimateViews.clear(); + mWorkspace.clearDropTargets(); + int count = workspace.getChildCount(); + for (int i = 0; i < count; i++) { + // Use removeAllViewsInLayout() to avoid an extra requestLayout() and invalidate(). + final CellLayout layoutParent = (CellLayout) workspace.getChildAt(i); + layoutParent.removeAllViewsInLayout(); + } + mWidgetsToAdvance.clear(); + if (mHotseat != null) { + mHotseat.resetLayout(); + } + } + + /** + * Bind the items start-end from the list. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindItems(final ArrayList shortcuts, final int start, final int end) { + if (waitUntilResume(new Runnable() { + public void run() { + bindItems(shortcuts, start, end); + } + })) { + return; + } + + // Get the list of added shortcuts and intersect them with the set of shortcuts here + Set newApps = new HashSet(); + newApps = mSharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps); + + Workspace workspace = mWorkspace; + for (int i = start; i < end; i++) { + final ItemInfo item = shortcuts.get(i); + + // Short circuit if we are loading dock items for a configuration which has no dock + if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT && + mHotseat == null) { + continue; + } + + switch (item.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + ShortcutInfo info = (ShortcutInfo) item; + String uri = info.intent.toUri(0).toString(); + View shortcut = createShortcut(info); + workspace.addInScreen(shortcut, item.container, item.screen, item.cellX, + item.cellY, 1, 1, false); + boolean animateIconUp = false; + synchronized (newApps) { + if (newApps.contains(uri)) { + animateIconUp = newApps.remove(uri); + } + } + if (animateIconUp) { + // Prepare the view to be animated up + shortcut.setAlpha(0f); + shortcut.setScaleX(0f); + shortcut.setScaleY(0f); + mNewShortcutAnimatePage = item.screen; + if (!mNewShortcutAnimateViews.contains(shortcut)) { + mNewShortcutAnimateViews.add(shortcut); + } + } + break; + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, + (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), + (FolderInfo) item, mIconCache); + workspace.addInScreen(newFolder, item.container, item.screen, item.cellX, + item.cellY, 1, 1, false); + break; + } + } + + workspace.requestLayout(); + } + + /** + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindFolders(final HashMap folders) { + if (waitUntilResume(new Runnable() { + public void run() { + bindFolders(folders); + } + })) { + return; + } + sFolders.clear(); + sFolders.putAll(folders); + } + + /** + * Add the views for a widget to the workspace. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindAppWidget(final LauncherAppWidgetInfo item) { + if (waitUntilResume(new Runnable() { + public void run() { + bindAppWidget(item); + } + })) { + return; + } + + final long start = DEBUG_WIDGETS ? SystemClock.uptimeMillis() : 0; + if (DEBUG_WIDGETS) { + Log.d(TAG, "bindAppWidget: " + item); + } + final Workspace workspace = mWorkspace; + + final int appWidgetId = item.appWidgetId; + final AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); + if (DEBUG_WIDGETS) { + Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider); + } + + item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + + item.hostView.setTag(item); + item.onBindAppWidget(this); + + workspace.addInScreen(item.hostView, item.container, item.screen, item.cellX, + item.cellY, item.spanX, item.spanY, false); + addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); + + workspace.requestLayout(); + + if (DEBUG_WIDGETS) { + Log.d(TAG, "bound widget id="+item.appWidgetId+" in " + + (SystemClock.uptimeMillis()-start) + "ms"); + } + } + + public void onPageBoundSynchronously(int page) { + mSynchronouslyBoundPages.add(page); + } + + /** + * Callback saying that there aren't any more items to bind. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void finishBindingItems() { + if (waitUntilResume(new Runnable() { + public void run() { + finishBindingItems(); + } + })) { + return; + } + if (mSavedState != null) { + if (!mWorkspace.hasFocus()) { + mWorkspace.getChildAt(mWorkspace.getCurrentPage()).requestFocus(); + } + mSavedState = null; + } + + mWorkspace.restoreInstanceStateForRemainingPages(); + + // If we received the result of any pending adds while the loader was running (e.g. the + // widget configuration forced an orientation change), process them now. + for (int i = 0; i < sPendingAddList.size(); i++) { + completeAdd(sPendingAddList.get(i)); + } + sPendingAddList.clear(); + + // Update the market app icon as necessary (the other icons will be managed in response to + // package changes in bindSearchablesChanged() + updateAppMarketIcon(); + + // Animate up any icons as necessary + if (mVisible || mWorkspaceLoading) { + Runnable newAppsRunnable = new Runnable() { + @Override + public void run() { + runNewAppsAnimation(false); + } + }; + + boolean willSnapPage = mNewShortcutAnimatePage > -1 && + mNewShortcutAnimatePage != mWorkspace.getCurrentPage(); + if (canRunNewAppsAnimation()) { + // If the user has not interacted recently, then either snap to the new page to show + // the new-apps animation or just run them if they are to appear on the current page + if (willSnapPage) { + mWorkspace.snapToPage(mNewShortcutAnimatePage, newAppsRunnable); + } else { + runNewAppsAnimation(false); + } + } else { + // If the user has interacted recently, then just add the items in place if they + // are on another page (or just normally if they are added to the current page) + runNewAppsAnimation(willSnapPage); + } + } + + mWorkspaceLoading = false; + } + + private boolean canRunNewAppsAnimation() { + long diff = System.currentTimeMillis() - mDragController.getLastGestureUpTime(); + return diff > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000); + } + + /** + * Runs a new animation that scales up icons that were added while Launcher was in the + * background. + * + * @param immediate whether to run the animation or show the results immediately + */ + private void runNewAppsAnimation(boolean immediate) { + AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); + Collection bounceAnims = new ArrayList(); + + // Order these new views spatially so that they animate in order + Collections.sort(mNewShortcutAnimateViews, new Comparator() { + @Override + public int compare(View a, View b) { + CellLayout.LayoutParams alp = (CellLayout.LayoutParams) a.getLayoutParams(); + CellLayout.LayoutParams blp = (CellLayout.LayoutParams) b.getLayoutParams(); + int cellCountX = LauncherModel.getCellCountX(); + return (alp.cellY * cellCountX + alp.cellX) - (blp.cellY * cellCountX + blp.cellX); + } + }); + + // Animate each of the views in place (or show them immediately if requested) + if (immediate) { + for (View v : mNewShortcutAnimateViews) { + v.setAlpha(1f); + v.setScaleX(1f); + v.setScaleY(1f); + } + } else { + for (int i = 0; i < mNewShortcutAnimateViews.size(); ++i) { + View v = mNewShortcutAnimateViews.get(i); + ValueAnimator bounceAnim = LauncherAnimUtils.ofPropertyValuesHolder(v, + PropertyValuesHolder.ofFloat("alpha", 1f), + PropertyValuesHolder.ofFloat("scaleX", 1f), + PropertyValuesHolder.ofFloat("scaleY", 1f)); + bounceAnim.setDuration(InstallShortcutReceiver.NEW_SHORTCUT_BOUNCE_DURATION); + bounceAnim.setStartDelay(i * InstallShortcutReceiver.NEW_SHORTCUT_STAGGER_DELAY); + bounceAnim.setInterpolator(new SmoothPagedView.OvershootInterpolator()); + bounceAnims.add(bounceAnim); + } + anim.playTogether(bounceAnims); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mWorkspace != null) { + mWorkspace.postDelayed(mBuildLayersRunnable, 500); + } + } + }); + anim.start(); + } + + // Clean up + mNewShortcutAnimatePage = -1; + mNewShortcutAnimateViews.clear(); + new Thread("clearNewAppsThread") { + public void run() { + mSharedPrefs.edit() + .putInt(InstallShortcutReceiver.NEW_APPS_PAGE_KEY, -1) + .putStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, null) + .commit(); + } + }.start(); + } + + @Override + public void bindSearchablesChanged() { + boolean searchVisible = updateGlobalSearchIcon(); + boolean voiceVisible = updateVoiceSearchIcon(searchVisible); + if (mSearchDropTargetBar != null) { + mSearchDropTargetBar.onSearchPackagesChanged(searchVisible, voiceVisible); + } + } + + /** + * Add the icons for all apps. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindAllApplications(final ArrayList apps) { + Runnable setAllAppsRunnable = new Runnable() { + public void run() { + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.setApps(apps); + } + } + }; + + // Remove the progress bar entirely; we could also make it GONE + // but better to remove it since we know it's not going to be used + View progressBar = mAppsCustomizeTabHost. + findViewById(R.id.apps_customize_progress_bar); + if (progressBar != null) { + ((ViewGroup)progressBar.getParent()).removeView(progressBar); + + // We just post the call to setApps so the user sees the progress bar + // disappear-- otherwise, it just looks like the progress bar froze + // which doesn't look great + mAppsCustomizeTabHost.post(setAllAppsRunnable); + } else { + // If we did not initialize the spinner in onCreate, then we can directly set the + // list of applications without waiting for any progress bars views to be hidden. + setAllAppsRunnable.run(); + } + } + + /** + * A package was installed. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindAppsAdded(final ArrayList apps) { + if (waitUntilResume(new Runnable() { + public void run() { + bindAppsAdded(apps); + } + })) { + return; + } + + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.addApps(apps); + } + } + + /** + * A package was updated. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindAppsUpdated(final ArrayList apps) { + if (waitUntilResume(new Runnable() { + public void run() { + bindAppsUpdated(apps); + } + })) { + return; + } + + if (mWorkspace != null) { + mWorkspace.updateShortcuts(apps); + } + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.updateApps(apps); + } + } + + /** + * A package was uninstalled. We take both the super set of packageNames + * in addition to specific applications to remove, the reason being that + * this can be called when a package is updated as well. In that scenario, + * we only remove specific components from the workspace, where as + * package-removal should clear all items by package name. + * + * Implementation of the method from LauncherModel.Callbacks. + */ + public void bindComponentsRemoved(final ArrayList packageNames, + final ArrayList appInfos, + final boolean matchPackageNamesOnly) { + if (waitUntilResume(new Runnable() { + public void run() { + bindComponentsRemoved(packageNames, appInfos, matchPackageNamesOnly); + } + })) { + return; + } + + if (matchPackageNamesOnly) { + mWorkspace.removeItemsByPackageName(packageNames); + } else { + mWorkspace.removeItemsByApplicationInfo(appInfos); + } + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.removeApps(appInfos); + } + + // Notify the drag controller + mDragController.onAppsRemoved(appInfos, this); + } + + /** + * A number of packages were updated. + */ + + private ArrayList mWidgetsAndShortcuts; + private Runnable mBindPackagesUpdatedRunnable = new Runnable() { + public void run() { + bindPackagesUpdated(mWidgetsAndShortcuts); + mWidgetsAndShortcuts = null; + } + }; + + public void bindPackagesUpdated(final ArrayList widgetsAndShortcuts) { + if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) { + mWidgetsAndShortcuts = widgetsAndShortcuts; + return; + } + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.onPackagesUpdated(widgetsAndShortcuts); + } + } + + private int mapConfigurationOriActivityInfoOri(int configOri) { + final Display d = getWindowManager().getDefaultDisplay(); + int naturalOri = Configuration.ORIENTATION_LANDSCAPE; + switch (d.getRotation()) { + case Surface.ROTATION_0: + case Surface.ROTATION_180: + // We are currently in the same basic orientation as the natural orientation + naturalOri = configOri; + break; + case Surface.ROTATION_90: + case Surface.ROTATION_270: + // We are currently in the other basic orientation to the natural orientation + naturalOri = (configOri == Configuration.ORIENTATION_LANDSCAPE) ? + Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; + break; + } + + int[] oriMap = { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + }; + // Since the map starts at portrait, we need to offset if this device's natural orientation + // is landscape. + int indexOffset = 0; + if (naturalOri == Configuration.ORIENTATION_LANDSCAPE) { + indexOffset = 1; + } + return oriMap[(d.getRotation() + indexOffset) % 4]; + } + + public boolean isRotationEnabled() { + boolean enableRotation = sForceEnableRotation || + getResources().getBoolean(R.bool.allow_rotation); + return enableRotation; + } + public void lockScreenOrientation() { + if (isRotationEnabled()) { + setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources() + .getConfiguration().orientation)); + } + } + public void unlockScreenOrientation(boolean immediate) { + if (isRotationEnabled()) { + if (immediate) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } else { + mHandler.postDelayed(new Runnable() { + public void run() { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + }, mRestoreScreenOrientationDelay); + } + } + } + + /* Cling related */ + private boolean isClingsEnabled() { + // disable clings when running in a test harness + if(ActivityManager.isRunningInTestHarness()) return false; + + // Restricted secondary users (child mode) will potentially have very few apps + // seeded when they start up for the first time. Clings won't work well with that + boolean supportsLimitedUsers = + android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; + Account[] accounts = AccountManager.get(this).getAccounts(); + if (supportsLimitedUsers && accounts.length == 0) { + UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); + Bundle restrictions = um.getUserRestrictions(); + if (restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) { + return false; + } + } + return true; + } + + private Cling initCling(int clingId, int[] positionData, boolean animate, int delay) { + final Cling cling = (Cling) findViewById(clingId); + if (cling != null) { + cling.init(this, positionData); + cling.setVisibility(View.VISIBLE); + cling.setLayerType(View.LAYER_TYPE_HARDWARE, null); + if (animate) { + cling.buildLayer(); + cling.setAlpha(0f); + cling.animate() + .alpha(1f) + .setInterpolator(new AccelerateInterpolator()) + .setDuration(SHOW_CLING_DURATION) + .setStartDelay(delay) + .start(); + } else { + cling.setAlpha(1f); + } + cling.setFocusableInTouchMode(true); + cling.post(new Runnable() { + public void run() { + cling.setFocusable(true); + cling.requestFocus(); + } + }); + mHideFromAccessibilityHelper.setImportantForAccessibilityToNo( + mDragLayer, clingId == R.id.all_apps_cling); + } + return cling; + } + + private void dismissCling(final Cling cling, 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. + if (cling != null && cling.getVisibility() != View.GONE) { + ObjectAnimator anim = LauncherAnimUtils.ofFloat(cling, "alpha", 0f); + anim.setDuration(duration); + anim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + cling.setVisibility(View.GONE); + cling.cleanup(); + // We should update the shared preferences on a background thread + new Thread("dismissClingThread") { + public void run() { + SharedPreferences.Editor editor = mSharedPrefs.edit(); + editor.putBoolean(flag, true); + editor.commit(); + } + }.start(); + }; + }); + anim.start(); + mHideFromAccessibilityHelper.restoreImportantForAccessibility(mDragLayer); + } + } + + private void removeCling(int id) { + final View cling = findViewById(id); + if (cling != null) { + final ViewGroup parent = (ViewGroup) cling.getParent(); + parent.post(new Runnable() { + @Override + public void run() { + parent.removeView(cling); + } + }); + mHideFromAccessibilityHelper.restoreImportantForAccessibility(mDragLayer); + } + } + + private boolean skipCustomClingIfNoAccounts() { + Cling cling = (Cling) findViewById(R.id.workspace_cling); + boolean customCling = cling.getDrawIdentifier().equals("workspace_custom"); + if (customCling) { + AccountManager am = AccountManager.get(this); + Account[] accounts = am.getAccountsByType("com.google"); + return accounts.length == 0; + } + return false; + } + + public void showFirstRunWorkspaceCling() { + // Enable the clings only if they have not been dismissed before + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.WORKSPACE_CLING_DISMISSED_KEY, false) && + !skipCustomClingIfNoAccounts() ) { + // If we're not using the default workspace layout, replace workspace cling + // with a custom workspace cling (usually specified in an overlay) + // For now, only do this on tablets + if (mSharedPrefs.getInt(LauncherProvider.DEFAULT_WORKSPACE_RESOURCE_ID, 0) != 0 && + getResources().getBoolean(R.bool.config_useCustomClings)) { + // Use a custom cling + View cling = findViewById(R.id.workspace_cling); + ViewGroup clingParent = (ViewGroup) cling.getParent(); + int clingIndex = clingParent.indexOfChild(cling); + clingParent.removeViewAt(clingIndex); + View customCling = mInflater.inflate(R.layout.custom_workspace_cling, clingParent, false); + clingParent.addView(customCling, clingIndex); + customCling.setId(R.id.workspace_cling); + } + initCling(R.id.workspace_cling, null, false, 0); + } else { + removeCling(R.id.workspace_cling); + } + } + public void showFirstRunAllAppsCling(int[] position) { + // Enable the clings only if they have not been dismissed before + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.ALLAPPS_CLING_DISMISSED_KEY, false)) { + initCling(R.id.all_apps_cling, position, true, 0); + } else { + removeCling(R.id.all_apps_cling); + } + } + public Cling showFirstRunFoldersCling() { + // Enable the clings only if they have not been dismissed before + if (isClingsEnabled() && + !mSharedPrefs.getBoolean(Cling.FOLDER_CLING_DISMISSED_KEY, false)) { + return initCling(R.id.folder_cling, null, true, 0); + } else { + removeCling(R.id.folder_cling); + return null; + } + } + public boolean isFolderClingVisible() { + Cling cling = (Cling) findViewById(R.id.folder_cling); + if (cling != null) { + return cling.getVisibility() == View.VISIBLE; + } + return false; + } + public void dismissWorkspaceCling(View v) { + Cling cling = (Cling) findViewById(R.id.workspace_cling); + dismissCling(cling, Cling.WORKSPACE_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION); + } + public void dismissAllAppsCling(View v) { + Cling cling = (Cling) findViewById(R.id.all_apps_cling); + dismissCling(cling, Cling.ALLAPPS_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION); + } + public void dismissFolderCling(View v) { + Cling cling = (Cling) findViewById(R.id.folder_cling); + dismissCling(cling, Cling.FOLDER_CLING_DISMISSED_KEY, DISMISS_CLING_DURATION); + } + + /** + * Prints out out state for debugging. + */ + public void dumpState() { + Log.d(TAG, "BEGIN launcher2 dump state for launcher " + this); + Log.d(TAG, "mSavedState=" + mSavedState); + Log.d(TAG, "mWorkspaceLoading=" + mWorkspaceLoading); + Log.d(TAG, "mRestoring=" + mRestoring); + Log.d(TAG, "mWaitingForResult=" + mWaitingForResult); + Log.d(TAG, "mSavedInstanceState=" + mSavedInstanceState); + Log.d(TAG, "sFolders.size=" + sFolders.size()); + mModel.dumpState(); + + if (mAppsCustomizeContent != null) { + mAppsCustomizeContent.dumpState(); + } + Log.d(TAG, "END launcher2 dump state"); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.println(" "); + writer.println("Debug logs: "); + for (int i = 0; i < sDumpLogs.size(); i++) { + writer.println(" " + sDumpLogs.get(i)); + } + } + + public static void dumpDebugLogsToConsole() { + Log.d(TAG, ""); + Log.d(TAG, "*********************"); + Log.d(TAG, "Launcher debug logs: "); + for (int i = 0; i < sDumpLogs.size(); i++) { + Log.d(TAG, " " + sDumpLogs.get(i)); + } + Log.d(TAG, "*********************"); + Log.d(TAG, ""); + } +} + +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); +} diff --git a/app/src/main/java/com/android/launcher2/LauncherAnimUtils.java b/app/src/main/java/com/android/launcher2/LauncherAnimUtils.java new file mode 100644 index 0000000..a89cb46 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherAnimUtils.java @@ -0,0 +1,129 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewTreeObserver; + +import java.util.HashSet; + +public class LauncherAnimUtils { + static HashSet sAnimators = new HashSet(); + static Animator.AnimatorListener sEndAnimListener = new Animator.AnimatorListener() { + public void onAnimationStart(Animator animation) { + } + + public void onAnimationRepeat(Animator animation) { + } + + public void onAnimationEnd(Animator animation) { + sAnimators.remove(animation); + } + + public void onAnimationCancel(Animator animation) { + sAnimators.remove(animation); + } + }; + + public static void cancelOnDestroyActivity(Animator a) { + sAnimators.add(a); + a.addListener(sEndAnimListener); + } + + // Helper method. Assumes a draw is pending, and that if the animation's duration is 0 + // it should be cancelled + public static void startAnimationAfterNextDraw(final Animator animator, final View view) { + view.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() { + private boolean mStarted = false; + public void onDraw() { + if (mStarted) return; + mStarted = true; + // Use this as a signal that the animation was cancelled + if (animator.getDuration() == 0) { + return; + } + animator.start(); + + final ViewTreeObserver.OnDrawListener listener = this; + view.post(new Runnable() { + public void run() { + view.getViewTreeObserver().removeOnDrawListener(listener); + } + }); + } + }); + } + + public static void onDestroyActivity() { + HashSet animators = new HashSet(sAnimators); + for (Animator a : animators) { + if (a.isRunning()) { + a.cancel(); + } else { + sAnimators.remove(a); + } + } + } + + public static AnimatorSet createAnimatorSet() { + AnimatorSet anim = new AnimatorSet(); + cancelOnDestroyActivity(anim); + return anim; + } + + public static ValueAnimator ofFloat(View target, float... values) { + ValueAnimator anim = new ValueAnimator(); + anim.setFloatValues(values); + cancelOnDestroyActivity(anim); + return anim; + } + + public static ObjectAnimator ofFloat(View target, String propertyName, float... values) { + ObjectAnimator anim = new ObjectAnimator(); + anim.setTarget(target); + anim.setPropertyName(propertyName); + anim.setFloatValues(values); + cancelOnDestroyActivity(anim); + new FirstFrameAnimatorHelper(anim, target); + return anim; + } + + public static ObjectAnimator ofPropertyValuesHolder(View target, + PropertyValuesHolder... values) { + ObjectAnimator anim = new ObjectAnimator(); + anim.setTarget(target); + anim.setValues(values); + cancelOnDestroyActivity(anim); + new FirstFrameAnimatorHelper(anim, target); + return anim; + } + + public static ObjectAnimator ofPropertyValuesHolder(Object target, + View view, PropertyValuesHolder... values) { + ObjectAnimator anim = new ObjectAnimator(); + anim.setTarget(target); + anim.setValues(values); + cancelOnDestroyActivity(anim); + new FirstFrameAnimatorHelper(anim, view); + return anim; + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherAnimatorUpdateListener.java b/app/src/main/java/com/android/launcher2/LauncherAnimatorUpdateListener.java new file mode 100644 index 0000000..dd82113 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherAnimatorUpdateListener.java @@ -0,0 +1,30 @@ +/* + * 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.launcher2; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; + +abstract class LauncherAnimatorUpdateListener implements AnimatorUpdateListener { + public void onAnimationUpdate(ValueAnimator animation) { + final float b = (Float) animation.getAnimatedValue(); + final float a = 1f - b; + onAnimationUpdate(a, b); + } + + abstract void onAnimationUpdate(float a, float b); +} \ No newline at end of file diff --git a/app/src/main/java/com/android/launcher2/LauncherAppWidgetHost.java b/app/src/main/java/com/android/launcher2/LauncherAppWidgetHost.java new file mode 100644 index 0000000..4d52ea8 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherAppWidgetHost.java @@ -0,0 +1,55 @@ +/* + * 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 com.android.launcher2; + +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; + +/** + * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView} + * which correctly captures all long-press events. This ensures that users can + * always pick up and move widgets. + */ +public class LauncherAppWidgetHost extends AppWidgetHost { + + Launcher mLauncher; + + public LauncherAppWidgetHost(Launcher launcher, int hostId) { + super(launcher, hostId); + mLauncher = launcher; + } + + @Override + protected AppWidgetHostView onCreateView(Context context, int appWidgetId, + AppWidgetProviderInfo appWidget) { + return new LauncherAppWidgetHostView(context); + } + + @Override + public void stopListening() { + super.stopListening(); + clearViews(); + } + + 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)); + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherAppWidgetHostView.java b/app/src/main/java/com/android/launcher2/LauncherAppWidgetHostView.java new file mode 100644 index 0000000..549d334 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherAppWidgetHostView.java @@ -0,0 +1,101 @@ +/* + * 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 com.android.launcher2; + +import android.appwidget.AppWidgetHostView; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RemoteViews; + +import com.android.launcher.R; + +/** + * {@inheritDoc} + */ +public class LauncherAppWidgetHostView extends AppWidgetHostView { + private CheckLongPressHelper mLongPressHelper; + private LayoutInflater mInflater; + private Context mContext; + private int mPreviousOrientation; + + public LauncherAppWidgetHostView(Context context) { + super(context); + mContext = context; + mLongPressHelper = new CheckLongPressHelper(this); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + protected View getErrorView() { + return mInflater.inflate(R.layout.appwidget_error, this, false); + } + + @Override + public void updateAppWidget(RemoteViews remoteViews) { + // Store the orientation in which the widget was inflated + mPreviousOrientation = mContext.getResources().getConfiguration().orientation; + super.updateAppWidget(remoteViews); + } + + public boolean orientationChangedSincedInflation() { + int orientation = mContext.getResources().getConfiguration().orientation; + if (mPreviousOrientation != orientation) { + return true; + } + return false; + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Consume any touch events for ourselves after longpress is triggered + if (mLongPressHelper.hasPerformedLongPress()) { + mLongPressHelper.cancelLongPress(); + return true; + } + + // Watch for longpress events at this level to make sure + // users can always pick up this widget + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: { + mLongPressHelper.postCheckForLongPress(); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mLongPressHelper.cancelLongPress(); + break; + } + + // Otherwise continue letting touch events fall through to children + return false; + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + + mLongPressHelper.cancelLongPress(); + } + + @Override + public int getDescendantFocusability() { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherAppWidgetInfo.java b/app/src/main/java/com/android/launcher2/LauncherAppWidgetInfo.java new file mode 100644 index 0000000..f001b2b --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherAppWidgetInfo.java @@ -0,0 +1,98 @@ +/* + * 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 com.android.launcher2; + +import android.appwidget.AppWidgetHostView; +import android.content.ComponentName; +import android.content.ContentValues; + +/** + * Represents a widget (either instantiated or about to be) in the Launcher. + */ +class LauncherAppWidgetInfo extends ItemInfo { + + /** + * Indicates that the widget hasn't been instantiated yet. + */ + static final int NO_ID = -1; + + /** + * Identifier for this widget when talking with + * {@link android.appwidget.AppWidgetManager} for updates. + */ + int appWidgetId = NO_ID; + + ComponentName providerName; + + // TODO: Are these necessary here? + int minWidth = -1; + int minHeight = -1; + + private boolean mHasNotifiedInitialWidgetSizeChanged; + + /** + * View that holds this widget after it's been created. This view isn't created + * until Launcher knows it's needed. + */ + AppWidgetHostView hostView = null; + + LauncherAppWidgetInfo(int appWidgetId, ComponentName providerName) { + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; + this.appWidgetId = appWidgetId; + this.providerName = providerName; + + // Since the widget isn't instantiated yet, we don't know these values. Set them to -1 + // to indicate that they should be calculated based on the layout and minWidth/minHeight + spanX = -1; + spanY = -1; + } + + @Override + void onAddToDatabase(ContentValues values) { + super.onAddToDatabase(values); + values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId); + } + + /** + * When we bind the widget, we should notify the widget that the size has changed if we have not + * done so already (only really for default workspace widgets). + */ + void onBindAppWidget(Launcher launcher) { + if (!mHasNotifiedInitialWidgetSizeChanged) { + notifyWidgetSizeChanged(launcher); + } + } + + /** + * Trigger an update callback to the widget to notify it that its size has changed. + */ + void notifyWidgetSizeChanged(Launcher launcher) { + AppWidgetResizeFrame.updateWidgetSizeRanges(hostView, launcher, spanX, spanY); + mHasNotifiedInitialWidgetSizeChanged = true; + } + + @Override + public String toString() { + return "AppWidget(id=" + Integer.toString(appWidgetId) + ")"; + } + + @Override + void unbind() { + super.unbind(); + hostView = null; + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherApplication.java b/app/src/main/java/com/android/launcher2/LauncherApplication.java new file mode 100644 index 0000000..9dd60f1 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherApplication.java @@ -0,0 +1,166 @@ +/* + * 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.launcher2; + +import android.app.Application; +import android.app.SearchManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.database.ContentObserver; +import android.os.Build; +import android.os.Handler; + +import com.android.launcher.R; + +import java.lang.ref.WeakReference; + +import lu.die.foza.SuperAPI.FozaCore; + +public class LauncherApplication extends Application { + private LauncherModel mModel; + private IconCache mIconCache; + private WidgetPreviewLoader.CacheDb mWidgetPreviewCacheDb; + private static boolean sIsScreenLarge; + private static float sScreenDensity; + private static int sLongPressTimeout = 300; + private static final String sSharedPreferencesKey = "com.android.launcher2.prefs"; + WeakReference mLauncherProvider; + + @Override + public void onCreate() { + super.onCreate(); + + // set sIsScreenXLarge and sScreenDensity *before* creating icon cache + sIsScreenLarge = getResources().getBoolean(R.bool.is_large_screen); + sScreenDensity = getResources().getDisplayMetrics().density; + + mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(this); + mIconCache = new IconCache(this); + mModel = new LauncherModel(this, mIconCache); + + // Register intent receivers + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(mModel, filter, RECEIVER_EXPORTED); + } else registerReceiver(mModel, filter); + filter = new IntentFilter(); + filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + filter.addAction(Intent.ACTION_LOCALE_CHANGED); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(mModel, filter, RECEIVER_EXPORTED); + } else registerReceiver(mModel, filter); + filter = new IntentFilter(); + filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(mModel, filter, RECEIVER_EXPORTED); + } else registerReceiver(mModel, filter); + filter = new IntentFilter(); + filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); + registerReceiver(mModel, filter); + + // Register for changes to the favorites + ContentResolver resolver = getContentResolver(); + resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true, + mFavoritesObserver); + } + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + FozaCore.startup(base); + } + + /** + * There's no guarantee that this function is ever called. + */ + @Override + public void onTerminate() { + super.onTerminate(); + + unregisterReceiver(mModel); + + ContentResolver resolver = getContentResolver(); + resolver.unregisterContentObserver(mFavoritesObserver); + } + + /** + * Receives notifications whenever the user favorites have changed. + */ + 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(); + } + }; + + LauncherModel setLauncher(Launcher launcher) { + mModel.initialize(launcher); + return mModel; + } + + IconCache getIconCache() { + return mIconCache; + } + + LauncherModel getModel() { + return mModel; + } + + WidgetPreviewLoader.CacheDb getWidgetPreviewCacheDb() { + return mWidgetPreviewCacheDb; + } + + void setLauncherProvider(LauncherProvider provider) { + mLauncherProvider = new WeakReference(provider); + } + + LauncherProvider getLauncherProvider() { + return mLauncherProvider.get(); + } + + public static String getSharedPreferencesKey() { + return sSharedPreferencesKey; + } + + public static boolean isScreenLarge() { + return sIsScreenLarge; + } + + public static boolean isScreenLandscape(Context context) { + return context.getResources().getConfiguration().orientation == + Configuration.ORIENTATION_LANDSCAPE; + } + + public static float getScreenDensity() { + return sScreenDensity; + } + + public static int getLongPressTimeout() { + return sLongPressTimeout; + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherModel.java b/app/src/main/java/com/android/launcher2/LauncherModel.java new file mode 100644 index 0000000..7c6f952 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherModel.java @@ -0,0 +1,2623 @@ +/* + * 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.launcher2; + +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.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.Intent.ShortcutIconResource; +import android.content.pm.ActivityInfo; +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.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.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; + +import com.android.launcher.R; +import com.android.launcher2.InstallWidgetReceiver.WidgetMimeTypeHandlerData; + +import java.lang.ref.WeakReference; +import java.net.URISyntaxException; +import java.text.Collator; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import lu.die.fozacompatibility.FozaPackageManager; + +/** + * Maintains in-memory state of the Launcher. It is expected that there should be only one + * LauncherModel object held in a static. Also provide APIs for updating the database state + * for the Launcher. + */ +public class LauncherModel extends BroadcastReceiver { + static final boolean DEBUG_LOADERS = false; + static final String TAG = "Launcher.Model"; + + private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons + private final boolean mAppsCanBeOnExternalStorage; + private int mBatchSize; // 0 is all apps at once + private int mAllAppsLoadDelay; // milliseconds between batches + + private final LauncherApplication mApp; + private final Object mLock = new Object(); + private DeferredHandler mHandler = new DeferredHandler(); + private LoaderTask mLoaderTask; + private boolean mIsLoaderTaskRunning; + private volatile boolean mFlushingWorkerThread; + + // 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; + + + private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); + static { + sWorkerThread.start(); + } + private 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; + + // 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 + // draw (in Workspace) to initiate the binding of the remaining side pages. Any time we start + // a normal load, we also clear this set of Runnables. + static final ArrayList mDeferredBindRunnables = new ArrayList(); + + private WeakReference mCallbacks; + + // < only access in worker thread > + private AllAppsList mBgAllAppsList; + + // 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 + // static data structures to be referenced outside of the worker thread except on the first + // load after configuration change. + static final Object sBgLock = new Object(); + + // sBgItemsIdMap maps *all* the ItemInfos (shortcuts, folders, and widgets) created by + // LauncherModel to their ids + static final HashMap sBgItemsIdMap = new HashMap(); + + // 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 + // shortcuts within folders). + static final ArrayList sBgWorkspaceItems = new ArrayList(); + + // sBgAppWidgets is all LauncherAppWidgetInfo created by LauncherModel. Passed to bindAppWidget() + static final ArrayList sBgAppWidgets = + 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(); + // + + private IconCache mIconCache; + private Bitmap mDefaultIcon; + + private static int mCellCountX; + private static int mCellCountY; + + protected int mPreviousConfigMcc; + + public interface Callbacks { + public boolean setLoadOnResume(); + public int getCurrentWorkspaceScreen(); + public void startBinding(); + public void bindItems(ArrayList shortcuts, int start, int end); + public void bindFolders(HashMap folders); + public void finishBindingItems(); + public void bindAppWidget(LauncherAppWidgetInfo info); + public void bindAllApplications(ArrayList apps); + public void bindAppsAdded(ArrayList apps); + public void bindAppsUpdated(ArrayList apps); + public void bindComponentsRemoved(ArrayList packageNames, + ArrayList appInfos, + boolean matchPackageNamesOnly); + public void bindPackagesUpdated(ArrayList widgetsAndShortcuts); + public boolean isAllAppsVisible(); + public boolean isAllAppsButtonRank(int rank); + public void bindSearchablesChanged(); + public void onPageBoundSynchronously(int page); + } + + LauncherModel(LauncherApplication app, IconCache iconCache) { + mAppsCanBeOnExternalStorage = !Environment.isExternalStorageEmulated(); + mApp = app; + mBgAllAppsList = new AllAppsList(iconCache); + mIconCache = iconCache; + + mDefaultIcon = Utilities.createIconBitmap( + mIconCache.getFullResDefaultActivityIcon(), app); + + final Resources res = app.getResources(); + mAllAppsLoadDelay = res.getInteger(R.integer.config_allAppsBatchLoadDelay); + mBatchSize = res.getInteger(R.integer.config_allAppsBatchSize); + Configuration config = res.getConfiguration(); + mPreviousConfigMcc = config.mcc; + } + + /** 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) { + if (sWorkerThread.getThreadId() == Process.myTid()) { + // If we are on the worker thread, post onto the main handler + mHandler.post(r); + } else { + r.run(); + } + } + + /** Runs the specified runnable immediately if called from the worker thread, otherwise it is + * posted on the worker thread handler. */ + private static void runOnWorkerThread(Runnable r) { + if (sWorkerThread.getThreadId() == Process.myTid()) { + r.run(); + } else { + // If we are not on the worker thread, then post to the worker handler + sWorker.post(r); + } + } + + public Bitmap getFallbackIcon() { + return Bitmap.createBitmap(mDefaultIcon); + } + + public void unbindItemInfosAndClearQueuedBindRunnables() { + if (sWorkerThread.getThreadId() == Process.myTid()) { + throw new RuntimeException("Expected unbindLauncherItemInfos() to be called from the " + + "main thread"); + } + + // Clear any deferred bind runnables + mDeferredBindRunnables.clear(); + // Remove any queued bind runnables + mHandler.cancelAllRunnablesOfType(MAIN_THREAD_BINDING_RUNNABLE); + // Unbind all the workspace items + unbindWorkspaceItemsOnMainThread(); + } + + /** Unbinds all the sBgWorkspaceItems and sBgAppWidgets on the main thread */ + 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(); + synchronized (sBgLock) { + tmpWorkspaceItems.addAll(sBgWorkspaceItems); + tmpAppWidgets.addAll(sBgAppWidgets); + } + Runnable r = new Runnable() { + @Override + public void run() { + for (ItemInfo item : tmpWorkspaceItems) { + item.unbind(); + } + for (ItemInfo item : tmpAppWidgets) { + item.unbind(); + } + } + }; + runOnMainThread(r); + } + + /** + * Adds an item to the DB if it was not created previously, or move it to a new + * + */ + static void addOrMoveItemInDatabase(Context context, ItemInfo item, long container, + int screen, int cellX, int cellY) { + if (item.container == ItemInfo.NO_ID) { + // From all apps + addItemToDatabase(context, item, container, screen, cellX, cellY, false); + } else { + // From somewhere else + moveItemInDatabase(context, item, container, screen, cellX, cellY); + } + } + + static void checkItemInfoLocked( + final long itemId, final ItemInfo item, StackTraceElement[] stackTrace) { + ItemInfo modelItem = sBgItemsIdMap.get(itemId); + if (modelItem != null && item != modelItem) { + // check all the data is consistent + if (modelItem instanceof ShortcutInfo && item instanceof ShortcutInfo) { + ShortcutInfo modelShortcut = (ShortcutInfo) modelItem; + ShortcutInfo shortcut = (ShortcutInfo) item; + if (modelShortcut.title.toString().equals(shortcut.title.toString()) && + modelShortcut.intent.filterEquals(shortcut.intent) && + modelShortcut.id == shortcut.id && + modelShortcut.itemType == shortcut.itemType && + modelShortcut.container == shortcut.container && + modelShortcut.screen == shortcut.screen && + modelShortcut.cellX == shortcut.cellX && + modelShortcut.cellY == shortcut.cellY && + modelShortcut.spanX == shortcut.spanX && + modelShortcut.spanY == shortcut.spanY && + ((modelShortcut.dropPos == null && shortcut.dropPos == null) || + (modelShortcut.dropPos != null && + shortcut.dropPos != null && + modelShortcut.dropPos[0] == shortcut.dropPos[0] && + modelShortcut.dropPos[1] == shortcut.dropPos[1]))) { + // For all intents and purposes, this is the same object + return; + } + } + + // the modelItem needs to match up perfectly with item if our model is + // to be consistent with the database-- for now, just require + // modelItem == item or the equality check above + String msg = "item: " + ((item != null) ? item.toString() : "null") + + "modelItem: " + + ((modelItem != null) ? modelItem.toString() : "null") + + "Error: ItemInfo passed to checkItemInfo doesn't match original"; + RuntimeException e = new RuntimeException(msg); + if (stackTrace != null) { + e.setStackTrace(stackTrace); + } + throw e; + } + } + + static void checkItemInfo(final ItemInfo item) { + final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); + final long itemId = item.id; + Runnable r = new Runnable() { + public void run() { + synchronized (sBgLock) { + checkItemInfoLocked(itemId, item, stackTrace); + } + } + }; + runOnWorkerThread(r); + } + + 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 ContentResolver cr = context.getContentResolver(); + + final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); + Runnable r = new Runnable() { + public void run() { + cr.update(uri, values, null, null); + + // Lock on mBgLock *after* the db operation + synchronized (sBgLock) { + checkItemInfoLocked(itemId, item, stackTrace); + + if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP && + item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + // Item is in a folder, make sure this folder exists + if (!sBgFolders.containsKey(item.container)) { + // An items container is being set to a that of an item which is not in + // the list of Folders. + String msg = "item: " + item + " container being set to: " + + item.container + ", not in the list of folders"; + Log.e(TAG, msg); + Launcher.dumpDebugLogsToConsole(); + } + } + + // Items are added/removed from the corresponding FolderInfo elsewhere, such + // as in Workspace.onDrop. Here, we just add/remove them from the list of items + // that are on the desktop, as appropriate + ItemInfo modelItem = sBgItemsIdMap.get(itemId); + if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP || + modelItem.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + switch (modelItem.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + if (!sBgWorkspaceItems.contains(modelItem)) { + sBgWorkspaceItems.add(modelItem); + } + break; + default: + break; + } + } else { + sBgWorkspaceItems.remove(modelItem); + } + } + } + }; + runOnWorkerThread(r); + } + + 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, + final int screen, final int cellX, final int cellY) { + String transaction = "DbDebug Modify item (" + item.title + ") in db, id: " + item.id + + " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY + + ") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")"; + Launcher.sDumpLogs.add(transaction); + Log.d(TAG, transaction); + item.container = container; + item.cellX = cellX; + item.cellY = cellY; + + // We store hotseat items in canonical form which is this orientation invariant position + // in the hotseat + if (context instanceof Launcher && screen < 0 && + container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY); + } else { + item.screen = screen; + } + + final ContentValues values = new ContentValues(); + 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.SCREEN, item.screen); + + updateItemInDatabaseHelper(context, values, item, "moveItemInDatabase"); + } + + /** + * Move and/or resize item in the DB to a new + */ + static void modifyItemInDatabase(Context context, final ItemInfo item, final long container, + final int screen, final int cellX, final int cellY, final int spanX, final int spanY) { + String transaction = "DbDebug Modify item (" + item.title + ") in db, id: " + item.id + + " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY + + ") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")"; + Launcher.sDumpLogs.add(transaction); + Log.d(TAG, transaction); + item.cellX = cellX; + item.cellY = cellY; + item.spanX = spanX; + item.spanY = spanY; + + // We store hotseat items in canonical form which is this orientation invariant position + // in the hotseat + if (context instanceof Launcher && screen < 0 && + container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY); + } else { + item.screen = screen; + } + + final ContentValues values = new ContentValues(); + 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.SPANX, item.spanX); + values.put(LauncherSettings.Favorites.SPANY, item.spanY); + values.put(LauncherSettings.Favorites.SCREEN, item.screen); + + updateItemInDatabaseHelper(context, values, item, "modifyItemInDatabase"); + } + + /** + * Update an item to the database in a specified container. + */ + static void updateItemInDatabase(Context context, final ItemInfo item) { + final ContentValues values = new ContentValues(); + item.onAddToDatabase(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) { + final ContentResolver cr = context.getContentResolver(); + Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, + new String[] { "title", "intent" }, "title=? and intent=?", + new String[] { title, intent.toUri(0) }, null); + boolean result = false; + try { + result = c.moveToFirst(); + } finally { + c.close(); + } + return result; + } + + /** + * Returns an ItemInfo array containing all the items in the LauncherModel. + * The ItemInfo.id is not set through this function. + */ + 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 }, 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); + + try { + while (c.moveToNext()) { + ItemInfo item = new ItemInfo(); + item.cellX = c.getInt(cellXIndex); + item.cellY = c.getInt(cellYIndex); + item.spanX = c.getInt(spanXIndex); + item.spanY = c.getInt(spanYIndex); + item.container = c.getInt(containerIndex); + item.itemType = c.getInt(itemTypeIndex); + item.screen = c.getInt(screenIndex); + + items.add(item); + } + } catch (Exception e) { + items.clear(); + } finally { + c.close(); + } + + return items; + } + + /** + * Find a folder in the db, creating the FolderInfo if necessary, and adding it to folderList. + */ + FolderInfo getFolderById(Context context, HashMap folderList, long id) { + final ContentResolver cr = context.getContentResolver(); + Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, null, + "_id=? and (itemType=? or itemType=?)", + new String[] { String.valueOf(id), + String.valueOf(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}, null); + + try { + if (c.moveToFirst()) { + final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); + final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); + 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); + + FolderInfo folderInfo = null; + switch (c.getInt(itemTypeIndex)) { + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + folderInfo = findOrMakeFolder(folderList, id); + break; + } + + folderInfo.title = c.getString(titleIndex); + folderInfo.id = id; + folderInfo.container = c.getInt(containerIndex); + folderInfo.screen = c.getInt(screenIndex); + folderInfo.cellX = c.getInt(cellXIndex); + folderInfo.cellY = c.getInt(cellYIndex); + + return folderInfo; + } + } finally { + c.close(); + } + + return null; + } + + /** + * 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 int screen, final int cellX, final int cellY, final boolean notify) { + item.container = container; + item.cellX = cellX; + item.cellY = cellY; + // We store hotseat items in canonical form which is this orientation invariant position + // in the hotseat + if (context instanceof Launcher && screen < 0 && + container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY); + } else { + item.screen = screen; + } + + final ContentValues values = new ContentValues(); + final ContentResolver cr = context.getContentResolver(); + item.onAddToDatabase(values); + + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + item.id = app.getLauncherProvider().generateNewId(); + values.put(LauncherSettings.Favorites._ID, item.id); + item.updateValuesWithCoordinates(values, item.cellX, item.cellY); + + Runnable r = new Runnable() { + public void run() { + String transaction = "DbDebug Add item (" + item.title + ") to db, id: " + + item.id + " (" + container + ", " + screen + ", " + cellX + ", " + + cellY + ")"; + Launcher.sDumpLogs.add(transaction); + Log.d(TAG, transaction); + + cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : + LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values); + + // Lock on mBgLock *after* the db operation + synchronized (sBgLock) { + checkItemInfoLocked(item.id, item, null); + sBgItemsIdMap.put(item.id, item); + switch (item.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + sBgFolders.put(item.id, (FolderInfo) item); + // Fall through + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP || + item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + sBgWorkspaceItems.add(item); + } else { + if (!sBgFolders.containsKey(item.container)) { + // Adding an item to a folder that doesn't exist. + String msg = "adding item: " + item + " to a folder that " + + " doesn't exist"; + Log.e(TAG, msg); + Launcher.dumpDebugLogsToConsole(); + } + } + break; + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + sBgAppWidgets.add((LauncherAppWidgetInfo) item); + break; + } + } + } + }; + runOnWorkerThread(r); + } + + /** + * Creates a new unique child id, for a given cell span across all layouts. + */ + static int getCellLayoutChildId( + long container, int screen, int localCellX, int localCellY, int spanX, int spanY) { + return (((int) container & 0xFF) << 24) + | (screen & 0xFF) << 16 | (localCellX & 0xFF) << 8 | (localCellY & 0xFF); + } + + static int getCellCountX() { + return mCellCountX; + } + + static int getCellCountY() { + return mCellCountY; + } + + /** + * Updates the model orientation helper to take into account the current layout dimensions + * when performing local/canonical coordinate transformations. + */ + static void updateWorkspaceLayoutCells(int shortAxisCellCount, int longAxisCellCount) { + mCellCountX = shortAxisCellCount; + mCellCountY = longAxisCellCount; + } + + /** + * Removes the specified item from the database + * @param context + * @param item + */ + static void deleteItemFromDatabase(Context context, final ItemInfo item) { + final ContentResolver cr = context.getContentResolver(); + final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false); + + Runnable r = new Runnable() { + public void run() { + String transaction = "DbDebug Delete item (" + item.title + ") from db, id: " + + item.id + " (" + item.container + ", " + item.screen + ", " + item.cellX + + ", " + item.cellY + ")"; + Launcher.sDumpLogs.add(transaction); + Log.d(TAG, transaction); + + cr.delete(uriToDelete, null, null); + + // Lock on mBgLock *after* the db operation + synchronized (sBgLock) { + switch (item.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + sBgFolders.remove(item.id); + for (ItemInfo info: sBgItemsIdMap.values()) { + if (info.container == item.id) { + // We are deleting a folder which still contains items that + // think they are contained by that folder. + String msg = "deleting a folder (" + item + ") which still " + + "contains items (" + info + ")"; + Log.e(TAG, msg); + Launcher.dumpDebugLogsToConsole(); + } + } + sBgWorkspaceItems.remove(item); + break; + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + sBgWorkspaceItems.remove(item); + break; + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + sBgAppWidgets.remove((LauncherAppWidgetInfo) item); + break; + } + sBgItemsIdMap.remove(item.id); + sBgDbIconCache.remove(item); + } + } + }; + runOnWorkerThread(r); + } + + /** + * Remove the contents of the specified folder from the database + */ + 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); + // 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, + 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); + } + } + } + }; + runOnWorkerThread(r); + } + + /** + * Set this as the current Launcher activity object for the loader. + */ + public void initialize(Callbacks callbacks) { + synchronized (mLock) { + mCallbacks = new WeakReference(callbacks); + } + } + + /** + * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and + * ACTION_PACKAGE_CHANGED. + */ + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG_LOADERS) Log.d(TAG, "onReceive intent=" + intent); + + final String action = intent.getAction(); + + if (Intent.ACTION_PACKAGE_CHANGED.equals(action) + || Intent.ACTION_PACKAGE_REMOVED.equals(action) + || Intent.ACTION_PACKAGE_ADDED.equals(action)) { + final String packageName = intent.getData().getSchemeSpecificPart(); + final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); + + int op = PackageUpdatedTask.OP_NONE; + + if (packageName == null || packageName.length() == 0) { + // they sent us a bad intent + return; + } + + if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + op = PackageUpdatedTask.OP_UPDATE; + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + if (!replacing) { + op = PackageUpdatedTask.OP_REMOVE; + } + // else, we are replacing the package, so a PACKAGE_ADDED will be sent + // later, we will update the package at this time + } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + if (!replacing) { + op = PackageUpdatedTask.OP_ADD; + } else { + op = PackageUpdatedTask.OP_UPDATE; + } + } + + if (op != PackageUpdatedTask.OP_NONE) { + enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName })); + } + + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { + // First, schedule to add these apps back in. + String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages)); + // Then, rebind everything. + startLoaderFromBackground(); + } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { + String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + enqueuePackageUpdated(new PackageUpdatedTask( + PackageUpdatedTask.OP_UNAVAILABLE, packages)); + } else 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)) { + if (mCallbacks != null) { + Callbacks callbacks = mCallbacks.get(); + if (callbacks != null) { + callbacks.bindSearchablesChanged(); + } + } + } + } + + private void forceReload() { + resetLoadedState(true, true); + + // Do this here because if the launcher activity is running it will be restarted. + // If it's not running startLoaderFromBackground will merely tell it that it needs + // to reload. + startLoaderFromBackground(); + } + + public void resetLoadedState(boolean resetAllAppsLoaded, boolean resetWorkspaceLoaded) { + synchronized (mLock) { + // Stop any existing loaders first, so they don't set mAllAppsLoaded or + // mWorkspaceLoaded to true later + stopLoaderLocked(); + if (resetAllAppsLoaded) mAllAppsLoaded = false; + if (resetWorkspaceLoaded) mWorkspaceLoaded = false; + } + } + + /** + * When the launcher is in the background, it's possible for it to miss paired + * configuration changes. So whenever we trigger the loader from the background + * tell the launcher that it needs to re-run the loader when it comes back instead + * of doing it now. + */ + public void startLoaderFromBackground() { + boolean runLoader = false; + if (mCallbacks != null) { + Callbacks callbacks = mCallbacks.get(); + if (callbacks != null) { + // Only actually run the loader if they're not paused. + if (!callbacks.setLoadOnResume()) { + runLoader = true; + } + } + } + if (runLoader) { + startLoader(false, -1); + } + } + + // 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; + LoaderTask oldTask = mLoaderTask; + if (oldTask != null) { + if (oldTask.isLaunching()) { + isLaunching = true; + } + oldTask.stopLocked(); + } + return isLaunching; + } + + public void startLoader(boolean isLaunching, int synchronousBindPage) { + 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. + mDeferredBindRunnables.clear(); + + // 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, isLaunching); + if (synchronousBindPage > -1 && mAllAppsLoaded && mWorkspaceLoaded) { + mLoaderTask.runBindSynchronousPage(synchronousBindPage); + } else { + sWorkerThread.setPriority(Thread.NORM_PRIORITY); + sWorker.post(mLoaderTask); + } + } + } + } + + void bindRemainingSynchronousPages() { + // Post the remaining side pages to be loaded + if (!mDeferredBindRunnables.isEmpty()) { + for (final Runnable r : mDeferredBindRunnables) { + mHandler.post(r, MAIN_THREAD_BINDING_RUNNABLE); + } + mDeferredBindRunnables.clear(); + } + } + + public void stopLoader() { + synchronized (mLock) { + if (mLoaderTask != null) { + mLoaderTask.stopLocked(); + } + } + } + + public boolean isAllAppsLoaded() { + return mAllAppsLoaded; + } + + boolean isLoadingWorkspace() { + synchronized (mLock) { + if (mLoaderTask != null) { + return mLoaderTask.isLoadingWorkspace(); + } + } + return false; + } + + /** + * Runnable for the thread that loads the contents of the launcher: + * - workspace icons + * - widgets + * - all apps icons + */ + private class LoaderTask implements Runnable { + private Context mContext; + private boolean mIsLaunching; + private boolean mIsLoadingAndBindingWorkspace; + private boolean mStopped; + private boolean mLoadAndBindStepFinished; + + private HashMap mLabelCache; + + LoaderTask(Context context, boolean isLaunching) { + mContext = context; + mIsLaunching = isLaunching; + mLabelCache = new HashMap(); + } + + boolean isLaunching() { + return mIsLaunching; + } + + boolean isLoadingWorkspace() { + return mIsLoadingAndBindingWorkspace; + } + + private void loadAndBindWorkspace() { + mIsLoadingAndBindingWorkspace = true; + + // Load the workspace + if (DEBUG_LOADERS) { + Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded); + } + + if (!mWorkspaceLoaded) { + loadWorkspace(); + synchronized (LoaderTask.this) { + if (mStopped) { + return; + } + mWorkspaceLoaded = true; + } + } + + // Bind the workspace + bindWorkspace(-1); + } + + private void waitForIdle() { + // Wait until the either we're stopped or the other threads are done. + // This way we don't start loading all apps until the workspace has settled + // down. + synchronized (LoaderTask.this) { + final long workspaceWaitTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + + mHandler.postIdle(new Runnable() { + public void run() { + synchronized (LoaderTask.this) { + mLoadAndBindStepFinished = true; + if (DEBUG_LOADERS) { + Log.d(TAG, "done with previous binding step"); + } + LoaderTask.this.notify(); + } + } + }); + + while (!mStopped && !mLoadAndBindStepFinished && !mFlushingWorkerThread) { + try { + // Just in case mFlushingWorkerThread changes but we aren't woken up, + // wait no longer than 1sec at a time + this.wait(1000); + } catch (InterruptedException ex) { + // Ignore + } + } + if (DEBUG_LOADERS) { + Log.d(TAG, "waited " + + (SystemClock.uptimeMillis()-workspaceWaitTime) + + "ms for previous step to finish binding"); + } + } + } + + void runBindSynchronousPage(int synchronousBindPage) { + if (synchronousBindPage < 0) { + // Ensure that we have a valid page index to load synchronously + throw new RuntimeException("Should not call runBindSynchronousPage() without " + + "valid page index"); + } + if (!mAllAppsLoaded || !mWorkspaceLoaded) { + // Ensure that we don't try and bind a specified page when the pages have not been + // loaded already (we should load everything asynchronously in that case) + throw new RuntimeException("Expecting AllApps and Workspace to be loaded"); + } + synchronized (mLock) { + if (mIsLoaderTaskRunning) { + // Ensure that we are never running the background loading at this point since + // we also touch the background collections + throw new RuntimeException("Error! Background loading is already running"); + } + } + + // XXX: Throw an exception if we are already loading (since we touch the worker thread + // data structures, we can't allow any other thread to touch that data, but because + // this call is synchronous, we can get away with not locking). + + // The LauncherModel is static in the LauncherApplication and mHandler may have queued + // operations from the previous activity. We need to ensure that all queued operations + // are executed before any synchronous binding work is done. + mHandler.flush(); + + // 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); + // XXX: For now, continue posting the binding of AllApps as there are other issues that + // arise from that. + onlyBindAllApps(); + } + + public void run() { + synchronized (mLock) { + 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). + final Callbacks cbk = mCallbacks.get(); + final boolean loadWorkspaceFirst = cbk != null ? (!cbk.isAllAppsVisible()) : true; + + 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 (loadWorkspaceFirst) { + if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); + loadAndBindWorkspace(); + } else { + if (DEBUG_LOADERS) Log.d(TAG, "step 1: special: loading all apps"); + loadAndBindAllApps(); + } + + 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 (loadWorkspaceFirst) { + if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps"); + loadAndBindAllApps(); + } else { + if (DEBUG_LOADERS) Log.d(TAG, "step 2: special: loading workspace"); + loadAndBindWorkspace(); + } + + // 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(); + } + + // Clear out this reference, otherwise we end up holding it until all of the + // callback runnables are done. + mContext = null; + + synchronized (mLock) { + // If we are still the last one to be scheduled, remove ourselves. + if (mLoaderTask == this) { + mLoaderTask = null; + } + mIsLoaderTaskRunning = false; + } + } + + public void stopLocked() { + synchronized (LoaderTask.this) { + mStopped = true; + this.notify(); + } + } + + /** + * Gets the callbacks object. If we've been stopped, or if the launcher object + * has somehow been garbage collected, return null instead. Pass in the Callbacks + * object that was around when the deferred message was scheduled, and if there's + * a new Callbacks object around then also return null. This will save us from + * calling onto it with data that will be ignored. + */ + Callbacks tryGetCallbacks(Callbacks oldCallbacks) { + synchronized (mLock) { + if (mStopped) { + return null; + } + + if (mCallbacks == null) { + return null; + } + + final Callbacks callbacks = mCallbacks.get(); + if (callbacks != oldCallbacks) { + return null; + } + if (callbacks == null) { + Log.w(TAG, "no mCallbacks"); + return null; + } + + return callbacks; + } + } + + // check & update map of what's occupied; used to discard overlapping/invalid items + private boolean checkItemPlacement(ItemInfo occupied[][][], ItemInfo item) { + int containerIndex = item.screen; + if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + // Return early if we detect that an item is under the hotseat button + if (mCallbacks == null || mCallbacks.get().isAllAppsButtonRank(item.screen)) { + return false; + } + + // We use the last index to refer to the hotseat and the screen as the rank, so + // test and update the occupied state accordingly + if (occupied[Launcher.SCREEN_COUNT][item.screen][0] != null) { + Log.e(TAG, "Error loading shortcut into hotseat " + item + + " into position (" + item.screen + ":" + item.cellX + "," + item.cellY + + ") occupied by " + occupied[Launcher.SCREEN_COUNT][item.screen][0]); + return false; + } else { + occupied[Launcher.SCREEN_COUNT][item.screen][0] = item; + return true; + } + } else if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { + // Skip further checking if it is not the hotseat or workspace container + return true; + } + + // Check if any workspace icons overlap with each other + for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { + for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { + if (occupied[containerIndex][x][y] != null) { + Log.e(TAG, "Error loading shortcut " + item + + " into cell (" + containerIndex + "-" + item.screen + ":" + + x + "," + y + + ") occupied by " + + occupied[containerIndex][x][y]); + return false; + } + } + } + for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { + for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { + occupied[containerIndex][x][y] = item; + } + } + + return 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(); + + // Make sure the default workspace is loaded, if needed + mApp.getLauncherProvider().loadDefaultFavoritesIfNecessary(0); + + synchronized (sBgLock) { + sBgWorkspaceItems.clear(); + sBgAppWidgets.clear(); + sBgFolders.clear(); + sBgItemsIdMap.clear(); + sBgDbIconCache.clear(); + + final ArrayList itemsToRemove = new ArrayList(); + + final Cursor c = contentResolver.query( + LauncherSettings.Favorites.CONTENT_URI, 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 ItemInfo occupied[][][] = + new ItemInfo[Launcher.SCREEN_COUNT + 1][mCellCountX + 1][mCellCountY + 1]; + + try { + 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 appWidgetIdIndex = c.getColumnIndexOrThrow( + LauncherSettings.Favorites.APPWIDGET_ID); + 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 uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); + //final int displayModeIndex = c.getColumnIndexOrThrow( + // LauncherSettings.Favorites.DISPLAY_MODE); + + ShortcutInfo info; + String intentDescription; + LauncherAppWidgetInfo appWidgetInfo; + int container; + long id; + Intent intent; + + while (!mStopped && c.moveToNext()) { + try { + int itemType = c.getInt(itemTypeIndex); + + switch (itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + intentDescription = c.getString(intentIndex); + try { + intent = Intent.parseUri(intentDescription, 0); + } catch (URISyntaxException e) { + continue; + } + + if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { + info = getShortcutInfo(manager, intent, context, c, iconIndex, + titleIndex, mLabelCache); + } else { + info = getShortcutInfo(c, context, iconTypeIndex, + iconPackageIndex, iconResourceIndex, iconIndex, + titleIndex); + + // App shortcuts that used to be automatically added to Launcher + // didn't always have the correct intent flags set, so do that + // here + if (intent.getAction() != null && + intent.getCategories() != null && + intent.getAction().equals(Intent.ACTION_MAIN) && + intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + } + } + + if (info != null) { + info.intent = intent; + info.id = c.getLong(idIndex); + container = c.getInt(containerIndex); + info.container = container; + info.screen = c.getInt(screenIndex); + info.cellX = c.getInt(cellXIndex); + info.cellY = c.getInt(cellYIndex); + + // check & update map of what's occupied + if (!checkItemPlacement(occupied, info)) { + break; + } + + switch (container) { + case LauncherSettings.Favorites.CONTAINER_DESKTOP: + case LauncherSettings.Favorites.CONTAINER_HOTSEAT: + sBgWorkspaceItems.add(info); + break; + default: + // Item is in a user folder + FolderInfo folderInfo = + findOrMakeFolder(sBgFolders, container); + folderInfo.add(info); + 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 { + // Failed to load the shortcut, probably because the + // activity manager couldn't resolve it (maybe the app + // was uninstalled), or the db row was somehow screwed up. + // Delete it. + id = c.getLong(idIndex); + Log.e(TAG, "Error loading shortcut " + id + ", removing it"); + contentResolver.delete(LauncherSettings.Favorites.getContentUri( + id, false), null, null); + } + break; + + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + id = c.getLong(idIndex); + FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id); + + folderInfo.title = c.getString(titleIndex); + folderInfo.id = id; + container = c.getInt(containerIndex); + folderInfo.container = container; + folderInfo.screen = c.getInt(screenIndex); + folderInfo.cellX = c.getInt(cellXIndex); + folderInfo.cellY = c.getInt(cellYIndex); + + // check & update map of what's occupied + if (!checkItemPlacement(occupied, folderInfo)) { + break; + } + switch (container) { + case LauncherSettings.Favorites.CONTAINER_DESKTOP: + case LauncherSettings.Favorites.CONTAINER_HOTSEAT: + sBgWorkspaceItems.add(folderInfo); + break; + } + + sBgItemsIdMap.put(folderInfo.id, folderInfo); + sBgFolders.put(folderInfo.id, folderInfo); + break; + + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + // Read all Launcher-specific widget details + int appWidgetId = c.getInt(appWidgetIdIndex); + id = c.getLong(idIndex); + + final AppWidgetProviderInfo provider = + widgets.getAppWidgetInfo(appWidgetId); + + if (!isSafeMode && (provider == null || provider.provider == null || + provider.provider.getPackageName() == null)) { + String log = "Deleting widget that isn't installed anymore: id=" + + id + " appWidgetId=" + appWidgetId; + Log.e(TAG, log); + Launcher.sDumpLogs.add(log); + itemsToRemove.add(id); + } else { + appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, + provider.provider); + appWidgetInfo.id = id; + appWidgetInfo.screen = c.getInt(screenIndex); + appWidgetInfo.cellX = c.getInt(cellXIndex); + appWidgetInfo.cellY = c.getInt(cellYIndex); + appWidgetInfo.spanX = c.getInt(spanXIndex); + appWidgetInfo.spanY = c.getInt(spanYIndex); + int[] minSpan = Launcher.getMinSpanForWidget(context, provider); + appWidgetInfo.minSpanX = minSpan[0]; + appWidgetInfo.minSpanY = minSpan[1]; + + container = c.getInt(containerIndex); + if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP && + container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + Log.e(TAG, "Widget found where container != " + + "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!"); + continue; + } + appWidgetInfo.container = c.getInt(containerIndex); + + // check & update map of what's occupied + if (!checkItemPlacement(occupied, appWidgetInfo)) { + break; + } + sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); + sBgAppWidgets.add(appWidgetInfo); + } + break; + } + } catch (Exception e) { + Log.w(TAG, "Desktop items loading interrupted:", e); + } + } + } finally { + c.close(); + } + + if (itemsToRemove.size() > 0) { + ContentProviderClient client = contentResolver.acquireContentProviderClient( + LauncherSettings.Favorites.CONTENT_URI); + // 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); + } + } + } + + if (DEBUG_LOADERS) { + Log.d(TAG, "loaded workspace in " + (SystemClock.uptimeMillis()-t) + "ms"); + Log.d(TAG, "workspace layout: "); + for (int y = 0; y < mCellCountY; y++) { + String line = ""; + for (int s = 0; s < Launcher.SCREEN_COUNT; s++) { + if (s > 0) { + line += " | "; + } + for (int x = 0; x < mCellCountX; x++) { + line += ((occupied[s][x][y] != null) ? "#" : "."); + } + } + Log.d(TAG, "[ " + line + " ]"); + } + } + } + } + + /** Filters the set of items who are directly or indirectly (via another container) on the + * specified screen. */ + private void filterCurrentWorkspaceItems(int currentScreen, + ArrayList allWorkspaceItems, + ArrayList currentScreenItems, + ArrayList otherScreenItems) { + // Purge any null ItemInfos + Iterator iter = allWorkspaceItems.iterator(); + while (iter.hasNext()) { + ItemInfo i = iter.next(); + if (i == null) { + iter.remove(); + } + } + + // If we aren't filtering on a screen, then the set of items to load is the full set of + // items given. + if (currentScreen < 0) { + currentScreenItems.addAll(allWorkspaceItems); + } + + // Order the set of items by their containers first, this allows use to walk through the + // list sequentially, build up a list of containers that are in the specified screen, + // as well as all items in those containers. + Set itemsOnScreen = new HashSet(); + Collections.sort(allWorkspaceItems, new Comparator() { + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return (int) (lhs.container - rhs.container); + } + }); + for (ItemInfo info : allWorkspaceItems) { + if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + if (info.screen == currentScreen) { + currentScreenItems.add(info); + itemsOnScreen.add(info.id); + } else { + otherScreenItems.add(info); + } + } else if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + currentScreenItems.add(info); + itemsOnScreen.add(info.id); + } else { + if (itemsOnScreen.contains(info.container)) { + currentScreenItems.add(info); + itemsOnScreen.add(info.id); + } else { + otherScreenItems.add(info); + } + } + } + } + + /** Filters the set of widgets which are on the specified screen. */ + private void filterCurrentAppWidgets(int currentScreen, + ArrayList appWidgets, + ArrayList currentScreenWidgets, + ArrayList otherScreenWidgets) { + // If we aren't filtering on a screen, then the set of items to load is the full set of + // widgets given. + if (currentScreen < 0) { + currentScreenWidgets.addAll(appWidgets); + } + + for (LauncherAppWidgetInfo widget : appWidgets) { + if (widget == null) continue; + if (widget.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && + widget.screen == currentScreen) { + currentScreenWidgets.add(widget); + } else { + otherScreenWidgets.add(widget); + } + } + } + + /** Filters the set of folders which are on the specified screen. */ + private void filterCurrentFolders(int currentScreen, + HashMap itemsIdMap, + HashMap folders, + HashMap currentScreenFolders, + HashMap otherScreenFolders) { + // If we aren't filtering on a screen, then the set of items to load is the full set of + // widgets given. + if (currentScreen < 0) { + currentScreenFolders.putAll(folders); + } + + 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.screen == currentScreen) { + currentScreenFolders.put(id, folder); + } else { + otherScreenFolders.put(id, folder); + } + } + } + + /** Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to + * right) */ + private void sortWorkspaceItemsSpatially(ArrayList workspaceItems) { + // XXX: review this + Collections.sort(workspaceItems, new Comparator() { + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + int cellCountX = LauncherModel.getCellCountX(); + int cellCountY = LauncherModel.getCellCountY(); + int screenOffset = cellCountX * cellCountY; + int containerOffset = screenOffset * (Launcher.SCREEN_COUNT + 1); // +1 hotseat + long lr = (lhs.container * containerOffset + lhs.screen * screenOffset + + lhs.cellY * cellCountX + lhs.cellX); + long rr = (rhs.container * containerOffset + rhs.screen * screenOffset + + rhs.cellY * cellCountX + rhs.cellX); + return (int) (lr - rr); + } + }); + } + + private void bindWorkspaceItems(final Callbacks oldCallbacks, + final ArrayList workspaceItems, + final ArrayList appWidgets, + final HashMap folders, + ArrayList deferredBindRunnables) { + + final boolean postOnMainThread = (deferredBindRunnables != null); + + // Bind the workspace items + int N = workspaceItems.size(); + for (int i = 0; i < N; i += ITEMS_CHUNK) { + final int start = i; + final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i); + final Runnable r = new Runnable() { + @Override + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.bindItems(workspaceItems, start, start+chunkSize); + } + } + }; + if (postOnMainThread) { + deferredBindRunnables.add(r); + } else { + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + } + + // Bind the folders + if (!folders.isEmpty()) { + final Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.bindFolders(folders); + } + } + }; + if (postOnMainThread) { + deferredBindRunnables.add(r); + } else { + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + } + + // Bind the widgets, one at a time + N = appWidgets.size(); + for (int i = 0; i < N; i++) { + final LauncherAppWidgetInfo widget = appWidgets.get(i); + final Runnable r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.bindAppWidget(widget); + } + } + }; + if (postOnMainThread) { + deferredBindRunnables.add(r); + } else { + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + } + } + + /** + * Binds all loaded data to actual views on the main thread. + */ + private void bindWorkspace(int synchronizeBindPage) { + final long t = SystemClock.uptimeMillis(); + Runnable r; + + // Don't use these two variables in any of the callback runnables. + // Otherwise we hold a reference to them. + final Callbacks oldCallbacks = mCallbacks.get(); + if (oldCallbacks == null) { + // This launcher has exited and nobody bothered to tell us. Just bail. + Log.w(TAG, "LoaderTask running with no launcher"); + return; + } + + final boolean isLoadingSynchronously = (synchronizeBindPage > -1); + final int currentScreen = isLoadingSynchronously ? synchronizeBindPage : + oldCallbacks.getCurrentWorkspaceScreen(); + + // Load all the items that are on the current page first (and in the process, unbind + // all the existing workspace items before we call startBinding() below. + unbindWorkspaceItemsOnMainThread(); + ArrayList workspaceItems = new ArrayList(); + ArrayList appWidgets = + new ArrayList(); + HashMap folders = new HashMap(); + HashMap itemsIdMap = new HashMap(); + synchronized (sBgLock) { + workspaceItems.addAll(sBgWorkspaceItems); + appWidgets.addAll(sBgAppWidgets); + folders.putAll(sBgFolders); + itemsIdMap.putAll(sBgItemsIdMap); + } + + ArrayList currentWorkspaceItems = new ArrayList(); + ArrayList otherWorkspaceItems = new ArrayList(); + ArrayList currentAppWidgets = + new ArrayList(); + ArrayList otherAppWidgets = + new ArrayList(); + HashMap currentFolders = new HashMap(); + HashMap otherFolders = new HashMap(); + + // Separate the items that are on the current screen, and all the other remaining items + filterCurrentWorkspaceItems(currentScreen, workspaceItems, currentWorkspaceItems, + otherWorkspaceItems); + filterCurrentAppWidgets(currentScreen, appWidgets, currentAppWidgets, + otherAppWidgets); + filterCurrentFolders(currentScreen, itemsIdMap, folders, currentFolders, + otherFolders); + sortWorkspaceItemsSpatially(currentWorkspaceItems); + sortWorkspaceItemsSpatially(otherWorkspaceItems); + + // Tell the workspace that we're about to start binding items + r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.startBinding(); + } + } + }; + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + + // Load items on the current page + bindWorkspaceItems(oldCallbacks, currentWorkspaceItems, currentAppWidgets, + currentFolders, null); + if (isLoadingSynchronously) { + r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.onPageBoundSynchronously(currentScreen); + } + } + }; + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + + // Load all the remaining pages (if we are loading synchronously, we want to defer this + // work until after the first render) + mDeferredBindRunnables.clear(); + bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets, otherFolders, + (isLoadingSynchronously ? mDeferredBindRunnables : null)); + + // Tell the workspace that we're done binding items + r = new Runnable() { + public void run() { + Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.finishBindingItems(); + } + + // If we're profiling, ensure this is the last thing in the queue. + if (DEBUG_LOADERS) { + Log.d(TAG, "bound workspace in " + + (SystemClock.uptimeMillis()-t) + "ms"); + } + + mIsLoadingAndBindingWorkspace = false; + } + }; + if (isLoadingSynchronously) { + mDeferredBindRunnables.add(r); + } else { + runOnMainThread(r, MAIN_THREAD_BINDING_RUNNABLE); + } + } + + private void loadAndBindAllApps() { + if (DEBUG_LOADERS) { + Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded); + } + if (!mAllAppsLoaded) { + loadAllAppsByBatch(); + synchronized (LoaderTask.this) { + if (mStopped) { + return; + } + mAllAppsLoaded = true; + } + } else { + onlyBindAllApps(); + } + } + + private void onlyBindAllApps() { + final Callbacks oldCallbacks = mCallbacks.get(); + if (oldCallbacks == null) { + // This launcher has exited and nobody bothered to tell us. Just bail. + Log.w(TAG, "LoaderTask running with no launcher (onlyBindAllApps)"); + return; + } + + // shallow copy + @SuppressWarnings("unchecked") + final ArrayList list + = (ArrayList) mBgAllAppsList.data.clone(); + Runnable r = new Runnable() { + public void run() { + final long t = SystemClock.uptimeMillis(); + final Callbacks callbacks = tryGetCallbacks(oldCallbacks); + if (callbacks != null) { + callbacks.bindAllApplications(list); + } + if (DEBUG_LOADERS) { + Log.d(TAG, "bound all " + list.size() + " apps from cache in " + + (SystemClock.uptimeMillis()-t) + "ms"); + } + } + }; + boolean isRunningOnMainThread = !(sWorkerThread.getThreadId() == Process.myTid()); + if (oldCallbacks.isAllAppsVisible() && isRunningOnMainThread) { + r.run(); + } else { + mHandler.post(r); + } + } + + private void loadAllAppsByBatch() { + final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + + // Don't use these two variables in any of the callback runnables. + // Otherwise we hold a reference to them. + final Callbacks oldCallbacks = mCallbacks.get(); + if (oldCallbacks == null) { + // This launcher has exited and nobody bothered to tell us. Just bail. + Log.w(TAG, "LoaderTask running with no launcher (loadAllAppsByBatch)"); + return; + } + + final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + + final PackageManager packageManager = mContext.getPackageManager(); + List apps = null; + + int N = Integer.MAX_VALUE; + + int startIndex; + int i=0; + int batchSize = -1; + while (i < N && !mStopped) { + if (i == 0) { + mBgAllAppsList.clear(); + final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + apps = packageManager.queryIntentActivities(mainIntent, 0); + if (DEBUG_LOADERS) { + Log.d(TAG, "queryIntentActivities took " + + (SystemClock.uptimeMillis()-qiaTime) + "ms"); + } + if (apps == null) { + return; + } + N = apps.size(); + if (DEBUG_LOADERS) { + Log.d(TAG, "queryIntentActivities got " + N + " apps"); + } + if (N == 0) { + // There are no apps?!? + return; + } + if (mBatchSize == 0) { + batchSize = N; + } else { + batchSize = mBatchSize; + } + + final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + Collections.sort(apps, + new LauncherModel.ShortcutNameComparator(packageManager, mLabelCache)); + if (DEBUG_LOADERS) { + Log.d(TAG, "sort took " + + (SystemClock.uptimeMillis()-sortTime) + "ms"); + } + } + + final long t2 = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; + + startIndex = i; + for (int j=0; i added = mBgAllAppsList.added; + mBgAllAppsList.added = new ArrayList(); + + mHandler.post(new Runnable() { + public void run() { + final long t = SystemClock.uptimeMillis(); + if (callbacks != null) { + if (first) { + callbacks.bindAllApplications(added); + } else { + callbacks.bindAppsAdded(added); + } + if (DEBUG_LOADERS) { + Log.d(TAG, "bound " + added.size() + " apps in " + + (SystemClock.uptimeMillis() - t) + "ms"); + } + } else { + Log.i(TAG, "not binding apps: no Launcher activity"); + } + } + }); + + if (DEBUG_LOADERS) { + Log.d(TAG, "batch of " + (i-startIndex) + " icons processed in " + + (SystemClock.uptimeMillis()-t2) + "ms"); + } + + if (mAllAppsLoadDelay > 0 && i < N) { + try { + if (DEBUG_LOADERS) { + Log.d(TAG, "sleeping for " + mAllAppsLoadDelay + "ms"); + } + Thread.sleep(mAllAppsLoadDelay); + } catch (InterruptedException exc) { } + } + } + + if (DEBUG_LOADERS) { + Log.d(TAG, "cached all " + N + " apps in " + + (SystemClock.uptimeMillis()-t) + "ms" + + (mAllAppsLoadDelay > 0 ? " (including delay)" : "")); + } + } + + 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()); + } + } + } + + void enqueuePackageUpdated(PackageUpdatedTask task) { + sWorker.post(task); + } + + private class PackageUpdatedTask implements Runnable { + int mOp; + String[] mPackages; + + public static final int OP_NONE = 0; + public static final int OP_ADD = 1; + public static final int OP_UPDATE = 2; + public static final int OP_REMOVE = 3; // uninstlled + public static final int OP_UNAVAILABLE = 4; // external media unmounted + + + public PackageUpdatedTask(int op, String[] packages) { + mOp = op; + mPackages = packages; + } + + public void run() { + final Context context = mApp; + + final String[] packages = mPackages; + final int N = packages.length; + switch (mOp) { + case OP_ADD: + for (int i=0; i added = null; + ArrayList modified = null; + final ArrayList removedApps = new ArrayList(); + + if (mBgAllAppsList.added.size() > 0) { + added = new ArrayList(mBgAllAppsList.added); + mBgAllAppsList.added.clear(); + } + if (mBgAllAppsList.modified.size() > 0) { + modified = new ArrayList(mBgAllAppsList.modified); + mBgAllAppsList.modified.clear(); + } + if (mBgAllAppsList.removed.size() > 0) { + removedApps.addAll(mBgAllAppsList.removed); + mBgAllAppsList.removed.clear(); + } + + final Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == null) { + Log.w(TAG, "Nobody to tell about the new app. Launcher is probably loading."); + return; + } + + if (added != null) { + final ArrayList addedFinal = added; + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindAppsAdded(addedFinal); + } + } + }); + } + if (modified != null) { + final ArrayList modifiedFinal = modified; + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindAppsUpdated(modifiedFinal); + } + } + }); + } + // If a package has been removed, or an app has been removed as a result of + // an update (for example), make the removed callback. + if (mOp == OP_REMOVE || !removedApps.isEmpty()) { + final boolean permanent = (mOp == OP_REMOVE); + final ArrayList removedPackageNames = + new ArrayList(Arrays.asList(packages)); + + mHandler.post(new Runnable() { + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindComponentsRemoved(removedPackageNames, + removedApps, permanent); + } + } + }); + } + + final ArrayList widgetsAndShortcuts = + getSortedWidgetsAndShortcuts(context); + mHandler.post(new Runnable() { + @Override + public void run() { + Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; + if (callbacks == cb && cb != null) { + callbacks.bindPackagesUpdated(widgetsAndShortcuts); + } + } + }); + FozaPackageManager.get().acquireObtainAppSplash(); + } + } + + // 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(AppWidgetManager.getInstance(context).getInstalledProviders()); + Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); + widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0)); + Collections.sort(widgetsAndShortcuts, + new LauncherModel.WidgetAndShortcutNameComparator(packageManager)); + return widgetsAndShortcuts; + } + + /** + * 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, Context context) { + return getShortcutInfo(manager, intent, context, null, -1, -1, null); + } + + /** + * 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, Context context, + Cursor c, int iconIndex, int titleIndex, HashMap labelCache) { + Bitmap icon = null; + final ShortcutInfo info = new ShortcutInfo(); + + ComponentName componentName = intent.getComponent(); + if (componentName == null) { + return null; + } + + try { + PackageInfo pi = manager.getPackageInfo(componentName.getPackageName(), 0); + if (!pi.applicationInfo.enabled) { + // If we return null here, the corresponding item will be removed from the launcher + // db and will not appear in the workspace. + return null; + } + } catch (NameNotFoundException e) { + Log.d(TAG, "getPackInfo failed for package " + componentName.getPackageName()); + } + + // TODO: See if the PackageManager knows about this case. If it doesn't + // then return null & delete this. + + // 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. + + // Attempt to use queryIntentActivities to get the ResolveInfo (with IntentFilter info) and + // if that fails, or is ambiguious, fallback to the standard way of getting the resolve info + // via resolveActivity(). + ResolveInfo resolveInfo = null; + ComponentName oldComponent = intent.getComponent(); + Intent newIntent = new Intent(intent.getAction(), null); + newIntent.addCategory(Intent.CATEGORY_LAUNCHER); + newIntent.setPackage(oldComponent.getPackageName()); + List infos = manager.queryIntentActivities(newIntent, 0); + for (ResolveInfo i : infos) { + ComponentName cn = new ComponentName(i.activityInfo.packageName, + i.activityInfo.name); + if (cn.equals(oldComponent)) { + resolveInfo = i; + } + } + if (resolveInfo == null) { + resolveInfo = manager.resolveActivity(intent, 0); + } + if (resolveInfo != null) { + icon = mIconCache.getIcon(componentName, resolveInfo, labelCache); + } + // the db + if (icon == null) { + if (c != null) { + icon = getIconFromCursor(c, iconIndex, context); + } + } + // the fallback icon + if (icon == null) { + icon = getFallbackIcon(); + info.usingFallbackIcon = true; + } + info.setIcon(icon); + + // from the resource + if (resolveInfo != null) { + ComponentName key = LauncherModel.getComponentNameFromResolveInfo(resolveInfo); + if (labelCache != null && labelCache.containsKey(key)) { + info.title = labelCache.get(key); + } else { + info.title = resolveInfo.activityInfo.loadLabel(manager); + if (labelCache != null) { + labelCache.put(key, info.title); + } + } + } + // from the db + if (info.title == null) { + if (c != null) { + info.title = 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; + return info; + } + + /** + * Returns the set of workspace ShortcutInfos with the specified intent. + */ + static ArrayList getWorkspaceShortcutItemInfosWithIntent(Intent intent) { + ArrayList items = new ArrayList(); + synchronized (sBgLock) { + for (ItemInfo info : sBgWorkspaceItems) { + if (info instanceof ShortcutInfo) { + ShortcutInfo shortcut = (ShortcutInfo) info; + if (shortcut.intent.toUri(0).equals(intent.toUri(0))) { + items.add(shortcut); + } + } + } + } + return items; + } + + /** + * 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; + final ShortcutInfo info = new ShortcutInfo(); + info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + + // TODO: If there's an explicit component and we can't install that, delete it. + + info.title = 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); + PackageManager packageManager = context.getPackageManager(); + info.customIcon = false; + // the resource + try { + Resources resources = packageManager.getResourcesForApplication(packageName); + if (resources != null) { + final int id = resources.getIdentifier(resourceName, null, null); + icon = Utilities.createIconBitmap( + mIconCache.getFullResIcon(resources, id), context); + } + } catch (Exception e) { + // drop this. we have other places to look for icons + } + // the db + if (icon == null) { + icon = getIconFromCursor(c, iconIndex, context); + } + // the fallback icon + if (icon == null) { + icon = getFallbackIcon(); + info.usingFallbackIcon = true; + } + break; + case LauncherSettings.Favorites.ICON_TYPE_BITMAP: + icon = getIconFromCursor(c, iconIndex, context); + if (icon == null) { + icon = getFallbackIcon(); + info.customIcon = false; + info.usingFallbackIcon = true; + } else { + info.customIcon = true; + } + break; + default: + icon = getFallbackIcon(); + 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; + } + } + + ShortcutInfo addShortcut(Context context, Intent data, long container, int screen, + int cellX, int cellY, boolean notify) { + final ShortcutInfo info = infoFromShortcutIntent(context, data, null); + if (info == null) { + return null; + } + addItemToDatabase(context, info, container, screen, cellX, cellY, notify); + + return info; + } + + /** + * Attempts to find an AppWidgetProviderInfo that matches the given component. + */ + 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; + } + + /** + * Returns a list of all the widgets that can handle configuration with a particular mimeType. + */ + List resolveWidgetsForMimeType(Context context, String mimeType) { + final PackageManager packageManager = context.getPackageManager(); + final List supportedConfigurationActivities = + new ArrayList(); + + final Intent supportsIntent = + new Intent(InstallWidgetReceiver.ACTION_SUPPORTS_CLIPDATA_MIMETYPE); + supportsIntent.setType(mimeType); + + // Create a set of widget configuration components that we can test against + final List widgets = + AppWidgetManager.getInstance(context).getInstalledProviders(); + final HashMap configurationComponentToWidget = + new HashMap(); + for (AppWidgetProviderInfo info : widgets) { + configurationComponentToWidget.put(info.configure, info); + } + + // Run through each of the intents that can handle this type of clip data, and cross + // reference them with the components that are actual configuration components + final List activities = packageManager.queryIntentActivities(supportsIntent, + PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo info : activities) { + final ActivityInfo activityInfo = info.activityInfo; + final ComponentName infoComponent = new ComponentName(activityInfo.packageName, + activityInfo.name); + if (configurationComponentToWidget.containsKey(infoComponent)) { + supportedConfigurationActivities.add( + new InstallWidgetReceiver.WidgetMimeTypeHandlerData(info, + configurationComponentToWidget.get(infoComponent))); + } + } + return supportedConfigurationActivities; + } + + ShortcutInfo infoFromShortcutIntent(Context context, Intent data, Bitmap fallbackIcon) { + Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); + String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); + Parcelable bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); + + if (intent == null) { + // If the intent is null, we can't construct a valid ShortcutInfo, so we return null + Log.e(TAG, "Can't construct ShorcutInfo with null intent"); + return null; + } + + Bitmap icon = null; + boolean customIcon = false; + ShortcutIconResource iconResource = null; + + if (bitmap != null && bitmap instanceof Bitmap) { + icon = Utilities.createIconBitmap(new FastBitmapDrawable((Bitmap)bitmap), context); + customIcon = true; + } else { + Parcelable extra = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE); + if (extra != null && extra instanceof ShortcutIconResource) { + try { + iconResource = (ShortcutIconResource) extra; + final PackageManager packageManager = context.getPackageManager(); + Resources resources = packageManager.getResourcesForApplication( + iconResource.packageName); + final int id = resources.getIdentifier(iconResource.resourceName, null, null); + icon = Utilities.createIconBitmap( + mIconCache.getFullResIcon(resources, id), context); + } catch (Exception e) { + Log.w(TAG, "Could not load shortcut icon: " + extra); + } + } + } + + final ShortcutInfo info = new ShortcutInfo(); + + if (icon == null) { + if (fallbackIcon != null) { + icon = fallbackIcon; + } else { + icon = getFallbackIcon(); + info.usingFallbackIcon = true; + } + } + info.setIcon(icon); + + info.title = name; + info.intent = intent; + info.customIcon = customIcon; + info.iconResource = iconResource; + + return info; + } + + boolean queueIconToBeChecked(HashMap cache, ShortcutInfo info, Cursor c, + int iconIndex) { + // If apps can't be on SD, don't even bother. + if (!mAppsCanBeOnExternalStorage) { + 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) { + // See if a placeholder was created for us already + FolderInfo folderInfo = folders.get(id); + if (folderInfo == null) { + // No placeholder -- create a new instance + folderInfo = new FolderInfo(); + folders.put(id, folderInfo); + } + return folderInfo; + } + + public static final Comparator getAppNameComparator() { + final Collator collator = Collator.getInstance(); + return new Comparator() { + public final int compare(ApplicationInfo a, ApplicationInfo b) { + int result = collator.compare(a.title.toString(), b.title.toString()); + if (result == 0) { + result = a.componentName.compareTo(b.componentName); + } + return result; + } + }; + } + public static final Comparator APP_INSTALL_TIME_COMPARATOR + = new Comparator() { + public final int compare(ApplicationInfo a, ApplicationInfo b) { + if (a.firstInstallTime < b.firstInstallTime) return 1; + if (a.firstInstallTime > b.firstInstallTime) return -1; + return 0; + } + }; + public static final Comparator getWidgetNameComparator() { + final Collator collator = Collator.getInstance(); + return new Comparator() { + public final int compare(AppWidgetProviderInfo a, AppWidgetProviderInfo b) { + return collator.compare(a.label.toString(), b.label.toString()); + } + }; + } + 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 PackageManager mPackageManager; + private HashMap mLabelCache; + ShortcutNameComparator(PackageManager pm) { + mPackageManager = pm; + mLabelCache = new HashMap(); + mCollator = Collator.getInstance(); + } + ShortcutNameComparator(PackageManager pm, HashMap labelCache) { + mPackageManager = pm; + mLabelCache = labelCache; + mCollator = Collator.getInstance(); + } + public final int compare(ResolveInfo a, ResolveInfo b) { + CharSequence labelA, labelB; + ComponentName keyA = LauncherModel.getComponentNameFromResolveInfo(a); + ComponentName keyB = LauncherModel.getComponentNameFromResolveInfo(b); + if (mLabelCache.containsKey(keyA)) { + labelA = mLabelCache.get(keyA); + } else { + labelA = a.loadLabel(mPackageManager).toString(); + + mLabelCache.put(keyA, labelA); + } + if (mLabelCache.containsKey(keyB)) { + labelB = mLabelCache.get(keyB); + } else { + labelB = b.loadLabel(mPackageManager).toString(); + + mLabelCache.put(keyB, labelB); + } + return mCollator.compare(labelA, labelB); + } + }; + public static class WidgetAndShortcutNameComparator implements Comparator { + private Collator mCollator; + private PackageManager mPackageManager; + private HashMap mLabelCache; + WidgetAndShortcutNameComparator(PackageManager pm) { + mPackageManager = pm; + 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) ? + ((AppWidgetProviderInfo) a).label : + ((ResolveInfo) a).loadLabel(mPackageManager).toString(); + mLabelCache.put(a, labelA); + } + if (mLabelCache.containsKey(b)) { + labelB = mLabelCache.get(b); + } else { + labelB = (b instanceof AppWidgetProviderInfo) ? + ((AppWidgetProviderInfo) b).label : + ((ResolveInfo) b).loadLabel(mPackageManager).toString(); + mLabelCache.put(b, labelB); + } + return mCollator.compare(labelA, labelB); + } + }; + + public void dumpState() { + Log.d(TAG, "mCallbacks=" + mCallbacks); + ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.data", mBgAllAppsList.data); + ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.added", mBgAllAppsList.added); + ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.removed", mBgAllAppsList.removed); + ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.modified", mBgAllAppsList.modified); + if (mLoaderTask != null) { + mLoaderTask.dumpState(); + } else { + Log.d(TAG, "mLoaderTask=null"); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherProvider.java b/app/src/main/java/com/android/launcher2/LauncherProvider.java new file mode 100644 index 0000000..70db1b1 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherProvider.java @@ -0,0 +1,1193 @@ +/* + * 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.launcher2; + +import android.app.SearchManager; +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.SQLException; +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.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import com.android.launcher.R; +import com.android.launcher2.LauncherSettings.Favorites; + +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.List; + +public class LauncherProvider extends ContentProvider { + private static final String TAG = "Launcher.LauncherProvider"; + private static final boolean LOGD = false; + + private static final String DATABASE_NAME = "launcher.db"; + + private static final int DATABASE_VERSION = 12; + + static final String AUTHORITY = "com.android.launcher2.settings"; + + static final String TABLE_FAVORITES = "favorites"; + static final String PARAMETER_NOTIFY = "notify"; + static final String DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED = + "DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED"; + static final String DEFAULT_WORKSPACE_RESOURCE_ID = + "DEFAULT_WORKSPACE_RESOURCE_ID"; + + private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE = + "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE"; + + /** + * {@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; + + @Override + public boolean onCreate() { + mOpenHelper = new DatabaseHelper(getContext()); + ((LauncherApplication) getContext()).setLauncherProvider(this); + return true; + } + + @Override + public String getType(Uri uri) { + SqlArguments args = new SqlArguments(uri, null, null); + if (TextUtils.isEmpty(args.where)) { + return "vnd.android.cursor.dir/" + args.table; + } else { + return "vnd.android.cursor.item/" + args.table; + } + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + + SqlArguments args = new SqlArguments(uri, selection, selectionArgs); + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(args.table); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder); + result.setNotificationUri(getContext().getContentResolver(), uri); + + return result; + } + + private static long dbInsertAndCheck(DatabaseHelper helper, + SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { + if (!values.containsKey(LauncherSettings.Favorites._ID)) { + throw new RuntimeException("Error: attempting to add item without specifying an id"); + } + return db.insert(table, nullColumnHack, values); + } + + private static void deleteId(SQLiteDatabase db, long id) { + Uri uri = LauncherSettings.Favorites.getContentUri(id, false); + SqlArguments args = new SqlArguments(uri, null, null); + db.delete(args.table, args.where, args.args); + } + + @Override + public Uri insert(Uri uri, ContentValues initialValues) { + SqlArguments args = new SqlArguments(uri); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); + if (rowId <= 0) return null; + + uri = ContentUris.withAppendedId(uri, rowId); + sendNotify(uri); + + return uri; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + SqlArguments args = new SqlArguments(uri); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + int numValues = values.length; + for (int i = 0; i < numValues; i++) { + if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) { + return 0; + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + sendNotify(uri); + return values.length; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + SqlArguments args = new SqlArguments(uri, selection, selectionArgs); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count = db.delete(args.table, args.where, args.args); + if (count > 0) sendNotify(uri); + + return count; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + SqlArguments args = new SqlArguments(uri, selection, selectionArgs); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count = db.update(args.table, values, args.where, args.args); + if (count > 0) sendNotify(uri); + + return count; + } + + private void sendNotify(Uri uri) { + String notify = uri.getQueryParameter(PARAMETER_NOTIFY); + if (notify == null || "true".equals(notify)) { + getContext().getContentResolver().notifyChange(uri, null); + } + } + + public long generateNewId() { + return mOpenHelper.generateNewId(); + } + + /** + * @param workspaceResId that can be 0 to use default or non-zero for specific resource + */ + synchronized public void loadDefaultFavoritesIfNecessary(int origWorkspaceResId) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE); + if (sp.getBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, false)) { + int workspaceResId = origWorkspaceResId; + + // Use default workspace resource if none provided + if (workspaceResId == 0) { + workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, R.xml.default_workspace); + } + + // Populate favorites table with initial favorites + SharedPreferences.Editor editor = sp.edit(); + editor.remove(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED); + if (origWorkspaceResId != 0) { + editor.putInt(DEFAULT_WORKSPACE_RESOURCE_ID, origWorkspaceResId); + } + mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), workspaceResId); + editor.commit(); + } + } + + private static class DatabaseHelper extends SQLiteOpenHelper { + private static final String TAG_FAVORITES = "favorites"; + private static final String TAG_FAVORITE = "favorite"; + private static final String TAG_CLOCK = "clock"; + private static final String TAG_SEARCH = "search"; + 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_EXTRA = "extra"; + + private final Context mContext; + private final AppWidgetHost mAppWidgetHost; + private long mMaxId = -1; + + DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + mContext = context; + mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); + + // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from + // the DB here + if (mMaxId == -1) { + mMaxId = initializeMaxId(getWritableDatabase()); + } + } + + /** + * 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"); + + mMaxId = 1; + + db.execSQL("CREATE TABLE favorites (" + + "_id INTEGER PRIMARY KEY," + + "title TEXT," + + "intent TEXT," + + "container INTEGER," + + "screen INTEGER," + + "cellX INTEGER," + + "cellY INTEGER," + + "spanX INTEGER," + + "spanY INTEGER," + + "itemType INTEGER," + + "appWidgetId INTEGER NOT NULL DEFAULT -1," + + "isShortcut INTEGER," + + "iconType INTEGER," + + "iconPackage TEXT," + + "iconResource TEXT," + + "icon BLOB," + + "uri TEXT," + + "displayMode INTEGER" + + ");"); + + // Database was just created, so wipe any previous widgets + if (mAppWidgetHost != null) { + mAppWidgetHost.deleteHost(); + sendAppWidgetResetNotify(); + } + + if (!convertDatabase(db)) { + // Set a shared pref so that we know we need to load the default workspace later + setFlagToLoadDefaultWorkspaceLater(); + } + } + + private void setFlagToLoadDefaultWorkspaceLater() { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, true); + editor.commit(); + } + + private boolean convertDatabase(SQLiteDatabase db) { + if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade"); + boolean converted = false; + + final Uri uri = Uri.parse("content://" + Settings.AUTHORITY + + "/old_favorites?notify=true"); + 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 && cursor.getCount() > 0) { + try { + converted = copyFromCursor(db, cursor) > 0; + } finally { + cursor.close(); + } + + if (converted) { + resolver.delete(uri, null, null); + } + } + + if (converted) { + // Convert widgets from this import into widgets + if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade"); + convertWidgets(db); + } + + return converted; + } + + private int copyFromCursor(SQLiteDatabase db, Cursor c) { + 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)); + rows[i++] = values; + } + + db.beginTransaction(); + int total = 0; + 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; + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (LOGD) Log.d(TAG, "onUpgrade triggered"); + + 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(); + } + + // Convert existing widgets only if table upgrade was successful + if (version == 3) { + convertWidgets(db); + } + } + + 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 launcher2 + // - 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 (mMaxId == -1) { + mMaxId = initializeMaxId(db); + } + + // Add default hotseat icons + loadFavorites(db, 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 != DATABASE_VERSION) { + Log.w(TAG, "Destroying all old data."); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); + onCreate(db); + } + } + + private boolean updateContactsShortcuts(SQLiteDatabase db) { + final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, + new int[] { Favorites.ITEM_TYPE_SHORTCUT }); + + Cursor c = null; + final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT"; + 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.setTransactionSuccessful(); + } catch (SQLException ex) { + Log.w(TAG, "Problem while upgrading contacts", 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.resampleIconBitmap( + 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 + // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp + // after that point + public long generateNewId() { + if (mMaxId < 0) { + throw new RuntimeException("Error: max id was not initialized"); + } + mMaxId += 1; + return mMaxId; + } + + private long initializeMaxId(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 id"); + } + + return id; + } + + /** + * 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(); + } + } + } + + private static final void beginDocument(XmlPullParser parser, String firstElementName) + throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + ; + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + if (!parser.getName().equals(firstElementName)) { + throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + + ", expected " + firstElementName); + } + } + + /** + * Loads the default set of favorite packages from an xml file. + * + * @param db The database to write the values into + * @param filterContainerId The specific container id of items to load + */ + private int loadFavorites(SQLiteDatabase db, int workspaceResourceId) { + Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + ContentValues values = new ContentValues(); + + PackageManager packageManager = mContext.getPackageManager(); + int allAppsButtonRank = + mContext.getResources().getInteger(R.integer.hotseat_all_apps_index); + int i = 0; + try { + XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId); + AttributeSet attrs = Xml.asAttributeSet(parser); + beginDocument(parser, TAG_FAVORITES); + + 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; + } + + boolean added = false; + final String name = parser.getName(); + + TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite); + + long container = LauncherSettings.Favorites.CONTAINER_DESKTOP; + if (a.hasValue(R.styleable.Favorite_container)) { + container = Long.valueOf(a.getString(R.styleable.Favorite_container)); + } + + String screen = a.getString(R.styleable.Favorite_screen); + String x = a.getString(R.styleable.Favorite_x); + String y = a.getString(R.styleable.Favorite_y); + + // If we are adding to the hotseat, the screen is used as the position in the + // hotseat. This screen can't be at position 0 because AllApps is in the + // zeroth position. + if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT + && Integer.valueOf(screen) == allAppsButtonRank) { + throw new RuntimeException("Invalid screen position for hotseat item"); + } + + values.clear(); + values.put(LauncherSettings.Favorites.CONTAINER, container); + values.put(LauncherSettings.Favorites.SCREEN, screen); + values.put(LauncherSettings.Favorites.CELLX, x); + values.put(LauncherSettings.Favorites.CELLY, y); + + if (TAG_FAVORITE.equals(name)) { + long id = addAppShortcut(db, values, a, packageManager, intent); + added = id >= 0; + } else if (TAG_SEARCH.equals(name)) { + added = addSearchWidget(db, values); + } else if (TAG_CLOCK.equals(name)) { + added = addClockWidget(db, values); + } else if (TAG_APPWIDGET.equals(name)) { + added = addAppWidget(parser, attrs, type, db, values, a, packageManager); + } else if (TAG_SHORTCUT.equals(name)) { + long id = addUriShortcut(db, values, a); + added = id >= 0; + } else if (TAG_FOLDER.equals(name)) { + String title; + int titleResId = a.getResourceId(R.styleable.Favorite_title, -1); + if (titleResId != -1) { + title = mContext.getResources().getString(titleResId); + } else { + title = ""; + } + values.put(LauncherSettings.Favorites.TITLE, title); + long folderId = addFolder(db, values); + added = folderId >= 0; + + ArrayList folderItems = new ArrayList(); + + int folderDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > folderDepth) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final String folder_item_name = parser.getName(); + + TypedArray ar = mContext.obtainStyledAttributes(attrs, + R.styleable.Favorite); + values.clear(); + values.put(LauncherSettings.Favorites.CONTAINER, folderId); + + if (TAG_FAVORITE.equals(folder_item_name) && folderId >= 0) { + long id = + addAppShortcut(db, values, ar, packageManager, intent); + if (id >= 0) { + folderItems.add(id); + } + } else if (TAG_SHORTCUT.equals(folder_item_name) && folderId >= 0) { + long id = addUriShortcut(db, values, ar); + if (id >= 0) { + folderItems.add(id); + } + } else { + throw new RuntimeException("Folders can " + + "contain only shortcuts"); + } + ar.recycle(); + } + // We can only have folders with >= 2 items, so we need to remove the + // folder and clean up if less than 2 items were included, or some + // failed to add, and less than 2 were actually added + if (folderItems.size() < 2 && folderId >= 0) { + // We just delete the folder and any items that made it + deleteId(db, folderId); + if (folderItems.size() > 0) { + deleteId(db, folderItems.get(0)); + } + added = false; + } + } + if (added) i++; + a.recycle(); + } + } catch (XmlPullParserException e) { + Log.w(TAG, "Got exception parsing favorites.", e); + } catch (IOException e) { + Log.w(TAG, "Got exception parsing favorites.", e); + } catch (RuntimeException e) { + Log.w(TAG, "Got exception parsing favorites.", e); + } + + return i; + } + + private long addAppShortcut(SQLiteDatabase db, ContentValues values, TypedArray a, + PackageManager packageManager, Intent intent) { + long id = -1; + ActivityInfo info; + String packageName = a.getString(R.styleable.Favorite_packageName); + String className = a.getString(R.styleable.Favorite_className); + try { + ComponentName cn; + try { + cn = new ComponentName(packageName, className); + info = packageManager.getActivityInfo(cn, 0); + } catch (PackageManager.NameNotFoundException nnfe) { + String[] packages = packageManager.currentToCanonicalPackageNames( + new String[] { packageName }); + cn = new ComponentName(packages[0], className); + info = packageManager.getActivityInfo(cn, 0); + } + id = generateNewId(); + intent.setComponent(cn); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + values.put(Favorites.INTENT, intent.toUri(0)); + values.put(Favorites.TITLE, info.loadLabel(packageManager).toString()); + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION); + values.put(Favorites.SPANX, 1); + values.put(Favorites.SPANY, 1); + values.put(Favorites._ID, generateNewId()); + if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { + return -1; + } + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Unable to add favorite: " + packageName + + "/" + className, e); + } + return id; + } + + private long addFolder(SQLiteDatabase db, ContentValues values) { + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER); + values.put(Favorites.SPANX, 1); + values.put(Favorites.SPANY, 1); + long id = generateNewId(); + values.put(Favorites._ID, id); + if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) <= 0) { + return -1; + } else { + return id; + } + } + + private ComponentName getSearchWidgetProvider() { + SearchManager searchManager = + (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + ComponentName searchComponent = searchManager.getGlobalSearchActivity(); + if (searchComponent == null) return null; + return getProviderInPackage(searchComponent.getPackageName()); + } + + /** + * Gets an appwidget provider from the given package. If the package contains more than + * one appwidget provider, an arbitrary one is returned. + */ + private ComponentName getProviderInPackage(String packageName) { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); + List providers = appWidgetManager.getInstalledProviders(); + if (providers == null) return null; + final int providerCount = providers.size(); + for (int i = 0; i < providerCount; i++) { + ComponentName provider = providers.get(i).provider; + if (provider != null && provider.getPackageName().equals(packageName)) { + return provider; + } + } + return null; + } + + private boolean addSearchWidget(SQLiteDatabase db, ContentValues values) { + ComponentName cn = getSearchWidgetProvider(); + return addAppWidget(db, values, cn, 4, 1, null); + } + + private boolean addClockWidget(SQLiteDatabase db, ContentValues values) { + ComponentName cn = new ComponentName("com.android.alarmclock", + "com.android.alarmclock.AnalogAppWidgetProvider"); + return addAppWidget(db, values, cn, 2, 2, null); + } + + private boolean addAppWidget(XmlResourceParser parser, AttributeSet attrs, int type, + SQLiteDatabase db, ContentValues values, TypedArray a, + PackageManager packageManager) throws XmlPullParserException, IOException { + + String packageName = a.getString(R.styleable.Favorite_packageName); + String className = a.getString(R.styleable.Favorite_className); + + if (packageName == null || className == null) { + return false; + } + + boolean hasPackage = true; + ComponentName cn = new ComponentName(packageName, className); + try { + packageManager.getReceiverInfo(cn, 0); + } catch (Exception e) { + String[] packages = packageManager.currentToCanonicalPackageNames( + new String[] { packageName }); + cn = new ComponentName(packages[0], className); + try { + packageManager.getReceiverInfo(cn, 0); + } catch (Exception e1) { + hasPackage = false; + } + } + + if (hasPackage) { + int spanX = a.getInt(R.styleable.Favorite_spanX, 0); + int spanY = a.getInt(R.styleable.Favorite_spanY, 0); + + // Read the extras + Bundle extras = new Bundle(); + int widgetDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > widgetDepth) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + TypedArray ar = mContext.obtainStyledAttributes(attrs, R.styleable.Extra); + if (TAG_EXTRA.equals(parser.getName())) { + String key = ar.getString(R.styleable.Extra_key); + String value = ar.getString(R.styleable.Extra_value); + if (key != null && value != null) { + extras.putString(key, value); + } else { + throw new RuntimeException("Widget extras must have a key and value"); + } + } else { + throw new RuntimeException("Widgets can contain only extras"); + } + ar.recycle(); + } + + return addAppWidget(db, values, cn, spanX, spanY, extras); + } + + return false; + } + + private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn, + int spanX, int spanY, Bundle extras) { + boolean allocatedAppWidgets = false; + final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); + + try { + int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); + + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); + values.put(Favorites.SPANX, spanX); + values.put(Favorites.SPANY, spanY); + values.put(Favorites.APPWIDGET_ID, appWidgetId); + values.put(Favorites._ID, generateNewId()); + dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values); + + allocatedAppWidgets = true; + + // TODO: need to check return value + appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn); + + // Send a broadcast to configure the widget + if (extras != null && !extras.isEmpty()) { + Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE); + intent.setComponent(cn); + intent.putExtras(extras); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + mContext.sendBroadcast(intent); + } + } catch (RuntimeException ex) { + Log.e(TAG, "Problem allocating appWidgetId", ex); + } + + return allocatedAppWidgets; + } + + private long addUriShortcut(SQLiteDatabase db, ContentValues values, + TypedArray a) { + Resources r = mContext.getResources(); + + final int iconResId = a.getResourceId(R.styleable.Favorite_icon, 0); + final int titleResId = a.getResourceId(R.styleable.Favorite_title, 0); + + Intent intent; + String uri = null; + try { + uri = a.getString(R.styleable.Favorite_uri); + intent = Intent.parseUri(uri, 0); + } catch (URISyntaxException e) { + Log.w(TAG, "Shortcut has malformed uri: " + uri); + return -1; // Oh well + } + + if (iconResId == 0 || titleResId == 0) { + Log.w(TAG, "Shortcut is missing title or icon resource ID"); + return -1; + } + + long id = generateNewId(); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + values.put(Favorites.INTENT, intent.toUri(0)); + values.put(Favorites.TITLE, r.getString(titleResId)); + values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT); + values.put(Favorites.SPANX, 1); + values.put(Favorites.SPANY, 1); + values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE); + values.put(Favorites.ICON_PACKAGE, mContext.getPackageName()); + values.put(Favorites.ICON_RESOURCE, r.getResourceName(iconResId)); + values.put(Favorites._ID, id); + + if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { + return -1; + } + return id; + } + } + + /** + * Build a query string that will match any row where the column matches + * anything in the values list. + */ + 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 "); + } + } + return selectWhere.toString(); + } + + static class SqlArguments { + public final String table; + public final String where; + public final String[] args; + + SqlArguments(Uri url, String where, String[] args) { + if (url.getPathSegments().size() == 1) { + this.table = url.getPathSegments().get(0); + this.where = where; + this.args = args; + } else if (url.getPathSegments().size() != 2) { + throw new IllegalArgumentException("Invalid URI: " + url); + } else if (!TextUtils.isEmpty(where)) { + throw new UnsupportedOperationException("WHERE clause not supported: " + url); + } else { + this.table = url.getPathSegments().get(0); + this.where = "_id=" + ContentUris.parseId(url); + this.args = null; + } + } + + SqlArguments(Uri url) { + if (url.getPathSegments().size() == 1) { + table = url.getPathSegments().get(0); + where = null; + args = null; + } else { + throw new IllegalArgumentException("Invalid URI: " + url); + } + } + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherSettings.java b/app/src/main/java/com/android/launcher2/LauncherSettings.java new file mode 100644 index 0000000..ee00371 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherSettings.java @@ -0,0 +1,236 @@ +/* + * 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.launcher2; + +import android.net.Uri; +import android.provider.BaseColumns; + +/** + * Settings related utilities. + */ +class LauncherSettings { + static interface BaseLauncherColumns extends BaseColumns { + /** + * Descriptive name of the gesture that can be displayed to the user. + *

    Type: TEXT

    + */ + static final String TITLE = "title"; + + /** + * The Intent URL of the gesture, describing what it points to. This + * value is given to {@link android.content.Intent#parseUri(String, int)} to create + * an Intent that can be launched. + *

    Type: TEXT

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

    Type: INTEGER

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

    Type: INTEGER

    + */ + 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; + + /** + * The icon is a bitmap. + */ + 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"; + + /** + * The icon resource id, if icon type is ICON_TYPE_RESOURCE. + *

    Type: TEXT

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

    Type: BLOB

    + */ + static final String ICON = "icon"; + } + + /** + * 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"); + + /** + * 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"); + + /** + * 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); + } + + /** + * The container holding the favorite + *

    Type: INTEGER

    + */ + 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; + + /** + * The screen holding the favorite (if container is CONTAINER_DESKTOP) + *

    Type: INTEGER

    + */ + 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"; + + /** + * The Y coordinate of the cell holding the favorite + * (if container is CONTAINER_DESKTOP) + *

    Type: INTEGER

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

    Type: INTEGER

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

    Type: INTEGER

    + */ + static final String SPANY = "spanY"; + + /** + * The favorite is a user created folder + */ + static final int ITEM_TYPE_FOLDER = 2; + + /** + * The favorite is a live folder + * + * Note: live folders can no longer be added to Launcher, and any live folders which + * 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. + */ + static final int ITEM_TYPE_LIVE_FOLDER = 3; + + /** + * The favorite is a widget + */ + static final int ITEM_TYPE_APPWIDGET = 4; + + /** + * The favorite is a clock + */ + static final int ITEM_TYPE_WIDGET_CLOCK = 1000; + + /** + * The favorite is a search widget + */ + static final int ITEM_TYPE_WIDGET_SEARCH = 1001; + + /** + * The favorite is a photo frame + */ + static final int ITEM_TYPE_WIDGET_PHOTO_FRAME = 1002; + + /** + * The appWidgetId of the widget + * + *

    Type: INTEGER

    + */ + static final String APPWIDGET_ID = "appWidgetId"; + + /** + * Indicates whether this favorite is an application-created shortcut or not. + * If the value is 0, the favorite is not an application-created shortcut, if the + * value is 1, it is an application-created shortcut. + *

    Type: INTEGER

    + */ + @Deprecated + static final String IS_SHORTCUT = "isShortcut"; + + /** + * The URI associated with the favorite. It is used, for instance, by + * live folders to find the content provider. + *

    Type: TEXT

    + */ + static final String URI = "uri"; + + /** + * The display mode if the item is a live folder. + *

    Type: INTEGER

    + * + * @see android.provider.LiveFolders#DISPLAY_MODE_GRID + * @see android.provider.LiveFolders#DISPLAY_MODE_LIST + */ + static final String DISPLAY_MODE = "displayMode"; + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherUtils.java b/app/src/main/java/com/android/launcher2/LauncherUtils.java new file mode 100644 index 0000000..a9cb1e3 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherUtils.java @@ -0,0 +1,20 @@ +package com.android.launcher2; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +public class LauncherUtils { + public static boolean isSystemApplication(Context context, String packageName){ + PackageManager mPackageManager = context.getPackageManager(); + try { + final PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName, PackageManager.GET_CONFIGURATIONS); + if((packageInfo.applicationInfo.flags & 1)!=0){ + return true; + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } +} diff --git a/app/src/main/java/com/android/launcher2/LauncherViewPropertyAnimator.java b/app/src/main/java/com/android/launcher2/LauncherViewPropertyAnimator.java new file mode 100644 index 0000000..258b2f4 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/LauncherViewPropertyAnimator.java @@ -0,0 +1,266 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.TimeInterpolator; +import android.view.View; +import android.view.ViewPropertyAnimator; + +import java.util.ArrayList; +import java.util.EnumSet; + +public class LauncherViewPropertyAnimator extends Animator implements AnimatorListener { + enum Properties { + TRANSLATION_X, + TRANSLATION_Y, + SCALE_X, + SCALE_Y, + ROTATION_Y, + ALPHA, + START_DELAY, + DURATION, + INTERPOLATOR + } + EnumSet mPropertiesToSet = EnumSet.noneOf(Properties.class); + ViewPropertyAnimator mViewPropertyAnimator; + View mTarget; + + float mTranslationX; + float mTranslationY; + float mScaleX; + float mScaleY; + float mRotationY; + float mAlpha; + long mStartDelay; + long mDuration; + TimeInterpolator mInterpolator; + ArrayList mListeners; + boolean mRunning = false; + FirstFrameAnimatorHelper mFirstFrameHelper; + + public LauncherViewPropertyAnimator(View target) { + mTarget = target; + mListeners = new ArrayList(); + } + + @Override + public void addListener(Animator.AnimatorListener listener) { + mListeners.add(listener); + } + + @Override + public void cancel() { + if (mViewPropertyAnimator != null) { + mViewPropertyAnimator.cancel(); + } + } + + @Override + public Animator clone() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void end() { + throw new RuntimeException("Not implemented"); + } + + @Override + public long getDuration() { + return mDuration; + } + + @Override + public ArrayList getListeners() { + return mListeners; + } + + @Override + public long getStartDelay() { + return mStartDelay; + } + + @Override + public void onAnimationCancel(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationCancel(this); + } + mRunning = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationEnd(this); + } + mRunning = false; + } + + @Override + public void onAnimationRepeat(Animator animation) { + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationRepeat(this); + } + } + + @Override + public void onAnimationStart(Animator animation) { + // This is the first time we get a handle to the internal ValueAnimator + // used by the ViewPropertyAnimator. + mFirstFrameHelper.onAnimationStart(animation); + + for (int i = 0; i < mListeners.size(); i++) { + Animator.AnimatorListener listener = mListeners.get(i); + listener.onAnimationStart(this); + } + mRunning = true; + } + + @Override + public boolean isRunning() { + return mRunning; + } + + @Override + public boolean isStarted() { + return mViewPropertyAnimator != null; + } + + @Override + public void removeAllListeners() { + mListeners.clear(); + } + + @Override + public void removeListener(Animator.AnimatorListener listener) { + mListeners.remove(listener); + } + + @Override + public Animator setDuration(long duration) { + mPropertiesToSet.add(Properties.DURATION); + mDuration = duration; + return this; + } + + @Override + public void setInterpolator(TimeInterpolator value) { + mPropertiesToSet.add(Properties.INTERPOLATOR); + mInterpolator = value; + } + + @Override + public void setStartDelay(long startDelay) { + mPropertiesToSet.add(Properties.START_DELAY); + mStartDelay = startDelay; + } + + @Override + public void setTarget(Object target) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setupEndValues() { + + } + + @Override + public void setupStartValues() { + } + + @Override + public void start() { + mViewPropertyAnimator = mTarget.animate(); + + // FirstFrameAnimatorHelper hooks itself up to the updates on the animator, + // and then adjusts the play time to keep the first two frames jank-free + mFirstFrameHelper = new FirstFrameAnimatorHelper(mViewPropertyAnimator, mTarget); + + if (mPropertiesToSet.contains(Properties.TRANSLATION_X)) { + mViewPropertyAnimator.translationX(mTranslationX); + } + if (mPropertiesToSet.contains(Properties.TRANSLATION_Y)) { + mViewPropertyAnimator.translationY(mTranslationY); + } + if (mPropertiesToSet.contains(Properties.SCALE_X)) { + mViewPropertyAnimator.scaleX(mScaleX); + } + if (mPropertiesToSet.contains(Properties.ROTATION_Y)) { + mViewPropertyAnimator.rotationY(mRotationY); + } + if (mPropertiesToSet.contains(Properties.SCALE_Y)) { + mViewPropertyAnimator.scaleY(mScaleY); + } + if (mPropertiesToSet.contains(Properties.ALPHA)) { + mViewPropertyAnimator.alpha(mAlpha); + } + if (mPropertiesToSet.contains(Properties.START_DELAY)) { + mViewPropertyAnimator.setStartDelay(mStartDelay); + } + if (mPropertiesToSet.contains(Properties.DURATION)) { + mViewPropertyAnimator.setDuration(mDuration); + } + if (mPropertiesToSet.contains(Properties.INTERPOLATOR)) { + mViewPropertyAnimator.setInterpolator(mInterpolator); + } + mViewPropertyAnimator.setListener(this); + mViewPropertyAnimator.start(); + LauncherAnimUtils.cancelOnDestroyActivity(this); + } + + public LauncherViewPropertyAnimator translationX(float value) { + mPropertiesToSet.add(Properties.TRANSLATION_X); + mTranslationX = value; + return this; + } + + public LauncherViewPropertyAnimator translationY(float value) { + mPropertiesToSet.add(Properties.TRANSLATION_Y); + mTranslationY = value; + return this; + } + + public LauncherViewPropertyAnimator scaleX(float value) { + mPropertiesToSet.add(Properties.SCALE_X); + mScaleX = value; + return this; + } + + public LauncherViewPropertyAnimator scaleY(float value) { + mPropertiesToSet.add(Properties.SCALE_Y); + mScaleY = value; + return this; + } + + public LauncherViewPropertyAnimator rotationY(float value) { + mPropertiesToSet.add(Properties.ROTATION_Y); + mRotationY = value; + return this; + } + + public LauncherViewPropertyAnimator alpha(float value) { + mPropertiesToSet.add(Properties.ALPHA); + mAlpha = value; + return this; + } +} diff --git a/app/src/main/java/com/android/launcher2/PackageChangedReceiver.java b/app/src/main/java/com/android/launcher2/PackageChangedReceiver.java new file mode 100644 index 0000000..e5e7e94 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PackageChangedReceiver.java @@ -0,0 +1,19 @@ +package com.android.launcher2; + +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; + } + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + WidgetPreviewLoader.removeFromDb(app.getWidgetPreviewCacheDb(), packageName); + } +} diff --git a/app/src/main/java/com/android/launcher2/PagedView.java b/app/src/main/java/com/android/launcher2/PagedView.java new file mode 100644 index 0000000..494534c --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedView.java @@ -0,0 +1,1981 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +import com.android.launcher.R; + +import java.util.ArrayList; + +/** + * An abstraction of the original Workspace which supports browsing through a + * sequential list of "pages" + */ +public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener { + private static final String TAG = "PagedView"; + private static final boolean DEBUG = false; + protected static final int INVALID_PAGE = -1; + + // the min drag distance for a fling to register, to prevent random page shifts + private static final int MIN_LENGTH_FOR_FLING = 25; + + protected static final int PAGE_SNAP_ANIMATION_DURATION = 550; + protected static final int MAX_PAGE_SNAP_DURATION = 750; + 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.14f; + + 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; + + // 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; + + static final int AUTOMATIC_PAGE_SPACING = -1; + + protected int mFlingThresholdVelocity; + protected int mMinFlingVelocity; + protected int mMinSnapVelocity; + + protected float mDensity; + protected float mSmoothingTime; + protected float mTouchX; + + protected boolean mFirstLayout = true; + + protected int mCurrentPage; + protected int mNextPage = INVALID_PAGE; + protected int mMaxScrollX; + protected Scroller mScroller; + private VelocityTracker mVelocityTracker; + + private float mDownMotionX; + protected float mLastMotionX; + protected float mLastMotionXRemainder; + protected float mLastMotionY; + protected float mTotalMotionX; + private int mLastScreenCenter = -1; + private int[] mChildOffsets; + private int[] mChildRelativeOffsets; + private int[] mChildOffsetsWithLayoutScale; + + protected final static int TOUCH_STATE_REST = 0; + protected final static int TOUCH_STATE_SCROLLING = 1; + protected final static int TOUCH_STATE_PREV_PAGE = 2; + protected final static int TOUCH_STATE_NEXT_PAGE = 3; + protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f; + + protected int mTouchState = TOUCH_STATE_REST; + protected boolean mForceScreenScrolled = false; + + protected OnLongClickListener mLongClickListener; + + protected boolean mAllowLongPress = true; + + protected int mTouchSlop; + private int mPagingTouchSlop; + private int mMaximumVelocity; + private int mMinimumWidth; + protected int mPageSpacing; + protected int mPageLayoutPaddingTop; + protected int mPageLayoutPaddingBottom; + protected int mPageLayoutPaddingLeft; + protected int mPageLayoutPaddingRight; + protected int mPageLayoutWidthGap; + protected int mPageLayoutHeightGap; + protected int mCellCountX = 0; + protected int mCellCountY = 0; + protected boolean mCenterPagesVertically; + protected boolean mAllowOverScroll = true; + protected int mUnboundedScrollX; + protected int[] mTempVisiblePagesRange = new int[2]; + protected boolean mForceDrawAllChildrenNextFrame; + + // 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; + + // parameter that adjusts the layout to be optimized for pages with that scale factor + protected float mLayoutScale = 1.0f; + + protected static final int INVALID_POINTER = -1; + + protected int mActivePointerId = INVALID_POINTER; + + 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 = true; + + // 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 mIsPageMoving = false; + + // All syncs and layout passes are deferred until data is ready. + protected boolean mIsDataReady = false; + + // Scrolling indicator + private ValueAnimator mScrollIndicatorAnimator; + private View mScrollIndicator; + private int mScrollIndicatorPaddingLeft; + private int mScrollIndicatorPaddingRight; + private boolean mHasScrollIndicator = true; + private boolean mShouldShowScrollIndicator = false; + private boolean mShouldShowScrollIndicatorImmediately = false; + protected static final int sScrollIndicatorFadeInDuration = 150; + protected static final int sScrollIndicatorFadeOutDuration = 650; + protected static final int sScrollIndicatorFlashDuration = 650; + private boolean mScrollingPaused = false; + + // If set, will defer loading associated pages until the scrolling settles + private boolean mDeferLoadAssociatedPagesUntilScrollCompletes; + + public interface PageSwitchListener { + void onPageSwitch(View newPage, int newPageIndex); + } + + public PagedView(Context context) { + this(context, null); + } + + public PagedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.PagedView, defStyle, 0); + setPageSpacing(a.getDimensionPixelSize(R.styleable.PagedView_pageSpacing, 0)); + mPageLayoutPaddingTop = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingTop, 0); + mPageLayoutPaddingBottom = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingBottom, 0); + mPageLayoutPaddingLeft = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingLeft, 0); + mPageLayoutPaddingRight = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutPaddingRight, 0); + mPageLayoutWidthGap = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutWidthGap, 0); + mPageLayoutHeightGap = a.getDimensionPixelSize( + R.styleable.PagedView_pageLayoutHeightGap, 0); + mScrollIndicatorPaddingLeft = + a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingLeft, 0); + mScrollIndicatorPaddingRight = + a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingRight, 0); + a.recycle(); + + setHapticFeedbackEnabled(false); + init(); + } + + /** + * Initializes various states for this workspace. + */ + protected void init() { + mDirtyPageContent = new ArrayList(); + mDirtyPageContent.ensureCapacity(32); + mScroller = new Scroller(getContext(), new ScrollInterpolator()); + mCurrentPage = 0; + mCenterPagesVertically = true; + + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mDensity = getResources().getDisplayMetrics().density; + + mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); + mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); + mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); + setOnHierarchyChangeListener(this); + } + + public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { + mPageSwitchListener = pageSwitchListener; + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); + } + } + + /** + * 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() { + return mCurrentPage; + } + int getNextPage() { + return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + } + + int getPageCount() { + return getChildCount(); + } + + View getPageAt(int index) { + return getChildAt(index); + } + + protected int indexToPage(int index) { + return index; + } + + /** + * Updates the scroll of the current page immediately to its final scroll position. We use this + * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of + * the previous tab page. + */ + protected void updateCurrentPageScroll() { + // If the current page is invalid, just reset the scroll position to zero + int newX = 0; + if (0 <= mCurrentPage && mCurrentPage < getPageCount()) { + int offset = getChildOffset(mCurrentPage); + int relOffset = getRelativeChildOffset(mCurrentPage); + newX = offset - relOffset; + } + scrollTo(newX, 0); + mScroller.setFinalX(newX); + mScroller.forceFinished(true); + } + + /** + * Called during AllApps/Home transitions to avoid unnecessary work. When that other animation + * ends, {@link #resumeScrolling()} should be called, along with + * {@link #updateCurrentPageScroll()} to correctly set the final state and re-enable scrolling. + */ + void pauseScrolling() { + mScroller.forceFinished(true); + cancelScrollingIndicatorAnimations(); + mScrollingPaused = true; + } + + /** + * Enables scrolling again. + * @see #pauseScrolling() + */ + void resumeScrolling() { + mScrollingPaused = false; + } + /** + * Sets the current page. + */ + void setCurrentPage(int currentPage) { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + // don't introduce any checks like mCurrentPage == currentPage here-- if we change the + // the default + if (getChildCount() == 0) { + return; + } + + + mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1)); + updateCurrentPageScroll(); + updateScrollingIndicator(); + notifyPageSwitchListener(); + invalidate(); + } + + protected void notifyPageSwitchListener() { + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); + } + } + + protected void pageBeginMoving() { + if (!mIsPageMoving) { + mIsPageMoving = true; + onPageBeginMoving(); + } + } + + protected void pageEndMoving() { + if (mIsPageMoving) { + mIsPageMoving = false; + onPageEndMoving(); + } + } + + protected boolean isPageMoving() { + return mIsPageMoving; + } + + // a method that subclasses can override to add behavior + protected void onPageBeginMoving() { + } + + // a method that subclasses can override to add behavior + protected void onPageEndMoving() { + } + + /** + * Registers the specified listener on each page contained in this workspace. + * + * @param l The listener used to respond to long clicks. + */ + @Override + public void setOnLongClickListener(OnLongClickListener l) { + mLongClickListener = l; + final int count = getPageCount(); + for (int i = 0; i < count; i++) { + getPageAt(i).setOnLongClickListener(l); + } + } + + @Override + public void scrollBy(int x, int y) { + scrollTo(mUnboundedScrollX + x, getScrollY() + y); + } + + @Override + public void scrollTo(int x, int y) { + final boolean isRtl = isLayoutRtl(); + mUnboundedScrollX = x; + + boolean isXBeforeFirstPage = isRtl ? (x > mMaxScrollX) : (x < 0); + boolean isXAfterLastPage = isRtl ? (x < 0) : (x > mMaxScrollX); + if (isXBeforeFirstPage) { + super.scrollTo(0, y); + if (mAllowOverScroll) { + if (isRtl) { + overScroll(x - mMaxScrollX); + } else { + overScroll(x); + } + } + } else if (isXAfterLastPage) { + super.scrollTo(mMaxScrollX, y); + if (mAllowOverScroll) { + if (isRtl) { + overScroll(x); + } else { + overScroll(x - mMaxScrollX); + } + } + } else { + mOverScrollX = x; + super.scrollTo(x, y); + } + + mTouchX = x; + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + } + + // we moved this functionality to a helper function so SmoothPagedView can reuse it + protected boolean computeScrollHelper() { + 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()) { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + } + invalidate(); + return true; + } else if (mNextPage != INVALID_PAGE) { + mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1)); + 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) { + pageEndMoving(); + } + + // Notify the user when the page changes + AccessibilityManager accessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isEnabled()) { + AccessibilityEvent ev = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); + ev.getText().add(getCurrentPageDescription()); + sendAccessibilityEventUnchecked(ev); + } + return true; + } + return false; + } + + @Override + public void computeScroll() { + computeScrollHelper(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (!mIsDataReady) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Workspace can only be used in EXACTLY mode."); + } + + // Return early if we aren't given a proper dimension + if (widthSize <= 0 || heightSize <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + /* Allow the height to be set as WRAP_CONTENT. This allows the particular case + * of the All apps view on XLarge displays to not take up more space then it needs. Width + * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect + * each page to have the same width. + */ + int maxChildHeight = 0; + + final int verticalPadding = getPaddingTop() + getPaddingBottom(); + final int horizontalPadding = getPaddingLeft() + getPaddingRight(); + + + // The children are given the same width and height as the workspace + // unless they were set to WRAP_CONTENT + if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + // disallowing padding in paged view (just pass 0) + final View child = getPageAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMode; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthMode = MeasureSpec.AT_MOST; + } else { + childWidthMode = MeasureSpec.EXACTLY; + } + + int childHeightMode; + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightMode = MeasureSpec.AT_MOST; + } else { + childHeightMode = MeasureSpec.EXACTLY; + } + + final int childWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(widthSize - horizontalPadding, childWidthMode); + final int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(heightSize - verticalPadding, childHeightMode); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight()); + if (DEBUG) Log.d(TAG, "\tmeasure-child" + i + ": " + child.getMeasuredWidth() + ", " + + child.getMeasuredHeight()); + } + + if (heightMode == MeasureSpec.AT_MOST) { + heightSize = maxChildHeight + verticalPadding; + } + + setMeasuredDimension(widthSize, heightSize); + + // We can't call getChildOffset/getRelativeChildOffset until we set the measured dimensions. + // We also wait until we set the measured dimensions before flushing the cache as well, to + // ensure that the cache is filled with good values. + invalidateCachedOffsets(); + + if (childCount > 0) { + if (DEBUG) Log.d(TAG, "getRelativeChildOffset(): " + getMeasuredWidth() + ", " + + getChildWidth(0)); + + // Calculate the variable page spacing if necessary + if (mPageSpacing == AUTOMATIC_PAGE_SPACING) { + // The gap between pages in the PagedView should be equal to the gap from the page + // to the edge of the screen (so it is not visible in the current screen). To + // account for unequal padding on each side of the paged view, we take the maximum + // of the left/right gap and use that as the gap between each page. + int offset = getRelativeChildOffset(0); + int spacing = Math.max(offset, widthSize - offset - + getChildAt(0).getMeasuredWidth()); + setPageSpacing(spacing); + } + } + + updateScrollingIndicatorPosition(); + + if (childCount > 0) { + final int index = isLayoutRtl() ? 0 : childCount - 1; + mMaxScrollX = getChildOffset(index) - getRelativeChildOffset(index); + } else { + mMaxScrollX = 0; + } + } + + protected void scrollToNewPageWithoutMovingPages(int newCurrentPage) { + int newX = getChildOffset(newCurrentPage) - getRelativeChildOffset(newCurrentPage); + int delta = newX - getScrollX(); + + final int pageCount = getChildCount(); + for (int i = 0; i < pageCount; i++) { + View page = (View) getPageAt(i); + page.setX(page.getX() + delta); + } + setCurrentPage(newCurrentPage); + } + + // A layout scale of 1.0f assumes that the pages, in their unshrunken state, have a + // scale of 1.0f. A layout scale of 0.8f assumes the pages have a scale of 0.8f, and + // tightens the layout accordingly + public void setLayoutScale(float childrenScale) { + mLayoutScale = childrenScale; + invalidateCachedOffsets(); + + // Now we need to do a re-layout, but preserving absolute X and Y coordinates + int childCount = getChildCount(); + float childrenX[] = new float[childCount]; + float childrenY[] = new float[childCount]; + for (int i = 0; i < childCount; i++) { + final View child = getPageAt(i); + childrenX[i] = child.getX(); + childrenY[i] = child.getY(); + } + // Trigger a full re-layout (never just call onLayout directly!) + int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY); + int heightSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY); + requestLayout(); + measure(widthSpec, heightSpec); + layout(getLeft(), getTop(), getRight(), getBottom()); + for (int i = 0; i < childCount; i++) { + final View child = getPageAt(i); + child.setX(childrenX[i]); + child.setY(childrenY[i]); + } + + // Also, the page offset has changed (since the pages are now smaller); + // update the page offset, but again preserving absolute X and Y coordinates + scrollToNewPageWithoutMovingPages(mCurrentPage); + } + + public void setPageSpacing(int pageSpacing) { + mPageSpacing = pageSpacing; + invalidateCachedOffsets(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!mIsDataReady) { + return; + } + + if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); + final int verticalPadding = getPaddingTop() + getPaddingBottom(); + final int childCount = getChildCount(); + final boolean isRtl = isLayoutRtl(); + + final int startIndex = isRtl ? childCount - 1 : 0; + final int endIndex = isRtl ? -1 : childCount; + final int delta = isRtl ? -1 : 1; + int childLeft = getRelativeChildOffset(startIndex); + for (int i = startIndex; i != endIndex; i += delta) { + final View child = getPageAt(i); + if (child.getVisibility() != View.GONE) { + final int childWidth = getScaledMeasuredWidth(child); + final int childHeight = child.getMeasuredHeight(); + int childTop = getPaddingTop(); + if (mCenterPagesVertically) { + childTop += ((getMeasuredHeight() - verticalPadding) - childHeight) / 2; + } + + if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop); + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), childTop + childHeight); + childLeft += childWidth + mPageSpacing; + } + } + + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + setHorizontalScrollBarEnabled(false); + updateCurrentPageScroll(); + setHorizontalScrollBarEnabled(true); + mFirstLayout = false; + } + } + + protected void screenScrolled(int screenCenter) { + if (isScrollingIndicatorEnabled()) { + updateScrollingIndicator(); + } + 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(); + } + } + + @Override + public void onChildViewAdded(View parent, View child) { + // This ensures that when children are added, they get the correct transforms / alphas + // in accordance with any scroll effects. + mForceScreenScrolled = true; + invalidate(); + invalidateCachedOffsets(); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + } + + protected void invalidateCachedOffsets() { + int count = getChildCount(); + if (count == 0) { + mChildOffsets = null; + mChildRelativeOffsets = null; + mChildOffsetsWithLayoutScale = null; + return; + } + + mChildOffsets = new int[count]; + mChildRelativeOffsets = new int[count]; + mChildOffsetsWithLayoutScale = new int[count]; + for (int i = 0; i < count; i++) { + mChildOffsets[i] = -1; + mChildRelativeOffsets[i] = -1; + mChildOffsetsWithLayoutScale[i] = -1; + } + } + + protected int getChildOffset(int index) { + final boolean isRtl = isLayoutRtl(); + int[] childOffsets = Float.compare(mLayoutScale, 1f) == 0 ? + mChildOffsets : mChildOffsetsWithLayoutScale; + + if (childOffsets != null && childOffsets[index] != -1) { + return childOffsets[index]; + } else { + if (getChildCount() == 0) + return 0; + + final int startIndex = isRtl ? getChildCount() - 1 : 0; + final int endIndex = isRtl ? index : index; + final int delta = isRtl ? -1 : 1; + int offset = getRelativeChildOffset(startIndex); + for (int i = startIndex; i != endIndex; i += delta) { + offset += getScaledMeasuredWidth(getPageAt(i)) + mPageSpacing; + } + if (childOffsets != null) { + childOffsets[index] = offset; + } + return offset; + } + } + + protected int getRelativeChildOffset(int index) { + if (mChildRelativeOffsets != null && mChildRelativeOffsets[index] != -1) { + return mChildRelativeOffsets[index]; + } else { + final int padding = getPaddingLeft() + getPaddingRight(); + final int offset = getPaddingLeft() + + (getMeasuredWidth() - padding - getChildWidth(index)) / 2; + if (mChildRelativeOffsets != null) { + mChildRelativeOffsets[index] = offset; + } + return offset; + } + } + + protected int getScaledMeasuredWidth(View child) { + // This functions are called enough times that it actually makes a difference in the + // profiler -- so just inline the max() here + final int measuredWidth = child.getMeasuredWidth(); + final int minWidth = mMinimumWidth; + final int maxWidth = (minWidth > measuredWidth) ? minWidth : measuredWidth; + return (int) (maxWidth * mLayoutScale + 0.5f); + } + + protected void getVisiblePages(int[] range) { + final boolean isRtl = isLayoutRtl(); + final int pageCount = getChildCount(); + + if (pageCount > 0) { + final int screenWidth = getMeasuredWidth(); + int leftScreen = isRtl ? pageCount - 1 : 0; + int rightScreen = 0; + int endIndex = isRtl ? 0 : pageCount - 1; + int delta = isRtl ? -1 : 1; + View currPage = getPageAt(leftScreen); + while (leftScreen != endIndex && + currPage.getX() + currPage.getWidth() - + currPage.getPaddingRight() < getScrollX()) { + leftScreen += delta; + currPage = getPageAt(leftScreen); + } + rightScreen = leftScreen; + currPage = getPageAt(rightScreen + delta); + while (rightScreen != endIndex && + currPage.getX() - currPage.getPaddingLeft() < getScrollX() + screenWidth) { + rightScreen += delta; + currPage = getPageAt(rightScreen + delta); + } + range[0] = Math.min(leftScreen, rightScreen); + range[1] = Math.max(leftScreen, rightScreen); + } else { + range[0] = -1; + range[1] = -1; + } + } + + protected boolean shouldDrawChild(View child) { + return child.getAlpha() > 0; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + int halfScreenSize = getMeasuredWidth() / 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; + + if (screenCenter != mLastScreenCenter || mForceScreenScrolled) { + // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can + // set it for the next frame + mForceScreenScrolled = false; + screenScrolled(screenCenter); + mLastScreenCenter = screenCenter; + } + + // Find out which screens are visible; as an optimization we only call draw on them + final int pageCount = getChildCount(); + if (pageCount > 0) { + getVisiblePages(mTempVisiblePagesRange); + final int leftScreen = mTempVisiblePagesRange[0]; + final int rightScreen = mTempVisiblePagesRange[1]; + if (leftScreen != -1 && rightScreen != -1) { + final long drawingTime = getDrawingTime(); + // Clip to the bounds + canvas.save(); + canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(), + getScrollY() + getBottom() - getTop()); + + for (int i = getChildCount() - 1; i >= 0; i--) { + final View v = getPageAt(i); + if (mForceDrawAllChildrenNextFrame || + (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) { + drawChild(canvas, v, drawingTime); + } + } + mForceDrawAllChildrenNextFrame = false; + canvas.restore(); + } + } + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { + int page = indexToPage(indexOfChild(child)); + if (page != mCurrentPage || !mScroller.isFinished()) { + snapToPage(page); + return true; + } + return false; + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + int focusablePage; + if (mNextPage != INVALID_PAGE) { + focusablePage = mNextPage; + } else { + focusablePage = mCurrentPage; + } + View v = getPageAt(focusablePage); + if (v != null) { + return v.requestFocus(direction, previouslyFocusedRect); + } + return false; + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + // XXX-RTL: This will be fixed in a future CL + if (direction == View.FOCUS_LEFT) { + if (getCurrentPage() > 0) { + snapToPage(getCurrentPage() - 1); + return true; + } + } else if (direction == View.FOCUS_RIGHT) { + if (getCurrentPage() < getPageCount() - 1) { + snapToPage(getCurrentPage() + 1); + return true; + } + } + return super.dispatchUnhandledMove(focused, direction); + } + + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + // XXX-RTL: This will be fixed in a future CL + if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { + getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode); + } + if (direction == View.FOCUS_LEFT) { + if (mCurrentPage > 0) { + getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode); + } + } else if (direction == View.FOCUS_RIGHT){ + if (mCurrentPage < getPageCount() - 1) { + getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode); + } + } + } + + /** + * If one of our descendant views decides that it could be focused now, only + * pass that along if it's on the current page. + * + * This happens when live folders requery, and if they're off page, they + * end up calling requestFocus, which pulls it on page. + */ + @Override + public void focusableViewAvailable(View focused) { + View current = getPageAt(mCurrentPage); + View v = focused; + while (true) { + if (v == current) { + super.focusableViewAvailable(focused); + return; + } + if (v == this) { + return; + } + ViewParent parent = v.getParent(); + if (parent instanceof View) { + v = (View)v.getParent(); + } else { + return; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + // We need to make sure to cancel our long press if + // a scrollable widget takes over touch events + final View currentPage = getPageAt(mCurrentPage); + currentPage.cancelLongPress(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + /** + * 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()) { + return (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing)); + } else { + return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing); + } + } + + /** + * 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()) { + return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing); + } else { + return (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing)); + } + + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onTouchEvent will be called and we do the actual + * scrolling there. + */ + acquireVelocityTrackerAndAddMovement(ev); + + // Skip touch handling if there are no pages to swipe + if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && + (mTouchState == TOUCH_STATE_SCROLLING)) { + return true; + } + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + if (mActivePointerId != INVALID_POINTER) { + determineScrollingStart(ev); + break; + } + // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN + // event. in that case, treat the first occurence of a move event as a ACTION_DOWN + // i.e. fall through to the next case (don't break) + // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events + // while it's small- this was causing a crash before we checked for INVALID_POINTER) + } + + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + // Remember location of down touch + mDownMotionX = x; + mLastMotionX = x; + mLastMotionY = y; + mLastMotionXRemainder = 0; + mTotalMotionX = 0; + mActivePointerId = ev.getPointerId(0); + mAllowLongPress = true; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); + final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop); + if (finishedScrolling) { + mTouchState = TOUCH_STATE_REST; + mScroller.abortAnimation(); + } else { + mTouchState = TOUCH_STATE_SCROLLING; + } + + // check if this can be the beginning of a tap on the side of the pages + // to scroll the current page + 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; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mTouchState = TOUCH_STATE_REST; + mAllowLongPress = false; + mActivePointerId = INVALID_POINTER; + releaseVelocityTracker(); + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + releaseVelocityTracker(); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mTouchState != TOUCH_STATE_REST; + } + + protected void determineScrollingStart(MotionEvent ev) { + determineScrollingStart(ev, 1.0f); + } + + /* + * 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, float touchSlopScale) { + /* + * Locally do absolute value. mLastMotionX is set to the y value + * of the down event. + */ + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + return; + } + 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 = 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 = getScrollX(); + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + pageBeginMoving(); + } + // Either way, cancel any pending longpress + cancelCurrentPageLongPress(); + } + } + + 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(); + } + } + } + + protected float getScrollProgress(int screenCenter, View v, int page) { + final int halfScreenSize = getMeasuredWidth() / 2; + + int totalDistance = getScaledMeasuredWidth(v) + mPageSpacing; + int delta = screenCenter - (getChildOffset(page) - + getRelativeChildOffset(page) + halfScreenSize); + + float scrollProgress = delta / (totalDistance * 1.0f); + scrollProgress = Math.min(scrollProgress, 1.0f); + scrollProgress = Math.max(scrollProgress, -1.0f); + return scrollProgress; + } + + // 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 void acceleratedOverScroll(float amount) { + int screenSize = getMeasuredWidth(); + + // 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; + + // Clamp this factor, f, to -1 < f < 1 + if (Math.abs(f) >= 1) { + f /= Math.abs(f); + } + + int overScrollAmount = (int) Math.round(f * screenSize); + if (amount < 0) { + mOverScrollX = overScrollAmount; + super.scrollTo(0, getScrollY()); + } else { + mOverScrollX = mMaxScrollX + overScrollAmount; + super.scrollTo(mMaxScrollX, getScrollY()); + } + invalidate(); + } + + protected void dampedOverScroll(float amount) { + int screenSize = getMeasuredWidth(); + + 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(0, getScrollY()); + } else { + mOverScrollX = mMaxScrollX + overScrollAmount; + super.scrollTo(mMaxScrollX, getScrollY()); + } + invalidate(); + } + + protected void overScroll(float amount) { + 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; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // Skip touch handling if there are no pages to swipe + if (getChildCount() <= 0) return super.onTouchEvent(ev); + + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mDownMotionX = mLastMotionX = ev.getX(); + mLastMotionXRemainder = 0; + mTotalMotionX = 0; + mActivePointerId = ev.getPointerId(0); + if (mTouchState == TOUCH_STATE_SCROLLING) { + pageBeginMoving(); + } + break; + + case MotionEvent.ACTION_MOVE: + if (mTouchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float deltaX = mLastMotionX + mLastMotionXRemainder - x; + + mTotalMotionX += Math.abs(deltaX); + + // Only scroll and update mLastMotionX if we have moved some discrete amount. We + // keep the remainder because we are actually testing if we've moved from the last + // scrolled position (which is discrete). + 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(); + } + mLastMotionX = x; + mLastMotionXRemainder = deltaX - (int) deltaX; + } else { + awakenScrollBars(); + } + } else { + determineScrollingStart(ev); + } + break; + + case MotionEvent.ACTION_UP: + if (mTouchState == TOUCH_STATE_SCROLLING) { + final int activePointerId = mActivePointerId; + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int velocityX = (int) velocityTracker.getXVelocity(activePointerId); + final int deltaX = (int) (x - mDownMotionX); + final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage)); + boolean isSignificantMove = Math.abs(deltaX) > pageWidth * + SIGNIFICANT_MOVE_THRESHOLD; + + mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); + + boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && + Math.abs(velocityX) > mFlingThresholdVelocity; + + // In the case that the page is moved far to one direction and then is flung + // in the opposite direction, we use a threshold to determine whether we should + // just return to the starting page, or if we should skip one further. + boolean returnToOriginalPage = false; + if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && + Math.signum(velocityX) != Math.signum(deltaX) && isFling) { + returnToOriginalPage = true; + } + + int finalPage; + // 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; + if (((isSignificantMove && !isDeltaXLeft && !isFling) || + (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { + finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; + snapToPageWithVelocity(finalPage, velocityX); + } else if (((isSignificantMove && isDeltaXLeft && !isFling) || + (isFling && isVelocityXLeft)) && + mCurrentPage < getChildCount() - 1) { + finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; + snapToPageWithVelocity(finalPage, velocityX); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + int nextPage = Math.max(0, mCurrentPage - 1); + if (nextPage != mCurrentPage) { + snapToPage(nextPage); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); + if (nextPage != mCurrentPage) { + snapToPage(nextPage); + } else { + snapToDestination(); + } + } else { + onUnhandledTap(ev); + } + mTouchState = TOUCH_STATE_REST; + mActivePointerId = INVALID_POINTER; + releaseVelocityTracker(); + break; + + case MotionEvent.ACTION_CANCEL: + if (mTouchState == TOUCH_STATE_SCROLLING) { + snapToDestination(); + } + mTouchState = TOUCH_STATE_REST; + mActivePointerId = INVALID_POINTER; + releaseVelocityTracker(); + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + return true; + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: { + // Handle mouse (or ext. device) by shifting the page depending on the scroll + final float vscroll; + final float hscroll; + if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { + vscroll = 0; + hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + } else { + vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); + hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + } + if (hscroll != 0 || vscroll != 0) { + boolean isForwardScroll = isLayoutRtl() ? (hscroll < 0 || vscroll < 0) + : (hscroll > 0 || vscroll > 0); + if (isForwardScroll) { + scrollRight(); + } else { + scrollLeft(); + } + return true; + } + } + } + } + return super.onGenericMotionEvent(event); + } + + private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); + mLastMotionY = ev.getY(newPointerIndex); + mLastMotionXRemainder = 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + protected void onUnhandledTap(MotionEvent ev) {} + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + int page = indexToPage(indexOfChild(child)); + if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { + snapToPage(page); + } + } + + protected int getChildIndexForRelativeOffset(int relativeOffset) { + final boolean isRtl = isLayoutRtl(); + final int childCount = getChildCount(); + int left; + int right; + final int startIndex = isRtl ? childCount - 1 : 0; + final int endIndex = isRtl ? -1 : childCount; + final int delta = isRtl ? -1 : 1; + for (int i = startIndex; i != endIndex; i += delta) { + left = getRelativeChildOffset(i); + right = (left + getScaledMeasuredWidth(getPageAt(i))); + if (left <= relativeOffset && relativeOffset <= right) { + return i; + } + } + return -1; + } + + protected int getChildWidth(int index) { + // This functions are called enough times that it actually makes a difference in the + // profiler -- so just inline the max() here + final int measuredWidth = getPageAt(index).getMeasuredWidth(); + final int minWidth = mMinimumWidth; + return (minWidth > measuredWidth) ? minWidth : measuredWidth; + } + + int getPageNearestToCenterOfScreen() { + int minDistanceFromScreenCenter = Integer.MAX_VALUE; + int minDistanceFromScreenCenterIndex = -1; + int screenCenter = getScrollX() + (getMeasuredWidth() / 2); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; ++i) { + View layout = (View) getPageAt(i); + int childWidth = getScaledMeasuredWidth(layout); + int halfChildWidth = (childWidth / 2); + int childCenter = getChildOffset(i) + halfChildWidth; + int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); + if (distanceFromScreenCenter < minDistanceFromScreenCenter) { + minDistanceFromScreenCenter = distanceFromScreenCenter; + minDistanceFromScreenCenterIndex = i; + } + } + return minDistanceFromScreenCenterIndex; + } + + protected void snapToDestination() { + snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); + } + + private static class ScrollInterpolator implements Interpolator { + public ScrollInterpolator() { + } + + public float getInterpolation(float t) { + t -= 1.0f; + return t*t*t*t*t + 1; + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // 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) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + protected void snapToPageWithVelocity(int whichPage, int velocity) { + whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); + int halfScreenSize = getMeasuredWidth() / 2; + + if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); + if (DEBUG) Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): " + + getMeasuredWidth() + ", " + getChildWidth(whichPage)); + final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + int delta = newX - mUnboundedScrollX; + int duration = 0; + + 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, PAGE_SNAP_ANIMATION_DURATION); + return; + } + + // Here we compute a "distance" that will be used in the computation of the overall + // snap duration. This is a function of the actual distance that needs to be traveled; + // we keep this value close to half screen size in order to reduce the variance in snap + // duration as a function of the distance the page needs to travel. + float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); + float distance = halfScreenSize + halfScreenSize * + distanceInfluenceForSnapDuration(distanceRatio); + + velocity = Math.abs(velocity); + velocity = Math.max(mMinSnapVelocity, velocity); + + // we want the page's snap velocity to approximately match the velocity at which the + // user flings, so we scale the duration by a value near to the derivative of the scroll + // interpolator at zero, ie. 5. We use 4 to make it a little slower. + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + duration = Math.min(duration, MAX_PAGE_SNAP_DURATION); + + snapToPage(whichPage, delta, duration); + } + + protected void snapToPage(int whichPage) { + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + } + + protected void snapToPage(int whichPage, int duration) { + whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1)); + + if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); + if (DEBUG) Log.d(TAG, "snapToPage.getRelativeChildOffset(): " + getMeasuredWidth() + ", " + + getChildWidth(whichPage)); + int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + final int sX = mUnboundedScrollX; + final int delta = newX - sX; + snapToPage(whichPage, delta, duration); + } + + protected void snapToPage(int whichPage, int delta, int duration) { + mNextPage = whichPage; + + View focusedChild = getFocusedChild(); + if (focusedChild != null && whichPage != mCurrentPage && + focusedChild == getPageAt(mCurrentPage)) { + focusedChild.clearFocus(); + } + + pageBeginMoving(); + awakenScrollBars(duration); + if (duration == 0) { + duration = Math.abs(delta); + } + + if (!mScroller.isFinished()) mScroller.abortAnimation(); + mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); + + // Load associated pages immediately if someone else is handling the scroll, otherwise defer + // loading associated pages until the scroll settles + if (mDeferScrollUpdate) { + loadAssociatedPages(mNextPage); + } else { + mDeferLoadAssociatedPagesUntilScrollCompletes = true; + } + notifyPageSwitchListener(); + invalidate(); + } + + public void scrollLeft() { + if (mScroller.isFinished()) { + if (mCurrentPage > 0) snapToPage(mCurrentPage - 1); + } else { + if (mNextPage > 0) snapToPage(mNextPage - 1); + } + } + + public void scrollRight() { + if (mScroller.isFinished()) { + if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1); + } else { + if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1); + } + } + + public int getPageForView(View v) { + int result = -1; + if (v != null) { + ViewParent vp = v.getParent(); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + if (vp == getPageAt(i)) { + return i; + } + } + } + return result; + } + + /** + * @return True is long presses are still allowed for the current touch + */ + public boolean allowLongPress() { + return mAllowLongPress; + } + + /** + * 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; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(currentPage); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + 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 + mScroller.forceFinished(true); + mNextPage = INVALID_PAGE; + + // 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(); + } + } + + protected View getScrollingIndicator() { + // We use mHasScrollIndicator to prevent future lookups if there is no sibling indicator + // found + if (mHasScrollIndicator && mScrollIndicator == null) { + ViewGroup parent = (ViewGroup) getParent(); + if (parent != null) { + mScrollIndicator = (View) (parent.findViewById(R.id.paged_view_indicator)); + mHasScrollIndicator = mScrollIndicator != null; + if (mHasScrollIndicator) { + mScrollIndicator.setVisibility(View.VISIBLE); + } + } + } + return mScrollIndicator; + } + + protected boolean isScrollingIndicatorEnabled() { + return true; + } + + Runnable hideScrollingIndicatorRunnable = new Runnable() { + @Override + public void run() { + hideScrollingIndicator(false); + } + }; + protected void flashScrollingIndicator(boolean animated) { + removeCallbacks(hideScrollingIndicatorRunnable); + showScrollingIndicator(!animated); + postDelayed(hideScrollingIndicatorRunnable, sScrollIndicatorFlashDuration); + } + + protected void showScrollingIndicator(boolean immediately) { + mShouldShowScrollIndicator = true; + mShouldShowScrollIndicatorImmediately = true; + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + mShouldShowScrollIndicator = false; + getScrollingIndicator(); + if (mScrollIndicator != null) { + // Fade the indicator in + updateScrollingIndicatorPosition(); + mScrollIndicator.setVisibility(View.VISIBLE); + cancelScrollingIndicatorAnimations(); + if (immediately || mScrollingPaused) { + mScrollIndicator.setAlpha(1f); + } else { + mScrollIndicatorAnimator = LauncherAnimUtils.ofFloat(mScrollIndicator, "alpha", 1f); + mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeInDuration); + mScrollIndicatorAnimator.start(); + } + } + } + + protected void cancelScrollingIndicatorAnimations() { + if (mScrollIndicatorAnimator != null) { + mScrollIndicatorAnimator.cancel(); + } + } + + protected void hideScrollingIndicator(boolean immediately) { + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + getScrollingIndicator(); + if (mScrollIndicator != null) { + // Fade the indicator out + updateScrollingIndicatorPosition(); + cancelScrollingIndicatorAnimations(); + if (immediately || mScrollingPaused) { + mScrollIndicator.setVisibility(View.INVISIBLE); + mScrollIndicator.setAlpha(0f); + } else { + mScrollIndicatorAnimator = LauncherAnimUtils.ofFloat(mScrollIndicator, "alpha", 0f); + mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeOutDuration); + mScrollIndicatorAnimator.addListener(new AnimatorListenerAdapter() { + private boolean cancelled = false; + @Override + public void onAnimationCancel(android.animation.Animator animation) { + cancelled = true; + } + @Override + public void onAnimationEnd(Animator animation) { + if (!cancelled) { + mScrollIndicator.setVisibility(View.INVISIBLE); + } + } + }); + mScrollIndicatorAnimator.start(); + } + } + } + + /** + * To be overridden by subclasses to determine whether the scroll indicator should stretch to + * fill its space on the track or not. + */ + protected boolean hasElasticScrollIndicator() { + return true; + } + + private void updateScrollingIndicator() { + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + getScrollingIndicator(); + if (mScrollIndicator != null) { + updateScrollingIndicatorPosition(); + } + if (mShouldShowScrollIndicator) { + showScrollingIndicator(mShouldShowScrollIndicatorImmediately); + } + } + + private void updateScrollingIndicatorPosition() { + final boolean isRtl = isLayoutRtl(); + if (!isScrollingIndicatorEnabled()) return; + if (mScrollIndicator == null) return; + int numPages = getChildCount(); + int pageWidth = getMeasuredWidth(); + int trackWidth = pageWidth - mScrollIndicatorPaddingLeft - mScrollIndicatorPaddingRight; + int indicatorWidth = mScrollIndicator.getMeasuredWidth() - + mScrollIndicator.getPaddingLeft() - mScrollIndicator.getPaddingRight(); + + float scrollPos = isRtl ? mMaxScrollX - getScrollX() : getScrollX(); + float offset = Math.max(0f, Math.min(1f, (float) scrollPos / mMaxScrollX)); + if (isRtl) { + offset = 1f - offset; + } + int indicatorSpace = trackWidth / numPages; + int indicatorPos = (int) (offset * (trackWidth - indicatorSpace)) + mScrollIndicatorPaddingLeft; + if (hasElasticScrollIndicator()) { + if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) { + mScrollIndicator.getLayoutParams().width = indicatorSpace; + mScrollIndicator.requestLayout(); + } + } else { + int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2; + indicatorPos += indicatorCenterOffset; + } + mScrollIndicator.setTranslationX(indicatorPos); + } + + public void showScrollIndicatorTrack() { + } + + public void hideScrollIndicatorTrack() { + } + + /* Accessibility */ + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setScrollable(getPageCount() > 1); + if (getCurrentPage() < getPageCount() - 1) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + } + if (getCurrentPage() > 0) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setScrollable(true); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + event.setFromIndex(mCurrentPage); + event.setToIndex(mCurrentPage); + event.setItemCount(getChildCount()); + } + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + if (getCurrentPage() < getPageCount() - 1) { + scrollRight(); + return true; + } + } break; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { + if (getCurrentPage() > 0) { + scrollLeft(); + return true; + } + } break; + } + return false; + } + + protected String getCurrentPageDescription() { + return String.format(getContext().getString(R.string.default_scroll_format), + getNextPage() + 1, getChildCount()); + } + + @Override + public boolean onHoverEvent(android.view.MotionEvent event) { + return true; + } +} diff --git a/app/src/main/java/com/android/launcher2/PagedViewCellLayout.java b/app/src/main/java/com/android/launcher2/PagedViewCellLayout.java new file mode 100644 index 0000000..9ce177b --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedViewCellLayout.java @@ -0,0 +1,505 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; + +import com.android.launcher.R; + +/** + * 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; + private int mMaxGap; + 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 + Resources resources = context.getResources(); + mOriginalCellWidth = mCellWidth = + resources.getDimensionPixelSize(R.dimen.apps_customize_cell_width); + mOriginalCellHeight = mCellHeight = + resources.getDimensionPixelSize(R.dimen.apps_customize_cell_height); + mCellCountX = LauncherModel.getCellCountX(); + mCellCountY = LauncherModel.getCellCountY(); + mOriginalWidthGap = mOriginalHeightGap = mWidthGap = mHeightGap = -1; + mMaxGap = resources.getDimensionPixelSize(R.dimen.apps_customize_max_gap); + + 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 = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0); + mHeightGap = Math.min(mMaxGap,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(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 (LauncherApplication.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 + ")"; + } + } +} + +interface Page { + public int getPageChildCount(); + public View getChildOnPageAt(int i); + public void removeAllViewsOnPage(); + public void removeViewOnPageAt(int i); + public int indexOfChildOnPage(View v); +} diff --git a/app/src/main/java/com/android/launcher2/PagedViewCellLayoutChildren.java b/app/src/main/java/com/android/launcher2/PagedViewCellLayoutChildren.java new file mode 100644 index 0000000..187a22d --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedViewCellLayoutChildren.java @@ -0,0 +1,160 @@ +/* + * 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.launcher2; + +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(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/app/src/main/java/com/android/launcher2/PagedViewGridLayout.java b/app/src/main/java/com/android/launcher2/PagedViewGridLayout.java new file mode 100644 index 0000000..aa9adc0 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedViewGridLayout.java @@ -0,0 +1,133 @@ +/* + * 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.launcher2; + +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); + } + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // PagedView currently has issues with different-sized pages since it calculates the + // offset of each page to scroll to before it updates the actual size of each page + // (which can change depending on the content if the contents aren't a fixed size). + // We work around this by having a minimum size on each widget page). + int widthSpecSize = Math.min(getSuggestedMinimumWidth(), + MeasureSpec.getSize(widthMeasureSpec)); + int widthSpecMode = MeasureSpec.EXACTLY; + super.onMeasure(MeasureSpec.makeMeasureSpec(widthSpecSize, widthSpecMode), + heightMeasureSpec); + } + + @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/app/src/main/java/com/android/launcher2/PagedViewIcon.java b/app/src/main/java/com/android/launcher2/PagedViewIcon.java new file mode 100644 index 0000000..d2aa31f --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedViewIcon.java @@ -0,0 +1,92 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.AttributeSet; +import android.widget.TextView; + +/** + * An icon on a PagedView, specifically for items in the launcher's paged view (with compound + * drawables on the top). + */ +public class PagedViewIcon extends TextView { + /** A simple callback interface to allow a PagedViewIcon to notify when it has been pressed */ + public static interface PressedCallback { + void iconPressed(PagedViewIcon icon); + } + + @SuppressWarnings("unused") + private static final String TAG = "PagedViewIcon"; + private static final float PRESS_ALPHA = 0.4f; + + private PagedViewIcon.PressedCallback mPressedCallback; + private boolean mLockDrawableState = false; + + private Bitmap mIcon; + + public PagedViewIcon(Context context) { + this(context, null); + } + + public PagedViewIcon(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedViewIcon(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void applyFromApplicationInfo(ApplicationInfo info, boolean scaleUp, + PagedViewIcon.PressedCallback cb) { + mIcon = info.iconBitmap; + mPressedCallback = cb; + setCompoundDrawablesWithIntrinsicBounds(null, new FastBitmapDrawable(mIcon), null, null); + setText(info.title); + setTag(info); + } + + public void lockDrawableState() { + mLockDrawableState = true; + } + + public void resetDrawableState() { + mLockDrawableState = false; + post(new Runnable() { + @Override + public void run() { + refreshDrawableState(); + } + }); + } + + protected void drawableStateChanged() { + super.drawableStateChanged(); + + // We keep in the pressed state until resetDrawableState() is called to reset the press + // feedback + if (isPressed()) { + setAlpha(PRESS_ALPHA); + if (mPressedCallback != null) { + mPressedCallback.iconPressed(this); + } + } else if (!mLockDrawableState) { + setAlpha(1f); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/PagedViewIconCache.java b/app/src/main/java/com/android/launcher2/PagedViewIconCache.java new file mode 100644 index 0000000..d65f68b --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedViewIconCache.java @@ -0,0 +1,133 @@ +/* + * 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.launcher2; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.pm.ComponentInfo; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; + +/** + * Simple cache mechanism for PagedView outlines. + */ +public class PagedViewIconCache { + public static class Key { + public enum Type { + ApplicationInfoKey, + AppWidgetProviderInfoKey, + ResolveInfoKey + } + private final ComponentName mComponentName; + private final Type mType; + + public Key(ApplicationInfo info) { + mComponentName = info.componentName; + mType = Type.ApplicationInfoKey; + } + public Key(ResolveInfo info) { + final ComponentInfo ci = info.activityInfo != null ? info.activityInfo : + info.serviceInfo; + mComponentName = new ComponentName(ci.packageName, ci.name); + mType = Type.ResolveInfoKey; + } + public Key(AppWidgetProviderInfo info) { + mComponentName = info.provider; + mType = Type.AppWidgetProviderInfoKey; + } + + private ComponentName getComponentName() { + return mComponentName; + } + public boolean isKeyType(Type t) { + return (mType == t); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Key) { + Key k = (Key) o; + return mComponentName.equals(k.mComponentName); + } + return super.equals(o); + } + @Override + public int hashCode() { + return getComponentName().hashCode(); + } + } + + private final HashMap mIconOutlineCache = new HashMap(); + + public void clear() { + for (Key key : mIconOutlineCache.keySet()) { + mIconOutlineCache.get(key).recycle(); + } + mIconOutlineCache.clear(); + } + private void retainAll(HashSet keysToKeep, Key.Type t) { + HashSet keysToRemove = new HashSet(mIconOutlineCache.keySet()); + keysToRemove.removeAll(keysToKeep); + for (Key key : keysToRemove) { + if (key.isKeyType(t)) { + mIconOutlineCache.get(key).recycle(); + mIconOutlineCache.remove(key); + } + } + } + /** Removes all the keys to applications that aren't in the passed in collection */ + public void retainAllApps(ArrayList keys) { + HashSet keysSet = new HashSet(); + for (ApplicationInfo info : keys) { + keysSet.add(new Key(info)); + } + retainAll(keysSet, Key.Type.ApplicationInfoKey); + } + /** Removes all the keys to shortcuts that aren't in the passed in collection */ + public void retainAllShortcuts(List keys) { + HashSet keysSet = new HashSet(); + for (ResolveInfo info : keys) { + keysSet.add(new Key(info)); + } + retainAll(keysSet, Key.Type.ResolveInfoKey); + } + /** Removes all the keys to widgets that aren't in the passed in collection */ + public void retainAllAppWidgets(List keys) { + HashSet keysSet = new HashSet(); + for (AppWidgetProviderInfo info : keys) { + keysSet.add(new Key(info)); + } + retainAll(keysSet, Key.Type.AppWidgetProviderInfoKey); + } + public void addOutline(Key key, Bitmap b) { + mIconOutlineCache.put(key, b); + } + public void removeOutline(Key key) { + if (mIconOutlineCache.containsKey(key)) { + mIconOutlineCache.get(key).recycle(); + mIconOutlineCache.remove(key); + } + } + public Bitmap getOutline(Key key) { + return mIconOutlineCache.get(key); + } +} diff --git a/app/src/main/java/com/android/launcher2/PagedViewWidget.java b/app/src/main/java/com/android/launcher2/PagedViewWidget.java new file mode 100644 index 0000000..86ab128 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedViewWidget.java @@ -0,0 +1,246 @@ +/* + * 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.launcher2; + +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.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher.R; + +/** + * 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(); + } + + 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) { + 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(info.label); + final TextView dims = (TextView) findViewById(R.id.widget_dims); + if (dims != null) { + int hSpan = Math.min(cellSpan[0], LauncherModel.getCellCountX()); + int vSpan = Math.min(cellSpan[1], LauncherModel.getCellCountY()); + 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/app/src/main/java/com/android/launcher2/PagedViewWidgetImageView.java b/app/src/main/java/com/android/launcher2/PagedViewWidgetImageView.java new file mode 100644 index 0000000..9928177 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedViewWidgetImageView.java @@ -0,0 +1,49 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.ImageView; + +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/app/src/main/java/com/android/launcher2/PagedViewWithDraggableItems.java b/app/src/main/java/com/android/launcher2/PagedViewWithDraggableItems.java new file mode 100644 index 0000000..9cdd74f --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PagedViewWithDraggableItems.java @@ -0,0 +1,178 @@ +/* + * 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.launcher2; + +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); + } + + @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(); + } + + /** Show the scrolling indicators when we move the page */ + protected void onPageBeginMoving() { + showScrollingIndicator(false); + } + protected void onPageEndMoving() { + hideScrollingIndicator(false); + } +} diff --git a/app/src/main/java/com/android/launcher2/PendingAddItemInfo.java b/app/src/main/java/com/android/launcher2/PendingAddItemInfo.java new file mode 100644 index 0000000..a1e7b06 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PendingAddItemInfo.java @@ -0,0 +1,107 @@ +/* + * 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.launcher2; + +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 + */ +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(); + } +} diff --git a/app/src/main/java/com/android/launcher2/PreloadReceiver.java b/app/src/main/java/com/android/launcher2/PreloadReceiver.java new file mode 100644 index 0000000..08350b6 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/PreloadReceiver.java @@ -0,0 +1,51 @@ +/* + * 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.launcher2; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; + +public class PreloadReceiver extends BroadcastReceiver { + private static final String TAG = "Launcher.PreloadReceiver"; + private static final boolean LOGD = false; + + public static final String EXTRA_WORKSPACE_NAME = + "com.android.launcher.action.EXTRA_WORKSPACE_NAME"; + + @Override + public void onReceive(Context context, Intent intent) { + final LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + final LauncherProvider provider = app.getLauncherProvider(); + if (provider != null) { + String name = intent.getStringExtra(EXTRA_WORKSPACE_NAME); + final int workspaceResId = !TextUtils.isEmpty(name) + ? context.getResources().getIdentifier(name, "xml", "com.android.launcher") : 0; + if (LOGD) { + Log.d(TAG, "workspace name: " + name + " id: " + workspaceResId); + } + new Thread(new Runnable() { + @Override + public void run() { + provider.loadDefaultFavoritesIfNecessary(workspaceResId); + } + }).start(); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/SearchDropTargetBar.java b/app/src/main/java/com/android/launcher2/SearchDropTargetBar.java new file mode 100644 index 0000000..a5b76c9 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/SearchDropTargetBar.java @@ -0,0 +1,239 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.widget.FrameLayout; + +import com.android.launcher.R; + +/* + * Ths bar will manage the transition between the QSB search bar and the delete drop + * targets so that each of the individual IconDropTargets don't have to. + */ +public class SearchDropTargetBar extends FrameLayout implements DragController.DragListener { + + private static final int sTransitionInDuration = 200; + private static final int sTransitionOutDuration = 175; + + private ObjectAnimator mDropTargetBarAnim; + private ObjectAnimator mQSBSearchBarAnim; + 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 Drawable mPreviousBackground; + private boolean mEnableDropDownDropTargets; + + public SearchDropTargetBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SearchDropTargetBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setup(Launcher launcher, DragController dragController) { + dragController.addDragListener(this); + dragController.addDragListener(mInfoDropTarget); + dragController.addDragListener(mDeleteDropTarget); + dragController.addDropTarget(mInfoDropTarget); + dragController.addDropTarget(mDeleteDropTarget); + dragController.setFlingToDeleteDropTarget(mDeleteDropTarget); + mInfoDropTarget.setLauncher(launcher); + mDeleteDropTarget.setLauncher(launcher); + } + + private void prepareStartAnimation(View v) { + // Enable the hw layers before the animation starts (will be disabled in the onAnimationEnd + // callback below) + v.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + private void setupAnimation(ObjectAnimator anim, final View v) { + anim.setInterpolator(sAccelerateInterpolator); + anim.setDuration(sTransitionInDuration); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + v.setLayerType(View.LAYER_TYPE_NONE, null); + } + }); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // Get the individual components + mQSBSearchBar = findViewById(R.id.qsb_search_bar); + 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); + mBarHeight = getResources().getDimensionPixelSize(R.dimen.qsb_bar_height); + + mInfoDropTarget.setSearchDropTargetBar(this); + mDeleteDropTarget.setSearchDropTargetBar(this); + + mEnableDropDownDropTargets = + getResources().getBoolean(R.bool.config_useDropTargetDownTransition); + + // Create the various fade animations + if (mEnableDropDownDropTargets) { + mDropTargetBar.setTranslationY(-mBarHeight); + mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "translationY", + -mBarHeight, 0f); + mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "translationY", 0, + -mBarHeight); + } else { + mDropTargetBar.setAlpha(0f); + mDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "alpha", 0f, 1f); + mQSBSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f); + } + setupAnimation(mDropTargetBarAnim, mDropTargetBar); + setupAnimation(mQSBSearchBarAnim, mQSBSearchBar); + } + + public void finishAnimations() { + prepareStartAnimation(mDropTargetBar); + mDropTargetBarAnim.reverse(); + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.reverse(); + } + + /* + * Shows and hides the search bar. + */ + public void showSearchBar(boolean animated) { + if (!mIsSearchBarHidden) return; + if (animated) { + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.reverse(); + } else { + mQSBSearchBarAnim.cancel(); + if (mEnableDropDownDropTargets) { + mQSBSearchBar.setTranslationY(0); + } else { + mQSBSearchBar.setAlpha(1f); + } + } + mIsSearchBarHidden = false; + } + public void hideSearchBar(boolean animated) { + if (mIsSearchBarHidden) return; + if (animated) { + prepareStartAnimation(mQSBSearchBar); + mQSBSearchBarAnim.start(); + } else { + mQSBSearchBarAnim.cancel(); + if (mEnableDropDownDropTargets) { + mQSBSearchBar.setTranslationY(-mBarHeight); + } else { + mQSBSearchBar.setAlpha(0f); + } + } + mIsSearchBarHidden = true; + } + + /* + * Gets various transition durations. + */ + public int getTransitionInDuration() { + return sTransitionInDuration; + } + public int getTransitionOutDuration() { + return sTransitionOutDuration; + } + + /* + * DragController.DragListener implementation + */ + @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(); + } + } + + public void deferOnDragEnd() { + mDeferOnDragEnd = true; + } + + @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(); + } + } else { + mDeferOnDragEnd = false; + } + } + + public void onSearchPackagesChanged(boolean searchVisible, boolean voiceVisible) { + if (mQSBSearchBar != null) { + Drawable bg = mQSBSearchBar.getBackground(); + if (bg != null && (!searchVisible && !voiceVisible)) { + // Save the background and disable it + mPreviousBackground = bg; + mQSBSearchBar.setBackgroundResource(0); + } else if (mPreviousBackground != null && (searchVisible || voiceVisible)) { + // Restore the background + mQSBSearchBar.setBackground(mPreviousBackground); + } + } + } + + public Rect getSearchBarBounds() { + if (mQSBSearchBar != null) { + final int[] pos = new int[2]; + mQSBSearchBar.getLocationOnScreen(pos); + + final Rect rect = new Rect(); + rect.left = pos[0]; + rect.top = pos[1]; + rect.right = pos[0] + mQSBSearchBar.getWidth(); + rect.bottom = pos[1] + mQSBSearchBar.getHeight(); + return rect; + } else { + return null; + } + } +} diff --git a/app/src/main/java/com/android/launcher2/ShortcutAndWidgetContainer.java b/app/src/main/java/com/android/launcher2/ShortcutAndWidgetContainer.java new file mode 100644 index 0000000..36f135a --- /dev/null +++ b/app/src/main/java/com/android/launcher2/ShortcutAndWidgetContainer.java @@ -0,0 +1,204 @@ +/* + * 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.launcher2; + +import android.app.WallpaperManager; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +public class ShortcutAndWidgetContainer extends ViewGroup { + static final String TAG = "CellLayoutChildren"; + + // 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[] mTmpCellXY = new int[2]; + + private final WallpaperManager mWallpaperManager; + + private int mCellWidth; + private int mCellHeight; + + private int mWidthGap; + private int mHeightGap; + + private int mCountX; + + private boolean mInvertIfRtl = false; + + public ShortcutAndWidgetContainer(Context context) { + super(context); + mWallpaperManager = WallpaperManager.getInstance(context); + } + + public void setCellDimensions(int cellWidth, int cellHeight, int widthGap, int heightGap, + int countX) { + mCellWidth = cellWidth; + mCellHeight = cellHeight; + mWidthGap = widthGap; + mHeightGap = heightGap; + mCountX = countX; + } + + public View getChildAt(int x, int y) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + if ((lp.cellX <= x) && (x < lp.cellX + lp.cellHSpan) && + (lp.cellY <= y) && (y < lp.cellY + lp.cellVSpan)) { + return child; + } + } + return null; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + // Debug drawing for hit space + Paint p = new Paint(); + p.setColor(0x6600FF00); + for (int i = getChildCount() - 1; i >= 0; i--) { + final View child = getChildAt(i); + final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + canvas.drawRect(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height, p); + } + } + super.dispatchDraw(canvas); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + measureChild(child); + } + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(widthSpecSize, heightSpecSize); + } + + public void setupLp(CellLayout.LayoutParams lp) { + lp.setup(mCellWidth, mCellHeight, mWidthGap, mHeightGap, invertLayoutHorizontally(), + mCountX); + } + + // Set whether or not to invert the layout horizontally if the layout is in RTL mode. + public void setInvertIfRtl(boolean invert) { + mInvertIfRtl = invert; + } + + public void measureChild(View child) { + final int cellWidth = mCellWidth; + final int cellHeight = mCellHeight; + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + lp.setup(cellWidth, cellHeight, mWidthGap, mHeightGap, invertLayoutHorizontally(), mCountX); + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); + int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + child.measure(childWidthMeasureSpec, childheightMeasureSpec); + } + + private boolean invertLayoutHorizontally() { + return mInvertIfRtl && isLayoutRtl(); + } + + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); + + int childLeft = lp.x; + int childTop = lp.y; + child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height); + + if (lp.dropped) { + lp.dropped = false; + + final int[] cellXY = mTmpCellXY; + getLocationOnScreen(cellXY); + mWallpaperManager.sendWallpaperCommand(getWindowToken(), + WallpaperManager.COMMAND_DROP, + cellXY[0] + childLeft + lp.width / 2, + cellXY[1] + childTop + lp.height / 2, 0, null); + } + } + } + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + @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 + 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(); + } + } + + @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() && enabled) { + view.buildDrawingCache(true); + } + } + } + + @Override + protected void setChildrenDrawnWithCacheEnabled(boolean enabled) { + super.setChildrenDrawnWithCacheEnabled(enabled); + } +} diff --git a/app/src/main/java/com/android/launcher2/ShortcutInfo.java b/app/src/main/java/com/android/launcher2/ShortcutInfo.java new file mode 100644 index 0000000..ccb663a --- /dev/null +++ b/app/src/main/java/com/android/launcher2/ShortcutInfo.java @@ -0,0 +1,162 @@ +/* + * 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.launcher2; + +import java.util.ArrayList; + +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Intent; +import android.graphics.Bitmap; +import android.util.Log; + +/** + * Represents a launchable icon on the workspaces and in folders. + */ +class ShortcutInfo extends ItemInfo { + + /** + * The intent used to start the application. + */ + Intent intent; + + /** + * Indicates whether the icon comes from an application's resource (if false) + * or from a custom Bitmap (if true.) + */ + boolean customIcon; + + /** + * Indicates whether we're using the default fallback icon instead of something from the + * app. + */ + boolean usingFallbackIcon; + + /** + * If isShortcut=true and customIcon=false, this contains a reference to the + * shortcut icon as an application's resource. + */ + Intent.ShortcutIconResource iconResource; + + /** + * The application icon. + */ + private Bitmap mIcon; + + ShortcutInfo() { + itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; + } + + public ShortcutInfo(ShortcutInfo info) { + super(info); + title = info.title.toString(); + intent = new Intent(info.intent); + if (info.iconResource != null) { + iconResource = new Intent.ShortcutIconResource(); + iconResource.packageName = info.iconResource.packageName; + iconResource.resourceName = info.iconResource.resourceName; + } + mIcon = info.mIcon; // TODO: should make a copy here. maybe we don't need this ctor at all + customIcon = info.customIcon; + } + + /** TODO: Remove this. It's only called by ApplicationInfo.makeShortcut. */ + public ShortcutInfo(ApplicationInfo info) { + super(info); + title = info.title.toString(); + intent = new Intent(info.intent); + customIcon = false; + } + + public void setIcon(Bitmap b) { + mIcon = b; + } + + public Bitmap getIcon(IconCache iconCache) { + if (mIcon == null) { + updateIcon(iconCache); + } + return mIcon; + } + + public void updateIcon(IconCache iconCache) { + mIcon = iconCache.getIcon(intent); + usingFallbackIcon = iconCache.isDefaultIcon(mIcon); + } + + /** + * Creates the application intent based on a component name and various launch flags. + * Sets {@link #itemType} to {@link LauncherSettings.BaseLauncherColumns#ITEM_TYPE_APPLICATION}. + * + * @param className the class name of the component representing the intent + * @param launchFlags the launch flags + */ + final void setActivity(ComponentName className, int launchFlags) { + intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(className); + intent.setFlags(launchFlags); + itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION; + } + + @Override + void onAddToDatabase(ContentValues values) { + super.onAddToDatabase(values); + + String titleStr = title != null ? title.toString() : null; + values.put(LauncherSettings.BaseLauncherColumns.TITLE, titleStr); + + String uri = intent != null ? intent.toUri(0) : null; + values.put(LauncherSettings.BaseLauncherColumns.INTENT, uri); + + if (customIcon) { + values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, + LauncherSettings.BaseLauncherColumns.ICON_TYPE_BITMAP); + writeBitmap(values, mIcon); + } else { + if (!usingFallbackIcon) { + writeBitmap(values, mIcon); + } + values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE, + LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE); + if (iconResource != null) { + values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE, + iconResource.packageName); + values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE, + iconResource.resourceName); + } + } + } + + @Override + public String toString() { + return "ShortcutInfo(title=" + title.toString() + "intent=" + intent + "id=" + this.id + + " type=" + this.itemType + " container=" + this.container + " screen=" + screen + + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX + " spanY=" + spanY + + " dropPos=" + dropPos + ")"; + } + + public static void dumpShortcutInfoList(String tag, String label, + ArrayList list) { + Log.d(tag, label + " size=" + list.size()); + for (ShortcutInfo info: list) { + Log.d(tag, " title=\"" + info.title + " icon=" + info.mIcon + + " customIcon=" + info.customIcon); + } + } +} + diff --git a/app/src/main/java/com/android/launcher2/SmoothPagedView.java b/app/src/main/java/com/android/launcher2/SmoothPagedView.java new file mode 100644 index 0000000..7e47f1a --- /dev/null +++ b/app/src/main/java/com/android/launcher2/SmoothPagedView.java @@ -0,0 +1,188 @@ +/* + * 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.launcher2; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +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) { + // _o(t) = t * t * ((tension + 1) * t + tension) + // o(t) = _o(t - 1) + 1 + 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(); + mScroller = new Scroller(getContext(), 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 = getChildOffset(whichPage) - getRelativeChildOffset(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/app/src/main/java/com/android/launcher2/SpringLoadedDragController.java b/app/src/main/java/com/android/launcher2/SpringLoadedDragController.java new file mode 100644 index 0000000..d96aab7 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/SpringLoadedDragController.java @@ -0,0 +1,62 @@ +/* + * 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.launcher2; + +public class SpringLoadedDragController implements OnAlarmListener { + // how long the user must hover over a mini-screen before it unshrinks + final long ENTER_SPRING_LOAD_HOVER_TIME = 500; + final long ENTER_SPRING_LOAD_CANCEL_HOVER_TIME = 950; + final long EXIT_SPRING_LOAD_HOVER_TIME = 200; + + Alarm mAlarm; + + // the screen the user is currently hovering over, if any + private CellLayout mScreen; + private Launcher mLauncher; + + public SpringLoadedDragController(Launcher launcher) { + mLauncher = launcher; + mAlarm = new Alarm(); + mAlarm.setOnAlarmListener(this); + } + + public void cancel() { + mAlarm.cancelAlarm(); + } + + // Set a new alarm to expire for the screen that we are hovering over now + public void setAlarm(CellLayout cl) { + mAlarm.cancelAlarm(); + mAlarm.setAlarm((cl == null) ? ENTER_SPRING_LOAD_CANCEL_HOVER_TIME : + ENTER_SPRING_LOAD_HOVER_TIME); + mScreen = cl; + } + + // this is called when our timer runs out + public void onAlarm(Alarm alarm) { + if (mScreen != null) { + // Snap to the screen that we are hovering over now + Workspace w = mLauncher.getWorkspace(); + int page = w.indexOfChild(mScreen); + if (page != w.getCurrentPage()) { + w.snapToPage(page); + } + } else { + mLauncher.getDragController().cancelDrag(); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/UninstallShortcutReceiver.java b/app/src/main/java/com/android/launcher2/UninstallShortcutReceiver.java new file mode 100644 index 0000000..02590c9 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/UninstallShortcutReceiver.java @@ -0,0 +1,166 @@ +/* + * 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.launcher2; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.widget.Toast; + +import com.android.launcher.R; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +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) { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sharedPrefs = context.getSharedPreferences(spKey, Context.MODE_PRIVATE); + + final Intent data = pendingInfo.data; + + LauncherApplication app = (LauncherApplication) context.getApplicationContext(); + synchronized (app) { + removeShortcut(context, data, sharedPrefs); + } + } + + private static void removeShortcut(Context context, Intent data, + final SharedPreferences sharedPrefs) { + 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(); + } + + // Remove any items due to be animated + boolean appRemoved; + Set newApps = new HashSet(); + newApps = sharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps); + synchronized (newApps) { + do { + appRemoved = newApps.remove(intent.toUri(0).toString()); + } while (appRemoved); + } + if (appRemoved) { + final Set savedNewApps = newApps; + new Thread("setNewAppsThread-remove") { + public void run() { + synchronized (savedNewApps) { + SharedPreferences.Editor editor = sharedPrefs.edit(); + editor.putStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, + savedNewApps); + if (savedNewApps.isEmpty()) { + // Reset the page index if there are no more items + editor.putInt(InstallShortcutReceiver.NEW_APPS_PAGE_KEY, -1); + } + editor.commit(); + } + } + }.start(); + } + } + } +} diff --git a/app/src/main/java/com/android/launcher2/UserInitializeReceiver.java b/app/src/main/java/com/android/launcher2/UserInitializeReceiver.java new file mode 100644 index 0000000..bf3330a --- /dev/null +++ b/app/src/main/java/com/android/launcher2/UserInitializeReceiver.java @@ -0,0 +1,70 @@ +/* + * 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.launcher2; + +import java.io.IOException; +import java.util.ArrayList; + +import com.android.launcher.R; + +import android.app.WallpaperManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; + +/** + * 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) { + final Resources resources = context.getResources(); + // Context.getPackageName() may return the "original" package name, + // com.android.launcher2; Resources needs the real package name, + // com.android.launcher. So we ask Resources for what it thinks the + // package name should be. + final String packageName = resources.getResourcePackageName(R.array.wallpapers); + ArrayList list = new ArrayList(); + addWallpapers(resources, packageName, R.array.wallpapers, list); + addWallpapers(resources, packageName, R.array.extra_wallpapers, list); + WallpaperManager wpm = (WallpaperManager) context.getSystemService( + Context.WALLPAPER_SERVICE); + for (int i=1; i outList) { + final String[] extras = resources.getStringArray(resid); + for (String extra : extras) { + int res = resources.getIdentifier(extra, "drawable", packageName); + if (res != 0) { + outList.add(res); + } + } + } +} diff --git a/app/src/main/java/com/android/launcher2/Utilities.java b/app/src/main/java/com/android/launcher2/Utilities.java new file mode 100644 index 0000000..d3e4516 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/Utilities.java @@ -0,0 +1,275 @@ +/* + * 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.launcher2; + +import java.util.Random; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.PaintDrawable; +import android.util.DisplayMetrics; + +import com.android.launcher.R; + +/** + * Various utilities shared amongst the Launcher's classes. + */ +final class Utilities { + @SuppressWarnings("unused") + private static final String TAG = "Launcher.Utilities"; + + private static int sIconWidth = -1; + private static int sIconHeight = -1; + private static int sIconTextureWidth = -1; + private static int sIconTextureHeight = -1; + + private static final Paint sBlurPaint = new Paint(); + private static final Paint sGlowColorPressedPaint = new Paint(); + private static final Paint sGlowColorFocusedPaint = new Paint(); + private static final Paint sDisabledPaint = new Paint(); + private static final Rect sOldBounds = new Rect(); + private static final Canvas sCanvas = new Canvas(); + + static { + sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, + Paint.FILTER_BITMAP_FLAG)); + } + static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff }; + static int sColorIndex = 0; + + /** + * Returns a bitmap suitable for the all apps view. Used to convert pre-ICS + * icon bitmaps that are stored in the database (which were 74x74 pixels at hdpi size) + * to the proper size (48dp) + */ + static Bitmap createIconBitmap(Bitmap icon, Context context) { + int textureWidth = sIconTextureWidth; + int textureHeight = sIconTextureHeight; + int sourceWidth = icon.getWidth(); + int sourceHeight = icon.getHeight(); + if (sourceWidth > textureWidth && sourceHeight > textureHeight) { + // Icon is bigger than it should be; clip it (solves the GB->ICS migration case) + return Bitmap.createBitmap(icon, + (sourceWidth - textureWidth) / 2, + (sourceHeight - textureHeight) / 2, + textureWidth, textureHeight); + } else if (sourceWidth == textureWidth && sourceHeight == textureHeight) { + // Icon is the right size, no need to change it + return icon; + } else { + // Icon is too small, render to a larger bitmap + final Resources resources = context.getResources(); + return createIconBitmap(new BitmapDrawable(resources, icon), context); + } + } + + /** + * Returns a bitmap suitable for the all apps view. + */ + static Bitmap createIconBitmap(Drawable icon, Context context) { + synchronized (sCanvas) { // we share the statics :-( + if (sIconWidth == -1) { + initStatics(context); + } + + int width = sIconWidth; + int height = sIconHeight; + + if (icon instanceof PaintDrawable) { + PaintDrawable painter = (PaintDrawable) icon; + painter.setIntrinsicWidth(width); + painter.setIntrinsicHeight(height); + } else if (icon instanceof BitmapDrawable) { + // Ensure the bitmap has a density. + BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; + Bitmap bitmap = bitmapDrawable.getBitmap(); + if (bitmap.getDensity() == Bitmap.DENSITY_NONE) { + bitmapDrawable.setTargetDensity(context.getResources().getDisplayMetrics()); + } + } + int sourceWidth = icon.getIntrinsicWidth(); + int sourceHeight = icon.getIntrinsicHeight(); + if (sourceWidth > 0 && sourceHeight > 0) { + // There are intrinsic sizes. + if (width < sourceWidth || height < sourceHeight) { + // It's too big, scale it down. + final float ratio = (float) sourceWidth / sourceHeight; + if (sourceWidth > sourceHeight) { + height = (int) (width / ratio); + } else if (sourceHeight > sourceWidth) { + width = (int) (height * ratio); + } + } else if (sourceWidth < width && sourceHeight < height) { + // Don't scale up the icon + width = sourceWidth; + height = sourceHeight; + } + } + + // no intrinsic size --> use default size + int textureWidth = sIconTextureWidth; + int textureHeight = sIconTextureHeight; + + final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, + Bitmap.Config.ARGB_8888); + final Canvas canvas = sCanvas; + canvas.setBitmap(bitmap); + + final int left = (textureWidth-width) / 2; + final int top = (textureHeight-height) / 2; + + @SuppressWarnings("all") // suppress dead code warning + final boolean debug = false; + if (debug) { + // draw a big box for the icon for debugging + canvas.drawColor(sColors[sColorIndex]); + if (++sColorIndex >= sColors.length) sColorIndex = 0; + Paint debugPaint = new Paint(); + debugPaint.setColor(0xffcccc00); + canvas.drawRect(left, top, left+width, top+height, debugPaint); + } + + sOldBounds.set(icon.getBounds()); + icon.setBounds(left, top, left+width, top+height); + icon.draw(canvas); + icon.setBounds(sOldBounds); + canvas.setBitmap(null); + + return bitmap; + } + } + + static void drawSelectedAllAppsBitmap(Canvas dest, int destWidth, int destHeight, + boolean pressed, Bitmap src) { + synchronized (sCanvas) { // we share the statics :-( + if (sIconWidth == -1) { + // We can't have gotten to here without src being initialized, which + // comes from this file already. So just assert. + //initStatics(context); + throw new RuntimeException("Assertion failed: Utilities not initialized"); + } + + dest.drawColor(0, PorterDuff.Mode.CLEAR); + + int[] xy = new int[2]; + Bitmap mask = src.extractAlpha(sBlurPaint, xy); + + float px = (destWidth - src.getWidth()) / 2; + float py = (destHeight - src.getHeight()) / 2; + dest.drawBitmap(mask, px + xy[0], py + xy[1], + pressed ? sGlowColorPressedPaint : sGlowColorFocusedPaint); + + mask.recycle(); + } + } + + /** + * Returns a Bitmap representing the thumbnail of the specified Bitmap. + * The size of the thumbnail is defined by the dimension + * android.R.dimen.launcher_application_icon_size. + * + * @param bitmap The bitmap to get a thumbnail of. + * @param context The application's context. + * + * @return A thumbnail for the specified bitmap or the bitmap itself if the + * thumbnail could not be created. + */ + static Bitmap resampleIconBitmap(Bitmap bitmap, Context context) { + synchronized (sCanvas) { // we share the statics :-( + if (sIconWidth == -1) { + initStatics(context); + } + + if (bitmap.getWidth() == sIconWidth && bitmap.getHeight() == sIconHeight) { + return bitmap; + } else { + final Resources resources = context.getResources(); + return createIconBitmap(new BitmapDrawable(resources, bitmap), context); + } + } + } + + static Bitmap drawDisabledBitmap(Bitmap bitmap, Context context) { + synchronized (sCanvas) { // we share the statics :-( + if (sIconWidth == -1) { + initStatics(context); + } + final Bitmap disabled = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), + Bitmap.Config.ARGB_8888); + final Canvas canvas = sCanvas; + canvas.setBitmap(disabled); + + canvas.drawBitmap(bitmap, 0.0f, 0.0f, sDisabledPaint); + + canvas.setBitmap(null); + + return disabled; + } + } + + private static void initStatics(Context context) { + final Resources resources = context.getResources(); + final DisplayMetrics metrics = resources.getDisplayMetrics(); + final float density = metrics.density; + + sIconWidth = sIconHeight = (int) resources.getDimension(R.dimen.app_icon_size); + sIconTextureWidth = sIconTextureHeight = sIconWidth; + + sBlurPaint.setMaskFilter(new BlurMaskFilter(5 * density, BlurMaskFilter.Blur.NORMAL)); + sGlowColorPressedPaint.setColor(0xffffc300); + sGlowColorFocusedPaint.setColor(0xffff8e00); + + ColorMatrix cm = new ColorMatrix(); + cm.setSaturation(0.2f); + sDisabledPaint.setColorFilter(new ColorMatrixColorFilter(cm)); + sDisabledPaint.setAlpha(0x88); + } + + /** Only works for positive numbers. */ + static int roundToPow2(int n) { + int orig = n; + n >>= 1; + int mask = 0x8000000; + while (mask != 0 && (n & mask) == 0) { + mask >>= 1; + } + while (mask != 0) { + n |= mask; + mask >>= 1; + } + n += 1; + if (n != orig) { + n <<= 1; + } + return n; + } + + static int generateRandomId() { + return new Random(System.currentTimeMillis()).nextInt(1 << 24); + } +} diff --git a/app/src/main/java/com/android/launcher2/WallpaperChooser.java b/app/src/main/java/com/android/launcher2/WallpaperChooser.java new file mode 100644 index 0000000..77e1e6f --- /dev/null +++ b/app/src/main/java/com/android/launcher2/WallpaperChooser.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher2; + +import com.android.launcher.R; + +import android.app.Activity; +import android.app.DialogFragment; +import android.app.Fragment; +import android.os.Bundle; + +public class WallpaperChooser extends Activity { + @SuppressWarnings("unused") + private static final String TAG = "Launcher.WallpaperChooser"; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.wallpaper_chooser_base); + + Fragment fragmentView = + getFragmentManager().findFragmentById(R.id.wallpaper_chooser_fragment); + // TODO: The following code is currently not exercised. Leaving it here in case it + // needs to be revived again. + if (fragmentView == null) { + /* When the screen is XLarge, the fragment is not included in the layout, so show it + * as a dialog + */ + DialogFragment fragment = WallpaperChooserDialogFragment.newInstance(); + fragment.show(getFragmentManager(), "dialog"); + } + } +} diff --git a/app/src/main/java/com/android/launcher2/WallpaperChooserDialogFragment.java b/app/src/main/java/com/android/launcher2/WallpaperChooserDialogFragment.java new file mode 100644 index 0000000..b99d8ec --- /dev/null +++ b/app/src/main/java/com/android/launcher2/WallpaperChooserDialogFragment.java @@ -0,0 +1,360 @@ +/* + * 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.launcher2; + +import android.app.Activity; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.WallpaperManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Gallery; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; + +import com.android.launcher.R; + +import java.io.IOException; +import java.util.ArrayList; + +public class WallpaperChooserDialogFragment extends DialogFragment implements + AdapterView.OnItemSelectedListener, AdapterView.OnItemClickListener { + + private static final String TAG = "Launcher.WallpaperChooserDialogFragment"; + private static final String EMBEDDED_KEY = "com.android.launcher2." + + "WallpaperChooserDialogFragment.EMBEDDED_KEY"; + + private boolean mEmbedded; + private Bitmap mBitmap = null; + + private ArrayList mThumbs; + private ArrayList mImages; + private WallpaperLoader mLoader; + private WallpaperDrawable mWallpaperDrawable = new WallpaperDrawable(); + + public static WallpaperChooserDialogFragment newInstance() { + WallpaperChooserDialogFragment fragment = new WallpaperChooserDialogFragment(); + fragment.setCancelable(true); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.containsKey(EMBEDDED_KEY)) { + mEmbedded = savedInstanceState.getBoolean(EMBEDDED_KEY); + } else { + mEmbedded = isInLayout(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putBoolean(EMBEDDED_KEY, mEmbedded); + } + + private void cancelLoader() { + if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { + mLoader.cancel(true); + mLoader = null; + } + } + + @Override + public void onDetach() { + super.onDetach(); + + cancelLoader(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + cancelLoader(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + /* On orientation changes, the dialog is effectively "dismissed" so this is called + * when the activity is no longer associated with this dying dialog fragment. We + * should just safely ignore this case by checking if getActivity() returns null + */ + Activity activity = getActivity(); + if (activity != null) { + activity.finish(); + } + } + + /* This will only be called when in XLarge mode, since this Fragment is invoked like + * a dialog in that mode + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + findWallpapers(); + + return null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + findWallpapers(); + + /* If this fragment is embedded in the layout of this activity, then we should + * generate a view to display. Otherwise, a dialog will be created in + * onCreateDialog() + */ + if (mEmbedded) { + View view = inflater.inflate(R.layout.wallpaper_chooser, container, false); + view.setBackground(mWallpaperDrawable); + + final Gallery gallery = (Gallery) view.findViewById(R.id.gallery); + gallery.setCallbackDuringFling(false); + gallery.setOnItemSelectedListener(this); + gallery.setAdapter(new ImageAdapter(getActivity())); + + View setButton = view.findViewById(R.id.set); + setButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + selectWallpaper(gallery.getSelectedItemPosition()); + } + }); + return view; + } + return null; + } + + private void selectWallpaper(int position) { + try { + WallpaperManager wpm = (WallpaperManager) getActivity().getSystemService( + Context.WALLPAPER_SERVICE); + wpm.setResource(mImages.get(position)); + Activity activity = getActivity(); + activity.setResult(Activity.RESULT_OK); + activity.finish(); + } catch (IOException e) { + Log.e(TAG, "Failed to set wallpaper: " + e); + } + } + + // Click handler for the Dialog's GridView + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + selectWallpaper(position); + } + + // Selection handler for the embedded Gallery view + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (mLoader != null && mLoader.getStatus() != WallpaperLoader.Status.FINISHED) { + mLoader.cancel(); + } + mLoader = (WallpaperLoader) new WallpaperLoader().execute(position); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + + private void findWallpapers() { + mThumbs = new ArrayList(24); + mImages = new ArrayList(24); + + final Resources resources = getResources(); + // Context.getPackageName() may return the "original" package name, + // com.android.launcher2; Resources needs the real package name, + // com.android.launcher. So we ask Resources for what it thinks the + // package name should be. + final String packageName = resources.getResourcePackageName(R.array.wallpapers); + + addWallpapers(resources, packageName, R.array.wallpapers); + addWallpapers(resources, packageName, R.array.extra_wallpapers); + } + + private void addWallpapers(Resources resources, String packageName, int list) { + final String[] extras = resources.getStringArray(list); + for (String extra : extras) { + int res = resources.getIdentifier(extra, "drawable", packageName); + if (res != 0) { + final int thumbRes = resources.getIdentifier(extra + "_small", + "drawable", packageName); + + if (thumbRes != 0) { + mThumbs.add(thumbRes); + mImages.add(res); + // Log.d(TAG, "add: [" + packageName + "]: " + extra + " (" + res + ")"); + } + } + } + } + + private class ImageAdapter extends BaseAdapter implements ListAdapter, SpinnerAdapter { + private LayoutInflater mLayoutInflater; + + ImageAdapter(Activity activity) { + mLayoutInflater = activity.getLayoutInflater(); + } + + public int getCount() { + return mThumbs.size(); + } + + public Object getItem(int position) { + return position; + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View view; + + if (convertView == null) { + view = mLayoutInflater.inflate(R.layout.wallpaper_item, parent, false); + } else { + view = convertView; + } + + ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); + + int thumbRes = mThumbs.get(position); + image.setImageResource(thumbRes); + Drawable thumbDrawable = image.getDrawable(); + if (thumbDrawable != null) { + thumbDrawable.setDither(true); + } else { + Log.e(TAG, "Error decoding thumbnail resId=" + thumbRes + " for wallpaper #" + + position); + } + + return view; + } + } + + class WallpaperLoader extends AsyncTask { + BitmapFactory.Options mOptions; + + WallpaperLoader() { + mOptions = new BitmapFactory.Options(); + mOptions.inDither = false; + mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; + } + + @Override + protected Bitmap doInBackground(Integer... params) { + if (isCancelled()) return null; + try { + return BitmapFactory.decodeResource(getResources(), + mImages.get(params[0]), mOptions); + } catch (OutOfMemoryError e) { + return null; + } + } + + @Override + protected void onPostExecute(Bitmap b) { + if (b == null) return; + + if (!isCancelled() && !mOptions.mCancel) { + // Help the GC + if (mBitmap != null) { + mBitmap.recycle(); + } + + View v = getView(); + if (v != null) { + mBitmap = b; + mWallpaperDrawable.setBitmap(b); + v.postInvalidate(); + } else { + mBitmap = null; + mWallpaperDrawable.setBitmap(null); + } + mLoader = null; + } else { + b.recycle(); + } + } + + void cancel() { + mOptions.requestCancelDecode(); + super.cancel(true); + } + } + + /** + * Custom drawable that centers the bitmap fed to it. + */ + static class WallpaperDrawable extends Drawable { + + Bitmap mBitmap; + int mIntrinsicWidth; + int mIntrinsicHeight; + + /* package */void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + if (mBitmap == null) + return; + mIntrinsicWidth = mBitmap.getWidth(); + mIntrinsicHeight = mBitmap.getHeight(); + } + + @Override + public void draw(Canvas canvas) { + if (mBitmap == null) return; + int width = canvas.getWidth(); + int height = canvas.getHeight(); + int x = (width - mIntrinsicWidth) / 2; + int y = (height - mIntrinsicHeight) / 2; + canvas.drawBitmap(mBitmap, x, y, null); + } + + @Override + public int getOpacity() { + return android.graphics.PixelFormat.OPAQUE; + } + + @Override + public void setAlpha(int alpha) { + // Ignore + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Ignore + } + } +} diff --git a/app/src/main/java/com/android/launcher2/WidgetPreviewLoader.java b/app/src/main/java/com/android/launcher2/WidgetPreviewLoader.java new file mode 100644 index 0000000..41a8904 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/WidgetPreviewLoader.java @@ -0,0 +1,610 @@ +package com.android.launcher2; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +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.Bitmap.Config; +import android.graphics.BitmapFactory; +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.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.util.Log; + +import com.android.launcher.R; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +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; + } + } +} + +class CanvasCache extends SoftReferenceThreadLocal { + @Override + protected Canvas initialValue() { + return new Canvas(); + } +} + +class PaintCache extends SoftReferenceThreadLocal { + @Override + protected Paint initialValue() { + return null; + } +} + +class BitmapCache extends SoftReferenceThreadLocal { + @Override + protected Bitmap initialValue() { + return null; + } +} + +class RectCache extends SoftReferenceThreadLocal { + @Override + protected Rect initialValue() { + return new Rect(); + } +} + +class BitmapFactoryOptionsCache extends SoftReferenceThreadLocal { + @Override + protected BitmapFactory.Options initialValue() { + return new BitmapFactory.Options(); + } +} + +public class WidgetPreviewLoader { + static final String TAG = "WidgetPreviewLoader"; + + private int mPreviewBitmapWidth; + private int mPreviewBitmapHeight; + private String mSize; + private Context mContext; + private Launcher mLauncher; + private PackageManager mPackageManager; + private PagedViewCellLayout mWidgetSpacingLayout; + + // Used for drawing shortcut previews + private BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); + private PaintCache mCachedShortcutPreviewPaint = new PaintCache(); + private CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); + + // Used for drawing widget previews + private CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); + private RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); + private RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); + private PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); + private String mCachedSelectQuery; + private BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache(); + + private int mAppIconSize; + private IconCache mIconCache; + + private final float sWidgetPreviewIconPaddingPercentage = 0.25f; + + private CacheDb mDb; + + private HashMap> mLoadedPreviews; + private ArrayList> mUnusedBitmaps; + private static HashSet sInvalidPackages; + + static { + sInvalidPackages = new HashSet(); + } + + public WidgetPreviewLoader(Launcher launcher) { + mContext = mLauncher = launcher; + mPackageManager = mContext.getPackageManager(); + mAppIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.app_icon_size); + LauncherApplication app = (LauncherApplication) launcher.getApplicationContext(); + mIconCache = app.getIconCache(); + mDb = app.getWidgetPreviewCacheDb(); + mLoadedPreviews = new HashMap>(); + mUnusedBitmaps = new ArrayList>(); + } + + public void setPreviewSize(int previewWidth, int previewHeight, + PagedViewCellLayout widgetSpacingLayout) { + mPreviewBitmapWidth = previewWidth; + mPreviewBitmapHeight = previewHeight; + mSize = previewWidth + "x" + previewHeight; + mWidgetSpacingLayout = widgetSpacingLayout; + } + + public Bitmap getPreview(final Object o) { + String name = getObjectName(o); + // check if the package is valid + boolean packageValid = true; + synchronized(sInvalidPackages) { + packageValid = !sInvalidPackages.contains(getObjectPackage(o)); + } + if (!packageValid) { + return null; + } + if (packageValid) { + synchronized(mLoadedPreviews) { + // check if it exists in our existing cache + if (mLoadedPreviews.containsKey(name) && mLoadedPreviews.get(name).get() != null) { + return mLoadedPreviews.get(name).get(); + } + } + } + + Bitmap unusedBitmap = null; + synchronized(mUnusedBitmaps) { + // not in cache; we need to load it from the db + while ((unusedBitmap == null || !unusedBitmap.isMutable() || + unusedBitmap.getWidth() != mPreviewBitmapWidth || + unusedBitmap.getHeight() != mPreviewBitmapHeight) + && mUnusedBitmaps.size() > 0) { + unusedBitmap = mUnusedBitmaps.remove(0).get(); + } + 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 = null; + + if (packageValid) { + 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 DB_NAME = "widgetpreviews.db"; + 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; + + public CacheDb(Context context) { + super(context, new File(context.getCacheDir(), DB_NAME).getPath(), null, DB_VERSION); + // Store the context for later use + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + + COLUMN_NAME + " TEXT NOT NULL, " + + COLUMN_SIZE + " TEXT NOT NULL, " + + COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " + + "PRIMARY KEY (" + COLUMN_NAME + ", " + 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); + } + } + } + + private static final String WIDGET_PREFIX = "Widget:"; + private static final String SHORTCUT_PREFIX = "Shortcut:"; + + private static String getObjectName(Object o) { + // should cache the string builder + StringBuilder sb = new StringBuilder(); + String output; + if (o instanceof AppWidgetProviderInfo) { + sb.append(WIDGET_PREFIX); + sb.append(((AppWidgetProviderInfo) o).provider.flattenToString()); + 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(); + } else { + ResolveInfo info = (ResolveInfo) o; + return info.activityInfo.packageName; + } + } + + private void writeToDb(Object o, Bitmap preview) { + String name = getObjectName(o); + SQLiteDatabase db = mDb.getWritableDatabase(); + ContentValues values = new ContentValues(); + + 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); + db.insert(CacheDb.TABLE_NAME, null, values); + } + + public static void removeFromDb(final CacheDb cacheDb, final String packageName) { + synchronized(sInvalidPackages) { + sInvalidPackages.add(packageName); + } + new AsyncTask() { + public Void doInBackground(Void ... args) { + SQLiteDatabase db = cacheDb.getWritableDatabase(); + 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 + ); + synchronized(sInvalidPackages) { + sInvalidPackages.remove(packageName); + } + 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 = 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); + 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; + Bitmap out = BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); + return out; + } else { + result.close(); + return null; + } + } + + public 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(mLauncher, info); + int maxWidth = maxWidthForWidgetPreview(cellSpans[0]); + int maxHeight = maxHeightForWidgetPreview(cellSpans[1]); + return generateWidgetPreview(info.provider, info.previewImage, info.icon, + 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(ComponentName provider, int previewImage, + int iconId, int cellHSpan, int cellVSpan, int maxPreviewWidth, int maxPreviewHeight, + Bitmap preview, int[] preScaledWidthOut) { + // Load the preview image if possible + String packageName = provider.getPackageName(); + if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; + if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; + + Drawable drawable = null; + if (previewImage != 0) { + drawable = mPackageManager.getDrawable(packageName, previewImage, null); + if (drawable == null) { + Log.w(TAG, "Can't load widget preview drawable 0x" + + Integer.toHexString(previewImage) + " for provider: " + provider); + } + } + + int previewWidth; + int previewHeight; + Bitmap defaultPreview = null; + boolean widgetPreviewExists = (drawable != 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; + + BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() + .getDrawable(R.drawable.widget_preview_tile); + final int previewDrawableWidth = previewDrawable + .getIntrinsicWidth(); + final int previewDrawableHeight = previewDrawable + .getIntrinsicHeight(); + previewWidth = previewDrawableWidth * cellHSpan; // subtract 2 dips + previewHeight = previewDrawableHeight * cellVSpan; + + defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, + Config.ARGB_8888); + final Canvas c = mCachedAppWidgetPreviewCanvas.get(); + c.setBitmap(defaultPreview); + previewDrawable.setBounds(0, 0, previewWidth, previewHeight); + previewDrawable.setTileModeXY(Shader.TileMode.REPEAT, + Shader.TileMode.REPEAT); + previewDrawable.draw(c); + c.setBitmap(null); + + // Draw the icon in the top left corner + int minOffset = (int) (mAppIconSize * sWidgetPreviewIconPaddingPercentage); + int smallestSide = Math.min(previewWidth, previewHeight); + float iconScale = Math.min((float) smallestSide + / (mAppIconSize + 2 * minOffset), 1f); + + try { + Drawable icon = null; + int hoffset = + (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); + int yoffset = + (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); + if (iconId > 0) + icon = mIconCache.getFullResIcon(packageName, iconId); + if (icon != null) { + renderDrawableToBitmap(icon, defaultPreview, hoffset, + yoffset, (int) (mAppIconSize * iconScale), + (int) (mAppIconSize * iconScale)); + } + } catch (Resources.NotFoundException e) { + } + } + + // Scale to fit width only - let the widget preview be clipped in the + // vertical dimension + float scale = 1f; + if (preScaledWidthOut != null) { + preScaledWidthOut[0] = previewWidth; + } + if (previewWidth > maxPreviewWidth) { + scale = maxPreviewWidth / (float) previewWidth; + } + if (scale != 1f) { + previewWidth = (int) (scale * previewWidth); + previewHeight = (int) (scale * previewHeight); + } + + // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size + if (preview == null) { + preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); + } + + // Draw the scaled preview into the final bitmap + int x = (preview.getWidth() - previewWidth) / 2; + if (widgetPreviewExists) { + renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, + previewHeight); + } 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); + + Paint p = mCachedAppWidgetPreviewPaint.get(); + if (p == null) { + p = new Paint(); + p.setFilterBitmap(true); + mCachedAppWidgetPreviewPaint.set(p); + } + c.drawBitmap(defaultPreview, src, dest, p); + c.setBitmap(null); + } + return preview; + } + + 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 = mIconCache.getFullResIcon(info); + + 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) { + preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); + } + + c.setBitmap(preview); + // 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); + c.setBitmap(null); + + renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize); + + return preview; + } + + + public static void renderDrawableToBitmap( + Drawable d, Bitmap bitmap, int x, int y, int w, int h) { + renderDrawableToBitmap(d, bitmap, x, y, w, h, 1f); + } + + private static void renderDrawableToBitmap( + Drawable d, Bitmap bitmap, int x, int y, int w, int h, + float scale) { + if (bitmap != null) { + Canvas c = new Canvas(bitmap); + c.scale(scale, scale); + Rect oldBounds = d.copyBounds(); + d.setBounds(x, y, x + w, y + h); + d.draw(c); + d.setBounds(oldBounds); // Restore the bounds + c.setBitmap(null); + } + } + +} diff --git a/app/src/main/java/com/android/launcher2/Workspace.java b/app/src/main/java/com/android/launcher2/Workspace.java new file mode 100644 index 0000000..b5c9be3 --- /dev/null +++ b/app/src/main/java/com/android/launcher2/Workspace.java @@ -0,0 +1,3893 @@ +/* + * 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.launcher2; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +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.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.os.IBinder; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.Display; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.launcher.R; +import com.android.launcher2.FolderIcon.FolderRingAnimator; +import com.android.launcher2.LauncherSettings.Favorites; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * The workspace is a wide area with a wallpaper and a finite number of pages. + * 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 + implements DropTarget, DragSource, DragScroller, View.OnTouchListener, + DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener { + 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 final int BACKGROUND_FADE_OUT_DURATION = 350; + private static final int ADJACENT_SCREEN_DROP_DURATION = 300; + private static final int FLING_THRESHOLD_VELOCITY = 500; + + // 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 Drawable mBackground; + boolean mDrawBackground = true; + private float mBackgroundAlpha = 0; + + private float mWallpaperScrollRatio = 1.0f; + private int mOriginalPageSpacing; + + private final WallpaperManager mWallpaperManager; + private IBinder mWindowToken; + private static final float WALLPAPER_SCREENS_SPAN = 2f; + + private int mDefaultPage; + + /** + * CellInfo for the cell that is currently being dragged + */ + private CellLayout.CellInfo mDragInfo; + + /** + * Target drop area calculated during last acceptDrop call. + */ + private int[] mTargetCell = new int[2]; + private int mDragOverX = -1; + private int mDragOverY = -1; + + static Rect mLandscapeCellLayoutMetrics = null; + static Rect mPortraitCellLayoutMetrics = null; + + /** + * The CellLayout that is currently being dragged over + */ + private CellLayout mDragTargetLayout = null; + /** + * The CellLayout that we will show as glowing + */ + private CellLayout mDragOverlappingLayout = null; + + /** + * The CellLayout which will be dropped to + */ + private CellLayout mDropToLayout = null; + + private Launcher mLauncher; + private IconCache mIconCache; + private 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[] mTempEstimate = new int[2]; + private float[] mDragViewVisualCenter = new float[2]; + private float[] mTempDragCoordinates = new float[2]; + private float[] mTempCellLayoutCenterCoordinates = new float[2]; + private float[] mTempDragBottomRightCoordinates = new float[2]; + private Matrix mTempInverseMatrix = new Matrix(); + + private SpringLoadedDragController mSpringLoadedDragController; + private float mSpringLoadedShrinkFactor; + + private static final int DEFAULT_CELL_COUNT_X = 4; + private static final int DEFAULT_CELL_COUNT_Y = 4; + + // State variable that indicates whether the pages are small (ie when you're + // in all apps or customize mode) + + enum State { NORMAL, SPRING_LOADED, SMALL }; + private State mState = State.NORMAL; + private boolean mIsSwitchingState = false; + + boolean mAnimatingViewIntoPlace = false; + boolean mIsDragOccuring = false; + boolean mChildrenLayersEnabled = true; + + /** Is the user is dragging an item near the edge of a page? */ + private boolean mInScrollArea = false; + + private final HolographicOutlineHelper mOutlineHelper = new HolographicOutlineHelper(); + private Bitmap mDragOutline = null; + private final Rect mTempRect = new Rect(); + private final int[] mTempXY = new int[2]; + private int[] mTempVisiblePagesRange = new int[2]; + private float mOverscrollFade = 0; + private boolean mOverscrollTransformsSet; + public static final int DRAG_BITMAP_PADDING = 2; + private boolean mWorkspaceFadeInAdjacentScreens; + + enum WallpaperVerticalOffset { TOP, MIDDLE, BOTTOM }; + int mWallpaperWidth; + int mWallpaperHeight; + WallpaperOffsetInterpolator mWallpaperOffset; + boolean mUpdateWallpaperOffsetImmediately = false; + private Runnable mDelayedResizeRunnable; + private Runnable mDelayedSnapToPageRunnable; + private Point mDisplaySize = new Point(); + private boolean mIsStaticWallpaper; + private int mWallpaperTravelWidth; + private int mSpringLoadedPageSpacing; + private int mCameraDistance; + + // Variables relating to the creation of user folders by hovering shortcuts over shortcuts + private static final int FOLDER_CREATION_TIMEOUT = 0; + private static final int REORDER_TIMEOUT = 250; + private final Alarm mFolderCreationAlarm = new Alarm(); + private final Alarm mReorderAlarm = new Alarm(); + private FolderRingAnimator mDragFolderRingAnimator = null; + private FolderIcon mDragOverFolderIcon = null; + private boolean mCreateUserFolderOnDrop = false; + private boolean mAddToExistingFolderOnDrop = false; + private DropTarget.DragEnforcer mDragEnforcer; + private float mMaxDistanceForFolderCreation; + + // Variables relating to touch disambiguation (scrolling workspace vs. scrolling a widget) + private float mXDown; + private float mYDown; + final static float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6; + final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3; + final static float TOUCH_SLOP_DAMPING_FACTOR = 4; + + // Relating to the animation of items being dropped externally + public static final int ANIMATE_INTO_POSITION_AND_DISAPPEAR = 0; + public static final int ANIMATE_INTO_POSITION_AND_REMAIN = 1; + public static final int ANIMATE_INTO_POSITION_AND_RESIZE = 2; + public static final int COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION = 3; + public static final int CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION = 4; + + // Related to dragging, folder creation and reordering + private static final int DRAG_MODE_NONE = 0; + private static final int DRAG_MODE_CREATE_FOLDER = 1; + 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; + + 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 mCurrentScaleX; + private float mCurrentScaleY; + private float mCurrentRotationY; + private float mCurrentTranslationX; + private float mCurrentTranslationY; + private float[] mOldTranslationXs; + private float[] mOldTranslationYs; + private float[] mOldScaleXs; + private float[] mOldScaleYs; + private float[] mOldBackgroundAlphas; + private float[] mOldAlphas; + private float[] mNewTranslationXs; + private float[] mNewTranslationYs; + private float[] mNewScaleXs; + private float[] mNewScaleYs; + private float[] mNewBackgroundAlphas; + private float[] mNewAlphas; + private float[] mNewRotationYs; + private float mTransitionProgress; + + private final Runnable mBindPages = new Runnable() { + @Override + public void run() { + mLauncher.getModel().bindRemainingSynchronousPages(); + } + }; + + /** + * 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 Workspace(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 Workspace(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mContentIsRefreshable = false; + mOriginalPageSpacing = mPageSpacing; + + mDragEnforcer = new DropTarget.DragEnforcer(context); + // With workspace, data is available straight from the get-go + setDataIsReady(); + + mLauncher = (Launcher) context; + final Resources res = getResources(); + mWorkspaceFadeInAdjacentScreens = res.getBoolean(R.bool.config_workspaceFadeAdjacentScreens); + mFadeInAdjacentScreens = false; + mWallpaperManager = WallpaperManager.getInstance(context); + + int cellCountX = DEFAULT_CELL_COUNT_X; + int cellCountY = DEFAULT_CELL_COUNT_Y; + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.Workspace, defStyle, 0); + + if (LauncherApplication.isScreenLarge()) { + // Determine number of rows/columns dynamically + // TODO: This code currently fails on tablets with an aspect ratio < 1.3. + // Around that ratio we should make cells the same size in portrait and + // landscape + TypedArray actionBarSizeTypedArray = + context.obtainStyledAttributes(new int[] { android.R.attr.actionBarSize }); + final float actionBarHeight = actionBarSizeTypedArray.getDimension(0, 0f); + + Point minDims = new Point(); + Point maxDims = new Point(); + mLauncher.getWindowManager().getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + cellCountX = 1; + while (CellLayout.widthInPortrait(res, cellCountX + 1) <= minDims.x) { + cellCountX++; + } + + cellCountY = 1; + while (actionBarHeight + CellLayout.heightInLandscape(res, cellCountY + 1) + <= minDims.y) { + cellCountY++; + } + } + + mSpringLoadedShrinkFactor = + res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; + mSpringLoadedPageSpacing = + res.getDimensionPixelSize(R.dimen.workspace_spring_loaded_page_spacing); + mCameraDistance = res.getInteger(R.integer.config_cameraDistance); + + // if the value is manually specified, use that instead + cellCountX = a.getInt(R.styleable.Workspace_cellCountX, cellCountX); + cellCountY = a.getInt(R.styleable.Workspace_cellCountY, cellCountY); + mDefaultPage = a.getInt(R.styleable.Workspace_defaultScreen, 1); + a.recycle(); + + setOnHierarchyChangeListener(this); + + LauncherModel.updateWorkspaceLayoutCells(cellCountX, cellCountY); + setHapticFeedbackEnabled(false); + + initWorkspace(); + + // Disable multitouch across the workspace/all apps/customize tray + setMotionEventSplittingEnabled(true); + + // Unless otherwise specified this view is important for accessibility. + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + // 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) { + int[] size = new int[2]; + if (getChildCount() > 0) { + CellLayout cl = (CellLayout) mLauncher.getWorkspace().getChildAt(0); + Rect r = estimateItemPosition(cl, itemInfo, 0, 0, hSpan, vSpan); + size[0] = r.width(); + size[1] = r.height(); + if (springLoaded) { + size[0] *= mSpringLoadedShrinkFactor; + size[1] *= mSpringLoadedShrinkFactor; + } + return size; + } else { + size[0] = Integer.MAX_VALUE; + size[1] = Integer.MAX_VALUE; + return size; + } + } + public Rect estimateItemPosition(CellLayout cl, ItemInfo pendingInfo, + int hCell, int vCell, int hSpan, int vSpan) { + Rect r = new Rect(); + cl.cellToRect(hCell, vCell, hSpan, vSpan, r); + return r; + } + + public void onDragStart(DragSource source, Object info, int dragAction) { + mIsDragOccuring = true; + updateChildrenLayersEnabled(false); + mLauncher.lockScreenOrientation(); + setChildrenBackgroundAlphaMultipliers(1f); + // Prevent any Un/InstallShortcutReceivers from updating the db while we are dragging + InstallShortcutReceiver.enableInstallQueue(); + UninstallShortcutReceiver.enableUninstallQueue(); + } + + public void onDragEnd() { + mIsDragOccuring = false; + updateChildrenLayersEnabled(false); + mLauncher.unlockScreenOrientation(false); + + // Re-enable any Un/InstallShortcutReceiver and now process any queued items + InstallShortcutReceiver.disableAndFlushInstallQueue(getContext()); + UninstallShortcutReceiver.disableAndFlushUninstallQueue(getContext()); + } + + /** + * Initializes various states for this workspace. + */ + protected void initWorkspace() { + Context context = getContext(); + mCurrentPage = mDefaultPage; + Launcher.setScreen(mCurrentPage); + LauncherApplication app = (LauncherApplication)context.getApplicationContext(); + mIconCache = app.getIconCache(); + setWillNotDraw(false); + setClipChildren(false); + setClipToPadding(false); + setChildrenDrawnWithCacheEnabled(true); + + final Resources res = getResources(); + try { + mBackground = res.getDrawable(R.drawable.apps_customize_bg); + } catch (Resources.NotFoundException e) { + // In this case, we will skip drawing background protection + } + + mWallpaperOffset = new WallpaperOffsetInterpolator(); + Display display = mLauncher.getWindowManager().getDefaultDisplay(); + display.getSize(mDisplaySize); + mWallpaperTravelWidth = (int) (mDisplaySize.x * + wallpaperTravelToScreenWidthRatio(mDisplaySize.x, mDisplaySize.y)); + + mMaxDistanceForFolderCreation = (0.55f * res.getDimensionPixelSize(R.dimen.app_icon_size)); + mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); + } + + @Override + protected int getScrollMode() { + return SmoothPagedView.X_LARGE_MODE; + } + + @Override + public void onChildViewAdded(View parent, View child) { + if (!(child instanceof CellLayout)) { + throw new IllegalArgumentException("A Workspace can only have CellLayout children."); + } + CellLayout cl = ((CellLayout) child); + cl.setOnInterceptTouchListener(this); + cl.setClickable(true); + cl.setContentDescription(getContext().getString( + R.string.workspace_description_format, getChildCount())); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + } + + protected boolean shouldDrawChild(View child) { + final CellLayout cl = (CellLayout) child; + return super.shouldDrawChild(child) && + (cl.getShortcutsAndWidgets().getAlpha() > 0 || + cl.getBackgroundAlpha() > 0); + } + + /** + * @return The open folder on the current screen, or null if there is none + */ + Folder getOpenFolder() { + DragLayer dragLayer = mLauncher.getDragLayer(); + int count = dragLayer.getChildCount(); + for (int i = 0; i < count; i++) { + View child = dragLayer.getChildAt(i); + if (child instanceof Folder) { + Folder folder = (Folder) child; + if (folder.getInfo().opened) + return folder; + } + } + return null; + } + + boolean isTouchActive() { + return mTouchState != TOUCH_STATE_REST; + } + + /** + * Adds the specified child in the specified screen. The position and dimension of + * the child are defined by x, y, spanX and spanY. + * + * @param child The child to add in one of the workspace's screens. + * @param screen The screen in which to add the child. + * @param x The X position of the child in the screen's grid. + * @param y The Y position of the child in the screen's grid. + * @param spanX The number of cells spanned horizontally by the child. + * @param spanY The number of cells spanned vertically by the child. + */ + void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY) { + addInScreen(child, container, screen, x, y, spanX, spanY, false); + } + + /** + * Adds the specified child in the specified screen. The position and dimension of + * the child are defined by x, y, spanX and spanY. + * + * @param child The child to add in one of the workspace's screens. + * @param screen The screen in which to add the child. + * @param x The X position of the child in the screen's grid. + * @param y The Y position of the child in the screen's grid. + * @param spanX The number of cells spanned horizontally by the child. + * @param spanY The number of cells spanned vertically by the child. + * @param insert When true, the child is inserted at the beginning of the children list. + */ + void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY, + boolean insert) { + if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + if (screen < 0 || screen >= getChildCount()) { + Log.e(TAG, "The screen must be >= 0 and < " + getChildCount() + + " (was " + screen + "); skipping child"); + return; + } + } + + final CellLayout layout; + if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + layout = mLauncher.getHotseat().getLayout(); + child.setOnKeyListener(null); + + // Hide folder title in the hotseat + if (child instanceof FolderIcon) { + ((FolderIcon) child).setTextVisible(false); + } + + if (screen < 0) { + screen = mLauncher.getHotseat().getOrderInHotseat(x, y); + } else { + // 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 + x = mLauncher.getHotseat().getCellXFromOrder(screen); + y = mLauncher.getHotseat().getCellYFromOrder(screen); + } + } else { + // Show folder title if not in the hotseat + if (child instanceof FolderIcon) { + ((FolderIcon) child).setTextVisible(true); + } + + layout = (CellLayout) getChildAt(screen); + child.setOnKeyListener(new IconKeyEventListener()); + } + + LayoutParams genericLp = child.getLayoutParams(); + CellLayout.LayoutParams lp; + if (genericLp == null || !(genericLp instanceof CellLayout.LayoutParams)) { + lp = new CellLayout.LayoutParams(x, y, spanX, spanY); + } else { + lp = (CellLayout.LayoutParams) genericLp; + lp.cellX = x; + lp.cellY = y; + lp.cellHSpan = spanX; + lp.cellVSpan = spanY; + } + + if (spanX < 0 && spanY < 0) { + lp.isLockedToGrid = false; + } + + // Get the canonical child id to uniquely represent this view in this screen + int childId = LauncherModel.getCellLayoutChildId(container, screen, x, y, spanX, spanY); + boolean markCellsAsOccupied = !(child instanceof Folder); + if (!layout.addViewToCellLayout(child, insert ? 0 : -1, childId, lp, markCellsAsOccupied)) { + // TODO: This branch occurs when the workspace is adding views + // outside of the defined grid + // maybe we should be deleting these items from the LauncherModel? + Log.w(TAG, "Failed to add to item at (" + lp.cellX + "," + lp.cellY + ") to CellLayout"); + } + + if (!(child instanceof Folder)) { + child.setHapticFeedbackEnabled(false); + child.setOnLongClickListener(mLongClickListener); + } + if (child instanceof DropTarget) { + mDragController.addDropTarget((DropTarget) child); + } + } + + /** + * Check if the point (x, y) hits a given page. + */ + private boolean hitsPage(int index, float x, float y) { + final View page = getChildAt(index); + if (page != null) { + float[] localXY = { x, y }; + mapPointFromSelfToChild(page, localXY); + return (localXY[0] >= 0 && localXY[0] < page.getWidth() + && localXY[1] >= 0 && localXY[1] < page.getHeight()); + } + return false; + } + + @Override + protected boolean hitsPreviousPage(float x, float y) { + // mNextPage is set to INVALID_PAGE whenever we are stationary. + // Calculating "next page" this way ensures that you scroll to whatever page you tap on + final int current = (mNextPage == INVALID_PAGE) ? mCurrentPage : mNextPage; + + // Only allow tap to next page on large devices, where there's significant margin outside + // the active workspace + return LauncherApplication.isScreenLarge() && hitsPage(current - 1, x, y); + } + + @Override + protected boolean hitsNextPage(float x, float y) { + // mNextPage is set to INVALID_PAGE whenever we are stationary. + // Calculating "next page" this way ensures that you scroll to whatever page you tap on + final int current = (mNextPage == INVALID_PAGE) ? mCurrentPage : mNextPage; + + // Only allow tap to next page on large devices, where there's significant margin outside + // the active workspace + return LauncherApplication.isScreenLarge() && hitsPage(current + 1, x, y); + } + + /** + * Called directly from a CellLayout (not by the framework), after we've been added as a + * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout + * that it should intercept touch events, which is not something that is normally supported. + */ + @Override + public boolean onTouch(View v, MotionEvent event) { + return (isSmall() || !isFinishedSwitchingState()); + } + + public boolean isSwitchingState() { + return mIsSwitchingState; + } + + /** This differs from isSwitchingState in that we take into account how far the transition + * has completed. */ + public boolean isFinishedSwitchingState() { + return !mIsSwitchingState || (mTransitionProgress > 0.5f); + } + + protected void onWindowVisibilityChanged (int visibility) { + mLauncher.onWindowVisibilityChanged(visibility); + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + if (isSmall() || !isFinishedSwitchingState()) { + // when the home screens are shrunken, shouldn't allow side-scrolling + return false; + } + return super.dispatchUnhandledMove(focused, direction); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + switch (ev.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mXDown = ev.getX(); + mYDown = ev.getY(); + break; + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: + if (mTouchState == TOUCH_STATE_REST) { + final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); + if (!currentPage.lastDownOnOccupiedCell()) { + onWallpaperTap(ev); + } + } + } + return super.onInterceptTouchEvent(ev); + } + + protected void reinflateWidgetsIfNecessary() { + final int clCount = getChildCount(); + for (int i = 0; i < clCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + ShortcutAndWidgetContainer swc = cl.getShortcutsAndWidgets(); + final int itemCount = swc.getChildCount(); + for (int j = 0; j < itemCount; j++) { + View v = swc.getChildAt(j); + + if (v.getTag() instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag(); + LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) info.hostView; + if (lahv != null && lahv.orientationChangedSincedInflation()) { + mLauncher.removeAppWidget(info); + // Remove the current widget which is inflated with the wrong orientation + cl.removeView(lahv); + mLauncher.bindAppWidget(info); + } + } + } + } + } + + @Override + protected void determineScrollingStart(MotionEvent ev) { + if (isSmall()) return; + if (!isFinishedSwitchingState()) return; + + float deltaX = Math.abs(ev.getX() - mXDown); + float deltaY = Math.abs(ev.getY() - mYDown); + + if (Float.compare(deltaX, 0f) == 0) return; + + float slope = deltaY / deltaX; + float theta = (float) Math.atan(slope); + + if (deltaX > mTouchSlop || deltaY > mTouchSlop) { + cancelCurrentPageLongPress(); + } + + if (theta > MAX_SWIPE_ANGLE) { + // Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace + return; + } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) { + // Above START_DAMPING_TOUCH_SLOP_ANGLE and below MAX_SWIPE_ANGLE, we want to + // increase the touch slop to make it harder to begin scrolling the workspace. This + // results in vertically scrolling widgets to more easily. The higher the angle, the + // more we increase touch slop. + theta -= START_DAMPING_TOUCH_SLOP_ANGLE; + float extraRatio = (float) + Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE))); + super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio); + } else { + // Below START_DAMPING_TOUCH_SLOP_ANGLE, we don't do anything special + super.determineScrollingStart(ev); + } + } + + protected void onPageBeginMoving() { + super.onPageBeginMoving(); + + if (isHardwareAccelerated()) { + updateChildrenLayersEnabled(false); + } else { + if (mNextPage != INVALID_PAGE) { + // we're snapping to a particular screen + enableChildrenCache(mCurrentPage, mNextPage); + } else { + // this is when user is actively dragging a particular screen, they might + // swipe it either left or right (but we won't advance by more than one screen) + enableChildrenCache(mCurrentPage - 1, mCurrentPage + 1); + } + } + + // Only show page outlines as we pan if we are on large screen + if (LauncherApplication.isScreenLarge()) { + showOutlines(); + mIsStaticWallpaper = mWallpaperManager.getWallpaperInfo() == null; + } + + // If we are not fading in adjacent screens, we still need to restore the alpha in case the + // user scrolls while we are transitioning (should not affect dispatchDraw optimizations) + if (!mWorkspaceFadeInAdjacentScreens) { + for (int i = 0; i < getChildCount(); ++i) { + ((CellLayout) getPageAt(i)).setShortcutAndWidgetAlpha(1f); + } + } + + // Show the scroll indicator as you pan the page + showScrollingIndicator(false); + } + + protected void onPageEndMoving() { + super.onPageEndMoving(); + + if (isHardwareAccelerated()) { + updateChildrenLayersEnabled(false); + } else { + clearChildrenCache(); + } + + + if (mDragController.isDragging()) { + if (isSmall()) { + // If we are in springloaded mode, then force an event to check if the current touch + // is under a new page (to scroll to) + mDragController.forceTouchMove(); + } + } else { + // If we are not mid-dragging, hide the page outlines if we are on a large screen + if (LauncherApplication.isScreenLarge()) { + hideOutlines(); + } + + // Hide the scroll indicator as you pan the page + if (!mDragController.isDragging()) { + hideScrollingIndicator(false); + } + } + + if (mDelayedResizeRunnable != null) { + mDelayedResizeRunnable.run(); + mDelayedResizeRunnable = null; + } + + if (mDelayedSnapToPageRunnable != null) { + mDelayedSnapToPageRunnable.run(); + mDelayedSnapToPageRunnable = null; + } + } + + @Override + protected void notifyPageSwitchListener() { + super.notifyPageSwitchListener(); + Launcher.setScreen(mCurrentPage); + }; + + // As a ratio of screen height, the total distance we want the parallax effect to span + // horizontally + private 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; + } + + // The range of scroll values for Workspace + private int getScrollRange() { + return getChildOffset(getChildCount() - 1) - getChildOffset(0); + } + + protected void setWallpaperDimension() { + Point minDims = new Point(); + Point maxDims = new Point(); + mLauncher.getWindowManager().getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + final int maxDim = Math.max(maxDims.x, maxDims.y); + final int minDim = Math.min(minDims.x, minDims.y); + + // We need to ensure that there is enough extra space in the wallpaper for the intended + // parallax effects + if (LauncherApplication.isScreenLarge()) { + mWallpaperWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); + mWallpaperHeight = maxDim; + } else { + mWallpaperWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); + mWallpaperHeight = maxDim; + } + new Thread("setWallpaperDimension") { + public void run() { + mWallpaperManager.suggestDesiredDimensions(mWallpaperWidth, mWallpaperHeight); + } + }.start(); + } + + private float wallpaperOffsetForCurrentScroll() { + // Set wallpaper offset steps (1 / (number of screens - 1)) + mWallpaperManager.setWallpaperOffsetSteps(1.0f / (getChildCount() - 1), 1.0f); + + // For the purposes of computing the scrollRange and overScrollOffset, we assume + // that mLayoutScale is 1. This means that when we're in spring-loaded mode, + // there's no discrepancy between the wallpaper offset for a given page. + float layoutScale = mLayoutScale; + mLayoutScale = 1f; + int scrollRange = getScrollRange(); + + // Again, we adjust the wallpaper offset to be consistent between values of mLayoutScale + float adjustedScrollX = Math.max(0, Math.min(getScrollX(), mMaxScrollX)); + adjustedScrollX *= mWallpaperScrollRatio; + mLayoutScale = layoutScale; + + float scrollProgress = + adjustedScrollX / (float) scrollRange; + + if (LauncherApplication.isScreenLarge() && mIsStaticWallpaper) { + // The wallpaper travel width is how far, from left to right, the wallpaper will move + // at this orientation. On tablets in portrait mode we don't move all the way to the + // edges of the wallpaper, or otherwise the parallax effect would be too strong. + int wallpaperTravelWidth = Math.min(mWallpaperTravelWidth, mWallpaperWidth); + + float offsetInDips = wallpaperTravelWidth * scrollProgress + + (mWallpaperWidth - wallpaperTravelWidth) / 2; // center it + float offset = offsetInDips / (float) mWallpaperWidth; + return offset; + } else { + return scrollProgress; + } + } + + private void syncWallpaperOffsetWithScroll() { + final boolean enableWallpaperEffects = isHardwareAccelerated(); + if (enableWallpaperEffects) { + mWallpaperOffset.setFinalX(wallpaperOffsetForCurrentScroll()); + } + } + + public void updateWallpaperOffsetImmediately() { + mUpdateWallpaperOffsetImmediately = true; + } + + private void updateWallpaperOffsets() { + boolean updateNow = false; + boolean keepUpdating = true; + if (mUpdateWallpaperOffsetImmediately) { + updateNow = true; + keepUpdating = false; + mWallpaperOffset.jumpToFinal(); + mUpdateWallpaperOffsetImmediately = false; + } else { + updateNow = keepUpdating = mWallpaperOffset.computeScrollOffset(); + } + if (updateNow) { + if (mWindowToken != null) { + mWallpaperManager.setWallpaperOffsets(mWindowToken, + mWallpaperOffset.getCurrX(), mWallpaperOffset.getCurrY()); + } + } + if (keepUpdating) { + invalidate(); + } + } + + @Override + protected void updateCurrentPageScroll() { + super.updateCurrentPageScroll(); + computeWallpaperScrollRatio(mCurrentPage); + } + + @Override + protected void snapToPage(int whichPage) { + super.snapToPage(whichPage); + computeWallpaperScrollRatio(whichPage); + } + + @Override + protected void snapToPage(int whichPage, int duration) { + super.snapToPage(whichPage, duration); + computeWallpaperScrollRatio(whichPage); + } + + protected void snapToPage(int whichPage, Runnable r) { + if (mDelayedSnapToPageRunnable != null) { + mDelayedSnapToPageRunnable.run(); + } + mDelayedSnapToPageRunnable = r; + snapToPage(whichPage, SLOW_PAGE_SNAP_ANIMATION_DURATION); + } + + private void computeWallpaperScrollRatio(int page) { + // Here, we determine what the desired scroll would be with and without a layout scale, + // and compute a ratio between the two. This allows us to adjust the wallpaper offset + // as though there is no layout scale. + float layoutScale = mLayoutScale; + int scaled = getChildOffset(page) - getRelativeChildOffset(page); + mLayoutScale = 1.0f; + float unscaled = getChildOffset(page) - getRelativeChildOffset(page); + mLayoutScale = layoutScale; + if (scaled > 0) { + mWallpaperScrollRatio = (1.0f * unscaled) / scaled; + } else { + mWallpaperScrollRatio = 1f; + } + } + + class WallpaperOffsetInterpolator { + float mFinalHorizontalWallpaperOffset = 0.0f; + float mFinalVerticalWallpaperOffset = 0.5f; + float mHorizontalWallpaperOffset = 0.0f; + float mVerticalWallpaperOffset = 0.5f; + long mLastWallpaperOffsetUpdateTime; + boolean mIsMovingFast; + boolean mOverrideHorizontalCatchupConstant; + float mHorizontalCatchupConstant = 0.35f; + float mVerticalCatchupConstant = 0.35f; + + public WallpaperOffsetInterpolator() { + } + + public void setOverrideHorizontalCatchupConstant(boolean override) { + mOverrideHorizontalCatchupConstant = override; + } + + public void setHorizontalCatchupConstant(float f) { + mHorizontalCatchupConstant = f; + } + + public void setVerticalCatchupConstant(float f) { + mVerticalCatchupConstant = f; + } + + public boolean computeScrollOffset() { + if (Float.compare(mHorizontalWallpaperOffset, mFinalHorizontalWallpaperOffset) == 0 && + Float.compare(mVerticalWallpaperOffset, mFinalVerticalWallpaperOffset) == 0) { + mIsMovingFast = false; + return false; + } + boolean isLandscape = mDisplaySize.x > mDisplaySize.y; + + long currentTime = System.currentTimeMillis(); + long timeSinceLastUpdate = currentTime - mLastWallpaperOffsetUpdateTime; + timeSinceLastUpdate = Math.min((long) (1000/30f), timeSinceLastUpdate); + timeSinceLastUpdate = Math.max(1L, timeSinceLastUpdate); + + float xdiff = Math.abs(mFinalHorizontalWallpaperOffset - mHorizontalWallpaperOffset); + if (!mIsMovingFast && xdiff > 0.07) { + mIsMovingFast = true; + } + + float fractionToCatchUpIn1MsHorizontal; + if (mOverrideHorizontalCatchupConstant) { + fractionToCatchUpIn1MsHorizontal = mHorizontalCatchupConstant; + } else if (mIsMovingFast) { + fractionToCatchUpIn1MsHorizontal = isLandscape ? 0.5f : 0.75f; + } else { + // slow + fractionToCatchUpIn1MsHorizontal = isLandscape ? 0.27f : 0.5f; + } + float fractionToCatchUpIn1MsVertical = mVerticalCatchupConstant; + + fractionToCatchUpIn1MsHorizontal /= 33f; + fractionToCatchUpIn1MsVertical /= 33f; + + final float UPDATE_THRESHOLD = 0.00001f; + float hOffsetDelta = mFinalHorizontalWallpaperOffset - mHorizontalWallpaperOffset; + float vOffsetDelta = mFinalVerticalWallpaperOffset - mVerticalWallpaperOffset; + boolean jumpToFinalValue = Math.abs(hOffsetDelta) < UPDATE_THRESHOLD && + Math.abs(vOffsetDelta) < UPDATE_THRESHOLD; + + // Don't have any lag between workspace and wallpaper on non-large devices + if (!LauncherApplication.isScreenLarge() || jumpToFinalValue) { + mHorizontalWallpaperOffset = mFinalHorizontalWallpaperOffset; + mVerticalWallpaperOffset = mFinalVerticalWallpaperOffset; + } else { + float percentToCatchUpVertical = + Math.min(1.0f, timeSinceLastUpdate * fractionToCatchUpIn1MsVertical); + float percentToCatchUpHorizontal = + Math.min(1.0f, timeSinceLastUpdate * fractionToCatchUpIn1MsHorizontal); + mHorizontalWallpaperOffset += percentToCatchUpHorizontal * hOffsetDelta; + mVerticalWallpaperOffset += percentToCatchUpVertical * vOffsetDelta; + } + + mLastWallpaperOffsetUpdateTime = System.currentTimeMillis(); + return true; + } + + public float getCurrX() { + return mHorizontalWallpaperOffset; + } + + public float getFinalX() { + return mFinalHorizontalWallpaperOffset; + } + + public float getCurrY() { + return mVerticalWallpaperOffset; + } + + public float getFinalY() { + return mFinalVerticalWallpaperOffset; + } + + public void setFinalX(float x) { + mFinalHorizontalWallpaperOffset = Math.max(0f, Math.min(x, 1.0f)); + } + + public void setFinalY(float y) { + mFinalVerticalWallpaperOffset = Math.max(0f, Math.min(y, 1.0f)); + } + + public void jumpToFinal() { + mHorizontalWallpaperOffset = mFinalHorizontalWallpaperOffset; + mVerticalWallpaperOffset = mFinalVerticalWallpaperOffset; + } + } + + @Override + public void computeScroll() { + super.computeScroll(); + syncWallpaperOffsetWithScroll(); + } + + void showOutlines() { + if (!isSmall() && !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 (!isSmall() && !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; + } + + void disableBackground() { + mDrawBackground = false; + } + void enableBackground() { + mDrawBackground = true; + } + + private void animateBackgroundGradient(float finalAlpha, boolean animated) { + if (mBackground == null) return; + if (mBackgroundFadeInAnimation != null) { + mBackgroundFadeInAnimation.cancel(); + mBackgroundFadeInAnimation = null; + } + if (mBackgroundFadeOutAnimation != null) { + mBackgroundFadeOutAnimation.cancel(); + mBackgroundFadeOutAnimation = null; + } + float startAlpha = getBackgroundAlpha(); + if (finalAlpha != startAlpha) { + if (animated) { + mBackgroundFadeOutAnimation = + LauncherAnimUtils.ofFloat(this, startAlpha, finalAlpha); + mBackgroundFadeOutAnimation.addUpdateListener(new AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + setBackgroundAlpha(((Float) animation.getAnimatedValue()).floatValue()); + } + }); + mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f)); + mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION); + mBackgroundFadeOutAnimation.start(); + } else { + setBackgroundAlpha(finalAlpha); + } + } + } + + public void setBackgroundAlpha(float alpha) { + if (alpha != mBackgroundAlpha) { + mBackgroundAlpha = alpha; + invalidate(); + } + } + + public float getBackgroundAlpha() { + return mBackgroundAlpha; + } + + float backgroundAlphaInterpolator(float r) { + float pivotA = 0.1f; + float pivotB = 0.4f; + if (r < pivotA) { + return 0; + } else if (r > pivotB) { + return 1.0f; + } else { + return (r - pivotA)/(pivotB - pivotA); + } + } + + private void updatePageAlphaValues(int screenCenter) { + boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; + if (mWorkspaceFadeInAdjacentScreens && + mState == State.NORMAL && + !mIsSwitchingState && + !isInOverscroll) { + for (int i = 0; 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); + if (!mIsDragOccuring) { + child.setBackgroundAlphaMultiplier( + backgroundAlphaInterpolator(Math.abs(scrollProgress))); + } else { + child.setBackgroundAlphaMultiplier(1f); + } + } + } + } + } + + private void setChildrenBackgroundAlphaMultipliers(float a) { + for (int i = 0; i < getChildCount(); i++) { + CellLayout child = (CellLayout) getChildAt(i); + child.setBackgroundAlphaMultiplier(a); + } + } + + @Override + protected void screenScrolled(int screenCenter) { + final boolean isRtl = isLayoutRtl(); + super.screenScrolled(screenCenter); + + updatePageAlphaValues(screenCenter); + enableHwLayersOnVisiblePages(); + + if (mOverScrollX < 0 || mOverScrollX > mMaxScrollX) { + int index = 0; + float pivotX = 0f; + final float leftBiasedPivot = 0.25f; + final float rightBiasedPivot = 0.75f; + final int lowerIndex = 0; + final int upperIndex = getChildCount() - 1; + if (isRtl) { + index = mOverScrollX < 0 ? upperIndex : lowerIndex; + pivotX = (index == 0 ? leftBiasedPivot : rightBiasedPivot); + } else { + index = mOverScrollX < 0 ? lowerIndex : upperIndex; + pivotX = (index == 0 ? rightBiasedPivot : leftBiasedPivot); + } + + CellLayout cl = (CellLayout) getChildAt(index); + float scrollProgress = getScrollProgress(screenCenter, cl, index); + final boolean isLeftPage = (isRtl ? index > 0 : index == 0); + cl.setOverScrollAmount(Math.abs(scrollProgress), isLeftPage); + float rotation = -WORKSPACE_OVERSCROLL_ROTATION * scrollProgress; + cl.setRotationY(rotation); + setFadeForOverScroll(Math.abs(scrollProgress)); + if (!mOverscrollTransformsSet) { + mOverscrollTransformsSet = true; + cl.setCameraDistance(mDensity * mCameraDistance); + cl.setPivotX(cl.getMeasuredWidth() * pivotX); + cl.setPivotY(cl.getMeasuredHeight() * 0.5f); + cl.setOverscrollTransformsDirty(true); + } + } else { + if (mOverscrollFade != 0) { + setFadeForOverScroll(0); + } + if (mOverscrollTransformsSet) { + mOverscrollTransformsSet = false; + ((CellLayout) getChildAt(0)).resetOverscrollTransforms(); + ((CellLayout) getChildAt(getChildCount() - 1)).resetOverscrollTransforms(); + } + } + } + + @Override + protected void overScroll(float amount) { + acceleratedOverScroll(amount); + } + + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mWindowToken = getWindowToken(); + computeScroll(); + mDragController.setWindowToken(mWindowToken); + } + + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mWindowToken = null; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + mUpdateWallpaperOffsetImmediately = true; + } + super.onLayout(changed, left, top, right, bottom); + } + + @Override + protected void onDraw(Canvas canvas) { + updateWallpaperOffsets(); + + // Draw the background gradient if necessary + if (mBackground != null && mBackgroundAlpha > 0.0f && mDrawBackground) { + int alpha = (int) (mBackgroundAlpha * 255); + mBackground.setAlpha(alpha); + mBackground.setBounds(getScrollX(), 0, getScrollX() + getMeasuredWidth(), + getMeasuredHeight()); + mBackground.draw(canvas); + } + + super.onDraw(canvas); + + // Call back to LauncherModel to finish binding after the first draw + post(mBindPages); + } + + boolean isDrawingBackgroundGradient() { + return (mBackground != null && mBackgroundAlpha > 0.0f && mDrawBackground); + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + if (!mLauncher.isAllAppsVisible()) { + final Folder openFolder = getOpenFolder(); + if (openFolder != null) { + return openFolder.requestFocus(direction, previouslyFocusedRect); + } else { + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); + } + } + return false; + } + + @Override + public int getDescendantFocusability() { + if (isSmall()) { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } + return super.getDescendantFocusability(); + } + + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + if (!mLauncher.isAllAppsVisible()) { + final Folder openFolder = getOpenFolder(); + if (openFolder != null) { + openFolder.addFocusables(views, direction); + } else { + super.addFocusables(views, direction, focusableMode); + } + } + } + + public boolean isSmall() { + return mState == State.SMALL || mState == State.SPRING_LOADED; + } + + void enableChildrenCache(int fromPage, int toPage) { + if (fromPage > toPage) { + final int temp = fromPage; + fromPage = toPage; + toPage = temp; + } + + final int screenCount = getChildCount(); + + fromPage = Math.max(fromPage, 0); + toPage = Math.min(toPage, screenCount - 1); + + for (int i = fromPage; i <= toPage; i++) { + final CellLayout layout = (CellLayout) getChildAt(i); + layout.setChildrenDrawnWithCacheEnabled(true); + layout.setChildrenDrawingCacheEnabled(true); + } + } + + void clearChildrenCache() { + final int screenCount = getChildCount(); + for (int i = 0; i < screenCount; i++) { + final CellLayout layout = (CellLayout) getChildAt(i); + layout.setChildrenDrawnWithCacheEnabled(false); + // In software mode, we don't want the items to continue to be drawn into bitmaps + if (!isHardwareAccelerated()) { + layout.setChildrenDrawingCacheEnabled(false); + } + } + } + + + private void updateChildrenLayersEnabled(boolean force) { + boolean small = mState == State.SMALL || mIsSwitchingState; + boolean enableChildrenLayers = force || small || mAnimatingViewIntoPlace || isPageMoving(); + + if (enableChildrenLayers != mChildrenLayersEnabled) { + mChildrenLayersEnabled = enableChildrenLayers; + if (mChildrenLayersEnabled) { + enableHwLayersOnVisiblePages(); + } else { + for (int i = 0; i < getPageCount(); i++) { + final CellLayout cl = (CellLayout) getChildAt(i); + cl.disableHardwareLayers(); + } + } + } + } + + private void enableHwLayersOnVisiblePages() { + if (mChildrenLayersEnabled) { + final int screenCount = getChildCount(); + getVisiblePages(mTempVisiblePagesRange); + int leftScreen = mTempVisiblePagesRange[0]; + int rightScreen = mTempVisiblePagesRange[1]; + if (leftScreen == rightScreen) { + // make sure we're caching at least two pages always + if (rightScreen < screenCount - 1) { + rightScreen++; + } else if (leftScreen > 0) { + leftScreen--; + } + } + for (int i = 0; i < screenCount; i++) { + final CellLayout layout = (CellLayout) getPageAt(i); + if (!(leftScreen <= i && i <= rightScreen && shouldDrawChild(layout))) { + layout.disableHardwareLayers(); + } + } + for (int i = 0; i < screenCount; i++) { + final CellLayout layout = (CellLayout) getPageAt(i); + if (leftScreen <= i && i <= rightScreen && shouldDrawChild(layout)) { + layout.enableHardwareLayers(); + } + } + } + } + + public void buildPageHardwareLayers() { + // force layers to be enabled just for the call to buildLayer + updateChildrenLayersEnabled(true); + if (getWindowToken() != null) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + cl.buildHardwareLayer(); + } + } + updateChildrenLayersEnabled(false); + } + + protected void onWallpaperTap(MotionEvent ev) { + final int[] position = mTempCell; + getLocationOnScreen(position); + + int pointerIndex = ev.getActionIndex(); + position[0] += (int) ev.getX(pointerIndex); + position[1] += (int) ev.getY(pointerIndex); + + mWallpaperManager.sendWallpaperCommand(getWindowToken(), + ev.getAction() == MotionEvent.ACTION_UP + ? WallpaperManager.COMMAND_TAP : WallpaperManager.COMMAND_SECONDARY_TAP, + 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 + * start a drag in Launcher, regardless of whether the drag has ever entered the Workspace + * + * These methods mark the appropriate pages as accepting drops (which alters their visual + * appearance). + * + */ + public void onDragStartedWithItem(View v) { + final Canvas canvas = new Canvas(); + + // The outline is used to visualize where the item will land if dropped + mDragOutline = createDragOutline(v, canvas, DRAG_BITMAP_PADDING); + } + + public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, boolean clipAlpha) { + final Canvas canvas = new Canvas(); + + int[] size = estimateItemSize(info.spanX, info.spanY, info, false); + + // The outline is used to visualize where the item will land if dropped + mDragOutline = createDragOutline(b, canvas, DRAG_BITMAP_PADDING, size[0], + size[1], clipAlpha); + } + + public void exitWidgetResizeMode() { + DragLayer dragLayer = mLauncher.getDragLayer(); + dragLayer.clearAllResizeFrames(); + } + + private void initAnimationArrays() { + final int childCount = getChildCount(); + if (mOldTranslationXs != null) return; + mOldTranslationXs = new float[childCount]; + mOldTranslationYs = new float[childCount]; + mOldScaleXs = new float[childCount]; + mOldScaleYs = new float[childCount]; + mOldBackgroundAlphas = new float[childCount]; + mOldAlphas = new float[childCount]; + mNewTranslationXs = new float[childCount]; + mNewTranslationYs = new float[childCount]; + mNewScaleXs = new float[childCount]; + mNewScaleYs = new float[childCount]; + mNewBackgroundAlphas = new float[childCount]; + mNewAlphas = new float[childCount]; + mNewRotationYs = new float[childCount]; + } + + Animator getChangeStateAnimation(final State state, boolean animated) { + return getChangeStateAnimation(state, animated, 0); + } + + Animator getChangeStateAnimation(final State state, boolean animated, int delay) { + if (mState == state) { + return null; + } + + // Initialize animation arrays for the first time if necessary + initAnimationArrays(); + + AnimatorSet anim = animated ? LauncherAnimUtils.createAnimatorSet() : null; + + // Stop any scrolling, move to the current page right away + setCurrentPage(getNextPage()); + + final State oldState = mState; + final boolean oldStateIsNormal = (oldState == State.NORMAL); + final boolean oldStateIsSpringLoaded = (oldState == State.SPRING_LOADED); + final boolean oldStateIsSmall = (oldState == State.SMALL); + mState = state; + final boolean stateIsNormal = (state == State.NORMAL); + final boolean stateIsSpringLoaded = (state == State.SPRING_LOADED); + final boolean stateIsSmall = (state == State.SMALL); + float finalScaleFactor = 1.0f; + float finalBackgroundAlpha = stateIsSpringLoaded ? 1.0f : 0f; + float translationX = 0; + float translationY = 0; + boolean zoomIn = true; + + if (state != State.NORMAL) { + finalScaleFactor = mSpringLoadedShrinkFactor - (stateIsSmall ? 0.1f : 0); + setPageSpacing(mSpringLoadedPageSpacing); + if (oldStateIsNormal && stateIsSmall) { + zoomIn = false; + setLayoutScale(finalScaleFactor); + updateChildrenLayersEnabled(false); + } else { + finalBackgroundAlpha = 1.0f; + setLayoutScale(finalScaleFactor); + } + } else { + setPageSpacing(mOriginalPageSpacing); + setLayoutScale(1.0f); + } + + final int duration = zoomIn ? + getResources().getInteger(R.integer.config_workspaceUnshrinkTime) : + getResources().getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime); + for (int i = 0; i < getChildCount(); i++) { + final CellLayout cl = (CellLayout) getChildAt(i); + float finalAlpha = (!mWorkspaceFadeInAdjacentScreens || stateIsSpringLoaded || + (i == mCurrentPage)) ? 1f : 0f; + float currentAlpha = cl.getShortcutsAndWidgets().getAlpha(); + float initialAlpha = currentAlpha; + + // Determine the pages alpha during the state transition + if ((oldStateIsSmall && stateIsNormal) || + (oldStateIsNormal && stateIsSmall)) { + // To/from workspace - only show the current page unless the transition is not + // animated and the animation end callback below doesn't run; + // or, if we're in spring-loaded mode + if (i == mCurrentPage || !animated || oldStateIsSpringLoaded) { + finalAlpha = 1f; + } else { + initialAlpha = 0f; + finalAlpha = 0f; + } + } + + mOldAlphas[i] = initialAlpha; + mNewAlphas[i] = finalAlpha; + if (animated) { + mOldTranslationXs[i] = cl.getTranslationX(); + mOldTranslationYs[i] = cl.getTranslationY(); + mOldScaleXs[i] = cl.getScaleX(); + mOldScaleYs[i] = cl.getScaleY(); + mOldBackgroundAlphas[i] = cl.getBackgroundAlpha(); + + mNewTranslationXs[i] = translationX; + mNewTranslationYs[i] = translationY; + mNewScaleXs[i] = finalScaleFactor; + mNewScaleYs[i] = finalScaleFactor; + mNewBackgroundAlphas[i] = finalBackgroundAlpha; + } else { + cl.setTranslationX(translationX); + cl.setTranslationY(translationY); + cl.setScaleX(finalScaleFactor); + cl.setScaleY(finalScaleFactor); + cl.setBackgroundAlpha(finalBackgroundAlpha); + cl.setShortcutAndWidgetAlpha(finalAlpha); + } + } + + if (animated) { + 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.setTranslationX(mNewTranslationXs[i]); + cl.setTranslationY(mNewTranslationYs[i]); + cl.setScaleX(mNewScaleXs[i]); + cl.setScaleY(mNewScaleYs[i]); + cl.setBackgroundAlpha(mNewBackgroundAlphas[i]); + cl.setShortcutAndWidgetAlpha(mNewAlphas[i]); + cl.setRotationY(mNewRotationYs[i]); + } else { + LauncherViewPropertyAnimator a = new LauncherViewPropertyAnimator(cl); + a.translationX(mNewTranslationXs[i]) + .translationY(mNewTranslationYs[i]) + .scaleX(mNewScaleXs[i]) + .scaleY(mNewScaleYs[i]) + .setDuration(duration) + .setInterpolator(mZoomInInterpolator); + anim.play(a); + + 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).setDuration(duration); + bgAnim.setInterpolator(mZoomInInterpolator); + bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() { + public void onAnimationUpdate(float a, float b) { + cl.setBackgroundAlpha( + a * mOldBackgroundAlphas[i] + + b * mNewBackgroundAlphas[i]); + } + }); + anim.play(bgAnim); + } + } + } + anim.setStartDelay(delay); + } + + if (stateIsSpringLoaded) { + // Right now we're covered by Apps Customize + // Show the background gradient immediately, so the gradient will + // be showing once AppsCustomize disappears + animateBackgroundGradient(getResources().getInteger( + R.integer.config_appsCustomizeSpringLoadedBgAlpha) / 100f, false); + } else { + // Fade the background gradient away + animateBackgroundGradient(0f, true); + } + return anim; + } + + @Override + public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { + mIsSwitchingState = true; + updateChildrenLayersEnabled(false); + cancelScrollingIndicatorAnimations(); + } + + @Override + public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { + } + + @Override + public void onLauncherTransitionStep(Launcher l, float t) { + mTransitionProgress = t; + } + + @Override + public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { + mIsSwitchingState = false; + mWallpaperOffset.setOverrideHorizontalCatchupConstant(false); + updateChildrenLayersEnabled(false); + // The code in getChangeStateAnimation to determine initialAlpha and finalAlpha will ensure + // ensure that only the current page is visible during (and subsequently, after) the + // transition animation. If fade adjacent pages is disabled, then re-enable the page + // visibility after the transition animation. + if (!mWorkspaceFadeInAdjacentScreens) { + for (int i = 0; i < getChildCount(); i++) { + final CellLayout cl = (CellLayout) getChildAt(i); + cl.setShortcutAndWidgetAlpha(1f); + } + } + } + + @Override + public View getContent() { + return this; + } + + /** + * Draw the View v into the given Canvas. + * + * @param v the view to draw + * @param destCanvas the canvas to draw on + * @param padding the horizontal and vertical padding to use when drawing + */ + private void drawDragView(View v, Canvas destCanvas, int padding, boolean pruneToDrawable) { + final Rect clipRect = mTempRect; + v.getDrawingRect(clipRect); + + boolean textVisible = false; + + destCanvas.save(); + if (v instanceof TextView && pruneToDrawable) { + Drawable d = ((TextView) v).getCompoundDrawables()[1]; + clipRect.set(0, 0, d.getIntrinsicWidth() + padding, d.getIntrinsicHeight() + padding); + destCanvas.translate(padding / 2, padding / 2); + d.draw(destCanvas); + } else { + if (v instanceof FolderIcon) { + // For FolderIcons the text can bleed into the icon area, and so we need to + // hide the text completely (which can't be achieved by clipping). + if (((FolderIcon) v).getTextVisible()) { + ((FolderIcon) v).setTextVisible(false); + textVisible = true; + } + } else if (v instanceof BubbleTextView) { + final BubbleTextView tv = (BubbleTextView) v; + clipRect.bottom = tv.getExtendedPaddingTop() - (int) BubbleTextView.PADDING_V + + tv.getLayout().getLineTop(0); + } else if (v instanceof TextView) { + final TextView tv = (TextView) v; + clipRect.bottom = tv.getExtendedPaddingTop() - tv.getCompoundDrawablePadding() + + tv.getLayout().getLineTop(0); + } + destCanvas.translate(-v.getScrollX() + padding / 2, -v.getScrollY() + padding / 2); + destCanvas.clipRect(clipRect); + v.draw(destCanvas); + + // Restore text visibility of FolderIcon if necessary + if (textVisible) { + ((FolderIcon) v).setTextVisible(true); + } + } + destCanvas.restore(); + } + + /** + * Returns a new bitmap to show when the given View is being dragged around. + * Responsibility for the bitmap is transferred to the caller. + */ + public Bitmap createDragBitmap(View v, Canvas canvas, int padding) { + Bitmap b; + + if (v instanceof TextView) { + Drawable d = ((TextView) v).getCompoundDrawables()[1]; + b = Bitmap.createBitmap(d.getIntrinsicWidth() + padding, + d.getIntrinsicHeight() + padding, Bitmap.Config.ARGB_8888); + } else { + b = Bitmap.createBitmap( + v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888); + } + + canvas.setBitmap(b); + drawDragView(v, canvas, padding, true); + canvas.setBitmap(null); + + return b; + } + + /** + * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createDragOutline(View v, Canvas canvas, int padding) { + final int outlineColor = getResources().getColor(android.R.color.white); + final Bitmap b = Bitmap.createBitmap( + v.getWidth() + padding, v.getHeight() + padding, Bitmap.Config.ARGB_8888); + + canvas.setBitmap(b); + drawDragView(v, canvas, padding, true); + mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor); + canvas.setBitmap(null); + return b; + } + + /** + * Returns a new bitmap to be used as the object outline, e.g. to visualize the drop location. + * Responsibility for the bitmap is transferred to the caller. + */ + private Bitmap createDragOutline(Bitmap orig, Canvas canvas, int padding, int w, int h, + boolean clipAlpha) { + final int outlineColor = getResources().getColor(android.R.color.white); + final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + canvas.setBitmap(b); + + Rect src = new Rect(0, 0, orig.getWidth(), orig.getHeight()); + float scaleFactor = Math.min((w - padding) / (float) orig.getWidth(), + (h - padding) / (float) orig.getHeight()); + int scaledWidth = (int) (scaleFactor * orig.getWidth()); + int scaledHeight = (int) (scaleFactor * orig.getHeight()); + Rect dst = new Rect(0, 0, scaledWidth, scaledHeight); + + // center the image + dst.offset((w - scaledWidth) / 2, (h - scaledHeight) / 2); + + canvas.drawBitmap(orig, src, dst, null); + mOutlineHelper.applyMediumExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor, + clipAlpha); + canvas.setBitmap(null); + + return b; + } + + void startDrag(CellLayout.CellInfo cellInfo) { + View child = cellInfo.cell; + + // Make sure the drag was started by a long press as opposed to a long click. + if (!child.isInTouchMode()) { + return; + } + + mDragInfo = cellInfo; + child.setVisibility(INVISIBLE); + CellLayout layout = (CellLayout) child.getParent().getParent(); + layout.prepareChildForDrag(child); + + child.clearFocus(); + child.setPressed(false); + + final Canvas canvas = new Canvas(); + + // The outline is used to visualize where the item will land if dropped + mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING); + beginDragShared(child, this); + } + + public void beginDragShared(View child, DragSource source) { + Resources r = getResources(); + + // The drag bitmap follows the touch point around on the screen + final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING); + + final int bmpWidth = b.getWidth(); + final int bmpHeight = b.getHeight(); + + float scale = mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY); + int dragLayerX = + Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2); + int dragLayerY = + Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 + - DRAG_BITMAP_PADDING / 2); + + Point dragVisualizeOffset = null; + Rect dragRect = null; + if (child instanceof BubbleTextView || child instanceof PagedViewIcon) { + int iconSize = r.getDimensionPixelSize(R.dimen.app_icon_size); + int iconPaddingTop = r.getDimensionPixelSize(R.dimen.app_icon_padding_top); + int top = child.getPaddingTop(); + int left = (bmpWidth - iconSize) / 2; + int right = left + iconSize; + int bottom = top + iconSize; + 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. + dragVisualizeOffset = new Point(-DRAG_BITMAP_PADDING / 2, + iconPaddingTop - DRAG_BITMAP_PADDING / 2); + dragRect = new Rect(left, top, right, bottom); + } else if (child instanceof FolderIcon) { + int previewSize = r.getDimensionPixelSize(R.dimen.folder_preview_size); + dragRect = new Rect(0, 0, child.getWidth(), previewSize); + } + + // Clear the pressed state if necessary + if (child instanceof BubbleTextView) { + BubbleTextView icon = (BubbleTextView) child; + icon.clearPressedOrFocusedBackground(); + } + + mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); + b.recycle(); + + // Show the scrolling indicator when you pick up an item + showScrollingIndicator(false); + } + + void addApplicationShortcut(ShortcutInfo info, CellLayout target, long container, int screen, + 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, screen, cellXY[0], cellXY[1], 1, 1, insertAtFirst); + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screen, cellXY[0], + cellXY[1]); + } + + public boolean transitionStateShouldAllowDrop() { + return ((!isSwitchingState() || mTransitionProgress > 0.5f) && mState != State.SMALL); + } + + /** + * {@inheritDoc} + */ + public boolean acceptDrop(DragObject d) { + // If it's an external drop (e.g. from All Apps), check if it should be accepted + CellLayout dropTargetLayout = mDropToLayout; + if (d.dragSource != this) { + // Don't accept the drop if we're not over a screen at time of drop + if (dropTargetLayout == null) { + return false; + } + if (!transitionStateShouldAllowDrop()) return false; + + mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, + d.dragView, mDragViewVisualCenter); + + // We want the point to be mapped to the dragTarget. + if (mLauncher.isHotseatLayout(dropTargetLayout)) { + mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter); + } else { + mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null); + } + + int spanX = 1; + int spanY = 1; + if (mDragInfo != null) { + final CellLayout.CellInfo dragCellInfo = mDragInfo; + spanX = dragCellInfo.spanX; + spanY = dragCellInfo.spanY; + } else { + final ItemInfo dragInfo = (ItemInfo) d.dragInfo; + spanX = dragInfo.spanX; + spanY = dragInfo.spanY; + } + + int minSpanX = spanX; + int minSpanY = spanY; + if (d.dragInfo instanceof PendingAddWidgetInfo) { + minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX; + minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY; + } + + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, dropTargetLayout, + mTargetCell); + float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + if (willCreateUserFolder((ItemInfo) d.dragInfo, dropTargetLayout, + mTargetCell, distance, true)) { + return true; + } + if (willAddToExistingUserFolder((ItemInfo) d.dragInfo, dropTargetLayout, + mTargetCell, distance)) { + return true; + } + + int[] resultSpan = new int[2]; + mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, + null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP); + boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0; + + // Don't accept the drop if there's no room for the item + if (!foundCell) { + // Don't show the message if we are dropping on the AllApps button and the hotseat + // is full + boolean isHotseat = mLauncher.isHotseatLayout(dropTargetLayout); + if (mTargetCell != null && isHotseat) { + Hotseat hotseat = mLauncher.getHotseat(); + if (hotseat.isAllAppsButtonRank( + hotseat.getOrderInHotseat(mTargetCell[0], mTargetCell[1]))) { + return false; + } + } + + mLauncher.showOutOfSpaceMessage(isHotseat); + return false; + } + } + return true; + } + + boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, float + distance, boolean considerTimeout) { + if (distance > mMaxDistanceForFolderCreation) return false; + View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + + if (dropOverView != null) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams(); + if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) { + return false; + } + } + + boolean hasntMoved = false; + if (mDragInfo != null) { + hasntMoved = dropOverView == mDragInfo.cell; + } + + if (dropOverView == null || hasntMoved || (considerTimeout && !mCreateUserFolderOnDrop)) { + return false; + } + + boolean aboveShortcut = (dropOverView.getTag() instanceof ShortcutInfo); + boolean willBecomeShortcut = + (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || + info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT); + + return (aboveShortcut && willBecomeShortcut); + } + + boolean willAddToExistingUserFolder(Object dragInfo, CellLayout target, int[] targetCell, + float distance) { + if (distance > mMaxDistanceForFolderCreation) return false; + View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + + if (dropOverView != null) { + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams(); + if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) { + return false; + } + } + + if (dropOverView instanceof FolderIcon) { + FolderIcon fi = (FolderIcon) dropOverView; + if (fi.acceptDrop(dragInfo)) { + return true; + } + } + return false; + } + + boolean createUserFolderIfNecessary(View newView, long container, CellLayout target, + int[] targetCell, float distance, boolean external, DragView dragView, + Runnable postAnimationRunnable) { + if (distance > mMaxDistanceForFolderCreation) return false; + View v = target.getChildAt(targetCell[0], targetCell[1]); + + boolean hasntMoved = false; + if (mDragInfo != null) { + CellLayout cellParent = getParentCellLayoutForView(mDragInfo.cell); + hasntMoved = (mDragInfo.cellX == targetCell[0] && + mDragInfo.cellY == targetCell[1]) && (cellParent == target); + } + + if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false; + mCreateUserFolderOnDrop = false; + final int screen = (targetCell == null) ? mDragInfo.screen : indexOfChild(target); + + boolean aboveShortcut = (v.getTag() instanceof ShortcutInfo); + boolean willBecomeShortcut = (newView.getTag() instanceof ShortcutInfo); + + if (aboveShortcut && willBecomeShortcut) { + ShortcutInfo sourceInfo = (ShortcutInfo) newView.getTag(); + ShortcutInfo destInfo = (ShortcutInfo) v.getTag(); + // if the drag started here, we need to remove it from the workspace + if (!external) { + getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell); + } + + Rect folderLocation = new Rect(); + float scale = mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v, folderLocation); + target.removeView(v); + + FolderIcon fi = + mLauncher.addFolder(target, container, screen, targetCell[0], targetCell[1]); + destInfo.cellX = -1; + destInfo.cellY = -1; + sourceInfo.cellX = -1; + sourceInfo.cellY = -1; + + // If the dragView is null, we can't animate + boolean animate = dragView != null; + if (animate) { + fi.performCreateAnimation(destInfo, v, sourceInfo, dragView, folderLocation, scale, + postAnimationRunnable); + } else { + fi.addItem(destInfo); + fi.addItem(sourceInfo); + } + return true; + } + return false; + } + + boolean addToExistingFolderIfNecessary(View newView, CellLayout target, int[] targetCell, + float distance, DragObject d, boolean external) { + if (distance > mMaxDistanceForFolderCreation) return false; + + View dropOverView = target.getChildAt(targetCell[0], targetCell[1]); + if (!mAddToExistingFolderOnDrop) return false; + mAddToExistingFolderOnDrop = false; + + if (dropOverView instanceof FolderIcon) { + FolderIcon fi = (FolderIcon) dropOverView; + if (fi.acceptDrop(d.dragInfo)) { + fi.onDrop(d); + + // if the drag started here, we need to remove it from the workspace + if (!external) { + getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell); + } + return true; + } + } + return false; + } + + public void onDrop(final DragObject d) { + mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, + mDragViewVisualCenter); + + CellLayout dropTargetLayout = mDropToLayout; + + // We want the point to be mapped to the dragTarget. + if (dropTargetLayout != null) { + if (mLauncher.isHotseatLayout(dropTargetLayout)) { + mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter); + } else { + mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null); + } + } + + int snapScreen = -1; + boolean resizeOnDrop = false; + if (d.dragSource != this) { + final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1] }; + onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d); + } else if (mDragInfo != null) { + final View cell = mDragInfo.cell; + + Runnable resizeRunnable = null; + if (dropTargetLayout != null) { + // Move internally + boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout); + boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout); + long container = hasMovedIntoHotseat ? + LauncherSettings.Favorites.CONTAINER_HOTSEAT : + LauncherSettings.Favorites.CONTAINER_DESKTOP; + int screen = (mTargetCell[0] < 0) ? + mDragInfo.screen : indexOfChild(dropTargetLayout); + int spanX = mDragInfo != null ? mDragInfo.spanX : 1; + int spanY = mDragInfo != null ? mDragInfo.spanY : 1; + // First we find the cell nearest to point at which the item is + // dropped, without any consideration to whether there is an item there. + + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int) + mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell); + float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + + // If the item being dropped is a shortcut and the nearest drop + // cell also contains a shortcut, then create a folder with the two shortcuts. + if (!mInScrollArea && createUserFolderIfNecessary(cell, container, + dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) { + return; + } + + if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell, + distance, d, false)) { + return; + } + + // Aside from the special case where we're dropping a shortcut onto a shortcut, + // we need to find the nearest cell location that is vacant + ItemInfo item = (ItemInfo) d.dragInfo; + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; + } + + int[] resultSpan = new int[2]; + mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell, + mTargetCell, resultSpan, CellLayout.MODE_ON_DROP); + + boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0; + + // if the widget resizes on drop + if (foundCell && (cell instanceof AppWidgetHostView) && + (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) { + resizeOnDrop = true; + item.spanX = resultSpan[0]; + item.spanY = resultSpan[1]; + AppWidgetHostView awhv = (AppWidgetHostView) cell; + AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0], + resultSpan[1]); + } + + if (mCurrentPage != screen && !hasMovedIntoHotseat) { + snapScreen = screen; + snapToPage(screen); + } + + if (foundCell) { + final ItemInfo info = (ItemInfo) cell.getTag(); + if (hasMovedLayouts) { + // Reparent the view + getParentCellLayoutForView(cell).removeView(cell); + addInScreen(cell, container, screen, mTargetCell[0], mTargetCell[1], + info.spanX, info.spanY); + } + + // update the item's position after drop + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams(); + lp.cellX = lp.tmpCellX = mTargetCell[0]; + lp.cellY = lp.tmpCellY = mTargetCell[1]; + lp.cellHSpan = item.spanX; + lp.cellVSpan = item.spanY; + lp.isLockedToGrid = true; + cell.setId(LauncherModel.getCellLayoutChildId(container, mDragInfo.screen, + mTargetCell[0], mTargetCell[1], mDragInfo.spanX, mDragInfo.spanY)); + + if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT && + cell instanceof LauncherAppWidgetHostView) { + final CellLayout cellLayout = dropTargetLayout; + // We post this call so that the widget has a chance to be placed + // in its final location + + final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell; + AppWidgetProviderInfo pinfo = hostView.getAppWidgetInfo(); + if (pinfo != null && + pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) { + final Runnable addResizeFrame = new Runnable() { + public void run() { + DragLayer dragLayer = mLauncher.getDragLayer(); + dragLayer.addResizeFrame(info, hostView, cellLayout); + } + }; + resizeRunnable = (new Runnable() { + public void run() { + if (!isPageMoving()) { + addResizeFrame.run(); + } else { + mDelayedResizeRunnable = addResizeFrame; + } + } + }); + } + } + + LauncherModel.moveItemInDatabase(mLauncher, info, container, screen, lp.cellX, + lp.cellY); + } else { + // If we can't find a drop location, we return the item to its original position + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams(); + mTargetCell[0] = lp.cellX; + mTargetCell[1] = lp.cellY; + CellLayout layout = (CellLayout) cell.getParent().getParent(); + layout.markCellsAsOccupiedForView(cell); + } + } + + final CellLayout parent = (CellLayout) cell.getParent().getParent(); + final Runnable finalResizeRunnable = resizeRunnable; + // Prepare it to be animated into its new position + // This must be called after the view has been re-parented + final Runnable onCompleteRunnable = new Runnable() { + @Override + public void run() { + mAnimatingViewIntoPlace = false; + updateChildrenLayersEnabled(false); + if (finalResizeRunnable != null) { + finalResizeRunnable.run(); + } + } + }; + mAnimatingViewIntoPlace = true; + if (d.dragView.hasDrawn()) { + final ItemInfo info = (ItemInfo) cell.getTag(); + if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) { + 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; + mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration, + onCompleteRunnable, this); + } + } else { + d.deferDragViewCleanupPostAnimation = false; + cell.setVisibility(VISIBLE); + } + parent.onDropChild(cell); + } + } + + public void setFinalScrollForPageChange(int screen) { + if (screen >= 0) { + mSavedScrollX = getScrollX(); + CellLayout cl = (CellLayout) getChildAt(screen); + mSavedTranslationX = cl.getTranslationX(); + mSavedRotationY = cl.getRotationY(); + final int newX = getChildOffset(screen) - getRelativeChildOffset(screen); + setScrollX(newX); + cl.setTranslationX(0f); + cl.setRotationY(0f); + } + } + + public void resetFinalScrollForPageChange(int screen) { + if (screen >= 0) { + CellLayout cl = (CellLayout) getChildAt(screen); + setScrollX(mSavedScrollX); + cl.setTranslationX(mSavedTranslationX); + cl.setRotationY(mSavedRotationY); + } + } + + public void getViewLocationRelativeToSelf(View v, int[] location) { + getLocationInWindow(location); + int x = location[0]; + int y = location[1]; + + v.getLocationInWindow(location); + int vX = location[0]; + int vY = location[1]; + + location[0] = vX - x; + location[1] = vY - y; + } + + public void onDragEnter(DragObject d) { + mDragEnforcer.onDragEnter(); + mCreateUserFolderOnDrop = false; + mAddToExistingFolderOnDrop = false; + + mDropToLayout = null; + CellLayout layout = getCurrentDropLayout(); + setCurrentDropLayout(layout); + setCurrentDragOverlappingLayout(layout); + + // Because we don't have space in the Phone UI (the CellLayouts run to the edge) we + // don't need to show the outlines + if (LauncherApplication.isScreenLarge()) { + showOutlines(); + } + } + + static Rect getCellLayoutMetrics(Launcher launcher, int orientation) { + Resources res = launcher.getResources(); + Display display = launcher.getWindowManager().getDefaultDisplay(); + Point smallestSize = new Point(); + Point largestSize = new Point(); + display.getCurrentSizeRange(smallestSize, largestSize); + if (orientation == CellLayout.LANDSCAPE) { + if (mLandscapeCellLayoutMetrics == null) { + int paddingLeft = res.getDimensionPixelSize(R.dimen.workspace_left_padding_land); + int paddingRight = res.getDimensionPixelSize(R.dimen.workspace_right_padding_land); + int paddingTop = res.getDimensionPixelSize(R.dimen.workspace_top_padding_land); + int paddingBottom = res.getDimensionPixelSize(R.dimen.workspace_bottom_padding_land); + int width = largestSize.x - paddingLeft - paddingRight; + int height = smallestSize.y - paddingTop - paddingBottom; + mLandscapeCellLayoutMetrics = new Rect(); + CellLayout.getMetrics(mLandscapeCellLayoutMetrics, res, + width, height, LauncherModel.getCellCountX(), LauncherModel.getCellCountY(), + orientation); + } + return mLandscapeCellLayoutMetrics; + } else if (orientation == CellLayout.PORTRAIT) { + if (mPortraitCellLayoutMetrics == null) { + int paddingLeft = res.getDimensionPixelSize(R.dimen.workspace_left_padding_land); + int paddingRight = res.getDimensionPixelSize(R.dimen.workspace_right_padding_land); + int paddingTop = res.getDimensionPixelSize(R.dimen.workspace_top_padding_land); + int paddingBottom = res.getDimensionPixelSize(R.dimen.workspace_bottom_padding_land); + int width = smallestSize.x - paddingLeft - paddingRight; + int height = largestSize.y - paddingTop - paddingBottom; + mPortraitCellLayoutMetrics = new Rect(); + CellLayout.getMetrics(mPortraitCellLayoutMetrics, res, + width, height, LauncherModel.getCellCountX(), LauncherModel.getCellCountY(), + orientation); + } + return mPortraitCellLayoutMetrics; + } + return null; + } + + public void onDragExit(DragObject d) { + mDragEnforcer.onDragExit(); + + // Here we store the final page that will be dropped to, if the workspace in fact + // receives the drop + if (mInScrollArea) { + if (isPageMoving()) { + // If the user drops while the page is scrolling, we should use that page as the + // destination instead of the page that is being hovered over. + mDropToLayout = (CellLayout) getPageAt(getNextPage()); + } else { + mDropToLayout = mDragOverlappingLayout; + } + } else { + mDropToLayout = mDragTargetLayout; + } + + if (mDragMode == DRAG_MODE_CREATE_FOLDER) { + mCreateUserFolderOnDrop = true; + } else if (mDragMode == DRAG_MODE_ADD_TO_FOLDER) { + mAddToExistingFolderOnDrop = true; + } + + // Reset the scroll area and previous drag target + onResetScrollArea(); + setCurrentDropLayout(null); + setCurrentDragOverlappingLayout(null); + + mSpringLoadedDragController.cancel(); + + if (!mIsPageMoving) { + hideOutlines(); + } + } + + void setCurrentDropLayout(CellLayout layout) { + if (mDragTargetLayout != null) { + mDragTargetLayout.revertTempState(); + mDragTargetLayout.onDragExit(); + } + mDragTargetLayout = layout; + if (mDragTargetLayout != null) { + mDragTargetLayout.onDragEnter(); + } + cleanupReorder(true); + cleanupFolderCreation(); + setCurrentDropOverCell(-1, -1); + } + + void setCurrentDragOverlappingLayout(CellLayout layout) { + if (mDragOverlappingLayout != null) { + mDragOverlappingLayout.setIsDragOverlapping(false); + } + mDragOverlappingLayout = layout; + if (mDragOverlappingLayout != null) { + mDragOverlappingLayout.setIsDragOverlapping(true); + } + invalidate(); + } + + void setCurrentDropOverCell(int x, int y) { + if (x != mDragOverX || y != mDragOverY) { + mDragOverX = x; + mDragOverY = y; + setDragMode(DRAG_MODE_NONE); + } + } + + void setDragMode(int dragMode) { + if (dragMode != mDragMode) { + if (dragMode == DRAG_MODE_NONE) { + cleanupAddToFolder(); + // We don't want to cancel the re-order alarm every time the target cell changes + // as this feels to slow / unresponsive. + cleanupReorder(false); + cleanupFolderCreation(); + } else if (dragMode == DRAG_MODE_ADD_TO_FOLDER) { + cleanupReorder(true); + cleanupFolderCreation(); + } else if (dragMode == DRAG_MODE_CREATE_FOLDER) { + cleanupAddToFolder(); + cleanupReorder(true); + } else if (dragMode == DRAG_MODE_REORDER) { + cleanupAddToFolder(); + cleanupFolderCreation(); + } + mDragMode = dragMode; + } + } + + private void cleanupFolderCreation() { + if (mDragFolderRingAnimator != null) { + mDragFolderRingAnimator.animateToNaturalState(); + } + mFolderCreationAlarm.cancelAlarm(); + } + + private void cleanupAddToFolder() { + if (mDragOverFolderIcon != null) { + mDragOverFolderIcon.onDragExit(null); + mDragOverFolderIcon = null; + } + } + + private void cleanupReorder(boolean cancelAlarm) { + // Any pending reorders are canceled + if (cancelAlarm) { + mReorderAlarm.cancelAlarm(); + } + mLastReorderX = -1; + mLastReorderY = -1; + } + + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + /* + * + * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's + * coordinate space. The argument xy is modified with the return result. + * + */ + void mapPointFromSelfToChild(View v, float[] xy) { + mapPointFromSelfToChild(v, xy, null); + } + + /* + * + * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's + * coordinate space. The argument xy is modified with the return result. + * + * if cachedInverseMatrix is not null, this method will just use that matrix instead of + * computing it itself; we use this to avoid redundant matrix inversions in + * findMatchingPageForDragOver + * + */ + void mapPointFromSelfToChild(View v, float[] xy, Matrix cachedInverseMatrix) { + if (cachedInverseMatrix == null) { + v.getMatrix().invert(mTempInverseMatrix); + cachedInverseMatrix = mTempInverseMatrix; + } + int scrollX = getScrollX(); + if (mNextPage != INVALID_PAGE) { + scrollX = mScroller.getFinalX(); + } + xy[0] = xy[0] + scrollX - v.getLeft(); + xy[1] = xy[1] + getScrollY() - v.getTop(); + cachedInverseMatrix.mapPoints(xy); + } + + + void mapPointFromSelfToHotseatLayout(Hotseat hotseat, float[] xy) { + hotseat.getLayout().getMatrix().invert(mTempInverseMatrix); + xy[0] = xy[0] - hotseat.getLeft() - hotseat.getLayout().getLeft(); + xy[1] = xy[1] - hotseat.getTop() - hotseat.getLayout().getTop(); + mTempInverseMatrix.mapPoints(xy); + } + + /* + * + * Convert the 2D coordinate xy from this CellLayout's coordinate space to + * the parent View's coordinate space. The argument xy is modified with the return result. + * + */ + void mapPointFromChildToSelf(View v, float[] xy) { + v.getMatrix().mapPoints(xy); + int scrollX = getScrollX(); + if (mNextPage != INVALID_PAGE) { + scrollX = mScroller.getFinalX(); + } + xy[0] -= (scrollX - v.getLeft()); + xy[1] -= (getScrollY() - v.getTop()); + } + + static private float squaredDistance(float[] point1, float[] point2) { + float distanceX = point1[0] - point2[0]; + float distanceY = point2[1] - point2[1]; + return distanceX * distanceX + distanceY * distanceY; + } + + /* + * + * Returns true if the passed CellLayout cl overlaps with dragView + * + */ + boolean overlaps(CellLayout cl, DragView dragView, + int dragViewX, int dragViewY, Matrix cachedInverseMatrix) { + // Transform the coordinates of the item being dragged to the CellLayout's coordinates + final float[] draggedItemTopLeft = mTempDragCoordinates; + draggedItemTopLeft[0] = dragViewX; + draggedItemTopLeft[1] = dragViewY; + final float[] draggedItemBottomRight = mTempDragBottomRightCoordinates; + draggedItemBottomRight[0] = draggedItemTopLeft[0] + dragView.getDragRegionWidth(); + draggedItemBottomRight[1] = draggedItemTopLeft[1] + dragView.getDragRegionHeight(); + + // Transform the dragged item's top left coordinates + // to the CellLayout's local coordinates + mapPointFromSelfToChild(cl, draggedItemTopLeft, cachedInverseMatrix); + float overlapRegionLeft = Math.max(0f, draggedItemTopLeft[0]); + float overlapRegionTop = Math.max(0f, draggedItemTopLeft[1]); + + if (overlapRegionLeft <= cl.getWidth() && overlapRegionTop >= 0) { + // Transform the dragged item's bottom right coordinates + // to the CellLayout's local coordinates + mapPointFromSelfToChild(cl, draggedItemBottomRight, cachedInverseMatrix); + float overlapRegionRight = Math.min(cl.getWidth(), draggedItemBottomRight[0]); + float overlapRegionBottom = Math.min(cl.getHeight(), draggedItemBottomRight[1]); + + if (overlapRegionRight >= 0 && overlapRegionBottom <= cl.getHeight()) { + float overlap = (overlapRegionRight - overlapRegionLeft) * + (overlapRegionBottom - overlapRegionTop); + if (overlap > 0) { + return true; + } + } + } + return false; + } + + /* + * + * This method returns the CellLayout that is currently being dragged to. In order to drag + * to a CellLayout, either the touch point must be directly over the CellLayout, or as a second + * strategy, we see if the dragView is overlapping any CellLayout and choose the closest one + * + * Return null if no CellLayout is currently being dragged over + * + */ + private CellLayout findMatchingPageForDragOver( + DragView dragView, float originX, float originY, boolean exact) { + // We loop through all the screens (ie CellLayouts) and see which ones overlap + // with the item being dragged and then choose the one that's closest to the touch point + final int screenCount = getChildCount(); + CellLayout bestMatchingScreen = null; + float smallestDistSoFar = Float.MAX_VALUE; + + for (int i = 0; i < screenCount; i++) { + CellLayout cl = (CellLayout) getChildAt(i); + + final float[] touchXy = {originX, originY}; + // Transform the touch coordinates to the CellLayout's local coordinates + // If the touch point is within the bounds of the cell layout, we can return immediately + cl.getMatrix().invert(mTempInverseMatrix); + mapPointFromSelfToChild(cl, touchXy, mTempInverseMatrix); + + if (touchXy[0] >= 0 && touchXy[0] <= cl.getWidth() && + touchXy[1] >= 0 && touchXy[1] <= cl.getHeight()) { + return cl; + } + + if (!exact) { + // Get the center of the cell layout in screen coordinates + final float[] cellLayoutCenter = mTempCellLayoutCenterCoordinates; + cellLayoutCenter[0] = cl.getWidth()/2; + cellLayoutCenter[1] = cl.getHeight()/2; + mapPointFromChildToSelf(cl, cellLayoutCenter); + + touchXy[0] = originX; + touchXy[1] = originY; + + // Calculate the distance between the center of the CellLayout + // and the touch point + float dist = squaredDistance(touchXy, cellLayoutCenter); + + if (dist < smallestDistSoFar) { + smallestDistSoFar = dist; + bestMatchingScreen = cl; + } + } + } + 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); + } + private boolean isExternalDragWidget(DragObject d) { + return d.dragSource != this && isDragWidget(d); + } + + public void onDragOver(DragObject d) { + // Skip drag over events while we are dragging over side pages + if (mInScrollArea || mIsSwitchingState || mState == State.SMALL) return; + + Rect r = new Rect(); + CellLayout layout = null; + ItemInfo item = (ItemInfo) d.dragInfo; + + // 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); + + final View child = (mDragInfo == null) ? null : mDragInfo.cell; + // Identify whether we have dragged over a side page + if (isSmall()) { + if (mLauncher.getHotseat() != null && !isExternalDragWidget(d)) { + mLauncher.getHotseat().getHitRect(r); + if (r.contains(d.x, d.y)) { + layout = mLauncher.getHotseat().getLayout(); + } + } + if (layout == null) { + layout = findMatchingPageForDragOver(d.dragView, d.x, d.y, false); + } + if (layout != mDragTargetLayout) { + + setCurrentDropLayout(layout); + setCurrentDragOverlappingLayout(layout); + + boolean isInSpringLoadedMode = (mState == State.SPRING_LOADED); + if (isInSpringLoadedMode) { + if (mLauncher.isHotseatLayout(layout)) { + mSpringLoadedDragController.cancel(); + } else { + mSpringLoadedDragController.setAlarm(mDragTargetLayout); + } + } + } + } else { + // Test to see if we are over the hotseat otherwise just use the current page + if (mLauncher.getHotseat() != null && !isDragWidget(d)) { + mLauncher.getHotseat().getHitRect(r); + if (r.contains(d.x, d.y)) { + layout = mLauncher.getHotseat().getLayout(); + } + } + if (layout == null) { + layout = getCurrentDropLayout(); + } + if (layout != mDragTargetLayout) { + setCurrentDropLayout(layout); + setCurrentDragOverlappingLayout(layout); + } + } + + // Handle the drag over + if (mDragTargetLayout != null) { + // We want the point to be mapped to the dragTarget. + if (mLauncher.isHotseatLayout(mDragTargetLayout)) { + mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter); + } else { + mapPointFromSelfToChild(mDragTargetLayout, mDragViewVisualCenter, null); + } + + ItemInfo info = (ItemInfo) d.dragInfo; + + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], item.spanX, item.spanY, + mDragTargetLayout, mTargetCell); + + setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]); + + float targetCellDistance = mDragTargetLayout.getDistanceFromCell( + mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell); + + final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], + mTargetCell[1]); + + manageFolderFeedback(info, mDragTargetLayout, mTargetCell, + targetCellDistance, dragOverView); + + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; + } + + boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int) + mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX, + item.spanY, child, mTargetCell); + + if (!nearestDropOccupied) { + mDragTargetLayout.visualizeDropLocation(child, mDragOutline, + (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], + mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, + d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion()); + } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER) + && !mReorderAlarm.alarmPending() && (mLastReorderX != mTargetCell[0] || + mLastReorderY != mTargetCell[1])) { + + // Otherwise, if we aren't adding to or creating a folder and there's no pending + // reorder, then we schedule a reorder + ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter, + minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child); + mReorderAlarm.setOnAlarmListener(listener); + mReorderAlarm.setAlarm(REORDER_TIMEOUT); + } + + if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER || + !nearestDropOccupied) { + if (mDragTargetLayout != null) { + mDragTargetLayout.revertTempState(); + } + } + } + } + + private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout, + int[] targetCell, float distance, View dragOverView) { + 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); + return; + } + + boolean willAddToFolder = + willAddToExistingUserFolder(info, targetLayout, targetCell, distance); + + if (willAddToFolder && mDragMode == DRAG_MODE_NONE) { + mDragOverFolderIcon = ((FolderIcon) dragOverView); + mDragOverFolderIcon.onDragEnter(info); + if (targetLayout != null) { + targetLayout.clearDragOutlines(); + } + setDragMode(DRAG_MODE_ADD_TO_FOLDER); + return; + } + + if (mDragMode == DRAG_MODE_ADD_TO_FOLDER && !willAddToFolder) { + setDragMode(DRAG_MODE_NONE); + } + if (mDragMode == DRAG_MODE_CREATE_FOLDER && !userFolderPending) { + setDragMode(DRAG_MODE_NONE); + } + + return; + } + + class FolderCreationAlarmListener implements OnAlarmListener { + CellLayout layout; + int cellX; + int cellY; + + public FolderCreationAlarmListener(CellLayout layout, int cellX, int cellY) { + this.layout = layout; + this.cellX = cellX; + this.cellY = cellY; + } + + public void onAlarm(Alarm alarm) { + if (mDragFolderRingAnimator == null) { + mDragFolderRingAnimator = new FolderRingAnimator(mLauncher, null); + } + mDragFolderRingAnimator.setCell(cellX, cellY); + mDragFolderRingAnimator.setCellLayout(layout); + mDragFolderRingAnimator.animateToAcceptState(); + layout.showFolderAccept(mDragFolderRingAnimator); + layout.clearDragOutlines(); + setDragMode(DRAG_MODE_CREATE_FOLDER); + } + } + + class ReorderAlarmListener implements OnAlarmListener { + float[] dragViewCenter; + int minSpanX, minSpanY, spanX, spanY; + DragView dragView; + View child; + + public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX, + int spanY, DragView dragView, View child) { + this.dragViewCenter = dragViewCenter; + this.minSpanX = minSpanX; + this.minSpanY = minSpanY; + this.spanX = spanX; + this.spanY = spanY; + this.child = child; + this.dragView = dragView; + } + + public void onAlarm(Alarm alarm) { + int[] resultSpan = new int[2]; + mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], spanX, spanY, mDragTargetLayout, mTargetCell); + mLastReorderX = mTargetCell[0]; + mLastReorderY = mTargetCell[1]; + + mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, + child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER); + + if (mTargetCell[0] < 0 || mTargetCell[1] < 0) { + mDragTargetLayout.revertTempState(); + } else { + setDragMode(DRAG_MODE_REORDER); + } + + boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY; + mDragTargetLayout.visualizeDropLocation(child, mDragOutline, + (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], + mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, + dragView.getDragVisualizeOffset(), dragView.getDragRegion()); + } + } + + @Override + public void getHitRect(Rect outRect) { + // We want the workspace to have the whole area of the display (it will find the correct + // cell layout to drop to in the existing drag/drop logic. + outRect.set(0, 0, mDisplaySize.x, mDisplaySize.y); + } + + /** + * Add the item specified by dragInfo to the given layout. + * @return true if successful + */ + public boolean addExternalItemToScreen(ItemInfo dragInfo, CellLayout layout) { + if (layout.findCellForSpan(mTempEstimate, dragInfo.spanX, dragInfo.spanY)) { + onDropExternal(dragInfo.dropPos, (ItemInfo) dragInfo, (CellLayout) layout, false); + return true; + } + mLauncher.showOutOfSpaceMessage(mLauncher.isHotseatLayout(layout)); + return false; + } + + private void onDropExternal(int[] touchXY, Object dragInfo, + CellLayout cellLayout, boolean insertAtFirst) { + onDropExternal(touchXY, dragInfo, cellLayout, insertAtFirst, null); + } + + /** + * Drop an item that didn't originate on one of the workspace screens. + * It may have come from Launcher (e.g. from all apps or customize), or it may have + * come from another app altogether. + * + * NOTE: This can also be called when we are outside of a drag event, when we want + * to add an item to one of the workspace screens. + */ + private void onDropExternal(final int[] touchXY, final Object dragInfo, + final CellLayout cellLayout, boolean insertAtFirst, DragObject d) { + final Runnable exitSpringLoadedRunnable = new Runnable() { + @Override + public void run() { + mLauncher.exitSpringLoadedDragModeDelayed(true, false, null); + } + }; + + ItemInfo info = (ItemInfo) dragInfo; + int spanX = info.spanX; + int spanY = info.spanY; + if (mDragInfo != null) { + spanX = mDragInfo.spanX; + spanY = mDragInfo.spanY; + } + + final long container = mLauncher.isHotseatLayout(cellLayout) ? + LauncherSettings.Favorites.CONTAINER_HOTSEAT : + LauncherSettings.Favorites.CONTAINER_DESKTOP; + final int screen = indexOfChild(cellLayout); + if (!mLauncher.isHotseatLayout(cellLayout) && screen != mCurrentPage + && mState != State.SPRING_LOADED) { + snapToPage(screen); + } + + if (info instanceof PendingAddItemInfo) { + final PendingAddItemInfo pendingInfo = (PendingAddItemInfo) dragInfo; + + boolean findNearestVacantCell = true; + if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { + mTargetCell = findNearestArea((int) touchXY[0], (int) touchXY[1], spanX, spanY, + cellLayout, mTargetCell); + float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + if (willCreateUserFolder((ItemInfo) d.dragInfo, cellLayout, mTargetCell, + distance, true) || willAddToExistingUserFolder((ItemInfo) d.dragInfo, + cellLayout, mTargetCell, distance)) { + findNearestVacantCell = false; + } + } + + final ItemInfo item = (ItemInfo) d.dragInfo; + boolean updateWidgetSize = false; + if (findNearestVacantCell) { + int minSpanX = item.spanX; + int minSpanY = item.spanY; + if (item.minSpanX > 0 && item.minSpanY > 0) { + minSpanX = item.minSpanX; + minSpanY = item.minSpanY; + } + int[] resultSpan = new int[2]; + mTargetCell = cellLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], minSpanX, minSpanY, info.spanX, info.spanY, + null, mTargetCell, resultSpan, CellLayout.MODE_ON_DROP_EXTERNAL); + + if (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY) { + updateWidgetSize = true; + } + item.spanX = resultSpan[0]; + item.spanY = resultSpan[1]; + } + + Runnable onAnimationCompleteRunnable = new Runnable() { + @Override + public void run() { + // 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, screen, mTargetCell, span, null); + break; + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + mLauncher.processShortcutFromDrop(pendingInfo.componentName, + container, screen, mTargetCell, null); + break; + default: + throw new IllegalStateException("Unknown item type: " + + pendingInfo.itemType); + } + } + }; + View finalView = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET + ? ((PendingAddWidgetInfo) pendingInfo).boundWidget : null; + + if (finalView instanceof AppWidgetHostView && updateWidgetSize) { + AppWidgetHostView awhv = (AppWidgetHostView) finalView; + AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, item.spanX, + item.spanY); + } + + int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR; + if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET && + ((PendingAddWidgetInfo) pendingInfo).info.configure != null) { + animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN; + } + animateWidgetDrop(info, cellLayout, d.dragView, onAnimationCompleteRunnable, + animationStyle, finalView, true); + } else { + // This is for other drag/drop cases, like dragging from All Apps + View view = null; + + switch (info.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + if (info.container == NO_ID && info instanceof ApplicationInfo) { + // Came from all apps -- make a copy + info = new ShortcutInfo((ApplicationInfo) info); + } + view = mLauncher.createShortcut(R.layout.application, cellLayout, + (ShortcutInfo) info); + break; + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout, + (FolderInfo) info, mIconCache); + break; + default: + throw new IllegalStateException("Unknown item type: " + info.itemType); + } + + // First we find the cell nearest to point at which the item is + // dropped, without any consideration to whether there is an item there. + if (touchXY != null) { + mTargetCell = findNearestArea((int) touchXY[0], (int) touchXY[1], spanX, spanY, + cellLayout, mTargetCell); + float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0], + mDragViewVisualCenter[1], mTargetCell); + d.postAnimationRunnable = exitSpringLoadedRunnable; + if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, distance, + true, d.dragView, d.postAnimationRunnable)) { + return; + } + if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, distance, d, + true)) { + return; + } + } + + if (touchXY != null) { + // when dragging and dropping, just find the closest free spot + mTargetCell = cellLayout.createArea((int) mDragViewVisualCenter[0], + (int) mDragViewVisualCenter[1], 1, 1, 1, 1, + null, mTargetCell, null, CellLayout.MODE_ON_DROP_EXTERNAL); + } else { + cellLayout.findCellForSpan(mTargetCell, 1, 1); + } + addInScreen(view, container, screen, mTargetCell[0], mTargetCell[1], info.spanX, + info.spanY, insertAtFirst); + cellLayout.onDropChild(view); + CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams(); + cellLayout.getShortcutsAndWidgets().measureChild(view); + + + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screen, + lp.cellX, lp.cellY); + + if (d.dragView != null) { + // We wrap the animation call in the temporary set and reset of the current + // cellLayout to its final transform -- this means we animate the drag view to + // the correct final location. + setFinalTransitionTransform(cellLayout); + mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, view, + exitSpringLoadedRunnable); + resetTransitionTransform(cellLayout); + } + } + } + + public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) { + int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo.spanX, + widgetInfo.spanY, widgetInfo, false); + int visibility = layout.getVisibility(); + layout.setVisibility(VISIBLE); + + int width = MeasureSpec.makeMeasureSpec(unScaledSize[0], MeasureSpec.EXACTLY); + int height = MeasureSpec.makeMeasureSpec(unScaledSize[1], MeasureSpec.EXACTLY); + Bitmap b = Bitmap.createBitmap(unScaledSize[0], unScaledSize[1], + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + + layout.measure(width, height); + layout.layout(0, 0, unScaledSize[0], unScaledSize[1]); + layout.draw(c); + c.setBitmap(null); + layout.setVisibility(visibility); + return b; + } + + private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY, + DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, + boolean external, boolean scale) { + // Now we animate the dragView, (ie. the widget or shortcut preview) into its final + // location and size on the home screen. + int spanX = info.spanX; + int spanY = info.spanY; + + Rect r = estimateItemPosition(layout, info, targetCell[0], targetCell[1], spanX, spanY); + loc[0] = r.left; + loc[1] = r.top; + + setFinalTransitionTransform(layout); + float cellLayoutScale = + mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(layout, loc); + resetTransitionTransform(layout); + + float dragViewScaleX; + float dragViewScaleY; + if (scale) { + dragViewScaleX = (1.0f * r.width()) / dragView.getMeasuredWidth(); + dragViewScaleY = (1.0f * r.height()) / dragView.getMeasuredHeight(); + } else { + dragViewScaleX = 1f; + dragViewScaleY = 1f; + } + + // The animation will scale the dragView about its center, so we need to center about + // the final location. + loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2; + loc[1] -= (dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2; + + scaleXY[0] = dragViewScaleX * cellLayoutScale; + scaleXY[1] = dragViewScaleY * cellLayoutScale; + } + + public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, DragView dragView, + final Runnable onCompleteRunnable, int animationType, final View finalView, + boolean external) { + Rect from = new Rect(); + mLauncher.getDragLayer().getViewRectRelativeToSelf(dragView, from); + + int[] finalPos = new int[2]; + float scaleXY[] = new float[2]; + boolean scalePreview = !(info instanceof PendingAddShortcutInfo); + getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell, + external, scalePreview); + + Resources res = mLauncher.getResources(); + int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200; + + // 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); + } + 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) { + scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]); + } + + DragLayer dragLayer = mLauncher.getDragLayer(); + if (animationType == CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION) { + mLauncher.getDragLayer().animateViewIntoPosition(dragView, finalPos, 0f, 0.1f, 0.1f, + DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration); + } else { + int endStyle; + if (animationType == ANIMATE_INTO_POSITION_AND_REMAIN) { + endStyle = DragLayer.ANIMATION_END_REMAIN_VISIBLE; + } else { + endStyle = DragLayer.ANIMATION_END_DISAPPEAR;; + } + + Runnable onComplete = new Runnable() { + @Override + public void run() { + if (finalView != null) { + finalView.setVisibility(VISIBLE); + } + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + }; + dragLayer.animateViewIntoPosition(dragView, from.left, from.top, finalPos[0], + finalPos[1], 1, 1, 1, scaleXY[0], scaleXY[1], onComplete, endStyle, + duration, this); + } + } + + public void setFinalTransitionTransform(CellLayout layout) { + if (isSwitchingState()) { + int index = indexOfChild(layout); + mCurrentScaleX = layout.getScaleX(); + mCurrentScaleY = layout.getScaleY(); + mCurrentTranslationX = layout.getTranslationX(); + mCurrentTranslationY = layout.getTranslationY(); + mCurrentRotationY = layout.getRotationY(); + layout.setScaleX(mNewScaleXs[index]); + layout.setScaleY(mNewScaleYs[index]); + layout.setTranslationX(mNewTranslationXs[index]); + layout.setTranslationY(mNewTranslationYs[index]); + layout.setRotationY(mNewRotationYs[index]); + } + } + public void resetTransitionTransform(CellLayout layout) { + if (isSwitchingState()) { + mCurrentScaleX = layout.getScaleX(); + mCurrentScaleY = layout.getScaleY(); + mCurrentTranslationX = layout.getTranslationX(); + mCurrentTranslationY = layout.getTranslationY(); + mCurrentRotationY = layout.getRotationY(); + layout.setScaleX(mCurrentScaleX); + layout.setScaleY(mCurrentScaleY); + layout.setTranslationX(mCurrentTranslationX); + layout.setTranslationY(mCurrentTranslationY); + layout.setRotationY(mCurrentRotationY); + } + } + + /** + * Return the current {@link CellLayout}, correctly picking the destination + * screen while a scroll is in progress. + */ + public CellLayout getCurrentDropLayout() { + return (CellLayout) getChildAt(getNextPage()); + } + + /** + * Return the current CellInfo describing our current drag; this method exists + * so that Launcher can sync this object with the correct info when the activity is created/ + * destroyed + * + */ + public CellLayout.CellInfo getDragInfo() { + return mDragInfo; + } + + /** + * Calculate the nearest cell where the given object would be dropped. + * + * pixelX and pixelY should be in the coordinate system of layout + */ + private int[] findNearestArea(int pixelX, int pixelY, + int spanX, int spanY, CellLayout layout, int[] recycle) { + return layout.findNearestArea( + pixelX, pixelY, spanX, spanY, recycle); + } + + void setup(DragController dragController) { + mSpringLoadedDragController = new SpringLoadedDragController(mLauncher); + mDragController = dragController; + + // hardware layers on children are enabled on startup, but should be disabled until + // needed + updateChildrenLayersEnabled(false); + setWallpaperDimension(); + } + + /** + * Called at the end of a drag which originated on the workspace. + */ + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + if (success) { + if (target != this) { + if (mDragInfo != null) { + getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell); + if (mDragInfo.cell instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) mDragInfo.cell); + } + } + } + } else if (mDragInfo != null) { + CellLayout cellLayout; + if (mLauncher.isHotseatLayout(target)) { + cellLayout = mLauncher.getHotseat().getLayout(); + } else { + cellLayout = (CellLayout) getChildAt(mDragInfo.screen); + } + cellLayout.onDropChild(mDragInfo.cell); + } + if (d.cancelled && mDragInfo.cell != null) { + mDragInfo.cell.setVisibility(VISIBLE); + } + mDragOutline = null; + mDragInfo = null; + + // Hide the scrolling indicator after you pick up an item + hideScrollingIndicator(false); + } + + void updateItemLocationsInDatabase(CellLayout cl) { + int count = cl.getShortcutsAndWidgets().getChildCount(); + + int screen = indexOfChild(cl); + int container = Favorites.CONTAINER_DESKTOP; + + if (mLauncher.isHotseatLayout(cl)) { + screen = -1; + container = Favorites.CONTAINER_HOTSEAT; + } + + for (int i = 0; i < count; i++) { + View v = cl.getShortcutsAndWidgets().getChildAt(i); + ItemInfo info = (ItemInfo) v.getTag(); + // Null check required as the AllApps button doesn't have an item info + if (info != null && info.requiresDbUpdate) { + info.requiresDbUpdate = false; + LauncherModel.modifyItemInDatabase(mLauncher, info, container, screen, info.cellX, + info.cellY, info.spanX, info.spanY); + } + } + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + @Override + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + @Override + public void onFlingToDeleteCompleted() { + // Do nothing + } + + public boolean isDropEnabled() { + 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. + // Some pages will be restored immediately as their items are bound immediately, and + // others we will need to wait until after their items are bound. + mSavedStates = container; + } + + public void restoreInstanceStateForChild(int child) { + if (mSavedStates != null) { + mRestoredPages.add(child); + CellLayout cl = (CellLayout) getChildAt(child); + cl.restoreInstanceState(mSavedStates); + } + } + + public void restoreInstanceStateForRemainingPages() { + int count = getChildCount(); + for (int i = 0; i < count; i++) { + if (!mRestoredPages.contains(i)) { + restoreInstanceStateForChild(i); + } + } + mRestoredPages.clear(); + } + + @Override + public void scrollLeft() { + if (!isSmall() && !mIsSwitchingState) { + super.scrollLeft(); + } + Folder openFolder = getOpenFolder(); + if (openFolder != null) { + openFolder.completeDragExit(); + } + } + + @Override + public void scrollRight() { + if (!isSmall() && !mIsSwitchingState) { + super.scrollRight(); + } + Folder openFolder = getOpenFolder(); + if (openFolder != null) { + openFolder.completeDragExit(); + } + } + + @Override + public boolean onEnterScrollArea(int x, int y, int direction) { + // Ignore the scroll area if we are dragging over the hot seat + boolean isPortrait = !LauncherApplication.isScreenLandscape(getContext()); + if (mLauncher.getHotseat() != null && isPortrait) { + Rect r = new Rect(); + mLauncher.getHotseat().getHitRect(r); + if (r.contains(x, y)) { + return false; + } + } + + boolean result = false; + if (!isSmall() && !mIsSwitchingState) { + mInScrollArea = true; + + final int page = getNextPage() + + (direction == DragController.SCROLL_LEFT ? -1 : 1); + + // We always want to exit the current layout to ensure parity of enter / exit + setCurrentDropLayout(null); + + if (0 <= page && page < getChildCount()) { + CellLayout layout = (CellLayout) getChildAt(page); + setCurrentDragOverlappingLayout(layout); + + // Workspace is responsible for drawing the edge glow on adjacent pages, + // so we need to redraw the workspace when this may have changed. + invalidate(); + result = true; + } + } + return result; + } + + @Override + public boolean onExitScrollArea() { + boolean result = false; + if (mInScrollArea) { + invalidate(); + CellLayout layout = getCurrentDropLayout(); + setCurrentDropLayout(layout); + setCurrentDragOverlappingLayout(layout); + + result = true; + mInScrollArea = false; + } + return result; + } + + private void onResetScrollArea() { + setCurrentDragOverlappingLayout(null); + mInScrollArea = false; + } + + /** + * Returns a specific CellLayout + */ + CellLayout getParentCellLayoutForView(View v) { + ArrayList layouts = getWorkspaceAndHotseatCellLayouts(); + for (CellLayout layout : layouts) { + if (layout.getShortcutsAndWidgets().indexOfChild(v) > -1) { + return layout; + } + } + return null; + } + + /** + * Returns a list of all the CellLayouts in the workspace. + */ + ArrayList getWorkspaceAndHotseatCellLayouts() { + ArrayList layouts = new ArrayList(); + int screenCount = getChildCount(); + for (int screen = 0; screen < screenCount; screen++) { + layouts.add(((CellLayout) getChildAt(screen))); + } + if (mLauncher.getHotseat() != null) { + layouts.add(mLauncher.getHotseat().getLayout()); + } + return layouts; + } + + /** + * We should only use this to search for specific children. Do not use this method to modify + * ShortcutsAndWidgetsContainer directly. Includes ShortcutAndWidgetContainers from + * the hotseat and workspace pages + */ + ArrayList getAllShortcutAndWidgetContainers() { + ArrayList childrenLayouts = + new ArrayList(); + int screenCount = getChildCount(); + for (int screen = 0; screen < screenCount; screen++) { + childrenLayouts.add(((CellLayout) getChildAt(screen)).getShortcutsAndWidgets()); + } + if (mLauncher.getHotseat() != null) { + childrenLayouts.add(mLauncher.getHotseat().getLayout().getShortcutsAndWidgets()); + } + return childrenLayouts; + } + + public Folder getFolderForTag(Object tag) { + ArrayList childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { + int count = layout.getChildCount(); + for (int i = 0; i < count; i++) { + View child = layout.getChildAt(i); + if (child instanceof Folder) { + Folder f = (Folder) child; + if (f.getInfo() == tag && f.getInfo().opened) { + return f; + } + } + } + } + return null; + } + + public View getViewForTag(Object tag) { + ArrayList childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { + int count = layout.getChildCount(); + for (int i = 0; i < count; i++) { + View child = layout.getChildAt(i); + if (child.getTag() == tag) { + return child; + } + } + } + return null; + } + + void clearDropTargets() { + ArrayList childrenLayouts = + getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { + int childCount = layout.getChildCount(); + for (int j = 0; j < childCount; j++) { + View v = layout.getChildAt(j); + if (v instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) v); + } + } + } + } + + // Removes ALL items that match a given package name, this is usually called when a package + // has been removed and we want to remove all components (widgets, shortcuts, apps) that + // belong to that package. + void removeItemsByPackageName(final ArrayList packages) { + HashSet packageNames = new HashSet(); + packageNames.addAll(packages); + + // Just create a hash table of all the specific components that this will affect + HashSet cns = new HashSet(); + ArrayList cellLayouts = getWorkspaceAndHotseatCellLayouts(); + for (CellLayout layoutParent : cellLayouts) { + ViewGroup layout = layoutParent.getShortcutsAndWidgets(); + int childCount = layout.getChildCount(); + for (int i = 0; i < childCount; ++i) { + View view = layout.getChildAt(i); + Object tag = view.getTag(); + + if (tag instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) tag; + ComponentName cn = info.intent.getComponent(); + if ((cn != null) && packageNames.contains(cn.getPackageName())) { + cns.add(cn); + } + } else if (tag instanceof FolderInfo) { + FolderInfo info = (FolderInfo) tag; + for (ShortcutInfo s : info.contents) { + ComponentName cn = s.intent.getComponent(); + if ((cn != null) && packageNames.contains(cn.getPackageName())) { + cns.add(cn); + } + } + } else if (tag instanceof LauncherAppWidgetInfo) { + LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) tag; + ComponentName cn = info.providerName; + if ((cn != null) && packageNames.contains(cn.getPackageName())) { + cns.add(cn); + } + } + } + } + + // Remove all the things + removeItemsByComponentName(cns); + } + + // Removes items that match the application info specified, when applications are removed + // as a part of an update, this is called to ensure that other widgets and application + // shortcuts are not removed. + void removeItemsByApplicationInfo(final ArrayList appInfos) { + // Just create a hash table of all the specific components that this will affect + HashSet cns = new HashSet(); + for (ApplicationInfo info : appInfos) { + cns.add(info.componentName); + } + + // Remove all the things + removeItemsByComponentName(cns); + } + + void removeItemsByComponentName(final HashSet componentNames) { + ArrayList cellLayouts = getWorkspaceAndHotseatCellLayouts(); + for (final CellLayout layoutParent: cellLayouts) { + final ViewGroup layout = layoutParent.getShortcutsAndWidgets(); + + // Avoid ANRs by treating each screen separately + post(new Runnable() { + public void run() { + final ArrayList childrenToRemove = new ArrayList(); + childrenToRemove.clear(); + + int childCount = layout.getChildCount(); + for (int j = 0; j < childCount; j++) { + final View view = layout.getChildAt(j); + Object tag = view.getTag(); + + if (tag instanceof ShortcutInfo) { + final ShortcutInfo info = (ShortcutInfo) tag; + final Intent intent = info.intent; + final ComponentName name = intent.getComponent(); + + if (name != null) { + if (componentNames.contains(name)) { + LauncherModel.deleteItemFromDatabase(mLauncher, info); + childrenToRemove.add(view); + } + } + } else if (tag instanceof FolderInfo) { + final FolderInfo info = (FolderInfo) tag; + final ArrayList contents = info.contents; + final int contentsCount = contents.size(); + final ArrayList appsToRemoveFromFolder = + new ArrayList(); + + for (int k = 0; k < contentsCount; k++) { + final ShortcutInfo appInfo = contents.get(k); + final Intent intent = appInfo.intent; + final ComponentName name = intent.getComponent(); + + if (name != null) { + if (componentNames.contains(name)) { + appsToRemoveFromFolder.add(appInfo); + } + } + } + for (ShortcutInfo item: appsToRemoveFromFolder) { + info.remove(item); + LauncherModel.deleteItemFromDatabase(mLauncher, item); + } + } else if (tag instanceof LauncherAppWidgetInfo) { + final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) tag; + final ComponentName provider = info.providerName; + if (provider != null) { + if (componentNames.contains(provider)) { + LauncherModel.deleteItemFromDatabase(mLauncher, info); + childrenToRemove.add(view); + } + } + } + } + + childCount = childrenToRemove.size(); + for (int j = 0; j < childCount; j++) { + View child = childrenToRemove.get(j); + // Note: We can not remove the view directly from CellLayoutChildren as this + // does not re-mark the spaces as unoccupied. + layoutParent.removeViewInLayout(child); + if (child instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget)child); + } + } + + if (childCount > 0) { + layout.requestLayout(); + layout.invalidate(); + } + } + }); + } + + // Clean up new-apps animation list + final Context context = getContext(); + post(new Runnable() { + @Override + public void run() { + String spKey = LauncherApplication.getSharedPreferencesKey(); + SharedPreferences sp = context.getSharedPreferences(spKey, + Context.MODE_PRIVATE); + Set newApps = sp.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, + null); + + // Remove all queued items that match the same package + if (newApps != null) { + synchronized (newApps) { + Iterator iter = newApps.iterator(); + while (iter.hasNext()) { + try { + Intent intent = Intent.parseUri(iter.next(), 0); + if (componentNames.contains(intent.getComponent())) { + iter.remove(); + } + + // It is possible that we've queued an item to be loaded, yet it has + // not been added to the workspace, so remove those items as well. + ArrayList shortcuts; + shortcuts = LauncherModel.getWorkspaceShortcutItemInfosWithIntent( + intent); + for (ItemInfo info : shortcuts) { + LauncherModel.deleteItemFromDatabase(context, info); + } + } catch (URISyntaxException e) {} + } + } + } + } + }); + } + + void updateShortcuts(ArrayList apps) { + ArrayList childrenLayouts = getAllShortcutAndWidgetContainers(); + for (ShortcutAndWidgetContainer layout: childrenLayouts) { + int childCount = layout.getChildCount(); + for (int j = 0; j < childCount; j++) { + final View view = layout.getChildAt(j); + Object tag = view.getTag(); + if (tag instanceof ShortcutInfo) { + ShortcutInfo info = (ShortcutInfo) tag; + // We need to check for ACTION_MAIN otherwise getComponent() might + // return null for some shortcuts (for instance, for shortcuts to + // web pages.) + final Intent intent = info.intent; + final ComponentName name = intent.getComponent(); + if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && + Intent.ACTION_MAIN.equals(intent.getAction()) && name != null) { + final int appCount = apps.size(); + for (int k = 0; k < appCount; k++) { + ApplicationInfo app = apps.get(k); + if (app.componentName.equals(name)) { + BubbleTextView shortcut = (BubbleTextView) view; + info.updateIcon(mIconCache); + info.title = app.title.toString(); + shortcut.applyFromShortcutInfo(info, mIconCache); + } + } + } + } + } + } + } + + void moveToDefaultScreen(boolean animate) { + if (!isSmall()) { + if (animate) { + snapToPage(mDefaultPage); + } else { + setCurrentPage(mDefaultPage); + } + } + getChildAt(mDefaultPage).requestFocus(); + } + + @Override + public void syncPages() { + } + + @Override + public void syncPageItems(int page, boolean immediate) { + } + + @Override + protected String getCurrentPageDescription() { + int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + return String.format(getContext().getString(R.string.workspace_scroll_format), + page + 1, getChildCount()); + } + + public void getLocationInDragLayer(int[] loc) { + mLauncher.getDragLayer().getLocationInDragLayer(this, loc); + } + + void setFadeForOverScroll(float fade) { + if (!isScrollingIndicatorEnabled()) return; + + mOverscrollFade = fade; + float reducedFade = 0.5f + 0.5f * (1 - fade); + final ViewGroup parent = (ViewGroup) getParent(); + final ImageView dockDivider = (ImageView) (parent.findViewById(R.id.dock_divider)); + final View scrollIndicator = getScrollingIndicator(); + + cancelScrollingIndicatorAnimations(); + if (dockDivider != null) dockDivider.setAlpha(reducedFade); + scrollIndicator.setAlpha(1 - fade); + } +} diff --git a/app/src/main/res/anim/fade_in_fast.xml b/app/src/main/res/anim/fade_in_fast.xml new file mode 100644 index 0000000..4fa9847 --- /dev/null +++ b/app/src/main/res/anim/fade_in_fast.xml @@ -0,0 +1,23 @@ + + + + diff --git a/app/src/main/res/anim/fade_out_fast.xml b/app/src/main/res/anim/fade_out_fast.xml new file mode 100644 index 0000000..a061a6c --- /dev/null +++ b/app/src/main/res/anim/fade_out_fast.xml @@ -0,0 +1,23 @@ + + + + diff --git a/app/src/main/res/drawable/all_apps_button_icon.xml b/app/src/main/res/drawable/all_apps_button_icon.xml new file mode 100644 index 0000000..7c69cad --- /dev/null +++ b/app/src/main/res/drawable/all_apps_button_icon.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/apps_customize_bg.png b/app/src/main/res/drawable/apps_customize_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..8210f88495df0a2a91c519abf836ad634b4be6a1 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Asp9}A3S&f2jC literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/bg_appwidget_error.9.png b/app/src/main/res/drawable/bg_appwidget_error.9.png new file mode 100644 index 0000000000000000000000000000000000000000..493c0d454c92e38a0d95f6ed067ce8714c1d8f4c GIT binary patch literal 794 zcmV+#1LgdQP)#&|`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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/bg_cling1.png b/app/src/main/res/drawable/bg_cling1.png new file mode 100644 index 0000000000000000000000000000000000000000..f284412a78a11e800707a42726d699afdf3cedab GIT binary patch literal 535 zcmeAS@N?(olHy`uVBq!ia0vp^0YKc!!2%@HUT@z8q?nSt-CY>|xA&jf59DzcctjQh zX%8@VJDF_<5-cllOb60ny$tayZhl~3V3hZCaSW-r_4d|n-ew0Lmq7KVf6_kcfj(9Z z{f@V?OO`h+mVKkzA6}cKpSLgiZAL6MLAg_9odjVciJH{kup^3g$?@36h2jX=Di zT*lGFAENo884CL!na?tREGaUBG4={jh@at5zL5A|xA&jf59DzcctjQh zX%8@VJDF_<5-cllOb60ny$tayZhl~3VC405aSW-r_4c+kU$X;G%R@%Ncl8%arc5j= zX4Y8tVaDI*Ec4%Nt>iF^{iR{P-+B4DtfRi$oP#$<6;9q6&OJ{k{-ARgTh{q!na8%z zP&9PzetRU`ppO4o_=bavmK_cI^gY35!@&tH;S)5?E-yUFz{teHA)w&Uz`*#7?Sa&R zz_k@j$HEO{EEY8`c4TGz<#6rH>BcpZ+Okn+ME1*_5Q$Fo2xBml5IP6cvi*Rdt%|K) zDMLJ8ipQFg48sGg4JU)|EN*&uVBwSVPb04|tUfHq5;2#rdzSviwIvqKP|FT@B{avY z@08y%sh*8-*MTSMR-Qa3nH`x%nxX zX_e?2%&bgIAsV(Ni%$S*kObKfoS#-wo>-L1;Fyx1l&avFo0y&&l$w}QS$Hzl2B?U^ M)78&qol`;+06|2zqyPW_ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/bg_cling3.png b/app/src/main/res/drawable/bg_cling3.png new file mode 100644 index 0000000000000000000000000000000000000000..fabdf7a0d834a234c3ede4a97d84fd0d52da3a6d GIT binary patch literal 617 zcmeAS@N?(olHy`uVBq!ia0vp^0YKc!!2%@HUT@z8q?nSt-CY>|xA&jf59DzcctjQh zX%8@VJDF_<5-cllOb60ny$tayZhl~3U@Y`>aSW-r_4ZESq-F(ymWN{MJO9Ui*EQN< zuE3uo`hU$`^&hL;l@n$<+gEkY))wiy+vWK#;MJYAGgO?NcSakY zWPm$!wZ_$QAWKNu4k+-FZ5^{P?-_^thlBWIck^D3$ty54mD*FT%~NBsVR576uLb3$ zqHZw^9`~*;ZG3%z|L+Ut`wJRhJN$Tgt<^5l-~)eyyp_Oxh2J^P_+o1oxbG3@uP_yL z%URIKR>!z0-Z>;=!k*MgA}@4w& zCUegV(mYk|c^3}c)nsT{pUxA*ZuUk0p{&(Qhi{vgFkMjo_x*slY)Afty)hE1Zaw(| z{9ii6jhynWHXJa`SkSn;@kwp$rN*@kA9~rUj+|#WWWOtknV*^0> zbAd6TTH+c}l9E`GYL#4+3Zxi}3=9o*4J>sH%|Z-KtxU|VjLmcn%&iOzgfCv7hN2-i zKP5A*5?zCtm5C`t!?tAc2|x{!ARB`7(@M${i&7aJQ}UBi6+Ckj(^G>|6H_V+Po~-c P6)||a`njxgN@xNA1^Mg% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/bg_cling4.png b/app/src/main/res/drawable/bg_cling4.png new file mode 100644 index 0000000000000000000000000000000000000000..2f152f4dbc14542270fb1c8d7f8e929c38786229 GIT binary patch literal 537 zcmeAS@N?(olHy`uVBq!ia0vp^0YKc!!2%@HUT@z8q?nSt-CY>|xA&jf59DzcctjQh zX%8@VJDF|Az`$tc>EaktaqI1DN4`S_0?gYleEPq?$i;JK0UvwwwRLH62AekD4hh{} zbuDn;o^MvKF6M4bTEF>RUMFjM%Y!%DeyC;4U$FDR7O^>uoVWNnMZE(yo>FlX6bd=% zaUs$mG4Qr}#{#Vrl2bfZNH~U0aG&eqbd*D>L!qf&Os4epwg`i7vrKpCim8P3Jb23L zo_kk}+s&bg<5knM?pcZx7bHy;{P5^q%!hQ3Iok6Wn?iIZZb*A0Ug~i4oC;6p-UYQs zzcqamwT|CrbPmMkWQ+MZcmEFKqgu)ejx3>$%YJ-n3h7DMzu>c^&0YJ9^pH0`ha|t$ zWjcLeZ(1YC@|*Q$Gl$X+krKIyJuHHI_AL1BAO7^8&bo%Kx2(+P7F0HU16t_Vq@}WA zQo-V7Nt{~K3jGBfXYT&Wlg=^gGuw-~<`TPSR_XwQQnkc2q9i4;B-JXpC>2OC7#SEE z>VlAIh@r8Sv7wcrsjh*Am4U&}jh{GBH00)|WTsW(*3j|BYYR|=B*=!~{Irtt#G+IN j$CUh}R0Yr6#Prml)Wnp^!jq{sKt&9mu6{1-oD!M9s(?>7Fi*As(H{2@3pw+8+fPFfl5d9elv7FK29IbSfa}!~g&P=dUT9 zFu_$k{ot}6ch~{t7dO~7yy4Si@Y*Z?{ePa=UZ4{hJYD@<);T3KDUb{Rn}KGH literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/btn_cling_pressed.9.png b/app/src/main/res/drawable/btn_cling_pressed.9.png new file mode 100644 index 0000000000000000000000000000000000000000..bed8f4364ab09370ad2106044924d3a1f30cd228 GIT binary patch literal 420 zcmeAS@N?(olHy`uVBq!ia0vp^GC-`v!3HFKYIk}9DVAa<&kznEsNqQI0P;BtJR*x3 z7`Qt@n9=;?>9q_Dj3S;cjv*eMZ>R6iYjTh|ZeJd9I7zaHQ%`nRY5D`zUov)%zXkR+ zo`2|Vv7`KTLXuOGM3=0G=z8XMCCA`?`%IqsEiE-C|JunMKPWq6;g>?=M-^t#eGKvi z=Raj>C@_fm>^bW><5XN@`z#mNZwKadZl7rNL3+xCYjN zmkg_QFJ&oN)~vO&yMO*NcmGFgLB7tFZzJw&dZ$L-P|4-KdiVPV;j%-1Km1!l_g^yS zj&=>Y|Lf`bLt!slN|oR3Yk#`hOz@bXlbsSzdQZ>-zKf;{mCWT27yP|;zir#r?rODt zOVZ!hu^rw~Azhis(DOOGv^i<97E6KDVb+&zi#5O9y7;ebt(W5B{b%&Mm7E#{gUTy` SfyUtJ>gTe~DWOS$WB>q^!JaYz literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/cling.png b/app/src/main/res/drawable/cling.png new file mode 100644 index 0000000000000000000000000000000000000000..fba3a0787853ea2cfa3b9546a2d3351328aaf506 GIT binary patch literal 32299 zcma%jcQjl78^2w9*4`1DDz*0(v15-`35rmgs#UeANQhLaDuUX3RgK!CR%#VBimI*D zUcY?4zw`V5mz;a-|zP`UeD`!o_CUDYOF^^&PI-hhexFk(uUyS;UoS#NJ(&4 zUUkZT!<|T+4fM3}?*99JY%j^cT_N)Y*#_Xwp#L4ZLon6Nl}J|NoyXmU>sYkA+ZjuY-M)%djvd`;mQry@5Y}h_JO- zko@i1%cU_bEx`h~WxroLto@8;x9S#<3v_ASB+OiuEZa6)8)V4XHU(^%t z>gd2J3h$4t6gbC&aErN(Qq`A|864I?i;Mf9!9SfGyWwg=lUa)^0P~ z4-C3%v+!RTH=3kG7(8sO&-2XVy9(A+`0-t-$kzC!?2x_2}KJF^Cxz0dj^6TaATeUd{K2a ze$3Ox*``9w2gb_xK4fGzdyRWSr6DeP#sWKO#yqg(JCvi-nRyKWm|XI6$0o@A!jy8BXF+I3HCPWz_wwekH7jo8@i zVGvtJ9jW+V?jGzZE)5aQQ!&OsSd1}`=$r0`(<~{AQ2PEkAe1wQNyygTU56UVFl>?X z@7YeUlZ)2B5A5sY1=y=LXI{eYzOm;4v$lJ3#-L2#>HQ%JQ=hYO|H zhTe8Cz*bc;T8xf?egK7WGI~4neI3?sn8LKfS=4I=sa|gbA7SSL0SW?ti0X(c==|Pj zu$gCo$DUUbde5r#15#=SF%0?xwt)oiSV)e9D!>iu=8thd|R{@-w_07?Vl78hB)lj+iS z>2TS~Ay~ipYne#3;$JnWMxKn)M9egszF<0Xg3N_T+F?_Oms&Ilm9As-6VffLZy-BV zrL%R+)kp-=^$Q7fdd)a99#BJTT8|Kk-4Hu|NlFDwYqO!u1 zrUv7&_=0cF^(ZaD^kSSdlBNzqS$ik5If@17z3u93hhceF?wnO3Ka4e`6zrc&It z_<<9`{6_{5zZF8hfwvsJAWYhi)$<6KUh~oz5^sYt3`h-XYXM9BOFO2Eg_Z0}81y+6 zqb=0+xx~9IM+B&0Kkkzk0mXkvZ;Y67SCV0LZqmK0@>=~S-$?LpnZ)M? z(_ld#&0q?C6nkZdkA2Y8|1&(^4B>2aF*A_Lmh$c_!s)cmWB6l6B5STX<=oQGJiDL< z0XTg(i&CXWj$64=2|T8Fk?jGilFRgKA-f_O?-cIEt4uMn%Ll zf=~!hN)Wm5V>_$t2mc#R99^pH`Mt>X{yfH8UlDEcL)vv>I*~kAT`KF9vb44olOHNw zPt@Oo@z?MV>4AI_{*+A_7^kqkXu1$DQZh-~fFoC1Kv$3!Ka;n-w%f9Z2%9eEN#RFnmdK#3%$U*E?#|M2`?8$K-XW4s`t0sDuD{wuv@f=)yO4;)hWsa8L<)JG7h zUIB!FDP?4Z^9(jG%Q=-|687hq1t|?2i>UFB!7?xh<+X(RR$obzO zuhshj)I&?jHe$Fd^&W7Y|As+_&@14cYRb2$GzW#PH|1g%_>ya%vgbIx^dSWFWO%H)E?4 zvE&q=(j^mq)0$UA)9N>r>bHN6`e+eK>0v-xp1J*rKh>@XupzSsNG%DZbiP0t)e+`u zuf#G8#^Qu(k&r3vN%~Z*ju$#oROCG@z-e7t0gSaQRP}d(l+*X0uSZg7am5)uT7oEs zc!hxsx{~-2{r?S%Eu&g)+Qv-2cEQ|w^pnLI1-uU!Q~-!4p~})qhn#F(0I3-bnOL07 zMK)by(g%}(2!2EYTHCYR5=z^zTXZgnOb6gS_KmfsHbt{yzSbkY;w;6k!t2v#%r-MI~h5dL@vR?gBed8ypW!cQ_ zLF8ABzK%*#ewhbYmj0&s2A7Oo-)_))FD*#xXhOb#BguU#0sYW*)5vM$2V8QCut zA_f_-4^vrxpK29KX)hjX$e8WvIy*ITlJSNrjw;S5XZ*)15ppnz2SGO2Q>GZdH&@nE zCMHY1JtdM=^SrE@1=|XRD*Wb^rq(gwnGgJafgI#}+k}YEtKv<8q)*T$$W&LF7buH?9|SpIppKe6!U-AvR(6b#LT&K-NbinV z2T%PtL%Vs;@1<)GaPz>uLjXZG#HR-k&e3KO4jbKFPG1)Kw7FRc)>(*>@)(s3agdpi zXCIv@Q1-MKh?ab8XJR02Y624N(tdX~X%W#gNLm<7cA^YbIs^rAY&7?E>({ScUzzt^l#*FPwr*N?A;CYO&nk5>J>N<6RRw}5cB$`!Ig<$CfG*Q_4|6j=GR(Yr1^lZ{0klw(^8-6E%&;?Rp}U;A2F6MMcLfJhaPsmVHF^A;tl`;Qm@3Ej+=pALPx?m z#a@-ZV)sG6@05!HX{FtkGA?w_7!Mf}(2}?v&@xwXvxP*g{O3%bM1Iv$>w<)EK zAM}zOs;{he_4s-c|I6p`FzTthrK`^sK8G_oRS9?7;Y1|CwX=2@1%R3G5U}fPAfL)b6NxQb`@9dVn;}ZKJ#*LR!piC}N!mXMG%u5t)~<;* zq~+gQ6mMTW4PGhi3hF9RD3kJp7xq7|G$%-XRHMk68bR2?_Ts)T72H93%odO_AGog+ z&|6^1Z>Px%?WvhAwjA)woz`PZ3pY`f z-V@P@dGPN1ksT_wq#}4owg))`jfVNbgwcI|qWbH@V?v$PaU%LZH1tI7dqD@*dl7?n z^dQ0`(qozA9+TOMAe$sDh7aJ((KiG93Wi{v<6t2>Ky7@Jv&X2FB1!hzJs23U^O6}o&04`0+5mvRub-t) z{2eCk-9q(30xsXd!CJmpJ2B&tZ_%)!fZ^yi6w1@tedK~gMn<6rx$(H4gg^L7H^kse zBNq=3<3m!b-L`ITK3nqH^y(StWKZD{SJ<2T=@Q_xjQyKZB(pI0x1MVpyVhw@kxSqa zJ}^DCF3InQ5@PWFS;Lu!x0Gs6NyBNk&*GkjbiG3vnOmh;<$tnKl^dB}f7a4>Cb-J9 z(zsf>I8fjK)^vl4(&K>4Du5Cq`}BrsdNx^|T+#!LCLWPm*Pr(P@P zyEkRmn*4Y=s%Z<;;(>v=*fl#E=RcXT9$!r(K zve-=#hEN=4M3<~AZ4?C^J-Z)d0K&u@dL|MnCS}zy5lKmfQf{HwC7Jv{o?pQ{^9L;3 zKdHY^HTTnX#MYqkJhA+t-tU-jgk2L)dCoUicpUpEGF^6Ljq9po`X)5fOjt+yFA!c zN}JQNZ+WVOY6NXE#rOO}sKuD05Be&R$}w6x{_z3hu;n2dC!-`D>9UA(@hC5v*TL}~ zQLAbW4rgan$E9nE2J1y}$F{|b$=MNtJ;S01@JW7U z^aEDMArja}O{;8mv^H0Vx7g7HBA(wNlaFjPgfeJsC2=j6BO=;K+}6gsC^lWx;D_Bu z;!qUr>*8-m>`s^Ai_TZyeK(WPlw(5P{7&Mc?;3dMgx>@K^j)U4Y-D!0B-{G+8v!(X zoK|epW?&vWsUHsijtRTi6)vBY&A6C>xx_8?hpl2B{-diOWpIuB!^iv}DNYtsmwuXn zi&V{c2lyXi!ae{Q>&Uep=Toik2%oY>3eNn-*BTsdo{ew7K(iGeWYeMpDP@AJNkq%l z-;9U3yM4qq0bPoCX;Lq1%HSONUIN?tE_F0tNY1#~u`J-VPpq)Lzd{#AxD;c$t3b6u zM9bW&-Xw`?G3sqN=f2FbU6CJ2E`dU!kb(FhOv?@`Q3dNa%}`6Rj8JvqCv%CNm=;0m zl`CN&u^;*2MvKc|I;hVP{?`pnwN3Lr;Nm7SEG)e{EF4Hzk_!YN=XNj)`IEdAuilI%!DtBovjkGK$z>Banw zW({&J&u%*y_pqG;F(9@IJlJDW@;+Ne*4}_iGE8I_!_Q0+8jk;h@lc_srE$KgjA7xo zS84cgakLwWLh-!2+u>a2ge(1nFH(yhdXC_Ddbv?yO~OQMjUB^)Va+K0i5u9Stb6(1=8NU{ zu(&%pg0nUE7SHn=oB3)6bl;KPdA^vML|naCCj$kvuV0V7Y&f&pLI~!1Kk=g~SkszG z(7Hg?P6D+#qw1UtV7w$x+TiTUkq@Vrm$5?oeV_Nlri)vmcdWDE8lLyhCkK@jKdTd4OPG;=s+liS` z#OD!}?0Nw5Zd! z<-1g9&u@EQ*z}9YL}G3F`(@-#(Fflfi`c2EiKqq6% z`#PvzFH;WfdtKf0_k+Gv2`Ss?p?>90kJ2#E1?=A^pUG{@2s}QjI%8i#bI zywoF{=8~>RKAfjNmWX;4_H5F^6PE$;2X8g|du(we74h3sR?etFnjlArU8Zhir0DFa zjic7WMOVrzJ^xQk@%EtsU|@6PuktsCrz`X_>E^(q9bT*geX-7hl;t3n)TMXAk}4F$ zTTcH=h(9fBJCSaL^2}?%KJM?JcO}M5lXhM)dwICgzMkrPx_CT#8<*sOK?VF@*XO-| zs4|>=8+8j2^mb162^CkB1NG`QO{C5b(XEo!$BUq_b7@$EhI*9OE@frt2* z>(XZwNc4#KsS-EX{*yJ@8i01KLwU=|N;3?!!g2RyIuN1lc{7(OB`8 zq;cj~KEHi6QHsVrvW(ea&Niceptw2=VrF92!X86w*`fn7sNr~9Y?Dmj!SZ=nK|i$~ zES?Rs77Ivj7zqK{xO-I#)r&D z;wZgJtWYjLYzGu7)l_rc(bY`&*4H8lGX*L$41oFWo-Ql6dTJMfAZwT({bThQZ&nc( zTXKw51Ssek@}_}1bvujSi|Gh4qvTI?q_EQLclSfGX`3ZR%#Jo?DtPoo?^uZ|1E}=K zT_3ReVtw1a-(6rXVUIc55&y}XuoA0ndHsP;>*sMPf9eyBAbi=&x%W;q`;glscApSK zjENc%>&E}3+K70|2n-i>gX&=kZyK-HiQh_ypVShVZMCJhe@HQSc{r=$ETV1qXh`LxLj(rEwlWwD9Mupp)=?5e z0&39+y_P89TALY_29pdQ07K$ksKh=ij!BsCCqqTL=iIL&S|fY0x3aEUH5Y?%_dUC< zoI2JS%&9B&`DNW!S-t_bYLpj0kUXLOg7@OJ^_}6NJwJjYh&-QsQs~EFn|Z|B{(K94 z!zxVyOCG*bn!R1cjS1PMr0o8K%149Ae3s}1&!uxElXY`Xpj;$^yKv5C zeXNzQN!W65iX7w8&pcx=O5l6}NqA?sGl)#qedj*7hH74hMTh8IGM~1^TiARl(cfYv}8b43`JNt?5%ktj=ewiJ3>IS8p(`SaWv;?dq z@}mN^RwQ3tREsR2^sXik66S^sPa;p6&asFyELInp)=L%x=R!Sy7Ou0>q+Cv<5BM6a zmHV0;cybiD8CI;h@W-aURxDQs)P_?l5Et;LJ#;LWw_5;fKE>t1fu2_{7Q-5Tb5td{ zqY`!E+-J1FPr{s8OCEBv(`}YftW8uxpqwQbN$I-s#NddxhXwT}A?~4AsL{Nph>S}*n`ms{ZumEaB8z!2 zN94O86|G^IBlV}G&E^-Qp-J?i()%W5D-QhhWl7&LnYtAG=@4Ek8BKtMH*|9iYcXg+ zj)}WG+^dbS568T#J)y2x^lqISAhnsP&;_3h66qa{Ta?X8F)WwICurt1}A`l7W_`^P*33L@G%<(HRx*;&X+K2 z>UlTHBT)n$Q}QwZ$JwB~%PJ!yI>&jfAaQT1)OAg{nG#A{*UI9KCX`uTBws5Iibm70 zTygJ&dEzRcrJQY6O_Ea}c~#~$F4Hz{Uq)YkU!TQ{@t#d*uNJ}CgWcJi>OQ?;vrke)?h5VJ2DU_S98#^%su>GzgSMfR{J3U?(OnV-8k(gZXPB>U zSDrR?JO6HrWtcBW>EKWyv*wEEh;1IBSMkmN9x9Vk+EvEInIBdDN)M05Hk>P#@6Ak$ z``^|)(@nQUZV{@yw6(+{l(g*YYq1QtaN5LgA1y`#zfC3)I>$KB4!=SsS{gTNW*Z3`L|hLgxgF9!dwH3p8~tc^FD*C={kd(omlVRR)lkl)Xo8 zi}hTUgFWD^d4xr|w=gpH)MzEYW%jjW+uRc;Mg5Wi?=C&xJGM~HLo&GH=czZ-1@v}4 zjefUQJAAYue{N6TwLH^-?C@r{)nfz~?1hl&l9T^(L5k=Eq~+ioIfg&$j7F^k`G?gmQ#Y|I30nn4_0eaN-)sCQOP_RF>IQ<{UO^rm~n3t0?Gr1RRqY8`ol zSs`TeubJ`>1=qKxN$wm?zxT!TlVUyeslL5dY=!D0;2Y*mZZ>gIz$t*Paw$zx;BZpE zYOQ~gvNHzBm%a09R_8KnN2}^irq>KtpT+?g3Km^LXrf8YTI4Hw=(Nb~6G6#rd@1eA zx33c^x4-B0)8(Rj@8`{k-)h~}%921Eu|q0F9#!(6rQ~Ww?z4)Xwde!;hAow@1wV>` zXYy}Bn)J1_dT>J?wJRfqB?L&!1)>S4?x}QCp${=Cay!XEE`#X?i=Q1wg~iBY5~$2q zTP^PHz1jX}50J}{&;^9!iowA28hL%AKO!dp?a`fCtvF=eGTyb~n1;=iu9sN90QKnT zz1D_I3od+RLOl*PJ>?aK7PTLj(D$FOx|YUfPU3HJoWerxJl{iJ{&K#etU9Ri1+Ro#N?o0o6(oD9(>KmYlo$5}i@3qRfwr!zMwi&5*>x(K& zZke(A;K@Zz9=oF!c0I0UmP`BjD2h-NeXI<(fg?q@%Ao~yUOQ-2U~zA(ljiysCz)sU zg6rf^oM<{)N-8?Z&faJ}&<=TJZe9JgGRMYJTf|XQWS%vI49PQ>I9Jt$9K4vzOhl^} zKe0$VdDN7NedZ1Qgs3HT@jOBLXIE-k-$tPfa0ZmdddS|way_^1WXZ|jYwDRJ-VbFz z3n@+;d=HT$=p_Df|HTPza8@{PQ^=W|@9=uqs{;})#E*^d@_-2TeJ8!qH!U+(1;+hW zd_AY0D{V|$^z>v0W8~9dZ=n$>5?s!Rqs?grn%NA4^wWACf52CFoC=Ko;6a)#weC@_ zw?wT;TnX%> z|CGyv@6R{VQYE=3VftQ@4zAPjW!Q9tvXl!)zxkx9nG>8Zzqt#+RjRqO52uA4ye>O` zxg`|AlpEKb_$@^>Fsie7YM_^?Yt$>ue!YH=S3N=ixCP(Qu2d65QHPwFQRWfcDkY@s z$zYuLGbQ!4>veru_ZOAYv{YQdI?+CU}(1s)@mXsMD)oJs|_fMX5^=Zo{Ip} zahVG)Z(YY#2TUr%K;+un^_kKJ{a-cdOY6#`x%=C#0P%PD*Lye>npotza{_Sw!i{%* zf8WMSyDXgT+@wX05T=ibe`<#SxfA9>rz;XLGoXGRK);*qOdJb*(5u6>2{w<}9+zoCTZRx5YC?NYkO@6ERtC*C`j%E(v#8hd;IJnA`R zTHJYF!GLjagxq4L*_9Ov?Jbgy$}CZHY(*;4mqff?;*X14!*VDbpI2LR!ePblnd~ zxeaP&{V~2g_PPP1d3`$?)1Wq%f1EbW2Pr9bh^y7KsecoJ{v^lXg^0^pPr;en?8=jqzm9;ualAw6E20Lf@qZh8lUptD;&MTU*v?LDvE9 z<1f;H=@2kI6OOAg=5K<=R>=;YwYuc;m54#|+j(zZtVsCp*ON38s!?AjNZu*d7mDNT zyOZWjY>ZK3GPlKg=u1p^VlnWWWnGIyKyx}jDoC+*`U}&OtMEb8cs$6%ownaIUmdJ3 zB)bEj7RGt-JE;@~^tSge<6uKa7T}E>Hx*e|3l! z-jnk>`>e*KJk%vMVDCtto_Y@9xL^v&&Pm$xQ2|JGwo0e03fve4dctIgHt zQPjK|1;_Uf$J#?PMjoxeh&56SQ6EdI_ggZx}blk^-VclJpGQ)gFn=?-$wTtEfU5Y zB>&S3@SjCEmpZ2p_AEb%%}MHb_@~EX#RpIAzolj_l1$wU}Bx9(s5v0x7-bcG8bRI(C zEs2u%6Q{^_#FVX{98D_DIpnUwE3E_mQIoSZK4xeVc}$3G%8^g_%g|!2e%?ZSUW~J+ zs2MG*0ScV4cfK;`TUAq5Wjk+cu|C}VKqE}%I2~JWidvgK{vNQ{sAS$S`%A*^QlzuJ z^)~Ltt4h}S6Fnb;sdAzSU7t$V zRp6aVoI-vqWte$;B=DwGy&)Z})T3@D5%SB!?Rfs(txDHU`?3Ib$$~rJrm10e{&|ve zRkuR2>60&n%}aEOXgD;!uWh)4O-|`d@;$qCA%)Whh(Cnt_h2lD3rj7d1FRl+JYv^f zZfVT^=f*a57&}*PX2=siXKM)YPz2@***z1Bi|#&}N~&X>BYAcuy9gUHkHCIIRREnW zIZIQNbh*yfe9>ELmR++r`W=^_3Wp}+WSZl5Aa~XH(cQF!rZ|-VIhEKa8MFVAFTIm6 zMEvUJusR4w@m00UH7h#A6SAT#>GVbwL8%0#nVK{2US}%%!{uedNI8AJMY1PQY-ay3 zsfdDH6*bveB>cAI&*^ISODS4)9PQvZZ|bm#Ji-}Jklf+T`>To7qORrNpXh(6KT&== zwlJko)m{})e}s>HbAvM(#hLgi!gyis0yADV>|t~1sX;JVW5<}F2IW?4s>wCq zn3n!Uf+B+%(`k35GU?!tfC#tx*wN#Mgt!)h*oQCu4i0&_uWtPzhOzv=~k9W zbl$ECaGVL=7U+m08>|4>lMTL#Fp{&gPJSn`%Cu5mx}oOSn)4;kH#$5{h0tWpdBIKh zBB@m9Ol%V*PXdqI_@54-+b+|^l3U@|T-VLtiRG{D$mxMW`h`X z{CZ=SotCh=g#6vfJ*Fwpx+5WnFV;SGxJ`8q+-FOxjCd}xU|-Lb&WB{`kU>At6^^!H zQSJXwdMQlo%ulmtC=irhZ2VEqbzUr0C#gFz7lnTJ*pn{e-q|xP)g{vKOM=llAh2$n&p{18z5F=9K1uU!=rYWpO0AWlpqP8+Ff)o*jp}^=;|(`{2NbuW)1F2(d1J})sJ+VWdGOyRQIFSWK_TYf_lyy7mSwP(hj8&Jc`zKb`@vO z&ep)#GzVJw1P$l3yshqwRGQ`XaKpUIwbAre0E9j~d=?YDOd39dTM9Ef*8m*)2IpQ9 zYq#`g-!&#_@YA{?pZ8a(4Uv^pF(ay*S)5&ccsrTWvlG#@dKE_Lt2>353kT8?qq(4)o34uR`8%KaLd)Ltts zRnp~2B7P#s0^TV86hbe_d`9v2#A)!DijdujlSkNd-YSa-uNCK68p`?ew>TK=ZW~rq zw7{YNONsph-y1cN&orA4%HDSHmCL@-0di}7+v-Sdk*JBS^}JLEQf;2{mAHvLJW%Un zODr~PjV@D2!ep4&s|P@0Q6xdrlwLG>0idgcqF;lsMchMZBknFyxIQbuFvy`*O9NeLNM+3$Bp%pD1u$r z%Xe%kY#YGF$udo@_$w_TZlAuJo~L?x6xYDR;jUBfE~tvyi?#pNIn95;)6ADrNwMxfEpi?{ioR&=%BYnnD0vP{ zGCH*@MPyaKJadLwf+1Jp9fmu6Q1`9sLiZwdx8yInQor3OipsQhvTiXYX^7q40T zKtM>LZg(&!B6&I^+)KnUmW|+@uy^ahAg9X?B|wiup+p65@&(kgA zDx6*K&hilN9N9N*e)d*tRXWx# z;;HwO{8jas?Dom)%YyN=>UtEtJMEoFPY!&Q@Xwp0Lt1j?`DpZPlQp@%Z4#95%=6TT zZ2mgnCNvFR`XAd-UE#j>yShKlvQaJu&3t3Ml2?ON?IV?&#aQcLfplRl6OOM3i5Ew+u&t*a!u z)87A3QBma38)x0@6WstT^EXroq36zwQx=SDRl#mT4hZYqbTtG3;HoR)H%wkL(I!OSsN4SernEOR7z0lB$cF76jP?a3QpuxwpC-f zcwDNI-7!V)AR@aP^@LQp3%2aB*x45pD8`_Me@=cbwRo)0R991mxNio^m}b$ux~K1$ z#;;A_9|qkM{+r8oe^2QYa4D8y5~Lm`)xbl&EPJoYpg~-Q9q-~Py2(5sXARN8yg&L$ z^>%b=%nB6G$(ezhGK~8?itsq)Re55DmMCKZFJ0>R-;wH`i^TWb!e&4qHk}}8BOtO_ z7*{$Fa~cVfuMquFvCY@qwXv!8b#*Vn&UWs#as$OiDeI5sU;FjkxUF+E^Bh<>-19n$ zqrKcOqfo*g#o?D~K86Hk89RLnU>?&$oula)3;w2-Mq&~Rh2MQ5#r&h0Wk?KC)RAgs z)!Y+Q4pkFtkeO|oc(PE;fu!6T@%33a`THH`u5!)F?wp)O|50v+^Q-^d^=7ghF;DDy z-y^l>#uw8LJr}q;<6m~?*J5l??X-%vQIa&@%~8Vd_D+JrIsXw-J1!U(8#&pEsF;Ua z!^Ql!*K!mt1jLXr(HOWsCfg$?sW)WEQ6@-d^;9TwY+oD-bk zrgwd$b0gMoM>zS3%s|lp(Ur~P9m6~GuAlMv)3o|?T%{2+3#vqylU2xzrEW@ zH%L#@(mUM-PN7Z<=Aje{**D<^#03{$-vKxm^s*&U?@c8)uWNe`z=BXu`vtC180Q#a z2e13__=8`goSdQwxL`xL$gr*2K~jRTuM}0kXf$xXqveK87$<4dMk?*bpk{XM<0O67 zrUPvrLG_SL@3m{+^)UOMmD#_u1EkeNmU++2%1|ymTVa~GhpRzc)lxsv)8o4)e?TO? zLa+i84jU_+@kp?*Jid_{d$wM+N`;+`px|cS(aYju6erdG?kk?rKMY>aoz{rzp0M5{ zvzcE)0mvk(n2EiwE^xD3`18lcN$iQ=Q8mFa1t@P*W=w%(i%oY(+3OLCISuHdA(EJcs!={vHk zb=4BTl=0H#2N#udAq@ariMCNA=gHDtaTE0Me=M6~vx9HvqIdixi@w@#F})rgZk0H` z5}pvh&a=LAkTpD8WpMV7bSt`yJ0=YK?+g6+c@~4FuPSTTW=W z?~H%k!r_)0FUC~K&IZislWdZ-v;7R}PdVJ=^J;zE^-5p94qpae@o27HRgz;+Lk)1b z#yr>H=*h_TT5+c~+Kx8j)jp>28~YG7{4ZsPErEez#5mJDAX(afC;RG|*{LFJD&LsG z(p0QOtWPJ`!pVoalHs1^k;Z{v_A<+F`|daY_2l%)dh6l_F3yzS-HAB>MfjA_V7&%j zc5gn&qFQb> zt>l$YTp^GTN;b6`NVjX@wm_*|64e~{ghrLp2y;Q*q|25!g;6Xr_ij^u$p-~i0G=d= z)rECw`9=*b9x**3OaE>>ef-xJUKQhRNy^$b#Wf^RbeY8-)stttz@;k;vjl0e(n1tZX@lN!{al8f(DwuuF)&!Gtn>ixT&9K^mc(pydFty096mQaeO3%AIf8JDt=C4kwS-@n{gn?4 ztYGU7I4^C;1$sur7g1L4l4xCtb-v5?g$;cyN;7sI#u-gaL1j5BXvoQcsK6tkNSuuo z#+%0aeDzv#>0m5++5Lr=q9Qu_aaYvMm{#=F)4bAIMMsZQ_|q#d4W)oRVKZ&3lPRLg zJnd#w35$iOp8i~eKO?*<3bf5Tjdti1cM%ZAnIF+2_g~5v3pmPsf}4{hlaI*@*Qb&6 zmzyHeK**yVnod5kHr`Kf=k+{cRX2!zttub-vLQDw;`E&ME5fHibESl|`x*XAAq36* z9p;ZHk|Lf}am#2Q#jZ4mh^86o4V0}k$9(x`@%i^lQE{!o5?}VQw>EqL;1Tdt|3qVc z9GeDc03hCft4H+>%fRI<IVMGN1ViZsC1vo@aEah`69UZs0h>$;W9fABoM z@$*~7&m&$c^or6O0RNZAJnFhq`L5G))}CqH#IqOBkYNJYy02=|AwDzoOeB{t20|e6 zI!?WTdJ#K8uTZRDn(n}Hr~}Ol@VDmEGABarEI?l$L{8)6_8tyji}B@LpZwEfMF^z# zP>DI)b2OZqJ{J(p*NEi6L{7o`=-&L5U2;&V6r*gOH#X>9EAjc(P!i|{c|-yJ!OeKx z54#VYo2)f{5rl}kA1a05@TpRr%MBKZDm=`oJ!sH1m%ehJulQ*~g;YmLK&A?cj&Zv$ zGYla>IWq5h$}hpEA9>$c+`wX%BfM87acF1x1zAXQqDp_EHr_Z%`bAmuiMgOmOsBAr zNn@I}0`i9LH0di_usQDlzWGudqv&^V?L5LPtCOll?{9AD!RU!?z)EA;I{Au7_5L$l zqed01Hz3*ltMuPlXfZD43)4U~7DLlkUmdI-!pGjTB}YV!sbMZ{ektV?#HxeYT#!zs zm*SebTDQNu|0dtK4O_8wkncQZxE`|?Sy-B^Eqeh&bjE*v$dvR&z#lPO;u}z4w0ee0 zw@p2m>i#mGC1E~A33tP*xcB7IPUrMo=i;}oW%16q26G4NVdBSIdz?9ZARXUJkZ8(I zveIWUc-^>PnRFw@;E-w}>T&E&?z(OLZv7$S)}rURv4K6ND%nPA7M@2To*2t#bK^|51;@MuKrn~T^CVmme;U1`=O!sl7{SbiwNuI z@K+UHW%S(dT%GripldcQq9Oh%Sw0OSJo(>^c!ZPDRm#J@wi^Gd-_CG_XPh&WgA0d@ z0zw@U)4>K;MH$MvTyOE;zC7u`A$(B;9xilL{)hE)hfd=SWXi9u6+x7(?ynm1h_!AO zKvy|)A@(=f!FlOT%uq^VtjJQa>-Zw2YWxD=s>wYn)eA0MR{8Pv3MYl7b9DrFRkTIJ z*E02f|CVOg)B3X4I6|IXj^K!Pk1|za_xX82UwZG*6&lu%tUWKmAvmgk`|9Z(jqt+H z$=WK?@xNE*vid|9c8&4*(B-X?PL7KY*a?UMJtEw6=vMFHG~jZMT7K4}^A@+$_GxMC zmJG1y!dTyJ+?P2mElp@)_W!qOGPP zru;@Vm9JB!HYz1UX!^N<*VMQpen_-PEX@6*bJcIveKt3;k7v&PO+Z&*-`rH3Zp3S-Abx(-1#rt!Dxt+nLOcUdd(sN&UZg{Tc9s9cshs8R55k;V^ZoEK65 z`6GXO?ttigEqOo@}x!AyU}>=(C}dEBTN)q;s2it2W@N-JEu6aA+4A24U+N6q)) zCOEOKqIsff-#-5<7DtReXce6EsX+V~Mhs06Nx69$+aLT({-+?Pw2d=uh#$@y-D|}o z&;Y9tN>GPua$8sbuKj)UFNS`GsB3tfdOu~+hHoTdb}4dkvUdL*jOgSUG@*lwd|k1( z^2~Mqwrq>rk!Jvk334Or+GwIE{|!%euij&kf>G;XQr!-U0Nn;bogQwwlrVF#h zHZsHzwza8m;P`Oy==J$r#gzcBz0Qon5a-9A+uOJwJO~ct&{O6ar19A_GHL>@cdHzO z<}XzUAAqJBFB7o0hHwahu!3)8md$Ylc7Lx_ekuU2!?;DuO@}R;KzD8&~jFwsSW9=CL!j@URLpH$D5x z=)L{N=3eaPxi8hafW2MYzsFARrtdlQPVThC4@!54&yG*liV=|AE74W-G^yZy%tlgX z`I!F^hCSvbjks4B&M{V;G?qF^JT+}91!C#<8A~0(F0DtXsGR%dM~2$sVuCa6^jEKM z^|4I;3PA+tr{ly|%KovnJAZ8W9T?WtOs<)1Jp8TN&}Q3jiiSMME-IDnL=d$-W~~|8 z?CdH&e<%OtqbU8?(&y!_bI0!mDMbfzgv4&Ps3j#UI2jQYpaUVxdk2rsGnrJuYZT^t z!#C)LPF4ki$y)^464q@d9H#H}4JM(C z&>DU4?w>?n9iES!O_&qhZ^aW(dv#rCzB;DJ(d3zqm;C$Pb%S~%hI}$3XN-yelu8f8 zJq#)$jmyUTBrLvsEe}vV%=$%lfXk(D|5dP@S2+GEJgLY2N5CvRkQ4lW8vD*@IKSxK zXi=h$=$+AfZwbO+^cXd25P~sUf`}eHVi<-XM8qJ2(MO2{LG&IyqKlv2qC~xK{`Y>l z>wdY*T9FUl^PcwZz0cXtex6`)r`6E$4;q-jj581x!N1f{oVASjk{J+_7FOq{z7Wpx z_kkpNa>l^Rc6zaa2$4@WcU4r7g6+>Pek9@QjD?h4&=-1B6^w6PPbTWsEd^c6ewDBS zRoi2qKwS+=7g@5@CZGCsw@WZOV1_jP2;Q@n5|)3lgv2!UVZH|S!MO*nRPxcv_*s5G z1ETcUqv^`#VnwWr(kY{6;oi}GWfsw}y@%o7i4lniGem|&TFUJUwI5QMz|kY-Daqdy z-m+ pM=ik<3nWxE1*t8%*)U05)AVHu4OUusSlniI0q3krX_i4hI1#r=+i3RVaN zvcXSFl+IJUOy&W!Ln;#K5(CUKiw9w^aKH|PdhEK`JdCmt^p$Ju(oLKov+c{?bkcu} zO-1&@BUJ(3aS;Ruq!jX`5ZRubGx%;hbN#8W+?xP`bqCq6SU+{d`dQ&FFaRXVCgl|@Sxr~-7};9sPXsH zIV`%HM@t{BkMPmJJPD8uZ_uPWXr;1yd!I;P3Ca0eAcgP!TeX&(i)!33sFhnugrimO zyDU|sxel8i=^Fo@S3LNY6X^L=X10EHGgRp8y3*>6lWw=lQtX3kt?5d7BQoXNM$-oE zVQ54$UaL$^EZod9NR*4}&5=kt$a1Pv@>wm?dc7!pr)AX?1nbX}QwAl54F|A?eb|2l zKwz%a$5fFd|A0qPm3IarUhUZQ{-u89Z;&7yTd*y%x}|vhwIB9y@UFfnVQ+O#>;%qO zRbHcspoQX>#_x9HCK>TWXu$3*wdJsz;O7iuexdtb$oc8RM{~9>eh3JPO=s}Mrc|QvfgR6bYD}Hy9GbGgC%(5g~Y8Raghs|?c{zldSF1`%8 zZx_xLcwCT;))OLUi3*864a-0XB^SG&Kiq_uNv)L`>aS+S;SD-;N|ZjIk(}4l{-pv1 zZW#(42b=7(NjgApkjQ}UAmYELAD(qJRiRwD5*+&>EgjuqzAczo6JArv3FKT%!1Ff; zGTMkH1v!l0v{Uv;MswLYc>#lHtmT9L8)pzv=mdhdUF4y zuZpuO`zNhKOFbwt1E#1kiO7+JkJ*Tvdq41RxmM>=}%1xu=OPBDlvOYIISO_bHHEKF3c6;nY z5x!^J5Oj(!Xhb42+5oSm-9|(FTk+X0MmXp`tsXs=o)>RYes9jFZQ}KbM}}UhQB!@L z0Qrw_7d2>BnW%J_SERpSoVLn&ep&S1Tfmhr$<@9$55t`^*XP@X-p(Zq@07X*#vZx1 z_FqS|A}aj4V>uU3FZV?S>?KUOW>t7|WzAW@*`vlk`5vTh7b_Me7h14v2erK|Csuhi zM^p8M(_R){{eHl5;?L{{BXz((w+^%l?*3P`EJ^ZLadGHi8iW0l!{fvUi-+2@$0{w1 zjxnL8_0?o1cLi2I99+3v7vVaAB6=H+z&;6%x_GjkDXA7PKW?XYu!Y(eb|bNOQAyBJ zbmkuL+%N$!Z%_UIq>NHWPRUYx#H8L$=xU&Rk??R>1@#6`vb>XWEckZ#Z;A6bZI7J? z0H8~o3E5WEXaCcIzAD{_FAgJR&x?%(%XsbGnrE%s>+Yv<)$PWd!5>_wL`eepqw#W4 z_bts!R^+z^^?~V@ai79+IL~`?^*S}%uyVvVeL^+DiQMLu1CYlU|P|Y5&)d2 z&lf07_0x)Y=jAtGfy|EMafL1~0T-pte)hiYGCns1xn2*XJbg;qwnx%>@C&ekDHfdZNTl9BT%N|FF457RQ!`>plUKQw zuDEwwaS_E=ua{GoR;AZ+*GV0BpL}a;KzxK!XR{3j+>H=;>aFJV?74UGO}$O4XHR_} z_Lu4Bh0=M%23y;LsFEcIhuPwu?;m8hB%zO>+5?fV6}Z(0ot)8*CuFNz4FT`!)KHx* zMc1lH(tGW%XXFM-I39~9vZty~lQssOJ9@Z>7`+qQNwzn?{(e&P!G56?=%Yn)%2Qok zhJWBU%FEccOaDdA!zEF1><2MEr+z2o5#1?&SYACh4OX2vzy*OG*Ejn_25IC{`kn*& z-EQeWUnFQe^yq^0YPQp-dzSaOq|T^`Udo)Cv{BXilJt&ZWd0aCaGOqkaH?z-_9gL8 za9*=oUBe|cRN0d^05fxe$6Up)z03?Zflsl0>Q%?T2y50{J2)GvniuuWiRMz3n zi7w#pNTXkm{39l2c%w5?&*ZbW8!?zgT2-i(1Z-bHzul0^aQf5Ple>PH+}cd3+np8n z6CCq}&sd(p~po{k~owvp#M04pyO;VZTDFDC0@%NfY zW{2l{1Zu38L+ZZ(sua9R6$b~u^_QBfrvx_(vZsEMBu06|th^pem}i}I%ldO{RnNko zh#eF7M?gb^tS2d5uDGD3#z<|KykX_vV>4l!#AJKUeW+Wn>$Ex6mfJ`ZYi=0p4m{fO zh3~a+aNzdy+NMbA-x8KoN{jW_u>UWH_2W<5pGC{|A2M**76NYK}bRHHpJC z(;8&xN^oddUt2FvIVEk~KM7GL;rVMEbCvT=ll$n+uUQQ$>C~!;Pm~@Zx9qx>Y!zG( znX|giaDJ*}ibVU+)~Jxd-pI1ZdB$Jj@mR@$=}e0Q;xMV?wA zR+IaGd8^p!4u*mLtB1s_%#ETtm!l^42gxdly}2vpU?RM-2!QMZDpE=2bnL^Piz)14Y~YxL=a^bu`x_DT(_n|c4lu~ zubk|*oK%LWs+3lp!J}?^Mq86wNY2DJHLWrq-KPkl*2+8bQ{y>(>?8i|(Iw}DD^Gdp zw&ktvFUg8iz8)Ioq#xqsoCQ~f7yVFb)^21)opgM1z&Ew%oqYNIgHZ~}^|yrX&xIY` zDFg+ZSK4+jT1XmR zR0MyvqyOyrKup3Q*AFmf%?d6K$Aa_*@3#L5 zi^P4fAKUAmoqe^930dWb{gq!la%fhFa-0LgT(37oKr~5~eht#iSGoc1_y5Q6ynlp& zoULc7s&#>JhzZkEDE|%BYe2taePQ-jpb7-lrgS>% zrN;^O;y&e@oVSiBm)em%DKo4~9tG%zQsg=ki@3<`MEW6nzf*z@;swCqoGqe9a_SUgMb$+u5@K2zGb9-7!O2ny=#MTqbB|a zgK^mfCAr?4A^OIdS%n#B=$nU&b_}3v@6r@leo^3WXlJR8L)WNP3(;lSKf_{+4Aa%6 z_-E8_U&}7Gs;u8$Y<0Hca8deA&@>Z~@W9Apmf!wu(RII1kk`9eN!>Tqd$+fPei{cb zSsJ@N0^203pxo%N^ylR4UpY*9sj-gOP|v6@42g48muyJ2$1mvi*?`7Ry`OzjH5LIz zCHd`s+h!&17tv+NV&G;EpFDP~B3fe}d%nMw8{d3_57+#uP}b3K)@AOfW5#3lC3#8& z?_YbCC0-&XcMKEU4`!c7WjKf5MP$q(JPPBKhKTcrmv;M44}2GkfEShg_n%PPvTNQA zdCg=;F66)Xi@#~O5rQ(;* zixtNqC;=*?gdL4hOVME3>h6(t!Lr8!sa=(Fi4%872bMqJ1+(jhQgeQ(6Md!V9Ry(% z4DR?g(JM1eer5||+gpzgyr~7=v?rom#UV%o%#~zYY{jMak!@7O_oaNuL`;Rv(5gJy zM7L~e*aLn3M|wnKI}>M`hqu15@^{`{c2;R+W6lIK*W3WGIThUW?#}z@@PD!q8vyxe-KB`d{mR* ziLf7xt$!;*0}AFv;CKeyvX*Iz#M1Owm2icV1KVbT@F6Gl^un9+Vvi(zUXGfzx$Z#V z{oSGR9}X81#18d{Ud_isEL$R^&98)6(HK=kXfw+5`qZ& zpk#Q0G5xnH5e$2`hcNxSIUK z*Wg|FsJYkvOUusJ#*ou6JIc#lAW#cD$R6<%`xpB_7cClZvJMVliwMk=iX5Daby?^t zCUv|!-ET?*_pMeiJSBmYtiFEKDuI^=CNa;aN7$!QOwFh==TB@lT58QYzpcuBA zEk5cq>6e+jh7K*xzdb%&oVFwQ;tv=yxabfHDc{A5{;S(Y6#a2(W&X3RK06K_AE5cW zW7zR^bFuhv`Rcgt9R^VSkJ(DJeX|K;f8!m4AOFqip=#E1{Y4pS2<}Oa(NI0mw0HD> z;?AGObG6k-0^@qSI0qo9CU9|`jwpNh+qBs}6 z{W6SmH>W;k&VREo@E2c^zB99hD^y5;R%<})JI0A)147o?t&%jQ;(*gqBf6JUC|hgZ z?6TryVKVu`{{>HXef6Kp<#v!-+^O3Y!w$iowT~Dl=#mIIk03!l3_XPAJuO;?m*I)I zU{8qVcJ?-`tN6o?y^)boUsHr_dVFkGJ) z61~S4+1do8Wi-00fszZgwgsYVy7l*5tsP4_x}Lne2sm1|cffw-$#`pLc?@ zzcj;W^|ZeD@PM5mPL(3ftP4j#~_HjPJZhVN#)9L&UHc%@x^uN$S>FpQL!|GyHj*IV zoJSca5d6@g9L1BfFKlNKk!;Z}hs287}vi>4}|Q)4Wy?`&xeD8w(}3^uLFR-V3Z04z-=GzZd@2@y+EISrQE zAAvZ^_wh|d=*40rAbQB|9`M>-HIhr0p)0EnpbD});$Ki(@V9)!`k#X|K>$90Z;_!< z3Yju|OFj0&V0F4vQ;V~R0f zg>CwXbaW`y#1LiQx#L0RrZnS_aB+maYpO3xVf3>o3$7<5stbSub#k?WwMKSXb|b15 zS^!%{tTOqpz-wyp{n}Zp(z&-{MuZb;nGvtw3CK+jm={UviW8YzM9RS zd~LK+Oa0HJ+y^^++dl$-mD$B3F9ACzX`@+`fN%v7suNuHhrgZlboP5Yk+L}773%RY zl=U?8*8)EmvJa)5=<@!KZIapT8J^Z#Os_b+wNLV&Ds!xw<&Vos*~n+Y#B;8T&jk^Q zYe3SR}GVmoemebNT}n49R>6H27GLt_07{xM*n&6ve$XHT{#*^_7I*#y8s0o zwQXC#a-riYMHkAcxIC$P`!Mxx{2xzts$h&}m9U_v1uraN{aYiv z?mNEXtAN3T?hq4DBYH2Cj&zk~q*SXAYChR_s*}NoF+a0 zp7l^ZyC0hNc4ru&=2xCDwSC^@sq*408xqs8{Hm{H$K)plPJi|pc*^ALw3{l1R^cKorv`fdkg8$QIGS}Nv6RE{3r0wYLW zk9?P_KMgQeclwrB^(G&OQRG<6z3nI4QlOjDgjG=gP5b=o*I=_0Tu^< zV`9Q1&*V=D=Cq<6+jJN$bv<}ea)u+b%t#s;gN(Yy(Ew@1rtoAuQ2`FbqVYK*yeX!4 zyf>>-rZ=E1LSC;lI>##aY{UGlJE*w!gP0EQ(v|@XT=O8u+JP*TF-SdZdWxCAvs>IS z{>q@p0BO3b$)VP;c)heg*cd-5LDRDJy-o4o*9l*I3P7S;9!?ii|0@H1OBBIz_KrQQ zF`y06Xtno+g@dK;h0;SdfZCG%U#%GoIUis#RuWv558<@&kEt7`K-I*D<*^THT91I= z$a`5WRg{~pcGy-w%=Ebt+gDct^TvdKKgaF_RqSE?M&gh%T^FQKJd(LrMRS8oKN)h>HM0uYJjGH_4}AJ^I^tnj5wSJdmKZx7!paE zN^QEPX+`Sr)uM4rgrPnM*sMF`;FG*2+n4drw~lFX(Q}4gVa`1$=r-eG6l6XzgrvSc zzbU+dmu`zS@%V^#k-xF=8D+z=x8T*3Jqqnqpma@W>}Qwp>BKL0^Gqthv;Cu-XHN&{ zx)(dG-JKtXn;Dfy&e-nDhD2pfXM#K+pPL}`%GbZ_+{sqG9mVnQl+Uq_z+;+RD;_- zA|$Y2?g#i|c^C;qNjs8gV-wi>*tO6y;A?W}S&YewD?UqtL0ZTZc&+NgG?T zpH1oBW?xJ=S&y{Bqi4EF{>8Ucmp*Ml=?<-5s7?<%td=-;2V%3!Ax`$}r-<2^(<*N( z8MgeDYojB6c83t@nyOUYPioGq(yRRPnPv_yWhuo=qRjCc&I_Vzwxy}k0Oy4{R16&; zD{(t42xEeM?n$Yy_e4ZSZ-OOZ)fPIOOQ7~DWO1hz0N?IBd-cy*sLw^zUJC72)ZYaP z=>jepl$NW~Ug3e#ZN|e-e#Ot`jyM-+>=M7U6!52-|K|x*z%p*~<@jLL9upHr6^nE> zh{Nwg$~64JHHppm)3pb!&SChI<`{@)Z>KX2g6WoHbV!vb!|baL!3BU&X%N5;*#3FC zbnaX&bnQHcXk6%Ns!Trcqu9(}9?bTw<*nK*#d~_3kOo5UE!}xxK zOSn^{Qxd02 zvRYKZZzkYMU&=(*I^pJ!!S)x}j`6ti$WMG>l)AlzCtXppud9<$ zaj~>81rYFKl=pj;fKQl~Sv&S@BZnlwKuDBfsHWu_{6!W{eW|h=!6bKu|MOKR*?l6> z66@aD4V3OZBOWH;EV}p0m}sy4m){?qMR5QCnQytPO8(EsRsA=0#2EIoEA~+C>}*7G z?wOo&_x-?GP_YO<_2A3ysxaF965n-`O^Gt1KMmVm?Us`x8FA*guoOhcb20VT=aGKo z;NvS3{)903-ro)QlTV@+&v)?7 zZx}PzSMcL(%sJn{_s)#$ocKp3iP>j9b4Isl+UYvA7+v|5lKGl~h$1LMIcNT2*c&s< z^~tq(lhDzBcEFp~JuA+0vN~_M(;=_Tse9;O$3f~`g38B9-`q%)D+j=N!hcESX*E)V zysyqeH=l5|beq80LrU*o=k!npiEafgIt0ng&2OD@@Z?3eGS<>n16z&wXOp7wQlP{* zd5V)KTSfl>Xy=@F*SpYaCHF60iMNTd#JDcTpJ11#7LGpdZuhWr-&9%DN*cDua2R_R zs~=mIDb>{?M1ymIKvjbO3R#B!SXG+HPGCKyj(G_l%h?LAsFUybL_sWI%Hz}mMrXiy zvGG(U1!e-Xc+~LXu7nD1yTOf84$hlXJysgaT2z2A+k8fGsy>xs7h3U6IL4wK#}4Y! zW!wnqQDyK5bkqdndU6W;cw1(4PZM&d$Ow2v->QDfGvsNpDaT0GYzNcexn`7jMeKb^f z6{?>I^#qqZ1h3JV@yPN>()F-Va~zG#_xth`;l7{Eb2W0U(D&mS_kXPW(np8Qz3dWC z@;4AFBtcpCV$2caF&_VeVztyfOi(@>FZ*d0;L^!`T!tCu(Vyu4)cN82U1DBTbbX5t zO(LISD&UGuSflTQ-}v50VHi}bcsF<=;*>6H;+NhNo!R$6J713o`_S%LAptC-jFv!} zlsHPqIEuFV*PCavdsN8&;6{9ss9&W;6D5;D>kHFB$vwsR<2TJpUvNi(x&DnD4S@mg zPHh#JG*R5D+6LBIE9C|~Peh7FbMCc}1mQXvKNo5H4!+d+B=kE`Wp)Ll+(mp$woM!$ zg-$k>DU7eu;OL@2Og^+65Qx!WmDTzKy$ z9`ZbQZCdf<)Pr}62U0`B-VWh0h}WmRMhxvWrKxj4F&hL;NooP8c&G9#5KD_XCA=ut zqH!V8OtaNwNg`EwS9m2CG+LVKTW-cvkPK1QR4ayDo)q2H6XP=DmG9&+(sb??$V^Fg z6_K>Rlo!G1pvGzAAgtn(%Kj^IUW&ez&rv-q+(PN?)-Wg>X~@d|gjF@(nF1VuT&uc! zOh7M83oT8xW~&ob`Tn|Hj-t34!@gOY6u2iP$YW1esmMg$y)JU8>( z?WGndw*6oH^zmYM4ijbh`7o9;0n5Ov;20J|@r3cxc!2RjZE){5^zI)pFGub8S*8fB zI*vbm?57^S@1sRRpC2XlgyX3S#B@cR?z%Q!EnCN8X^GS3uWuZl|M24a+q?*Z|IL z&QNs#zI;@)#apdf>km-UK`xKeZ2iFgqAy96GkAq9L^D&0e^rNf)x&0A0X^$;IAl?k zo6z*5{Mqlf>Zg1Vkjx$oToOveWWd;^Nz&M!ZcPImnVeacVQv1L;PKK_boJjz;lp8L= zgVJfpY#pGR(mgJeA4inF+eNsz1gOt#=}9cl#S`Ow1~bNi{Ze+92LlBT`-CBz>}f;N z;=+ddoK?iYnqgC&Y-P&4gM%zTaqFWK>fHPzn^m_8UsvyUYp5DG`PIIG8{kdLRO*(> z=H#V^r^RX!$@K4Eo69YjbY&VC{Dk`k*;3IrO7|45;y&YpY(0AA1314AHDx^Hs`_G; z4UF#JNlNBkbNziHO$M1ycXoa~lD2+YKh|5D2)rn{O*N)^zro2CwgOU>kS2-xXnf~i zlmxIRD+GA~@)8`iQUDoy_-6=#p%F74Bkctc{iVwYwpO6dQ~Wmq?d}AhtpZe&8E*o1 z@1e#(0)jNwX@msFH?h#*odMgGPvvml%m9>cnJF0iRzA*iHM*@#>6DA!#U;Z`z($@o zxzKzvP0ll+15{5T$S=K02CkbBLS+r=3pBmVS>R7Mo@qZcT8wRTNu-DDy2j`DW}GOe z1oKMLCGh9E@=whD4d_&uya`*ak|T`L(|i;T5Y2u$gz$m@q8ZM6Z^Q>^z(>Jz2F$Hq z%OpS6M`K+>80E~cyl&t& zn`5%k{?ff4i_Fy=Z4q#dyD-^r(CzA1(ZK=)StB_<+Kl{QUf?ZZ1F{N$gh(k$YN3=m zyY5L6;_A;2zZ-IO-3D6DixlMC5eI*e1Kv)mlIIaR_;J%FQD&tEH%=4t$hX|VyBJ5V z8?&IQVPj2`p*!dta@O23wtrNe6i)v_V)t8z)uMx<+Ml1LI8enVUtkbt_DpZcn9IrpkqR|(4K+F3_L z&>1!HJXr)VchLYTaUL96i?!W&%%@ml-)NOSfICBwfTl~lDngq1j*^PRg>381~8@QPVeQfH(&s!Q}4@1(!5ucAO`1prk3{<0ZJwEAc zQ8Vv|vVCT)k^urYa#t!_yjcc(LA z%8}atdA5lN(^g@V=DZXY3yp*w0nPbqLby1zLrZroz6y{ zWw;}M$lw{OTJNOjanW6}1d~tQ+mpwsPGuH+V}d0RF%bxidp7peNc$?v{3Nnrd3mtu zKtI`Or~WwT|IjP*Z*P>?0^%4y&sCq*8!cwH{a|##^2Db3W@IXXk4-f>L0F(bwy(H1 zA1}F;cy!kisqsMsy|!+Z41p0Z|9l2c6-{rqamurevLebid{Yz)#ne={!(4=Dc8(vi zcGRoaU8n6|8S9ny(Qe9-oK`lJkjp=D*2PV9zIuF?8ro<#T_RqAu88Lo1R)W zApfQEIl_y%p9>AK!AZF%9abHuKDUplBMqYV2rGyj?#wa1(H5(-3L%8ICsn&F`c3C5l4}eri%;&so2t_y9YXP=sH}?(C?0iOU)+NBZldiY> z?n*Y>Q-fe^AG2*3WtL{F`xp)ZV_l?ET5hCy2@;|*%lL$}>nTm|hFVX^({jmg@xVBQ zcO5=UfGQFxF{w68DE9oj5rY$Y_GHz^sn&U-QA{fi3^xLcE7{U%lg48EDON8+doV7o z`Tg-#?0bxGp6XEYVCz1-hi@f!F!g$i|5t%O7}L8&r2wUGODxBxJO733F1Xi=A$a=5CUK zE2T6Yz-XpuZv=S^5hZ`UZ9`hX5Bg_dV_a#7-bY=uQs1FN-+PNXe;@3oo(%B#bChXt zigE#yO3hPFfPI)5;3#a!8(ZuAm;F*|GIe2po9t&n7@LDi=37nEoFr{t<{QF$w>Zf{ z1-+A35pD5bTk2S4H(q@2qdjafs!9Kp?)k3xiZ#T;-9V)Fx{LC~-Q9}Pi)G}xuR(>y zeSj=tkX2W7LYgtyx|(s70FKf4pn_)jZIF?WUmvdDVWVsT1JVz)Q--EDqHSD6d*8ll z&VP=yFO={c*R3cg&Xh-GA;kRk)+CAFligzpva%RjqEXmk^D!U;mjK#j)QM7_y(`nP z!0uy+YI6st@ug=1|D;SpWJIfcD2|At z`E?$+Q>WN_)^q)K;VEX?*L!5br2^HI94sfsLkV4sf8Sf^9fa7BDldA z_EAQzo~JeY?_iaYc;0cLa4%j!0H_6iS=9<5qNr@?$46HD>Cy2h~@f+=p+CE9;zmzF&% z#OnV3b5g z0rrkMF`j#M5g&Q=c);1kon>k~m^+UvJa0b;-|F$;j>Si;YR9h!$L@2DG!ZGGzB>KFK)*TAXHB5JC1yoIn(g%;eUA|gWKX8FV6M1~1Rqg*t{K|Hrtg9M zIeNxU?j7c7p1s&lKmGauNDty$$HI<&cX?-pI2W7p`eUA?tT1|A)~RK5*Vmrmc|7C; z4_(shQBW6iCqP$d4GJ#*`8hrX5b~Q4vNlhdF@QdquIHt@lZ~GHmcf?}IvmL?8arKd z5WJDL%i0p{TFDaqflf(=5u*AeyJAZ?qeAk|ga6~GT%87Fc??*U4@cs#Hou&Tc#;`c zd1j}w?>+v@WJUi5${Rkb$s2B?C6=Ro0+Rn|Dmp7qRW>#aMH^gwrPCG4@4A1bCf06? zgnN9IHm%hml(>7$Jx9L`&?lse{N^?rvTUw+Y(GAk7Uv4*Nk((T-p|aehuNrvcW2&v zEH6KT+LP%KgJmQnZU^loj$5gx)w=^46%k4_ zZ?6XE3fXrpmz+ZG#4~Mv&K(X9scidY6B6MQLgyzaq-?%J*@A}XGkt|aU09Y%jX7_Z z_mh-#QKTp214KsP5}VKeIecVPKnI8e+?ux42P|Gs0s2gVT<0d&>MPKOJj0-}(}N)% zJ4(Aaz9dnm{18dCcbyy#18&(Fa42MD5x2#dDNl9B$@=}qCkytxduXLVV#4(Fp41_f z9yc?rHUZ8 z73x^k;}~gnhiPe2_;Tla(r}Sd+O0en(JS`^hz;KXqNqTytgos9f&N4#>hb(g>TzB^ z5lTwX=q|QqWOvs zC240O-y~C#X1{Q{QwaG`B0W0R<8EN`kAxx<(%>CK6;~fUFix*spIP2Xget0Y47(!7 z8vLYhlCc{WrEwt3D;;8-c(!3D)Zehi{#aBS20E;XDU?fE#e4E8298-9*be?pZMUoP zs0Vo=_KeGozU1wEm0W#^q7V!xk-13MBt&!T( zco#!X-vM%B0UkPa#LVv=4BA{QX%D~p*g_NFlkQ?P0J`Ld*YuYWM<-Y$kBilrJh+SB zViSk*m1xNQ)16WdZX@TWb=T)D5LfJ&7)p~Ls+lO6oOzq7*-vr=TDEp2WU060hv^IO z54C2ZZA7V)(~^>(4Kb>|2fSQ7y#}YRCK<6gI88(n=mE}xrv>4qSzU@A3$sbn0WQT{ z^xFmUXoQ<5QLF4$eH$K6=WuHL`UORwh4?A&cvzEZ8E}I9+Q@lmlpYWhT7W^d4ZJ9) zcV=LxmP~Uw;|MR;f*lj@H!ua6`Z?+0Zyy2P(0?aySGvSZX8m5GqWg+EIXDH5{`Kvr z{U#K^Q20^_vezLW-ay*FbY**xcbdTUw9HCkZgp`};g(+~C%Ts1N7}6BA}79iSd2d4;*SDp`8oBXQZ)S@%J(m0fwfTwdcK5SCstRMBKPF76_ zp>1SFC*fGBx}Bk-A{=^%!}g9GMW>1FxUL8;V6o{iC*`DBeg}DMR`dN|_bVk-^|m;q zb6*ffnKDgvgc``F>Ay|WtN0SAjt&$y`DUqapg`qm4lR3VndP%IXS*HWt|K5m^Kja^ z`;%VJYhz9g!PHuJLtrmxi&)Cbc)@$X1Gc>%>ZvJg%hl7w6^olzP_^+RN_mzdU^h_< zFziv8N|S;!6b^`hJ*O1wmbRJi?I@s6h}Q=tX5azV-TGJ&@*_O4%at1T{%p9uEui3% zETCnVpH}lWb8R+#EB;A;ashgB74JIIyF#WC1$kKA_+Es~ESb`l3OgRt z?5t2`e~vJ)nf)jJC|pNf76B(?ZtpdBle#RVh#OQ>-U<1sl+kWs^r`3DmH?03jc|PrL-b^T@Pl%+`v?(ys60y|G zPBZsB#L8d+Nd@mvv+fe&%v3$>5!&s*lGh`<0mE$~r*1&z9WZxXkVTlW8Q@ur5H})za46u+C%9uL}J@FhxuyN~pk^ zDQb;YuhfeoW4XjO>}~N?lQe294R@O#4679Y)#AC)V|7mqGW4z{isfB=go(~y#xnxa zme#LW$&ZShaC3o_n!QE*?}`m&1!kqEGNXP;<9SioMECvHc+A0E9E()` zTXoVAOx1jY(QyV)9t+F9wy|FTmC$TATOi7p|71^k@Pq#+E3A4}&b3J`17o%#HuKDN zB(ab=PeVkifR!A<4*}I?l+qdHSuW(kn-u_EP**jZ`0C3)%%zahADhj1gruG=BB!P{ zKqVMmNDn0v6E0`?-7SzdSHGr~bA|WVnV!^KnFHh@%pbugV6}rYnE$Qu2}px>ngTkw z$s-96*qRxAqPc_M+SL%mr!M<9^hstO}z$?k-`4CzU_Y>ppu<$U<-%)H?w;0*8ms zV4TH8^=!MU7;(Ej`bD-CZ|gpl3BTQhA1a4bY(u{qbM62ln2qCBNcK&)8w6+gY+!2D}Nn+%Ub{wtkF3x{F}<`IUx9u}5ql6c!K z4MAkaKc(Mj6tpg&o6>LMw{0p!*u${;HgCq;fz=XlY_*p8tO_70ylgEFyx7#x5R9#{ z;AS?+OH)HerI9Y!r0eOmOco2*jw{aubX^P~0<-64ppMeKpW;vd7|mO%!Y)#_f9S2e zaG8*(LDHE%5a7QsIxr7Q3N%ihl)V~d#2Z{p>8-3jo{+3Zq8{oA6kQ}0`=t2MiL%cI^9?< zm_{=an_X}io$SSo(mBsCS#`61;3=rPem}W@rxdgO25v2(*U_ck3O@)C1N>~C^@8Ge zA3Qh;8&Q_7tt+h(^;|AmS43*QDPV4Rrn=|9?bE=T|J-B$sc@H*>6nXIifwU`muOnu z~q-vzXUn(m$dup1nYhm z#jUolTme0G{^#1(&!0FwcU5umcLjdkl97^@7MD^ImzFV?mQs + + + + + + diff --git a/app/src/main/res/drawable/divider_launcher_holo.9.png b/app/src/main/res/drawable/divider_launcher_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..1ba42cfb220d0ef66464be612e12961bba924f5e GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^96+qV!3HGtKUiJ>QXZZzjv*GOlYjjGZ_jMR#>Bz+ zI63e@LolPj2gZ#S9mkeB$avgl%uL= + + + + + + diff --git a/app/src/main/res/drawable/flying_icon_bg_pressed.9.png b/app/src/main/res/drawable/flying_icon_bg_pressed.9.png new file mode 100644 index 0000000000000000000000000000000000000000..fb74449a2d3bd96873a67f511883670bc9adad8f GIT binary patch literal 1749 zcmYjSdo&Y@A6JuSw(4dcX(;k5Y$V1!hIC{^VcsJ#5|^>jWIco{k2}x2P1>~XWv)Cf zd6v~!oi>kSdE{+gVVpaY_toil&*_iv_xsQH`#GP__jA7In{wUJR!&A)Modgh&hDy> z^FcNH9#Rqq8L=}IB_<|OVrOIJdV6fS*e}c_`ec`OfN|)QwVL9MGqhC1; zg4k;$PTS~(XYtk8f|25q-0hZ*@r`Zsj`7XXf4t+Q#lf@LmeY59ey@zwy}lPa^LB{I z|MJ|j83@*k>&dxp_2>!hlIFnQbgVuvD?3Oh$;9vyNI@I_R<;AKw2mWjZgi_Ty};Vt z^nsuc>9UW<>P%hL%XY&r-h@ESRu8%snwV?Kh|sz?be^R$LNg<|i=RR}%ZQ7rCD^;= zy$?5#f8FZUa$1%|oJdf3P#osYol-`a&n-!mqJvIZixWWIIhYX7*S~tWEq8a<_10Gb z<+0cL>UwO}jnc{`px;*CNWymJEkVtD3qmvZ{2Kprhk}2*9P4=;`oX~8mXd+VHfkGJ zEY1D2R#o41IwWJ3cGjyZ{ydO4@fZXmt#)Prg$2-o;7Riibd39FdqrI^3e_1FI>-Rc z@sG$ga(Z5drIFd3mGcXIqU#a9F#{jo02F?ZB@E6~LWSjcN@W$m6C9q-=mfUF z*lE4$Ly+@VJ>1p6l+4c!_s7TVV(Uz`BG6v-YTJT=a8+4I(*zTs+PXSItAr5BxhEEu zF1Rn$B&7sh3Cd~`YU#O|0(pl)ralW0-Gq z<6nTI?@mQj5IvO+kK{49cvt> zR8;S@Mu#|`X$@=5XuHz%>hKfP47RpWFGs-UI`tUiiJMw&GQ{>jFvx{ww&S zQ${Z13k$#1TNarvDQSj#+bDIW^{K)Gf(P3f6VqeIMYO&|Bpbe`x5`UoUKWeaa<&p_ zln+AsYXUTn(Qo0U26{@KVCh}AsBqhu%IJ(y6>|Dmw+$;I(Dzh}g{#p@$ehXUclj;G z1||yb=O*GozOcCDo&=~MP{9H1S7er4YLaZSiBo)L;e4_CxdvEN#CRcdsUMv>v)BCi zm$>9=zcGWIxnRuY4!@%hcSf|z|LO3RDy`IOqnFMJsU6qIWJ!8@h__Ab_wyh;)AC%X zaS2UMyBk>`&QDU;4&eN`I$DUdkgNC$Gq}|Yn~N#gRNq}>3pg>NUE{%_jok^+^f+ZQ zCvG^_5xHeRUYqcs5k~+NQxzzW?GF@|L<(kWuxMECF*2AkyTo1K{DPEg@7w=mX}h>6 zSqvmaH{ajS7eqYRPTTmvGG8{MtL`cVJ8F|pjfEdhoC^n1KWE11`n9-RL%#l@@2jvucH&WRQ)+~! ztOdU`nd$tLsRH;2bU`v`xAQdDY80fMygj3z>+blE7cm4X-a{0#=BWNLsy%#(ba&`C zjEMbzPF+NtP8N3585=!}SKK=i^^I4>%msI5ioc_1ICz?rc=m!3wS)nCTd%S)qH)u$ z(qa6M*)m2wIl#;~@6E}(oJnulueeTp0W)F1MOQx%bK-bh4i0-H@j=|-Rj|gHds{4~`*_Ia zinqP$1K2X%4?@})$#!~ZE*upw_u>F=`~<{rE4mQBb~?WWzDC-tu7HidSYO`mJ9q|$ k)hvF_{#KN#+vl$lNTed)6bgkNyel!gD~>kR);Ayj3-z0L_W%F@ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/focusable_view_bg.xml b/app/src/main/res/drawable/focusable_view_bg.xml new file mode 100644 index 0000000..66661e2 --- /dev/null +++ b/app/src/main/res/drawable/focusable_view_bg.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/focused_bg.9.png b/app/src/main/res/drawable/focused_bg.9.png new file mode 100644 index 0000000000000000000000000000000000000000..d4447cf2cb8ae7f3893e0996137c7396781f3ca8 GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYk`7co-U3d5>s>g4+=Io@Gwo6wiiou{g&vztt|idfp$yxi#0+LY#iG< zcXHld(PtaVFln3Sj<{+zv4(G3bR60H98Gp4wPhz5C7qRfyZ+AV6}c+4b*vlK3NK+5 Yd(Ek=&;+!B!PC{xWt~$(lLE;A0AklX4nJ zaCd?*qxs3xYk`8Uo-U3d5>s3I4{{zb;9>S(s9d`L(%(&&zg`sa;dt42!0FJ96(TZK zy^{<}#8fQ%AHCPy&k!BEbJ5#F>nFrM{O)mTwx82e#yx9geyw{d#|Si#!PC{xWt~$( IlLE;A0PDj%c>n+a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/grid_pressed.9.png b/app/src/main/res/drawable/grid_pressed.9.png new file mode 100644 index 0000000000000000000000000000000000000000..576adb34b0840e2dbed733beef61f295baf83bf8 GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYk`7Jo-U3d5>s3I9JvlC2r#Q(`u=reT|w1~Lt01Kn52z#9G9MWXs#i7 z+{st&?f?J) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/grid_selected.9.png b/app/src/main/res/drawable/grid_selected.9.png new file mode 100644 index 0000000000000000000000000000000000000000..3ab01ef0aa0b06abda7d47dceebe535639aad6cc GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYk`7xo-U3d5>spYZ*wsya5!&&arp1&%g$^{R~s9r8wNITEh%Fz5?jc< z%~{|1oP_D8V?T8J585BjF}Dt@oUFzeb?&HA;mo8xT0oN+JYD@<);T3KDUb{RKaw<` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/hand.png b/app/src/main/res/drawable/hand.png new file mode 100644 index 0000000000000000000000000000000000000000..fe5a035dee5917837cd00a4d92d4859d41e9b040 GIT binary patch literal 9752 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/homescreen_blue_normal_holo.9.png b/app/src/main/res/drawable/homescreen_blue_normal_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..03930e5d24b51ce87c7225769b633fad81a884f0 GIT binary patch literal 2936 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/hotseat_scrubber_holo.9.png b/app/src/main/res/drawable/hotseat_scrubber_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..6aeda7fa527a4bc2f985208d45b9aac154dceca9 GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqEX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYk`7Bo-U3d5>u0Z{QqyyEcAo_r~uz88CHh8zR&NT|DRiK?{44nJ zaCd?*qxs3xYk`7}o-U3d5>uD<8*(uy@G$$U*FX4Ov(LW3nq5|lgHKLGC@QGQ>wxX) zIlHYjUoc%+Yi+<#)UhTjINHLbet*U1RhEoF2W0Mtw(~^*4P)?h^>bP0l+dI=G5`Q2 CU@?CH literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_allapps.png b/app/src/main/res/drawable/ic_allapps.png new file mode 100644 index 0000000000000000000000000000000000000000..a3cce14e097ad7991e07051a1f9974cbeb8a3441 GIT binary patch literal 3234 zcmZ{mX*d*&7RSd}B1>c6!eA6dOokY;Om^~)A);iglaLwvPKfg{(UDk7YD8nUy zR=S#MfQ#_>;f~9W4y$GAdP%MSMq|)H>EEFTRuyx}|GI#Yofs1UfWA~)P1Wde&zr~g zG0!Hw14ew(oHI_^Gn_r*GrTxftyW{z!$8&bR#0>VU94^~a<44W+Sh2K7WGNU7Q;|X^4omURtvdA|mMsnY4YeK3*Z%n!6PqBmb z8N{OX=;_-GKIOfsjgb)Fer*Z8=&xQ2w{l18l9VCfWK)Ar8;(T!tX)&zp8myvN;(!A z_xCXrTzULJsbVjm-S8HVrPEiL5~9W#72^|`OGV(0_#LjlSqDF)oLU3N8;0gc-?H}i z@_aP|rIlc=#c)E9F)_(}vaZnKT~S+>tPsCIz&7Vm>o3XgFAUjxd*ys(fr!W-LgKRe zRQJl8opmqZ5ute<^XD5mnD{SIh9l@bT3TAmggYJerz7=gj>8hJgI^Q^JGMGB%bYoR&Be0B`PLhsHDdXwjLDjv`>()VD_qpO*gp2h7n_tX2X=_= zlCpa#zZOn_z;4x1H*OU=%`eF3yM?!Lg9k^%YxCcRmilJb_{UUw2f4Suf5xf0;%*$h zeNs8=n5*frNUlxwHqFU4WQGl!jtz8m71t%QIZZhVm=;bQ9-V)MA8fqhKsA$o+;%&(Jda*-?V+!H=6;{Vlc~?gYN?oY z{|YCA^3Ov?rO1b|=efuB-~K@jSXBt5Fyjx4EV+x*uuT!5e?XxrI0hamjk#)- zf$C(&u72cBF|gbgqmk1rbJvNRc==>utX#uFVU}=PtVr?jb(;s}nMIp4Y>_=yq^j4I z_)hC!D#Z4$m$4*hHGg`5*9lOi^|?eo_xdVhKo|ZUteXBSLkav*tU{h=e0$sJro=s` z#v?vfLO*XG#9|3rHJ0*rCInKFH1V3>dBS%!ZjpX9okfG+>v*n%AKi1j{PwB8D;wpj zi~aErO83CwpYN|e{hE^IlB71EL&e$z#|cyM#z}s}w`oNUp|05L9&~*-*<0ND%XS z)6`pFET}J@UkG@bJIBUx$cOi|yq3>e*9{VP2oG>((8-U^7!t1ogOZswnlR|b&{U3W zY!B^X;pAf0Ib!6J1_7@5oRE zWW9E}{b8Zll|igvc@7HeQ!OW7jug1w!$u{}#?o67sszJ5X9B83QQ=Cv3b&2eS=^wx zoSuNyZ-vP+2%8z$amFo3t%fJ9JqmR5SAK<=U9A0-cMhmi`oO1O${}G~git5kE$TZg z<&etZMOupKnRF{XtAZ#){7OjNG`N^x)$VRYCl4i5qL7=aijCs2x)CjvD4Qt$YxISp zhH9pWt-YBvAY36;tzOUCV#j?23IafG-dDaLh0$T1 zW*ELQieiy>neg>(6y6jVX4bfB-*%F&=faruz(e&j>Oi+_kK?6s7QZo7srm%drg zBv_23+Ry{S86pRlGkA?X|GdP;|8iJ&cVff|Slv;9>YL@fj_Hc(X`S(u4 zrdwz}_GX`C@%x!XueIa8+@scm)3l#_npcG&GMe*0C-?L=@Cudkl2J)bAMZKOA$na$ z=TBU8ovdyX)litEiiiRLAx?^enf7+#IIW~?or>xOGI!h(*@QUr0AX05o3n#>VY7 zhmb3^x$>~4n&gL`mBbnx+7=lc^dLq=nCTintr1K{@oanNG$3h&jlDcTNVq*&t`~Pp zp@&#CVs7{FV*~qEnr8z?v})swPQ;7aTI-};{#hug#X3J#oBnNS%5MXYyJGO*X63;y z@bY-ohKv+?arj0<7}eE68OD`8x%1@D3FDsKw31Q5*xde`CFU>Ik<(VvnfbM)WeXqc zT*+7dKBq2>Q{pqfeBnjh)E~RqSi0H5tzB#{0k|s#g-S@tOF-`$L1A!dSvXWiLh2q| zN~#heIQTz;le3Mzo%jD276mfgF9q&@FBm%8xp`W;*aB2-oULrZ+D?`jTa>M(jW=%4 RR`GHPpslX2R)IhV{|7h0BsKs5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_allapps_pressed.png b/app/src/main/res/drawable/ic_allapps_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..518e360e5cc7e731ac174b5da70c63dd9fd423ba GIT binary patch literal 2455 zcmZ`*dpy$%8~<&y&0Rz;Ar*4T{jxYDOf$D?=C~zW*eto`vc$1bM^1!IXc93*uDPw_ z(#R1i&RL9v>E@EloK)6O?|T1x|9C&o=lgs=-{<*!pXd4W$vW+Yl95!C1OPw=?TW;R z*z_mFVWR8{{+1~sF`}cVBLKAIO6}mGA`QBPK{)}9qw32dp%vna@dSWmT>zk51puL_ zL|FoW1ksd_H~_FM1_0%lJAaUQeng15eU3{IHX zEU)v&YlV}wwh14{m(vrD({>_23`5+StsN{>;d?_16*)Q`s-~b3kMN!MZaO!26RiSv}qp$~6W^p*|i z_Iu14ly+FmAlgl_g(KRtI;1WM720?Kc11_~T~Mo;^I%8-XO`v#x7pSXiclGCKy7Hp*q|ECFw}w?8lnHC#GO|OnjHeL_>Zgg8=%YFL7lp-Ku9^~Lq-X%r_ys1BwZ;B{n`lAsgaaQPYk8d!2BUVP+S|_^eO)MvBDX_QZc>+gv zR{C)SwYvOyDXc%5|KJ+ek}>r9<%Z6@N=(_89LNE1!0)$D;#e|MVOLQU9*qMULtU`m zFu;8J6>(6CO=k|&N7|V{=a9D#)zjjb#B8DrONB)%IH!2>k~BHpTrQxc11_+Sk=f9k zzrqft^egk5nI^E{u0Rqz`%De~%@& zrEX=uOO*tgCoMQmio$Nuu~Mv2LPTLfu~Fu$rvy@Zfm=v*lJc}VAx^t8)-ankDtB#o z^J=raAp3T9aeISWVJiFC*Y#w&@0L|Fl-!z{wvT-CQ=6jF{mi5vHnJE($@gXcX5NPp zRvG=Dc0=JbmmsaoRyq#q-KeHCI8rTC$;uBI)Enu(5Kr}&ql4S^zj`8#4;#czH-pZC zyJ31WVDG-wF}lVYc(b)S9;<25f_mVqcS&&Q?StGj5PUcxC2D9H8x@X3Dfz?%wJoO zEjYjjM@j8M^|5mi>4_q+loDz2j+?9DDhADj#tn#Dz*j_U-) z?@7QU|GOtlekBoeEyPMMW<2~{U2%6I{t3H`-G3idRLQlG@qlfO+#XB(L~hhZ1>*D$szS@9vmXkxa0SG zehDx3wJ+>M-&oZfejE+@Exh_uGuOI-m|e?fI)9BC2Axk z?bhM^bIf@C+!SRHzdb1pE^AZWfAczoiG3T$M&-?}vfZz!arlFUb6s$w_wqBocARy=6%Jbj?!s;fUMNoZ===rMHQg%OuCn&7W9it~ z#G;b7Ls3faelKvDRuwz2zR9zWY47A#Lv;P6(U)tdvmvAh|9*-dS5o4tqi|~7iM;v-u*9w9G=M1We+RUAC4gYzU znCUjB!A$fo`Nof%AjXKW!KM0moP{m1yv&>p z*Z*;0zE&`}J5es@(k(n)Y);qqeRLh0^2e*CqsvRlV1e@D=9uQ<>FP;dq(SEN;_o}N z3%raQ7Vh0|>>rAL51n`yzxYsme3)%WT$qReb5k=5BU2kAb1SUbF;D)cqeDrA%m3fdS32M(GN}IC;2lkfPr}EA0gj>3=fkwnk$7SlCJY~XId(Yg Q=Ys^GUA&NuPPp{{0L9&Ot^fc4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_home_all_apps_holo_dark.png b/app/src/main/res/drawable/ic_home_all_apps_holo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..84fa5947da5df94148418fd12d75c0d9c8dfada6 GIT binary patch literal 889 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 M>FVdQ&MBb@02mWt@c;k- literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_launcher_clear_active_holo.png b/app/src/main/res/drawable/ic_launcher_clear_active_holo.png new file mode 100644 index 0000000000000000000000000000000000000000..2683beaa3b73f463f4e4d5c22b33fcaa69ec8d45 GIT binary patch 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_launcher_info_active_holo.png b/app/src/main/res/drawable/ic_launcher_info_active_holo.png new file mode 100644 index 0000000000000000000000000000000000000000..f84b4a6ba6c0fd0a8a00b7381b53bf1421d430df GIT binary patch literal 2736 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 VBZE3AXZTCN+{D_r?40|J{{X<&G{^t| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_launcher_trashcan_normal_holo.png b/app/src/main/res/drawable/ic_launcher_trashcan_normal_holo.png new file mode 100644 index 0000000000000000000000000000000000000000..799b62f8bcee219dbdb3ab630dbc43b1c34da449 GIT binary patch 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- + + + + + + diff --git a/app/src/main/res/drawable/overscroll_glow_left.9.png b/app/src/main/res/drawable/overscroll_glow_left.9.png new file mode 100644 index 0000000000000000000000000000000000000000..b79cdcdc2d0a969acd66e7caf65efdc3688bb91e GIT binary patch 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/page_hover_left_holo.9.png b/app/src/main/res/drawable/page_hover_left_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..d21ea1f032ea8889dd568fc437200f5979705938 GIT binary patch literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^>_9BU!3HF!dmi}>q*#ibJVQ8upoSx*1IXtr@Q5sC zVBqcqVMg4nJ zaCd?*qxs3xYk`7!o-U3d5>u~UG~_xIAkgwKUw-8!?xuT(lAr9cY%AZeQJ{L`gq$vw z{`MKn7I)9>H{P)3%$KESd}3Wfo*Uh3KC0atdEqe+Yjk)!bKZ$r5z~K_Wr$WEsBiqk zs>6I(=%A_OJWC T66iGsPgg&ebxsLQ3M2ynd;NJD literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/portal_container_holo.9.png b/app/src/main/res/drawable/portal_container_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..9a4751730109a5469537690812e1ec9546c88ef6 GIT binary patch literal 864 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 w(EfMBb5}c0AC$W-AYXld*|-zKAG-r zeyxJ5wS6pW8ks>zgd_?EQ5Yn>7}8?~67=3f(UZNX1W~=%gI0(yQV}K9YAPu!m`l33 z7MqdovhK&7`S)<|+&kxE?!7CPFP`BJ=bky|{D1$C^ECta(p^q+d(BG1JlZKp0klk+ zNp4N+{{T=b$rAeL=K+?oP}*oib&6a3#Rx-smLaZ9&-^CTUp#psK&?C+yeDGfg6+2|%fX9rC91iUbIO1&}sQMU#rJ+!H;o@95!~sYVjQ{6`Um zmAor^g6>cPLITpec}ex|0E%?6neM`wa6FznKqRtG4hCC-C>2~skeXk)GErtzj{Zsddu#zV@6OP8M14JU5DX0_ws;uRzxV zR3W62S*%ZTGQ1kM1`xq|IS}-ko(?k+AYP+aP{=xkZZ0I*SIVG*& z(8G?R_${zPwuzYC8grUemKOSPoNctN`T0MD?ToK9Vg##X7vfr$cwtY)JJpxDM% z3Ei$+v4OimvB8ZVBeoSo2Y@B?=S6g58(=&!$AMj|`g@#C7obnN&1!il+3sya`!;L! zTDAU`((3@oS|Y1jljPqUx{zYj2SMiZumw;I^&y{jjZ%Fh)~JRL1>upRWr2CJ*sr@b z?L50F^&zPzGUy>P0oqy6SnqnrYV3eB)2t<2J-Hgj0@^FNLbW7fzT2R}t~GA0=};-l zwlIZcaeWBdky;=HC~84G9cV_h31JJYTSRj#Vx(fa3>NGsS*=>t-xrX+nDF7G0JD@` zb7@AP;XzMVYFWXCP_y?LViRD7iLA#emnqyQRt#UOb-6aJhx_$X6nb0BY*iSHmIxXjRL^&^X`cc_obSs|zs7Y1?l}Q=H0` z)@@e5cKyJZ15lpUFSOjMuM!{{7^$SQWFt=9r4tj}0*^`lRG?VMn$p$2Q&;avY zm~Vkq>uKe`H#p0@uSYcxK$BpYXIW(JVXg6Q&wJ)-V6FvSyt$T*uR zn&K-Vj<-;r?*#z4$yuJF(>{9Sg<#zV*=ZMRJTR!c;}KVy z2S5q_Rs)Ov} z%p6`+f)b2yf;IG{CNSRh-fzLR^aNNRoqn1Bfg#le`U_0wrMu)GND&t(? zM~?85jwLOA#GB)1yP&{A>7}0sS;0cuC;?MUF~%@MdX`IEPg?j?b=v?n5Qu1}KmjOI zW-^(*y`Ju+yPp096}>)r<5+w#0000bbVXQnWMOn=I%9HWVRU5xGB7bQEio`HF*8&# zFgi3fIx#UVFgH3dFiiCJ#sB~SC3HntbYx+4WjbwdWNBu305UK!F)cALEip4xF)%tb zH99ddEigAaFfcu-PI>?U02y>eSaefwW^{L9a%BKPWN%_+AW3auXJt}lVPtu6$z?nM O0000 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/portal_ring_outer_holo.png b/app/src/main/res/drawable/portal_ring_outer_holo.png new file mode 100644 index 0000000000000000000000000000000000000000..e9b35f3f369753f8be97ece5486503213bfb69d1 GIT binary patch literal 4798 zcmZ`-XE+;vw2nl~(juvvReQy1gT@LCV$awrRtZI0VwT3%Vb-eJMTxzMnx*!rQl+Z4 z3W}Pwdj0RW`{Dl1bI$vm@jmaD^ZRn18|rH^(6Q42000IpG|KoA-T!kMs>}GO{ppoU zpmb5yQw0F3ub?0D*!403iYZIJqnlegOb@DFA?s z1ppA~008TwOp=k}WrNB=M-v73_n+mqzJGmLp?!q5^u9#&|GGxMyD|X46sLt!HT9oa z$@=Rhpvly$;uy45KefTf;nh{4)yJR~6T!!C!^;zLOO1e}*%dpp-QlB&K=K@eA3vvr z^l5cfU&FHcrfnwHw<>>K;|M9Pn1Y`NZXN}$W=WL5y?!c>`V2Rgk}jX9V}$VNp7@!> zV9kf0=fsyax!jeoN?dpoE06h1ti1ADWFF!~vb<(60SuD8(k?x;3UU+u%4 z`@%8x(YDLvPbSO=Ig%D%`?1MK7f`X!}bf~DVA0d3l-JA5UewjE4eOsZyGp7 zxCsBsT6G0&76u{Ee$l5i_qg4`Tm=}Scu&A#MgKH$Vd=k7dV)$j^p1YC;et9bZ^4zc zAu1D2201qf15uVo-S&yb(cPNCO5}fD7frc0>PzC58_yT03LHL397`1^t0%N9i z#sdEoFejAl3nNV`vZ&@jid3@(HV)(>k9pol6vrG@p#)Z7lV%Qb`0f_y8TEbeMJS*-mU=YImq1-s>TNaEL02F`HGT&;&bdP||FOeC z?u}l7b;nJDIRQV<$lYZ7;l(B?8NX7&gd1;0t7d2*PiS@^yqIwPw;kI|^N>0Pi0TG0 zd6F@eLVHahg&%`|Pq+%q0Y2CW3AyUZbqwXjy^XQj}1dIfWS4 z`_WO5&pX!EhY=+|>ln^%_;V%Jz)%CeBH9)**nVsZ03J*5VL!gIvnx7!Lv0}RCl6H& z=A|3V3@DCUummO(Tfy`69j7Pft0zb{hZPBcN*+Gd&7)SAzDQ0=;&#G(KFlqvrm#s8eOl`L)`u zby1pou))^n&1YC3%dtdNn4F?o%D2N|1b9$}8+ZUG@fqFa&gA;g;_^1%d=_Dz;*cGD zph;|T9|*1ta=?H2N!Y$+efxS9DED*!*`)86?=^;gob+%3KB~Ye$CZ zw3^p`zf9}TEZYkLjUOb||NPK0f(Q}ZWi}UT1)lXC1c&FRd~4|Z{52C=tm167Rx)ZR zX-GFN8}f1I`c8wp*lz`{yvUySxawiO1L6=54-DuBa+zHVaGI7@Z_QN-MSqpVG2CH3+qIQkiJGDYG8xb^#1XyTle0S~+51u>^Vn`$eyHS<&XU^{NjCyaq)Si72^rkW4 zTl`+k#PA}l*VKwinf5F*e|rawx1dXv7A+Gf{LX2Bd1^pB{CW*@#F zu`l~*EdE9-jjIA=N%D|svIz#=zOzHl1=2{MDiygA{eV$u)sg+G}@yhN61nAXuvylKz6t#V=s1-OAQ6c2Hg!$P4 zbQT$`(a2sy^zDOZb|M3g;*2Lc6?bkqSr=CMzS1(+t?9z2P}wSH^;H7v(4;T~; zhglaXtiA0b85i!351+(SNE*!^A$Vl`Ql5J??VVDLf9zqe;FG&nY7qN851`$DU_#My z<&3@>>m`3-{MXQ6OzSC403@v|E!14eG*l$nr0z^&@p6 zos|55m5lU6DM;g^(hxdnNs~7-k9E&SrF{pbXZ-dQQ*7FyRgJs9tDzsh3rBMIZkLzz zw(lfmV8PtPg%f6SQt#^I>Z?Ss>OY;3{aZpP9!Y^Y0lA>=g;Hc-|hbDrO&nCjlrVl_&G zF~P|1i}d4{Zr8tbBpOBgaXuqu)oPJ^lwLH_Sj(#P^03p~4z&hnOob^C~=TKiQ zPWkvT$S-NGevF>-Z6>gf)Vpp~B9=tDdvI3)cQ%SLlkv|Di-^jR>BK}LbBK9jr)A_% zw(cXMn;`{A(PE+Dh!STEmKm45_MEh?96xS@$wiXx=sSU=k;cE2Kls*K(r62~huzsG zWo(da+djGKq^wvMZg`-eMeOW@khQX6r$}jE7Z!!qEkx;K7L3{9M>SDfj+kmA=Y+X? zViwWbsoZ>9$&3k1Mb*K6H$w4~Mg`wR;(p=>gSxh^Gg;>eID9+F5x3AO2uL{E-3?)< zks%uw7KD7%Lv=N1fyC~G`956b+)4uq$# z1<+g2S-4TL#*X8D2gZ@;Ol4j=4L&0;`p040!j3cH8Z&b#O32);CUiameA}P zBys<#bpOCL5MQJ?wuFo8f$@dMlL`nzi+|AiSJc$MOx?|5B^Au|AZ4{ik@WSRJ2rr~aZUS_|Fhy8+PJ8IY#Ha)%G*QqWad+64%A4@Ih z_ea?nT%$GR#K^oy!9xlM@><|8)PV%ED$^P>X6{_?)FR0-i?AhLTcYK@e{vBHz3?nD z-|^OH%`538S4isjCYGDcjS+%PPKSsb)>qgr3R-caQmX17o|3Pr!`*h`4_)VZX zXbCVK-<ar2$1 zE&fkSWG4E#AR|ZkYHD3rq0pDc#E%YHNpW=nPbCVzbaWSMr7dY1d>3z))g(dBBo}GV zL(TTfowj%n@)Hn8sCqVsW-D7!KZH zdwN(0MvWUYl%zd3wW?By`C)-s$Ua??OT?Z+YTj)t_a;YCUoFMEV$K2z+_E3&>ZA}i z(1PI>55~K~-!8{hMreHED!5`qSK?g!N@>1gufr*O?H)LNW1F8>eJ#|lc>_3kmMdwf zyIR;UNv|SWYgHyXtHJyyB&^N{Nn*6@Dm9zzcMr(Es;>MBQ0zQ^i?U20v+%9|#&@*n z*VGw-TRj~$W_iRehJvvv7Z$|3X1oj2OOZP|)ZA4uD5VHakyh5q3*$m^KzG$xKCVzY z4Tg2L6r{BdzEQPm{EyFAOG1G0K5M*poMmbJZS3TV27B>0|Ju|Vq*B;&rAoASVIt!M zT~cHJ1NE0gq_BCo?{8yFQ(OV*!oi_8c|Blpy2mUuOL3HXQ8BIhq-Y1Jv}xr%S2#o3 zWj$U}jn;-H6Pydg%1n3Ilb(3ot@id!CbxUsT8Cm4M)%sXH~l;dXg`c@E9M_^7tokO zLF>FbRBiGzdB))A7IlcdJ5-;yaIZ~^^u~>X-aW&nbEOaNR_&7e2IctrI15yNG8Z{y z^uB~~f_vw-Q$>yF&REr+aaCBePN6$}f7f5dB-OhqW!k0~?}64BwZnDA(U$CQ@@Te- zQ)8WDylHwciI{x>laVSn<+4Pt3P(E%+Xhk|$i--*G12oBCXxqtfOBk=Qp38n`e{gS zp6Nwk#?(3Yi+~?+R9+k)SEs`pA~sczN0AMyv^CFOb;gP1=~WSx>4xUeT-edv!#}~+EPwjaCMCifY6xcNU``QF zv#Ao66l}Trlh+QgB#c$*avh3dfxg#5*~8V(;w6eYx?J6XzHg$_4s^`uU|O~?W#-}j z%J;~&3^6OM+rE%qmXtEnW~di_d;R{`u$F&!pB`Sv#FF#o-kkw*hL7A??x$k@zVRO2 z(!dqj@jO2&F9|6yvM+&EX;0+EP1v=houKq|EosMYbhjh~EXykQMmZy9sTu{|JUkZ{ zGx9EqJg6n#|J52ZWI}|_$cl>Bx4v6v{&KI{Gpg-dj{Jk4wtV4%Uc+NW_V>RZlYN39 zer6xv#jWWRnM;<{FJs658S9U0hKJpH5e7_i4_Q?Y&I!$!p}0*m5&C}4p`@@A+h=V z2ds`o=W-Jn22&&8Jv37tX|>9}fR*WL54(D929YF$f02iKM39pgU+J@QfQn>IJ*#}U zT^wo=JbKH`AeM)tCEL`HU9_z)m$}Kd-4)z&bh&%RuXC@ktcM**(xAU?Pi2~v`eQ}o z16i6%ywfEzT@v7fcb%+Pvd#mdmGX3G480a6?+=c6VazRIqM}lW&2>#N=Ny!1Wf2vU zk#~zPWPD8|V-;qkjoUbG9T)+P@+Rtk)MsVlTcpbeHoBIFV>k@QBKuX+ zdmldc0eeuDxb5M(pK!`rsrnAt^_t&mOzA7f-QQ@?Hy4M4EL`WT#~viwuEq1ZJsDe< z&HsHrUtSWItK*-ZdqJ-Ju{c!RBh}mv9yVis#=IMNmEBfz@#G>xuIF9?ol6M`dT;yy zZit0N<*sKIZK*m*p9|w~`{aDCZp^-n!@9@Y1|2bUy+x3wk-_z{(Bw$#>lwEl2I|_q?@&Rk5*?h{8 zD65$&Sf{Vx6QHQS_LiNtWwn-_0mk!LJ%#mMTT>Ws()GZP@T@rS=ZjPRn%?@DN;)}@ z5Cwf{^GdV*`yb8W-)KW>S#4nQ3;lny!wM$++HIxYM>4y7r!3bh`!xKsZaKLSKAbpg z?|v1_iLwgkamxL9V0yxsYK)o@r)dZzcsL`SA&mxV9pcyQIW^PgiIT>Ucb&h~kyP@n zsi8MOB`dzG?4SyP%9|j{gu>1ZBU(0mz_+MHBWzDau({XP_!wbQVtSw9BT%hn{~1Ul zG$A}FSN!rsBjBTM;p1fQ@c$FKirNM)32grjm3V7V_{NEi3 Nprx*ls!+p*{SOVn=uZFu literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/portal_ring_rest.png b/app/src/main/res/drawable/portal_ring_rest.png new file mode 100644 index 0000000000000000000000000000000000000000..3b5ec717e4ea8c442d8ab422ceb7f9bbbae6f0e0 GIT binary patch literal 1551 zcmZ`(dpOez82%AEE+ znZu+O!*Sg5h-7k^9BHyBDJf^pf9H?$e9!y6&-;DP`@H|XIcGdw_9|#9006Mp6^+75 z*kKoPAjz)3c1%Y?GDIW>2>{IniraWuiI$4Rx;O)@VXb9}&Fy%=@XKyqR%T-r7i|ebKN{`N5F&YvZ`0-*aCa^gh`zcj{i8{tv0l4z*{#cKX3; zar&0rs8ZT!RuJ->Tx~5Wa=!73rwh%BE=Aa`-E>KxJxaadS{EMd=(UVpTlz zv2$rUR_sVT*pEJC{cGWkH#>fRu|X`3U0A^)0&{h0^pdk#ZR(QLfd@G0qOvaXD-&o{ zFa@U*+g3$(N=ti$nLG;|hyrJv#i>r<>;y-eJkav7s-;v*LE^c=*%6K`Tp7po&BTPl z@vB{@qIyl5`nVOMTJmxdd^mX!=LTcaU2R|)v zZN<~YZ=6X+Wdh<`8a-_;#K9Pxq-wv#i3mxTx6cvJGh~t=w>s&w3#U7ql9{x_=XsUw zrfGdD$r%IwZ+~hb@|4;l=Til2#p8Ujn;B)}(FN^= zhduyReA0?pZH0ymD~usnyQt#3W7?Ext*?G1LrWAbgsQB+#G6+s7k;d5ovSWhS3Tcr zyX~_weyk@|?7GA{cDe0u&FMUsX0;A?Ge1G5D7~UvM-n=HYtK43HP7V}zUPu(sK=Kb z?ND+4{1CpIcH&9CB5X6b!M!nkLCtU}ejD#!y0BwF$^PwvVFceejLKsi8X!e)aJ9u? zsMdA>Y6WBP98+`Ys+59XSxbVpz|XBbgv9++wvR4L^sd`kzWe)2pY^mGXLHcGrH(h z;U3&8kdCVPyfy?0Z~CQ_1;4y!;>{UWjw2h4y;!-wa>GCL8RRE0eR*+Nvh$iQ&AJ8Sfx5bO0n813gt_I^ zUC}eg4v_gTJ~lH-6~z z$h{ZkKikj4)Hlku-!Gppcj=`y>oVlbb@+{(iMl|#AnRpM)wjF^lawq`=Y6*=Cg!La zpC;V;#5S!?&YmsC4sXC#Z=eM`M(UKOJzIQLFZy!iATrl#L74Ag?nVe&3oGnSR;QF; z88q0;6~uKSN)M*Tpf7X<-B$1(;VK2{Mq$5>8E~Ei_{W*2>ScBz=R@nT0qv%u6q|cV zsJ1tFtpjH}j9O8%wp|(KkIU05(a(K_FxqA5DD(%8!69l;>cqL3r=B;*UJd&{_6Qjx z_BZ+R=tj~L3C>L()#LJx7Sru!cq=2`BE22Arg5}1O}D;9aAeMcjmZr|WZbsHLrJq9 z*@<}tFF_B+>Fdvb zTsS_C01t~HNC>coKy54_FbivIAE+(d))o$hSwQUJ5Xb-`I{7~aG9^4RBJuwXTzo%I uV$j%4@TNq>UB<@{0Ax5Nl%VHI#uEux0zN!3x}UIH%z*1jPZY};m-!Fkc*>dp literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/remove_target_selector.xml b/app/src/main/res/drawable/remove_target_selector.xml new file mode 100644 index 0000000..5e071fb --- /dev/null +++ b/app/src/main/res/drawable/remove_target_selector.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/drawable/search_frame.9.png b/app/src/main/res/drawable/search_frame.9.png new file mode 100644 index 0000000000000000000000000000000000000000..cfaa0f94b811f6e43c363a9334aff928229ae2d6 GIT binary patch literal 342 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|mSQK*5Dp-y;YjHK@;M7UB8wRq zxI00Z(fs7;wLrmRo-U3d7QJt$T;x4uz~eICxm6>HgKc4QIy2h~4xUB<$A!J!i%4>6Twv>+hNprMzWIaew~` lEhW+PmqC98LZ*tpUf`g#&U+)sn+%?=elF{r5}Fi91^@s@bGrZl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/tab_selected_focused_holo.9.png b/app/src/main/res/drawable/tab_selected_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..867e505f224b84188ed8a55366b06823eae18437 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^EI`b~!3HEJ|NhSh5-4`^4B-HR8jh3>AfL0qBeIx* zfx8og8O=|gUJDd7^K@|x;h4Gh+(zC520Y9MizoD2>~^~(JZa^V sUoEkkaeSh9w)TUlKa6vZ+qJ0**((%Vy8umK@O1TaS?83{q(CwN0HHB4AfL0qBeIx* zfx8og8O=|gUJDeo^mK6y;h4Gh+(zC53JlB#xw8$}k`45e4cLk;e3y7!_%!uv(Z^d6 v0#9x~Qa^f(Id{#0H&;LIP<)f9-+fKc+F|3Yl|VxnJYD@<);T3KDUb{R+fX&9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/tab_selected_pressed_focused_holo.9.png b/app/src/main/res/drawable/tab_selected_pressed_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..eecfa9a01372c595de4045f0140d6f5c0006a6ca GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^>_BY9!3HEJJUw_8NU;<^|Er@x*&M1u+2B$ uw$q;<^q>6N+n=DoIq|3Yq$$FSrA7WRZMk^`XeooItDnm{r-UX2k^ul4>Pn9Q literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/tab_selected_pressed_holo.9.png b/app/src/main/res/drawable/tab_selected_pressed_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..ce0cb0ad5db7bb7ea32d19a6c2dc84f4e47cf248 GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^%s{Nf!3HE}-AL8}QY^(zo*^7SP{WbZ0pxQQctjR6 zFmQK*Fr)d&(`$i(PM$7~Are!QfBgS%&&A^|Fftq|;-9woONKGfGzL#sKbLh*2~7$l F0{~Z;IP(Ai literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/tab_unselected_focused_holo.9.png b/app/src/main/res/drawable/tab_unselected_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..98dbcee6057648a6fce0ded48ee775920abc4ec7 GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c0!3HFsSlX9@1d5$JLpXq-h9ji|$mcBZh%9Dc z;O+!rM)Q-W*8&ACJY5_^IA+G4Imp@IAi#1kNN+LcIT4=>=UW!n`aACInYB!EW!u3$ uIYDzn*o$v%D|qmWd&mA1`&tG?86nH1GYjqj&0z3!^>bP0l+dI=G5`R=Wisdh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/tab_unselected_holo.9.png b/app/src/main/res/drawable/tab_unselected_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..5b216b25382b18ccd67c53b20385c6450274d676 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c0!3HFsSlX9@1d5$JLpXq-h9ji|$mcBZh%9Dc z;O+!rM)Q-W*8&CYJzX3_IA$jQ`2XLYnVI=;y+&GsLz;ns!JqUb2WA}}9-f|K(evAk zfFg6gPD@K@^jg!TZFujONZyK%*Ov1zxbO?iIdA+_5oi>Hr>mdKI;Vst1(E>(c``D` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/tab_unselected_pressed_focused_holo.9.png b/app/src/main/res/drawable/tab_unselected_pressed_focused_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..6edefd9b74ec8e50fb79afb52e4923019cc875e4 GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^>_BY9!3HEJJUw_8NU;<8z@8=ZnShos&d@%y^Kup`QU?+W`& zsfwTPBV1b4emlDqFie^CQrP*^^7Cr@LIs`QExbHy)wlNi)}_0ue=IP~l5$+{sJvEC y#_m$c!-p+>x92r-dPHshyykd>UK{KDIPvcr4`!JI?Pc(E^>bP0l+dI=G5`R%#ZZ3$ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/tab_unselected_pressed_holo.9.png b/app/src/main/res/drawable/tab_unselected_pressed_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..fc42ae102244c2892e8e8b62d7a0b9b3873dc0cf GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^%s{Nf!3HE}-AL8}QY^(zo*^7SP{WbZ0pxQQctjR6 zFmQK*Fr)d&(`$i(KAtX)Are!QfBgS%&&PcZ+XEmLOm$Z_ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/uninstall_target_selector.xml b/app/src/main/res/drawable/uninstall_target_selector.xml new file mode 100644 index 0000000..229942e --- /dev/null +++ b/app/src/main/res/drawable/uninstall_target_selector.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/drawable/wallpaper_gallery_background.xml b/app/src/main/res/drawable/wallpaper_gallery_background.xml new file mode 100644 index 0000000..271c1df --- /dev/null +++ b/app/src/main/res/drawable/wallpaper_gallery_background.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/wallpaper_gallery_item.xml b/app/src/main/res/drawable/wallpaper_gallery_item.xml new file mode 100644 index 0000000..b7052bd --- /dev/null +++ b/app/src/main/res/drawable/wallpaper_gallery_item.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/widget_container_holo.9.png b/app/src/main/res/drawable/widget_container_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..698b600b39a57d269aa61ae5d8f3aa8e5f9d7ea0 GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|mSQK*5Dp-y;YjHK@;M7UB8wRq zxI00Z(fs7;wLroBo-U3d7QJVuSo0k+5OBGFOEadRGo)j7L7R+0`U5qQOpW3O5j`1w z*1!}eyS@$<|4mQMzsU&O*0X9;6JPRF-hO$-mwQ&NTD|p~Q1Xa@jS9WmOcD7zcxzuZWYu=1^mvTZWZo55?MgUY|9!YT0hLA4uApb18~= zAJPoY-PMtMo9+3XNS8;mI#)@(|H~{NS%G}U;vjb? zhIQv;K#~f{9znhg3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh z?!xdN1Q+aGJ{c&&S>O>_45U54*zIJt9gvac>EalYaqsPUM!^FL0tYr2+P{zIXfl($ zbJ4|dciOXRr*HRO+sYO;z2v$qx=hk~ z*NBpo#FA92zopr047I(nE(I) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/widget_resize_frame_holo.9.png b/app/src/main/res/drawable/widget_resize_frame_holo.9.png new file mode 100644 index 0000000000000000000000000000000000000000..028bd6248969b53ffe8999c2d0402eb14b58b5a8 GIT binary patch literal 619 zcmeAS@N?(olHy`uVBq!ia0vp^Q6S901|%(3I5Gh#mSQK*5Dp-y;YjHK@;M7UB8wRq zxI00Z(fs7;wG0eQyq+$OAr*7p-to`7>>$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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/widget_resize_handle_bottom.png b/app/src/main/res/drawable/widget_resize_handle_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..c838bf405af66e4b87b00bb5098894f30daad50e GIT binary patch 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/widget_resize_handle_left.png b/app/src/main/res/drawable/widget_resize_handle_left.png new file mode 100644 index 0000000000000000000000000000000000000000..ff0b0d3570bee5dd4b5de6f559613e2a8cc425d5 GIT binary patch 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/widget_resize_handle_top.png b/app/src/main/res/drawable/widget_resize_handle_top.png new file mode 100644 index 0000000000000000000000000000000000000000..3b1df01702f89a7ec5f586f24431b7312b881895 GIT binary patch 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}*)|aoQ~N)r@T}9yljc4iUOxY zN-2X^zrca*2ihu+Sgv%Gm40y~Sv&oadhRhcE5-FklwZusEb9tt)D%C)bKqOui}O7f zKPG%s@;x+bZpE5;H+PpAr#Y$d-gLVE``JM;;qs^Rk8zu^r~RLuc<>s~2@IaDelF{r J5}Fi91^`c0ROtW! literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/add_list_item.xml b/app/src/main/res/layout/add_list_item.xml new file mode 100644 index 0000000..0ae0113 --- /dev/null +++ b/app/src/main/res/layout/add_list_item.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/layout/all_apps_cling.xml b/app/src/main/res/layout/all_apps_cling.xml new file mode 100644 index 0000000..4527353 --- /dev/null +++ b/app/src/main/res/layout/all_apps_cling.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + +