From 6ecec8e9329d70c8546a1bcb3d8cd94d202aaedd Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 17 Dec 2019 16:54:37 +0530 Subject: [PATCH 01/23] Bump version to v1.3.1 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0a6d4663af..244d893663 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'kotlin-android' // Manifest version information! def versionMajor = 1 def versionMinor = 3 -def versionPatch = 0 +def versionPatch = 1 android { compileSdkVersion rootProject.ext.compileSdkVersion -- GitLab From 8d1ac18cec5c37e6de0f1bafd24e222a7cc9b3ee Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 27 Feb 2020 21:10:23 +0530 Subject: [PATCH 02/23] Initial implementation of MVI and clean architecture --- .idea/dictionaries/amit.xml | 11 + app/build.gradle | 2 +- .../core/customviews/Workspace.java | 67 - blisslauncherv2/.gitignore | 1 + blisslauncherv2/build.gradle | 67 + blisslauncherv2/proguard-rules.pro | 21 + .../blisslauncher/ExampleInstrumentedTest.kt | 24 + blisslauncherv2/src/main/AndroidManifest.xml | 22 + .../e/blisslauncher/BlissLauncher.kt | 36 + .../e/blisslauncher/base/BaseActivity.kt | 75 + .../base/BaseDraggingActivity.kt | 157 ++ .../base/presentation/BaseIntent.kt | 26 + .../base/presentation/BaseView.kt | 6 + .../base/presentation/BaseViewEvent.kt | 3 + .../base/presentation/BaseViewModel.kt | 25 + .../base/presentation/BaseViewState.kt | 3 + .../blisslauncher/base/presentation/Model.kt | 22 + .../e/blisslauncher/common/Functions.kt | 8 + .../common/util/SystemUiController.kt | 64 + .../blisslauncher/common/util/TraceHelper.kt | 60 + .../features/launcher/LauncherActivity.kt | 116 ++ .../launcher/LauncherActivityModule.kt | 13 + .../features/launcher/LauncherState.kt | 436 ++++++ .../features/launcher/LauncherView.kt | 10 + .../features/launcher/LauncherViewEvent.kt | 7 + .../features/launcher/LauncherViewModel.kt | 63 + .../features/launcher}/PagedView.java | 8 +- .../launcher}/pageindicators/PageIndicator.kt | 2 +- .../pageindicators/PageIndicatorDots.kt | 2 +- .../inject/ActivityBindsModule.kt | 17 + .../e/blisslauncher/inject/AppComponent.kt | 30 + .../e/blisslauncher/inject/AppModule.kt | 13 + .../e/blisslauncher/inject/Qualifiers.kt | 6 + .../e/blisslauncher/touch/OverScroll.java | 40 + .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 ++ .../src/main/res/layout/activity_main.xml | 3 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes blisslauncherv2/src/main/res/values/attrs.xml | 9 + .../src/main/res/values/colors.xml | 7 + .../src/main/res/values/dimens.xml | 4 + .../src/main/res/values/strings.xml | 6 + .../src/main/res/values/styles.xml | 11 + .../e/blisslauncher}/ExampleUnitTest.kt | 4 +- build.gradle | 57 - build.gradle.kts | 48 + .../e/blisslauncher/buildsrc/Dependencies.kt | 15 +- bump-version.sh | 2 +- common.gradle | 33 + common/.gitignore | 1 + common/build.gradle | 4 + common/consumer-rules.pro | 0 common/proguard-rules.pro | 21 + .../amitkma/common/ExampleInstrumentedTest.kt | 24 + common/src/main/AndroidManifest.xml | 2 + .../e/blisslauncher/common/Utilities.java | 267 ++++ .../common/compat/LauncherAppsCompat.kt | 131 ++ .../common/compat/ShortcutInfoCompat.kt | 74 + .../common/executors/AppExecutors.kt | 6 + .../common/executors/MainThreadExecutor.kt | 33 + .../common/extensions/CommonExtensions.kt | 1 + .../common/inject/Annotations.kt | 6 + .../blisslauncher/common/util/LongArrayMap.kt | 39 + common/src/main/res/values-sw340dp/dimens.xml | 20 + common/src/main/res/values-sw600dp/config.xml | 4 + common/src/main/res/values-sw600dp/dimens.xml | 27 + common/src/main/res/values-sw720dp/config.xml | 14 + common/src/main/res/values/attrs.xml | 39 + common/src/main/res/values/config.xml | 10 + common/src/main/res/values/strings.xml | 3 + .../main/res/xml/default_workspace_3x3.xml | 88 ++ .../main/res/xml/default_workspace_4x4.xml | 47 + .../main/res/xml/default_workspace_5x5.xml | 48 + .../main/res/xml/default_workspace_5x6.xml | 37 + common/src/main/res/xml/device_profiles.xml | 158 ++ common/src/main/res/xml/dw_phone_hotseat.xml | 63 + common/src/main/res/xml/dw_tablet_hotseat.xml | 78 + .../com/amitkma/common/ExampleUnitTest.kt | 17 + data-bridge/.gitignore | 1 + data-bridge/build.gradle | 5 + .../databridge/ExampleInstrumentedTest.kt | 24 + data-bridge/src/main/AndroidManifest.xml | 2 + .../databridge/DataBridgeInitializer.kt | 12 + .../databridge/ExampleUnitTest.kt | 17 + data/build.gradle | 15 + .../data/DataLayerInitializer.kt | 18 + .../e/blisslauncher/data/DeviceProfile.kt | 616 ++++++++ .../data/InvariantDeviceProfile.kt | 432 ++++++ .../data/LauncherAppsChangedCallbackCompat.kt | 83 + .../e/blisslauncher/data/LauncherProvider.kt | 43 + .../data/LauncherStateManagerImpl.kt | 59 + .../blisslauncher/data/SettingsObserver.java | 100 ++ .../data/badge/BadgeRenderer.java | 93 ++ .../data/compat/LauncherAppsCompatVL.kt | 206 +++ .../data/compat/LauncherAppsCompatVO.kt | 139 ++ .../data/compat/PackageInstallerCompat.kt | 103 ++ .../data/compat/UserManagerCompatVN.kt | 90 ++ .../data/compat/UserManagerCompatVNMr1.kt | 12 + .../data/compat/UserManagerCompatVP.kt | 13 + .../data/database/BlissLauncherDatabase.kt | 7 + .../data/database/BlissLauncherFiles.kt | 25 + .../e/blisslauncher/data/database/IconDao.kt | 21 + .../data/database/IconDatabase.kt | 9 + .../blisslauncher/data/database/IconEntity.kt | 54 + .../blisslauncher/data/graphics/BitmapInfo.kt | 28 + .../data/graphics/BitmapRenderer.kt | 62 + .../e/blisslauncher/data/icon/IconCache.kt | 220 +++ .../e/blisslauncher/data/icon/IconProvider.kt | 36 + .../blisslauncher/data/inject/CompatModule.kt | 60 + .../data/inject/DataComponent.kt | 22 + .../data/inject/DataRepoBindingModule.kt | 38 + .../data/notification/NotificationListener.kt | 139 ++ .../receiver/AppWidgetsRestoredReceiver.kt | 1 + .../data/receiver/ConfigChangedReceiver.kt | 33 + .../data/receiver/InstallShortcutReceiver.kt | 1 + .../data/receiver/ProfileReceiver.kt | 77 + .../data/receiver/SdCardAvailableReceiver.kt | 1 + .../data/receiver/SessionCommitReceiver.kt | 12 + .../data/receiver/WallpaperChangedReceiver.kt | 1 + .../data/shortcuts/ShortcutsRepositoryImpl.kt | 12 + .../data/widgets/WidgetsRepositoryImpl.kt | 8 + data/src/main/res/values/config.xml | 4 + data/src/main/res/values/dimens.xml | 30 + domain/build.gradle | 27 +- .../e/blisslauncher/domain/Functions.kt | 56 + .../domain/entity/ApplicationItem.kt | 73 + .../e/blisslauncher/domain/entity/Empty.kt | 8 + .../e/blisslauncher/domain/entity/Entity.kt | 10 + .../blisslauncher/domain/entity/FolderItem.kt | 111 ++ .../domain/entity/LauncherConstants.kt | 36 + .../domain/entity/LauncherItem.kt | 148 ++ .../domain/entity/LauncherItemWithIcon.kt | 100 ++ .../domain/entity/WorkspaceItem.kt | 109 ++ .../domain/executors/PostExecutionThread.kt | 7 - .../domain/executors/ThreadExecutor.kt | 5 - .../domain/inject/DomainComponent.kt | 27 + .../domain/interactor/AddPackages.kt | 31 + .../interactor/ChangeUserAvailability.kt | 28 + .../domain/interactor/ChangeUserLockState.kt | 16 + .../domain/interactor/DeleteComponents.kt | 14 + .../domain/interactor/Interactor.kt | 117 ++ .../interactor/LauncherStateInteractor.kt | 19 + .../domain/interactor/LoadLauncher.kt | 30 + .../interactor/MakePackageUnavailable.kt | 28 + .../domain/interactor/ObserveAddedApps.kt | 23 + .../interactor/ObserveAddedLauncherItems.kt | 2 + .../interactor/ObserveRemovedLauncherItems.kt | 2 + .../interactor/ObserveUpdatedLauncherItems.kt | 15 + .../domain/interactor/RemovePackages.kt | 26 + .../domain/interactor/SuspendPackages.kt | 28 + .../domain/interactor/UnsuspendPackages.kt | 28 + .../domain/interactor/UpdateLauncher.kt | 162 ++ .../domain/interactor/UpdatePackages.kt | 33 + .../domain/interactors/Interactor.kt | 52 - .../blisslauncher/domain/keys/ComponentKey.kt | 19 + .../domain/keys/NotificationKey.kt | 59 + .../domain/keys/PackageUserKey.kt | 43 + .../domain/manager/LauncherStateManager.kt | 6 + .../domain/repository/LauncherRepository.kt | 36 + .../repository/UserManagerRepository.kt | 29 + .../domain/entity/LauncherItemTest.kt | 18 + .../interactor/LoadAllAppsInteractorTest.kt | 88 ++ quickstep/.gitignore | 1 + quickstep/build.gradle | 39 + quickstep/libs/sysui_shared.jar | Bin 0 -> 118069 bytes quickstep/proguard-rules.pro | 21 + .../quickstep/ExampleInstrumentedTest.java | 26 + quickstep/src/main/AndroidManifest.xml | 36 + .../LauncherAnimationRunner.java | 149 ++ .../LauncherAppTransitionManagerImpl.java | 816 ++++++++++ .../e/blisslauncher/LauncherInitListener.java | 91 ++ .../quickstep/ActivityControlHelper.java | 615 ++++++++ .../quickstep/AnimatedFloat.java | 89 ++ .../quickstep/DeferredTouchConsumer.java | 114 ++ .../quickstep/InstantAppResolverImpl.java | 77 + .../LauncherSearchIndexablesProvider.java | 96 ++ .../quickstep/LongSwipeHelper.java | 175 +++ .../quickstep/MotionEventQueue.java | 248 +++ .../quickstep/MultiStateCallback.java | 66 + .../quickstep/NormalizedIconLoader.java | 95 ++ .../quickstep/OtherActivityTouchConsumer.java | 449 ++++++ .../quickstep/OverviewCallbacks.java | 45 + .../quickstep/OverviewCommandHelper.java | 377 +++++ .../quickstep/OverviewInteractionState.java | 252 +++ .../quickstep/QuickScrubController.java | 258 ++++ .../QuickstepProcessInitializer.java | 35 + .../quickstep/RecentsActivity.java | 284 ++++ .../quickstep/RecentsActivityTracker.java | 131 ++ .../quickstep/RecentsAnimationWrapper.java | 159 ++ .../blisslauncher/quickstep/RecentsModel.java | 297 ++++ .../quickstep/RemoteRunnable.java | 33 + .../quickstep/TaskOverlayFactory.java | 62 + .../quickstep/TaskSystemShortcut.java | 272 ++++ .../e/blisslauncher/quickstep/TaskUtils.java | 214 +++ .../quickstep/TouchConsumer.java | 74 + .../quickstep/TouchInteractionService.java | 414 +++++ .../WindowTransformSwipeHandler.java | 1102 +++++++++++++ .../fallback/FallbackRecentsView.java | 74 + .../quickstep/fallback/RecentsRootView.java | 91 ++ .../fallback/RecentsTaskController.java | 31 + .../logging/UserEventDispatcherExtension.java | 84 + .../quickstep/util/ClipAnimationHelper.java | 320 ++++ .../quickstep/util/LayoutUtils.java | 113 ++ .../util/MultiValueUpdateListener.java | 68 + .../util/RemoteAnimationProvider.java | 69 + .../util/RemoteAnimationTargetSet.java | 63 + .../util/RemoteFadeOutAnimationListener.java | 53 + .../quickstep/util/TaskViewDrawable.java | 142 ++ .../quickstep/util/TransformedRect.java | 32 + .../quickstep/views/ClearAllButton.java | 79 + .../quickstep/views/IconView.java | 91 ++ .../views/LauncherLayoutListener.java | 108 ++ .../quickstep/views/LauncherRecentsView.java | 171 +++ .../quickstep/views/RecentsView.java | 1367 +++++++++++++++++ .../quickstep/views/ShelfScrimView.java | 206 +++ .../quickstep/views/TaskMenuView.java | 232 +++ .../quickstep/views/TaskThumbnailView.java | 333 ++++ .../quickstep/views/TaskView.java | 364 +++++ .../uioverrides/AllAppsState.java | 89 ++ .../uioverrides/BackButtonAlphaHandler.java | 71 + .../uioverrides/DisplayRotationListener.java | 48 + .../uioverrides/FastOverviewState.java | 85 + .../LandscapeEdgeSwipeController.java | 79 + .../uioverrides/OverviewState.java | 133 ++ .../OverviewToAllAppsTouchController.java | 80 + .../PortraitStatesTouchController.java | 271 ++++ .../RecentsViewStateController.java | 104 ++ .../uioverrides/StatusBarTouchController.java | 116 ++ .../uioverrides/TaskViewTouchController.java | 290 ++++ .../blisslauncher/uioverrides/UiFactory.java | 265 ++++ .../uioverrides/WallpaperColorInfo.java | 115 ++ quickstep/src/main/res/values/strings.xml | 3 + .../quickstep/ExampleUnitTest.java | 17 + settings.gradle | 3 - settings.gradle.kts | 1 + 246 files changed, 20628 insertions(+), 236 deletions(-) create mode 100644 .idea/dictionaries/amit.xml delete mode 100644 app/src/main/java/foundation/e/blisslauncher/core/customviews/Workspace.java create mode 100644 blisslauncherv2/.gitignore create mode 100644 blisslauncherv2/build.gradle create mode 100644 blisslauncherv2/proguard-rules.pro create mode 100644 blisslauncherv2/src/androidTest/java/foundation/e/blisslauncher/ExampleInstrumentedTest.kt create mode 100644 blisslauncherv2/src/main/AndroidManifest.xml create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/BlissLauncher.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseDraggingActivity.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewEvent.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewState.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/Model.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt rename {app/src/main/java/foundation/e/blisslauncher/core/customviews => blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher}/PagedView.java (99%) rename {app/src/main/java/foundation/e/blisslauncher/core/customviews => blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher}/pageindicators/PageIndicator.kt (75%) rename {app/src/main/java/foundation/e/blisslauncher/core/customviews => blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher}/pageindicators/PageIndicatorDots.kt (99%) create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppComponent.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/Qualifiers.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/OverScroll.java create mode 100644 blisslauncherv2/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 blisslauncherv2/src/main/res/drawable/ic_launcher_background.xml create mode 100644 blisslauncherv2/src/main/res/layout/activity_main.xml create mode 100644 blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 blisslauncherv2/src/main/res/values/attrs.xml create mode 100644 blisslauncherv2/src/main/res/values/colors.xml create mode 100644 blisslauncherv2/src/main/res/values/dimens.xml create mode 100644 blisslauncherv2/src/main/res/values/strings.xml create mode 100644 blisslauncherv2/src/main/res/values/styles.xml rename {domain/src/test/java/foundation/e/blisslauncher/domain => blisslauncherv2/src/test/java/foundation/e/blisslauncher}/ExampleUnitTest.kt (78%) delete mode 100755 build.gradle create mode 100755 build.gradle.kts create mode 100644 common.gradle create mode 100644 common/.gitignore create mode 100644 common/build.gradle create mode 100644 common/consumer-rules.pro create mode 100644 common/proguard-rules.pro create mode 100644 common/src/androidTest/java/com/amitkma/common/ExampleInstrumentedTest.kt create mode 100644 common/src/main/AndroidManifest.xml create mode 100755 common/src/main/java/foundation/e/blisslauncher/common/Utilities.java create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/compat/LauncherAppsCompat.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/executors/MainThreadExecutor.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/extensions/CommonExtensions.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/inject/Annotations.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt create mode 100644 common/src/main/res/values-sw340dp/dimens.xml create mode 100644 common/src/main/res/values-sw600dp/config.xml create mode 100644 common/src/main/res/values-sw600dp/dimens.xml create mode 100644 common/src/main/res/values-sw720dp/config.xml create mode 100644 common/src/main/res/values/attrs.xml create mode 100644 common/src/main/res/values/config.xml create mode 100644 common/src/main/res/values/strings.xml create mode 100644 common/src/main/res/xml/default_workspace_3x3.xml create mode 100644 common/src/main/res/xml/default_workspace_4x4.xml create mode 100644 common/src/main/res/xml/default_workspace_5x5.xml create mode 100644 common/src/main/res/xml/default_workspace_5x6.xml create mode 100644 common/src/main/res/xml/device_profiles.xml create mode 100644 common/src/main/res/xml/dw_phone_hotseat.xml create mode 100644 common/src/main/res/xml/dw_tablet_hotseat.xml create mode 100644 common/src/test/java/com/amitkma/common/ExampleUnitTest.kt create mode 100644 data-bridge/.gitignore create mode 100644 data-bridge/build.gradle create mode 100644 data-bridge/src/androidTest/java/foundation/e/blisslauncher/databridge/ExampleInstrumentedTest.kt create mode 100644 data-bridge/src/main/AndroidManifest.xml create mode 100644 data-bridge/src/main/java/foundation/e/blisslauncher/databridge/DataBridgeInitializer.kt create mode 100644 data-bridge/src/test/java/foundation/e/blisslauncher/databridge/ExampleUnitTest.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/DataLayerInitializer.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/DeviceProfile.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/InvariantDeviceProfile.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/LauncherAppsChangedCallbackCompat.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/LauncherProvider.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/LauncherStateManagerImpl.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/SettingsObserver.java create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/badge/BadgeRenderer.java create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVL.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVO.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/compat/PackageInstallerCompat.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVNMr1.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVP.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherFiles.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/IconDao.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/IconEntity.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapInfo.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapRenderer.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt create mode 100755 data/src/main/java/foundation/e/blisslauncher/data/notification/NotificationListener.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/receiver/AppWidgetsRestoredReceiver.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/receiver/ConfigChangedReceiver.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/receiver/InstallShortcutReceiver.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/receiver/ProfileReceiver.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/receiver/SdCardAvailableReceiver.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/receiver/WallpaperChangedReceiver.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/shortcuts/ShortcutsRepositoryImpl.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt create mode 100644 data/src/main/res/values/config.xml create mode 100644 data/src/main/res/values/dimens.xml create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/Empty.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/Entity.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt delete mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/executors/PostExecutionThread.kt delete mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/executors/ThreadExecutor.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserLockState.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LauncherStateInteractor.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt delete mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/interactors/Interactor.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/keys/ComponentKey.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/keys/NotificationKey.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/keys/PackageUserKey.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/manager/LauncherStateManager.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/repository/UserManagerRepository.kt create mode 100644 domain/src/test/java/foundation/e/blisslauncher/domain/entity/LauncherItemTest.kt create mode 100644 domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt create mode 100644 quickstep/.gitignore create mode 100644 quickstep/build.gradle create mode 100644 quickstep/libs/sysui_shared.jar create mode 100644 quickstep/proguard-rules.pro create mode 100644 quickstep/src/androidTest/java/foundation/e/blisslauncher/quickstep/ExampleInstrumentedTest.java create mode 100644 quickstep/src/main/AndroidManifest.xml create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/LauncherAnimationRunner.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/LauncherAppTransitionManagerImpl.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/LauncherInitListener.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/ActivityControlHelper.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/AnimatedFloat.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/DeferredTouchConsumer.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/InstantAppResolverImpl.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LauncherSearchIndexablesProvider.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LongSwipeHelper.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MotionEventQueue.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MultiStateCallback.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/NormalizedIconLoader.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OtherActivityTouchConsumer.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCallbacks.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCommandHelper.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewInteractionState.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickScrubController.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickstepProcessInitializer.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivity.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivityTracker.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsAnimationWrapper.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsModel.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RemoteRunnable.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskOverlayFactory.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskSystemShortcut.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskUtils.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchConsumer.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchInteractionService.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/WindowTransformSwipeHandler.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/FallbackRecentsView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsRootView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsTaskController.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/logging/UserEventDispatcherExtension.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/ClipAnimationHelper.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/LayoutUtils.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/MultiValueUpdateListener.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationProvider.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationTargetSet.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteFadeOutAnimationListener.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TaskViewDrawable.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TransformedRect.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ClearAllButton.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/IconView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherLayoutListener.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherRecentsView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/RecentsView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ShelfScrimView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskMenuView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskThumbnailView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskView.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/AllAppsState.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/BackButtonAlphaHandler.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/DisplayRotationListener.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/FastOverviewState.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/LandscapeEdgeSwipeController.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewState.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewToAllAppsTouchController.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/PortraitStatesTouchController.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/RecentsViewStateController.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/StatusBarTouchController.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/TaskViewTouchController.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/UiFactory.java create mode 100644 quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/WallpaperColorInfo.java create mode 100644 quickstep/src/main/res/values/strings.xml create mode 100644 quickstep/src/test/java/foundation/e/blisslauncher/quickstep/ExampleUnitTest.java delete mode 100755 settings.gradle create mode 100755 settings.gradle.kts diff --git a/.idea/dictionaries/amit.xml b/.idea/dictionaries/amit.xml new file mode 100644 index 0000000000..09877e869f --- /dev/null +++ b/.idea/dictionaries/amit.xml @@ -0,0 +1,11 @@ + + + + badging + flowable + interactor + interactors + unsuspend + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index babdb775f4..066a3df987 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -111,7 +111,7 @@ dependencies { // Rx Relay implementation "com.jakewharton.rxrelay2:rxrelay:2.1.1" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61" // Blur Library implementation 'com.hoko:hoko-blur:1.3.4' diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/Workspace.java b/app/src/main/java/foundation/e/blisslauncher/core/customviews/Workspace.java deleted file mode 100644 index 934693cd08..0000000000 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/Workspace.java +++ /dev/null @@ -1,67 +0,0 @@ -package foundation.e.blisslauncher.core.customviews; - -import android.animation.LayoutTransition; -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import foundation.e.blisslauncher.core.customviews.pageindicators.PageIndicatorDots; -import foundation.e.blisslauncher.features.launcher.LauncherActivity; - -public class Workspace extends PagedView implements View.OnTouchListener{ - - private static final String TAG = "Workspace"; - private static final int DEFAULT_PAGE = 0; - private final LauncherActivity mLauncher; - private LayoutTransition mLayoutTransition; - - public Workspace(Context context, AttributeSet attributeSet) { - this(context, attributeSet, 0); - } - - public Workspace(Context context, AttributeSet attributeSet, int defStyle) { - super(context, attributeSet, defStyle); - - mLauncher = LauncherActivity.getLauncher(context); - setHapticFeedbackEnabled(false); - initWorkspace(); - - setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return false; - } - }); - } - - private void initWorkspace() { - mCurrentPage = DEFAULT_PAGE; - setClipToPadding(false); - setupLayoutTransition(); - - //setWallpaperDimension(); - } - - private void setupLayoutTransition() { - // We want to show layout transitions when pages are deleted, to close the gap. - mLayoutTransition = new LayoutTransition(); - mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING); - mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); - mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING); - mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING); - setLayoutTransition(mLayoutTransition); - } - - void enableLayoutTransitions() { - setLayoutTransition(mLayoutTransition); - } - void disableLayoutTransitions() { - setLayoutTransition(null); - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - return false; - } -} diff --git a/blisslauncherv2/.gitignore b/blisslauncherv2/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/blisslauncherv2/.gitignore @@ -0,0 +1 @@ +/build diff --git a/blisslauncherv2/build.gradle b/blisslauncherv2/build.gradle new file mode 100644 index 0000000000..bfda6f31c6 --- /dev/null +++ b/blisslauncherv2/build.gradle @@ -0,0 +1,67 @@ +import foundation.e.blisslauncher.buildsrc.Libs +import foundation.e.blisslauncher.buildsrc.Versions + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion Versions.compile_sdk + buildToolsVersion "29.0.2" + + + defaultConfig { + applicationId "foundation.e.blisslauncher.v2" + minSdkVersion Versions.min_sdk + targetSdkVersion Versions.target_sdk + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(path: ':common') + implementation project(path: ':data-bridge') + implementation project(path: ':domain') + implementation Libs.Kotlin.stdlib + implementation Libs.AndroidX.appcompat + implementation Libs.AndroidX.recyclerview + implementation Libs.AndroidX.coreKtx + implementation Libs.AndroidX.constraintlayout + + // Rx + implementation Libs.RxJava.rxKotlin + implementation Libs.RxJava.rxJava + implementation Libs.RxJava.rxAndroid + + implementation Libs.Dagger.dagger + implementation Libs.Dagger.android + kapt Libs.Dagger.compiler + kapt Libs.Dagger.androidProcessor + + implementation Libs.timber + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/blisslauncherv2/proguard-rules.pro b/blisslauncherv2/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/blisslauncherv2/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/blisslauncherv2/src/androidTest/java/foundation/e/blisslauncher/ExampleInstrumentedTest.kt b/blisslauncherv2/src/androidTest/java/foundation/e/blisslauncher/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..89eecd711b --- /dev/null +++ b/blisslauncherv2/src/androidTest/java/foundation/e/blisslauncher/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package foundation.e.blisslauncher + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("foundation.e.blisslauncher", appContext.packageName) + } +} diff --git a/blisslauncherv2/src/main/AndroidManifest.xml b/blisslauncherv2/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bf1b03f521 --- /dev/null +++ b/blisslauncherv2/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/BlissLauncher.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/BlissLauncher.kt new file mode 100644 index 0000000000..4b4b7f9148 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/BlissLauncher.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher + +import android.app.Application +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import foundation.e.blisslauncher.databridge.DataBridgeInitializer +import foundation.e.blisslauncher.domain.inject.DomainComponent +import foundation.e.blisslauncher.inject.DaggerAppComponent +import timber.log.Timber +import javax.inject.Inject + +class BlissLauncher : Application(), HasAndroidInjector { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + + override fun onCreate() { + super.onCreate() + DataBridgeInitializer.initialize(this) + DaggerAppComponent.factory().create( + this, DomainComponent.INSTANCE + ).inject(this) + setupTimber() + } + + private fun setupTimber() { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } + + override fun androidInjector(): AndroidInjector { + return androidInjector + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt new file mode 100644 index 0000000000..a84288f1ad --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt @@ -0,0 +1,75 @@ +package foundation.e.blisslauncher.base + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import androidx.annotation.IntDef +import foundation.e.blisslauncher.common.util.SystemUiController +import javax.inject.Inject + +open class BaseActivity : Activity() { + + /*val dpChangeListeners = ArrayList() + + @Inject + lateinit var deviceProfile: DeviceProfile*/ + + @Inject + lateinit var systemUiController: SystemUiController + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + flag = true, + value = [ACTIVITY_STATE_STARTED, ACTIVITY_STATE_RESUMED, ACTIVITY_STATE_USER_ACTIVE] + ) + annotation class ActivityFlags + + @ActivityFlags + private var activityFlags: Int = 0 + + val isStarted: Boolean + get() = activityFlags and ACTIVITY_STATE_STARTED != 0 + + val hasBeenResumed: Boolean + get() = activityFlags and ACTIVITY_STATE_RESUMED != 0 + + override fun onStart() { + activityFlags = activityFlags or ACTIVITY_STATE_STARTED + super.onStart() + } + + override fun onResume() { + activityFlags = activityFlags or ACTIVITY_STATE_RESUMED or ACTIVITY_STATE_USER_ACTIVE + super.onResume() + } + + override fun onUserLeaveHint() { + activityFlags = activityFlags and ACTIVITY_STATE_USER_ACTIVE.inv() + super.onUserLeaveHint() + } + + override fun onPause() { + activityFlags = activityFlags and ACTIVITY_STATE_RESUMED.inv() + super.onPause() + } + + override fun onStop() { + super.onStop() + activityFlags = + activityFlags and ACTIVITY_STATE_STARTED.inv() and ACTIVITY_STATE_USER_ACTIVE.inv() + } + + protected fun dispatchDeviceProfileChanged() { + //dpChangeListeners.forEach { it.onDeviceProfileChanged(deviceProfile) } + } + + companion object { + private const val ACTIVITY_STATE_STARTED = 1 shl 0 + private const val ACTIVITY_STATE_RESUMED = 1 shl 1 + private const val ACTIVITY_STATE_USER_ACTIVE = 1 shl 2 + + fun fromContext(context: Context): BaseActivity = + if (context is BaseActivity) context + else ((context as ContextWrapper).baseContext) as BaseActivity + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseDraggingActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseDraggingActivity.kt new file mode 100644 index 0000000000..140caeb728 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseDraggingActivity.kt @@ -0,0 +1,157 @@ +package foundation.e.blisslauncher.base + +import android.app.ActivityOptions +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.os.Process +import android.os.StrictMode +import android.os.StrictMode.VmPolicy +import android.view.ActionMode +import android.view.View +import android.widget.Toast +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherItem +import javax.inject.Inject + +/** + * BaseActivity Extension with the support of Drag and Drop + */ +abstract class BaseDraggingActivity : BaseActivity() { + + private var currentActionMode: ActionMode? = null + protected var isSafeModeEnabled = false + + @Inject + lateinit var launcherAppsRepository: LauncherAppsCompat + + // TODO Replace with LauncherTheme + var themeRes: Int = R.style.AppTheme + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isSafeModeEnabled = packageManager.isSafeMode + setTheme(themeRes) + } + + override fun onActionModeStarted(mode: ActionMode?) { + super.onActionModeStarted(mode) + currentActionMode = mode + } + + override fun onActionModeFinished(mode: ActionMode?) { + super.onActionModeFinished(mode) + currentActionMode = null + } + + abstract fun getRootView(): View + + abstract fun invalidateParent(launcherItem: LauncherItem) + + fun getViewBounds(v: View): Rect { + val pos = IntArray(2) + v.getLocationOnScreen(pos) + return Rect(pos[0], pos[1], pos[0] + v.width, pos[1] + v.height) + } + + abstract fun getActivityLaunchOptions(v: View): ActivityOptions? + + fun getActivityLaunchOptionsAsBundle(v: View): Bundle? { + val activityOptions = getActivityLaunchOptions(v) + return activityOptions?.toBundle() + } + + fun startActivitySafely(v: View, intent: Intent, item: LauncherItem?): Boolean { + if (isSafeModeEnabled && !Utilities.isSystemApp(this, intent)) { + Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show() + return false + } + + val useLaunchAnimation = !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION) + val optsBundle = if (useLaunchAnimation) getActivityLaunchOptionsAsBundle(v) else null + + val user = item?.user + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.sourceBounds = getViewBounds(v) + try { + + //TODO + /*val isShortcut = (item is ShortcutInfo + && (item!!.itemType === Favorites.ITEM_TYPE_SHORTCUT + || item!!.itemType === Favorites.ITEM_TYPE_DEEP_SHORTCUT) + && !(item as ShortcutInfo).isPromise()) */ + val isShortcut = false + if (isShortcut) + startShortcutIntentSafely(intent, optsBundle!!, item!!) + else if (user == null || user == Process.myUserHandle()) { + startActivity(intent, optsBundle) + } else launcherAppsRepository.startActivityForProfile( + intent.component, + user, + intent.sourceBounds, + optsBundle + ) + + return true + } catch (e: Exception) { + when (e) { + is SecurityException, is ActivityNotFoundException -> Toast.makeText( + this, + R.string.activity_not_found, + Toast.LENGTH_SHORT + ).show() + else -> throw e + } + } + return false + } + + private fun startShortcutIntentSafely( + intent: Intent, + optsBundle: Bundle, + item: LauncherItem + ) { + try { + val oldPolicy = StrictMode.getVmPolicy() + try { + // Temporarily disable deathPenalty on all default checks. For eg, shortcuts + // containing file Uri's would cause a crash as penaltyDeathOnFileUriExposure + // is enabled by default on NYC. + StrictMode.setVmPolicy( + VmPolicy.Builder().detectAll() + .penaltyLog().build() + ) + if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { + /*val id: String = (info as ShortcutInfo).getDeepShortcutId() + val packageName = intent.getPackage() + DeepShortcutManager.getInstance(this).startShortcut( + packageName, id, intent.sourceBounds, optsBundle, info.user + )*/ + } else { // Could be launching some bookkeeping activity + startActivity(intent, optsBundle) + } + } finally { + StrictMode.setVmPolicy(oldPolicy) + } + } catch (e: SecurityException) { + throw e + } + } + + companion object { + private const val TAG = "BaseDraggingActivity" + const val INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = + "foundation.e.blisslauncher.intent.extra.shortcut.IGNORE_LAUNCH_ANIMATION" + val AUTO_CANCEL_ACTION_MODE = Any() + + fun fromContext(context: Context): BaseDraggingActivity = + if (context is BaseDraggingActivity) context + else ((context as ContextWrapper).baseContext) as BaseDraggingActivity + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt new file mode 100644 index 0000000000..9794914a68 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt @@ -0,0 +1,26 @@ +package foundation.e.blisslauncher.base.presentation + +interface BaseIntent { + fun reduce(oldState: T): T +} + +/** + * + * NOTE: Magic of extension functions, (T)->T and T.()->T interchangeable. + */ +fun intent(block: T.() -> T): BaseIntent = object : + BaseIntent { + override fun reduce(oldState: T): T = block(oldState) +} + +/** + * By delegating work to other models, repositories or services, we + * end up with situations where we don't need to update our ModelStore + * state until the delegated work completes. + * + * Use the `sideEffect {}` DSL function for those situations. + */ +fun sideEffect(block: T.() -> Unit): BaseIntent = object : + BaseIntent { + override fun reduce(oldState: T): T = oldState.apply(block) +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt new file mode 100644 index 0000000000..b41591b82e --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.base.presentation + +interface BaseView { + + fun render(state: State) +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewEvent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewEvent.kt new file mode 100644 index 0000000000..37b674ab70 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewEvent.kt @@ -0,0 +1,3 @@ +package foundation.e.blisslauncher.base.presentation + +interface BaseViewEvent \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt new file mode 100644 index 0000000000..42ca7956ce --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt @@ -0,0 +1,25 @@ +package foundation.e.blisslauncher.base.presentation + +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.subjects.PublishSubject +import timber.log.Timber + +abstract class BaseViewModel(initialState: S) : + Model { + + // State reducers + private val intents = PublishSubject.create>() + + private val store = intents + .observeOn(AndroidSchedulers.mainThread()) + .scan(initialState) { oldState, intent -> intent.reduce(oldState) } + .replay(1) + .apply { connect() } + + private val internalLogger = store.subscribe({ Timber.i("$it") }, { throw it }) + + override fun process(intent: BaseIntent) = intents.onNext(intent) + + override fun states(): Observable = store +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewState.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewState.kt new file mode 100644 index 0000000000..f11510aaca --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewState.kt @@ -0,0 +1,3 @@ +package foundation.e.blisslauncher.base.presentation + +interface BaseViewState \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/Model.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/Model.kt new file mode 100644 index 0000000000..83f55229d0 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/Model.kt @@ -0,0 +1,22 @@ +package foundation.e.blisslauncher.base.presentation + +import io.reactivex.Observable + +interface Model { + + /** + * Model will receive intents to be processed via this function + * + * Model State is immutable. Processed intents will copy and create a new modified state. + */ + fun process(intent: BaseIntent) + + /** + * Observable stream of changes to the Model state + * + * Every time a model state is replaced by a new one, this observable will emit that. + * + * Views should only subscribe to this. + */ + fun states(): Observable +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt new file mode 100644 index 0000000000..0b182d953c --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt @@ -0,0 +1,8 @@ +package foundation.e.blisslauncher.common + +import foundation.e.blisslauncher.base.presentation.BaseViewState +import io.reactivex.Observable +import io.reactivex.disposables.Disposable + +fun Observable.subscribeToState(onNext: (state: S) -> Unit): Disposable = + this.subscribe(onNext) \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt new file mode 100644 index 0000000000..8a79d48e6d --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt @@ -0,0 +1,64 @@ +package foundation.e.blisslauncher.common.util + +import android.view.View +import android.view.Window +import foundation.e.blisslauncher.common.Utilities +import javax.inject.Inject + +class SystemUiController @Inject constructor(private val window: Window) { + private val states = IntArray(5) + + fun updateUiState(uiState: Int, isLight: Boolean) { + updateUiState( + uiState, + if (isLight) FLAG_LIGHT_NAV or FLAG_LIGHT_STATUS else FLAG_DARK_NAV or FLAG_DARK_STATUS + ) + } + + fun updateUiState(uiState: Int, flags: Int) { + if (states[uiState] == flags) { + return + } + states[uiState] = flags + val oldFlags = window.decorView.systemUiVisibility + // Apply the state flags in priority order + var newFlags = oldFlags + for (stateFlag in states) { + if (Utilities.ATLEAST_OREO) { + if (stateFlag and FLAG_LIGHT_NAV != 0) { + newFlags = newFlags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else if (stateFlag and FLAG_DARK_NAV != 0) { + newFlags = + newFlags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() + } + } + if (stateFlag and FLAG_LIGHT_STATUS != 0) { + newFlags = newFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } else if (stateFlag and FLAG_DARK_STATUS != 0) { + newFlags = newFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + } + } + if (newFlags != oldFlags) { + window.decorView.systemUiVisibility = newFlags + } + } + + override fun toString(): String { + return "states=${states.contentToString()}" + } + + companion object { + // Various UI states in increasing order of priority + const val UI_STATE_BASE_WINDOW = 0 + const val UI_STATE_ALL_APPS = 1 + const val UI_STATE_WIDGET_BOTTOM_SHEET = 2 + const val UI_STATE_ROOT_VIEW = 3 + const val UI_STATE_OVERVIEW = 4 + + const val FLAG_LIGHT_NAV = 1 shl 0 + const val FLAG_DARK_NAV = 1 shl 1 + const val FLAG_LIGHT_STATUS = 1 shl 2 + const val FLAG_DARK_STATUS = 1 shl 3 + + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt new file mode 100644 index 0000000000..a692f60eaf --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt @@ -0,0 +1,60 @@ +package foundation.e.blisslauncher.common.util + +import android.os.SystemClock +import android.os.Trace +import android.util.ArrayMap +import android.util.Log +import android.util.Log.VERBOSE + +class TraceHelper { + companion object { + private const val SYSTEM_TRACE = false + private val upTimes = ArrayMap() + + fun beginSection(sectionName: String) { + var time = upTimes[sectionName] + if (time == null) { + time = if (Log.isLoggable(sectionName, VERBOSE)) 0 else -1 + upTimes.put(sectionName, time) + } + + if (time >= 0) { + if (SYSTEM_TRACE) { + Trace.beginSection(sectionName) + } + time = SystemClock.uptimeMillis() + } + } + + fun partitionSection(sectionName: String, partition: String) { + var time = upTimes[sectionName] + if (time != null && time >= 0) { + if (SYSTEM_TRACE) { + Trace.endSection() + Trace.beginSection(sectionName) + } + + val now = SystemClock.uptimeMillis() + Log.d(sectionName, "${partition} : ${now - time}") + time = now + } + } + + fun endSection(sectionName: String) { + endSection(sectionName, "End") + } + + fun endSection(sectionName: String, msg: String) { + val time = upTimes[sectionName] + if (time != null && time >= 0) { + if (SYSTEM_TRACE) { + Trace.endSection() + } + Log.d( + sectionName, + "${msg} : ${(SystemClock.uptimeMillis() - time)}" + ) + } + } + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt new file mode 100644 index 0000000000..b4dc36ce15 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt @@ -0,0 +1,116 @@ +package foundation.e.blisslauncher.features.launcher + +import android.app.ActivityOptions +import android.content.res.Configuration +import android.os.Bundle +import android.os.StrictMode +import android.os.StrictMode.VmPolicy +import android.view.View +import dagger.android.AndroidInjection +import foundation.e.blisslauncher.base.BaseDraggingActivity +import foundation.e.blisslauncher.common.subscribeToState +import foundation.e.blisslauncher.common.util.TraceHelper +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.interactor.LoadLauncher +import foundation.e.blisslauncher.domain.keys.PackageUserKey +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign +import javax.inject.Inject + +class LauncherActivity : BaseDraggingActivity(), LauncherView { + + private lateinit var oldConfig: Configuration + + private lateinit var loadLauncher: LoadLauncher + + private val compositeDisposable: CompositeDisposable = CompositeDisposable() + + @Inject + lateinit var launcherViewModel: LauncherViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + if (DEBUG_STRICT_MODE) { + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() // or .detectAll() for all detectable problems + .penaltyLog() + .build() + ) + StrictMode.setVmPolicy( + VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build() + ) + } + + TraceHelper.beginSection("Launcher-onCreate") + + super.onCreate(savedInstanceState) + TraceHelper.partitionSection("Launcher-onCreate", "super call") + + oldConfig = Configuration(resources.configuration) + + compositeDisposable += (launcherViewModel.states().subscribeToState { render(it) }) + //TODO set model and state here + launcherViewModel.loadLauncher() + } + + override fun getRootView(): View { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun invalidateParent(launcherItem: LauncherItem) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getActivityLaunchOptions(v: View): ActivityOptions? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onDestroy() { + super.onDestroy() + launcherViewModel.terminate() + } + + companion object { + const val TAG = "Launcher" + const val LOGD = false + + const val DEBUG_STRICT_MODE = false + + private const val REQUEST_CREATE_SHORTCUT = 1 + private const val REQUEST_CREATE_APPWIDGET = 5 + + private const val REQUEST_PICK_APPWIDGET = 9 + + private const val REQUEST_BIND_APPWIDGET = 11 + + const val REQUEST_BIND_PENDING_APPWIDGET = 12 + const val REQUEST_RECONFIGURE_APPWIDGET = 13 + + // Type: int + private const val RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen" + // Type: int + private const val RUNTIME_STATE = "launcher.state" + // Type: PendingRequestArgs + private const val RUNTIME_STATE_PENDING_REQUEST_ARGS = "launcher.request_args" + // Type: ActivityResultInfo + private const val RUNTIME_STATE_PENDING_ACTIVITY_RESULT = + "launcher.activity_result" + // Type: SparseArray + private const val RUNTIME_STATE_WIDGET_PANEL = "launcher.widget_panel" + } + + override fun updateIconBadges(updatedBadges: Set) { + } + + override fun render(state: LauncherState) { + + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt new file mode 100644 index 0000000000..da745e6cb8 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt @@ -0,0 +1,13 @@ +package foundation.e.blisslauncher.features.launcher + +import dagger.Module +import dagger.Provides +import foundation.e.blisslauncher.common.util.SystemUiController + +@Module +class LauncherActivityModule { + + @Provides + fun provideSystemUiController(launcherActivity: LauncherActivity): SystemUiController = + SystemUiController(launcherActivity.window) +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt new file mode 100644 index 0000000000..6e8e016134 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt @@ -0,0 +1,436 @@ +package foundation.e.blisslauncher.features.launcher + +import android.content.ComponentName +import android.content.Context +import android.content.pm.LauncherActivityInfo +import android.os.UserHandle +import androidx.core.util.set +import foundation.e.blisslauncher.base.presentation.BaseViewState +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.ItemInfoMatcher +import foundation.e.blisslauncher.domain.Matcher +import foundation.e.blisslauncher.domain.addFlag +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.FolderItem +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.WorkspaceItem +import foundation.e.blisslauncher.domain.removeFlag +import javax.inject.Inject + +/** + * Stores data related to Launcher in memory. + */ +data class LauncherState @Inject constructor( + /*val context: Context, + val launcherApps: LauncherAppsCompat,*/ + /** + * Map of all the items (apps, shortcuts, folder or widgets) to their ids + */ + val itemsIdMap: LongArrayMap, + + /** + * List of all apps, folders, shortcuts and widgets directly on screen + * (no apps, shortcuts within folders). + */ + val allItems: List, + + /** + * Map of all the folders to their ids + */ + val folders: LongArrayMap, + + /** + * Ordered list of workspace screen ids + */ + val workspaceScreen: List, + + /** The list of all apps. */ + val data: List +) : BaseViewState { + + @Synchronized + fun clear() { + itemsIdMap.clear() + folders.clear() + } + + /*override fun getAllActivities( + user: UserHandle, + quietMode: Boolean + ): List { + val apps = launcherApps.getActivityList(null, user) + if (apps.isNotEmpty()) { + apps.forEach { + add(ApplicationItem(it, user, quietMode), it) + } + } + return data + } + + override fun add( + packageName: String, + user: UserHandle, + quietMode: Boolean + ): ArrayList { + val addedPackageApps = ArrayList() + val matches = launcherApps.getActivityList(packageName, user) + matches.forEach { info -> + add(ApplicationItem( + info, user, quietMode + ).apply { + id = System.nanoTime() + }.let { + addedPackageApps.add(it) + it + }, + info + ) + } + return addedPackageApps + } +*/ + fun remove(packageName: String, user: UserHandle) { + val data = data + val iterator = data.iterator() + /*while (iterator.hasNext()) { + val item = iterator.next() + if (item.componentName.packageName == packageName && item.user == user) { + removed.add(item) + iterator.remove() + } + }*/ + } + + /*override fun updatedPackages( + packages: Array, + user: UserHandle, + quietMode: Boolean + ): List { + //TODO: Update icon cache for packages + val addedApps = ArrayList() + val modifiedApps = ArrayList() + + val removedPackages = HashSet() + val removedComponents = HashSet() + + packages.forEach { + if (!launcherApps.isPackageEnabledForProfile(it, user)) { + removedPackages.add(it) + } else { + val matches = launcherApps.getActivityList(it, user) + if (matches.isNotEmpty()) { + removedComponents.addAll( + removeIfNoActivityFound( + context, + matches, + it, + user + ) + ) + + matches.forEach { + var applicationItem = + findApplicationItem(it.componentName, user) + if (applicationItem == null) { + applicationItem = + ApplicationItem(it, user, quietMode) + add(applicationItem, it) + addedApps.add(applicationItem) + } else { + //TODO: update icon and title + modifiedApps.add(applicationItem) + } + } + } else { + removedPackages.add(it) + } + } + } + val flagOp = removeFlag(LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE) + val matcher = Matcher.ofPackages(packages.toHashSet(), user) + itemsIdMap.forEach { + //TODO: If user and packageSet of icon resource equals, + //TODO: Update item flag here. + } + return modifiedApps + }*/ + + fun suspendPackages( + packages: Array, + user: UserHandle + ): List { + val matcher: ItemInfoMatcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + addFlag(LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED) + ) + } + + fun unsuspendPackages( + packages: Array, + user: UserHandle + ): List { + val matcher: ItemInfoMatcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + removeFlag(LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED) + ) + } + + fun updateUserAvailability(user: UserHandle, quietMode: Boolean): List { + val matcher: ItemInfoMatcher = Matcher.ofUser(user) + val flagOp = if (quietMode) addFlag else removeFlag + return updateDisabledFlags( + matcher, + flagOp(LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER) + ) + } + + fun makePackagesUnavailable( + packages: Array, + user: UserHandle + ): List { + val matcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + addFlag(LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE) + ) + } + + fun removePackages(packages: Array, user: UserHandle): List { + val removedPackages = packages.toHashSet() + val matcher = Matcher.ofPackages(removedPackages, user) + // Remove any queued items from the install queue + //TODO: InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser) + return removePackages(matcher) + } + + @Synchronized + fun removeItem(context: Context, vararg items: LauncherItem) { + items.forEach { + when (it.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.remove(it.id) + //allItems.remove(it) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + //allItems.remove(it) + } + } + itemsIdMap.remove(it.id) + } + } + + @Synchronized + fun addItem(item: LauncherItem, newItem: Boolean): LauncherState { + val mutableAllItems = allItems.toMutableList() + when (item.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.put(item.id, item as FolderItem) + mutableAllItems.add(item) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP + || item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT + ) { + mutableAllItems.add(item) + } else { + if (newItem) { + if (!folders.containsKey(item.container)) { + + } + } else { + findOrMakeFolder(item.container).add(item as WorkspaceItem, false) + } + } + } + } + return copy(allItems = mutableAllItems) + } + + fun findOrMakeFolder(id: Long): FolderItem { + var folderItem: FolderItem? = folders[id] + if (folderItem == null) { + folderItem = FolderItem() + folders[id] = folderItem + } + return folderItem + } + + /** + * Returns whether *apps* contains *component*. + */ + fun checkForComponent( + apps: List, + component: ComponentName + ): Boolean { + for (info in apps) { + if (info.componentName == component) { + return true + } + } + return false + } + + /** + * Finds an application item corresponding to the given component name and user. + */ + fun findApplicationItem( + componentName: ComponentName, + user: UserHandle + ): ApplicationItem? { + for (item in data) { + if (componentName == item.componentName && user == item.user) { + return item + } + } + return null + } + + fun add(item: ApplicationItem, info: LauncherActivityInfo): LauncherState { + if (findApplicationItem(item.componentName, item.user) != null) { + return + } + // TODO: Update icon from IconCache + val mutableData = data.toMutableList() + mutableData.add(item) + return copy(data = mutableData) + data.add(item) + addItem(item, true) + } + + @Synchronized + fun updateDisabledFlags( + matcher: ItemInfoMatcher, + flagOp: (oldFlags: Int) -> Int + ): List { + val updatedItems = ArrayList() + itemsIdMap.filter { + it is LauncherItemWithIcon && matcher(it, it.getTargetComponent()!!) + }.forEach { + it as LauncherItemWithIcon + val oldFlags = it.runtimeStatusFlags + it.apply { flagOp(runtimeStatusFlags) } + if (it.runtimeStatusFlags != oldFlags) + updatedItems.add(it) + } + return updatedItems + } + + @Synchronized + fun removePackages( + matches: (item: LauncherItem, cn: ComponentName) -> Boolean + ): List { + val removedItems = HashSet() + itemsIdMap.forEach { + if (it is LauncherItemWithIcon) it.let { + val cn = it.getTargetComponent() + if (cn != null && matches(it, cn)) removedItems.add(it) + } else if (it is FolderItem) it.let { folder -> + folder.contents.forEach { + val cn = it.getTargetComponent() + if (cn != null && matches(it, cn)) removedItems.add(it) + } + } + } + //TODO: delete items from database sequentially and remove them from itemsIdMap + return removedItems.toList() + } + + @Synchronized + fun removeIfNoActivityFound( + context: Context, + matches: List, + packageName: String, + user: UserHandle + ): HashSet { + val removedComponents = HashSet() + val modified = ArrayList() + itemsIdMap.filter { it.itemType == LauncherConstants.ItemType.APPLICATION } + .forEach { + it as ApplicationItem + if (it.user == user && packageName == it.componentName.packageName) { + if (!findActivity(matches, it.componentName)) { + removedComponents.add(it.componentName) + } + } + } + return removedComponents + } + + /** + * Returns whether *apps* contains *component*. + */ + private fun findActivity( + apps: List, + component: ComponentName + ): Boolean { + for (info in apps) { + if (info.componentName == component) { + return true + } + } + return false + } + + /** + * Find an AppInfo object for the given componentName + * + * @return the corresponding AppInfo or null + */ + fun findAppInfo( + componentName: ComponentName, + user: UserHandle + ): ApplicationItem? { + for (item in itemsIdMap) { + if (item is ApplicationItem + && componentName == item.componentName + && user == item.user + ) { + return item + } + } + return null + } + + fun addItem(item: LauncherItem, + mutableData: MutableList, + mutableAllItems: MutableList, + newItem: Boolean + ) { + mutableData.add(item as ApplicationItem) + itemsIdMap.put(item.id, item) + when (item.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.put(item.id, item as FolderItem) + mutableAllItems.add(item) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP + || item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT + ) { + mutableAllItems.add(item) + } else { + if (newItem) { + if (!folders.containsKey(item.container)) { + } + } else { + findOrMakeFolder(item.container).add( + item as WorkspaceItem, + false + ) + } + } + } + } + } + + companion object { + const val TAG = "LauncherModelStore" + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt new file mode 100644 index 0000000000..f337917ef7 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.features.launcher + +import foundation.e.blisslauncher.base.presentation.BaseView +import foundation.e.blisslauncher.domain.keys.PackageUserKey + +interface LauncherView: + BaseView { + + fun updateIconBadges(updatedBadges: Set) +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt new file mode 100644 index 0000000000..ae00b0028a --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt @@ -0,0 +1,7 @@ +package foundation.e.blisslauncher.features.launcher + +import foundation.e.blisslauncher.base.presentation.BaseViewEvent + +sealed class LauncherViewEvent: BaseViewEvent { + object LoadLauncher: LauncherViewEvent() +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt new file mode 100644 index 0000000000..b0ce46ded2 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt @@ -0,0 +1,63 @@ +package foundation.e.blisslauncher.features.launcher + +import foundation.e.blisslauncher.base.presentation.BaseIntent +import foundation.e.blisslauncher.base.presentation.BaseViewModel +import foundation.e.blisslauncher.base.presentation.intent +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.interactor.LauncherStateInteractor +import foundation.e.blisslauncher.domain.interactor.LoadLauncher +import foundation.e.blisslauncher.domain.interactor.ObserveAddedApps +import io.reactivex.disposables.CompositeDisposable +import javax.inject.Inject + +class LauncherViewModel @Inject constructor( + private val launcherStateInteractor: LauncherStateInteractor, + private val observeAddedApps: ObserveAddedApps, + private val loadLauncher: LoadLauncher +) : BaseViewModel( + LauncherState( + itemsIdMap = LongArrayMap(), + allItems = emptyList(), + folders = LongArrayMap(), + workspaceScreen = emptyList(), + data = emptyList() + ) +) { + private val disposable = CompositeDisposable() + + init { + launcherStateInteractor(LauncherStateInteractor.Command.INIT) + + observeAddedApps.observe { + } + } + + fun loadLauncher() { + process(loadLauncherIntent()) + } + + private fun loadLauncherIntent(): BaseIntent { + return intent { + loadLauncher( + onSuccess = { list -> + process(intent { + val mutableAllItems = allItems.toMutableList() + mutableAllItems.addAll(list) + copy(data = list, allItems = mutableAllItems) + }) + }, + onError = { + it.printStackTrace() + process(intent { copy(data = emptyList()) }) + } + ) + copy() + } + } + + fun terminate() { + launcherStateInteractor(LauncherStateInteractor.Command.TERMINATE) + disposable.dispose() + observeAddedApps.dispose() + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/PagedView.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/PagedView.java similarity index 99% rename from app/src/main/java/foundation/e/blisslauncher/core/customviews/PagedView.java rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/PagedView.java index 9ec64ffa76..3aa58ade47 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/PagedView.java +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/PagedView.java @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.core.customviews; +package foundation.e.blisslauncher.features.launcher; /* * Copyright (C) 2012 The Android Open Source Project @@ -43,9 +43,9 @@ import android.widget.Scroller; import java.util.ArrayList; import foundation.e.blisslauncher.R; -import foundation.e.blisslauncher.core.Utilities; -import foundation.e.blisslauncher.core.customviews.pageindicators.PageIndicator; -import foundation.e.blisslauncher.core.touch.OverScroll; +import foundation.e.blisslauncher.common.Utilities; +import foundation.e.blisslauncher.features.launcher.pageindicators.PageIndicator; +import foundation.e.blisslauncher.touch.OverScroll; /** * An abstraction of the original Workspace which supports browsing through a diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicator.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicator.kt similarity index 75% rename from app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicator.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicator.kt index d75d080011..dd4f689ee5 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicator.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicator.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.core.customviews.pageindicators +package foundation.e.blisslauncher.features.launcher.pageindicators /** * Base class for a page indicator. diff --git a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicatorDots.kt similarity index 99% rename from app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicatorDots.kt index 1272c8c71e..eef6dc5573 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/customviews/pageindicators/PageIndicatorDots.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicatorDots.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.core.customviews.pageindicators +package foundation.e.blisslauncher.features.launcher.pageindicators import android.animation.Animator import android.animation.AnimatorListenerAdapter diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt new file mode 100644 index 0000000000..b14bb2c153 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt @@ -0,0 +1,17 @@ +package foundation.e.blisslauncher.inject + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import foundation.e.blisslauncher.common.inject.PerActivity +import foundation.e.blisslauncher.features.launcher.LauncherActivity +import foundation.e.blisslauncher.features.launcher.LauncherActivityModule + +@Module +abstract class ActivityBindsModule { + + @PerActivity + @ContributesAndroidInjector(modules = [LauncherActivityModule::class]) + abstract fun contributesLauncherActivity(): LauncherActivity + + +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppComponent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppComponent.kt new file mode 100644 index 0000000000..9a8ce92651 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppComponent.kt @@ -0,0 +1,30 @@ +package foundation.e.blisslauncher.inject + +import dagger.BindsInstance +import dagger.Component +import dagger.android.AndroidInjectionModule +import dagger.android.AndroidInjector +import foundation.e.blisslauncher.BlissLauncher +import foundation.e.blisslauncher.domain.inject.DomainComponent +import javax.inject.Singleton + +@Singleton +@Component( + modules = [ + AppModule::class, + AndroidInjectionModule::class, + ActivityBindsModule::class + ], + dependencies = [ + DomainComponent::class + ] +) +interface AppComponent : AndroidInjector { + @Component.Factory + interface Factory { + fun create( + @BindsInstance application: BlissLauncher, + domainComponent: DomainComponent + ): AppComponent + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt new file mode 100644 index 0000000000..7e083ee310 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt @@ -0,0 +1,13 @@ +package foundation.e.blisslauncher.inject + +import android.content.Context +import dagger.Module +import dagger.Provides +import foundation.e.blisslauncher.BlissLauncher + +@Module +class AppModule { + + @Provides + fun provideContext(application: BlissLauncher): Context = application.applicationContext +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/Qualifiers.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/Qualifiers.kt new file mode 100644 index 0000000000..a4c2ec6f90 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/Qualifiers.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.inject + +import javax.inject.Qualifier + +@Qualifier +annotation class ThemeRef \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/OverScroll.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/OverScroll.java new file mode 100644 index 0000000000..ebeede0ac1 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/OverScroll.java @@ -0,0 +1,40 @@ +package foundation.e.blisslauncher.touch; + +/** + * Utility methods for overscroll damping and related effect. + */ +public class OverScroll { + + private static final float OVERSCROLL_DAMP_FACTOR = 0.07f; + + /** + * This curve determines how the effect of scrolling over the limits of the page diminishes + * as the user pulls further and further from the bounds + * + * @param f The percentage of how much the user has overscrolled. + * @return A transformed percentage based on the influence curve. + */ + private static float overScrollInfluenceCurve(float f) { + f -= 1.0f; + return f * f * f + 1.0f; + } + + /** + * @param amount The original amount overscrolled. + * @param max The maximum amount that the View can overscroll. + * @return The dampened overscroll amount. + */ + public static int dampedScroll(float amount, int max) { + if (Float.compare(amount, 0) == 0) return 0; + + float f = amount / max; + 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); + } + + return Math.round(OVERSCROLL_DAMP_FACTOR * f * max); + } +} diff --git a/blisslauncherv2/src/main/res/drawable-v24/ic_launcher_foreground.xml b/blisslauncherv2/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..1f6bb29060 --- /dev/null +++ b/blisslauncherv2/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/blisslauncherv2/src/main/res/drawable/ic_launcher_background.xml b/blisslauncherv2/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..0d025f9bf6 --- /dev/null +++ b/blisslauncherv2/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blisslauncherv2/src/main/res/layout/activity_main.xml b/blisslauncherv2/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..95459fce8c --- /dev/null +++ b/blisslauncherv2/src/main/res/layout/activity_main.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..eca70cfe52 --- /dev/null +++ b/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..eca70cfe52 --- /dev/null +++ b/blisslauncherv2/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher.png b/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..898f3ed59ac9f3248734a00e5902736c9367d455 GIT binary patch literal 2963 zcmeAS@N?(olHy`uVBq!ia0y~yVDJE84mJh`hS0a0-5D6TqdZ+4Ln>~)ox_Sh7V2l8OE(I-~0OZ?r*;i#YvyJ zYi1Gn!qwT5%af&3L35#?ZNOwUsVQ=bDi0XAt7`w}mmlB!_$R~q+?ovcU&+eNZxZ1XmACEk$ZF@ked3LU*{umT zA4=C8d4DMVo2;S2v~|v(-db$mnB)^O^=(8*Dr4r?2xnH?n?c@HW*Kd_J@4O))7^SL zYt@pSXZ2Rfn&l{OetaXtIjueaL*V^9YqPD3l6(p#gglwI*B)~TP28P)tV zT%#m!PTR3{&X=4EJG7*I*J)3^CBIT{$;YingRFCIxTPK6c(Lsa-~9)R_u1Y~)-exu z`SnWts$n_Xg#{edWiK08Q%-+#vD6Wnxba$RL^Gp_#U_Q~*9TaqJnbxs(lkmwTV{Bs ziNVCg#(r(0!*>NX6Pw&n?j&BWfRwtb#}4cwGiU8l&TvS6b2D#=X9|Wq74)35MJZ<~0tFlUS6=ELScR%Ab)A(z^~pT7RCq{)IG zLX)MpvRqO<*A`arpxQoP%~tx$mvdE7o7Q_~`QN(haQHH$$al|Q-9bc*_Y8O#gZAeTW-6? z9QFR-m)7ntXWs2yvR%K$D*fL#DM)15Z@=z4b-7=_$J_3%dlzp7IdXeG&vBEz2OSr0 zzRu3CvF^l^W47XV!VcWL#MOFh(q3QLYt|Cy-ddNRDE@8s_uu>HnSPtQzkG9NSW)~X zM|>`vg^Ye);vYm`i3(M{mBIfWz;rDz-g|H~eJ+nGR04{r91yRD7@w zll}Lf>0NL04w=cv{1Z==`={%>LU*D0zfb8$%O+PpPqVuwz;;7_b3~1=py@nIO(ww! z7B{v5qOWfnTwhYa5Ps{{ zF4?~L_${%^d)xUswtcB-$o~*{zHLTY(R;bwmChd;k|YG?`9A!}9kecNg>u1px7~G2 z+OJQ%f3aHn{uc?E{TJMRJ-pvxwzckn`HvOPt&?V54skuUj)AMJqjvW7zhC$Ne^+Qx zH~0Vf=ZntP8L}_jceDImZKlEXop~SAx1M_;eJ<)mlF@HFJ;k=nOSSV^w!Ar@FVR$| z@ILlW%e<_DOP~JketB6t{*Jkf&nNvinTu3)zm|NOFYkTe{*u)7@88~TXe%(S{)$CkzjIr->2dpe=?h`|6hwc|40_@Idz&Zsnak7dmMTvBvX49CVPDm+U&nZ3 zp9emeTAV67ZNA%mtrL&tedzzqR`MeMd1gr>AFtu^pu@ZKy|+8x=AIpsa>74#zVUB4 zh36-Qy_T+CZYIZ;{zGT}-Wt!CBlq+ba;h8E{|d{;3ml)2+rDk?dl_ZsW|I${9&7Yp zM>@>){5<)U*UL*+_2na;O?r80dU%G@(q17M)-5}h_B}KGo-pgZ&%%!9>GKrUZQK?8 zA+au5^=N2BMqsh&*;`SL{X42w1iU#T5WQpZy7LTCClU7Cz&rs z%WvE+uaXydC$eWx{dDnwQbEbnpN`a|Y{*iu&2ZRJ@MqTPf7x?9MG6Bu4jClO^}4?M zV4ZKWyV~uN54?=7j47Uy=3TTxM7C+=ZM^e=@$l(xqz{STl2Ht?ajO z&BpJ&t+x6{m;1)e+&k;VmOzs?O4%!?E@RfXt8Kp|Zsiiw3Ga9{*2_*{>l2^g%Bms2 zx4>u37sgACm-$ZI;d5t55>5H?=&ypwyy=%L9jpopddeJRAO2uGI%Rh0!v)tl9G*UN zun3FS-;`D+8?d3jR;_rh;l;$I>aR{^1TGGC;F{w4LeuZy{ioTRJLgQ=xHrsy_v#GB z_x2Yf7fkYPXq>d|>V?Vgyk>=@Cf~e$^d@&zUb?L<)Z_^_La>r5Dv0K4MqVwMG6HL(_P4yug}e+vFI z$V{{Q_N$w1lK2GIsOJy5Vi+b}u{;qH=kRjZN*jg=QhfpOEKk~57%J@?7!+0(TQPC1 zDc;kdGs()G)r0%^HQ$D+H>MAM1-dtu11m`B@nhsvwA?2RA( zT$%Sjthv35BTjv4zj(u%OIGaDdE*=;cdd+JT++Ij$;9Ss`oa3uf;0a@M>%X0YM2w2IrlQ- z1hweS%2m~)$jAC9bNCEcxTgrP)j|VKuK+_4_qr{ZgPF@ zov@!*&|9tdR7CN#sb;Q`TCqxck$7d_QKcV`?eoacYRfw z<(ps9k)Cd3Ew}r@%@B6!?_C;xk2nAL;qf?d&b5!*!+&-?E1Gp&mpdj{vuF!GVi#-js-cIG0pEd1=PLhdEoXr)1D@JuS7XPjLOtt^CKHtt0zq2I&#;d36Ti3>I zv)8Gte0}1pcGTkFNzos7&vRQFe>3Zj#`gZW?VC#991AVq_&-ELr!q79hSvN^)3xfQ zudmONRef{1WXm4YS^c&}ce&nwj7ywbP0l+XkKD`=m< literal 0 HcmV?d00001 diff --git a/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..dffca3601eba7bf5f409bdd520820e2eb5122c75 GIT binary patch literal 4905 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 literal 0 HcmV?d00001 diff --git a/blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..dae5e082342fcdeee5db8a6e0b27028e2d2808f5 GIT binary patch literal 2783 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 VzEe;BFfcGMc)I$ztaD0e0s!qfa#8>Q literal 0 HcmV?d00001 diff --git a/blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..14ed0af35023e4f1901cf03487b6c524257b8483 GIT binary patch literal 6895 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 literal 0 HcmV?d00001 diff --git a/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher.png b/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b0907cac3bfd8fbfdc46e1108247f0a1055387ec GIT binary patch literal 6387 zcmeAS@N?(olHy`uVBq!ia0y~yV3+{H9Bd2>4A0#j?OR&CQAkwDQr=p0IW8jAPSMoKO6cIr^<$=WM&6t7KPBb=t~=pr27YHY>PJ ztlb~OyY>D2)uAc&7WOl5eOEgEu;jPe{+f%XwuzgTzK`1TZ07d8HHjzMVid}mS;W&{ zHeXMD=`NmrSHZ~Q@|m?u+m`;`F8w4&d)e~xFv)p0w?=OKJE_*R?dI)MA~z=$o9!t` zXn#0ym5}Q<;ZxUxetSM`?Rv93L@fCC$0rtZa;lkYol?ID_MEz?w=K&yO6%E6#|*a$ zt0ik0-%rVyTV79^QRk0aicEIG-X|5fXSu)N{g)Fh>1mH&^r@7*;L+UK%L zQ0~$>rqv(v6igX3HoD%8kMmH_3d*rh&eqIjn|kAykh{X4X6{L%H-Ar7V|}|(GbG7g z!@l!Yxkqo})}{MiO_lgr`kzzy&zhQ7hi)Br_K4Xjayv9<+k`y6fZ2>39tmsTPu;}! zt}FH@*H_h*JTcbG6gcKJSuL8zw55;v$}@&bF0ZO5|7BBklU3gLMaU`ksH*|MQv}0d! zmNA9n#MJ{ArtDaKXwC6sLGyPl5%Zey#&z``@p-bXR~6V-tYs2$WR#jFv|K#R=8yZU z%i_nYcXKJ^CwgXVSP;APF~ioZtiD&j~RAyI`rEabpA3a znbQB7VV1pm3-c2-2H(B^>-YrNSNJkMIn7vP-B4rcAfv!1z<%N_g|W-f6ooh`Tzaa-49Rtyj-_gbnpLj!LB0b zG@I);{t%I%-|X65A~)@KRoZUdpPnqYS@+(T$=cnCNpW5KK7U!M*5H(H>+sl@;cM(R zinH~{!MFBZPIZykE-2# z^XhZkm437CUcK_~BHOE7Z>GP!@Ar1Tk~za>+nq1PCrjt26|UR&{LM>|1%c}rI?NOI za6V=zY_NcMuY>sr<0B%tYXufe;I$0+dh&O{pWXQiAAblIF`949Y=~QRmv^?^Kk+#h z#Zp}NtQ{Bb(EZd{r8isNXorGpySV)Hh=ZX<8_W_!pM6g7YMYZQxFx@S7ms<~oduW7 zO%5!du6O9wt|-^vZmu$x4xc-x+^~y@^!eWVQpD?xA@7qb{q`pF{FyISKRWx4)#ub6*3e({NKf{%90*wtL|@65LP+FJ2Bd;Y{<{LE?p zY1W$e9CssKzxQ{huK8WRVbKYL=~v?VcgQW6mVM~aJsX{>gCEb?{yMuqF(EE!cE+I( zbMEgi@cR-Ia6{*_#0xQo7R@E$rC%or$XeG<(z|wL7Nb<$6h8U?&-bX~nv&V} zpyU5OZ(VKB{V=MaL8R>Q;#YC)*?bDCtpA$cnJB0JdiRE%6=mPWq|PkgSIkpZ`%!bx z&$pUC9z3#?dJwm-s`cNI^T+qudj9**xnxf;d-u1n%dA$0|0XgA{I}7q`Fi;Kq~m{- zZ}PKk-6Q>;so~bdD2D7s+F!p;;w{_(f_AT#MpMS9Z^>xSP z*DuS>y;9!ExqZEQSw{VX`|}S^sx>uX`+ULW)?4EV4B!5>Zk}}i>*cGfOV&E`+wncw z)##Grw?VB)M0oGNug?W0%=Os*-YaX#&)w#SrI+tpRK9zi<9U`IW5t%~>;Lfm=A6=h zJ;dSo71NUa&RoF{?-t+py!E%}t<+oAN4^Kyg<@@ldJQ?s-mZUrboOSpAn!eHS?x1+ zIe)l#^z)<*rrYb*@8|oka$$|tFRMMp&$b+%q5k~t^ zGk3$pNsDX$J7j6s$nml({tR*BKHvIq^1@Zy?-fMy7H>YZEKiX0R`uj(Q_r_K-KM&rAxcgmX2FA_%+|8s zN}?9~&H35Z)c-g@W`P(B$HSlhzTdwkVv{!a|JU@6b*rB;Y!z|%KKa)3Kl=9DJuA-a z`f_`{LFwmTxASWiV(z6L+2te4_x!Q;2CLomV-F2qzd3Jz zF*Pq@J!4;8cAxyxvkITLuGcfwxwBa`P2{{{5|)fkENg2 zCa%eg1lY`G`xdmtl$19dUt?_l-y>Ui+G~T_({2%k74~x{C+~fi?6ry4L{L}%>Q-)_ zttG;3!7S|lb)Hqmym_nweY39D{dIjT!qMND$LL^jb4BfMjTcgPxaS@FX;KtdQ70#} z;2ArI{;Msz(lX}`>~8+>=Yx%{g~M5Hfpg31D?aXtKAe1wUHqKm^eq;A0mY0d=l%yi z>RJ7y@86qCtShX4iq84@(tTE0VfJiA+q?Yl_`g?&CiY6!Wyo)N%wWpz@T2Hv_x^u} z-n9Jx_fOhp_gbbs$>9wGpSemx-JPsnelxqXNP>&G`cK^*{&(lC^H^=7gB|;uzwZ7} z(_FMgVTH1VgE{kr@R4M9Rmd=q#Y4E~0^%=mobj*5208LriLYrPq|RL=IP z+NAZK@eMP(>Ui$=x*T4PC2GP#$`%e@3|*FG0UMp)>z)!|)p(fkR+rHuw)_O2+?Bnr zH;L4z^K|}g*%9v8vAw;6`3cJcJIhzIUsXQcSIkrMC@kXOW$jGUKd;5k-l?znr}M=x zGMTmC|Bu&+zxT~W>z}(Xalghs-$1YttaE1c=@*mvz4Yzu3>PYHJ#=D@%8Thz+``?P z6Thd+-`^CNbLQ(!=Y91_o_t@F?-u0jGC9t7`EkInxfTD6S6`E6JT$wJA+xQ6`3Zx9 zj7PNgM;)8jFZ9Y*+wU$349xfFEU@2UZIwL5Kz89%FJiBho z8@0_sWrszy`N|qyP@9W!{+c7Z8$*BW)2>OK`1Zbe%?G{q<4&sY=QiA&s_Hgz2FKQt z>ta%mZTD{O`g`m86zO@l_sgbDfAaG8u5YFOZVgsCbw*37!}jor^HeNZb;MwcL9d`G#a=bAnIE&cX4`AIhzU76wwuVm zJL`Apx9|KnaX%gAc?N!Xck{i4g;>@1Gut1`6i9n>YPN|%uxqxG{T!=8gQVKqtQVO- zZ7KNliC;#B`(aiZGl#)8hO;+wm^iA_d<1qHIwW6D*Pdlln*Q*sMD&bz5(->_GkiMk zNG#}7G1y=8l#BiNv%(jhOOD@Sx!}bVu&9=|CFo@1iC)G-dVliwaOWGZ%6oHa_QQ=# z9>O<&XMCqRHyoO!!Z}fz`O5q`H-e*2KUn!H zXnB|24)G+ux0AQ7opHrIJMm~^tk8e`VhtZLZ@V&yf7{j{tJqVtWv!=oS$^Y`?R^I> z-M#vxcxUie^#eDUw;W_!V36%;mj2FeW|w;2WFEC;zppJjeB=<%^4p#-FY^90uyR+) zKcS}T?DMv>IW2Tev!f)Z!V$gg7gj&&+I)rUgMZ&b=4RUhw*7@&PF~t?O$<(pEifny z?8rM_qaZ18*T#YM8^g&4cPWL+4;@TL82Fizc_%Pegd_>v)w8_V`H)XBtE?gAQ1gZx z%qsG7)(sL5H?nD*va|_#C?;^%um7t>!-`18Nt}I=*BCY)ROVO6tdL^7wJA_WVJeG5 zufW|r|7BhdS8^B(4_)TCpvKN%*|1!2!K$x943=%jIVMEOD7b$xaVoLad#}WF(U*D4 zM20D|kMQubdbBc#3O9tD`C!3z!qSGLOT(e)fum}hBva89-I@GfQap}tG2K)qheRI@ z>QyyAcStsLOSS+1X==+X3YQ+2auZp4c*hr)z1Odtd^er{iAR6<^h0YtDb0usIQQgJ+`Kz7RMONpy*k*X6tQLbteflDS9VrSn3X8C zvT<2{47aUEuF~3!?9AC;L<;XPAHDhH`_*q=lLS`U2F$S7pAc=?BC^Ev-`c<>&kEGm zREi08v`thK2@ZQZC2h5ZcB@G0p+$3NCo-)3TRrJ@;`)l})ET1L5}{KVA72WovJb!J zur{mL<cn`G{Mg| zvJ@EEv!(1W{W;ZLt>c=s;EjvKERogXYWI(I=lI>3!9KZblgPnjmJD5%ZpJ4p^V}M= zx)lwAkMi_OD&#DC!!U`lKe=JfLFc24M(c(DCPX%_$>iS{thZ?{ht%wG9!r>uj~!;S16ymBo^`4reH-Y_OLwlk~z zyH}yvA=C5Tgh}Ky8~-&6KgJ>>+eB6YWt(dZuEvLwnFZ>vI!^qOuy`_~6IVc-k+ID+ z234cO$*ciUI>t7>3{#!jmt?qA%wmw5CUcx|6VnMHCCN!_Cw|@$v%90tHQ`KWpt6H> z4g=R@OG7EP<|31lnB#m3%RkIu3@UDLVO$jZr(p46QAU;gq!1B>w#ZVM&~0oUb~Y7t zix^HewR_0s%{lde!IMFSeNlvA!;|x?ZZjU4F)5K(fZIlh;bdDovkJdLaj_a(;2H^s z98JkE2F(-CGnq8{B;~9dW;}SvaFX4F2c+}H=SnHYQ(hrc&O1Z1>*txJnGJIe9%f8p zJ|UzuJE1a4l2OCnHPDWqbw$=*7Kd}eMg?rKA_`OYY~fLMNHt?P+1So}gl~f6q8;J6 z3|b=W9>?xXGjaIzsnUgU((LoeOd0D$9f}@y8mLX#%H!a>y13+Y&-I6^1cJ4?d3Q`R zUN__DDtGhs?Ww=FYj1LWc)QN2JnZE5lUENpF#T;P31QN?6UG?qY23DdG20@eoTZz> zmvyas+~d9GftKWtot~Z7PHmbxi|ccxfhu$Q)j}V4ACAr`YI$ecpMSr4U_(<#RotRe z!ofnP3ceq+*`e@-Ze?ifCX{3(@$B=1nrT}XWnO;b z5%F2U=HC*tw+?+ry)=`$HeSd&{Gdrh;^`~VQ@2WZRD^_mwat$@aDA$rm87tBQH@+r z ze3+k`7uVpmskg5)-TUMQ<_<&E!*UASrtoNklbNZ#!?VIg6+bt>0;j-@Y#MVcRS$Cn zSn^J|`5=>*!PfXUf0J6!`!I$`Cza&9)q)FhK(1GISo$TC^IMIau!Ct5gJtt^Aq629 zd!v_(*BMPJH;Lc9xhFE2(;@s0pTb$K#lk6PawQpWB`{C;6Iohwfd6>u8-_`ihF#4~ z-xy9dF6UgJc*``rm0jIuiJh#!c6)S;6RZEO&54J9O`2?-%=Dryno))8qL(DY#;2b< zn2s2%&f}jDQ?Vj1!TjI49Ur)z{{M5lvV1bD{z2BIvNspX|9q*xCbEu)QAxmLhpa=d zz}hKuZhyKO!E}VJU&EnCQ}P;v;X!7Vy4)9YCo(i2j<4$fv8ny!^0zU}7iJjBYC6e_|#AzGTPH#xzB7zGQNYrLZ`+4=TR>yoH@x+$?yDsUoVj_9EzvqG)V9ye|4t?V~ z`i=X0&QjxFn{qu4$V`~&U~1sb++CjAxZ9IgY{JS(Vbf*^CAuV9JrL5~;l*aRSXFQ9 z$EYq2XnE6HKiMviXGZn0_QwCGz4c4#-aHI_vH6?Z{hzaIZu%7@el!TJ^vG-Qe}3-p z$@2;O&&^f;GWUN<-h)r#_A)X%{{#j6S(WX46Y}@R5{ue^nk9RMWHi0x*TgRVADlO< zj^}6R{9IEP(YFDQgCBi0KJl^t!jTD&ySCZ;m4-0?oR{6aIBu`Vk5k<8Y&N#${XaO4 z*EIRsaU1-+aC_?3{#4zTgC*gM>Y5BCq~E@MYuopNKknbZU>o=Tnp?$pPO$b}Dg3j8 z_3p9$_y;-Gns*)F1bV$Kye}(y>o?2dzdKm>J#G@Om~=n#Y4WZ8EB5yM{oTjE?3-QR zHfh^Ol3!Rqo~-!7`|HG$)pct3x0aW_O^)QQoVCi{X2QNF0Zk_k_KMZi9Gh@H*~g%8 z>xK*BSJYofe-Zq3>V_M-XQp@3Jyi}R~y)m(k_;f7O-aG|UZdwXM~ez5fI<4g9% z=uQ2(i+x5-qutA-PZRyK8;=Vq8GGDYR(AD#$p%^9S4W-(&0Js2K3V_P?3%}!57&o< zpJBgtPQ`y+nBr#pC|!$l4Ue)SKOW-N-}3>TIPE+2KYQ50;20N&WKISK1_n=8KbLh* G2~7ZK1n3h0 literal 0 HcmV?d00001 diff --git a/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ae03154975f397f8ed1b84f2d4bf9783ecfa26 GIT binary patch literal 10413 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 literal 0 HcmV?d00001 diff --git a/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2c18de9e66108411737e910f5c1972476f03ddbf GIT binary patch literal 9128 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 S=`aQc1_n=8KbLh*2~7aW0lbm` literal 0 HcmV?d00001 diff --git a/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/blisslauncherv2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..beed3cdd2c32af5114a7dc70b9ef5b698eb8797e GIT binary patch literal 15132 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 + + + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/values/colors.xml b/blisslauncherv2/src/main/res/values/colors.xml new file mode 100644 index 0000000000..2566ee833e --- /dev/null +++ b/blisslauncherv2/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #008577 + #00574B + #D81B60 + #f00 + diff --git a/blisslauncherv2/src/main/res/values/dimens.xml b/blisslauncherv2/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..86d89a2827 --- /dev/null +++ b/blisslauncherv2/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 8dp + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/values/strings.xml b/blisslauncherv2/src/main/res/values/strings.xml new file mode 100644 index 0000000000..45446b00bd --- /dev/null +++ b/blisslauncherv2/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + BlissLauncherV2 + Downloaded app disabled in Safe mode + App isn\'t installed + Default Scroll + diff --git a/blisslauncherv2/src/main/res/values/styles.xml b/blisslauncherv2/src/main/res/values/styles.xml new file mode 100644 index 0000000000..5885930df6 --- /dev/null +++ b/blisslauncherv2/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/domain/src/test/java/foundation/e/blisslauncher/domain/ExampleUnitTest.kt b/blisslauncherv2/src/test/java/foundation/e/blisslauncher/ExampleUnitTest.kt similarity index 78% rename from domain/src/test/java/foundation/e/blisslauncher/domain/ExampleUnitTest.kt rename to blisslauncherv2/src/test/java/foundation/e/blisslauncher/ExampleUnitTest.kt index 7c5a7b55e6..65638904ca 100644 --- a/domain/src/test/java/foundation/e/blisslauncher/domain/ExampleUnitTest.kt +++ b/blisslauncherv2/src/test/java/foundation/e/blisslauncher/ExampleUnitTest.kt @@ -1,8 +1,8 @@ -package foundation.e.blisslauncher.domain +package foundation.e.blisslauncher import org.junit.Test -import org.junit.Assert.assertEquals +import org.junit.Assert.* /** * Example local unit test, which will execute on the development machine (host). diff --git a/build.gradle b/build.gradle deleted file mode 100755 index b16585da3c..0000000000 --- a/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -import foundation.e.blisslauncher.buildsrc.Libs - -buildscript { - ext.kotlin_version = '1.3.61' - repositories { - google() - jcenter() - maven { - url 'https://maven.fabric.io/public' - } - } - dependencies { - classpath Libs.androidGradlePlugin - - classpath Libs.Kotlin.gradlePlugin - classpath Libs.Kotlin.extensions - - classpath Libs.Google.fabricPlugin - classpath Libs.Google.gmsGoogleServices - - classpath Libs.dexcountGradlePlugin - } -} - -plugins { - id "com.diffplug.gradle.spotless" version "3.14.0" - id 'com.github.ben-manes.versions' version "0.25.0" -} - -allprojects { - repositories { - google() - jcenter() - maven { url 'https://jitpack.io' } - mavenCentral() - } -} - -subprojects { - apply plugin: 'com.diffplug.gradle.spotless' - spotless { - java { - target '**/*.java' - removeUnusedImports() // removes any unused imports - } - kotlin { - target "**/*.kt" - ktlint() - } - kotlinGradle { - // same as kotlin, but for .gradle.kts files (defaults to '*.gradle.kts') - target '*.gradle.kts', 'additionalScripts/*.gradle.kts' - ktlint() - } - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100755 index 0000000000..d6d16769ab --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,48 @@ +buildscript { + repositories { + google() + jcenter() + maven { + url = uri("https://maven.fabric.io/public") + } + } + dependencies { + classpath(foundation.e.blisslauncher.buildsrc.Libs.androidGradlePlugin) + classpath(foundation.e.blisslauncher.buildsrc.Libs.Kotlin.gradlePlugin) + classpath(foundation.e.blisslauncher.buildsrc.Libs.Kotlin.extensions) + classpath(foundation.e.blisslauncher.buildsrc.Libs.dexcountGradlePlugin) + } +} + +plugins { + id("com.diffplug.gradle.spotless") version "3.14.0" + id("com.github.ben-manes.versions") version "0.25.0" +} + +allprojects { + repositories { + google() + jcenter() + maven { url = uri("https://jitpack.io") } + mavenCentral() + } +} + +subprojects { + apply(plugin = ("com.diffplug.gradle.spotless")) + spotless { + java { + target("**/*.java") + removeUnusedImports() // removes any unused imports + } + kotlin { + target("**/*.kt") + ktlint() + } + kotlinGradle { + // same as kotlin, but for .gradle.kts files (defaults to '*.gradle.kts') + target("*.gradle.kts", "additionalScripts/*.gradle.kts") + ktlint() + } + } +} diff --git a/buildSrc/src/main/java/foundation/e/blisslauncher/buildsrc/Dependencies.kt b/buildSrc/src/main/java/foundation/e/blisslauncher/buildsrc/Dependencies.kt index d7f98933d3..942c7f85a3 100644 --- a/buildSrc/src/main/java/foundation/e/blisslauncher/buildsrc/Dependencies.kt +++ b/buildSrc/src/main/java/foundation/e/blisslauncher/buildsrc/Dependencies.kt @@ -11,10 +11,6 @@ object Versions { const val junit = "4.12" const val robolectric = "4.3" const val mockK = "1.9.3" - const val firebase_core = "17.1.0" - const val crashlytics = "2.10.1" - const val google_services = "4.3.0" - const val fabric = "1.31.0" const val okhttp = "4.1.0" const val retrofit = "2.6.1" const val dagger = "2.24" @@ -34,13 +30,6 @@ object Libs { const val robolectric = "org.robolectric:robolectric:${Versions.robolectric}" const val mockK = "io.mockk:mockk:${Versions.mockK}" - object Google { - const val firebaseCore = "com.google.firebase:firebase-core:${Versions.firebase_core}" - const val crashlytics = "com.crashlytics.sdk.android:crashlytics:${Versions.crashlytics}" - const val gmsGoogleServices = "com.google.gms:google-services:${Versions.google_services}" - const val fabricPlugin = "io.fabric.tools:gradle:${Versions.fabric}" - } - object Kotlin { const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}" const val reflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" @@ -94,7 +83,7 @@ object Libs { } object Room { - private const val version = "2.2.0-beta01" + private const val version = "2.2.3" const val common = "androidx.room:room-common:$version" const val runtime = "androidx.room:room-runtime:$version" const val compiler = "androidx.room:room-compiler:$version" @@ -111,7 +100,7 @@ object Libs { object Dagger { const val dagger = "com.google.dagger:dagger:${Versions.dagger}" - const val androidSupport = "com.google.dagger:dagger-android-support:${Versions.dagger}" + const val android = "com.google.dagger:dagger-android:${Versions.dagger}" const val compiler = "com.google.dagger:dagger-compiler:${Versions.dagger}" const val androidProcessor = "com.google.dagger:dagger-android-processor:${Versions.dagger}" } diff --git a/bump-version.sh b/bump-version.sh index f6d197c70f..0854b0fa23 100755 --- a/bump-version.sh +++ b/bump-version.sh @@ -34,7 +34,7 @@ usage() { echo -e " " echo -e " Used to increment the version of ${RED}BlissLauncher${NOCOLOR} safely by performing predefined actions: " echo " 1. Checks the category of the revision (major,minor or patch) based on the argument (revision_type) passed into the script." - echo -e " 2. Overwrite the version name and version code in app module level build.gradle based on the following logic" + echo -e " 2. Overwrite the version name and version code in app module level build.gradle.kts based on the following logic" echo -e " * - If this upgrade is a major, new version name will be ${CYAN}{old_major + 1}.0.0${NOCOLOR}" echo -e " * - If upgrade type is minor, updated version name will be ${CYAN}old_major.{old_minor + 1}.0${NOCOLOR}" echo -e " * - If it is a patch (hotfix), updated version name will be ${CYAN}old_major.old_minor.{old_patch + 1}.0${NOCOLOR}" diff --git a/common.gradle b/common.gradle new file mode 100644 index 0000000000..94b0822752 --- /dev/null +++ b/common.gradle @@ -0,0 +1,33 @@ +import foundation.e.blisslauncher.buildsrc.Libs +import foundation.e.blisslauncher.buildsrc.Versions + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion Versions.compile_sdk + + defaultConfig { + minSdkVersion Versions.min_sdk + targetSdkVersion Versions.target_sdk + } + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation Libs.Kotlin.stdlib + + // Dagger + implementation Libs.Dagger.dagger + kapt Libs.Dagger.compiler +} \ No newline at end of file diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000000..7b54df9d77 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,4 @@ +apply from: "../common.gradle" +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + diff --git a/common/consumer-rules.pro b/common/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/common/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/common/src/androidTest/java/com/amitkma/common/ExampleInstrumentedTest.kt b/common/src/androidTest/java/com/amitkma/common/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..cb70abd82c --- /dev/null +++ b/common/src/androidTest/java/com/amitkma/common/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.amitkma.common + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.amitkma.common.test", appContext.packageName) + } +} diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8e4906f2de --- /dev/null +++ b/common/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/common/src/main/java/foundation/e/blisslauncher/common/Utilities.java b/common/src/main/java/foundation/e/blisslauncher/common/Utilities.java new file mode 100755 index 0000000000..c04efde585 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/Utilities.java @@ -0,0 +1,267 @@ +package foundation.e.blisslauncher.common; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Utilities { + + private static final String TAG = "Utilities"; + + private static final Pattern sTrimPattern = + Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); + + /** + * Use hard coded values to compile with android source. + */ + public static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; + + public static final boolean ATLEAST_OREO = + Build.VERSION.SDK_INT >= 26; + + public static final boolean ATLEAST_NOUGAT_MR1 = + Build.VERSION.SDK_INT >= 25; + + + // These values are same as that in {@link AsyncTask}. + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = CPU_COUNT + 1; + private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final int KEEP_ALIVE = 1; + /** + * An {@link Executor} to be used with async task with no limit on the queue size. + */ + public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( + CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, new LinkedBlockingQueue()); + + /** + * Compresses the bitmap to a byte array for serialization. + */ + public static byte[] flattenBitmap(Bitmap bitmap) { + // Try go guesstimate how much space the icon will take when serialized + // to avoid unnecessary allocations/copies during the write. + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + + public static float dpiFromPx(int size, DisplayMetrics metrics){ + float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; + return (size / densityRatio); + } + public static int pxFromDp(float size, DisplayMetrics metrics) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + size, metrics)); + } + + public static float pxFromDp(int dp, Context context) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + return dp * (metrics.densityDpi / 160f); + } + + public static int pxFromSp(float size, DisplayMetrics metrics) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + size, metrics)); + } + + /** + * Calculates the height of a given string at a specific text size. + */ + public static int calculateTextHeight(float textSizePx) { + Paint p = new Paint(); + p.setTextSize(textSizePx); + Paint.FontMetrics fm = p.getFontMetrics(); + return (int) Math.ceil(fm.bottom - fm.top); + } + + public static String convertMonthToString(int month) { + switch (month) { + case Calendar.JANUARY: + return "JAN"; + case Calendar.FEBRUARY: + return "FEB"; + case Calendar.MARCH: + return "MAR"; + case Calendar.APRIL: + return "APR"; + case Calendar.MAY: + return "MAY"; + case Calendar.JUNE: + return "JUN"; + case Calendar.JULY: + return "JUL"; + case Calendar.AUGUST: + return "AUG"; + case Calendar.SEPTEMBER: + return "SEP"; + case Calendar.OCTOBER: + return "OCT"; + case Calendar.NOVEMBER: + return "NOV"; + case Calendar.DECEMBER: + return "DEC"; + default: + return ""; + } + } + + /** + * Trims the string, removing all whitespace at the beginning and end of the string. + * Non-breaking whitespaces are also removed. + */ + public static String trim(CharSequence s) { + if (s == null) { + return null; + } + + // Just strip any sequence of whitespace or java space characters from the beginning and end + Matcher m = sTrimPattern.matcher(s); + return m.replaceAll("$1"); + } + + public static ArrayList getAllChildrenViews(View view) { + if (!(view instanceof ViewGroup)) { + ArrayList viewArrayList = new ArrayList(); + viewArrayList.add(view); + + return viewArrayList; + } + + ArrayList result = new ArrayList(); + + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + + View child = viewGroup.getChildAt(i); + + ArrayList viewArrayList = new ArrayList(); + viewArrayList.add(view); + viewArrayList.addAll(getAllChildrenViews(child)); + + result.addAll(viewArrayList); + } + + return result; + } + + public static Bitmap drawableToBitmap(Drawable drawable) { + return Utilities.drawableToBitmap(drawable, true); + } + + public static Bitmap drawableToBitmap(Drawable drawable, boolean forceCreate) { + return drawableToBitmap(drawable, forceCreate, 0); + } + + public static Bitmap drawableToBitmap(Drawable drawable, boolean forceCreate, int fallbackSize) { + if (!forceCreate && drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + + if (width <= 0 || height <= 0) { + if (fallbackSize > 0) { + width = height = fallbackSize; + } else { + return null; + } + } + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + public static boolean isBootCompleted() { + return "1".equals(getSystemProperty("sys.boot_completed", "1")); + } + + public static String getSystemProperty(String property, String defaultValue) { + try { + Class clazz = Class.forName("android.os.SystemProperties"); + Method getter = clazz.getDeclaredMethod("get", String.class); + String value = (String) getter.invoke(null, property); + if (!TextUtils.isEmpty(value)) { + return value; + } + } catch (Exception e) { + Log.d(TAG, "Unable to read system properties"); + } + return defaultValue; + } + + /** + * Ensures that a value is within given bounds. Specifically: + * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound, + * return upperBound; else return value unchanged. + */ + public static int boundToRange(int value, int lowerBound, int upperBound) { + return Math.max(lowerBound, Math.min(value, upperBound)); + } + + public static boolean isSystemApp(Context context, Intent intent) { + PackageManager pm = context.getPackageManager(); + ComponentName cn = intent.getComponent(); + String packageName = null; + if (cn == null) { + ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + if ((info != null) && (info.activityInfo != null)) { + packageName = info.activityInfo.packageName; + } + } else { + packageName = cn.getPackageName(); + } + if (packageName != null) { + try { + PackageInfo info = pm.getPackageInfo(packageName, 0); + return (info != null) && (info.applicationInfo != null) && + ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } else { + return false; + } + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/LauncherAppsCompat.kt b/common/src/main/java/foundation/e/blisslauncher/common/compat/LauncherAppsCompat.kt new file mode 100644 index 0000000000..e7a98f02b0 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/LauncherAppsCompat.kt @@ -0,0 +1,131 @@ +package foundation.e.blisslauncher.common.compat + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherActivityInfo +import android.graphics.Rect +import android.os.Bundle +import android.os.UserHandle + +/** + * Interface repository of [android.content.pm.LauncherApps] + */ +interface LauncherAppsCompat { + + /** + * Wrapper callback for [android.content.pm.LauncherApps.Callback] + */ + interface OnAppsChangedCallbackCompat { + fun onPackageRemoved( + packageName: String, + user: UserHandle + ) + + fun onPackageAdded( + packageName: String, + user: UserHandle + ) + + fun onPackageChanged( + packageName: String, + user: UserHandle + ) + + fun onPackagesAvailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) + + fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) + + fun onPackagesSuspended( + packageNames: Array, + user: UserHandle + ) + + fun onPackagesUnsuspended( + packageNames: Array, + user: UserHandle + ) + + fun onShortcutsChanged( + packageName: String, + shortcuts: List, + user: UserHandle + ) + } + + /** + * Returns a list of Activities for a given package and user handle. + * + * If packageName is null, then it returns all the activities + * installed on device for the given user. + */ + fun getActivityList( + packageName: String?, + user: UserHandle? + ): List + + /** + * @see android.content.pm.LauncherApps.resolveActivity + */ + fun resolveActivity( + intent: Intent?, + user: UserHandle? + ): LauncherActivityInfo? + + fun startActivityForProfile( + component: ComponentName?, + user: UserHandle?, + sourceBounds: Rect?, + opts: Bundle? + ) + + /** + * @see android.content.pm.LauncherApps.getApplicationInfo + */ + fun getApplicationInfo( + packageName: String, + flags: Int, + user: UserHandle + ): ApplicationInfo? + + fun showAppDetailsForProfile( + component: ComponentName?, + user: UserHandle?, + sourceBounds: Rect?, + opts: Bundle? + ) + + fun addOnAppsChangedCallback(listener: OnAppsChangedCallbackCompat) + + fun removeOnAppsChangedCallback(listener: OnAppsChangedCallbackCompat) + + fun isPackageEnabledForProfile( + packageName: String?, + user: UserHandle? + ): Boolean + + fun isActivityEnabledForProfile( + component: ComponentName?, + user: UserHandle? + ): Boolean + + //TODO + /*abstract fun getCustomShortcutActivityList( + packageUser: PackageUserKey? + ): List?*/ + + fun showAppDetailsForProfile( + component: ComponentName?, + user: UserHandle? + ) { + showAppDetailsForProfile(component, user, null, null) + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt new file mode 100644 index 0000000000..37849abfc0 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt @@ -0,0 +1,74 @@ +package foundation.e.blisslauncher.common.compat + +import android.annotation.TargetApi +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.os.Build +import android.os.UserHandle + +/** + * Wrapper class for [android.content.pm.ShortcutInfo] representing deep shortcuts into apps. + */ +@TargetApi(Build.VERSION_CODES.N_MR1) +class ShortcutInfoCompat(private val shortcutInfo: ShortcutInfo) { + + @TargetApi(Build.VERSION_CODES.N) + fun makeIntent(): Intent = Intent(Intent.ACTION_MAIN) + .addCategory(INTENT_CATEGORY) + .setComponent(getActivity()) + .setPackage(getPackage()) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + .putExtra(EXTRA_SHORTCUT_ID, getId()) + + fun getShortcutInfo(): ShortcutInfo = shortcutInfo + + fun getPackage(): String = shortcutInfo.`package` + + //@RequiresApi(Build.VERSION_CODES.N_MR1) + fun getBadgePackage(context: Context): String? { + val whitelistedPkg = "" + return if (whitelistedPkg == getPackage() && shortcutInfo.getExtras().containsKey( + EXTRA_BADGEPKG + ) + ) { + shortcutInfo.getExtras() + .getString(EXTRA_BADGEPKG) + } else getPackage() + } + + fun getId(): String = shortcutInfo.id + + fun getShortLabel(): CharSequence? = shortcutInfo.shortLabel + + fun getLongLabel(): CharSequence? = shortcutInfo.longLabel + + fun getLastChangedTimestamp(): Long = shortcutInfo.getLastChangedTimestamp() + + fun getActivity(): ComponentName? = shortcutInfo.getActivity() + + fun getUserHandle(): UserHandle? = shortcutInfo.getUserHandle() + + fun hasKeyFieldsOnly(): Boolean = shortcutInfo.hasKeyFieldsOnly() + + fun isPinned(): Boolean = shortcutInfo.isPinned() + + fun isDeclaredInManifest(): Boolean = shortcutInfo.isDeclaredInManifest() + + fun isEnabled(): Boolean = shortcutInfo.isEnabled() + + fun isDynamic(): Boolean = shortcutInfo.isDynamic() + + fun getRank(): Int = shortcutInfo.getRank() + + fun getDisabledMessage(): CharSequence? = shortcutInfo.getDisabledMessage() + + override fun toString(): String = shortcutInfo.toString() + + companion object { + private const val INTENT_CATEGORY = "com.android.launcher3.DEEP_SHORTCUT" + private const val EXTRA_BADGEPKG = "badge_package" + const val EXTRA_SHORTCUT_ID = "shortcut_id" + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt b/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt new file mode 100644 index 0000000000..137b115745 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.common.executors + +import java.util.concurrent.Executor + +data class AppExecutors(val io: Executor, val computation: Executor, val main: Executor) + diff --git a/common/src/main/java/foundation/e/blisslauncher/common/executors/MainThreadExecutor.kt b/common/src/main/java/foundation/e/blisslauncher/common/executors/MainThreadExecutor.kt new file mode 100644 index 0000000000..0aa7a57f90 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/executors/MainThreadExecutor.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.common.executors + +import android.os.Handler +import android.os.Looper +import java.util.concurrent.AbstractExecutorService +import java.util.concurrent.TimeUnit + +class MainThreadExecutor : AbstractExecutorService() { + + private val mHandler: Handler = Handler(Looper.getMainLooper()) + + override fun shutdown(): Unit = throw UnsupportedOperationException() + + override fun shutdownNow(): List = throw UnsupportedOperationException() + + override fun isShutdown(): Boolean = false + + override fun isTerminated(): Boolean = false + + @Throws(InterruptedException::class) + override fun awaitTermination( + timeout: Long, + unit: TimeUnit + ): Boolean = throw UnsupportedOperationException() + + override fun execute(runnable: Runnable) { + if (mHandler.looper == Looper.myLooper()) { + runnable.run() + } else { + mHandler.post(runnable) + } + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/extensions/CommonExtensions.kt b/common/src/main/java/foundation/e/blisslauncher/common/extensions/CommonExtensions.kt new file mode 100644 index 0000000000..56971cc9d3 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/extensions/CommonExtensions.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.common.extensions diff --git a/common/src/main/java/foundation/e/blisslauncher/common/inject/Annotations.kt b/common/src/main/java/foundation/e/blisslauncher/common/inject/Annotations.kt new file mode 100644 index 0000000000..c0a9ebad9f --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/inject/Annotations.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.common.inject + +import javax.inject.Scope + +@Scope +annotation class PerActivity diff --git a/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt b/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt new file mode 100644 index 0000000000..1f01029528 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt @@ -0,0 +1,39 @@ +package foundation.e.blisslauncher.common.util + +import android.util.LongSparseArray + +/** + * Extension of [LongSparseArray] with some utility methods. + */ +class LongArrayMap : LongSparseArray(), + Iterable { + fun containsKey(key: Long): Boolean { + return indexOfKey(key) >= 0 + } + + val isEmpty: Boolean + get() = size() <= 0 + + override fun clone(): LongArrayMap { + return super.clone() as LongArrayMap + } + + override fun iterator(): Iterator { + return ValueIterator() + } + + internal inner class ValueIterator : MutableIterator { + private var mNextIndex = 0 + override fun hasNext(): Boolean { + return mNextIndex < size() + } + + override fun next(): E { + return valueAt(mNextIndex++) + } + + override fun remove() { + throw UnsupportedOperationException() + } + } +} \ No newline at end of file diff --git a/common/src/main/res/values-sw340dp/dimens.xml b/common/src/main/res/values-sw340dp/dimens.xml new file mode 100644 index 0000000000..f7e3f58ed0 --- /dev/null +++ b/common/src/main/res/values-sw340dp/dimens.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/common/src/main/res/values-sw600dp/config.xml b/common/src/main/res/values-sw600dp/config.xml new file mode 100644 index 0000000000..eb9af97938 --- /dev/null +++ b/common/src/main/res/values-sw600dp/config.xml @@ -0,0 +1,4 @@ + + true + true + diff --git a/common/src/main/res/values-sw600dp/dimens.xml b/common/src/main/res/values-sw600dp/dimens.xml new file mode 100644 index 0000000000..15b6193b36 --- /dev/null +++ b/common/src/main/res/values-sw600dp/dimens.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/common/src/main/res/values-sw720dp/config.xml b/common/src/main/res/values-sw720dp/config.xml new file mode 100644 index 0000000000..c536dbe88b --- /dev/null +++ b/common/src/main/res/values-sw720dp/config.xml @@ -0,0 +1,14 @@ + + true + true + + + + + true + diff --git a/common/src/main/res/values/attrs.xml b/common/src/main/res/values/attrs.xml new file mode 100644 index 0000000000..efc02f494b --- /dev/null +++ b/common/src/main/res/values/attrs.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/values/config.xml b/common/src/main/res/values/config.xml new file mode 100644 index 0000000000..1aed339bc6 --- /dev/null +++ b/common/src/main/res/values/config.xml @@ -0,0 +1,10 @@ + + + false + false + false + false + + + true + \ No newline at end of file diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml new file mode 100644 index 0000000000..9e44af55ba --- /dev/null +++ b/common/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + common + diff --git a/common/src/main/res/xml/default_workspace_3x3.xml b/common/src/main/res/xml/default_workspace_3x3.xml new file mode 100644 index 0000000000..2aee3d82df --- /dev/null +++ b/common/src/main/res/xml/default_workspace_3x3.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/default_workspace_4x4.xml b/common/src/main/res/xml/default_workspace_4x4.xml new file mode 100644 index 0000000000..979a1b4c84 --- /dev/null +++ b/common/src/main/res/xml/default_workspace_4x4.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/default_workspace_5x5.xml b/common/src/main/res/xml/default_workspace_5x5.xml new file mode 100644 index 0000000000..f9cc0e789b --- /dev/null +++ b/common/src/main/res/xml/default_workspace_5x5.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/default_workspace_5x6.xml b/common/src/main/res/xml/default_workspace_5x6.xml new file mode 100644 index 0000000000..8493c265e2 --- /dev/null +++ b/common/src/main/res/xml/default_workspace_5x6.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/device_profiles.xml b/common/src/main/res/xml/device_profiles.xml new file mode 100644 index 0000000000..6d250a27de --- /dev/null +++ b/common/src/main/res/xml/device_profiles.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/xml/dw_phone_hotseat.xml b/common/src/main/res/xml/dw_phone_hotseat.xml new file mode 100644 index 0000000000..031d0d7a11 --- /dev/null +++ b/common/src/main/res/xml/dw_phone_hotseat.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/dw_tablet_hotseat.xml b/common/src/main/res/xml/dw_tablet_hotseat.xml new file mode 100644 index 0000000000..671ccba3c5 --- /dev/null +++ b/common/src/main/res/xml/dw_tablet_hotseat.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/test/java/com/amitkma/common/ExampleUnitTest.kt b/common/src/test/java/com/amitkma/common/ExampleUnitTest.kt new file mode 100644 index 0000000000..2be77c0772 --- /dev/null +++ b/common/src/test/java/com/amitkma/common/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.amitkma.common + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/data-bridge/.gitignore b/data-bridge/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data-bridge/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data-bridge/build.gradle b/data-bridge/build.gradle new file mode 100644 index 0000000000..d475358d6b --- /dev/null +++ b/data-bridge/build.gradle @@ -0,0 +1,5 @@ +apply from: '../common.gradle' + +dependencies { + implementation project(path: ':data') +} diff --git a/data-bridge/src/androidTest/java/foundation/e/blisslauncher/databridge/ExampleInstrumentedTest.kt b/data-bridge/src/androidTest/java/foundation/e/blisslauncher/databridge/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..a019b8502c --- /dev/null +++ b/data-bridge/src/androidTest/java/foundation/e/blisslauncher/databridge/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package foundation.e.blisslauncher.databridge + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("foundation.e.blisslauncher.databridge.test", appContext.packageName) + } +} diff --git a/data-bridge/src/main/AndroidManifest.xml b/data-bridge/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5b6809ed36 --- /dev/null +++ b/data-bridge/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/data-bridge/src/main/java/foundation/e/blisslauncher/databridge/DataBridgeInitializer.kt b/data-bridge/src/main/java/foundation/e/blisslauncher/databridge/DataBridgeInitializer.kt new file mode 100644 index 0000000000..820d15ca40 --- /dev/null +++ b/data-bridge/src/main/java/foundation/e/blisslauncher/databridge/DataBridgeInitializer.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.databridge + +import android.content.Context +import foundation.e.blisslauncher.data.DataLayerInitializer + +object DataBridgeInitializer { + private val dataInitializer: DataLayerInitializer by lazy { DataLayerInitializer() } + + fun initialize(appContext: Context) { + dataInitializer.initialize(appContext) + } +} \ No newline at end of file diff --git a/data-bridge/src/test/java/foundation/e/blisslauncher/databridge/ExampleUnitTest.kt b/data-bridge/src/test/java/foundation/e/blisslauncher/databridge/ExampleUnitTest.kt new file mode 100644 index 0000000000..c40869b2a3 --- /dev/null +++ b/data-bridge/src/test/java/foundation/e/blisslauncher/databridge/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package foundation.e.blisslauncher.databridge + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/data/build.gradle b/data/build.gradle index 2e3d8bef1a..b6c74afcdd 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -29,19 +29,34 @@ android { } } } + + lintOptions { + abortOnError false + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(path: ':domain') + implementation project(path: ':common') + implementation Libs.Kotlin.stdlib implementation Libs.AndroidX.appcompat implementation Libs.AndroidX.coreKtx + implementation Libs.RxJava.rxKotlin + implementation Libs.AndroidX.Room.runtime + implementation Libs.AndroidX.Room.ktx kapt Libs.AndroidX.Room.compiler + implementation Libs.Dagger.dagger kapt Libs.Dagger.compiler + // Timber + implementation Libs.timber + testImplementation Libs.junit testImplementation Libs.robolectric testImplementation Libs.mockK diff --git a/data/src/main/java/foundation/e/blisslauncher/data/DataLayerInitializer.kt b/data/src/main/java/foundation/e/blisslauncher/data/DataLayerInitializer.kt new file mode 100644 index 0000000000..0047c0299a --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/DataLayerInitializer.kt @@ -0,0 +1,18 @@ +package foundation.e.blisslauncher.data + +import android.content.Context +import foundation.e.blisslauncher.data.inject.DaggerDataComponent +import foundation.e.blisslauncher.data.inject.DataComponent +import foundation.e.blisslauncher.domain.inject.DomainComponent + +class DataLayerInitializer { + fun initialize(appContext: Context): DataComponent { + return initializeDataComponent(appContext) + } + + private fun initializeDataComponent(appContext: Context): DataComponent { + val dataComponent = DaggerDataComponent.factory().create(appContext) + DomainComponent.INSTANCE = dataComponent + return dataComponent + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/DeviceProfile.kt b/data/src/main/java/foundation/e/blisslauncher/data/DeviceProfile.kt new file mode 100644 index 0000000000..538e754eef --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/DeviceProfile.kt @@ -0,0 +1,616 @@ +/* + * 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 foundation.e.blisslauncher.data + +import android.appwidget.AppWidgetHostView +import android.content.ComponentName +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Point +import android.graphics.PointF +import android.graphics.Rect +import android.util.DisplayMetrics +import android.view.Surface +import android.view.WindowManager +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.data.badge.BadgeRenderer +import foundation.e.blisslauncher.domain.entity.LauncherConstants + +class DeviceProfile( + context: Context, + inv: InvariantDeviceProfile, + minSize: Point, + maxSize: Point, + width: Int, + height: Int, + isLandscape: Boolean, + isMultiWindowMode: Boolean +) { + val inv: InvariantDeviceProfile + // Device properties + val isTablet: Boolean + val isLargeTablet: Boolean + val isPhone: Boolean + val transposeLayoutWithOrientation: Boolean + // Device properties in current orientation + val isLandscape: Boolean + val isMultiWindowMode: Boolean + val widthPx: Int + val heightPx: Int + var availableWidthPx = 0 + var availableHeightPx = 0 + // Workspace + val desiredWorkspaceLeftRightMarginPx: Int + val cellLayoutPaddingLeftRightPx: Int + val cellLayoutBottomPaddingPx: Int + val edgeMarginPx: Int + val defaultWidgetPadding: Rect + val defaultPageSpacingPx: Int + private val topWorkspacePadding: Int + var workspaceSpringLoadShrinkFactor = 0f + val workspaceSpringLoadedBottomSpace: Int + // Drag handle + val verticalDragHandleSizePx: Int + // Workspace icons + var iconSizePx = 0 + var iconTextSizePx = 0 + var iconDrawablePaddingPx = 0 + var iconDrawablePaddingOriginalPx: Int + var cellWidthPx = 0 + var cellHeightPx = 0 + var workspaceCellPaddingXPx: Int + // Folder + var folderIconSizePx = 0 + var folderIconOffsetYPx = 0 + // Folder cell + var folderCellWidthPx = 0 + var folderCellHeightPx = 0 + // Folder child + var folderChildIconSizePx = 0 + var folderChildTextSizePx = 0 + var folderChildDrawablePaddingPx = 0 + // Hotseat + var hotseatCellHeightPx = 0 + // In portrait: size = height, in landscape: size = width + var hotseatBarSizePx: Int + val hotseatBarTopPaddingPx: Int + val hotseatBarBottomPaddingPx: Int + val hotseatBarSidePaddingPx: Int + // All apps + var allAppsCellHeightPx = 0 + var allAppsIconSizePx = 0 + var allAppsIconDrawablePaddingPx = 0 + var allAppsIconTextSizePx = 0f + // Widgets + val appWidgetScale = PointF(1.0f, 1.0f) + // Drop Target + var dropTargetBarSizePx: Int + // Insets + val insets = Rect() + val workspacePadding = Rect() + private val mHotseatPadding = Rect() + private var mIsSeascape = false + // Icon badges + var mBadgeRenderer: BadgeRenderer + + fun copy(context: Context): DeviceProfile { + val size = + Point(availableWidthPx, availableHeightPx) + return DeviceProfile( + context, inv, size, size, widthPx, heightPx, isLandscape, + isMultiWindowMode + ) + } + + fun getMultiWindowProfile( + context: Context, + mwSize: Point + ): DeviceProfile { + // We take the minimum sizes of this profile and it's multi-window variant to ensure that + // the system decor is always excluded. + mwSize[Math.min(availableWidthPx, mwSize.x)] = Math.min(availableHeightPx, mwSize.y) + // In multi-window mode, we can have widthPx = availableWidthPx + // and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles' + // widthPx and heightPx values where it's needed. + val profile = + DeviceProfile( + context, inv, mwSize, mwSize, mwSize.x, mwSize.y, + isLandscape, true + ) + // If there isn't enough vertical cell padding with the labels displayed, hide the labels. + val workspaceCellPaddingY = (profile.cellSize.y - profile.iconSizePx - + iconDrawablePaddingPx - profile.iconTextSizePx).toFloat() + if (workspaceCellPaddingY < profile.iconDrawablePaddingPx * 2) { + profile.adjustToHideWorkspaceLabels() + } + // We use these scales to measure and layout the widgets using their full invariant profile + // sizes and then draw them scaled and centered to fit in their multi-window mode cellspans. + val appWidgetScaleX = + profile.cellSize.x.toFloat() / cellSize.x + val appWidgetScaleY = + profile.cellSize.y.toFloat() / cellSize.y + profile.appWidgetScale[appWidgetScaleX] = appWidgetScaleY + profile.updateWorkspacePadding() + return profile + } + + /** + * Inverse of [.getMultiWindowProfile] + * @return device profile corresponding to the current orientation in non multi-window mode. + */ + val fullScreenProfile: DeviceProfile? + get() = if (isLandscape) inv.landscapeProfile else inv.portraitProfile + + /** + * Adjusts the profile so that the labels on the Workspace are hidden. + * It is important to call this method after the All Apps variables have been set. + */ + private fun adjustToHideWorkspaceLabels() { + iconTextSizePx = 0 + iconDrawablePaddingPx = 0 + cellHeightPx = iconSizePx + // In normal cases, All Apps cell height should equal the Workspace cell height. + // Since we are removing labels from the Workspace, we need to manually compute the + // All Apps cell height. + val topBottomPadding = + allAppsIconDrawablePaddingPx * if (isVerticalBarLayout) 2 else 1 + allAppsCellHeightPx = (allAppsIconSizePx + allAppsIconDrawablePaddingPx + + Utilities.calculateTextHeight( + allAppsIconTextSizePx + ) + + topBottomPadding * 2) + } + + private fun updateAvailableDimensions( + dm: DisplayMetrics, + res: Resources + ) { + updateIconSize(1f, res, dm) + // Check to see if the icons fit within the available height. If not, then scale down. + val usedHeight = (cellHeightPx * inv.numRows).toFloat() + val maxHeight = availableHeightPx - totalWorkspacePadding.y + if (usedHeight > maxHeight) { + val scale = maxHeight / usedHeight + updateIconSize(scale, res, dm) + } + updateAvailableFolderCellDimensions(dm, res) + } + + private fun updateIconSize( + scale: Float, + res: Resources, + dm: DisplayMetrics + ) { + // Workspace + val isVerticalLayout = isVerticalBarLayout + val invIconSizePx = + if (isVerticalLayout) inv.landscapeIconSize else inv.iconSize + iconSizePx = + (Utilities.pxFromDp( + invIconSizePx, + dm + ) * scale).toInt() + iconTextSizePx = (Utilities.pxFromSp( + inv.iconTextSize, + dm + ) * scale).toInt() + iconDrawablePaddingPx = (iconDrawablePaddingOriginalPx * scale).toInt() + cellHeightPx = (iconSizePx + iconDrawablePaddingPx + + Utilities.calculateTextHeight( + iconTextSizePx.toFloat() + )) + val cellYPadding = (cellSize.y - cellHeightPx) / 2 + if (iconDrawablePaddingPx > cellYPadding && !isVerticalLayout && + !isMultiWindowMode + ) { + // Ensures that the label is closer to its corresponding icon. This is not an issue + // with vertical bar layout or multi-window mode since the issue is handled separately + // with their calls to {@link #adjustToHideWorkspaceLabels}. + cellHeightPx -= iconDrawablePaddingPx - cellYPadding + iconDrawablePaddingPx = cellYPadding + } + cellWidthPx = iconSizePx + iconDrawablePaddingPx + // All apps + allAppsIconTextSizePx = iconTextSizePx.toFloat() + allAppsIconSizePx = iconSizePx + allAppsIconDrawablePaddingPx = iconDrawablePaddingPx + allAppsCellHeightPx = cellSize.y + if (isVerticalLayout) { // Always hide the Workspace text with vertical bar layout. + adjustToHideWorkspaceLabels() + } + // Hotseat + if (isVerticalLayout) { + hotseatBarSizePx = iconSizePx + } + hotseatCellHeightPx = iconSizePx + workspaceSpringLoadShrinkFactor = if (!isVerticalLayout) { + val expectedWorkspaceHeight = (availableHeightPx - hotseatBarSizePx - + verticalDragHandleSizePx - topWorkspacePadding) + val minRequiredHeight = + dropTargetBarSizePx + workspaceSpringLoadedBottomSpace.toFloat() + Math.min( + res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f, + 1 - minRequiredHeight / expectedWorkspaceHeight + ) + } else { + res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f + } + // Folder icon + folderIconSizePx = iconSizePx + folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2 + } + + private fun updateAvailableFolderCellDimensions( + dm: DisplayMetrics, + res: Resources + ) { + val folderBottomPanelSize = + (res.getDimensionPixelSize(R.dimen.folder_label_padding_top) + + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom) + + Utilities.calculateTextHeight( + res.getDimension( + R.dimen.folder_label_text_size + ) + )) + updateFolderCellSize(1f, dm, res) + // Don't let the folder get too close to the edges of the screen. + val folderMargin = edgeMarginPx + val totalWorkspacePadding = totalWorkspacePadding + // Check if the icons fit within the available height. + val usedHeight = + folderCellHeightPx * inv.numFolderRows + folderBottomPanelSize.toFloat() + val maxHeight = availableHeightPx - totalWorkspacePadding.y - folderMargin + val scaleY = maxHeight / usedHeight + // Check if the icons fit within the available width. + val usedWidth = folderCellWidthPx * inv.numFolderColumns.toFloat() + val maxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin + val scaleX = maxWidth / usedWidth + val scale = Math.min(scaleX, scaleY) + if (scale < 1f) { + updateFolderCellSize(scale, dm, res) + } + } + + private fun updateFolderCellSize( + scale: Float, + dm: DisplayMetrics, + res: Resources + ) { + folderChildIconSizePx = + (Utilities.pxFromDp( + inv.iconSize, + dm + ) * scale).toInt() + folderChildTextSizePx = + (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale).toInt() + val textHeight = + Utilities.calculateTextHeight( + folderChildTextSizePx.toFloat() + ) + val cellPaddingX = + (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding) * scale).toInt() + val cellPaddingY = + (res.getDimensionPixelSize(R.dimen.folder_cell_y_padding) * scale).toInt() + folderCellWidthPx = folderChildIconSizePx + 2 * cellPaddingX + folderCellHeightPx = folderChildIconSizePx + 2 * cellPaddingY + textHeight + folderChildDrawablePaddingPx = Math.max( + 0, + (folderCellHeightPx - folderChildIconSizePx - textHeight) / 3 + ) + } + + fun updateInsets(insets: Rect?) { + insets!!.set(insets) + updateWorkspacePadding() + } + + // Since we are only concerned with the overall padding, layout direction does +// not matter. + val cellSize: Point + get() { + val result = Point() + // Since we are only concerned with the overall padding, layout direction does +// not matter. + val padding = totalWorkspacePadding + result.x = + calculateCellWidth( + availableWidthPx - padding.x - + cellLayoutPaddingLeftRightPx * 2, inv.numColumns + ) + result.y = + calculateCellHeight( + availableHeightPx - padding.y - + cellLayoutBottomPaddingPx, inv.numRows + ) + return result + } + + val totalWorkspacePadding: Point + get() { + updateWorkspacePadding() + return Point( + workspacePadding.left + workspacePadding.right, + workspacePadding.top + workspacePadding.bottom + ) + } + + /** + * Updates [.workspacePadding] as a result of any internal value change to reflect the + * new workspace padding + */ + private fun updateWorkspacePadding() { + val padding = workspacePadding + if (isVerticalBarLayout) { + padding.top = 0 + padding.bottom = edgeMarginPx + padding.left = hotseatBarSidePaddingPx + padding.right = hotseatBarSidePaddingPx + if (isSeascape) { + padding.left += hotseatBarSizePx + padding.right += verticalDragHandleSizePx + } else { + padding.left += verticalDragHandleSizePx + padding.right += hotseatBarSizePx + } + } else { + val paddingBottom = hotseatBarSizePx + verticalDragHandleSizePx + if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing +// between all icons +// The amount of screen space available for left/right padding. + var availablePaddingX = Math.max( + 0, widthPx - (inv.numColumns * cellWidthPx + + (inv.numColumns - 1) * cellWidthPx) + ) + availablePaddingX = Math.min( + availablePaddingX.toFloat(), + widthPx * MAX_HORIZONTAL_PADDING_PERCENT + ).toInt() + val availablePaddingY = Math.max( + 0, heightPx - topWorkspacePadding - paddingBottom - + 2 * inv.numRows * cellHeightPx - hotseatBarTopPaddingPx - + hotseatBarBottomPaddingPx + ) + padding[availablePaddingX / 2, topWorkspacePadding + availablePaddingY / 2, availablePaddingX / 2] = + paddingBottom + availablePaddingY / 2 + } else { // Pad the top and bottom of the workspace with search/hotseat bar sizes + padding[desiredWorkspaceLeftRightMarginPx, topWorkspacePadding, desiredWorkspaceLeftRightMarginPx] = + paddingBottom + } + } + } + + // We want the edges of the hotseat to line up with the edges of the workspace, but the +// icons in the hotseat are a different size, and so don't line up perfectly. To account +// for this, we pad the left and right of the hotseat with half of the difference of a +// workspace cell vs a hotseat cell. + val hotseatLayoutPadding: Rect + get() { + if (isVerticalBarLayout) { + if (isSeascape) { + mHotseatPadding[insets.left, insets.top, hotseatBarSidePaddingPx] = + insets.bottom + } else { + mHotseatPadding[hotseatBarSidePaddingPx, insets.top, insets.right] = + insets.bottom + } + } else { // We want the edges of the hotseat to line up with the edges of the workspace, but the +// icons in the hotseat are a different size, and so don't line up perfectly. To account +// for this, we pad the left and right of the hotseat with half of the difference of a +// workspace cell vs a hotseat cell. + val workspaceCellWidth = widthPx.toFloat() / inv.numColumns + val hotseatCellWidth = widthPx.toFloat() / inv.numHotseatIcons + val hotseatAdjustment = + Math.round((workspaceCellWidth - hotseatCellWidth) / 2) + mHotseatPadding[hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx, hotseatBarTopPaddingPx, hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx] = + hotseatBarBottomPaddingPx + insets.bottom + cellLayoutBottomPaddingPx + } + return mHotseatPadding + } // Folders should only appear below the drop target bar and above the hotseat// Folders should only appear right of the drop target bar and left of the hotseat + + /** + * @return the bounds for which the open folders should be contained within + */ + val absoluteOpenFolderBounds: Rect + get() = if (isVerticalBarLayout) { // Folders should only appear right of the drop target bar and left of the hotseat + Rect( + insets.left + dropTargetBarSizePx + edgeMarginPx, + insets.top, + insets.left + availableWidthPx - hotseatBarSizePx - edgeMarginPx, + insets.top + availableHeightPx + ) + } else { // Folders should only appear below the drop target bar and above the hotseat + Rect( + insets.left + edgeMarginPx, + insets.top + dropTargetBarSizePx + edgeMarginPx, + insets.left + availableWidthPx - edgeMarginPx, + insets.top + availableHeightPx - hotseatBarSizePx - + verticalDragHandleSizePx - edgeMarginPx + ) + } + + /** + * When `true`, the device is in landscape mode and the hotseat is on the right column. + * When `false`, either device is in portrait mode or the device is in landscape mode and + * the hotseat is on the bottom row. + */ + val isVerticalBarLayout: Boolean + get() = isLandscape && transposeLayoutWithOrientation + + /** + * Updates orientation information and returns true if it has changed from the previous value. + */ + fun updateIsSeascape(wm: WindowManager): Boolean { + if (isVerticalBarLayout) { + val isSeascape = + wm.defaultDisplay.rotation == Surface.ROTATION_270 + if (mIsSeascape != isSeascape) { + mIsSeascape = isSeascape + return true + } + } + return false + } + + val isSeascape: Boolean + get() = isVerticalBarLayout && mIsSeascape + + fun shouldFadeAdjacentWorkspaceScreens(): Boolean { + return isVerticalBarLayout || isLargeTablet + } + + fun getCellHeight(containerType: Int): Int { + return when (containerType) { + LauncherConstants.ContainerType.CONTAINER_DESKTOP -> cellHeightPx + LauncherConstants.ContainerType.CONTAINER_HOTSEAT -> hotseatCellHeightPx + else -> 0 + } + } + + /** + * Callback when a component changes the DeviceProfile associated with it, as a result of + * configuration change + */ + interface OnDeviceProfileChangeListener { + /** + * Called when the device profile is reassigned. Note that for layout and measurements, it + * is sufficient to listen for inset changes. Use this callback when you need to perform + * a one time operation. + */ + fun onDeviceProfileChanged(dp: DeviceProfile?) + } + + companion object { + /** + * The maximum amount of left/right workspace padding as a percentage of the screen width. + * To be clear, this means that up to 7% of the screen width can be used as left padding, and + * 7% of the screen width can be used as right padding. + */ + private const val MAX_HORIZONTAL_PADDING_PERCENT = 0.14f + private const val TALL_DEVICE_ASPECT_RATIO_THRESHOLD = 2.0f + fun calculateCellWidth(width: Int, countX: Int): Int { + return width / countX + } + + fun calculateCellHeight(height: Int, countY: Int): Int { + return height / countY + } + + private fun getContext( + c: Context, + orientation: Int + ): Context { + val context = + Configuration(c.resources.configuration) + context.orientation = orientation + return c.createConfigurationContext(context) + } + } + + init { + var context = context + this.inv = inv + this.isLandscape = isLandscape + this.isMultiWindowMode = isMultiWindowMode + var res = context.resources + val dm = res.displayMetrics + // Constants from resources + isTablet = res.getBoolean( + R.bool.is_tablet + ) + isLargeTablet = res.getBoolean(R.bool.is_large_tablet) + isPhone = !isTablet && !isLargeTablet + // Some more constants + transposeLayoutWithOrientation = + res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation) + context = + getContext( + context, + if (isVerticalBarLayout) Configuration.ORIENTATION_LANDSCAPE else Configuration.ORIENTATION_PORTRAIT + ) + res = context.resources + val cn = ComponentName( + context.packageName, + this.javaClass.name + ) + defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null) + edgeMarginPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin) + desiredWorkspaceLeftRightMarginPx = if (isVerticalBarLayout) 0 else edgeMarginPx + cellLayoutPaddingLeftRightPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding) + cellLayoutBottomPaddingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_bottom_padding) + verticalDragHandleSizePx = res.getDimensionPixelSize( + R.dimen.vertical_drag_handle_size + ) + defaultPageSpacingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing) + topWorkspacePadding = + res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_top_padding) + iconDrawablePaddingOriginalPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding) + dropTargetBarSizePx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_drop_target_size) + workspaceSpringLoadedBottomSpace = + res.getDimensionPixelSize(R.dimen.dynamic_grid_min_spring_loaded_space) + workspaceCellPaddingXPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_x) + hotseatBarTopPaddingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_top_padding) + hotseatBarBottomPaddingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding) + hotseatBarSidePaddingPx = + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding) + hotseatBarSizePx = + if (isVerticalBarLayout) Utilities.pxFromDp( + inv.iconSize, + dm + ) else res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_size) + +hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx + // Determine sizes. + widthPx = width + heightPx = height + if (isLandscape) { + availableWidthPx = maxSize.x + availableHeightPx = minSize.y + } else { + availableWidthPx = minSize.x + availableHeightPx = maxSize.y + } + // Calculate all of the remaining variables. + updateAvailableDimensions(dm, res) + // Now that we have all of the variables calculated, we can tune certain sizes. + val aspectRatio = + Math.max(widthPx, heightPx).toFloat() / Math.min( + widthPx, + heightPx + ) + val isTallDevice = aspectRatio.compareTo(TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0 + if (!isVerticalBarLayout && isPhone && isTallDevice) { // We increase the hotseat size when there is extra space. +// ie. For a display with a large aspect ratio, we can keep the icons on the workspace +// in portrait mode closer together by adding more height to the hotseat. +// Note: This calculation was created after noticing a pattern in the design spec. + val extraSpace = cellSize.y - iconSizePx - iconDrawablePaddingPx + hotseatBarSizePx += extraSpace - verticalDragHandleSizePx + // Recalculate the available dimensions using the new hotseat size. + updateAvailableDimensions(dm, res) + } + updateWorkspacePadding() + // This is done last, after iconSizePx is calculated above. + mBadgeRenderer = BadgeRenderer(iconSizePx) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/InvariantDeviceProfile.kt b/data/src/main/java/foundation/e/blisslauncher/data/InvariantDeviceProfile.kt new file mode 100644 index 0000000000..2c7398831a --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/InvariantDeviceProfile.kt @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.data + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Point +import android.util.DisplayMetrics +import android.util.Xml +import android.view.WindowManager +import com.amitkma.common.R +import foundation.e.blisslauncher.common.Utilities +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException +import java.util.ArrayList +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.hypot + +@Singleton +open class InvariantDeviceProfile { + // Profile-defining invariant properties + var name: String? = null + var minWidthDps = 0f + var minHeightDps = 0f + /** + * Number of icons per row and column in the workspace. + */ + var numRows = 0 + var numColumns = 0 + /** + * Number of icons per row and column in the folder. + */ + var numFolderRows = 0 + var numFolderColumns = 0 + + var iconSize = 0f + var landscapeIconSize = 0f + var iconBitmapSize = 0 + var fillResIconDpi = 0 + var iconTextSize = 0f + /** + * Number of icons inside the hotseat area. + */ + var numHotseatIcons = 0 + var defaultLayoutId = 0 + var demoModeLayoutId = 0 + var landscapeProfile: DeviceProfile? = null + var portraitProfile: DeviceProfile? = null + var defaultWallpaperSize: Point? = null + + constructor() + private constructor(p: InvariantDeviceProfile) : this( + p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, + p.numFolderRows, p.numFolderColumns, + p.iconSize, p.landscapeIconSize, p.iconTextSize, p.numHotseatIcons, + p.defaultLayoutId, p.demoModeLayoutId + ) + + private constructor( + n: String?, + w: Float, + h: Float, + r: Int, + c: Int, + fr: Int, + fc: Int, + `is`: Float, + lis: Float, + its: Float, + hs: Int, + dlId: Int, + dmlId: Int + ) { + name = n + minWidthDps = w + minHeightDps = h + numRows = r + numColumns = c + numFolderRows = fr + numFolderColumns = fc + iconSize = `is` + landscapeIconSize = lis + iconTextSize = its + numHotseatIcons = hs + defaultLayoutId = dlId + demoModeLayoutId = dmlId + } + + @Inject + constructor(context: Context) { + val wm = + context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val display = wm.defaultDisplay + val dm = DisplayMetrics() + display.getMetrics(dm) + val smallestSize = Point() + val largestSize = Point() + display.getCurrentSizeRange(smallestSize, largestSize) + // This guarantees that width < height + minWidthDps = Utilities.dpiFromPx( + Math.min( + smallestSize.x, + smallestSize.y + ), dm + ) + minHeightDps = Utilities.dpiFromPx( + Math.min( + largestSize.x, + largestSize.y + ), dm + ) + val closestProfiles = + findClosestDeviceProfiles( + minWidthDps, minHeightDps, getPredefinedDeviceProfiles(context) + ) + val interpolatedDeviceProfileOut = + invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles) + val closestProfile = closestProfiles[0] + numRows = closestProfile.numRows + numColumns = closestProfile.numColumns + numHotseatIcons = closestProfile.numHotseatIcons + defaultLayoutId = closestProfile.defaultLayoutId + demoModeLayoutId = closestProfile.demoModeLayoutId + numFolderRows = closestProfile.numFolderRows + numFolderColumns = closestProfile.numFolderColumns + iconSize = interpolatedDeviceProfileOut.iconSize + landscapeIconSize = interpolatedDeviceProfileOut.landscapeIconSize + iconBitmapSize = + Utilities.pxFromDp(iconSize, dm) + iconTextSize = interpolatedDeviceProfileOut.iconTextSize + fillResIconDpi = getLauncherIconDensity(iconBitmapSize) + // If the partner customization apk contains any grid overrides, apply them + // Supported overrides: numRows, numColumns, iconSize + //applyPartnerDeviceProfileOverrides(context, dm); + val realSize = Point() + display.getRealSize(realSize) + // The real size never changes. smallSide and largeSide will remain the + // same in any orientation. + val smallSide = Math.min(realSize.x, realSize.y) + val largeSide = Math.max(realSize.x, realSize.y) + landscapeProfile = DeviceProfile( + context, this, smallestSize, largestSize, + largeSide, smallSide, true /* isLandscape */, false /* isMultiWindowMode */ + ) + portraitProfile = DeviceProfile( + context, this, smallestSize, largestSize, + smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */ + ) + // We need to ensure that there is enough extra space in the wallpaper + // for the intended parallax effects + defaultWallpaperSize = if (context.resources.configuration.smallestScreenWidthDp >= 720) { + Point( + (largeSide * wallpaperTravelToScreenWidthRatio( + largeSide, + smallSide + )).toInt(), + largeSide + ) + } else { + Point(Math.max(smallSide * 2, largeSide), largeSide) + } + } + + private fun getPredefinedDeviceProfiles(context: Context): ArrayList { + val profiles = + ArrayList() + try { + context.resources.getXml(R.xml.device_profiles).use { parser -> + val depth = parser.depth + var type: Int + while ((parser.next().also { + type = it + } != XmlPullParser.END_TAG || + parser.depth > depth) && type != XmlPullParser.END_DOCUMENT + ) { + if (type == XmlPullParser.START_TAG && "profile" == parser.name) { + val a = context.obtainStyledAttributes( + Xml.asAttributeSet(parser), + R.styleable.InvariantDeviceProfile + ) + val numRows = a.getInt( + R.styleable.InvariantDeviceProfile_numRows, + 0 + ) + val numColumns = a.getInt( + R.styleable.InvariantDeviceProfile_numColumns, + 0 + ) + val iconSize = a.getFloat( + R.styleable.InvariantDeviceProfile_iconSize, + 0f + ) + profiles.add( + InvariantDeviceProfile( + a.getString(R.styleable.InvariantDeviceProfile_name), + a.getFloat( + R.styleable.InvariantDeviceProfile_minWidthDps, + 0f + ), + a.getFloat( + R.styleable.InvariantDeviceProfile_minHeightDps, + 0f + ), + numRows, + numColumns, + a.getInt( + R.styleable.InvariantDeviceProfile_numFolderRows, + numRows + ), + a.getInt( + R.styleable.InvariantDeviceProfile_numFolderColumns, + numColumns + ), + iconSize, + a.getFloat( + R.styleable.InvariantDeviceProfile_landscapeIconSize, + iconSize + ), + a.getFloat( + R.styleable.InvariantDeviceProfile_iconTextSize, + 0f + ), + a.getInt( + R.styleable.InvariantDeviceProfile_numHotseatIcons, + numColumns + ), + a.getResourceId( + R.styleable.InvariantDeviceProfile_defaultLayoutId, + 0 + ), + a.getResourceId( + R.styleable.InvariantDeviceProfile_demoModeLayoutId, + 0 + ) + ) + ) + a.recycle() + } + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: XmlPullParserException) { + throw RuntimeException(e) + } + return profiles + } + + private fun getLauncherIconDensity(requiredSize: Int): Int { // Densities typically defined by an app. + val densityBuckets = intArrayOf( + DisplayMetrics.DENSITY_LOW, + DisplayMetrics.DENSITY_MEDIUM, + DisplayMetrics.DENSITY_TV, + DisplayMetrics.DENSITY_HIGH, + DisplayMetrics.DENSITY_XHIGH, + DisplayMetrics.DENSITY_XXHIGH, + DisplayMetrics.DENSITY_XXXHIGH + ) + var density = DisplayMetrics.DENSITY_XXXHIGH + for (i in densityBuckets.indices.reversed()) { + val expectedSize = + (ICON_SIZE_DEFINED_IN_APP_DP * densityBuckets[i] / + DisplayMetrics.DENSITY_DEFAULT) + if (expectedSize >= requiredSize) { + density = densityBuckets[i] + } + } + return density + } + + /** + * Apply any Partner customization grid overrides. + * + * Currently we support: all apps row / column count. + */ + /*private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { + Partner p = Partner.get(context.getPackageManager()); + if (p != null) { + p.applyInvariantDeviceProfileOverrides(this, dm); + } + }*/ + + fun dist(x0: Float, y0: Float, x1: Float, y1: Float): Float { + return hypot(x1 - x0.toDouble(), y1 - y0.toDouble()).toFloat() + } + + /** + * Returns the closest device profiles ordered by closeness to the specified width and height + */ + fun findClosestDeviceProfiles( + width: Float, + height: Float, + points: ArrayList + ): ArrayList { // Sort the profiles by their closeness to the dimensions + points.sortWith(Comparator { a, b -> + dist(width, height, a.minWidthDps, a.minHeightDps).compareTo( + dist( + width, + height, + b.minWidthDps, + b.minHeightDps + ) + ) + }) + return points + } + + // Package private visibility for testing. + fun invDistWeightedInterpolate( + width: Float, + height: Float, + points: ArrayList + ): InvariantDeviceProfile { + var weights = 0f + var p = points[0] + if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0f) { + return p + } + val out = InvariantDeviceProfile() + var i = 0 + while (i < points.size && i < KNEARESTNEIGHBOR) { + p = InvariantDeviceProfile(points[i]) + val w = weight( + width, + height, + p.minWidthDps, + p.minHeightDps, + WEIGHT_POWER + ) + weights += w + out += p * w + ++i + } + return out * (1.0f / weights) + } + + operator fun plusAssign(p: InvariantDeviceProfile) { + iconSize += p.iconSize + landscapeIconSize += p.landscapeIconSize + iconTextSize += p.iconTextSize + } + + operator fun times(w: Float): InvariantDeviceProfile { + iconSize *= w + landscapeIconSize *= w + iconTextSize *= w + return this + } + + val allAppsButtonRank: Int + get() = numHotseatIcons / 2 + + fun isAllAppsButtonRank(rank: Int): Boolean { + return rank == allAppsButtonRank + } + + fun getDeviceProfile(context: Context): DeviceProfile? { + return if (context.resources.configuration.orientation + == Configuration.ORIENTATION_LANDSCAPE + ) landscapeProfile else portraitProfile + } + + private fun weight( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + pow: Float + ): Float { + val d = dist(x0, y0, x1, y1) + return if (d.compareTo(0f) == 0) { + Float.POSITIVE_INFINITY + } else (WEIGHT_EFFICIENT / Math.pow( + d.toDouble(), + pow.toDouble() + )).toFloat() + } + + companion object { + // This is a static that we use for the default icon size on a 4/5-inch phone + private const val DEFAULT_ICON_SIZE_DP = 60f + private const val ICON_SIZE_DEFINED_IN_APP_DP = 48f + // Constants that affects the interpolation curve between statically defined device profile + // buckets. + private const val KNEARESTNEIGHBOR = 3f + private const val WEIGHT_POWER = 5f + // used to offset float not being able to express extremely small weights in extreme cases. + private const val WEIGHT_EFFICIENT = 100000f + + /** + * As a ratio of screen height, the total distance we want the parallax effect to span + * horizontally + */ + private fun wallpaperTravelToScreenWidthRatio(width: Int, height: Int): Float { + val aspectRatio = width / height.toFloat() + // 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: + val ASPECT_RATIO_LANDSCAPE = 16 / 10f + val ASPECT_RATIO_PORTRAIT = 10 / 16f + val WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f + val 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: + val x = + (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / + (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT) + val y = + WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT + return x * aspectRatio + y + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherAppsChangedCallbackCompat.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherAppsChangedCallbackCompat.kt new file mode 100644 index 0000000000..21cd8b2fba --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherAppsChangedCallbackCompat.kt @@ -0,0 +1,83 @@ +package foundation.e.blisslauncher.data + +import android.os.UserHandle +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.domain.interactor.AddPackages +import foundation.e.blisslauncher.domain.interactor.MakePackageUnavailable +import foundation.e.blisslauncher.domain.interactor.RemovePackages +import foundation.e.blisslauncher.domain.interactor.SuspendPackages +import foundation.e.blisslauncher.domain.interactor.UnsuspendPackages +import foundation.e.blisslauncher.domain.interactor.UpdatePackages +import javax.inject.Inject + +class LauncherAppsChangedCallbackCompat +@Inject constructor( + private val updatePackages: UpdatePackages, + private val addPackages: AddPackages, + private val removePackages: RemovePackages, + private val makePackageUnavailable: MakePackageUnavailable, + private val suspendPackages: SuspendPackages, + private val unsuspendPackages: UnsuspendPackages +) : + LauncherAppsCompat.OnAppsChangedCallbackCompat { + override fun onPackageRemoved(packageName: String, user: UserHandle) { + removePackages( + RemovePackages.Params(user, packageName) + ) + } + + override fun onPackageAdded(packageName: String, user: UserHandle) { + addPackages( + AddPackages.Params(user, packageName) + ) + } + + override fun onPackageChanged(packageName: String, user: UserHandle) { + updatePackages( + UpdatePackages.Params(user, packageName) + ) + } + + override fun onPackagesAvailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + updatePackages( + UpdatePackages.Params(user, *packageNames) + ) + } + + override fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + if (!replacing) { + makePackageUnavailable( + MakePackageUnavailable.Params(user, *packageNames) + ) + } + } + + override fun onPackagesSuspended(packageNames: Array, user: UserHandle) { + suspendPackages( + SuspendPackages.Params(user, *packageNames) + ) + } + + override fun onPackagesUnsuspended(packageNames: Array, user: UserHandle) { + unsuspendPackages( + UnsuspendPackages.Params(user, *packageNames) + ) + } + + override fun onShortcutsChanged( + packageName: String, + shortcuts: List, + user: UserHandle + ) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherProvider.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherProvider.kt new file mode 100644 index 0000000000..4678f85d67 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherProvider.kt @@ -0,0 +1,43 @@ +package foundation.e.blisslauncher.data + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri + +class LauncherProvider : ContentProvider() { + override fun insert(uri: Uri, values: ContentValues?): Uri? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onCreate(): Boolean { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getType(uri: Uri): String? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherStateManagerImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherStateManagerImpl.kt new file mode 100644 index 0000000000..c3da819487 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherStateManagerImpl.kt @@ -0,0 +1,59 @@ +package foundation.e.blisslauncher.data + +import android.content.Context +import android.os.Process +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.data.notification.NotificationListener +import foundation.e.blisslauncher.data.receiver.ConfigChangedReceiver +import foundation.e.blisslauncher.data.receiver.ProfileReceiver +import foundation.e.blisslauncher.domain.manager.LauncherStateManager +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LauncherStateManagerImpl @Inject constructor( + private val context: Context, + private val profileReceiver: ProfileReceiver, + private val userManagerRepository: UserManagerRepository, + private val configReceiver: ConfigChangedReceiver, + private val onAppsChangedCallbackCompat: LauncherAppsCompat.OnAppsChangedCallbackCompat, + private val appExecutors: AppExecutors, + private val launcherAppsCompat: LauncherAppsCompat +) : LauncherStateManager { + private lateinit var notificationBadgingObserver: SettingsObserver + + override fun init() { + Timber.d("Initialising Launcher components") + + launcherAppsCompat.addOnAppsChangedCallback(onAppsChangedCallbackCompat) + + profileReceiver.register() + userManagerRepository.enableAndResetCache() + configReceiver.register() + notificationBadgingObserver = object : SettingsObserver.Secure(context.contentResolver) { + override fun onSettingChanged(isNotificationBadgingEnabled: Boolean) { + if (isNotificationBadgingEnabled) { + NotificationListener.requestRebind(context) + } + } + } + notificationBadgingObserver.register(NotificationListener.NOTIFICATION_BADGING) + Timber.d("Initialisation completed") + } + + override fun terminate() { + launcherAppsCompat.removeOnAppsChangedCallback(onAppsChangedCallbackCompat) + profileReceiver.unregister() + configReceiver.unregister() + notificationBadgingObserver.unregister() + } + + fun changeThreadPriority(priority: Int) { + appExecutors.io.execute { + Process.setThreadPriority(priority) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/SettingsObserver.java b/data/src/main/java/foundation/e/blisslauncher/data/SettingsObserver.java new file mode 100644 index 0000000000..4f36ac0a90 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/SettingsObserver.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.data; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.os.Handler; +import android.provider.Settings; + +public interface SettingsObserver { + + /** + * Registers the content observer to call {@link #onSettingChanged(boolean)} when any of the + * passed settings change. The value passed to onSettingChanged() is based on the key setting. + */ + void register(String keySetting, String... dependentSettings); + void unregister(); + void onSettingChanged(boolean keySettingEnabled); + + + abstract class Secure extends ContentObserver implements SettingsObserver { + private ContentResolver mResolver; + private String mKeySetting; + + public Secure(ContentResolver resolver) { + super(new Handler()); + mResolver = resolver; + } + + @Override + public void register(String keySetting, String ... dependentSettings) { + mKeySetting = keySetting; + mResolver.registerContentObserver( + Settings.Secure.getUriFor(mKeySetting), false, this); + for (String setting : dependentSettings) { + mResolver.registerContentObserver( + Settings.Secure.getUriFor(setting), false, this); + } + onChange(true); + } + + @Override + public void unregister() { + mResolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + onSettingChanged(Settings.Secure.getInt(mResolver, mKeySetting, 1) == 1); + } + } + + abstract class System extends ContentObserver implements SettingsObserver { + private ContentResolver mResolver; + private String mKeySetting; + + public System(ContentResolver resolver) { + super(new Handler()); + mResolver = resolver; + } + + @Override + public void register(String keySetting, String ... dependentSettings) { + mKeySetting = keySetting; + mResolver.registerContentObserver( + Settings.System.getUriFor(mKeySetting), false, this); + for (String setting : dependentSettings) { + mResolver.registerContentObserver( + Settings.System.getUriFor(setting), false, this); + } + onChange(true); + } + + @Override + public void unregister() { + mResolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + onSettingChanged(Settings.System.getInt(mResolver, mKeySetting, 1) == 1); + } + } +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/badge/BadgeRenderer.java b/data/src/main/java/foundation/e/blisslauncher/data/badge/BadgeRenderer.java new file mode 100644 index 0000000000..f094ea978f --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/badge/BadgeRenderer.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.data.badge; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.Log; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +/** + * Contains parameters necessary to draw a badge for an icon (e.g. the size of the badge). + */ +public class BadgeRenderer { + + private static final String TAG = "BadgeRenderer"; + + // The badge sizes are defined as percentages of the app icon size. + private static final float SIZE_PERCENTAGE = 0.38f; + + // Extra scale down of the dot + private static final float DOT_SCALE = 0.6f; + + // Used to expand the width of the badge for each additional digit. + private static final float OFFSET_PERCENTAGE = 0.02f; + + private final float mDotCenterOffset; + private final int mOffset; + private final float mCircleRadius; + private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); + + //private final Bitmap mBackgroundWithShadow; + private final float mBitmapOffset; + + public BadgeRenderer(int iconSizePx) { + mDotCenterOffset = SIZE_PERCENTAGE * iconSizePx; + mOffset = (int) (OFFSET_PERCENTAGE * iconSizePx); + + int size = (int) (DOT_SCALE * mDotCenterOffset); + /*ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT); + mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size);*/ + mCircleRadius = size/2; + + mBitmapOffset = 0f; // Same as width. + } + + /** + * Draw a circle in the top right corner of the given bounds, and draw + * @param color The color (based on the icon) to use for the badge. + * @param iconBounds The bounds of the icon being badged. + * @param badgeScale The progress of the animation, from 0 to 1. + * @param spaceForOffset How much space is available to offset the badge up and to the right. + */ + public void draw( + Canvas canvas, int color, Rect iconBounds, float badgeScale, Point spaceForOffset) { + if (iconBounds == null || spaceForOffset == null) { + Log.e(TAG, "Invalid null argument(s) passed in call to draw."); + return; + } + canvas.save(); + // We draw the badge relative to its center. + float badgeCenterX = iconBounds.right - mDotCenterOffset / 2; + float badgeCenterY = iconBounds.top + mDotCenterOffset / 2; + + int offsetX = Math.min(mOffset, spaceForOffset.x); + int offsetY = Math.min(mOffset, spaceForOffset.y); + canvas.translate(badgeCenterX + offsetX, badgeCenterY - offsetY); + canvas.scale(badgeScale, badgeScale); + + /* mCirclePaint.setColor(Color.BLACK); + canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint);*/ + mCirclePaint.setColor(color); + canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint); + canvas.restore(); + } +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVL.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVL.kt new file mode 100644 index 0000000000..953feaa7b7 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVL.kt @@ -0,0 +1,206 @@ +package foundation.e.blisslauncher.data.compat + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo +import android.graphics.Rect +import android.os.Bundle +import android.os.Process +import android.os.UserHandle +import android.util.ArrayMap +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import java.util.ArrayList + +open class LauncherAppsCompatVL internal constructor(protected val context: Context) : + LauncherAppsCompat { + + protected val launcherApps: LauncherApps = + context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + private val callbacks = + ArrayMap() + + override fun getActivityList( + packageName: String?, + user: UserHandle? + ): List { + return launcherApps.getActivityList(packageName, user) + } + + override fun resolveActivity( + intent: Intent?, + user: UserHandle? + ): LauncherActivityInfo? { + return launcherApps.resolveActivity(intent, user) + } + + override fun startActivityForProfile( + component: ComponentName?, + user: UserHandle?, + sourceBounds: Rect?, + opts: Bundle? + ) { + launcherApps.startMainActivity(component, user, sourceBounds, opts) + } + + override fun getApplicationInfo( + packageName: String, + flags: Int, + user: UserHandle + ): ApplicationInfo? { + val isPrimaryUser = Process.myUserHandle() == user + if (!isPrimaryUser && flags == 0) { + // We are looking for an installed app on a secondary profile. Prior to O, the only + // entry point for work profiles is through the LauncherActivity. + val activityList = + launcherApps.getActivityList(packageName, user) + return if (activityList.size > 0) activityList[0].applicationInfo else null + } + return try { + val info = + context.packageManager.getApplicationInfo(packageName, flags) + // There is no way to check if the app is installed for managed profile. But for + // primary profile, we can still have this check. + if (isPrimaryUser && info.flags and ApplicationInfo.FLAG_INSTALLED == 0 || + !info.enabled + ) { + null + } else info + } catch (e: PackageManager.NameNotFoundException) { // Package not found + null + } + } + + override fun showAppDetailsForProfile( + component: ComponentName?, + user: UserHandle?, + sourceBounds: Rect?, + opts: Bundle? + ) { + launcherApps.startAppDetailsActivity(component, user, sourceBounds, opts) + } + + override fun addOnAppsChangedCallback(listener: LauncherAppsCompat.OnAppsChangedCallbackCompat) { + val wrappedCallback = WrappedCallback(listener) + synchronized(callbacks) { callbacks.put(listener, wrappedCallback) } + launcherApps.registerCallback(wrappedCallback) + } + + override fun removeOnAppsChangedCallback(listener: LauncherAppsCompat.OnAppsChangedCallbackCompat) { + var wrappedCallback: WrappedCallback? + synchronized(callbacks) { wrappedCallback = callbacks.remove(listener) } + if (wrappedCallback != null) { + launcherApps.unregisterCallback(wrappedCallback) + } + } + + override fun isPackageEnabledForProfile( + packageName: String?, + user: UserHandle? + ): Boolean { + return launcherApps.isPackageEnabled(packageName, user) + } + + override fun isActivityEnabledForProfile( + component: ComponentName?, + user: UserHandle? + ): Boolean { + return launcherApps.isActivityEnabled(component, user) + } + + private class WrappedCallback(private val mCallback: LauncherAppsCompat.OnAppsChangedCallbackCompat) : + LauncherApps.Callback() { + override fun onPackageRemoved( + packageName: String, + user: UserHandle + ) { + mCallback.onPackageRemoved(packageName, user) + } + + override fun onPackageAdded( + packageName: String, + user: UserHandle + ) { + mCallback.onPackageAdded(packageName, user) + } + + override fun onPackageChanged( + packageName: String, + user: UserHandle + ) { + mCallback.onPackageChanged(packageName, user) + } + + override fun onPackagesAvailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + mCallback.onPackagesAvailable(packageNames, user, replacing) + } + + override fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + mCallback.onPackagesUnavailable(packageNames, user, replacing) + } + + override fun onPackagesSuspended( + packageNames: Array, + user: UserHandle + ) { + mCallback.onPackagesSuspended(packageNames, user) + } + + override fun onPackagesUnsuspended( + packageNames: Array, + user: UserHandle + ) { + mCallback.onPackagesUnsuspended(packageNames, user) + } + + override fun onShortcutsChanged( + packageName: String, + shortcuts: List, + user: UserHandle + ) { + val shortcutInfoCompats: MutableList = + ArrayList( + shortcuts.size + ) + for (shortcutInfo in shortcuts) { + shortcutInfoCompats.add( + ShortcutInfoCompat( + shortcutInfo + ) + ) + } + mCallback.onShortcutsChanged(packageName, shortcutInfoCompats, user) + } + } + + /*@Override + public List getCustomShortcutActivityList( + @Nullable PackageUserKey packageUser) { + List result = new ArrayList<>(); + if (packageUser != null && !packageUser.mUser.equals(Process.myUserHandle())) { + return result; + } + PackageManager pm = mContext.getPackageManager(); + for (ResolveInfo info : + pm.queryIntentActivities(new Intent(Intent.ACTION_CREATE_SHORTCUT), 0)) { + if (packageUser == null || packageUser.mPackageName + .equals(info.activityInfo.packageName)) { + result.add(new ShortcutConfigActivityInfoVL(info.activityInfo, pm)); + } + } + return result; + }*/ +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVO.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVO.kt new file mode 100644 index 0000000000..fe95d2c36e --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/LauncherAppsCompatVO.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.data.compat + +import android.annotation.TargetApi +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.PinItemRequest +import android.content.pm.PackageManager +import android.os.Parcelable +import android.os.UserHandle +import androidx.annotation.Nullable + +@TargetApi(26) +class LauncherAppsCompatVO internal constructor(context: Context) : + LauncherAppsCompatVL(context) { + + override fun getApplicationInfo( + packageName: String, + flags: Int, + user: UserHandle + ): ApplicationInfo? { + return try { + val info = launcherApps.getApplicationInfo(packageName, flags, user) + if (info.flags and ApplicationInfo.FLAG_INSTALLED == 0 || !info.enabled) null else info + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + companion object { + //TODO + /*@Override + public List getCustomShortcutActivityList( + @Nullable PackageUserKey packageUser) { + List result = new ArrayList<>(); + UserHandle myUser = Process.myUserHandle(); + + final List users; + final String packageName; + if (packageUser == null) { + users = UserManagerCompat.getInstance(mContext).getUserProfiles(); + packageName = null; + } else { + users = new ArrayList<>(1); + users.add(packageUser.mUser); + packageName = packageUser.mPackageName; + } + for (UserHandle user : users) { + boolean ignoreTargetSdk = myUser.equals(user); + List activities = + mLauncherApps.getShortcutConfigActivityList(packageName, user); + for (LauncherActivityInfo activityInfo : activities) { + if (ignoreTargetSdk || activityInfo.getApplicationInfo().targetSdkVersion >= + Build.VERSION_CODES.O) { + result.add(new ShortcutConfigActivityInfoVO(activityInfo)); + } + } + } + + return result; + }*/ + + /** + * request.accept() will initiate the following flow: + * -> go-to-system-process for actual processing (a) + * -> callback-to-launcher on UI thread (b) + * -> post callback on the worker thread (c) + * -> Update model and unpin (in system) any shortcut not in out model. (d) + * + * Note that (b) will take at-least one frame as it involves posting callback from binder + * thread to UI thread. + * If (d) happens before we add this shortcut to our model, we will end up unpinning + * the shortcut in the system. + * Here its the caller's responsibility to add the newly created ShortcutInfo immediately + * to the model (which may involves a single post-to-worker-thread). That will guarantee + * that (d) happens after model is updated. + */ + + //TODO for shortcuts + @Nullable + /*fun createShortcutInfoFromPinItemRequest( + context: Context?, request: PinItemRequest?, acceptDelay: Long + ): ShortcutInfo? { + return if (request != null && request.requestType == PinItemRequest.REQUEST_TYPE_SHORTCUT && + request.isValid + ) { + if (acceptDelay <= 0) { + if (!request.accept()) { + return null + } + } else { // Block the worker thread until the accept() is called. + LooperExecutor(LauncherModel.getWorkerLooper()).execute(Runnable { + try { + Thread.sleep(acceptDelay) + } catch (e: InterruptedException) { // Ignore + } + if (request.isValid) { + request.accept() + } + }) + } + val compat = + ShortcutInfoCompat(request.shortcutInfo) + val info = ShortcutInfo(compat, context) + // Apply the unbadged icon and fetch the actual icon asynchronously. + val li: LauncherIcons = LauncherIcons.obtain(context) + li.createShortcutIcon(compat, false *//* badged *//*).applyTo(info) + li.recycle() + LauncherAppState.getInstance(context).getModel() + .updateAndBindShortcutInfo(info, compat) + info + } else { + null + } + }*/ + + fun getPinItemRequest(intent: Intent): PinItemRequest? { + val extra = + intent.getParcelableExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST) + return if (extra is PinItemRequest) extra else null + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/PackageInstallerCompat.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/PackageInstallerCompat.kt new file mode 100644 index 0000000000..bda351d7d9 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/PackageInstallerCompat.kt @@ -0,0 +1,103 @@ +package foundation.e.blisslauncher.data.compat + +import android.content.ComponentName +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageInstaller.SessionInfo +import android.os.Process +import android.util.SparseArray +import androidx.annotation.NonNull + +class PackageInstallerCompat(private val context: Context) { + + val activeSessions = SparseArray() + val installer = context.packageManager.packageInstaller + val appContext = context.applicationContext + val sessionVerifiedMap = HashMap() + + fun updateAndGetActiveSessionCache(): HashMap { + val activePackages = HashMap() + val user = Process.myUserHandle() + getAllVerifiedSessions().forEach { + if (it.appPackageName != null) { + //TODO: Cache to IconCache + activePackages[it.appPackageName] = it + activeSessions.run { put(it.sessionId, it.appPackageName) } + } + } + return activePackages + } + + fun onStop() { + } + + fun getAllVerifiedSessions(): List { + val list = ArrayList(installer.allSessions) + val iterator = list.iterator() + iterator.forEachRemaining { + if (verify(it) == null) { + iterator.remove() + } + } + + return list + } + + private fun verify(sessionInfo: SessionInfo?): SessionInfo? { + if (sessionInfo == null || sessionInfo.installerPackageName == null || + sessionInfo.appPackageName.isNullOrEmpty() + ) { + return null + } + val pkg = sessionInfo.installerPackageName + synchronized(sessionVerifiedMap) { + if (!sessionVerifiedMap.containsKey(pkg)) { + val launcherApps = LauncherAppsCompatVO(appContext) + val hasSystemFlag = launcherApps.getApplicationInfo( + pkg, + ApplicationInfo.FLAG_SYSTEM, Process.myUserHandle() + ) != null + sessionVerifiedMap[pkg] = hasSystemFlag + } + } + return if (sessionVerifiedMap[pkg] == true) sessionInfo else null + } + + companion object { + const val STATUS_INSTALLED = 0 + const val STATUS_INSTALLING = 1 + const val STATUS_FAILED = 2 + } + + class PackageInstallInfo { + val componentName: ComponentName + val packageName: String + val state: Int + val progress: Int + + private constructor(@NonNull info: SessionInfo) { + state = STATUS_INSTALLING + packageName = info.appPackageName + componentName = ComponentName(packageName, "") + progress = (info.progress * 100f).toInt() + } + + constructor(packageName: String, state: Int, progress: Int) { + this.state = state + this.packageName = packageName + componentName = ComponentName(packageName, "") + this.progress = progress + } + + companion object { + fun fromInstallingState(info: SessionInfo): PackageInstallInfo { + return PackageInstallInfo(info) + } + + fun fromState(state: Int, packageName: String): PackageInstallInfo { + return PackageInstallInfo(packageName, state, 0 /* progress */) + } + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt new file mode 100644 index 0000000000..593dd82f9c --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt @@ -0,0 +1,90 @@ +package foundation.e.blisslauncher.data.compat + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Process +import android.os.UserHandle +import android.os.UserManager +import android.util.ArrayMap +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import java.util.ArrayList + +open class UserManagerCompatVN(context: Context) : UserManagerRepository { + + protected val userManager: UserManager = context.getSystemService(Context.USER_SERVICE) as UserManager + private val pm: PackageManager = context.packageManager + + private lateinit var users: LongArrayMap + // Create a separate reverse map as LongArrayMap.indexOfValue checks if objects are same + // and not {@link Object#equals} + private lateinit var userToSerialMap: ArrayMap + + override fun enableAndResetCache() { + synchronized(this) { + users = LongArrayMap() + userToSerialMap = ArrayMap() + val _users: List = userManager.userProfiles + _users.forEach { + val serial = userManager.getSerialNumberForUser(it) + users.put(serial, it) + userToSerialMap[it] = serial + } + } + } + + override val userProfiles: List + get() { + synchronized(this) { + if (::users.isInitialized) { + return ArrayList(userToSerialMap.keys) + } + } + return userManager.userProfiles + } + + override fun getSerialNumberForUser(user: UserHandle): Long { + synchronized(this) { + if (::userToSerialMap.isInitialized) { + return userToSerialMap[user] ?: 0 + } + } + return userManager.getSerialNumberForUser(user) + } + + override fun getUserForSerialNumber(serialNumber: Long): UserHandle? { + synchronized(this) { + if (::users.isInitialized) { + users[serialNumber] + } + } + return userManager.getUserForSerialNumber(serialNumber) + } + + override fun getBadgedLabelForUser(label: CharSequence, user: UserHandle?): CharSequence = + when (user) { + null -> label + else -> pm.getUserBadgedLabel(label, user) + } + + override fun isQuietModeEnabled(user: UserHandle): Boolean = + userManager.isQuietModeEnabled(user) + + override fun isUserUnlocked(user: UserHandle): Boolean = userManager.isUserUnlocked(user) + + override val isDemoUser: Boolean + get() = false + + override fun requestQuietModeEnabled(enableQuietMode: Boolean, user: UserHandle?): Boolean = + false + + override val isAnyProfileQuietModeEnabled: Boolean + get() { + for (userProfile in userProfiles) { + if (Process.myUserHandle().equals(userProfile)) continue + + if (isQuietModeEnabled(userProfile)) return true + } + return false + } +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVNMr1.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVNMr1.kt new file mode 100644 index 0000000000..899d001640 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVNMr1.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.data.compat + +import android.annotation.TargetApi +import android.content.Context +import android.os.Build + +@TargetApi(Build.VERSION_CODES.N_MR1) +open class UserManagerCompatVNMr1(context: Context) : UserManagerCompatVN(context) { + + override val isDemoUser: Boolean + get() = userManager.isDemoUser +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVP.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVP.kt new file mode 100644 index 0000000000..53f7aa21ff --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVP.kt @@ -0,0 +1,13 @@ +package foundation.e.blisslauncher.data.compat + +import android.annotation.TargetApi +import android.content.Context +import android.os.Build +import android.os.UserHandle + +@TargetApi(Build.VERSION_CODES.P) +open class UserManagerCompatVP(context: Context) : UserManagerCompatVNMr1(context) { + + override fun requestQuietModeEnabled(enableQuietMode: Boolean, user: UserHandle?): Boolean = + userManager.requestQuietModeEnabled(enableQuietMode, user) +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt new file mode 100644 index 0000000000..69c0266a93 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt @@ -0,0 +1,7 @@ +package foundation.e.blisslauncher.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database(entities = [], version = 1) +abstract class BlissLauncherDatabase : RoomDatabase() \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherFiles.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherFiles.kt new file mode 100644 index 0000000000..0256e90779 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherFiles.kt @@ -0,0 +1,25 @@ +package foundation.e.blisslauncher.data.database + +/** + * File names of all the file BlissLauncher uses to store data. + */ +class BlissLauncherFiles { + companion object { + private val XML = ".xml" + + const val LAUNCHER_DB = "launcher.db" + const val SHARED_PREFERENCES_KEY = "foundation.e.blisslauncher.prefs" + const val DEVICE_PREFERENCES_KEY = "foundation.e.blisslauncher.device.prefs" + + const val WIDGET_PREVIEWS_DB = "widgetpreviews.db" + const val APP_ICONS_DB = "app_icons.db" + + val ALL_FILES = listOf( + LAUNCHER_DB, + SHARED_PREFERENCES_KEY + XML, + WIDGET_PREVIEWS_DB, + DEVICE_PREFERENCES_KEY + XML, + APP_ICONS_DB + ) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDao.kt new file mode 100644 index 0000000000..94a94a452d --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDao.kt @@ -0,0 +1,21 @@ +package foundation.e.blisslauncher.data.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface IconDao { + @Query("DELETE FROM icons WHERE componentName LIKE :componentName AND profileId = :userSerial") + fun delete(componentName: String, userSerial: Int) + + @Query("DROP TABLE IF EXISTS icons") + fun clear() + + @Query("SELECT * FROM icons WHERE profileId = :userSerial") + fun query(userSerial: Int): IconEntity + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(iconEntity: IconEntity) +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt new file mode 100644 index 0000000000..72e4e6fa5a --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt @@ -0,0 +1,9 @@ +package foundation.e.blisslauncher.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database(entities = [IconDatabase::class], version = 1) +abstract class IconDatabase: RoomDatabase() { + abstract fun iconDao(): IconDao +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconEntity.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/IconEntity.kt new file mode 100644 index 0000000000..22c8c87642 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/IconEntity.kt @@ -0,0 +1,54 @@ +package foundation.e.blisslauncher.data.database + +import androidx.room.ColumnInfo +import androidx.room.Entity + +@Entity(tableName = "icons", primaryKeys = ["componentName", "profileId"]) +data class IconEntity( + @ColumnInfo(name = "componentName", typeAffinity = ColumnInfo.TEXT) + val componentName: String, + @ColumnInfo(name = "profileId", typeAffinity = ColumnInfo.INTEGER) + val profileId: Int, + @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) + val lastUpdated: Int = 0, + @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) + val version: Int = 0, + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + val icon: ByteArray, + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + val iconLowRes: ByteArray, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT) + val label: String, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT) + val systemState: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IconEntity + + if (componentName != other.componentName) return false + if (profileId != other.profileId) return false + if (lastUpdated != other.lastUpdated) return false + if (version != other.version) return false + if (!icon.contentEquals(other.icon)) return false + if (!iconLowRes.contentEquals(other.iconLowRes)) return false + if (label != other.label) return false + if (systemState != other.systemState) return false + + return true + } + + override fun hashCode(): Int { + var result = componentName.hashCode() + result = 31 * result + profileId + result = 31 * result + lastUpdated + result = 31 * result + version + result = 31 * result + icon.contentHashCode() + result = 31 * result + iconLowRes.contentHashCode() + result = 31 * result + label.hashCode() + result = 31 * result + systemState.hashCode() + return result + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapInfo.kt b/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapInfo.kt new file mode 100644 index 0000000000..335539f772 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapInfo.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.data.graphics + +import android.graphics.Bitmap +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon + +open class BitmapInfo { + + var icon: Bitmap? = null + var color = 0 + fun applyTo(info: LauncherItemWithIcon) { + info.iconBitmap = icon + info.iconColor = color + } + + fun applyTo(info: BitmapInfo) { + info.icon = icon + info.color = color + } + + companion object { + fun fromBitmap(bitmap: Bitmap?): BitmapInfo { + val info = BitmapInfo() + info.icon = bitmap + //info.color = ColorExtractor.findDominantColorByHue(bitmap) + return info + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapRenderer.kt b/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapRenderer.kt new file mode 100644 index 0000000000..54be67afbb --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapRenderer.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.data.graphics + +import android.annotation.TargetApi +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Picture +import android.os.Build +import foundation.e.blisslauncher.common.Utilities + +object BitmapRenderer { + val USE_HARDWARE_BITMAP: Boolean = Utilities.ATLEAST_P + fun createSoftwareBitmap( + width: Int, + height: Int, + renderer: Renderer + ): Bitmap { + val result = Bitmap.createBitmap( + width, + height, + Bitmap.Config.ARGB_8888 + ) + renderer.draw(Canvas(result)) + return result + } + + @TargetApi(Build.VERSION_CODES.P) + fun createHardwareBitmap( + width: Int, + height: Int, + renderer: Renderer + ): Bitmap { + if (!USE_HARDWARE_BITMAP) { + return createSoftwareBitmap(width, height, renderer) + } + val picture = Picture() + renderer.draw(picture.beginRecording(width, height)) + picture.endRecording() + return Bitmap.createBitmap(picture) + } + + /** + * Interface representing a bitmap draw operation. + */ + interface Renderer { + fun draw(out: Canvas?) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt new file mode 100644 index 0000000000..badc42ac60 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt @@ -0,0 +1,220 @@ +package foundation.e.blisslauncher.data.icon + +import android.R +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.LauncherActivityInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.util.Log +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import foundation.e.blisslauncher.data.InvariantDeviceProfile +import foundation.e.blisslauncher.data.database.IconDao +import foundation.e.blisslauncher.data.graphics.BitmapInfo +import foundation.e.blisslauncher.data.graphics.BitmapRenderer +import foundation.e.blisslauncher.domain.keys.ComponentKey +import java.util.HashSet +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IconCache @Inject constructor( + private val context: Context, + private val inv: InvariantDeviceProfile, + private val iconProvider: IconProvider, + private val launcherApps: LauncherAppsCompat, + private val userManager: UserManagerRepository, + private val iconDao: IconDao +) { + + inner class CacheEntry : BitmapInfo() { + var title: CharSequence = "" + var contentDescription: CharSequence = "" + var isLowResIcon = false + } + + private val mDefaultIcons: HashMap = HashMap() + private val packageManager: PackageManager = context.packageManager + private val cache = HashMap() + private val iconDpi: Int = inv.fillResIconDpi + private val lowResOptions = + BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.RGB_565 } + + @SuppressLint("NewApi") + private val highResOptions = if (BitmapRenderer.USE_HARDWARE_BITMAP) { + BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.HARDWARE } + } else { + null + } + + private fun getFullResDefaultActivityIcon(): Drawable { + return getFullResIcon( + Resources.getSystem(), + if (Utilities.ATLEAST_OREO) R.drawable.sym_def_app_icon else R.mipmap.sym_def_app_icon + ) + } + + private fun getFullResIcon( + resources: Resources, + iconId: Int + ): Drawable { + val d: Drawable? = try { + resources.getDrawableForDensity(iconId, iconDpi) + } catch (e: Resources.NotFoundException) { + null + } + return d ?: getFullResDefaultActivityIcon() + } + + fun getFullResIcon(packageName: String, iconId: Int): Drawable { + return try { + packageManager.getResourcesForApplication(packageName) + } catch (e: PackageManager.NameNotFoundException) { + null + }.let { + if (it != null && iconId != 0) + getFullResIcon(it, iconId) + else getFullResDefaultActivityIcon() + } + } + + fun getFullResIcon(info: ActivityInfo): Drawable? { + return try { + packageManager.getResourcesForApplication( + info.applicationInfo + ) + } catch (e: PackageManager.NameNotFoundException) { + null + }.let { + if (it != null && info.iconResource != 0) + getFullResIcon(it, info.iconResource) + else getFullResDefaultActivityIcon() + } + } + + fun getFullResIcon(info: LauncherActivityInfo): Drawable { + return getFullResIcon(info, true) + } + + fun getFullResIcon( + info: LauncherActivityInfo, + flattenDrawable: Boolean + ): Drawable { + return iconProvider.getIcon(info, iconDpi, flattenDrawable) + } + + //TODO + /*protected fun makeDefaultIcon(user: UserHandle?): BitmapInfo? { + LauncherIcons.obtain(mContext).use({ li -> + return li.createBadgedIconBitmap( + getFullResDefaultActivityIcon(), user, VERSION.SDK_INT + ) + }) + }*/ + + /** + * Remove any records for the supplied ComponentName. + */ + @Synchronized + fun remove(componentName: ComponentName, user: UserHandle) { + cache.remove(ComponentKey(componentName, user)) + } + + /** + * Remove any records for the supplied package name from memory. + */ + private fun removeFromMemCacheLocked( + packageName: String, + user: UserHandle + ) { + val forDeletion = HashSet() + for (key in cache.keys) { + if (key.componentName.packageName == packageName + && key.user == user + ) { + forDeletion.add(key) + } + } + for (condemned in forDeletion) { + cache.remove(condemned) + } + } + + /** + * Updates the entries related to the given package in memory and persistent DB. + */ + @Synchronized + fun updateIconsForPkg( + packageName: String, + user: UserHandle + ) { + removeIconsForPkg(packageName, user) + try { + val info: PackageInfo = packageManager.getPackageInfo( + packageName, + PackageManager.GET_UNINSTALLED_PACKAGES + ) + val userSerial: Long = userManager.getSerialNumberForUser(user) + for (app in launcherApps.getActivityList(packageName, user)) { + //addIconToDBAndMemCache(app, info, userSerial, false /*replace existing*/) + } + } catch (e: PackageManager.NameNotFoundException) { + Log.d(TAG, "Package not found", e) + } + } + + /** + * Removes the entries related to the given package in memory and persistent DB. + */ + @Synchronized + fun removeIconsForPkg( + packageName: String, + user: UserHandle + ) { + removeFromMemCacheLocked(packageName, user) + val userSerial: Long = userManager.getSerialNumberForUser(user) + iconDao.delete("${packageName}/%", userSerial.toInt()) + } + + fun updateDbIcons(ignorePackagesForMainUser: Set) { + //TODO: Dispose all current running tasks + // Remove all active icon update tasks. + //mWorkerHandler.removeCallbacksAndMessages(IconCache.ICON_UPDATE_TOKEN) + iconProvider.updateSystemStateString(context) + for (user in userManager.userProfiles) { + // Query for the set of apps + val apps: List = launcherApps.getActivityList(null, user) + // Fail if we don't have any apps + // TODO: Fix this. Only fail for the current user. + if (apps.isEmpty()) { + return + } + // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} + // is called by the icon cache when the job is complete. + /*updateDBIcons( + user, + apps, + if (Process.myUserHandle() == user) ignorePackagesForMainUser else emptySet() + )*/ + } + } + + companion object { + private const val TAG = "IconCache" + + private const val INITIAL_ICON_CAPACITY = 50 + + private const val EMPTY_CLASS_NAME = "." + + private const val LOW_RES_SCALE_FACTOR = 5 + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt new file mode 100644 index 0000000000..ee57d23e11 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher.data.icon + +import android.content.Context +import android.content.pm.LauncherActivityInfo +import android.graphics.drawable.Drawable +import android.os.Build +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IconProvider @Inject constructor(context: Context) { + private var mSystemState: String? = null + + init { + updateSystemStateString(context) + } + + fun updateSystemStateString(context: Context) { + val locale: String = context.resources.configuration.locales.toLanguageTags() + mSystemState = locale + "," + Build.VERSION.SDK_INT + } + + fun getIconSystemState(packageName: String?): String? { + return mSystemState + } + + /** + * @param flattenDrawable true if the caller does not care about the specification of the + * original icon as long as the flattened version looks the same. + */ + fun getIcon( + info: LauncherActivityInfo, + iconDpi: Int, + flattenDrawable: Boolean + ): Drawable = info.getIcon(iconDpi) +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt new file mode 100644 index 0000000000..8a7b59b17f --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt @@ -0,0 +1,60 @@ +package foundation.e.blisslauncher.data.inject + +import android.content.Context +import android.os.Process +import dagger.Module +import dagger.Provides +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.common.executors.MainThreadExecutor +import foundation.e.blisslauncher.data.LauncherAppsChangedCallbackCompat +import foundation.e.blisslauncher.data.compat.LauncherAppsCompatVL +import foundation.e.blisslauncher.data.compat.LauncherAppsCompatVO +import foundation.e.blisslauncher.data.compat.UserManagerCompatVN +import foundation.e.blisslauncher.data.compat.UserManagerCompatVNMr1 +import foundation.e.blisslauncher.data.compat.UserManagerCompatVP +import java.util.concurrent.Executors +import javax.inject.Singleton + +@Module +class CompatModule { + + @Provides + @Singleton + fun provideLauncherAppsCompat(context: Context): LauncherAppsCompat = + if (Utilities.ATLEAST_OREO) { + LauncherAppsCompatVO(context) + } else LauncherAppsCompatVL(context) + + @Provides + @Singleton + fun provideUserManagerCompat(context: Context): UserManagerRepository = when { + Utilities.ATLEAST_P -> { + UserManagerCompatVP(context) + } + Utilities.ATLEAST_NOUGAT_MR1 -> { + UserManagerCompatVNMr1(context) + } + else -> UserManagerCompatVN(context) + } + + @Provides + @Singleton + fun provideExecutors() = + AppExecutors( + io = Executors.newSingleThreadExecutor().apply { + execute { + Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT) + } + }, + computation = Executors.newSingleThreadExecutor(), + main = MainThreadExecutor() + ) + + @Provides + @Singleton + fun provideOnAppsChangedCallback(launcherAppsChangedCallbackCompat: LauncherAppsChangedCallbackCompat): LauncherAppsCompat.OnAppsChangedCallbackCompat = + launcherAppsChangedCallbackCompat +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt new file mode 100644 index 0000000000..9b0fdadfc7 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt @@ -0,0 +1,22 @@ +package foundation.e.blisslauncher.data.inject + +import android.content.Context +import dagger.BindsInstance +import dagger.Component +import foundation.e.blisslauncher.domain.inject.DomainComponent +import javax.inject.Singleton + +@Singleton +@Component( + modules = [ + CompatModule::class, + DataRepoBindingModule::class + ] +) +interface DataComponent: DomainComponent { + + @Component.Factory + interface Factory { + fun create(@BindsInstance applicationContext: Context): DataComponent + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt new file mode 100644 index 0000000000..83caa02ce7 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt @@ -0,0 +1,38 @@ +package foundation.e.blisslauncher.data.inject + +import dagger.Module +import dagger.Provides +import foundation.e.blisslauncher.data.LauncherStateManagerImpl +import foundation.e.blisslauncher.data.apps.AppsRepositoryImpl +import foundation.e.blisslauncher.data.launcher.LauncherRepositoryImpl +import foundation.e.blisslauncher.data.shortcuts.ShortcutsRepositoryImpl +import foundation.e.blisslauncher.data.widgets.WidgetsRepositoryImpl +import foundation.e.blisslauncher.data.workspace.WorkspaceRepositoryImpl +import foundation.e.blisslauncher.domain.manager.LauncherStateManager +import foundation.e.blisslauncher.domain.repository.AppsRepository +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.ShortcutRepository +import foundation.e.blisslauncher.domain.repository.WidgetsRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository + +@Module +class DataRepoBindingModule { + + @Provides + fun bindLauncherStateManager(launcherStateManagerImpl: LauncherStateManagerImpl): LauncherStateManager = launcherStateManagerImpl + + @Provides + fun bindAppsRepository(appsRepositoryImpl: AppsRepositoryImpl): AppsRepository = appsRepositoryImpl + + @Provides + fun bindShortcutRepository(shortcutRepositoryImpl: ShortcutsRepositoryImpl): ShortcutRepository = shortcutRepositoryImpl + + @Provides + fun bindLauncherRepository(launcherRepositoryImpl: LauncherRepositoryImpl): LauncherRepository = launcherRepositoryImpl + + @Provides + fun bindWorkspaceRepository(workspaceRepositoryImpl: WorkspaceRepositoryImpl): WorkspaceRepository = workspaceRepositoryImpl + + @Provides + fun bindWidgetsRepository(widgetsRepositoryImpl: WidgetsRepositoryImpl): WidgetsRepository = widgetsRepositoryImpl +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/notification/NotificationListener.kt b/data/src/main/java/foundation/e/blisslauncher/data/notification/NotificationListener.kt new file mode 100755 index 0000000000..a9a5aaed7f --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/notification/NotificationListener.kt @@ -0,0 +1,139 @@ +package foundation.e.blisslauncher.data.notification + +import android.app.Notification +import android.app.NotificationChannel +import android.content.ComponentName +import android.content.Context +import android.os.Build +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import android.util.Log +import foundation.e.blisslauncher.data.SettingsObserver +import io.reactivex.subjects.Subject + +/** + * Created by falcon on 14/3/18. + */ +class NotificationListener : NotificationListenerService() { + + private lateinit var notificationBadgingObserver: SettingsObserver + + val tempRanking = Ranking() + + init { + instance = this + } + + override fun onCreate() { + super.onCreate() + isCreated = true + } + + override fun onDestroy() { + super.onDestroy() + isCreated = false + } + + override fun onListenerConnected() { + isConnected = true + + notificationBadgingObserver = object : SettingsObserver.Secure(contentResolver) { + override fun onSettingChanged(isNotificationBadgingEnabled: Boolean) { + if (!isNotificationBadgingEnabled) { + requestUnbind() + } + } + } + notificationBadgingObserver.register(NOTIFICATION_BADGING) + updateNotifications() + } + + override fun onListenerDisconnected() { + super.onListenerDisconnected() + isConnected = false + notificationBadgingObserver.unregister() + } + + override fun onNotificationPosted(sbn: StatusBarNotification) { + updateNotifications() + } + + override fun onNotificationRemoved(sbn: StatusBarNotification) { + updateNotifications() + } + + fun getListenerIfConnected(): NotificationListener? { + return if (isConnected) instance else null + } + + fun observeNotifications(behaviourSubject: Subject>) { + /*subject = behaviourSubject + val notificationListener = getListenerIfConnected() + if (notificationListener != null) { + updateNotifications() + } else if (!isCreated) { + subject.onNext(emptyList()) + }*/ + } + + private fun updateNotifications() { + if (isSubjectInitialised()) { + if (isConnected) { + try { + subject.onNext(filterNotifications(activeNotifications).map { it.packageName }) + } catch (e: SecurityException) { + Log.e( + TAG, + "SecurityException: failed to fetch notifications" + ) + subject.onNext(emptyList()) + } + } + subject.onNext(emptyList()) + } + } + + private fun filterNotifications(notifications: Array?): List { + if (notifications == null) return emptyList() + return notifications.filter { shouldBeFilteredOut(it) } + } + + private fun shouldBeFilteredOut(sbn: StatusBarNotification): Boolean { + val notification = sbn.notification + currentRanking.getRanking(sbn.key, tempRanking) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (!tempRanking.canShowBadge()) + return true + + if (tempRanking.channel.id == NotificationChannel.DEFAULT_CHANNEL_ID) { + if (notification.flags and Notification.FLAG_ONGOING_EVENT != 0) { + return true + } + } + } + + val title = notification.extras.getCharSequence(Notification.EXTRA_TITLE) + val text = notification.extras.getCharSequence(Notification.EXTRA_TEXT) + val missingTitleAndText = title.isNullOrEmpty() and text.isNullOrEmpty() + val isGroupHeader = notification.flags and Notification.FLAG_GROUP_SUMMARY != 0 + return isGroupHeader or missingTitleAndText + } + + companion object { + private var isCreated: Boolean = false + private var isConnected: Boolean = false + private var instance: NotificationListener? = null + + private lateinit var subject: Subject> + + const val NOTIFICATION_BADGING = "notification_badging" + + private const val TAG = "NotificationListener" + + private fun isSubjectInitialised() = ::subject.isInitialized + + fun requestRebind(context: Context) { + requestRebind(ComponentName(context, NotificationListener::class.java)) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/AppWidgetsRestoredReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/AppWidgetsRestoredReceiver.kt new file mode 100644 index 0000000000..c8d7b21cca --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/AppWidgetsRestoredReceiver.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.data.receiver diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/ConfigChangedReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/ConfigChangedReceiver.kt new file mode 100644 index 0000000000..79dfd154b2 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/ConfigChangedReceiver.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.data.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Process +import timber.log.Timber +import javax.inject.Inject + +class ConfigChangedReceiver @Inject constructor(private val context: Context) : + BroadcastReceiver() { + + private val fontScale = context.resources.configuration.fontScale + private val density = context.resources.configuration.densityDpi + + override fun onReceive(context: Context, intent: Intent) { + val config = context.resources.configuration + if (fontScale != config.fontScale || density != config.densityDpi) { + Timber.d("Configuration changed, restarting launcher") + unregister() + Process.killProcess(Process.myPid()) + } + } + + fun register() { + context.registerReceiver(this, IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)) + } + + fun unregister() { + context.unregisterReceiver(this) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/InstallShortcutReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/InstallShortcutReceiver.kt new file mode 100644 index 0000000000..c8d7b21cca --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/InstallShortcutReceiver.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.data.receiver diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/ProfileReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/ProfileReceiver.kt new file mode 100644 index 0000000000..9464d2ae2b --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/ProfileReceiver.kt @@ -0,0 +1,77 @@ +package foundation.e.blisslauncher.data.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle +import foundation.e.blisslauncher.domain.interactor.ChangeUserAvailability +import foundation.e.blisslauncher.domain.interactor.ChangeUserLockState +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ProfileReceiver @Inject constructor( + private val context: Context, + private val changeUserAvailability: ChangeUserAvailability, + private val changeUserLockState: ChangeUserLockState, + private val userManager: UserManagerRepository +) : + BroadcastReceiver() { + + fun register() { + // Register intent receivers + val filter = IntentFilter() + filter.addAction(Intent.ACTION_LOCALE_CHANGED) + // For handling managed profiles + // For handling managed profiles + filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED) + filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED) + filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) + filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) + + context.registerReceiver(this, filter) + } + + fun unregister() { + context.unregisterReceiver(this) + } + + override fun onReceive(context: Context, intent: Intent) { + + if (DEBUG_RECEIVER) Timber.d("onReceive intent=$intent") + + when (val action = intent.action) { + Intent.ACTION_LOCALE_CHANGED -> TODO("Force reload here") // If locale has been changed, clear out all the labels in workspace + Intent.ACTION_MANAGED_PROFILE_ADDED, Intent.ACTION_MANAGED_PROFILE_REMOVED -> { + userManager.enableAndResetCache() + TODO("Force reload here") + } + Intent.ACTION_MANAGED_PROFILE_AVAILABLE, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, Intent.ACTION_MANAGED_PROFILE_UNLOCKED -> { + val user = intent.getParcelableExtra(Intent.EXTRA_USER) + if (user != null) { + if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE == action || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE == action) { + changeUserAvailability(user) + // TODO + } + + // ACTION_MANAGED_PROFILE_UNAVAILABLE sends the profile back to locked mode, so + // we need to run the state change task again. + // ACTION_MANAGED_PROFILE_UNAVAILABLE sends the profile back to locked mode, so + // we need to run the state change task again. + if (Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE == action || Intent.ACTION_MANAGED_PROFILE_UNLOCKED == action) { + changeUserLockState(user) + } + } + } + } + } + + companion object { + private const val TAG = "ProfileReceiver" + private const val DEBUG_RECEIVER = true + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/SdCardAvailableReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SdCardAvailableReceiver.kt new file mode 100644 index 0000000000..c8d7b21cca --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SdCardAvailableReceiver.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.data.receiver diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt new file mode 100644 index 0000000000..3dfc63e5c8 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.data.receiver + +import android.content.Context +import android.os.UserHandle + +class SessionCommitReceiver { + companion object { + fun queueAppIconAddition(context: Context, it: String, user: UserHandle) { + + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/WallpaperChangedReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/WallpaperChangedReceiver.kt new file mode 100644 index 0000000000..c8d7b21cca --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/WallpaperChangedReceiver.kt @@ -0,0 +1 @@ +package foundation.e.blisslauncher.data.receiver diff --git a/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/ShortcutsRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/ShortcutsRepositoryImpl.kt new file mode 100644 index 0000000000..c2ab24a5cf --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/ShortcutsRepositoryImpl.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.data.shortcuts + +import android.content.pm.ShortcutInfo +import foundation.e.blisslauncher.domain.repository.ShortcutRepository +import io.reactivex.Single +import javax.inject.Inject + +class ShortcutsRepositoryImpl @Inject constructor() : ShortcutRepository { + override fun getAllShortcuts(): Single> { + return Single.just(emptyList()) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt new file mode 100644 index 0000000000..f23ecbc30c --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt @@ -0,0 +1,8 @@ +package foundation.e.blisslauncher.data.widgets + +import foundation.e.blisslauncher.domain.repository.WidgetsRepository +import javax.inject.Inject + +class WidgetsRepositoryImpl @Inject constructor(): WidgetsRepository { + +} \ No newline at end of file diff --git a/data/src/main/res/values/config.xml b/data/src/main/res/values/config.xml new file mode 100644 index 0000000000..1c972ebdd5 --- /dev/null +++ b/data/src/main/res/values/config.xml @@ -0,0 +1,4 @@ + + + 90 + \ No newline at end of file diff --git a/data/src/main/res/values/dimens.xml b/data/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..ade66cc90b --- /dev/null +++ b/data/src/main/res/values/dimens.xml @@ -0,0 +1,30 @@ + + + + 8dp + 1dp + 8dp + 8dp + 8dp + + 8dp + + 5.5dp + 0dp + 8dp + + 8dp + 2dp + 80dp + 0dp + + 9dp + 6dp + 13sp + 4dp + 12dp + 14sp + 48dp + 24dp + + \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle index dcca293e86..7fb0e35af2 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -1,30 +1,13 @@ import foundation.e.blisslauncher.buildsrc.Libs -import foundation.e.blisslauncher.buildsrc.Versions -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -android { - compileSdkVersion Versions.compile_sdk - - defaultConfig { - minSdkVersion Versions.min_sdk - targetSdkVersion Versions.target_sdk - } - - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } -} +apply from: '../common.gradle' dependencies { - implementation Libs.Kotlin.stdlib - - kapt Libs.Dagger.compiler - + implementation project(path: ":common") + implementation Libs.RxJava.rxJava implementation Libs.RxJava.rxKotlin + implementation Libs.timber + testImplementation Libs.junit } diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt new file mode 100644 index 0000000000..a301468699 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt @@ -0,0 +1,56 @@ +package foundation.e.blisslauncher.domain + +import android.content.ComponentName +import android.os.UserHandle +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.entity.LauncherItem + +typealias ItemInfoMatcher = (item: LauncherItem, cn: ComponentName) -> Boolean + +fun ItemInfoMatcher.or(itemInfoMatcher: ItemInfoMatcher): ItemInfoMatcher { + return { launcherItem: LauncherItem, componentName: ComponentName -> + itemInfoMatcher(launcherItem, componentName) || this(launcherItem, componentName) + } +} + +fun ItemInfoMatcher.and(itemInfoMatcher: ItemInfoMatcher): ItemInfoMatcher { + return { launcherItem: LauncherItem, componentName: ComponentName -> + itemInfoMatcher(launcherItem, componentName) && this(launcherItem, componentName) + } +} + +fun ItemInfoMatcher.not(itemInfoMatcher: ItemInfoMatcher): ItemInfoMatcher { + return { launcherItem: LauncherItem, componentName: ComponentName -> + !itemInfoMatcher(launcherItem, componentName) + } +} + +class Matcher { + companion object { + fun ofPackages(packageNames: HashSet, user: UserHandle): ItemInfoMatcher = + { launcherItem: LauncherItem, componentName: ComponentName -> + packageNames.contains(componentName.packageName) && launcherItem.user == user + } + + fun ofComponents(components: HashSet, user: UserHandle): ItemInfoMatcher = + { launcherItem: LauncherItem, componentName: ComponentName -> + components.contains(componentName) && launcherItem.user == user + } + + fun ofItemIds(ids: LongArrayMap, matchDefault: Boolean): ItemInfoMatcher = + { launcherItem: LauncherItem, _: ComponentName -> + ids.get(launcherItem.id, matchDefault) + } + + fun ofUser(user: UserHandle): ItemInfoMatcher = + { launcherItem: LauncherItem, componentName: ComponentName -> + launcherItem.user == user + } + } +} + +typealias ApplyFlag = (flag: Int) -> (oldFlags: Int) -> Int + +val addFlag: ApplyFlag = { flag -> { oldFlags -> oldFlags or flag } } + +val removeFlag: ApplyFlag = { flag -> { oldFlags -> oldFlags and flag.inv() } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt new file mode 100644 index 0000000000..c4fa4a2494 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt @@ -0,0 +1,73 @@ +package foundation.e.blisslauncher.domain.entity + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherActivityInfo +import android.os.Build +import android.os.Process +import android.os.UserHandle +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.domain.keys.ComponentKey + +/** + * Represents an app in AllAppsStore + */ +open class ApplicationItem : WorkspaceItem { + /** + * The intent used to start the application. + */ + lateinit var componentName: ComponentName + + constructor() : super() { + itemType = LauncherConstants.ItemType.APPLICATION + } + + constructor(info: LauncherActivityInfo, user: UserHandle, quietModeEnabled: Boolean) { + this.componentName = info.componentName + this.container = NO_ID.toLong() + this.user = user + this.title = info.label + actionIntent = makeLaunchIntent(componentName) + + if (quietModeEnabled) { + runtimeStatusFlags = runtimeStatusFlags or FLAG_DISABLED_QUIET_USER + } + updateRuntimeFlagsForActivityTarget(this, info) + } + + override fun getIntent(): Intent? = actionIntent + + fun toComponentKey(): ComponentKey = + ComponentKey( + componentName, + user + ) + + companion object { + fun makeLaunchIntent(info: LauncherActivityInfo) = makeLaunchIntent(info.componentName) + + fun makeLaunchIntent(cn: ComponentName): Intent { + return Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(cn) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + } + + fun updateRuntimeFlagsForActivityTarget( + info: LauncherItemWithIcon, lai: LauncherActivityInfo + ) { + val appInfo = lai.applicationInfo + /*if (PackageManagerHelper.isAppSuspended(appInfo)) { + info.runtimeStatusFlags = info.runtimeStatusFlags or FLAG_DISABLED_SUSPENDED + }*/ + info.runtimeStatusFlags = + info.runtimeStatusFlags or if (appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0) FLAG_SYSTEM_NO else FLAG_SYSTEM_YES + if (Utilities.ATLEAST_OREO + && appInfo.targetSdkVersion >= Build.VERSION_CODES.O && Process.myUserHandle() == lai.user + ) { // The icon for a non-primary user is badged, hence it's not exactly an adaptive icon. + info.runtimeStatusFlags = info.runtimeStatusFlags or FLAG_ADAPTIVE_ICON + } + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Empty.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Empty.kt new file mode 100644 index 0000000000..baee2c0e1c --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Empty.kt @@ -0,0 +1,8 @@ +package foundation.e.blisslauncher.domain.entity + +/** + * The entity with only one value: the `Empty` object. Can be used in interactors those are intended to return void. + */ +object Empty : Entity { + override fun toString(): String = "BlissLauncher.Empty" +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Entity.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Entity.kt new file mode 100644 index 0000000000..b7c4422b47 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/Entity.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.domain.entity + +/** + * Application-wide critical business rule which is high level, changes very infrequently. + * It defines the data representation and the logic on how to manipulate this data + * and exposes both to the interactors. + * + * It should not load the data. + */ +interface Entity \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt new file mode 100644 index 0000000000..d9fdd302ca --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt @@ -0,0 +1,111 @@ +package foundation.e.blisslauncher.domain.entity + +import android.os.Process +import foundation.e.blisslauncher.common.Utilities + +class FolderItem : LauncherItem() { + + var options: Int = 0 + + val contents = mutableListOf() + + val listeners = ArrayList() + + init { + itemType = LauncherConstants.ItemType.FOLDER + user = Process.myUserHandle() + } + + /** + * Add an app or shortcut + * + * @param item + */ + fun add(item: WorkspaceItem, animate: Boolean) { + add(item, contents.size, animate) + } + + /** + * Add an app or shortcut for a specified rank. + */ + fun add(item: WorkspaceItem, rank: Int, animate: Boolean) { + var rank = rank + rank = Utilities.boundToRange(rank, 0, contents.size) + contents.add(rank, item) + /*for (i in listeners.indices) { + listeners.get(i).onAdd(item, rank) + }*/ + itemsChanged(animate) + } + + /** + * Remove an app or shortcut. Does not change the DB. + * + * @param item + */ + fun remove(item: WorkspaceItem, animate: Boolean) { + contents.remove(item) + /*for (i in listeners.indices) { + listeners.get(i).onRemove(item) + }*/ + itemsChanged(animate) + } + + fun addListener(listener: FolderListener) { + listeners.add(listener) + } + + fun removeListener(listener: FolderListener) { + listeners.remove(listener) + } + + fun itemsChanged(animate: Boolean) { + for (i in listeners.indices) { + listeners.get(i).onItemsChanged(animate) + } + } + + fun prepareAutoUpdate() { + for (i in listeners.indices) { + listeners.get(i).prepareAutoUpdate() + } + } + + interface FolderListener { + fun onAdd(item: WorkspaceItem, rank: Int) + fun onRemove(item: WorkspaceItem) + fun onTitleChanged(title: CharSequence) + fun onItemsChanged(animate: Boolean) + fun prepareAutoUpdate() + } + + fun hasOption(optionFlag: Int): Boolean { + return options and optionFlag != 0 + } + + /** + * @param option flag to set or clear + * @param isEnabled whether to set or clear the flag + * @param writer if not null, save changes to the db. + */ + fun setOption(option: Int, isEnabled: Boolean) { + val oldOptions = options + options = if (isEnabled) { + options or option + } else { + options and option.inv() + } + /*if (writer != null && oldOptions != options) { + writer.updateItemInDatabase(this) + }*/ + } + + companion object { + const val NO_FLAGS = 0x00000000 + + /** + * Represent a work folder + */ + const val FLAG_WORK_FOLDER = 0x00000002 + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt new file mode 100644 index 0000000000..5e5af323af --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher.domain.entity + +class LauncherConstants { + + object ItemType { + + const val APPLICATION = 0 + const val SHORTCUT = 1 + const val FOLDER = 2 + const val APPWIDGET = 4 + const val CUSTOM_APPWIDGET = 5 + const val DEEP_SHORTCUT = 6 + + fun itemTypeToString(type: Int): String = when (type) { + APPLICATION -> "APP" + SHORTCUT -> "SHORTCUT" + FOLDER -> "FOLDER" + APPWIDGET -> "WIDGET" + CUSTOM_APPWIDGET -> "CUSTOMWIDGET" + DEEP_SHORTCUT -> "DEEPSHORTCUT" + else -> type.toString() + } + } + + object ContainerType { + + const val CONTAINER_DESKTOP = -100 + const val CONTAINER_HOTSEAT = -101 + + fun containerToString(container: Int): String = when (container) { + CONTAINER_DESKTOP -> "desktop" + CONTAINER_HOTSEAT -> "hotseat" + else -> container.toString() + } + } +} diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt new file mode 100644 index 0000000000..5c63431fd6 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt @@ -0,0 +1,148 @@ +package foundation.e.blisslauncher.domain.entity + +import android.content.ComponentName +import android.content.Intent +import android.os.Process +import android.os.UserHandle + +/** + * Represents an item in BlissLauncher. + */ +open class LauncherItem : Entity { + + /** + * The id in the settings database for this item + */ + var id: Long = NO_ID.toLong() + + /** + * One of [LauncherConstants.ItemType.APPLICATION], + * [LauncherConstants.ItemType.SHORTCUT], + * [LauncherConstants.ItemType.FOLDER] + * [LauncherConstants.ItemType.APPWIDGET], + * [LauncherConstants.ItemType.CUSTOM_APPWIDGET] or + * [LauncherConstants.ItemType.DEEP_SHORTCUT]. + */ + var itemType = 0 + + /** + * The id of the container that holds this item. For the desktop, this will be + * [LauncherConstants.ContainerType.CONTAINER_DESKTOP]. For the all applications folder it + * will be [.NO_ID] (since it is not stored in the settings DB). For user folders + * it will be the id of the folder. + */ + var container: Long = NO_ID.toLong() + + /** + * Indicates the screen in which the shortcut appears if the container types is + * [LauncherConstants.ContainerType.CONTAINER_DESKTOP]. (i.e., ignore if the container type is + * [LauncherConstants.ContainerType.CONTAINER_HOTSEAT]) + */ + var screenId: Long = -1 + + /** + * Indicates the X position of the associated cell. + */ + var cellX = -1 + + /** + * Indicates the Y position of the associated cell. + */ + var cellY = -1 + + /** + * Indicates the X cell span. + */ + var spanX = 1 + + /** + * Indicates the Y cell span. + */ + var spanY = 1 + + /** + * Indicates the minimum X cell span. + */ + var minSpanX = 1 + + /** + * Indicates the minimum Y cell span. + */ + var minSpanY = 1 + + /** + * Indicates the position in an ordered list. + */ + var rank = 0 + + /** + * Title of the item + */ + var title: CharSequence? = null + + /** + * Content description of the item. + */ + var contentDescription: CharSequence? = null + + lateinit var user: UserHandle + + constructor() { + user = Process.myUserHandle() + } + + constructor(item: LauncherItem) { + copyFrom(item) + } + + private fun copyFrom(item: LauncherItem) { + id = item.id + cellX = item.cellX + cellY = item.cellY + spanX = item.spanX + spanY = item.spanY + rank = item.rank + screenId = item.screenId + itemType = item.itemType + container = item.container + user = item.user + contentDescription = item.contentDescription + } + + open fun getIntent(): Intent? { + return null + } + + open fun getTargetComponent(): ComponentName? { + val intent = getIntent() + return intent?.component + } + + override fun toString(): String { + return javaClass.simpleName + "(" + dumpProperties() + ")" + } + + protected open fun dumpProperties(): String? { + return ("id=" + id + + " type=" + LauncherConstants.ItemType.itemTypeToString(itemType) + + " container=" + LauncherConstants.ContainerType.containerToString(container.toInt()) + + " screen=" + screenId + + " cell(" + cellX + "," + cellY + ")" + + " span(" + spanX + "," + spanY + ")" + + " minSpan(" + minSpanX + "," + minSpanY + ")" + + " rank=" + rank + + " user=" + user + + " title=" + title) + } + + /** + * Whether this item is disabled. + */ + open fun isDisabled(): Boolean { + return false + } + + companion object { + const val NO_ID = -1 + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt new file mode 100644 index 0000000000..b62c30910c --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt @@ -0,0 +1,100 @@ +package foundation.e.blisslauncher.domain.entity + +import android.graphics.Bitmap + +/** + * Represents a LauncherItem which also have an icon. + */ +abstract class LauncherItemWithIcon : LauncherItem { + + /** + * A bitmap version of the application icon. + */ + var iconBitmap: Bitmap? = null + + /** + * Dominant color in the [.iconBitmap]. + */ + var iconColor = 0 + + /** + * Indicates whether we're using a low res icon + */ + var usingLowResIcon = false + + /** + * Status associated with the system state of the underlying item. This is calculated every + * time a new info is created and not persisted on the disk. + */ + var runtimeStatusFlags = 0 + + constructor() + + constructor(item: LauncherItemWithIcon) : super(item) { + iconBitmap = item.iconBitmap + iconColor = item.iconColor + usingLowResIcon = item.usingLowResIcon + runtimeStatusFlags = item.runtimeStatusFlags + } + + override fun isDisabled(): Boolean = (runtimeStatusFlags and FLAG_DISABLED_MASK) != 0 + + companion object { + /** + * Indicates that the icon is disabled due to safe mode restrictions. + */ + const val FLAG_DISABLED_SAFEMODE = 1 shl 0 + + /** + * Indicates that the icon is disabled as the app is not available. + */ + const val FLAG_DISABLED_NOT_AVAILABLE = 1 shl 1 + + /** + * Indicates that the icon is disabled as the app is suspended + */ + const val FLAG_DISABLED_SUSPENDED = 1 shl 2 + + /** + * Indicates that the icon is disabled as the user is in quiet mode. + */ + const val FLAG_DISABLED_QUIET_USER = 1 shl 3 + + /** + * Indicates that the icon is disabled as the publisher has disabled the actual shortcut. + */ + const val FLAG_DISABLED_BY_PUBLISHER = 1 shl 4 + + /** + * Indicates that the icon is disabled as the user partition is currently locked. + */ + const val FLAG_DISABLED_LOCKED_USER = 1 shl 5 + + const val FLAG_DISABLED_MASK = FLAG_DISABLED_SAFEMODE or + FLAG_DISABLED_NOT_AVAILABLE or FLAG_DISABLED_SUSPENDED or + FLAG_DISABLED_QUIET_USER or FLAG_DISABLED_BY_PUBLISHER or FLAG_DISABLED_LOCKED_USER + + /** + * The item points to a system app. + */ + const val FLAG_SYSTEM_YES = 1 shl 6 + + /** + * The item points to a non system app. + */ + const val FLAG_SYSTEM_NO = 1 shl 7 + + const val FLAG_SYSTEM_MASK = FLAG_SYSTEM_YES or FLAG_SYSTEM_NO + + /** + * Flag indicating that the icon is an [android.graphics.drawable.AdaptiveIconDrawable] + * that can be optimized in various way. + */ + const val FLAG_ADAPTIVE_ICON = 1 shl 8 + + /** + * Flag indicating that the icon is badged. + */ + const val FLAG_ICON_BADGED = 1 shl 9 + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt new file mode 100644 index 0000000000..4310af50db --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt @@ -0,0 +1,109 @@ +package foundation.e.blisslauncher.domain.entity + +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.ShortcutIconResource + +/** + * Represents an item in workspace and inside folder + * Also used for pinned and dynamic shortcuts of the apps. + */ +open class WorkspaceItem : LauncherItemWithIcon { + + /** + * The intent used to start the application. + */ + var actionIntent: Intent? = null + + /** + * If isShortcut=true and customIcon=false, this contains a reference to the + * shortcut icon as an application's resource. + */ + var iconResource: ShortcutIconResource? = null + + /** + * A message to display when the user tries to start a disabled shortcut. + * This is currently only used for deep shortcuts. + */ + var disabledMessage: CharSequence? = null + + var status = 0 + + /** + * The installation progress [0-100] of the package that this shortcut represents. + */ + private var installProgress = 0 + set(value) { + status = status or FLAG_INSTALL_SESSION_ACTIVE + field = value + } + + constructor() { + itemType = LauncherConstants.ItemType.SHORTCUT + } + + constructor(item: WorkspaceItem) : super(item) { + title = item.title + actionIntent = item.getIntent() + iconResource = item.iconResource + status = item.status + installProgress = item.installProgress + } + + override fun getIntent(): Intent? { + return actionIntent + } + + fun hasStatusFlag(flag: Int) = status and flag != 0 + + fun isPromise() = hasStatusFlag(FLAG_RESTORED_ICON or FLAG_AUTOINSTALL_ICON) + + fun hasPromiseIconUi(): Boolean = isPromise() && !hasStatusFlag(FLAG_SUPPORTS_WEB_UI) + + override fun getTargetComponent(): ComponentName? { + val cn = super.getTargetComponent() + if (cn == null && (itemType == LauncherConstants.ItemType.SHORTCUT + || hasStatusFlag(FLAG_SUPPORTS_WEB_UI)) + ) { + // Legacy shortcuts and promise icons with web UI may not have a componentName but just + // a packageName. In that case create a dummy componentName instead of adding additional + // check everywhere. + val pkg: String? = actionIntent?.getPackage() + return if (pkg == null) null else ComponentName(pkg, ".") + } + return cn + } + + companion object { + val DEFAULT = 0 + + /** + * The shortcut was restored from a backup and it not ready to be used. This is automatically + * set during backup/restore + */ + const val FLAG_RESTORED_ICON = 1 + + /** + * The icon was added as an auto-install app, and is not ready to be used. This flag can't + * be present along with [.FLAG_RESTORED_ICON], and is set during default layout + * parsing. + */ + const val FLAG_AUTOINSTALL_ICON = 2 //0B10; + + /** + * The icon is being installed. If [.FLAG_RESTORED_ICON] or [.FLAG_AUTOINSTALL_ICON] + * is set, then the icon is either being installed or is in a broken state. + */ + const val FLAG_INSTALL_SESSION_ACTIVE = 4 // 0B100; + + /** + * Indicates that the widget restore has started. + */ + const val FLAG_RESTORE_STARTED = 8 //0B1000; + + /** + * Web UI supported. + */ + const val FLAG_SUPPORTS_WEB_UI = 16 //0B10000; + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/executors/PostExecutionThread.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/executors/PostExecutionThread.kt deleted file mode 100644 index ba8ed2ca00..0000000000 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/executors/PostExecutionThread.kt +++ /dev/null @@ -1,7 +0,0 @@ -package foundation.e.blisslauncher.domain.executors - -import io.reactivex.Scheduler - -interface PostExecutionThread { - val scheduler: Scheduler -} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/executors/ThreadExecutor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/executors/ThreadExecutor.kt deleted file mode 100644 index f2b27ad31b..0000000000 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/executors/ThreadExecutor.kt +++ /dev/null @@ -1,5 +0,0 @@ -package foundation.e.blisslauncher.domain.executors - -import java.util.concurrent.Executor - -interface ThreadExecutor : Executor \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt new file mode 100644 index 0000000000..09466cc517 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt @@ -0,0 +1,27 @@ +package foundation.e.blisslauncher.domain.inject + +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.manager.LauncherStateManager +import foundation.e.blisslauncher.domain.repository.LauncherRepository + +/** + * Interface that lists all public repositories and data access layer components which are needed + * to be exposed to the `domain` layer + */ +interface DomainComponent { + + fun launcherStateManager(): LauncherStateManager + + fun launcherRepository(): LauncherRepository + + fun appExecutors(): AppExecutors + + fun launcherAppsCompat(): LauncherAppsCompat + + companion object { + @Volatile + @JvmStatic + lateinit var INSTANCE: DomainComponent + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt new file mode 100644 index 0000000000..44bc2c20e6 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt @@ -0,0 +1,31 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class AddPackages @Inject constructor( + appExecutors: AppExecutors, + private val launcherRepository: LauncherRepository, + private val userManager: UserManagerRepository, + private val observeAddedApps: ObserveAddedApps +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + params.packages.forEach { + //TODO: Update icons cache + launcherRepository.add(it, params.user, userManager.isQuietModeEnabled(params.user)) + //TODO: Add SessionCommitReceiver for below O devices + } + }/*.doOnComplete { + observeAddedApps(Unit) + }*/ +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt new file mode 100644 index 0000000000..d1b2661e64 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class ChangeUserAvailability @Inject constructor( + appExecutors: AppExecutors, + private val launcherRepository: LauncherRepository, + private val userManager: UserManagerRepository, + private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems +) : CompletableInteractor() { + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: UserHandle): Completable = Completable.fromAction { + observeUpdatedLauncherItems( + launcherRepository.updateUserAvailability( + params, + userManager.isQuietModeEnabled(params) + ) + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserLockState.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserLockState.kt new file mode 100644 index 0000000000..7a1140977a --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserLockState.kt @@ -0,0 +1,16 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import io.reactivex.Flowable +import javax.inject.Inject + +class ChangeUserLockState @Inject constructor(appExecutors: AppExecutors) : + PublishSubjectInteractor() { + override val subscribeExecutor = appExecutors.io + override val observeExecutor = appExecutors.main + + override fun createObservable(params: UserHandle): Flowable { + return Flowable.just("") + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt new file mode 100644 index 0000000000..5e5625d08f --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt @@ -0,0 +1,14 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.ItemInfoMatcher +import io.reactivex.Completable +import java.util.concurrent.Executor + +class DeleteComponents(appExecutors: AppExecutors): CompletableInteractor() { + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: ItemInfoMatcher): Completable { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt new file mode 100644 index 0000000000..f26dbe2938 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt @@ -0,0 +1,117 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.Looper +import io.reactivex.BackpressureStrategy +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject +import timber.log.Timber +import java.util.concurrent.Executor + +/** + * Interactor to execute tasks synchronously on main thread. + */ +abstract class SynchronousInteractor { + abstract fun doWork(params: P) + + operator fun invoke(params: P, block: () -> Unit = {}) { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw IllegalStateException("Can't be executed from thread other than main") + } + doWork(params) + block() // To do any additional work + } +} + +abstract class AsyncInteractor : Disposable { + abstract val subscribeExecutor: Executor + + protected val disposables: CompositeDisposable = CompositeDisposable() + + override fun dispose() = disposables.dispose() + + override fun isDisposed(): Boolean = disposables.isDisposed +} + +abstract class CompletableInteractor : AsyncInteractor

() { + + protected abstract fun doWork(params: P): Completable + + operator fun invoke(params: P, onComplete: () -> Unit = {}) { + this.disposables += doWork(params) + .subscribeOn(Schedulers.from(subscribeExecutor)) + .subscribe(onComplete, Timber::w) + } + + fun executeSync(params: P) { + doWork(params) + } +} + +abstract class ObservableInteractor : AsyncInteractor

() { + abstract val observeExecutor: Executor +} + +abstract class ResultInteractor : ObservableInteractor

() { + + abstract fun doWork(params: P? = null): Single + + operator fun invoke( + params: P? = null, + onSuccess: (result: T) -> Unit = {}, + onError: (e: Throwable) -> Unit = {} + ) { + disposables += this.doWork(params) + .subscribeOn(Schedulers.from(subscribeExecutor)) + .observeOn(Schedulers.from(observeExecutor)) + .subscribe(onSuccess, onError) + } +} + +abstract class SubjectInteractor : ObservableInteractor

() { + abstract val subject: Subject

+ + protected abstract fun createObservable(params: P): Flowable + + operator fun invoke(params: P) = subject.onNext(params) + + fun observe(onNext: (result: T) -> Unit = {}) { + disposables += subject.toFlowable(BackpressureStrategy.BUFFER) + .flatMap { createObservable(it) } + .subscribeOn(Schedulers.from(subscribeExecutor)) + .observeOn(Schedulers.from(observeExecutor)) + .subscribe(onNext) + } +} + +abstract class PublishSubjectInteractor : SubjectInteractor() { + override val subject: Subject

= PublishSubject.create() +} + +abstract class BehaviourSubjectInteractor : SubjectInteractor() { + override val subject: Subject

= BehaviorSubject.create() +} + +abstract class FlowableInteractor : ObservableInteractor

() { + + protected abstract fun buildObservable(params: P? = null): Flowable + + operator fun invoke( + params: P, + onNext: (next: T) -> Unit = {}, + onError: (e: Throwable) -> Unit = {}, + onComplete: () -> Unit = {} + ) { + disposables += this.buildObservable(params) + .subscribeOn(Schedulers.from(subscribeExecutor)) + .observeOn(Schedulers.from(observeExecutor)) + .subscribe(onNext, onError, onComplete) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LauncherStateInteractor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LauncherStateInteractor.kt new file mode 100644 index 0000000000..2596ee0b34 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LauncherStateInteractor.kt @@ -0,0 +1,19 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.domain.manager.LauncherStateManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LauncherStateInteractor @Inject constructor(private val launcherStateManager: LauncherStateManager) : + SynchronousInteractor() { + + override fun doWork(command: Command) { + if (command == Command.INIT) + launcherStateManager.init() + else if (command == Command.TERMINATE) + launcherStateManager.terminate() + } + + enum class Command { INIT, TERMINATE } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt new file mode 100644 index 0000000000..279077c4c5 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt @@ -0,0 +1,30 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import io.reactivex.Single +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LoadLauncher @Inject constructor( + private val launcherApps: LauncherAppsCompat, + private val userManager: UserManagerRepository, + appExecutors: AppExecutors +) : ResultInteractor>() { + + override val subscribeExecutor: Executor = appExecutors.io + override val observeExecutor: Executor = appExecutors.main + + override fun doWork(params: Unit?): Single> { + return Single.just( + userManager.userProfiles.map { user -> + launcherApps.getActivityList(null, user) + .map { ApplicationItem(it, user, userManager.isQuietModeEnabled(user)) } + }.flatten() + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt new file mode 100644 index 0000000000..dc2fa424ad --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class MakePackageUnavailable @Inject constructor( + appExecutors: AppExecutors, + private val launcherRepository: LauncherRepository, + private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + observeUpdatedLauncherItems( + launcherRepository.makePackagesUnavailable( + params.packages, + params.user + ) + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt new file mode 100644 index 0000000000..a11f116818 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt @@ -0,0 +1,23 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import io.reactivex.Flowable +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ObserveAddedApps @Inject constructor( + appExecutors: AppExecutors, + private val launcherRepository: LauncherRepository +) : PublishSubjectInteractor, List>() { + + override val subscribeExecutor: Executor = appExecutors.io + + override val observeExecutor: Executor = appExecutors.main + + override fun createObservable(params: List): Flowable> = + Flowable.just(params) +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt new file mode 100644 index 0000000000..4f6126ed1f --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt @@ -0,0 +1,2 @@ +package foundation.e.blisslauncher.domain.interactor + diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt new file mode 100644 index 0000000000..4f6126ed1f --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt @@ -0,0 +1,2 @@ +package foundation.e.blisslauncher.domain.interactor + diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt new file mode 100644 index 0000000000..417032036c --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt @@ -0,0 +1,15 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.entity.LauncherItem +import io.reactivex.Flowable +import java.util.concurrent.Executor + +class ObserveUpdatedLauncherItems(appExecutors: AppExecutors) : + PublishSubjectInteractor, List>() { + override val subscribeExecutor: Executor = appExecutors.io + override val observeExecutor: Executor = appExecutors.main + + override fun createObservable(params: List): Flowable> = + Flowable.just(params) +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt new file mode 100644 index 0000000000..dda7ce499c --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt @@ -0,0 +1,26 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class RemovePackages @Inject constructor( + appExecutors: AppExecutors, + private val launcherRepository: LauncherRepository, + private val observeAddedApps: ObserveAddedApps +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + + launcherRepository.removePackages(params.packages, params.user) + }.doOnComplete { + observeAddedApps(Unit) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt new file mode 100644 index 0000000000..db453f6c24 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class SuspendPackages @Inject constructor( + appExecutors: AppExecutors, + private val launcherRepository: LauncherRepository, + private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + observeUpdatedLauncherItems( + launcherRepository.suspendPackages( + params.packages, + params.user + ) + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt new file mode 100644 index 0000000000..94c16db8b8 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt @@ -0,0 +1,28 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.os.UserHandle +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class UnsuspendPackages @Inject constructor( + appExecutors: AppExecutors, + private val launcherRepository: LauncherRepository, + private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + observeUpdatedLauncherItems( + launcherRepository.unsuspendPackages( + params.packages, + params.user + ) + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt new file mode 100644 index 0000000000..9a5c97d128 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt @@ -0,0 +1,162 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.content.ComponentName +import android.os.UserHandle +import android.util.ArrayMap +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.ItemInfoMatcher +import foundation.e.blisslauncher.domain.Matcher +import foundation.e.blisslauncher.domain.and +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.WorkspaceItem +import foundation.e.blisslauncher.domain.or +import foundation.e.blisslauncher.domain.repository.AppsRepository +import io.reactivex.Completable +import java.util.concurrent.Executor + +class UpdateLauncher( + appExecutors: AppExecutors, + private val appsRepository: AppsRepository, + private val launcherRepository: LauncherRepository, + private val launcherAppsCompat: LauncherAppsCompat, + private val deleteComponents: DeleteComponents +) : CompletableInteractor() { + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + val addedOrUpdated = ArrayList() + addedOrUpdated.addAll(appsRepository.getModifiedApps()) + addedOrUpdated.addAll(appsRepository.getAddedApps()) + + val removedApps = ArrayList(appsRepository.getRemovedApps()) + appsRepository.clear() + + val addedOrUpdatedApps = ArrayMap() + if (addedOrUpdated.isNotEmpty()) { + // TODO: Push updated apps here + addedOrUpdated.forEach { + addedOrUpdatedApps[it.componentName] = it + } + } + + // LauncherItems that are about to be removed + val removedItems = LongArrayMap() + + val isNewApkAvailable = params.command == Command.ADD || params.command == Command.UPDATE + val updatedItems = ArrayList() + val map = launcherRepository.allItemsMap() + map.forEach { + if (it is WorkspaceItem && params.user == it.user) it.let { workspaceItem -> + var itemUpdated = false + var shortcutUpdated = false + if (workspaceItem.iconResource != null && params.packages.contains(workspaceItem.iconResource!!.packageName)) { + //TODO: Update shortcut icon here + itemUpdated = true + } + + val cn = workspaceItem.getTargetComponent() + if (cn != null && params.matcher(workspaceItem, cn)) { + val applicationItem = addedOrUpdatedApps[cn] + if (workspaceItem.hasStatusFlag(WorkspaceItem.FLAG_SUPPORTS_WEB_UI)) { + removedItems.put(it.id, false) + if (params.command == Command.REMOVE) { + return@forEach + } + } + + if (workspaceItem.isPromise() && isNewApkAvailable) { + if(workspaceItem.hasStatusFlag(WorkspaceItem.FLAG_AUTOINSTALL_ICON)) { + if(launcherAppsCompat.isActivityEnabledForProfile(cn, params.user)) { + + } + } + } + + if (isNewApkAvailable && + workspaceItem.itemType == LauncherConstants.ItemType.APPLICATION) { + // update icon cache from tile + itemUpdated = true + } + + val oldRuntimeFlags = workspaceItem.runtimeStatusFlags + workspaceItem.runtimeStatusFlags = + params.flagOp(workspaceItem.runtimeStatusFlags) + if (oldRuntimeFlags != workspaceItem.runtimeStatusFlags) { + shortcutUpdated = true + } + } + + if (itemUpdated || shortcutUpdated) { + updatedItems.add(workspaceItem) + } + + if (itemUpdated) { + //TODO: Updated item in database here + } + + } + //TODO: Update launcher widgets here + } + + // TODO: Update Shortcut here + if (!removedItems.isEmpty) { + deleteComponents(Matcher.ofItemIds(removedItems, false)) + } + // TODO: Update or Add new apps here + // TODO: Update widgets here + + val removedPackages = HashSet() + val removedComponents = HashSet() + + if (params.command == Command.REMOVE) { + removedPackages.addAll(params.packages) + } else if (params.command == Command.UPDATE) { + params.packages.forEach { + if (!launcherAppsCompat.isPackageEnabledForProfile(it, params.user)) { + removedPackages.add(it) + } + } + + // Update removedComponents because some packages can get removed during package update + removedApps.forEach { + removedComponents.add(it.componentName) + } + } + + if (removedPackages.isNotEmpty() || removedComponents.isNotEmpty()) { + val removeMatch = Matcher.ofPackages(removedPackages, params.user) + .or(Matcher.ofComponents(removedComponents, params.user)) + .and(Matcher.ofItemIds(removedItems, true)) + deleteComponents(removeMatch) + + //TODO: Remove packages from InstallQueue + } + + if (Utilities.ATLEAST_OREO && params.command == Command.ADD) { + // Load widgets for the new package. Changes due to app updates are handled through + // AppWidgetHost events, this is just to initialize the long-press options. + /*for (i in 0 until N) { + dataModel.widgetsModel.update(app, PackageUserKey(packages.get(i), mUser)) + } + bindUpdatedWidgets(dataModel)*/ + //TODO: Update widget model here + } + } + + data class Params( + val command: Command, + val packages: HashSet, + val user: UserHandle, + val matcher: ItemInfoMatcher, + val flagOp: (flag: Int) -> Int + ) + + enum class Command { + ADD, UPDATE, REMOVE, UNAVAILABLE, SUSPEND, UNSUSPEND, USER_AVAILABILITY_CHANGE + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt new file mode 100644 index 0000000000..6b56e1d8ac --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.domain.interactor + +import android.content.Context +import android.os.UserHandle +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.AppsRepository +import io.reactivex.Completable +import java.util.concurrent.Executor +import javax.inject.Inject + +class UpdatePackages @Inject constructor( + appExecutors: AppExecutors, + private val context: Context, + private val appsRepository: AppsRepository, + private val observeAddedApps: ObserveAddedApps, + private val launcherAppsCompat: LauncherAppsCompat +) : CompletableInteractor() { + + class Params(val user: UserHandle, vararg val packages: String) + + override val subscribeExecutor: Executor = appExecutors.io + + override fun doWork(params: Params): Completable = Completable.fromAction { + params.packages.forEach { + // TODO: update icon cache + appsRepository.updateApp(context, it, params.user) + //TODO: Remove from widget cache + } + }.doOnComplete { + observeAddedApps(Unit) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactors/Interactor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactors/Interactor.kt deleted file mode 100644 index 144ad143f1..0000000000 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactors/Interactor.kt +++ /dev/null @@ -1,52 +0,0 @@ -package foundation.e.blisslauncher.domain.interactors - -import foundation.e.blisslauncher.domain.executors.PostExecutionThread -import foundation.e.blisslauncher.domain.executors.ThreadExecutor -import io.reactivex.Completable -import io.reactivex.Flowable -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.schedulers.Schedulers -import timber.log.Timber - -interface Interactor { - val threadExecutor: ThreadExecutor - val postExecutionThread: PostExecutionThread -} - -abstract class FlowableInteractor : Interactor

, Disposable { - - private val disposables: CompositeDisposable = CompositeDisposable() - - protected abstract fun buildObservable(params: P? = null): Flowable - - operator fun invoke(params: P, onNext: (next: T) -> Unit = {}, onComplete: () -> Unit = {}) { - disposables += this.buildObservable(params) - .subscribeOn(Schedulers.from(threadExecutor)) - .observeOn(postExecutionThread.scheduler) - .subscribe(onNext, Timber::w, onComplete) - } - - override fun dispose() = disposables.dispose() - - override fun isDisposed(): Boolean = disposables.isDisposed -} - -abstract class CompletableInteractor : Interactor

, Disposable { - - private val disposables: CompositeDisposable = CompositeDisposable() - - protected abstract fun buildObservable(params: P? = null): Completable - - operator fun invoke(params: P, onComplete: () -> Unit = {}) { - disposables += this.buildObservable(params) - .subscribeOn(Schedulers.from(threadExecutor)) - .observeOn(postExecutionThread.scheduler) - .subscribe(onComplete, Timber::w) - } - - override fun dispose() = disposables.dispose() - - override fun isDisposed(): Boolean = disposables.isDisposed -} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ComponentKey.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ComponentKey.kt new file mode 100644 index 0000000000..967d7eb51c --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ComponentKey.kt @@ -0,0 +1,19 @@ +package foundation.e.blisslauncher.domain.keys + +import android.content.ComponentName +import android.os.UserHandle + +open class ComponentKey(val componentName: ComponentName, val user: UserHandle) { + private val hashCode = arrayOf(componentName, user).contentHashCode() + + override fun hashCode(): Int = hashCode + + override fun equals(any: Any?): Boolean { + val other = any as ComponentKey + return (other.componentName == componentName) and (other.user == user) + } + + override fun toString(): String { + return "${componentName.flattenToString()}#$user" + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/keys/NotificationKey.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/NotificationKey.kt new file mode 100644 index 0000000000..7f640e2350 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/NotificationKey.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.domain.keys + +import android.service.notification.StatusBarNotification +import java.util.ArrayList + +/** + * The key data associated with the notification, used to determine what to include + * in badges and dummy popup views before they are populated. + * + * @see NotificationInfo for the full data used when populating the dummy views. + */ +class NotificationKey private constructor( + val notificationKey: String, + val shortcutId: String, + count: Int +) { + var count: Int + override fun equals(obj: Any?): Boolean { + return if (obj !is NotificationKey) { + false + } else obj.notificationKey == notificationKey + // Only compare the keys. + } + + companion object { + fun fromNotification(sbn: StatusBarNotification): NotificationKey { + val notif = sbn.notification + return NotificationKey(sbn.key, notif.shortcutId, notif.number) + } + + fun extractKeysOnly(notificationKeys: List): List { + val keysOnly: MutableList = + ArrayList(notificationKeys.size) + for (notificationKey in notificationKeys) { + keysOnly.add(notificationKey.notificationKey) + } + return keysOnly + } + } + + init { + this.count = Math.max(1, count) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/keys/PackageUserKey.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/PackageUserKey.kt new file mode 100644 index 0000000000..13d1042b1a --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/PackageUserKey.kt @@ -0,0 +1,43 @@ +package foundation.e.blisslauncher.domain.keys + +import android.os.UserHandle +import android.service.notification.StatusBarNotification +import foundation.e.blisslauncher.domain.entity.LauncherItem + +class PackageUserKey(var packageName: String?, var user: UserHandle?) { + private val hashCode: Int = arrayOf(packageName, user).contentHashCode() + + override fun hashCode(): Int = hashCode + + override fun equals(other: Any?): Boolean { + if (other !is PackageUserKey) return false + return other.packageName == packageName && other.user == user + } + + /** + * This should only be called to avoid new object creations in a loop. + * @return Whether this PackageUserKey was successfully updated - it shouldn't be used if not. + */ + fun updateFromItemInfo(info: LauncherItem): Boolean { + /*if (DeepShortcutManager.supportsShortcuts(info)) { + update(info.getTargetComponent().getPackageName(), info.user) + return true + } + return false*/ + TODO() + } + + companion object { + fun fromLauncherItem(item: LauncherItem): PackageUserKey = + PackageUserKey( + item.getTargetComponent()?.packageName, + item.user + ) + + fun fromNotification(sbn: StatusBarNotification): PackageUserKey = + PackageUserKey( + sbn.packageName, + sbn.user + ) + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/manager/LauncherStateManager.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/manager/LauncherStateManager.kt new file mode 100644 index 0000000000..499f25bbb8 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/manager/LauncherStateManager.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.domain.manager + +interface LauncherStateManager { + fun init() + fun terminate() +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt new file mode 100644 index 0000000000..193dbb15dc --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher.domain.repository + +import android.os.UserHandle +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.LauncherItem + +/** + * Loads and update all the LauncherItems + */ +interface LauncherRepository { + + fun getAllActivities(user: UserHandle, quietMode: Boolean): List + + /** + * Functions to fetch/add/update/remove AppsRepository + */ + fun add(packageName: String, user: UserHandle, quietMode: Boolean): List + + fun remove(packageName: String, user: UserHandle) + + fun updatedPackages( + packages: Array, + user: UserHandle, + quietMode: Boolean + ): List + + fun suspendPackages(packages: Array, user: UserHandle): List + + fun unsuspendPackages(packages: Array, user: UserHandle): List + + fun updateUserAvailability(user: UserHandle, quietMode: Boolean): List + + fun makePackagesUnavailable(packages: Array, user: UserHandle): List + + fun removePackages(packages: Array, user: UserHandle): List +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/UserManagerRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/UserManagerRepository.kt new file mode 100644 index 0000000000..715a3628f8 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/UserManagerRepository.kt @@ -0,0 +1,29 @@ +package foundation.e.blisslauncher.domain.repository + +import android.os.UserHandle + +interface UserManagerRepository { + + val userProfiles: List + val isDemoUser: Boolean + val isAnyProfileQuietModeEnabled: Boolean + + /** + * Creates a cache for users. + */ + fun enableAndResetCache() + + fun getSerialNumberForUser(user: UserHandle): Long + fun getUserForSerialNumber(serialNumber: Long): UserHandle? + fun getBadgedLabelForUser( + label: CharSequence, + user: UserHandle? + ): CharSequence + + fun isQuietModeEnabled(user: UserHandle): Boolean + fun isUserUnlocked(user: UserHandle): Boolean + fun requestQuietModeEnabled( + enableQuietMode: Boolean, + user: UserHandle? + ): Boolean +} diff --git a/domain/src/test/java/foundation/e/blisslauncher/domain/entity/LauncherItemTest.kt b/domain/src/test/java/foundation/e/blisslauncher/domain/entity/LauncherItemTest.kt new file mode 100644 index 0000000000..b787b515c2 --- /dev/null +++ b/domain/src/test/java/foundation/e/blisslauncher/domain/entity/LauncherItemTest.kt @@ -0,0 +1,18 @@ +package foundation.e.blisslauncher.domain.entity + +import org.junit.Test + +class LauncherItemTest { + + lateinit var launcherItem: LauncherItem + + @org.junit.Before + fun setUp() { + launcherItem = LauncherItem() + } + + @Test + fun testDumpProperties() { + launcherItem.toString() + } +} \ No newline at end of file diff --git a/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt b/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt new file mode 100644 index 0000000000..08855b1042 --- /dev/null +++ b/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt @@ -0,0 +1,88 @@ +package foundation.e.blisslauncher.domain.interactor + +import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.common.executors.MainThreadExecutor +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.verify +import io.reactivex.Single +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.util.concurrent.Executors + +class LoadAllAppsInteractorTest { + + private lateinit var loadAllAppsInteractor: LoadAllAppsInteractor + lateinit var launcherRepository: LauncherRepository + lateinit var appExecutors: AppExecutors + + @MockK + lateinit var mainThreadExecutor: MainThreadExecutor + + @Before + fun setUp() { + launcherRepository = mockk { + every { + getAllApps() + } returns Single.just(listOf( + mockk { + every { + title = "App1" + } + }, + mockk { + every { + title = "App2" + } + } + )) + } + MockKAnnotations.init(this) + appExecutors = AppExecutors( + Executors.newSingleThreadExecutor(), + Executors.newSingleThreadExecutor(), + mainThreadExecutor + ) + loadAllAppsInteractor = LoadAllAppsInteractor(launcherRepository, appExecutors) + } + + @After + fun tearDown() { + } + + @Test + fun doWorkCallsRepository() { + launcherRepository = mockk { + every { + getAllApps() + } returns Single.just(listOf( + mockk { + every { + title = "App1" + } + }, + mockk { + every { + title = "App2" + } + } + )) + } + loadAllAppsInteractor.doWork() + verify(exactly = 1) { launcherRepository.getAllApps() } + } + + @Test + fun invokeCallsRepositoryAndCompletes() { + + loadAllAppsInteractor() + verify(exactly = 1) { launcherRepository.getAllApps() } + + loadAllAppsInteractor(onSuccess = { Assert.assertEquals(2, it.size) }) + } +} \ No newline at end of file diff --git a/quickstep/.gitignore b/quickstep/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/quickstep/.gitignore @@ -0,0 +1 @@ +/build diff --git a/quickstep/build.gradle b/quickstep/build.gradle new file mode 100644 index 0000000000..f241c0b840 --- /dev/null +++ b/quickstep/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.0" + + + defaultConfig { + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +final String SUPPORT_LIBS_VERSION = '28.0.0' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "com.android.support:support-v4:${SUPPORT_LIBS_VERSION}" + +} diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar new file mode 100644 index 0000000000000000000000000000000000000000..4ed224193b74f7cf4c785e95930b8ca44ad0e0e7 GIT binary patch literal 118069 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 z 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 z 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ℑ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 AQ~&?~ literal 0 HcmV?d00001 diff --git a/quickstep/proguard-rules.pro b/quickstep/proguard-rules.pro new file mode 100644 index 0000000000..2f9dc5a47e --- /dev/null +++ b/quickstep/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/quickstep/src/androidTest/java/foundation/e/blisslauncher/quickstep/ExampleInstrumentedTest.java b/quickstep/src/androidTest/java/foundation/e/blisslauncher/quickstep/ExampleInstrumentedTest.java new file mode 100644 index 0000000000..637b472d81 --- /dev/null +++ b/quickstep/src/androidTest/java/foundation/e/blisslauncher/quickstep/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package foundation.e.blisslauncher.quickstep; + +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.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("foundation.e.blisslauncher.quickstep.test", appContext.getPackageName()); + } +} diff --git a/quickstep/src/main/AndroidManifest.xml b/quickstep/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ff0a74d66e --- /dev/null +++ b/quickstep/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAnimationRunner.java b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAnimationRunner.java new file mode 100644 index 0000000000..e0ca4935fb --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAnimationRunner.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.BinderThread; +import android.support.annotation.UiThread; + +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously; + +@TargetApi(Build.VERSION_CODES.P) +public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { + + private static final int SINGLE_FRAME_MS = 16; + + private final Handler mHandler; + private final boolean mStartAtFrontOfQueue; + private AnimationResult mAnimationResult; + + /** + * @param startAtFrontOfQueue If true, the animation start will be posted at the front of the + * queue to minimize latency. + */ + public LauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue) { + mHandler = handler; + mStartAtFrontOfQueue = startAtFrontOfQueue; + } + + @BinderThread + @Override + public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats, Runnable runnable) { + Runnable r = () -> { + finishExistingAnimation(); + mAnimationResult = new AnimationResult(runnable); + onCreateAnimation(targetCompats, mAnimationResult); + }; + if (mStartAtFrontOfQueue) { + postAtFrontOfQueueAsynchronously(mHandler, r); + } else { + postAsyncCallback(mHandler, r); + } + } + + /** + * Called on the UI thread when the animation targets are received. The implementation must + * call {@link AnimationResult#setAnimation(AnimatorSet)} with the target animation to be run. + */ + @UiThread + public abstract void onCreateAnimation( + RemoteAnimationTargetCompat[] targetCompats, AnimationResult result); + + @UiThread + private void finishExistingAnimation() { + if (mAnimationResult != null) { + mAnimationResult.finish(); + mAnimationResult = null; + } + } + + /** + * Called by the system + */ + @BinderThread + @Override + public void onAnimationCancelled() { + postAsyncCallback(mHandler, this::finishExistingAnimation); + } + + public static final class AnimationResult { + + private final Runnable mFinishRunnable; + + private AnimatorSet mAnimator; + private boolean mFinished = false; + private boolean mInitialized = false; + + private AnimationResult(Runnable finishRunnable) { + mFinishRunnable = finishRunnable; + } + + @UiThread + private void finish() { + if (!mFinished) { + mFinishRunnable.run(); + mFinished = true; + } + } + + @UiThread + public void setAnimation(AnimatorSet animation) { + if (mInitialized) { + throw new IllegalStateException("Animation already initialized"); + } + mInitialized = true; + mAnimator = animation; + if (mAnimator == null) { + finish(); + } else if (mFinished) { + // Animation callback was already finished, skip the animation. + mAnimator.start(); + mAnimator.end(); + } else { + // Start the animation + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finish(); + } + }); + mAnimator.start(); + + // Because t=0 has the app icon in its original spot, we can skip the + // first frame and have the same movement one frame earlier. + mAnimator.setCurrentPlayTime(SINGLE_FRAME_MS); + } + } + } + + /** + * Utility method to post a runnable on the handler, skipping the synchronization barriers. + */ + private void postAsyncCallback(Handler handler, Runnable callback) { + Message msg = Message.obtain(handler, callback); + msg.setAsynchronous(true); + handler.sendMessage(msg); + } +} \ No newline at end of file diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAppTransitionManagerImpl.java b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAppTransitionManagerImpl.java new file mode 100644 index 0000000000..714c2966ab --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherAppTransitionManagerImpl.java @@ -0,0 +1,816 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; +import android.util.Pair; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.InsettableFrameLayout.LayoutParams; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.graphics.DrawableFactory; +import com.android.launcher3.shortcuts.DeepShortcutView; +import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.RemoteAnimationProvider; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.ActivityCompat; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat; +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier.SurfaceParams; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import static com.android.launcher3.BaseActivity.INVISIBLE_ALL; +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS; +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS; +import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; +import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE; +import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS; +import static com.android.quickstep.TaskUtils.findTaskViewToLaunch; +import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator; +import static com.android.quickstep.TaskUtils.taskIsATargetWithMode; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +/** + * Manages the opening and closing app transitions from Launcher. + */ +@TargetApi(Build.VERSION_CODES.O) +@SuppressWarnings("unused") +public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManager + implements OnDeviceProfileChangeListener { + + private static final String TAG = "LauncherTransition"; + + /** Duration of status bar animations. */ + public static final int STATUS_BAR_TRANSITION_DURATION = 120; + + /** + * Since our animations decelerate heavily when finishing, we want to start status bar animations + * x ms before the ending. + */ + public static final int STATUS_BAR_TRANSITION_PRE_DELAY = 96; + + private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION = + "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"; + + private static final int APP_LAUNCH_DURATION = 500; + // Use a shorter duration for x or y translation to create a curve effect + private static final int APP_LAUNCH_CURVED_DURATION = APP_LAUNCH_DURATION / 2; + // We scale the durations for the downward app launch animations (minus the scale animation). + private static final float APP_LAUNCH_DOWN_DUR_SCALE_FACTOR = 0.8f; + private static final int APP_LAUNCH_ALPHA_START_DELAY = 32; + private static final int APP_LAUNCH_ALPHA_DURATION = 50; + + public static final int RECENTS_LAUNCH_DURATION = 336; + public static final int RECENTS_QUICKSCRUB_LAUNCH_DURATION = 300; + private static final int LAUNCHER_RESUME_START_DELAY = 100; + private static final int CLOSING_TRANSITION_DURATION_MS = 250; + + // Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down. + public static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f; + + private final Launcher mLauncher; + private final DragLayer mDragLayer; + private final AlphaProperty mDragLayerAlpha; + + private final Handler mHandler; + private final boolean mIsRtl; + + private final float mContentTransY; + private final float mWorkspaceTransY; + private final float mClosingWindowTransY; + + private DeviceProfile mDeviceProfile; + private View mFloatingView; + + private RemoteAnimationProvider mRemoteAnimationProvider; + + private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mLauncher.addForceInvisibleFlag(INVISIBLE_BY_APP_TRANSITIONS); + } + + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.clearForceInvisibleFlag(INVISIBLE_BY_APP_TRANSITIONS); + } + }; + + public LauncherAppTransitionManagerImpl(Context context) { + mLauncher = Launcher.getLauncher(context); + mDragLayer = mLauncher.getDragLayer(); + mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS); + mHandler = new Handler(Looper.getMainLooper()); + mIsRtl = Utilities.isRtl(mLauncher.getResources()); + mDeviceProfile = mLauncher.getDeviceProfile(); + + Resources res = mLauncher.getResources(); + mContentTransY = res.getDimensionPixelSize(R.dimen.content_trans_y); + mWorkspaceTransY = res.getDimensionPixelSize(R.dimen.workspace_trans_y); + mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y); + + mLauncher.addOnDeviceProfileChangeListener(this); + registerRemoteAnimations(); + } + + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + mDeviceProfile = dp; + } + + /** + * @return ActivityOptions with remote animations that controls how the window of the opening + * targets are displayed. + */ + @Override + public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) { + if (hasControlRemoteAppTransitionPermission()) { + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler, + true /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + AnimatorSet anim = new AnimatorSet(); + + boolean launcherClosing = + launcherIsATargetWithMode(targetCompats, MODE_CLOSING); + + if (!composeRecentsLaunchAnimator(v, targetCompats, anim)) { + // Set the state animation first so that any state listeners are called + // before our internal listeners. + mLauncher.getStateManager().setCurrentAnimation(anim); + + Rect windowTargetBounds = getWindowTargetBounds(targetCompats); + playIconAnimators(anim, v, windowTargetBounds); + if (launcherClosing) { + Pair launcherContentAnimator = + getLauncherContentAnimator(true /* isAppOpening */); + anim.play(launcherContentAnimator.first); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + launcherContentAnimator.second.run(); + } + }); + } + anim.play(getOpeningWindowAnimators(v, targetCompats, windowTargetBounds)); + } + + if (launcherClosing) { + anim.addListener(mForceInvisibleListener); + } + + result.setAnimation(anim); + } + }; + + boolean fromRecents = mLauncher.getStateManager().getState().overviewUi + && findTaskViewToLaunch(launcher, v, null) != null; + int duration = fromRecents + ? RECENTS_LAUNCH_DURATION + : APP_LAUNCH_DURATION; + + int statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION + - STATUS_BAR_TRANSITION_PRE_DELAY; + return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat( + runner, duration, statusBarTransitionDelay)); + } + return super.getActivityLaunchOptions(launcher, v); + } + + /** + * Return the window bounds of the opening target. + * In multiwindow mode, we need to get the final size of the opening app window target to help + * figure out where the floating view should animate to. + */ + private Rect getWindowTargetBounds(RemoteAnimationTargetCompat[] targets) { + Rect bounds = new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); + if (mLauncher.isInMultiWindowModeCompat()) { + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == MODE_OPENING) { + bounds.set(target.sourceContainerBounds); + bounds.offsetTo(target.position.x, target.position.y); + return bounds; + } + } + } + return bounds; + } + + public void setRemoteAnimationProvider(final RemoteAnimationProvider animationProvider, + CancellationSignal cancellationSignal) { + mRemoteAnimationProvider = animationProvider; + cancellationSignal.setOnCancelListener(() -> { + if (animationProvider == mRemoteAnimationProvider) { + mRemoteAnimationProvider = null; + } + }); + } + + /** + * Composes the animations for a launch from the recents list if possible. + */ + private boolean composeRecentsLaunchAnimator(View v, + RemoteAnimationTargetCompat[] targets, AnimatorSet target) { + // Ensure recents is actually visible + if (!mLauncher.getStateManager().getState().overviewUi) { + return false; + } + + RecentsView recentsView = mLauncher.getOverviewPanel(); + boolean launcherClosing = launcherIsATargetWithMode(targets, MODE_CLOSING); + boolean skipLauncherChanges = !launcherClosing; + boolean isLaunchingFromQuickscrub = + recentsView.getQuickScrubController().isWaitingForTaskLaunch(); + + TaskView taskView = findTaskViewToLaunch(mLauncher, v, targets); + if (taskView == null) { + return false; + } + + int duration = isLaunchingFromQuickscrub + ? RECENTS_QUICKSCRUB_LAUNCH_DURATION + : RECENTS_LAUNCH_DURATION; + + ClipAnimationHelper helper = new ClipAnimationHelper(); + target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets, helper) + .setDuration(duration)); + + Animator childStateAnimation = null; + // Found a visible recents task that matches the opening app, lets launch the app from there + Animator launcherAnim; + final AnimatorListenerAdapter windowAnimEndListener; + if (launcherClosing) { + launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView, helper); + launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); + launcherAnim.setDuration(duration); + + // Make sure recents gets fixed up by resetting task alphas and scales, etc. + windowAnimEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.getStateManager().moveToRestState(); + mLauncher.getStateManager().reapplyState(); + } + }; + } else { + AnimatorPlaybackController controller = + mLauncher.getStateManager().createAnimationToNewWorkspace(NORMAL, duration); + controller.dispatchOnStart(); + childStateAnimation = controller.getTarget(); + launcherAnim = controller.getAnimationPlayer().setDuration(duration); + windowAnimEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.getStateManager().goToState(NORMAL, false); + } + }; + } + target.play(launcherAnim); + + // Set the current animation first, before adding windowAnimEndListener. Setting current + // animation adds some listeners which need to be called before windowAnimEndListener + // (the ordering of listeners matter in this case). + mLauncher.getStateManager().setCurrentAnimation(target, childStateAnimation); + target.addListener(windowAnimEndListener); + return true; + } + + /** + * Content is everything on screen except the background and the floating view (if any). + * + * @param isAppOpening True when this is called when an app is opening. + * False when this is called when an app is closing. + */ + private Pair getLauncherContentAnimator(boolean isAppOpening) { + AnimatorSet launcherAnimator = new AnimatorSet(); + Runnable endListener; + + float[] alphas = isAppOpening + ? new float[] {1, 0} + : new float[] {0, 1}; + float[] trans = isAppOpening + ? new float[] {0, mContentTransY} + : new float[] {-mContentTransY, 0}; + + if (mLauncher.isInState(ALL_APPS)) { + // All Apps in portrait mode is full screen, so we only animate AllAppsContainerView. + final View appsView = mLauncher.getAppsView(); + final float startAlpha = appsView.getAlpha(); + final float startY = appsView.getTranslationY(); + appsView.setAlpha(alphas[0]); + appsView.setTranslationY(trans[0]); + + ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + appsView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + alpha.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + appsView.setLayerType(View.LAYER_TYPE_NONE, null); + } + }); + ObjectAnimator transY = ObjectAnimator.ofFloat(appsView, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + + launcherAnimator.play(alpha); + launcherAnimator.play(transY); + + endListener = () -> { + appsView.setAlpha(startAlpha); + appsView.setTranslationY(startY); + appsView.setLayerType(View.LAYER_TYPE_NONE, null); + }; + } else if (mLauncher.isInState(OVERVIEW)) { + AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); + launcherAnimator.play(ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS, + allAppsController.getProgress(), ALL_APPS_PROGRESS_OFF_SCREEN)); + + RecentsView overview = mLauncher.getOverviewPanel(); + ObjectAnimator alpha = ObjectAnimator.ofFloat(overview, + RecentsView.CONTENT_ALPHA, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + launcherAnimator.play(alpha); + + ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + launcherAnimator.play(transY); + + endListener = mLauncher.getStateManager()::reapplyState; + } else { + mDragLayerAlpha.setValue(alphas[0]); + ObjectAnimator alpha = + ObjectAnimator.ofFloat(mDragLayerAlpha, MultiValueAlpha.VALUE, alphas); + alpha.setDuration(217); + alpha.setInterpolator(LINEAR); + launcherAnimator.play(alpha); + + mDragLayer.setTranslationY(trans[0]); + ObjectAnimator transY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, trans); + transY.setInterpolator(AGGRESSIVE_EASE); + transY.setDuration(350); + launcherAnimator.play(transY); + + mDragLayer.getScrim().hideSysUiScrim(true); + // Pause page indicator animations as they lead to layer trashing. + mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); + mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + endListener = this::resetContentView; + } + return new Pair<>(launcherAnimator, endListener); + } + + /** + * Animators for the "floating view" of the view used to launch the target. + */ + private void playIconAnimators(AnimatorSet appOpenAnimator, View v, Rect windowTargetBounds) { + final boolean isBubbleTextView = v instanceof BubbleTextView; + mFloatingView = new View(mLauncher); + if (isBubbleTextView && v.getTag() instanceof ItemInfoWithIcon ) { + // Create a copy of the app icon + mFloatingView.setBackground( + DrawableFactory.get(mLauncher).newIcon((ItemInfoWithIcon) v.getTag())); + } + + // Position the floating view exactly on top of the original + Rect rect = new Rect(); + final boolean fromDeepShortcutView = v.getParent() instanceof DeepShortcutView; + if (fromDeepShortcutView) { + // Deep shortcut views have their icon drawn in a separate view. + DeepShortcutView view = (DeepShortcutView) v.getParent(); + mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), rect); + } else { + mDragLayer.getDescendantRectRelativeToSelf(v, rect); + } + int viewLocationLeft = rect.left; + int viewLocationTop = rect.top; + + float startScale = 1f; + if (isBubbleTextView && !fromDeepShortcutView) { + BubbleTextView btv = (BubbleTextView) v; + btv.getIconBounds(rect); + Drawable dr = btv.getIcon(); + if (dr instanceof FastBitmapDrawable) { + startScale = ((FastBitmapDrawable) dr).getAnimatedScale(); + } + } else { + rect.set(0, 0, rect.width(), rect.height()); + } + viewLocationLeft += rect.left; + viewLocationTop += rect.top; + int viewLocationStart = mIsRtl + ? windowTargetBounds.width() - rect.right + : viewLocationLeft; + LayoutParams lp = new LayoutParams(rect.width(), rect.height()); + lp.ignoreInsets = true; + lp.setMarginStart(viewLocationStart); + lp.topMargin = viewLocationTop; + mFloatingView.setLayoutParams(lp); + + // Set the properties here already to make sure they'are available when running the first + // animation frame. + mFloatingView.setLeft(viewLocationLeft); + mFloatingView.setTop(viewLocationTop); + mFloatingView.setRight(viewLocationLeft + rect.width()); + mFloatingView.setBottom(viewLocationTop + rect.height()); + + // Swap the two views in place. + ((ViewGroup) mDragLayer.getParent()).addView(mFloatingView); + v.setVisibility(View.INVISIBLE); + + int[] dragLayerBounds = new int[2]; + mDragLayer.getLocationOnScreen(dragLayerBounds); + + // Animate the app icon to the center of the window bounds in screen coordinates. + float centerX = windowTargetBounds.centerX() - dragLayerBounds[0]; + float centerY = windowTargetBounds.centerY() - dragLayerBounds[1]; + + float xPosition = mIsRtl + ? windowTargetBounds.width() - lp.getMarginStart() - rect.width() + : lp.getMarginStart(); + float dX = centerX - xPosition - (lp.width / 2); + float dY = centerY - lp.topMargin - (lp.height / 2); + + ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX); + ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY); + + // Use upward animation for apps that are either on the bottom half of the screen, or are + // relatively close to the center. + boolean useUpwardAnimation = lp.topMargin > centerY + || Math.abs(dY) < mLauncher.getDeviceProfile().cellHeightPx; + if (useUpwardAnimation) { + x.setDuration(APP_LAUNCH_CURVED_DURATION); + y.setDuration(APP_LAUNCH_DURATION); + } else { + x.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_DURATION)); + y.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_CURVED_DURATION)); + } + x.setInterpolator(AGGRESSIVE_EASE); + y.setInterpolator(AGGRESSIVE_EASE); + appOpenAnimator.play(x); + appOpenAnimator.play(y); + + // Scale the app icon to take up the entire screen. This simplifies the math when + // animating the app window position / scale. + float maxScaleX = windowTargetBounds.width() / (float) rect.width(); + float maxScaleY = windowTargetBounds.height() / (float) rect.height(); + float scale = Math.max(maxScaleX, maxScaleY); + ObjectAnimator scaleAnim = ObjectAnimator + .ofFloat(mFloatingView, SCALE_PROPERTY, startScale, scale); + scaleAnim.setDuration(APP_LAUNCH_DURATION) + .setInterpolator(Interpolators.EXAGGERATED_EASE); + appOpenAnimator.play(scaleAnim); + + // Fade out the app icon. + ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f); + if (useUpwardAnimation) { + alpha.setStartDelay(APP_LAUNCH_ALPHA_START_DELAY); + alpha.setDuration(APP_LAUNCH_ALPHA_DURATION); + } else { + alpha.setStartDelay((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR + * APP_LAUNCH_ALPHA_START_DELAY)); + alpha.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_ALPHA_DURATION)); + } + alpha.setInterpolator(LINEAR); + appOpenAnimator.play(alpha); + + appOpenAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Reset launcher to normal state + v.setVisibility(View.VISIBLE); + ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView); + } + }); + } + + /** + * @return Animator that controls the window of the opening targets. + */ + private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets, + Rect windowTargetBounds) { + Rect bounds = new Rect(); + if (v.getParent() instanceof DeepShortcutView) { + // Deep shortcut views have their icon drawn in a separate view. + DeepShortcutView view = (DeepShortcutView) v.getParent(); + mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), bounds); + } else if (v instanceof BubbleTextView) { + ((BubbleTextView) v).getIconBounds(bounds); + } else { + mDragLayer.getDescendantRectRelativeToSelf(v, bounds); + } + int[] floatingViewBounds = new int[2]; + + Rect crop = new Rect(); + Matrix matrix = new Matrix(); + + RemoteAnimationTargetSet openingTargets = new RemoteAnimationTargetSet(targets, + MODE_OPENING); + RemoteAnimationTargetSet closingTargets = new RemoteAnimationTargetSet(targets, + MODE_CLOSING); + SyncRtSurfaceTransactionApplier surfaceApplier = new SyncRtSurfaceTransactionApplier( + mFloatingView); + + ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); + appAnimator.setDuration(APP_LAUNCH_DURATION); + appAnimator.addUpdateListener(new MultiValueUpdateListener() { + // Fade alpha for the app window. + FloatProp mAlpha = new FloatProp(0f, 1f, 0, 60, LINEAR); + + @Override + public void onUpdate(float percent) { + final float easePercent = AGGRESSIVE_EASE.getInterpolation(percent); + + // Calculate app icon size. + float iconWidth = bounds.width() * mFloatingView.getScaleX(); + float iconHeight = bounds.height() * mFloatingView.getScaleY(); + + // Scale the app window to match the icon size. + float scaleX = iconWidth / windowTargetBounds.width(); + float scaleY = iconHeight / windowTargetBounds.height(); + float scale = Math.min(1f, Math.min(scaleX, scaleY)); + + // Position the scaled window on top of the icon + int windowWidth = windowTargetBounds.width(); + int windowHeight = windowTargetBounds.height(); + float scaledWindowWidth = windowWidth * scale; + float scaledWindowHeight = windowHeight * scale; + + float offsetX = (scaledWindowWidth - iconWidth) / 2; + float offsetY = (scaledWindowHeight - iconHeight) / 2; + mFloatingView.getLocationOnScreen(floatingViewBounds); + + float transX0 = floatingViewBounds[0] - offsetX; + float transY0 = floatingViewBounds[1] - offsetY; + + // Animate the window crop so that it starts off as a square, and then reveals + // horizontally. + float cropHeight = windowHeight * easePercent + windowWidth * (1 - easePercent); + float initialTop = (windowHeight - windowWidth) / 2f; + crop.left = 0; + crop.top = (int) (initialTop * (1 - easePercent)); + crop.right = windowWidth; + crop.bottom = (int) (crop.top + cropHeight); + + SurfaceParams[] params = new SurfaceParams[targets.length]; + for (int i = targets.length - 1; i >= 0; i--) { + RemoteAnimationTargetCompat target = targets[i]; + + Rect targetCrop; + float alpha; + if (target.mode == MODE_OPENING) { + matrix.setScale(scale, scale); + matrix.postTranslate(transX0, transY0); + targetCrop = crop; + alpha = mAlpha.value; + } else { + matrix.setTranslate(target.position.x, target.position.y); + alpha = 1f; + targetCrop = target.sourceContainerBounds; + } + + params[i] = new SurfaceParams(target.leash, alpha, matrix, targetCrop, + RemoteAnimationProvider.getLayer(target, MODE_OPENING)); + } + surfaceApplier.scheduleApply(params); + } + }); + return appAnimator; + } + + /** + * Registers remote animations used when closing apps to home screen. + */ + private void registerRemoteAnimations() { + // Unregister this + if (hasControlRemoteAppTransitionPermission()) { + RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat(); + definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN, + WindowManagerWrapper.ACTIVITY_TYPE_STANDARD, + new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(), + CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */)); + + // TODO: Transition for unlock to home TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER + new ActivityCompat(mLauncher).registerRemoteAnimations(definition); + } + } + + private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) { + return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode); + } + + /** + * @return Runner that plays when user goes to Launcher + * ie. pressing home, swiping up from nav bar. + */ + private RemoteAnimationRunnerCompat getWallpaperOpenRunner() { + return new LauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */) { + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + if (!mLauncher.hasBeenResumed()) { + // If launcher is not resumed, wait until new async-frame after resume + mLauncher.setOnResumeCallback(() -> + postAsyncCallback(mHandler, () -> + onCreateAnimation(targetCompats, result))); + return; + } + + if (mLauncher.hasSomeInvisibleFlag(PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION)) { + mLauncher.addForceInvisibleFlag(INVISIBLE_BY_PENDING_FLAGS); + mLauncher.getStateManager().moveToRestState(); + } + + AnimatorSet anim = null; + RemoteAnimationProvider provider = mRemoteAnimationProvider; + if (provider != null) { + anim = provider.createWindowAnimation(targetCompats); + } + + if (anim == null) { + anim = new AnimatorSet(); + anim.play(getClosingWindowAnimators(targetCompats)); + + // Normally, we run the launcher content animation when we are transitioning + // home, but if home is already visible, then we don't want to animate the + // contents of launcher unless we know that we are animating home as a result + // of the home button press with quickstep, which will result in launcher being + // started on touch down, prior to the animation home (and won't be in the + // targets list because it is already visible). In that case, we force + // invisibility on touch down, and only reset it after the animation to home + // is initialized. + if (launcherIsATargetWithMode(targetCompats, MODE_OPENING) + || mLauncher.isForceInvisible()) { + // Only register the content animation for cancellation when state changes + mLauncher.getStateManager().setCurrentAnimation(anim); + createLauncherResumeAnimation(anim); + } + } + + mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL); + result.setAnimation(anim); + } + }; + } + + /** + * Animator that controls the transformations of the windows the targets that are closing. + */ + private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] targets) { + SyncRtSurfaceTransactionApplier surfaceApplier = + new SyncRtSurfaceTransactionApplier(mDragLayer); + Matrix matrix = new Matrix(); + ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1); + int duration = CLOSING_TRANSITION_DURATION_MS; + closingAnimator.setDuration(duration); + closingAnimator.addUpdateListener(new MultiValueUpdateListener() { + FloatProp mDy = new FloatProp(0, mClosingWindowTransY, 0, duration, DEACCEL_1_7); + FloatProp mScale = new FloatProp(1f, 1f, 0, duration, DEACCEL_1_7); + FloatProp mAlpha = new FloatProp(1f, 0f, 25, 125, LINEAR); + + @Override + public void onUpdate(float percent) { + SurfaceParams[] params = new SurfaceParams[targets.length]; + for (int i = targets.length - 1; i >= 0; i--) { + RemoteAnimationTargetCompat target = targets[i]; + float alpha; + if (target.mode == MODE_CLOSING) { + matrix.setScale(mScale.value, mScale.value, + target.sourceContainerBounds.centerX(), + target.sourceContainerBounds.centerY()); + matrix.postTranslate(0, mDy.value); + matrix.postTranslate(target.position.x, target.position.y); + alpha = mAlpha.value; + } else { + matrix.setTranslate(target.position.x, target.position.y); + alpha = 1f; + } + params[i] = new SurfaceParams(target.leash, alpha, matrix, + target.sourceContainerBounds, + RemoteAnimationProvider.getLayer(target, MODE_CLOSING)); + } + surfaceApplier.scheduleApply(params); + } + }); + + return closingAnimator; + } + + /** + * Creates an animator that modifies Launcher as a result from {@link #getWallpaperOpenRunner}. + */ + private void createLauncherResumeAnimation(AnimatorSet anim) { + if (mLauncher.isInState(LauncherState.ALL_APPS)) { + Pair contentAnimator = + getLauncherContentAnimator(false /* isAppOpening */); + contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY); + anim.play(contentAnimator.first); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + contentAnimator.second.run(); + } + }); + } else { + AnimatorSet workspaceAnimator = new AnimatorSet(); + + mDragLayer.setTranslationY(-mWorkspaceTransY);; + workspaceAnimator.play(ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, + -mWorkspaceTransY, 0)); + + mDragLayerAlpha.setValue(0); + workspaceAnimator.play(ObjectAnimator.ofFloat( + mDragLayerAlpha, MultiValueAlpha.VALUE, 0, 1f)); + + workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY); + workspaceAnimator.setDuration(333); + workspaceAnimator.setInterpolator(Interpolators.DEACCEL_1_7); + + mDragLayer.getScrim().hideSysUiScrim(true); + + // Pause page indicator animations as they lead to layer trashing. + mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); + mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + workspaceAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + resetContentView(); + } + }); + anim.play(workspaceAnimator); + } + } + + private void resetContentView() { + mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd(); + mDragLayerAlpha.setValue(1f); + mDragLayer.setLayerType(View.LAYER_TYPE_NONE, null); + mDragLayer.setTranslationY(0f); + mDragLayer.getScrim().hideSysUiScrim(false); + } + + private boolean hasControlRemoteAppTransitionPermission() { + return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION) + == PackageManager.PERMISSION_GRANTED; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/LauncherInitListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherInitListener.java new file mode 100644 index 0000000000..4bbc872a72 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/LauncherInitListener.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; + +import com.android.launcher3.states.InternalStateHandler; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.OverviewCallbacks; +import com.android.quickstep.util.RemoteAnimationProvider; + +import java.util.function.BiPredicate; + +@TargetApi(Build.VERSION_CODES.P) +public class LauncherInitListener extends InternalStateHandler implements ActivityInitListener { + + private final BiPredicate mOnInitListener; + + private RemoteAnimationProvider mRemoteAnimationProvider; + + public LauncherInitListener(BiPredicate onInitListener) { + mOnInitListener = onInitListener; + } + + @Override + protected boolean init(Launcher launcher, boolean alreadyOnHome) { + if (mRemoteAnimationProvider != null) { + LauncherAppTransitionManagerImpl appTransitionManager = + (LauncherAppTransitionManagerImpl) launcher.getAppTransitionManager(); + + // Set a one-time animation provider. After the first call, this will get cleared. + // TODO: Probably also check the intended target id. + CancellationSignal cancellationSignal = new CancellationSignal(); + appTransitionManager.setRemoteAnimationProvider((targets) -> { + + // On the first call clear the reference. + cancellationSignal.cancel(); + RemoteAnimationProvider provider = mRemoteAnimationProvider; + mRemoteAnimationProvider = null; + + if (provider != null && launcher.getStateManager().getState().overviewUi) { + return provider.createWindowAnimation(targets); + } + return null; + }, cancellationSignal); + } + OverviewCallbacks.get(launcher).onInitOverviewTransition(); + return mOnInitListener.test(launcher, alreadyOnHome); + } + + @Override + public void register() { + initWhenReady(); + } + + @Override + public void unregister() { + mRemoteAnimationProvider = null; + clearReference(); + } + + @Override + public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration) { + mRemoteAnimationProvider = animProvider; + + register(); + + Bundle options = animProvider.toActivityOptions(handler, duration).toBundle(); + context.startActivity(addToIntent(new Intent((intent))), options); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/ActivityControlHelper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/ActivityControlHelper.java new file mode 100644 index 0000000000..21eaf159f0 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/ActivityControlHelper.java @@ -0,0 +1,615 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.view.View; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherInitListener; +import com.android.launcher3.LauncherState; +import com.android.launcher3.R; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.uioverrides.FastOverviewState; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.quickstep.TouchConsumer.InteractionType; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.util.RemoteAnimationProvider; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.util.TransformedRect; +import com.android.quickstep.views.LauncherLayoutListener; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Consumer; + +import static android.view.View.TRANSLATION_Y; +import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.FAST_OVERVIEW; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; +import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_ROTATION; + +/** + * Utility class which abstracts out the logical differences between Launcher and RecentsActivity. + */ +@TargetApi(Build.VERSION_CODES.P) +public interface ActivityControlHelper { + + LayoutListener createLayoutListener(T activity); + + /** + * Updates the UI to indicate quick interaction. + */ + void onQuickInteractionStart(T activity, @Nullable ActivityManager.RunningTaskInfo taskInfo, + boolean activityVisible); + + float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context); + + void executeOnWindowAvailable(T activity, Runnable action); + + void onTransitionCancelled(T activity, boolean activityVisible); + + int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect); + + void onSwipeUpComplete(T activity); + + AnimationFactory prepareRecentsUI(T activity, boolean activityVisible, + Consumer callback); + + ActivityInitListener createActivityInitListener(BiPredicate onInitListener); + + @Nullable + T getCreatedActivity(); + + @UiThread + @Nullable + RecentsView getVisibleRecentsView(); + + @UiThread + boolean switchToRecentsIfVisible(boolean fromRecentsButton); + + Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target); + + boolean shouldMinimizeSplitScreen(); + + /** + * @return {@code true} if recents activity should be started immediately on touchDown, + * {@code false} if it should deferred until some threshold is crossed. + */ + boolean deferStartingActivity(int downHitTarget); + + boolean supportsLongSwipe(T activity); + + AlphaProperty getAlphaProperty(T activity); + + /** + * Must return a non-null controller is supportsLongSwipe was true. + */ + LongSwipeHelper getLongSwipeController(T activity, RemoteAnimationTargetSet targetSet); + + /** + * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher} + */ + int getContainerType(); + + class LauncherActivityControllerHelper implements ActivityControlHelper { + + @Override + public LayoutListener createLayoutListener(Launcher activity) { + return new LauncherLayoutListener(activity); + } + + @Override + public void onQuickInteractionStart(Launcher activity, RunningTaskInfo taskInfo, + boolean activityVisible) { + LauncherState fromState = activity.getStateManager().getState(); + activity.getStateManager().goToState(FAST_OVERVIEW, activityVisible); + + QuickScrubController controller = activity.getOverviewPanel() + .getQuickScrubController(); + controller.onQuickScrubStart(activityVisible && !fromState.overviewUi, this); + } + + @Override + public float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context) { + // The padding calculations are exactly same as that of RecentsView.setInsets + int topMargin = context.getResources() + .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + int paddingTop = targetRect.rect.top - topMargin - dp.getInsets().top; + int paddingBottom = dp.availableHeightPx + dp.getInsets().top - targetRect.rect.bottom; + + return FastOverviewState.OVERVIEW_TRANSLATION_FACTOR * (paddingBottom - paddingTop); + } + + @Override + public void executeOnWindowAvailable(Launcher activity, Runnable action) { + activity.getWorkspace().runOnOverlayHidden(action); + } + + @Override + public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect) { + LayoutUtils.calculateLauncherTaskSize(context, dp, outRect.rect); + if (interactionType == INTERACTION_QUICK_SCRUB) { + outRect.scale = FastOverviewState.getOverviewScale(dp, outRect.rect, context); + } + if (dp.isVerticalBarLayout()) { + Rect targetInsets = dp.getInsets(); + int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right; + return dp.hotseatBarSizePx + hotseatInset; + } else { + int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom; + // Track slightly below the top of the shelf (between top and content). + return shelfHeight - dp.edgeMarginPx * 2; + } + } + + @Override + public void onTransitionCancelled(Launcher activity, boolean activityVisible) { + LauncherState startState = activity.getStateManager().getRestState(); + activity.getStateManager().goToState(startState, activityVisible); + } + + @Override + public void onSwipeUpComplete(Launcher activity) { + // Re apply state in case we did something funky during the transition. + activity.getStateManager().reapplyState(); + DiscoveryBounce.showForOverviewIfNeeded(activity); + } + + @Override + public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible, + Consumer callback) { + final LauncherState startState = activity.getStateManager().getState(); + + LauncherState resetState = startState; + if (startState.disableRestore) { + resetState = activity.getStateManager().getRestState(); + } + activity.getStateManager().setRestState(resetState); + + if (!activityVisible) { + // Since the launcher is not visible, we can safely reset the scroll position. + // This ensures then the next swipe up to all-apps starts from scroll 0. + activity.getAppsView().reset(false /* animate */); + activity.getStateManager().goToState(OVERVIEW, false); + + // Optimization, hide the all apps view to prevent layout while initializing + activity.getAppsView().getContentView().setVisibility(View.GONE); + } + + return new AnimationFactory() { + @Override + public void createActivityController(long transitionLength, + @InteractionType int interactionType) { + createActivityControllerInternal(activity, activityVisible, startState, + transitionLength, interactionType, callback); + } + + @Override + public void onTransitionCancelled() { + activity.getStateManager().goToState(startState, false /* animate */); + } + }; + } + + private void createActivityControllerInternal(Launcher activity, boolean wasVisible, + LauncherState startState, long transitionLength, + @InteractionType int interactionType, + Consumer callback) { + LauncherState endState = interactionType == INTERACTION_QUICK_SCRUB + ? FAST_OVERVIEW : OVERVIEW; + if (wasVisible) { + DeviceProfile dp = activity.getDeviceProfile(); + long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx); + callback.accept(activity.getStateManager() + .createAnimationToNewWorkspace(startState, endState, accuracy)); + return; + } + + AnimatorSet anim = new AnimatorSet(); + + if (!activity.getDeviceProfile().isVerticalBarLayout()) { + AllAppsTransitionController controller = activity.getAllAppsController(); + float scrollRange = Math.max(controller.getShiftRange(), 1); + float progressDelta = (transitionLength / scrollRange); + + float endProgress = endState.getVerticalProgress(activity); + float startProgress = endProgress + progressDelta; + ObjectAnimator shiftAnim = ObjectAnimator.ofFloat( + controller, ALL_APPS_PROGRESS, startProgress, endProgress); + shiftAnim.setInterpolator(LINEAR); + anim.play(shiftAnim); + + // Since we are changing the start position of the UI, reapply the state, at the end + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + activity.getStateManager().reapplyState(); + } + }); + } + + if (interactionType == INTERACTION_NORMAL) { + playScaleDownAnim(anim, activity); + } + + anim.setDuration(transitionLength * 2); + activity.getStateManager().setCurrentAnimation(anim); + callback.accept(AnimatorPlaybackController.wrap(anim, transitionLength * 2)); + } + + /** + * Scale down recents from the center task being full screen to being in overview. + */ + private void playScaleDownAnim(AnimatorSet anim, Launcher launcher) { + RecentsView recentsView = launcher.getOverviewPanel(); + TaskView v = recentsView.getTaskViewAt(recentsView.getCurrentPage()); + if (v == null) { + return; + } + ClipAnimationHelper clipHelper = new ClipAnimationHelper(); + clipHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), null); + if (!clipHelper.getSourceRect().isEmpty() && !clipHelper.getTargetRect().isEmpty()) { + float fromScale = clipHelper.getSourceRect().width() + / clipHelper.getTargetRect().width(); + float fromTranslationY = clipHelper.getSourceRect().centerY() + - clipHelper.getTargetRect().centerY(); + Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, fromScale, 1); + Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y, + fromTranslationY, 0); + scale.setInterpolator(LINEAR); + translateY.setInterpolator(LINEAR); + anim.playTogether(scale, translateY); + } + } + + @Override + public ActivityInitListener createActivityInitListener( + BiPredicate onInitListener) { + return new LauncherInitListener(onInitListener); + } + + @Nullable + @Override + public Launcher getCreatedActivity() { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app == null) { + return null; + } + return (Launcher) app.getModel().getCallback(); + } + + @Nullable + @UiThread + private Launcher getVisibleLaucher() { + Launcher launcher = getCreatedActivity(); + return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ? + launcher : null; + } + + @Nullable + @Override + public RecentsView getVisibleRecentsView() { + Launcher launcher = getVisibleLaucher(); + return launcher != null && launcher.getStateManager().getState().overviewUi + ? launcher.getOverviewPanel() : null; + } + + @Override + public boolean switchToRecentsIfVisible(boolean fromRecentsButton) { + Launcher launcher = getVisibleLaucher(); + if (launcher != null) { + if (fromRecentsButton) { + launcher.getUserEventDispatcher().logActionCommand( + LauncherLogProto.Action.Command.RECENTS_BUTTON, + getContainerType(), + LauncherLogProto.ContainerType.TASKSWITCHER); + } + launcher.getStateManager().goToState(OVERVIEW); + return true; + } + return false; + } + + @Override + public boolean deferStartingActivity(int downHitTarget) { + return downHitTarget == HIT_TARGET_BACK || downHitTarget == HIT_TARGET_ROTATION; + } + + @Override + public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) { + return homeBounds; + } + + @Override + public boolean shouldMinimizeSplitScreen() { + return true; + } + + @Override + public boolean supportsLongSwipe(Launcher activity) { + return !activity.getDeviceProfile().isVerticalBarLayout(); + } + + @Override + public LongSwipeHelper getLongSwipeController(Launcher activity, + RemoteAnimationTargetSet targetSet) { + if (activity.getDeviceProfile().isVerticalBarLayout()) { + return null; + } + return new LongSwipeHelper(activity, targetSet); + } + + @Override + public AlphaProperty getAlphaProperty(Launcher activity) { + return activity.getDragLayer().getAlphaProperty(DragLayer.ALPHA_INDEX_SWIPE_UP); + } + + @Override + public int getContainerType() { + final Launcher launcher = getVisibleLaucher(); + return launcher != null ? launcher.getStateManager().getState().containerType + : LauncherLogProto.ContainerType.APP; + } + } + + class FallbackActivityControllerHelper implements ActivityControlHelper { + + private final ComponentName mHomeComponent; + private final Handler mUiHandler = new Handler(Looper.getMainLooper()); + + public FallbackActivityControllerHelper(ComponentName homeComponent) { + mHomeComponent = homeComponent; + } + + @Override + public void onQuickInteractionStart(RecentsActivity activity, RunningTaskInfo taskInfo, + boolean activityVisible) { + QuickScrubController controller = activity.getOverviewPanel() + .getQuickScrubController(); + + // TODO: match user is as well + boolean startingFromHome = !activityVisible && + (taskInfo == null || Objects.equals(taskInfo.topActivity, mHomeComponent)); + controller.onQuickScrubStart(startingFromHome, this); + if (activityVisible) { + mUiHandler.postDelayed(controller::onFinishedTransitionToQuickScrub, + OVERVIEW_TRANSITION_MS); + } + } + + @Override + public float getTranslationYForQuickScrub(TransformedRect targetRect, DeviceProfile dp, + Context context) { + return 0; + } + + @Override + public void executeOnWindowAvailable(RecentsActivity activity, Runnable action) { + action.run(); + } + + @Override + public void onTransitionCancelled(RecentsActivity activity, boolean activityVisible) { + // TODO: + } + + @Override + public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, + @InteractionType int interactionType, TransformedRect outRect) { + LayoutUtils.calculateFallbackTaskSize(context, dp, outRect.rect); + if (dp.isVerticalBarLayout()) { + Rect targetInsets = dp.getInsets(); + int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right; + return dp.hotseatBarSizePx + hotseatInset; + } else { + return dp.heightPx - outRect.rect.bottom; + } + } + + @Override + public void onSwipeUpComplete(RecentsActivity activity) { + // TODO: + } + + @Override + public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible, + Consumer callback) { + if (activityVisible) { + return (transitionLength, interactionType) -> { }; + } + + RecentsView rv = activity.getOverviewPanel(); + rv.setContentAlpha(0); + + return new AnimationFactory() { + + boolean isAnimatingHome = false; + + @Override + public void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { + isAnimatingHome = targets != null && targets.isAnimatingHome(); + if (!isAnimatingHome) { + rv.setContentAlpha(1); + } + createActivityController(getSwipeUpDestinationAndLength( + activity.getDeviceProfile(), activity, INTERACTION_NORMAL, + new TransformedRect()), INTERACTION_NORMAL); + } + + @Override + public void createActivityController(long transitionLength, int interactionType) { + if (!isAnimatingHome) { + return; + } + + ObjectAnimator anim = ObjectAnimator.ofFloat(rv, CONTENT_ALPHA, 0, 1); + anim.setDuration(transitionLength).setInterpolator(LINEAR); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.play(anim); + callback.accept(AnimatorPlaybackController.wrap(animatorSet, transitionLength)); + } + }; + } + + @Override + public LayoutListener createLayoutListener(RecentsActivity activity) { + // We do not change anything as part of layout changes in fallback activity. Return a + // default layout listener. + return new LayoutListener() { + @Override + public void open() { } + + @Override + public void setHandler(WindowTransformSwipeHandler handler) { } + + @Override + public void finish() { } + }; + } + + @Override + public ActivityInitListener createActivityInitListener( + BiPredicate onInitListener) { + return new RecentsActivityTracker(onInitListener); + } + + @Nullable + @Override + public RecentsActivity getCreatedActivity() { + return RecentsActivityTracker.getCurrentActivity(); + } + + @Nullable + @Override + public RecentsView getVisibleRecentsView() { + RecentsActivity activity = getCreatedActivity(); + if (activity != null && activity.hasWindowFocus()) { + return activity.getOverviewPanel(); + } + return null; + } + + @Override + public boolean switchToRecentsIfVisible(boolean fromRecentsButton) { + return false; + } + + @Override + public boolean deferStartingActivity(int downHitTarget) { + // Always defer starting the activity when using fallback + return true; + } + + @Override + public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) { + // TODO: Remove this once b/77875376 is fixed + return target.sourceContainerBounds; + } + + @Override + public boolean shouldMinimizeSplitScreen() { + // TODO: Remove this once b/77875376 is fixed + return false; + } + + @Override + public boolean supportsLongSwipe(RecentsActivity activity) { + return false; + } + + @Override + public LongSwipeHelper getLongSwipeController(RecentsActivity activity, + RemoteAnimationTargetSet targetSet) { + return null; + } + + @Override + public AlphaProperty getAlphaProperty(RecentsActivity activity) { + return activity.getDragLayer().getAlphaProperty(0); + } + + @Override + public int getContainerType() { + return LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER; + } + } + + interface LayoutListener { + + void open(); + + void setHandler(WindowTransformSwipeHandler handler); + + void finish(); + } + + interface ActivityInitListener { + + void register(); + + void unregister(); + + void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration); + } + + interface AnimationFactory { + + default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { } + + void createActivityController(long transitionLength, @InteractionType int interactionType); + + default void onTransitionCancelled() { } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/AnimatedFloat.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/AnimatedFloat.java new file mode 100644 index 0000000000..6604dabd12 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/AnimatedFloat.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.util.FloatProperty; + +/** + * A mutable float which allows animating the value + */ +public class AnimatedFloat { + + public static FloatProperty VALUE = new FloatProperty("value") { + @Override + public void setValue(AnimatedFloat obj, float v) { + obj.updateValue(v); + } + + @Override + public Float get(AnimatedFloat obj) { + return obj.value; + } + }; + + private final Runnable mUpdateCallback; + private ObjectAnimator mValueAnimator; + + public float value; + + public AnimatedFloat(Runnable updateCallback) { + mUpdateCallback = updateCallback; + } + + public ObjectAnimator animateToValue(float start, float end) { + cancelAnimation(); + mValueAnimator = ObjectAnimator.ofFloat(this, VALUE, start, end); + mValueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + if (mValueAnimator == animator) { + mValueAnimator = null; + } + } + }); + return mValueAnimator; + } + + /** + * Changes the value and calls the callback. + * Note that the value can be directly accessed as well to avoid notifying the callback. + */ + public void updateValue(float v) { + if (Float.compare(v, value) != 0) { + value = v; + mUpdateCallback.run(); + } + } + + public void cancelAnimation() { + if (mValueAnimator != null) { + mValueAnimator.cancel(); + } + } + + public void finishAnimation() { + if (mValueAnimator != null && mValueAnimator.isRunning()) { + mValueAnimator.end(); + } + } + + public ObjectAnimator getCurrentAnimation() { + return mValueAnimator; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/DeferredTouchConsumer.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/DeferredTouchConsumer.java new file mode 100644 index 0000000000..4aac3e10c8 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/DeferredTouchConsumer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.os.Build; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.VelocityTracker; + +/** + * A TouchConsumer which defers all events on the UIThread until the consumer is created. + */ +@TargetApi(Build.VERSION_CODES.P) +public class DeferredTouchConsumer implements TouchConsumer { + + private final VelocityTracker mVelocityTracker; + private final DeferredTouchProvider mTouchProvider; + + private MotionEventQueue mMyQueue; + private TouchConsumer mTarget; + + public DeferredTouchConsumer(DeferredTouchProvider touchProvider) { + mVelocityTracker = VelocityTracker.obtain(); + mTouchProvider = touchProvider; + } + + @Override + public void accept(MotionEvent event) { + mTarget.accept(event); + } + + @Override + public void reset() { + mTarget.reset(); + } + + @Override + public void updateTouchTracking(int interactionType) { + mTarget.updateTouchTracking(interactionType); + } + + @Override + public void onQuickScrubEnd() { + mTarget.onQuickScrubEnd(); + } + + @Override + public void onQuickScrubProgress(float progress) { + mTarget.onQuickScrubProgress(progress); + } + + @Override + public void onQuickStep(MotionEvent ev) { + mTarget.onQuickStep(ev); + } + + @Override + public void onCommand(int command) { + mTarget.onCommand(command); + } + + @Override + public void preProcessMotionEvent(MotionEvent ev) { + mVelocityTracker.addMovement(ev); + } + + @Override + public Choreographer getIntrimChoreographer(MotionEventQueue queue) { + mMyQueue = queue; + return null; + } + + @Override + public void deferInit() { + mTarget = mTouchProvider.createTouchConsumer(mVelocityTracker); + mTarget.getIntrimChoreographer(mMyQueue); + } + + @Override + public boolean forceToLauncherConsumer() { + return mTarget.forceToLauncherConsumer(); + } + + @Override + public boolean deferNextEventToMainThread() { + // If our target is still null, defer the next target as well + TouchConsumer target = mTarget; + return target == null ? true : target.deferNextEventToMainThread(); + } + + @Override + public void onShowOverviewFromAltTab() { + mTarget.onShowOverviewFromAltTab(); + } + + public interface DeferredTouchProvider { + + TouchConsumer createTouchConsumer(VelocityTracker tracker); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/InstantAppResolverImpl.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/InstantAppResolverImpl.java new file mode 100644 index 0000000000..02afb35c17 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/InstantAppResolverImpl.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.InstantAppInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.util.InstantAppResolver; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of InstantAppResolver using platform APIs + */ +@SuppressWarnings("unused") +public class InstantAppResolverImpl extends InstantAppResolver { + + private static final String TAG = "InstantAppResolverImpl"; + public static final String COMPONENT_CLASS_MARKER = "@instantapp"; + + private final PackageManager mPM; + + public InstantAppResolverImpl(Context context) + throws NoSuchMethodException, ClassNotFoundException { + mPM = context.getPackageManager(); + } + + @Override + public boolean isInstantApp(ApplicationInfo info) { + return info.isInstantApp(); + } + + @Override + public boolean isInstantApp(AppInfo info) { + ComponentName cn = info.getTargetComponent(); + return cn != null && cn.getClassName().equals(COMPONENT_CLASS_MARKER); + } + + @Override + public List getInstantApps() { + try { + List result = new ArrayList<>(); + for (InstantAppInfo iai : mPM.getInstantApps()) { + ApplicationInfo info = iai.getApplicationInfo(); + if (info != null) { + result.add(info); + } + } + return result; + } catch (SecurityException se) { + Log.w(TAG, "getInstantApps failed. Launcher may not be the default home app.", se); + } catch (Exception e) { + Log.e(TAG, "Error calling API: getInstantApps", e); + } + return super.getInstantApps(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LauncherSearchIndexablesProvider.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LauncherSearchIndexablesProvider.java new file mode 100644 index 0000000000..dcf32c75a7 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LauncherSearchIndexablesProvider.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.Build; +import android.provider.SearchIndexablesContract.XmlResource; +import android.provider.SearchIndexablesProvider; +import android.util.Xml; + +import com.android.launcher3.R; +import com.android.launcher3.graphics.IconShapeOverride; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; +import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS; +import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS; + +@TargetApi(Build.VERSION_CODES.O) +public class LauncherSearchIndexablesProvider extends SearchIndexablesProvider { + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor queryXmlResources(String[] strings) { + MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS); + ResolveInfo settingsActivity = getContext().getPackageManager().resolveActivity( + new Intent(Intent.ACTION_APPLICATION_PREFERENCES) + .setPackage(getContext().getPackageName()), 0); + cursor.newRow() + .add(XmlResource.COLUMN_XML_RESID, R.xml.indexable_launcher_prefs) + .add(XmlResource.COLUMN_INTENT_ACTION, Intent.ACTION_APPLICATION_PREFERENCES) + .add(XmlResource.COLUMN_INTENT_TARGET_PACKAGE, getContext().getPackageName()) + .add(XmlResource.COLUMN_INTENT_TARGET_CLASS, settingsActivity.activityInfo.name); + return cursor; + } + + @Override + public Cursor queryRawData(String[] projection) { + return new MatrixCursor(INDEXABLES_RAW_COLUMNS); + } + + @Override + public Cursor queryNonIndexableKeys(String[] projection) { + MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS); + if (!getContext().getSystemService(LauncherApps.class).hasShortcutHostPermission()) { + // We are not the current launcher. Hide all preferences + try (XmlResourceParser parser = getContext().getResources() + .getXml(R.xml.indexable_launcher_prefs)) { + final int depth = parser.getDepth(); + final int[] attrs = new int[] { android.R.attr.key }; + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type == XmlPullParser.START_TAG) { + TypedArray a = getContext().obtainStyledAttributes( + Xml.asAttributeSet(parser), attrs); + cursor.addRow(new String[] {a.getString(0)}); + a.recycle(); + } + } + } catch (IOException | XmlPullParserException e) { + throw new RuntimeException(e); + } + } else if (!IconShapeOverride.isSupported(getContext())) { + cursor.addRow(new String[] {IconShapeOverride.KEY_PREFERENCE}); + } + return cursor; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LongSwipeHelper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LongSwipeHelper.java new file mode 100644 index 0000000000..f289f2eff4 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/LongSwipeHelper.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.ValueAnimator; +import android.view.animation.Interpolator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.anim.Interpolators.OvershootParams; +import com.android.launcher3.uioverrides.PortraitStatesTouchController; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.launcher3.util.FlingBlockCheck; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import static com.android.launcher3.LauncherAnimUtils.MIN_PROGRESS_TO_ALL_APPS; +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION; +import static com.android.quickstep.WindowTransformSwipeHandler.MIN_OVERSHOOT_DURATION; + +/** + * Utility class to handle long swipe from an app. + * This assumes the presence of Launcher activity as long swipe is not supported on the + * fallback activity. + */ +public class LongSwipeHelper { + + private static final float SWIPE_DURATION_MULTIPLIER = + Math.min(1 / MIN_PROGRESS_TO_ALL_APPS, 1 / (1 - MIN_PROGRESS_TO_ALL_APPS)); + + private final Launcher mLauncher; + private final RemoteAnimationTargetSet mTargetSet; + + private float mMaxSwipeDistance = 1; + private AnimatorPlaybackController mAnimator; + private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); + + LongSwipeHelper(Launcher launcher, RemoteAnimationTargetSet targetSet) { + mLauncher = launcher; + mTargetSet = targetSet; + init(); + } + + private void init() { + mFlingBlockCheck.blockFling(); + + // Init animations + AllAppsTransitionController controller = mLauncher.getAllAppsController(); + // TODO: Scale it down so that we can reach all-apps in screen space + mMaxSwipeDistance = Math.max(1, controller.getProgress() * controller.getShiftRange()); + + AnimatorSetBuilder builder = PortraitStatesTouchController.getOverviewToAllAppsAnimation(); + mAnimator = mLauncher.getStateManager().createAnimationToNewWorkspace(ALL_APPS, builder, + Math.round(2 * mMaxSwipeDistance), null, LauncherStateManager.ANIM_ALL); + mAnimator.dispatchOnStart(); + } + + public void onMove(float displacement) { + mAnimator.setPlayFraction(displacement / mMaxSwipeDistance); + mFlingBlockCheck.onEvent(); + } + + public void destroy() { + // TODO: We can probably also show the task view + + mLauncher.getStateManager().goToState(OVERVIEW, false); + } + + public void end(float velocity, boolean isFling, Runnable callback) { + float velocityPxPerMs = velocity / 1000; + long duration = MAX_SWIPE_DURATION; + Interpolator interpolator = DEACCEL; + + final float currentFraction = mAnimator.getProgressFraction(); + final boolean toAllApps; + float endProgress; + + boolean blockedFling = isFling && mFlingBlockCheck.isBlocked(); + if (blockedFling) { + isFling = false; + } + + if (!isFling) { + toAllApps = currentFraction > MIN_PROGRESS_TO_ALL_APPS; + endProgress = toAllApps ? 1 : 0; + + long expectedDuration = Math.abs(Math.round((endProgress - currentFraction) + * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); + duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); + + if (blockedFling && !toAllApps) { + Interpolators.OvershootParams overshoot = new OvershootParams(currentFraction, + currentFraction, endProgress, velocityPxPerMs, (int) mMaxSwipeDistance); + duration = (overshoot.duration + duration); + duration = Utilities.boundToRange(duration, MIN_OVERSHOOT_DURATION, + MAX_SWIPE_DURATION); + interpolator = overshoot.interpolator; + endProgress = overshoot.end; + } + } else { + toAllApps = velocity < 0; + endProgress = toAllApps ? 1 : 0; + + float minFlingVelocity = mLauncher.getResources() + .getDimension(R.dimen.quickstep_fling_min_velocity); + if (Math.abs(velocity) > minFlingVelocity && mMaxSwipeDistance > 0) { + float distanceToTravel = (endProgress - currentFraction) * mMaxSwipeDistance; + + // 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. 2. + long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); + } + } + + final boolean finalIsFling = isFling; + mAnimator.setEndAction(() -> onSwipeAnimationComplete(toAllApps, finalIsFling, callback)); + + ValueAnimator animator = mAnimator.getAnimationPlayer(); + animator.setDuration(duration).setInterpolator(interpolator); + animator.setFloatValues(currentFraction, endProgress); + animator.start(); + } + + private void onSwipeAnimationComplete(boolean toAllApps, boolean isFling, Runnable callback) { + mLauncher.getStateManager().goToState(toAllApps ? ALL_APPS : OVERVIEW, false); + if (!toAllApps) { + DiscoveryBounce.showForOverviewIfNeeded(mLauncher); + mLauncher.getOverviewPanel().setSwipeDownShouldLaunchApp(true); + } + + mLauncher.getUserEventDispatcher().logStateChangeAction( + isFling ? Touch.FLING : Touch.SWIPE, Direction.UP, + ContainerType.NAVBAR, ContainerType.APP, + toAllApps ? ContainerType.ALLAPPS : ContainerType.TASKSWITCHER, + 0); + + callback.run(); + } + + public float getTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) { + if (!(app.isNotInRecents + || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) { + return 0; + } + return expectedAlpha; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MotionEventQueue.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MotionEventQueue.java new file mode 100644 index 0000000000..84e43d93bd --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MotionEventQueue.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.os.Build; +import android.util.Log; +import android.view.Choreographer; +import android.view.MotionEvent; + +import com.android.systemui.shared.system.ChoreographerCompat; + +import java.util.ArrayList; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_MASK; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; + +/** + * Helper class for batching input events + */ +@TargetApi(Build.VERSION_CODES.O) +public class MotionEventQueue { + + private static final String TAG = "MotionEventQueue"; + + private static final int ACTION_VIRTUAL = ACTION_MASK - 1; + + private static final int ACTION_QUICK_SCRUB_START = + ACTION_VIRTUAL | (1 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_SCRUB_PROGRESS = + ACTION_VIRTUAL | (2 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_SCRUB_END = + ACTION_VIRTUAL | (3 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_RESET = + ACTION_VIRTUAL | (4 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_DEFER_INIT = + ACTION_VIRTUAL | (5 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_SHOW_OVERVIEW_FROM_ALT_TAB = + ACTION_VIRTUAL | (6 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_QUICK_STEP = + ACTION_VIRTUAL | (7 << ACTION_POINTER_INDEX_SHIFT); + private static final int ACTION_COMMAND = + ACTION_VIRTUAL | (8 << ACTION_POINTER_INDEX_SHIFT); + + private final EventArray mEmptyArray = new EventArray(); + private final Object mExecutionLock = new Object(); + + // We use two arrays and swap the current index when one array is being consumed + private final EventArray[] mArrays = new EventArray[] {new EventArray(), new EventArray()}; + private int mCurrentIndex = 0; + + private final Runnable mMainFrameCallback = this::frameCallbackForMainChoreographer; + private final Runnable mInterimFrameCallback = this::frameCallbackForInterimChoreographer; + + private final Choreographer mMainChoreographer; + + private final TouchConsumer mConsumer; + + private Choreographer mInterimChoreographer; + private Choreographer mCurrentChoreographer; + + private Runnable mCurrentRunnable; + + public MotionEventQueue(Choreographer choreographer, TouchConsumer consumer) { + mMainChoreographer = choreographer; + mConsumer = consumer; + mCurrentChoreographer = mMainChoreographer; + mCurrentRunnable = mMainFrameCallback; + + setInterimChoreographer(consumer.getIntrimChoreographer(this)); + } + + public void setInterimChoreographer(Choreographer choreographer) { + synchronized (mExecutionLock) { + synchronized (mArrays) { + setInterimChoreographerLocked(choreographer); + ChoreographerCompat.postInputFrame(mCurrentChoreographer, mCurrentRunnable); + } + } + } + + private void setInterimChoreographerLocked(Choreographer choreographer) { + mInterimChoreographer = choreographer; + if (choreographer == null) { + mCurrentChoreographer = mMainChoreographer; + mCurrentRunnable = mMainFrameCallback; + } else { + mCurrentChoreographer = mInterimChoreographer; + mCurrentRunnable = mInterimFrameCallback; + } + } + + public void queue(MotionEvent event) { + mConsumer.preProcessMotionEvent(event); + queueNoPreProcess(event); + } + + private void queueNoPreProcess(MotionEvent event) { + synchronized (mArrays) { + EventArray array = mArrays[mCurrentIndex]; + if (array.isEmpty()) { + ChoreographerCompat.postInputFrame(mCurrentChoreographer, mCurrentRunnable); + } + + int eventAction = event.getAction(); + if (eventAction == ACTION_MOVE && array.lastEventAction == ACTION_MOVE) { + // Replace and recycle the last event + array.set(array.size() - 1, event).recycle(); + } else { + array.add(event); + array.lastEventAction = eventAction; + } + } + } + + private void frameCallbackForMainChoreographer() { + runFor(mMainChoreographer); + } + + private void frameCallbackForInterimChoreographer() { + runFor(mInterimChoreographer); + } + + private void runFor(Choreographer caller) { + synchronized (mExecutionLock) { + EventArray array = swapAndGetCurrentArray(caller); + int size = array.size(); + for (int i = 0; i < size; i++) { + MotionEvent event = array.get(i); + if (event.getActionMasked() == ACTION_VIRTUAL) { + switch (event.getAction()) { + case ACTION_QUICK_SCRUB_START: + mConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB); + break; + case ACTION_QUICK_SCRUB_PROGRESS: + mConsumer.onQuickScrubProgress(event.getX()); + break; + case ACTION_QUICK_SCRUB_END: + mConsumer.onQuickScrubEnd(); + break; + case ACTION_RESET: + mConsumer.reset(); + break; + case ACTION_DEFER_INIT: + mConsumer.deferInit(); + break; + case ACTION_SHOW_OVERVIEW_FROM_ALT_TAB: + mConsumer.onShowOverviewFromAltTab(); + mConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB); + break; + case ACTION_QUICK_STEP: + mConsumer.onQuickStep(event); + break; + case ACTION_COMMAND: + mConsumer.onCommand(event.getSource()); + break; + default: + Log.e(TAG, "Invalid virtual event: " + event.getAction()); + } + } else { + mConsumer.accept(event); + } + event.recycle(); + } + array.clear(); + array.lastEventAction = ACTION_CANCEL; + } + } + + private EventArray swapAndGetCurrentArray(Choreographer caller) { + synchronized (mArrays) { + if (caller != mCurrentChoreographer) { + return mEmptyArray; + } + EventArray current = mArrays[mCurrentIndex]; + mCurrentIndex = mCurrentIndex ^ 1; + return current; + } + } + + private void queueVirtualAction(int action, float progress) { + queueNoPreProcess(MotionEvent.obtain(0, 0, action, progress, 0, 0)); + } + + public void onQuickScrubStart() { + queueVirtualAction(ACTION_QUICK_SCRUB_START, 0); + } + + public void onOverviewShownFromAltTab() { + queueVirtualAction(ACTION_SHOW_OVERVIEW_FROM_ALT_TAB, 0); + } + + public void onQuickScrubProgress(float progress) { + queueVirtualAction(ACTION_QUICK_SCRUB_PROGRESS, progress); + } + + public void onQuickScrubEnd() { + queueVirtualAction(ACTION_QUICK_SCRUB_END, 0); + } + + public void onQuickStep(MotionEvent event) { + event.setAction(ACTION_QUICK_STEP); + queueNoPreProcess(event); + } + + public void reset() { + queueVirtualAction(ACTION_RESET, 0); + } + + public void deferInit() { + queueVirtualAction(ACTION_DEFER_INIT, 0); + } + + public void onCommand(int command) { + MotionEvent ev = MotionEvent.obtain(0, 0, ACTION_COMMAND, 0, 0, 0); + ev.setSource(command); + queueNoPreProcess(ev); + } + + public TouchConsumer getConsumer() { + return mConsumer; + } + + private static class EventArray extends ArrayList { + + public int lastEventAction = ACTION_CANCEL; + + public EventArray() { + super(4); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MultiStateCallback.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MultiStateCallback.java new file mode 100644 index 0000000000..ba07ff569f --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/MultiStateCallback.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.util.SparseArray; + +/** + * Utility class to help manage multiple callbacks based on different states. + */ +public class MultiStateCallback { + + private final SparseArray mCallbacks = new SparseArray<>(); + + private int mState = 0; + + /** + * Adds the provided state flags to the global state and executes any callbacks as a result. + * @param stateFlag + */ + public void setState(int stateFlag) { + mState = mState | stateFlag; + + int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + int state = mCallbacks.keyAt(i); + + if ((mState & state) == state) { + Runnable callback = mCallbacks.valueAt(i); + if (callback != null) { + // Set the callback to null, so that it does not run again. + mCallbacks.setValueAt(i, null); + callback.run(); + } + } + } + } + + /** + * Sets the callbacks to be run when the provided states are enabled. + * The callback is only run once. + */ + public void addCallback(int stateMask, Runnable callback) { + mCallbacks.put(stateMask, callback); + } + + public int getState() { + return mState; + } + + public boolean hasStates(int stateMask) { + return (mState & stateMask) == stateMask; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/NormalizedIconLoader.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/NormalizedIconLoader.java new file mode 100644 index 0000000000..07e4deb2fe --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/NormalizedIconLoader.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager.TaskDescription; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.UserHandle; +import android.util.LruCache; +import android.util.SparseArray; + +import com.android.launcher3.FastBitmapDrawable; +import com.android.launcher3.graphics.BitmapInfo; +import com.android.launcher3.graphics.DrawableFactory; +import com.android.launcher3.graphics.LauncherIcons; +import com.android.systemui.shared.recents.model.IconLoader; +import com.android.systemui.shared.recents.model.TaskKeyLruCache; + +/** + * Extension of {@link IconLoader} with icon normalization support + */ +@TargetApi(Build.VERSION_CODES.O) +public class NormalizedIconLoader extends IconLoader { + + private final SparseArray mDefaultIcons = new SparseArray<>(); + private final DrawableFactory mDrawableFactory; + private LauncherIcons mLauncherIcons; + + public NormalizedIconLoader(Context context, TaskKeyLruCache iconCache, + LruCache activityInfoCache) { + super(context, iconCache, activityInfoCache); + mDrawableFactory = DrawableFactory.get(context); + } + + @Override + public Drawable getDefaultIcon(int userId) { + synchronized (mDefaultIcons) { + BitmapInfo info = mDefaultIcons.get(userId); + if (info == null) { + info = getBitmapInfo(Resources.getSystem() + .getDrawable(android.R.drawable.sym_def_app_icon), userId, 0, false); + mDefaultIcons.put(userId, info); + } + + return new FastBitmapDrawable(info); + } + } + + @Override + protected Drawable createBadgedDrawable(Drawable drawable, int userId, TaskDescription desc) { + return new FastBitmapDrawable(getBitmapInfo(drawable, userId, desc.getPrimaryColor(), + false)); + } + + private synchronized BitmapInfo getBitmapInfo(Drawable drawable, int userId, + int primaryColor, boolean isInstantApp) { + if (mLauncherIcons == null) { + mLauncherIcons = LauncherIcons.obtain(mContext); + } + + mLauncherIcons.setWrapperBackgroundColor(primaryColor); + // User version code O, so that the icon is always wrapped in an adaptive icon container. + return mLauncherIcons.createBadgedIconBitmap(drawable, UserHandle.of(userId), + Build.VERSION_CODES.O, isInstantApp); + } + + @Override + protected Drawable getBadgedActivityIcon(ActivityInfo activityInfo, int userId, + TaskDescription desc) { + BitmapInfo bitmapInfo = getBitmapInfo( + activityInfo.loadUnbadgedIcon(mContext.getPackageManager()), + userId, + desc.getPrimaryColor(), + activityInfo.applicationInfo.isInstantApp()); + return mDrawableFactory.newIcon(bitmapInfo, activityInfo); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OtherActivityTouchConsumer.java new file mode 100644 index 0000000000..a9a7b0c49c --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OtherActivityTouchConsumer.java @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.SystemClock; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.Display; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.AssistDataReceiver; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.NavigationBarCompat; +import com.android.systemui.shared.system.NavigationBarCompat.HitTarget; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; +import com.android.systemui.shared.system.RecentsAnimationListener; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; +import static android.view.MotionEvent.INVALID_POINTER_ID; +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +/** + * Touch consumer for handling events originating from an activity other than Launcher + */ +@TargetApi(Build.VERSION_CODES.P) +public class OtherActivityTouchConsumer extends ContextWrapper implements TouchConsumer { + + private static final long LAUNCHER_DRAW_TIMEOUT_MS = 150; + + private final SparseArray mAnimationStates = new SparseArray<>(); + private final RunningTaskInfo mRunningTask; + private final RecentsModel mRecentsModel; + private final Intent mHomeIntent; + private final ActivityControlHelper mActivityControlHelper; + private final MainThreadExecutor mMainThreadExecutor; + private final Choreographer mBackgroundThreadChoreographer; + private final OverviewCallbacks mOverviewCallbacks; + private final TaskOverlayFactory mTaskOverlayFactory; + + private final boolean mIsDeferredDownTarget; + private final PointF mDownPos = new PointF(); + private final PointF mLastPos = new PointF(); + private int mActivePointerId = INVALID_POINTER_ID; + private boolean mPassedInitialSlop; + // Used for non-deferred gestures to determine when to start dragging + private int mQuickStepDragSlop; + private float mStartDisplacement; + private WindowTransformSwipeHandler mInteractionHandler; + private int mDisplayRotation; + private Rect mStableInsets = new Rect(); + + private VelocityTracker mVelocityTracker; + private MotionEventQueue mEventQueue; + private boolean mIsGoingToHome; + + public OtherActivityTouchConsumer(Context base, RunningTaskInfo runningTaskInfo, + RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl, + MainThreadExecutor mainThreadExecutor, Choreographer backgroundThreadChoreographer, + @HitTarget int downHitTarget, OverviewCallbacks overviewCallbacks, + TaskOverlayFactory taskOverlayFactory, VelocityTracker velocityTracker) { + super(base); + + mRunningTask = runningTaskInfo; + mRecentsModel = recentsModel; + mHomeIntent = homeIntent; + mVelocityTracker = velocityTracker; + mActivityControlHelper = activityControl; + mMainThreadExecutor = mainThreadExecutor; + mBackgroundThreadChoreographer = backgroundThreadChoreographer; + mIsDeferredDownTarget = activityControl.deferStartingActivity(downHitTarget); + mOverviewCallbacks = overviewCallbacks; + mTaskOverlayFactory = taskOverlayFactory; + } + + @Override + public void onShowOverviewFromAltTab() { + startTouchTrackingForWindowAnimation(SystemClock.uptimeMillis()); + } + + @Override + public void accept(MotionEvent ev) { + if (mVelocityTracker == null) { + return; + } + switch (ev.getActionMasked()) { + case ACTION_DOWN: { + TraceHelper.beginSection("TouchInt"); + mActivePointerId = ev.getPointerId(0); + mDownPos.set(ev.getX(), ev.getY()); + mLastPos.set(mDownPos); + mPassedInitialSlop = false; + mQuickStepDragSlop = NavigationBarCompat.getQuickStepDragSlopPx(); + + // Start the window animation on down to give more time for launcher to draw if the + // user didn't start the gesture over the back button + if (!mIsDeferredDownTarget) { + startTouchTrackingForWindowAnimation(ev.getEventTime()); + } + + Display display = getSystemService(WindowManager.class).getDefaultDisplay(); + mDisplayRotation = display.getRotation(); + WindowManagerWrapper.getInstance().getStableInsets(mStableInsets); + break; + } + case ACTION_POINTER_UP: { + int ptrIdx = ev.getActionIndex(); + int ptrId = ev.getPointerId(ptrIdx); + if (ptrId == mActivePointerId) { + final int newPointerIdx = ptrIdx == 0 ? 1 : 0; + mDownPos.set( + ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), + ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); + mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); + mActivePointerId = ev.getPointerId(newPointerIdx); + } + break; + } + case ACTION_MOVE: { + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == INVALID_POINTER_ID) { + break; + } + mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); + float displacement = getDisplacement(ev); + if (!mPassedInitialSlop) { + if (!mIsDeferredDownTarget) { + // Normal gesture, ensure we pass the drag slop before we start tracking + // the gesture + if (Math.abs(displacement) > mQuickStepDragSlop) { + mPassedInitialSlop = true; + mStartDisplacement = displacement; + } + } + } + + if (mPassedInitialSlop && mInteractionHandler != null) { + // Move + mInteractionHandler.updateDisplacement(displacement - mStartDisplacement); + } + break; + } + case ACTION_CANCEL: + // TODO: Should be different than ACTION_UP + case ACTION_UP: { + TraceHelper.endSection("TouchInt"); + + finishTouchTracking(ev); + break; + } + } + } + + private void notifyGestureStarted() { + if (mInteractionHandler == null) { + return; + } + + mOverviewCallbacks.closeAllWindows(); + ActivityManagerWrapper.getInstance().closeSystemWindows( + CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + + // Notify the handler that the gesture has actually started + mInteractionHandler.onGestureStarted(); + } + + private boolean isNavBarOnRight() { + return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0; + } + + private boolean isNavBarOnLeft() { + return mDisplayRotation == Surface.ROTATION_270 && mStableInsets.left > 0; + } + + private void startTouchTrackingForWindowAnimation(long touchTimeMs) { + // Create the shared handler + RecentsAnimationState animationState = new RecentsAnimationState(); + final WindowTransformSwipeHandler handler = new WindowTransformSwipeHandler( + animationState.id, mRunningTask, this, touchTimeMs, mActivityControlHelper); + + // Preload the plan + mRecentsModel.loadTasks(mRunningTask.id, null); + mInteractionHandler = handler; + handler.setGestureEndCallback(mEventQueue::reset); + + CountDownLatch drawWaitLock = new CountDownLatch(1); + handler.setLauncherOnDrawCallback(() -> { + drawWaitLock.countDown(); + if (handler == mInteractionHandler) { + switchToMainChoreographer(); + } + }); + handler.initWhenReady(); + + TraceHelper.beginSection("RecentsController"); + + AssistDataReceiver assistDataReceiver = !mTaskOverlayFactory.needAssist() ? null : + new AssistDataReceiver() { + @Override + public void onHandleAssistData(Bundle bundle) { + if (mInteractionHandler == null) { + // Interaction is probably complete + mRecentsModel.preloadAssistData(mRunningTask.id, bundle); + } else if (handler == mInteractionHandler) { + handler.onAssistDataReceived(bundle); + } + } + }; + + Runnable startActivity = () -> ActivityManagerWrapper.getInstance().startRecentsActivity( + mHomeIntent, assistDataReceiver, animationState, null, null); + + if (Looper.myLooper() != Looper.getMainLooper()) { + startActivity.run(); + try { + drawWaitLock.await(LAUNCHER_DRAW_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // We have waited long enough for launcher to draw + } + } else { + // We should almost always get touch-town on background thread. This is an edge case + // when the background Choreographer has not yet initialized. + BackgroundExecutor.get().submit(startActivity); + } + } + + @Override + public void onCommand(int command) { + RecentsAnimationState state = mAnimationStates.get(command); + if (state != null) { + state.execute(); + } + } + + /** + * Called when the gesture has ended. Does not correlate to the completion of the interaction as + * the animation can still be running. + */ + private void finishTouchTracking(MotionEvent ev) { + if (mPassedInitialSlop && mInteractionHandler != null) { + mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement); + + mVelocityTracker.computeCurrentVelocity(1000, + ViewConfiguration.get(this).getScaledMaximumFlingVelocity()); + + float velocity = isNavBarOnRight() ? mVelocityTracker.getXVelocity(mActivePointerId) + : isNavBarOnLeft() ? -mVelocityTracker.getXVelocity(mActivePointerId) + : mVelocityTracker.getYVelocity(mActivePointerId); + mInteractionHandler.onGestureEnded(velocity); + } else { + // Since we start touch tracking on DOWN, we may reach this state without actually + // starting the gesture. In that case, just cleanup immediately. + reset(); + + // Also clean up in case the system has handled the UP and canceled the animation before + // we had a chance to start the recents animation. In such a case, we will not receive + ActivityManagerWrapper.getInstance().cancelRecentsAnimation( + true /* restoreHomeStackPosition */); + } + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + @Override + public void reset() { + // Clean up the old interaction handler + if (mInteractionHandler != null) { + final WindowTransformSwipeHandler handler = mInteractionHandler; + mInteractionHandler = null; + mIsGoingToHome = handler.mIsGoingToHome; + mMainThreadExecutor.execute(handler::reset); + } + } + + @Override + public void updateTouchTracking(int interactionType) { + if (!mPassedInitialSlop && mIsDeferredDownTarget && mInteractionHandler == null) { + // If we deferred starting the window animation on touch down, then + // start tracking now + startTouchTrackingForWindowAnimation(SystemClock.uptimeMillis()); + mPassedInitialSlop = true; + } + + if (mInteractionHandler != null) { + mInteractionHandler.updateInteractionType(interactionType); + } + notifyGestureStarted(); + } + + @Override + public Choreographer getIntrimChoreographer(MotionEventQueue queue) { + mEventQueue = queue; + return mBackgroundThreadChoreographer; + } + + @Override + public void onQuickScrubEnd() { + if (mInteractionHandler != null) { + mInteractionHandler.onQuickScrubEnd(); + } + } + + @Override + public void onQuickScrubProgress(float progress) { + if (mInteractionHandler != null) { + mInteractionHandler.onQuickScrubProgress(progress); + } + } + + @Override + public void onQuickStep(MotionEvent ev) { + if (mIsDeferredDownTarget) { + // Deferred gesture, start the animation and gesture tracking once we pass the actual + // touch slop + startTouchTrackingForWindowAnimation(ev.getEventTime()); + mPassedInitialSlop = true; + mStartDisplacement = getDisplacement(ev); + } + notifyGestureStarted(); + } + + private float getDisplacement(MotionEvent ev) { + float eventX = ev.getX(); + float eventY = ev.getY(); + float displacement = eventY - mDownPos.y; + if (isNavBarOnRight()) { + displacement = eventX - mDownPos.x; + } else if (isNavBarOnLeft()) { + displacement = mDownPos.x - eventX; + } + return displacement; + } + + public void switchToMainChoreographer() { + mEventQueue.setInterimChoreographer(null); + } + + @Override + public void preProcessMotionEvent(MotionEvent ev) { + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(ev); + if (ev.getActionMasked() == ACTION_POINTER_UP) { + mVelocityTracker.clear(); + } + } + } + + @Override + public boolean forceToLauncherConsumer() { + return mIsGoingToHome; + } + + @Override + public boolean deferNextEventToMainThread() { + // TODO: Consider also check if the eventQueue is using mainThread of not. + return mInteractionHandler != null; + } + + private class RecentsAnimationState implements RecentsAnimationListener { + + private final int id; + + private RecentsAnimationControllerCompat mController; + private RemoteAnimationTargetSet mTargets; + private Rect mHomeContentInsets; + private Rect mMinimizedHomeBounds; + private boolean mCancelled; + + public RecentsAnimationState() { + id = mAnimationStates.size(); + mAnimationStates.put(id, this); + } + + @Override + public void onAnimationStart( + RecentsAnimationControllerCompat controller, + RemoteAnimationTargetCompat[] apps, Rect homeContentInsets, + Rect minimizedHomeBounds) { + mController = controller; + mTargets = new RemoteAnimationTargetSet(apps, MODE_CLOSING); + mHomeContentInsets = homeContentInsets; + mMinimizedHomeBounds = minimizedHomeBounds; + mEventQueue.onCommand(id); + } + + @Override + public void onAnimationCanceled() { + mCancelled = true; + mEventQueue.onCommand(id); + } + + public void execute() { + if (mInteractionHandler == null || mInteractionHandler.id != id) { + if (!mCancelled && mController != null) { + TraceHelper.endSection("RecentsController", "Finishing no handler"); + mController.finish(false /* toHome */); + } + } else if (mCancelled) { + TraceHelper.endSection("RecentsController", + "Cancelled: " + mInteractionHandler); + mInteractionHandler.onRecentsAnimationCanceled(); + } else { + TraceHelper.partitionSection("RecentsController", "Received"); + mInteractionHandler.onRecentsAnimationStart(mController, mTargets, + mHomeContentInsets, mMinimizedHomeBounds); + } + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCallbacks.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCallbacks.java new file mode 100644 index 0000000000..3f5096d919 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCallbacks.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.content.Context; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Preconditions; + +/** + * Callbacks related to overview/quicksteps. + */ +public class OverviewCallbacks { + + private static OverviewCallbacks sInstance; + + public static OverviewCallbacks get(Context context) { + Preconditions.assertUIThread(); + if (sInstance == null) { + sInstance = Utilities.getOverrideObject(OverviewCallbacks.class, + context.getApplicationContext(), R.string.overview_callbacks_class); + } + return sInstance; + } + + public void onInitOverviewTransition() { } + + public void onResetOverview() { } + + public void closeAllWindows() { } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCommandHelper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCommandHelper.java new file mode 100644 index 0000000000..520f5662cb --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewCommandHelper.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ResolveInfo; +import android.graphics.Rect; +import android.os.Build; +import android.os.PatternMatcher; +import android.os.SystemClock; +import android.util.Log; +import android.view.View; +import android.view.ViewConfiguration; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.ActivityControlHelper.AnimationFactory; +import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper; +import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.util.TransformedRect; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.LatencyTrackerCompat; +import com.android.systemui.shared.system.PackageManagerWrapper; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; +import com.android.systemui.shared.system.TransactionCompat; + +import java.util.ArrayList; + +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_CHANGED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +/** + * Helper class to handle various atomic commands for switching between Overview. + */ +@TargetApi(Build.VERSION_CODES.P) +public class OverviewCommandHelper { + + private static final long RECENTS_LAUNCH_DURATION = 250; + + private static final String TAG = "OverviewCommandHelper"; + + private final Context mContext; + private final ActivityManagerWrapper mAM; + private final RecentsModel mRecentsModel; + private final MainThreadExecutor mMainThreadExecutor; + private final ComponentName mMyHomeComponent; + + private final BroadcastReceiver mUserPreferenceChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initOverviewTargets(); + } + }; + private final BroadcastReceiver mOtherHomeAppUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initOverviewTargets(); + } + }; + private String mUpdateRegisteredPackage; + + public Intent overviewIntent; + public ComponentName overviewComponent; + private ActivityControlHelper mActivityControlHelper; + + private long mLastToggleTime; + + public OverviewCommandHelper(Context context) { + mContext = context; + mAM = ActivityManagerWrapper.getInstance(); + mMainThreadExecutor = new MainThreadExecutor(); + mRecentsModel = RecentsModel.getInstance(mContext); + + Intent myHomeIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setPackage(mContext.getPackageName()); + ResolveInfo info = context.getPackageManager().resolveActivity(myHomeIntent, 0); + mMyHomeComponent = new ComponentName(context.getPackageName(), info.activityInfo.name); + + mContext.registerReceiver(mUserPreferenceChangeReceiver, + new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED)); + initOverviewTargets(); + } + + private void initOverviewTargets() { + ComponentName defaultHome = PackageManagerWrapper.getInstance() + .getHomeActivities(new ArrayList<>()); + + final String overviewIntentCategory; + if (defaultHome == null || mMyHomeComponent.equals(defaultHome)) { + // User default home is same as out home app. Use Overview integrated in Launcher. + overviewComponent = mMyHomeComponent; + mActivityControlHelper = new LauncherActivityControllerHelper(); + overviewIntentCategory = Intent.CATEGORY_HOME; + + if (mUpdateRegisteredPackage != null) { + // Remove any update listener as we don't care about other packages. + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + mUpdateRegisteredPackage = null; + } + } else { + // The default home app is a different launcher. Use the fallback Overview instead. + overviewComponent = new ComponentName(mContext, RecentsActivity.class); + mActivityControlHelper = new FallbackActivityControllerHelper(defaultHome); + overviewIntentCategory = Intent.CATEGORY_DEFAULT; + + // User's default home app can change as a result of package updates of this app (such + // as uninstalling the app or removing the "Launcher" feature in an update). + // Listen for package updates of this app (and remove any previously attached + // package listener). + if (!defaultHome.getPackageName().equals(mUpdateRegisteredPackage)) { + if (mUpdateRegisteredPackage != null) { + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + } + + mUpdateRegisteredPackage = defaultHome.getPackageName(); + IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED); + updateReceiver.addAction(ACTION_PACKAGE_CHANGED); + updateReceiver.addAction(ACTION_PACKAGE_REMOVED); + updateReceiver.addDataScheme("package"); + updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage, + PatternMatcher.PATTERN_LITERAL); + mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver); + } + } + + overviewIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(overviewIntentCategory) + .setComponent(overviewComponent) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + public void onDestroy() { + mContext.unregisterReceiver(mUserPreferenceChangeReceiver); + + if (mUpdateRegisteredPackage != null) { + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + mUpdateRegisteredPackage = null; + } + } + + public void onOverviewToggle() { + // If currently screen pinning, do not enter overview + if (mAM.isScreenPinningActive()) { + return; + } + + mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + mMainThreadExecutor.execute(new RecentsActivityCommand<>()); + } + + public void onOverviewShown() { + mMainThreadExecutor.execute(new ShowRecentsCommand()); + } + + public void onTip(int actionType, int viewType) { + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + UserEventDispatcher.newInstance(mContext, + new InvariantDeviceProfile(mContext).getDeviceProfile(mContext)) + .logActionTip(actionType, viewType); + } + }); + } + + public ActivityControlHelper getActivityControlHelper() { + return mActivityControlHelper; + } + + private class ShowRecentsCommand extends RecentsActivityCommand { + + @Override + protected boolean handleCommand(long elapsedTime) { + return mHelper.getVisibleRecentsView() != null; + } + } + + private class RecentsActivityCommand implements Runnable { + + protected final ActivityControlHelper mHelper; + private final long mCreateTime; + private final int mRunningTaskId; + + private ActivityInitListener mListener; + private T mActivity; + private RecentsView mRecentsView; + private final long mToggleClickedTime = SystemClock.uptimeMillis(); + private boolean mUserEventLogged; + + public RecentsActivityCommand() { + mHelper = getActivityControlHelper(); + mCreateTime = SystemClock.elapsedRealtime(); + mRunningTaskId = mAM.getRunningTask().id; + + // Preload the plan + mRecentsModel.loadTasks(mRunningTaskId, null); + } + + @Override + public void run() { + long elapsedTime = mCreateTime - mLastToggleTime; + mLastToggleTime = mCreateTime; + + if (!handleCommand(elapsedTime)) { + // Start overview + if (!mHelper.switchToRecentsIfVisible(true)) { + mListener = mHelper.createActivityInitListener(this::onActivityReady); + mListener.registerAndStartActivity(overviewIntent, this::createWindowAnimation, + mContext, mMainThreadExecutor.getHandler(), RECENTS_LAUNCH_DURATION); + } + } + } + + protected boolean handleCommand(long elapsedTime) { + // TODO: We need to fix this case with PIP, when an activity first enters PIP, it shows + // the menu activity which takes window focus, preventing the right condition from + // being run below + RecentsView recents = mHelper.getVisibleRecentsView(); + if (recents != null) { + // Launch the next task + recents.showNextTask(); + return true; + } else if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) { + // The user tried to launch back into overview too quickly, either after + // launching an app, or before overview has actually shown, just ignore for now + return true; + } + return false; + } + + private boolean onActivityReady(T activity, Boolean wasVisible) { + activity.getOverviewPanel().setCurrentTask(mRunningTaskId); + AbstractFloatingView.closeAllOpenViews(activity, wasVisible); + AnimationFactory factory = mHelper.prepareRecentsUI(activity, wasVisible, + (controller) -> { + controller.dispatchOnStart(); + ValueAnimator anim = controller.getAnimationPlayer() + .setDuration(RECENTS_LAUNCH_DURATION); + anim.setInterpolator(FAST_OUT_SLOW_IN); + anim.start(); + }); + factory.onRemoteAnimationReceived(null); + if (wasVisible) { + factory.createActivityController(RECENTS_LAUNCH_DURATION, INTERACTION_NORMAL); + } + mActivity = activity; + mRecentsView = mActivity.getOverviewPanel(); + mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); + if (!mUserEventLogged) { + activity.getUserEventDispatcher().logActionCommand(Action.Command.RECENTS_BUTTON, + mHelper.getContainerType(), ContainerType.TASKSWITCHER); + mUserEventLogged = true; + } + return false; + } + + private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) { + if (LatencyTrackerCompat.isEnabled(mContext)) { + LatencyTrackerCompat.logToggleRecents( + (int) (SystemClock.uptimeMillis() - mToggleClickedTime)); + } + + if (mListener != null) { + mListener.unregister(); + } + AnimatorSet anim = new AnimatorSet(); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + if (mRecentsView != null) { + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, + true /* animate */); + } + } + }); + if (mActivity == null) { + Log.e(TAG, "Animation created, before activity"); + anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); + return anim; + } + + RemoteAnimationTargetSet targetSet = + new RemoteAnimationTargetSet(targetCompats, MODE_CLOSING); + + // Use the top closing app to determine the insets for the animation + RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId); + if (runningTaskTarget == null) { + Log.e(TAG, "No closing app"); + anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); + return anim; + } + + final ClipAnimationHelper clipHelper = new ClipAnimationHelper(); + + // At this point, the activity is already started and laid-out. Get the home-bounds + // relative to the screen using the rootView of the activity. + int loc[] = new int[2]; + View rootView = mActivity.getRootView(); + rootView.getLocationOnScreen(loc); + Rect homeBounds = new Rect(loc[0], loc[1], + loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight()); + clipHelper.updateSource(homeBounds, runningTaskTarget); + + TransformedRect targetRect = new TransformedRect(); + mHelper.getSwipeUpDestinationAndLength(mActivity.getDeviceProfile(), mActivity, + INTERACTION_NORMAL, targetRect); + clipHelper.updateTargetRect(targetRect); + clipHelper.prepareAnimation(false /* isOpening */); + + SyncRtSurfaceTransactionApplier syncTransactionApplier = + new SyncRtSurfaceTransactionApplier(rootView); + ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); + valueAnimator.setDuration(RECENTS_LAUNCH_DURATION); + valueAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); + valueAnimator.addUpdateListener((v) -> + clipHelper.applyTransform(targetSet, (float) v.getAnimatedValue(), + syncTransactionApplier)); + + if (targetSet.isAnimatingHome()) { + // If we are animating home, fade in the opening targets + RemoteAnimationTargetSet openingSet = + new RemoteAnimationTargetSet(targetCompats, MODE_OPENING); + + TransactionCompat transaction = new TransactionCompat(); + valueAnimator.addUpdateListener((v) -> { + for (RemoteAnimationTargetCompat app : openingSet.apps) { + transaction.setAlpha(app.leash, (float) v.getAnimatedValue()); + } + transaction.apply(); + }); + } + anim.play(valueAnimator); + return anim; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewInteractionState.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewInteractionState.java new file mode 100644 index 0000000000..36ceef5a73 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/OverviewInteractionState.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.provider.Settings; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.util.UiThreadHelper; +import com.android.systemui.shared.recents.ISystemUiProxy; + +import java.util.concurrent.ExecutionException; + +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_QUICK_SCRUB; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_SHOW_OVERVIEW_BUTTON; +import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME; + +/** + * Sets overview interaction flags, such as: + * + * - FLAG_DISABLE_QUICK_SCRUB + * - FLAG_DISABLE_SWIPE_UP + * - FLAG_SHOW_OVERVIEW_BUTTON + * + * @see com.android.systemui.shared.system.NavigationBarCompat.InteractionType and associated flags. + */ +public class OverviewInteractionState { + + private static final String TAG = "OverviewFlags"; + + private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once"; + private static final String SWIPE_UP_SETTING_AVAILABLE_RES_NAME = + "config_swipe_up_gesture_setting_available"; + private static final String SWIPE_UP_ENABLED_DEFAULT_RES_NAME = + "config_swipe_up_gesture_default"; + + // We do not need any synchronization for this variable as its only written on UI thread. + private static OverviewInteractionState INSTANCE; + + public static OverviewInteractionState getInstance(final Context context) { + if (INSTANCE == null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + INSTANCE = new OverviewInteractionState(context.getApplicationContext()); + } else { + try { + return new MainThreadExecutor().submit( + () -> OverviewInteractionState.getInstance(context)).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + return INSTANCE; + } + + private static final int MSG_SET_PROXY = 200; + private static final int MSG_SET_BACK_BUTTON_ALPHA = 201; + private static final int MSG_SET_SWIPE_UP_ENABLED = 202; + + private final SwipeUpGestureEnabledSettingObserver mSwipeUpSettingObserver; + + private final Context mContext; + private final Handler mUiHandler; + private final Handler mBgHandler; + + // These are updated on the background thread + private ISystemUiProxy mISystemUiProxy; + private boolean mSwipeUpEnabled = true; + private float mBackButtonAlpha = 1; + + private Runnable mOnSwipeUpSettingChangedListener; + + private OverviewInteractionState(Context context) { + mContext = context; + + // Data posted to the uihandler will be sent to the bghandler. Data is sent to uihandler + // because of its high send frequency and data may be very different than the previous value + // For example, send back alpha on uihandler to avoid flickering when setting its visibility + mUiHandler = new Handler(this::handleUiMessage); + mBgHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleBgMessage); + + if (getSystemBooleanRes(SWIPE_UP_SETTING_AVAILABLE_RES_NAME)) { + mSwipeUpSettingObserver = new SwipeUpGestureEnabledSettingObserver(mUiHandler, + context.getContentResolver()); + mSwipeUpSettingObserver.register(); + } else { + mSwipeUpSettingObserver = null; + mSwipeUpEnabled = getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME); + } + } + + public boolean isSwipeUpGestureEnabled() { + return mSwipeUpEnabled; + } + + public float getBackButtonAlpha() { + return mBackButtonAlpha; + } + + public void setBackButtonAlpha(float alpha, boolean animate) { + if (!mSwipeUpEnabled) { + alpha = 1; + } + mUiHandler.removeMessages(MSG_SET_BACK_BUTTON_ALPHA); + mUiHandler.obtainMessage(MSG_SET_BACK_BUTTON_ALPHA, animate ? 1 : 0, 0, alpha) + .sendToTarget(); + } + + public void setSystemUiProxy(ISystemUiProxy proxy) { + mBgHandler.obtainMessage(MSG_SET_PROXY, proxy).sendToTarget(); + } + + private boolean handleUiMessage(Message msg) { + if (msg.what == MSG_SET_BACK_BUTTON_ALPHA) { + mBackButtonAlpha = (float) msg.obj; + } + mBgHandler.obtainMessage(msg.what, msg.arg1, msg.arg2, msg.obj).sendToTarget(); + return true; + } + + private boolean handleBgMessage(Message msg) { + switch (msg.what) { + case MSG_SET_PROXY: + mISystemUiProxy = (ISystemUiProxy) msg.obj; + break; + case MSG_SET_BACK_BUTTON_ALPHA: + applyBackButtonAlpha((float) msg.obj, msg.arg1 == 1); + return true; + case MSG_SET_SWIPE_UP_ENABLED: + mSwipeUpEnabled = msg.arg1 != 0; + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); + + if (mOnSwipeUpSettingChangedListener != null) { + mOnSwipeUpSettingChangedListener.run(); + } + break; + } + applyFlags(); + return true; + } + + public void setOnSwipeUpSettingChangedListener(Runnable listener) { + mOnSwipeUpSettingChangedListener = listener; + } + + @WorkerThread + private void applyFlags() { + if (mISystemUiProxy == null) { + return; + } + + int flags = 0; + if (!mSwipeUpEnabled) { + flags = FLAG_DISABLE_SWIPE_UP | FLAG_DISABLE_QUICK_SCRUB | FLAG_SHOW_OVERVIEW_BUTTON; + } + try { + mISystemUiProxy.setInteractionState(flags); + } catch (RemoteException e) { + Log.w(TAG, "Unable to update overview interaction flags", e); + } + } + + @WorkerThread + private void applyBackButtonAlpha(float alpha, boolean animate) { + if (mISystemUiProxy == null) { + return; + } + try { + mISystemUiProxy.setBackButtonAlpha(alpha, animate); + } catch (RemoteException e) { + Log.w(TAG, "Unable to update overview back button alpha", e); + } + } + + private class SwipeUpGestureEnabledSettingObserver extends ContentObserver { + private Handler mHandler; + private ContentResolver mResolver; + private final int defaultValue; + + SwipeUpGestureEnabledSettingObserver(Handler handler, ContentResolver resolver) { + super(handler); + mHandler = handler; + mResolver = resolver; + defaultValue = getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME) ? 1 : 0; + } + + public void register() { + mResolver.registerContentObserver(Settings.Secure.getUriFor(SWIPE_UP_SETTING_NAME), + false, this); + mSwipeUpEnabled = getValue(); + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mHandler.removeMessages(MSG_SET_SWIPE_UP_ENABLED); + mHandler.obtainMessage(MSG_SET_SWIPE_UP_ENABLED, getValue() ? 1 : 0, 0).sendToTarget(); + } + + private boolean getValue() { + return Settings.Secure.getInt(mResolver, SWIPE_UP_SETTING_NAME, defaultValue) == 1; + } + } + + private boolean getSystemBooleanRes(String resName) { + Resources res = Resources.getSystem(); + int resId = res.getIdentifier(resName, "bool", "android"); + + if (resId != 0) { + return res.getBoolean(resId); + } else { + Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); + return false; + } + } + + private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() { + if (mSwipeUpEnabled && !Utilities.getPrefs(mContext).getBoolean( + HAS_ENABLED_QUICKSTEP_ONCE, true)) { + Utilities.getPrefs(mContext).edit() + .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true) + .putBoolean(DiscoveryBounce.HOME_BOUNCE_SEEN, false) + .apply(); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickScrubController.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickScrubController.java new file mode 100644 index 0000000000..9b362c0160 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickScrubController.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.animation.Interpolator; + +import com.android.launcher3.Alarm; +import com.android.launcher3.BaseActivity; +import com.android.launcher3.OnAlarmListener; +import com.android.launcher3.Utilities; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; + +/** + * Responds to quick scrub callbacks to page through and launch recent tasks. + * + * The behavior is to evenly divide the progress into sections, each of which scrolls one page. + * The first and last section set an alarm to auto-advance backwards or forwards, respectively. + */ +public class QuickScrubController implements OnAlarmListener { + + public static final int QUICK_SCRUB_FROM_APP_START_DURATION = 240; + public static final int QUICK_SCRUB_FROM_HOME_START_DURATION = 200; + // We want the translation y to finish faster than the rest of the animation. + public static final float QUICK_SCRUB_TRANSLATION_Y_FACTOR = 5f / 6; + public static final Interpolator QUICK_SCRUB_START_INTERPOLATOR = FAST_OUT_SLOW_IN; + + /** + * Snap to a new page when crossing these thresholds. The first and last auto-advance. + */ + private static final float[] QUICK_SCRUB_THRESHOLDS = new float[] { + 0.05f, 0.20f, 0.35f, 0.50f, 0.65f, 0.80f, 0.95f + }; + + private static final String TAG = "QuickScrubController"; + private static final boolean ENABLE_AUTO_ADVANCE = true; + private static final long AUTO_ADVANCE_DELAY = 500; + private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325; + private static final int QUICKSCRUB_END_SNAP_DURATION_PER_PAGE = 60; + + private final Alarm mAutoAdvanceAlarm; + private final RecentsView mRecentsView; + private final BaseActivity mActivity; + + private boolean mInQuickScrub; + private boolean mWaitingForTaskLaunch; + private int mQuickScrubSection; + private boolean mStartedFromHome; + private boolean mFinishedTransitionToQuickScrub; + private Runnable mOnFinishedTransitionToQuickScrubRunnable; + private ActivityControlHelper mActivityControlHelper; + + public QuickScrubController(BaseActivity activity, RecentsView recentsView) { + mActivity = activity; + mRecentsView = recentsView; + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm = new Alarm(); + mAutoAdvanceAlarm.setOnAlarmListener(this); + } + } + + public void onQuickScrubStart(boolean startingFromHome, ActivityControlHelper controlHelper) { + prepareQuickScrub(TAG); + mInQuickScrub = true; + mStartedFromHome = startingFromHome; + mQuickScrubSection = 0; + mFinishedTransitionToQuickScrub = false; + mActivityControlHelper = controlHelper; + + snapToNextTaskIfAvailable(); + mActivity.getUserEventDispatcher().resetActionDurationMillis(); + } + + public void onQuickScrubEnd() { + mInQuickScrub = false; + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm.cancelAlarm(); + } + int page = mRecentsView.getNextPage(); + Runnable launchTaskRunnable = () -> { + TaskView taskView = mRecentsView.getTaskViewAt(page); + if (taskView != null) { + mWaitingForTaskLaunch = true; + taskView.launchTask(true, (result) -> { + if (!result) { + taskView.notifyTaskLaunchFailed(TAG); + breakOutOfQuickScrub(); + } else { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(Touch.DRAGDROP, + LauncherLogProto.Action.Direction.NONE, page, + TaskUtils.getLaunchComponentKeyForTask(taskView.getTask().key)); + } + mWaitingForTaskLaunch = false; + }, taskView.getHandler()); + } else { + breakOutOfQuickScrub(); + } + mActivityControlHelper = null; + }; + int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen()) + * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE; + if (mRecentsView.getChildCount() > 0 && mRecentsView.snapToPage(page, snapDuration)) { + // Settle on the page then launch it + mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable); + } else { + // No page move needed, just launch it + if (mFinishedTransitionToQuickScrub) { + launchTaskRunnable.run(); + } else { + mOnFinishedTransitionToQuickScrubRunnable = launchTaskRunnable; + } + } + } + + public void cancelActiveQuickscrub() { + if (!mInQuickScrub) { + return; + } + Log.d(TAG, "Quickscrub was active, cancelling"); + mInQuickScrub = false; + mActivityControlHelper = null; + mOnFinishedTransitionToQuickScrubRunnable = null; + mRecentsView.setNextPageSwitchRunnable(null); + } + + /** + * Initializes the UI for quick scrub, returns true if success. + */ + public boolean prepareQuickScrub(String tag) { + if (mWaitingForTaskLaunch || mInQuickScrub) { + Log.d(tag, "Waiting for last scrub to finish, will skip this interaction"); + return false; + } + mOnFinishedTransitionToQuickScrubRunnable = null; + mRecentsView.setNextPageSwitchRunnable(null); + return true; + } + + public boolean isWaitingForTaskLaunch() { + return mWaitingForTaskLaunch; + } + + /** + * Attempts to go to normal overview or back to home, so UI doesn't prevent user interaction. + */ + private void breakOutOfQuickScrub() { + if (mRecentsView.getChildCount() == 0 || mActivityControlHelper == null + || !mActivityControlHelper.switchToRecentsIfVisible(false)) { + mActivity.onBackPressed(); + } + } + + public void onQuickScrubProgress(float progress) { + int quickScrubSection = 0; + for (float threshold : QUICK_SCRUB_THRESHOLDS) { + if (progress < threshold) { + break; + } + quickScrubSection++; + } + if (quickScrubSection != mQuickScrubSection) { + boolean cameFromAutoAdvance = mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length + || mQuickScrubSection == 0; + int pageToGoTo = mRecentsView.getNextPage() + quickScrubSection - mQuickScrubSection; + if (mFinishedTransitionToQuickScrub && !cameFromAutoAdvance) { + goToPageWithHaptic(pageToGoTo); + } + if (ENABLE_AUTO_ADVANCE) { + if (quickScrubSection == QUICK_SCRUB_THRESHOLDS.length || quickScrubSection == 0) { + mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY); + } else { + mAutoAdvanceAlarm.cancelAlarm(); + } + } + mQuickScrubSection = quickScrubSection; + } + } + + public void onFinishedTransitionToQuickScrub() { + mFinishedTransitionToQuickScrub = true; + Runnable action = mOnFinishedTransitionToQuickScrubRunnable; + // Clear the runnable before executing it, to prevent potential recursion. + mOnFinishedTransitionToQuickScrubRunnable = null; + if (action != null) { + action.run(); + } + } + + public void snapToNextTaskIfAvailable() { + if (mInQuickScrub && mRecentsView.getChildCount() > 0) { + int duration = mStartedFromHome ? QUICK_SCRUB_FROM_HOME_START_DURATION + : QUICK_SCRUB_FROM_APP_START_DURATION; + int pageToGoTo = mStartedFromHome ? 0 : mRecentsView.getNextPage() + 1; + goToPageWithHaptic(pageToGoTo, duration, true /* forceHaptic */, + QUICK_SCRUB_START_INTERPOLATOR); + } + } + + private void goToPageWithHaptic(int pageToGoTo) { + goToPageWithHaptic(pageToGoTo, -1 /* overrideDuration */, false /* forceHaptic */, null); + } + + private void goToPageWithHaptic(int pageToGoTo, int overrideDuration, boolean forceHaptic, + Interpolator interpolator) { + pageToGoTo = Utilities.boundToRange(pageToGoTo, 0, mRecentsView.getTaskViewCount() - 1); + boolean snappingToPage = pageToGoTo != mRecentsView.getNextPage(); + if (snappingToPage) { + int duration = overrideDuration > -1 ? overrideDuration + : Math.abs(pageToGoTo - mRecentsView.getNextPage()) + * QUICKSCRUB_SNAP_DURATION_PER_PAGE; + mRecentsView.snapToPage(pageToGoTo, duration, interpolator); + } + if (snappingToPage || forceHaptic) { + mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + } + + @Override + public void onAlarm(Alarm alarm) { + int currPage = mRecentsView.getNextPage(); + boolean recentsVisible = mActivityControlHelper != null + && mActivityControlHelper.getVisibleRecentsView() != null; + if (!recentsVisible) { + Log.w(TAG, "Failed to auto advance; recents not visible"); + return; + } + if (mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length + && currPage < mRecentsView.getTaskViewCount() - 1) { + goToPageWithHaptic(currPage + 1); + } else if (mQuickScrubSection == 0 && currPage > 0) { + goToPageWithHaptic(currPage - 1); + } + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickstepProcessInitializer.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickstepProcessInitializer.java new file mode 100644 index 0000000000..4adddd21f0 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/QuickstepProcessInitializer.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.content.Context; + +import com.android.launcher3.MainProcessInitializer; +import com.android.systemui.shared.system.ThreadedRendererCompat; + +@SuppressWarnings("unused") +public class QuickstepProcessInitializer extends MainProcessInitializer { + + public QuickstepProcessInitializer(Context context) { } + + @Override + protected void init(Context context) { + super.init(context); + + // Elevate GPU priority for Quickstep and Remote animations. + ThreadedRendererCompat.setContextPriority(ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivity.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivity.java new file mode 100644 index 0000000000..3247abe123 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivity.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.app.ActivityOptions; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAnimationRunner; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.badge.BadgeInfo; +import com.android.launcher3.uioverrides.UiFactory; +import com.android.launcher3.util.SystemUiController; +import com.android.launcher3.util.Themes; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.fallback.RecentsRootView; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_PRE_DELAY; +import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator; +import static com.android.quickstep.TaskUtils.taskIsATargetWithMode; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +/** + * A simple activity to show the recently launched tasks + */ +public class RecentsActivity extends BaseDraggingActivity { + + private Handler mUiHandler = new Handler(Looper.getMainLooper()); + private RecentsRootView mRecentsRootView; + private FallbackRecentsView mFallbackRecentsView; + + private Configuration mOldConfig; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mOldConfig = new Configuration(getResources().getConfiguration()); + initDeviceProfile(); + + setContentView(R.layout.fallback_recents_activity); + mRecentsRootView = findViewById(R.id.drag_layer); + mFallbackRecentsView = findViewById(R.id.overview_panel); + + mRecentsRootView.setup(); + + getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW, + Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText)); + RecentsActivityTracker.onRecentsActivityCreate(this); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + int diff = newConfig.diff(mOldConfig); + if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) { + onHandleConfigChanged(); + } + mOldConfig.setTo(newConfig); + super.onConfigurationChanged(newConfig); + } + + @Override + public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) { + onHandleConfigChanged(); + super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig); + } + + public void onRootViewSizeChanged() { + if (isInMultiWindowModeCompat()) { + onHandleConfigChanged(); + } + } + + private void onHandleConfigChanged() { + mUserEventDispatcher = null; + initDeviceProfile(); + + AbstractFloatingView.closeOpenViews(this, true, + AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); + dispatchDeviceProfileChanged(); + + mRecentsRootView.setup(); + reapplyUi(); + } + + @Override + protected void reapplyUi() { + mRecentsRootView.dispatchInsets(); + } + + private void initDeviceProfile() { + // In case we are reusing IDP, create a copy so that we dont conflict with Launcher + // activity. + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + if (isInMultiWindowModeCompat()) { + InvariantDeviceProfile idp = appState == null + ? new InvariantDeviceProfile(this) : appState.getInvariantDeviceProfile(); + DeviceProfile dp = idp.getDeviceProfile(this); + mDeviceProfile = mRecentsRootView == null ? dp.copy(this) + : dp.getMultiWindowProfile(this, mRecentsRootView.getLastKnownSize()); + } else { + // If we are reusing the Invariant device profile, make a copy. + mDeviceProfile = appState == null + ? new InvariantDeviceProfile(this).getDeviceProfile(this) + : appState.getInvariantDeviceProfile().getDeviceProfile(this).copy(this); + } + onDeviceProfileInitiated(); + } + + @Override + public BaseDragLayer getDragLayer() { + return mRecentsRootView; + } + + @Override + public View getRootView() { + return mRecentsRootView; + } + + @Override + public T getOverviewPanel() { + return (T) mFallbackRecentsView; + } + + @Override + public BadgeInfo getBadgeInfoForItem(ItemInfo info) { + return null; + } + + @Override + public ActivityOptions getActivityLaunchOptions(final View v) { + if (!(v instanceof TaskView)) { + return null; + } + + final TaskView taskView = (TaskView) v; + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler, + true /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + result.setAnimation(composeRecentsLaunchAnimator(taskView, targetCompats)); + } + }; + return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat( + runner, RECENTS_LAUNCH_DURATION, + RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION + - STATUS_BAR_TRANSITION_PRE_DELAY)); + } + + /** + * Composes the animations for a launch from the recents list if possible. + */ + private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView, + RemoteAnimationTargetCompat[] targets) { + AnimatorSet target = new AnimatorSet(); + boolean activityClosing = taskIsATargetWithMode(targets, getTaskId(), MODE_CLOSING); + ClipAnimationHelper helper = new ClipAnimationHelper(); + target.play(getRecentsWindowAnimator(taskView, !activityClosing, targets, helper) + .setDuration(RECENTS_LAUNCH_DURATION)); + + // Found a visible recents task that matches the opening app, lets launch the app from there + if (activityClosing) { + Animator adjacentAnimation = mFallbackRecentsView + .createAdjacentPageAnimForTaskLaunch(taskView, helper); + adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); + adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION); + adjacentAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mFallbackRecentsView.resetTaskVisuals(); + } + }); + target.play(adjacentAnimation); + } + return target; + } + + @Override + public void invalidateParent(ItemInfo info) { } + + @Override + protected void onStart() { + // Set the alpha to 1 before calling super, as it may get set back to 0 due to + // onActivityStart callback. + mFallbackRecentsView.setContentAlpha(1); + super.onStart(); + UiFactory.onStart(this); + mFallbackRecentsView.resetTaskVisuals(); + } + + @Override + protected void onStop() { + super.onStop(); + + // Workaround for b/78520668, explicitly trim memory once UI is hidden + onTrimMemory(TRIM_MEMORY_UI_HIDDEN); + } + + @Override + public void onEnterAnimationComplete() { + super.onEnterAnimationComplete(); + UiFactory.onEnterAnimationComplete(this); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + UiFactory.onTrimMemory(this, level); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + RecentsActivityTracker.onRecentsActivityNewIntent(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + RecentsActivityTracker.onRecentsActivityDestroy(this); + } + + @Override + public void onBackPressed() { + // TODO: Launch the task we came from + startHome(); + } + + public void startHome() { + startActivity(new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.println(prefix + "Misc:"); + dumpMisc(writer); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivityTracker.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivityTracker.java new file mode 100644 index 0000000000..9e73691273 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsActivityTracker.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; + +import com.android.launcher3.MainThreadExecutor; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.util.RemoteAnimationProvider; + +import java.lang.ref.WeakReference; +import java.util.function.BiPredicate; + +/** + * Utility class to track create/destroy for RecentsActivity + */ +@TargetApi(Build.VERSION_CODES.P) +public class RecentsActivityTracker implements ActivityInitListener { + + private static WeakReference sCurrentActivity = new WeakReference<>(null); + private static final Scheduler sScheduler = new Scheduler(); + + private final BiPredicate mOnInitListener; + + public RecentsActivityTracker(BiPredicate onInitListener) { + mOnInitListener = onInitListener; + } + + @Override + public void register() { + sScheduler.schedule(this); + } + + @Override + public void unregister() { + sScheduler.clearReference(this); + } + + private boolean init(RecentsActivity activity, boolean visible) { + return mOnInitListener.test(activity, visible); + } + + public static RecentsActivity getCurrentActivity() { + return sCurrentActivity.get(); + } + + @Override + public void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, + Context context, Handler handler, long duration) { + register(); + + Bundle options = animProvider.toActivityOptions(handler, duration).toBundle(); + context.startActivity(intent, options); + } + + public static void onRecentsActivityCreate(RecentsActivity activity) { + sCurrentActivity = new WeakReference<>(activity); + sScheduler.initIfPending(activity, false); + } + + + public static void onRecentsActivityNewIntent(RecentsActivity activity) { + sScheduler.initIfPending(activity, activity.isStarted()); + } + + public static void onRecentsActivityDestroy(RecentsActivity activity) { + if (sCurrentActivity.get() == activity) { + sCurrentActivity.clear(); + } + } + + + private static class Scheduler implements Runnable { + + private WeakReference mPendingTracker = new WeakReference<>(null); + private MainThreadExecutor mMainThreadExecutor; + + public synchronized void schedule(RecentsActivityTracker tracker) { + mPendingTracker = new WeakReference<>(tracker); + if (mMainThreadExecutor == null) { + mMainThreadExecutor = new MainThreadExecutor(); + } + mMainThreadExecutor.execute(this); + } + + @Override + public void run() { + RecentsActivity activity = sCurrentActivity.get(); + if (activity != null) { + initIfPending(activity, activity.isStarted()); + } + } + + public synchronized boolean initIfPending(RecentsActivity activity, boolean alreadyOnHome) { + RecentsActivityTracker tracker = mPendingTracker.get(); + if (tracker != null) { + if (!tracker.init(activity, alreadyOnHome)) { + mPendingTracker.clear(); + } + return true; + } + return false; + } + + public synchronized boolean clearReference(RecentsActivityTracker tracker) { + if (mPendingTracker.get() == tracker) { + mPendingTracker.clear(); + return true; + } + return false; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsAnimationWrapper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsAnimationWrapper.java new file mode 100644 index 0000000000..753cb4bb6a --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsAnimationWrapper.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import com.android.launcher3.util.LooperExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.launcher3.util.UiThreadHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; + +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; + +/** + * Wrapper around RecentsAnimationController to help with some synchronization + */ +public class RecentsAnimationWrapper { + + // A list of callbacks to run when we receive the recents animation target. There are different + // than the state callbacks as these run on the current worker thread. + private final ArrayList mCallbacks = new ArrayList<>(); + + public RemoteAnimationTargetSet targetSet; + + private RecentsAnimationControllerCompat mController; + private boolean mInputConsumerEnabled = false; + private boolean mBehindSystemBars = true; + private boolean mSplitScreenMinimized = false; + + private final ExecutorService mExecutorService = + new LooperExecutor(UiThreadHelper.getBackgroundLooper()); + + public synchronized void setController( + RecentsAnimationControllerCompat controller, RemoteAnimationTargetSet targetSet) { + TraceHelper.partitionSection("RecentsController", "Set controller " + controller); + this.mController = controller; + this.targetSet = targetSet; + + if (controller == null) { + return; + } + if (mInputConsumerEnabled) { + enableInputConsumer(); + } + + if (!mCallbacks.isEmpty()) { + for (Runnable action : new ArrayList<>(mCallbacks)) { + action.run(); + } + mCallbacks.clear(); + } + } + + public synchronized void runOnInit(Runnable action) { + if (targetSet == null) { + mCallbacks.add(action); + } else { + action.run(); + } + } + + /** + * @param onFinishComplete A callback that runs after the animation controller has finished + * on the background thread. + */ + public void finish(boolean toHome, Runnable onFinishComplete) { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + mController = null; + TraceHelper.endSection("RecentsController", + "Finish " + controller + ", toHome=" + toHome); + if (controller != null) { + controller.setInputConsumerEnabled(false); + controller.finish(toHome); + if (onFinishComplete != null) { + onFinishComplete.run(); + } + } + }); + } + + public void enableInputConsumer() { + mInputConsumerEnabled = true; + if (mInputConsumerEnabled) { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Enabling consumer on " + controller); + if (controller != null) { + controller.setInputConsumerEnabled(true); + } + }); + } + } + + public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) { + if (mBehindSystemBars == behindSystemBars) { + return; + } + mBehindSystemBars = behindSystemBars; + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Setting behind system bars on " + controller); + if (controller != null) { + controller.setAnimationTargetsBehindSystemBars(behindSystemBars); + } + }); + } + + /** + * NOTE: As a workaround for conflicting animations (Launcher animating the task leash, and + * SystemUI resizing the docked stack, which resizes the task), we currently only set the + * minimized mode, and not the inverse. + * TODO: Synchronize the minimize animation with the launcher animation + */ + public void setSplitScreenMinimizedForTransaction(boolean minimized) { + if (mSplitScreenMinimized || !minimized) { + return; + } + mSplitScreenMinimized = minimized; + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Setting minimize dock on " + controller); + if (controller != null) { + controller.setSplitScreenMinimized(minimized); + } + }); + } + + public void hideCurrentInputMethod() { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Hiding currentinput method on " + controller); + if (controller != null) { + controller.hideCurrentInputMethod(); + } + }); + } + + public RecentsAnimationControllerCompat getController() { + return mController; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsModel.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsModel.java new file mode 100644 index 0000000000..e0ea597f4d --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RecentsModel.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.ComponentCallbacks2; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.util.LruCache; +import android.util.SparseArray; +import android.view.accessibility.AccessibilityManager; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.R; +import com.android.launcher3.util.Preconditions; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.recents.model.IconLoader; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.PreloadOptions; +import com.android.systemui.shared.recents.model.RecentsTaskLoader; +import com.android.systemui.shared.recents.model.TaskKeyLruCache; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.TaskStackChangeListener; + +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; + +/** + * Singleton class to load and manage recents model. + */ +@TargetApi(Build.VERSION_CODES.O) +public class RecentsModel extends TaskStackChangeListener { + // We do not need any synchronization for this variable as its only written on UI thread. + private static RecentsModel INSTANCE; + + public static RecentsModel getInstance(final Context context) { + if (INSTANCE == null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + INSTANCE = new RecentsModel(context.getApplicationContext()); + } else { + try { + return new MainThreadExecutor().submit( + () -> RecentsModel.getInstance(context)).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + return INSTANCE; + } + + private final SparseArray mCachedAssistData = new SparseArray<>(1); + private final ArrayList mAssistDataListeners = new ArrayList<>(); + + private final Context mContext; + private final RecentsTaskLoader mRecentsTaskLoader; + private final MainThreadExecutor mMainThreadExecutor; + + private RecentsTaskLoadPlan mLastLoadPlan; + private int mLastLoadPlanId; + private int mTaskChangeId; + private ISystemUiProxy mSystemUiProxy; + private boolean mClearAssistCacheOnStackChange = true; + private final boolean mIsLowRamDevice; + private boolean mPreloadTasksInBackground; + private final AccessibilityManager mAccessibilityManager; + + private RecentsModel(Context context) { + mContext = context; + + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + mIsLowRamDevice = activityManager.isLowRamDevice(); + mMainThreadExecutor = new MainThreadExecutor(); + + Resources res = context.getResources(); + mRecentsTaskLoader = new RecentsTaskLoader(mContext, + res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize), + res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0) { + + @Override + protected IconLoader createNewIconLoader(Context context, + TaskKeyLruCache iconCache, + LruCache activityInfoCache) { + return new NormalizedIconLoader(context, iconCache, activityInfoCache); + } + }; + mRecentsTaskLoader.startLoader(mContext); + ActivityManagerWrapper.getInstance().registerTaskStackListener(this); + + mTaskChangeId = 1; + loadTasks(-1, null); + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + } + + public RecentsTaskLoader getRecentsTaskLoader() { + return mRecentsTaskLoader; + } + + /** + * Preloads the task plan + * @param taskId The running task id or -1 + * @param callback The callback to receive the task plan once its complete or null. This is + * always called on the UI thread. + * @return the request id associated with this call. + */ + public int loadTasks(int taskId, Consumer callback) { + final int requestId = mTaskChangeId; + + // Fail fast if nothing has changed. + if (mLastLoadPlanId == mTaskChangeId) { + if (callback != null) { + final RecentsTaskLoadPlan plan = mLastLoadPlan; + mMainThreadExecutor.execute(() -> callback.accept(plan)); + } + return requestId; + } + + BackgroundExecutor.get().submit(() -> { + // Preload the plan + RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(mContext); + PreloadOptions opts = new PreloadOptions(); + opts.loadTitles = mAccessibilityManager.isEnabled(); + loadPlan.preloadPlan(opts, mRecentsTaskLoader, taskId, UserHandle.myUserId()); + // Set the load plan on UI thread + mMainThreadExecutor.execute(() -> { + mLastLoadPlan = loadPlan; + mLastLoadPlanId = requestId; + + if (callback != null) { + callback.accept(loadPlan); + } + }); + }); + return requestId; + } + + public void setPreloadTasksInBackground(boolean preloadTasksInBackground) { + mPreloadTasksInBackground = preloadTasksInBackground && !mIsLowRamDevice; + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + mTaskChangeId++; + } + + @Override + public void onActivityUnpinned() { + mTaskChangeId++; + } + + @Override + public void onTaskStackChanged() { + mTaskChangeId++; + + Preconditions.assertUIThread(); + if (mClearAssistCacheOnStackChange) { + mCachedAssistData.clear(); + } else { + mClearAssistCacheOnStackChange = true; + } + } + + @Override + public void onTaskStackChangedBackground() { + int userId = UserHandle.myUserId(); + if (!mPreloadTasksInBackground || !checkCurrentOrManagedUserId(userId, mContext)) { + // TODO: Only register this for the current user + return; + } + + // Preload a fixed number of task icons/thumbnails in the background + ActivityManager.RunningTaskInfo runningTaskInfo = + ActivityManagerWrapper.getInstance().getRunningTask(); + RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(mContext); + RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); + launchOpts.runningTaskId = runningTaskInfo != null ? runningTaskInfo.id : -1; + launchOpts.numVisibleTasks = 2; + launchOpts.numVisibleTaskThumbnails = 2; + launchOpts.onlyLoadForCache = true; + launchOpts.onlyLoadPausedActivities = true; + launchOpts.loadThumbnails = true; + PreloadOptions preloadOpts = new PreloadOptions(); + preloadOpts.loadTitles = mAccessibilityManager.isEnabled(); + plan.preloadPlan(preloadOpts, mRecentsTaskLoader, -1, userId); + mRecentsTaskLoader.loadTasks(plan, launchOpts); + } + + public boolean isLoadPlanValid(int resultId) { + return mTaskChangeId == resultId; + } + + public RecentsTaskLoadPlan getLastLoadPlan() { + return mLastLoadPlan; + } + + public void setSystemUiProxy(ISystemUiProxy systemUiProxy) { + mSystemUiProxy = systemUiProxy; + } + + public ISystemUiProxy getSystemUiProxy() { + return mSystemUiProxy; + } + + public void onStart() { + mRecentsTaskLoader.startLoader(mContext); + } + + public void onTrimMemory(int level) { + if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + // We already stop the loader in UI_HIDDEN, so stop the high res loader as well + mRecentsTaskLoader.getHighResThumbnailLoader().setVisible(false); + } + mRecentsTaskLoader.onTrimMemory(level); + } + + public void onOverviewShown(boolean fromHome, String tag) { + if (mSystemUiProxy == null) { + return; + } + try { + mSystemUiProxy.onOverviewShown(fromHome); + } catch (RemoteException e) { + Log.w(tag, + "Failed to notify SysUI of overview shown from " + (fromHome ? "home" : "app") + + ": ", e); + } + } + + public void resetAssistCache() { + mCachedAssistData.clear(); + } + + @WorkerThread + public void preloadAssistData(int taskId, Bundle data) { + mMainThreadExecutor.execute(() -> { + mCachedAssistData.put(taskId, data); + // We expect a stack change callback after the assist data is set. So ignore the + // very next stack change callback. + mClearAssistCacheOnStackChange = false; + + int count = mAssistDataListeners.size(); + for (int i = 0; i < count; i++) { + mAssistDataListeners.get(i).onAssistDataReceived(taskId); + } + }); + } + + public Bundle getAssistData(int taskId) { + Preconditions.assertUIThread(); + return mCachedAssistData.get(taskId); + } + + public void addAssistDataListener(AssistDataListener listener) { + mAssistDataListeners.add(listener); + } + + public void removeAssistDataListener(AssistDataListener listener) { + mAssistDataListeners.remove(listener); + } + + /** + * Callback for receiving assist data + */ + public interface AssistDataListener { + + void onAssistDataReceived(int taskId); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RemoteRunnable.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RemoteRunnable.java new file mode 100644 index 0000000000..1385f927f3 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/RemoteRunnable.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.os.RemoteException; +import android.util.Log; + +@FunctionalInterface +public interface RemoteRunnable { + + void run() throws RemoteException; + + static void executeSafely(RemoteRunnable r) { + try { + r.run(); + } catch (final RemoteException e) { + Log.e("RemoteRunnable", "Error calling remote method", e); + } + } +} \ No newline at end of file diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskOverlayFactory.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskOverlayFactory.java new file mode 100644 index 0000000000..c83dce6399 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskOverlayFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.content.Context; +import android.graphics.Matrix; +import android.support.annotation.AnyThread; +import android.view.View; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Preconditions; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +/** + * Factory class to create and add an overlays on the TaskView + */ +public class TaskOverlayFactory { + + private static TaskOverlayFactory sInstance; + + public static TaskOverlayFactory get(Context context) { + Preconditions.assertUIThread(); + if (sInstance == null) { + sInstance = Utilities.getOverrideObject(TaskOverlayFactory.class, + context.getApplicationContext(), R.string.task_overlay_factory_class); + } + return sInstance; + } + + @AnyThread + public boolean needAssist() { + return false; + } + + public TaskOverlay createOverlay(View thumbnailView) { + return new TaskOverlay(); + } + + public static class TaskOverlay { + + public void setTaskInfo(Task task, ThumbnailData thumbnail, Matrix matrix) { } + + public void reset() { } + + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskSystemShortcut.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskSystemShortcut.java new file mode 100644 index 0000000000..c6e98fc157 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskSystemShortcut.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; +import android.view.View; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.popup.SystemShortcut; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.InstantAppResolver; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat; +import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture; +import com.android.systemui.shared.recents.view.RecentsTransition; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP; + +/** + * Represents a system shortcut that can be shown for a recent task. + */ +public class TaskSystemShortcut extends SystemShortcut { + + private static final String TAG = "TaskSystemShortcut"; + + protected T mSystemShortcut; + + protected TaskSystemShortcut(T systemShortcut) { + super(systemShortcut.iconResId, systemShortcut.labelResId); + mSystemShortcut = systemShortcut; + } + + protected TaskSystemShortcut(int iconResId, int labelResId) { + super(iconResId, labelResId); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, ItemInfo itemInfo) { + return null; + } + + public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) { + Task task = view.getTask(); + + ShortcutInfo dummyInfo = new ShortcutInfo(); + dummyInfo.intent = new Intent(); + ComponentName component = task.getTopComponent(); + dummyInfo.intent.setComponent(component); + dummyInfo.user = UserHandle.of(task.key.userId); + dummyInfo.title = TaskUtils.getTitle(activity, task); + + return getOnClickListenerForTask(activity, task, dummyInfo); + } + + protected View.OnClickListener getOnClickListenerForTask( + BaseDraggingActivity activity, Task task, ItemInfo dummyInfo) { + return mSystemShortcut.getOnClickListener(activity, dummyInfo); + } + + public static class AppInfo extends TaskSystemShortcut { + public AppInfo() { + super(new SystemShortcut.AppInfo()); + } + } + + public static class SplitScreen extends TaskSystemShortcut { + + private Handler mHandler; + + public SplitScreen() { + super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, TaskView taskView) { + if (activity.getDeviceProfile().isMultiWindowMode) { + return null; + } + final Task task = taskView.getTask(); + final int taskId = task.key.id; + if (!task.isDockable) { + return null; + } + final RecentsView recentsView = activity.getOverviewPanel(); + + final TaskThumbnailView thumbnailView = taskView.getThumbnail(); + return (v -> { + final View.OnLayoutChangeListener onLayoutChangeListener = + new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int l, int t, int r, int b, + int oldL, int oldT, int oldR, int oldB) { + taskView.getRootView().removeOnLayoutChangeListener(this); + recentsView.removeIgnoreResetTask(taskView); + + // Start animating in the side pages once launcher has been resized + recentsView.dismissTask(taskView, false, false); + } + }; + + final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener = + new DeviceProfile.OnDeviceProfileChangeListener() { + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + activity.removeOnDeviceProfileChangeListener(this); + if (dp.isMultiWindowMode) { + taskView.getRootView().addOnLayoutChangeListener( + onLayoutChangeListener); + } + } + }; + + dismissTaskMenuView(activity); + + final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(); + if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) { + return; + } + boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT; + if (ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId, + ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft))) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + try { + sysUiProxy.onSplitScreenInvoked(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to notify SysUI of split screen: ", e); + return; + } + activity.getUserEventDispatcher().logActionOnControl(TAP, + LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET); + // Add a device profile change listener to kick off animating the side tasks + // once we enter multiwindow mode and relayout + activity.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener); + + final Runnable animStartedListener = () -> { + // Hide the task view and wait for the window to be resized + // TODO: Consider animating in launcher and do an in-place start activity + // afterwards + recentsView.addIgnoreResetTask(taskView); + taskView.setAlpha(0f); + }; + + final int[] position = new int[2]; + thumbnailView.getLocationOnScreen(position); + final int width = (int) (thumbnailView.getWidth() * taskView.getScaleX()); + final int height = (int) (thumbnailView.getHeight() * taskView.getScaleY()); + final Rect taskBounds = new Rect(position[0], position[1], + position[0] + width, position[1] + height); + + // Take the thumbnail of the task without a scrim and apply it back after + float alpha = thumbnailView.getDimAlpha(); + thumbnailView.setDimAlpha(0); + Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap( + taskBounds.width(), taskBounds.height(), thumbnailView, 1f, + Color.BLACK); + thumbnailView.setDimAlpha(alpha); + + AppTransitionAnimationSpecsFuture future = + new AppTransitionAnimationSpecsFuture(mHandler) { + @Override + public List composeSpecs() { + return Collections.singletonList(new AppTransitionAnimationSpecCompat( + taskId, thumbnail, taskBounds)); + } + }; + WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture( + future, animStartedListener, mHandler, true /* scaleUp */); + } + }); + } + } + + public static class Pin extends TaskSystemShortcut { + + private static final String TAG = Pin.class.getSimpleName(); + + private Handler mHandler; + + public Pin() { + super(R.drawable.ic_pin, R.string.recent_task_option_pin); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public View.OnClickListener getOnClickListener( + BaseDraggingActivity activity, TaskView taskView) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + if (sysUiProxy == null) { + return null; + } + if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) { + return null; + } + if (ActivityManagerWrapper.getInstance().isLockToAppActive()) { + // We shouldn't be able to pin while an app is locked. + return null; + } + return view -> { + Consumer resultCallback = success -> { + if (success) { + try { + sysUiProxy.startScreenPinning(taskView.getTask().key.id); + } catch (RemoteException e) { + Log.w(TAG, "Failed to start screen pinning: ", e); + } + } else { + taskView.notifyTaskLaunchFailed(TAG); + } + }; + taskView.launchTask(true, resultCallback, mHandler); + dismissTaskMenuView(activity); + }; + } + } + + public static class Install extends TaskSystemShortcut { + public Install() { + super(new SystemShortcut.Install()); + } + + @Override + protected View.OnClickListener getOnClickListenerForTask( + BaseDraggingActivity activity, Task task, ItemInfo itemInfo) { + if (InstantAppResolver.newInstance(activity).isInstantApp(activity, + task.getTopComponent().getPackageName())) { + return mSystemShortcut.createOnClickListener(activity, itemInfo); + } + return null; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskUtils.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskUtils.java new file mode 100644 index 0000000000..3b8ee22bca --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TaskUtils.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep; + +import android.animation.ValueAnimator; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.RectF; +import android.os.UserHandle; +import android.util.Log; +import android.view.View; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.util.ComponentKey; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; + +import java.util.List; + +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +/** + * Contains helpful methods for retrieving data from {@link Task}s. + */ +public class TaskUtils { + + private static final String TAG = "TaskUtils"; + + /** + * TODO: remove this once we switch to getting the icon and label from IconCache. + */ + public static CharSequence getTitle(Context context, Task task) { + LauncherAppsCompat launcherAppsCompat = LauncherAppsCompat.getInstance(context); + UserManagerCompat userManagerCompat = UserManagerCompat.getInstance(context); + PackageManager packageManager = context.getPackageManager(); + UserHandle user = UserHandle.of(task.key.userId); + ApplicationInfo applicationInfo = launcherAppsCompat.getApplicationInfo( + task.getTopComponent().getPackageName(), 0, user); + if (applicationInfo == null) { + Log.e(TAG, "Failed to get title for task " + task); + return ""; + } + return userManagerCompat.getBadgedLabelForUser( + applicationInfo.loadLabel(packageManager), user); + } + + public static ComponentKey getLaunchComponentKeyForTask(Task.TaskKey taskKey) { + final ComponentName cn = taskKey.sourceComponent != null + ? taskKey.sourceComponent + : taskKey.getComponent(); + return new ComponentKey(cn, UserHandle.of(taskKey.userId)); + } + + + /** + * Try to find a TaskView that corresponds with the component of the launched view. + * + * If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation. + * Otherwise, we will assume we are using a normal app transition, but it's possible that the + * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView. + */ + public static TaskView findTaskViewToLaunch( + BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) { + if (v instanceof TaskView) { + return (TaskView) v; + } + RecentsView recentsView = activity.getOverviewPanel(); + + // It's possible that the launched view can still be resolved to a visible task view, check + // the task id of the opening task and see if we can find a match. + if (v.getTag() instanceof ItemInfo) { + ItemInfo itemInfo = (ItemInfo) v.getTag(); + ComponentName componentName = itemInfo.getTargetComponent(); + int userId = itemInfo.user.getIdentifier(); + if (componentName != null) { + for (int i = 0; i < recentsView.getTaskViewCount(); i++) { + TaskView taskView = recentsView.getTaskViewAt(i); + if (recentsView.isTaskViewVisible(taskView)) { + Task.TaskKey key = taskView.getTask().key; + if (componentName.equals(key.getComponent()) && userId == key.userId) { + return taskView; + } + } + } + } + } + + if (targets == null) { + return null; + } + // Resolve the opening task id + int openingTaskId = -1; + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == MODE_OPENING) { + openingTaskId = target.taskId; + break; + } + } + + // If there is no opening task id, fall back to the normal app icon launch animation + if (openingTaskId == -1) { + return null; + } + + // If the opening task id is not currently visible in overview, then fall back to normal app + // icon launch animation + TaskView taskView = recentsView.getTaskView(openingTaskId); + if (taskView == null || !recentsView.isTaskViewVisible(taskView)) { + return null; + } + return taskView; + } + + /** + * @return Animator that controls the window of the opening targets for the recents launch + * animation. + */ + public static ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges, + RemoteAnimationTargetCompat[] targets, final ClipAnimationHelper inOutHelper) { + SyncRtSurfaceTransactionApplier syncTransactionApplier = + new SyncRtSurfaceTransactionApplier(v); + final ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); + appAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); + appAnimator.addUpdateListener(new MultiValueUpdateListener() { + + // Defer fading out the view until after the app window gets faded in + final FloatProp mViewAlpha = new FloatProp(1f, 0f, 75, 75, LINEAR); + final FloatProp mTaskAlpha = new FloatProp(0f, 1f, 0, 75, LINEAR); + + final RemoteAnimationTargetSet mTargetSet; + + final RectF mThumbnailRect; + + { + mTargetSet = new RemoteAnimationTargetSet(targets, MODE_OPENING); + inOutHelper.setTaskAlphaCallback((t, alpha) -> mTaskAlpha.value); + + inOutHelper.prepareAnimation(true /* isOpening */); + inOutHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), + mTargetSet.apps.length == 0 ? null : mTargetSet.apps[0]); + + mThumbnailRect = new RectF(inOutHelper.getTargetRect()); + mThumbnailRect.offset(-v.getTranslationX(), -v.getTranslationY()); + Utilities.scaleRectFAboutCenter(mThumbnailRect, 1 / v.getScaleX()); + } + + @Override + public void onUpdate(float percent) { + RectF taskBounds = inOutHelper.applyTransform(mTargetSet, 1 - percent, + syncTransactionApplier); + if (!skipViewChanges) { + float scale = taskBounds.width() / mThumbnailRect.width(); + v.setScaleX(scale); + v.setScaleY(scale); + v.setTranslationX(taskBounds.centerX() - mThumbnailRect.centerX()); + v.setTranslationY(taskBounds.centerY() - mThumbnailRect.centerY()); + v.setAlpha(mViewAlpha.value); + } + } + }); + return appAnimator; + } + + public static boolean taskIsATargetWithMode(RemoteAnimationTargetCompat[] targets, + int taskId, int mode) { + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == mode && target.taskId == taskId) { + return true; + } + } + return false; + } + + public static boolean checkCurrentOrManagedUserId(int currentUserId, Context context) { + if (currentUserId == UserHandle.myUserId()) { + return true; + } + List allUsers = UserManagerCompat.getInstance(context).getUserProfiles(); + for (int i = allUsers.size() - 1; i >= 0; i--) { + if (currentUserId == allUsers.get(i).getIdentifier()) { + return true; + } + } + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchConsumer.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchConsumer.java new file mode 100644 index 0000000000..faeb205279 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchConsumer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.os.Build; +import android.support.annotation.IntDef; +import android.view.Choreographer; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.Consumer; + +@TargetApi(Build.VERSION_CODES.O) +@FunctionalInterface +public interface TouchConsumer extends Consumer { + + @IntDef(flag = true, value = { + INTERACTION_NORMAL, + INTERACTION_QUICK_SCRUB + }) + @Retention(RetentionPolicy.SOURCE) + @interface InteractionType {} + int INTERACTION_NORMAL = 0; + int INTERACTION_QUICK_SCRUB = 1; + + default void reset() { } + + default void updateTouchTracking(@InteractionType int interactionType) { } + + default void onQuickScrubEnd() { } + + default void onQuickScrubProgress(float progress) { } + + default void onQuickStep(MotionEvent ev) { } + + default void onCommand(int command) { } + + /** + * Called on the binder thread to allow the consumer to process the motion event before it is + * posted on a handler thread. + */ + default void preProcessMotionEvent(MotionEvent ev) { } + + default Choreographer getIntrimChoreographer(MotionEventQueue queue) { + return null; + } + + default void deferInit() { } + + default boolean deferNextEventToMainThread() { + return false; + } + + default boolean forceToLauncherConsumer() { + return false; + } + + default void onShowOverviewFromAltTab() {} +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchInteractionService.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchInteractionService.java new file mode 100644 index 0000000000..b44cfb6b7e --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/TouchInteractionService.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.Service; +import android.content.Intent; +import android.graphics.PointF; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; +import android.view.Choreographer; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.util.TraceHelper; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.recents.IOverviewProxy; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.ChoreographerCompat; +import com.android.systemui.shared.system.NavigationBarCompat.HitTarget; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_POINTER_DOWN; +import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE; + +/** + * Service connected by system-UI for handling touch interaction. + */ +@TargetApi(Build.VERSION_CODES.O) +public class TouchInteractionService extends Service { + + private static final SparseArray sMotionEventNames; + + static { + sMotionEventNames = new SparseArray<>(3); + sMotionEventNames.put(ACTION_DOWN, "ACTION_DOWN"); + sMotionEventNames.put(ACTION_UP, "ACTION_UP"); + sMotionEventNames.put(ACTION_CANCEL, "ACTION_CANCEL"); + } + + public static final int EDGE_NAV_BAR = 1 << 8; + + private static final String TAG = "TouchInteractionService"; + + /** + * A background thread used for handling UI for another window. + */ + private static HandlerThread sRemoteUiThread; + + private final IBinder mMyBinder = new IOverviewProxy.Stub() { + + @Override + public void onPreMotionEvent(@HitTarget int downHitTarget) throws RemoteException { + TraceHelper.beginSection("SysUiBinder"); + setupTouchConsumer(downHitTarget); + TraceHelper.partitionSection("SysUiBinder", "Down target " + downHitTarget); + } + + @Override + public void onMotionEvent(MotionEvent ev) { + mEventQueue.queue(ev); + + String name = sMotionEventNames.get(ev.getActionMasked()); + if (name != null){ + TraceHelper.partitionSection("SysUiBinder", name); + } + } + + @Override + public void onBind(ISystemUiProxy iSystemUiProxy) { + mISystemUiProxy = iSystemUiProxy; + mRecentsModel.setSystemUiProxy(mISystemUiProxy); + mOverviewInteractionState.setSystemUiProxy(mISystemUiProxy); + } + + @Override + public void onQuickScrubStart() { + mEventQueue.onQuickScrubStart(); + TraceHelper.partitionSection("SysUiBinder", "onQuickScrubStart"); + } + + @Override + public void onQuickScrubProgress(float progress) { + mEventQueue.onQuickScrubProgress(progress); + } + + @Override + public void onQuickScrubEnd() { + mEventQueue.onQuickScrubEnd(); + TraceHelper.endSection("SysUiBinder", "onQuickScrubEnd"); + } + + @Override + public void onOverviewToggle() { + mOverviewCommandHelper.onOverviewToggle(); + } + + @Override + public void onOverviewShown(boolean triggeredFromAltTab) { + if (triggeredFromAltTab) { + setupTouchConsumer(HIT_TARGET_NONE); + mEventQueue.onOverviewShownFromAltTab(); + } else { + mOverviewCommandHelper.onOverviewShown(); + } + } + + @Override + public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { + if (triggeredFromAltTab && !triggeredFromHomeKey) { + // onOverviewShownFromAltTab initiates quick scrub. Ending it here. + mEventQueue.onQuickScrubEnd(); + } + } + + @Override + public void onQuickStep(MotionEvent motionEvent) { + mEventQueue.onQuickStep(motionEvent); + TraceHelper.endSection("SysUiBinder", "onQuickStep"); + + } + + @Override + public void onTip(int actionType, int viewType) { + mOverviewCommandHelper.onTip(actionType, viewType); + } + }; + + private final TouchConsumer mNoOpTouchConsumer = (ev) -> {}; + + private static boolean sConnected = false; + + public static boolean isConnected() { + return sConnected; + } + + private ActivityManagerWrapper mAM; + private RecentsModel mRecentsModel; + private MotionEventQueue mEventQueue; + private MainThreadExecutor mMainThreadExecutor; + private ISystemUiProxy mISystemUiProxy; + private OverviewCommandHelper mOverviewCommandHelper; + private OverviewInteractionState mOverviewInteractionState; + private OverviewCallbacks mOverviewCallbacks; + private TaskOverlayFactory mTaskOverlayFactory; + + private Choreographer mMainThreadChoreographer; + private Choreographer mBackgroundThreadChoreographer; + + @Override + public void onCreate() { + super.onCreate(); + mAM = ActivityManagerWrapper.getInstance(); + mRecentsModel = RecentsModel.getInstance(this); + mRecentsModel.setPreloadTasksInBackground(true); + mMainThreadExecutor = new MainThreadExecutor(); + mOverviewCommandHelper = new OverviewCommandHelper(this); + mMainThreadChoreographer = Choreographer.getInstance(); + mEventQueue = new MotionEventQueue(mMainThreadChoreographer, mNoOpTouchConsumer); + mOverviewInteractionState = OverviewInteractionState.getInstance(this); + mOverviewCallbacks = OverviewCallbacks.get(this); + mTaskOverlayFactory = TaskOverlayFactory.get(this); + + sConnected = true; + + // Temporarily disable model preload + // new ModelPreload().start(this); + initBackgroundChoreographer(); + } + + @Override + public void onDestroy() { + mOverviewCommandHelper.onDestroy(); + sConnected = false; + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "Touch service connected"); + return mMyBinder; + } + + private void setupTouchConsumer(@HitTarget int downHitTarget) { + mEventQueue.reset(); + TouchConsumer oldConsumer = mEventQueue.getConsumer(); + if (oldConsumer.deferNextEventToMainThread()) { + mEventQueue = new MotionEventQueue(mMainThreadChoreographer, + new DeferredTouchConsumer((v) -> getCurrentTouchConsumer(downHitTarget, + oldConsumer.forceToLauncherConsumer(), v))); + mEventQueue.deferInit(); + } else { + mEventQueue = new MotionEventQueue( + mMainThreadChoreographer, getCurrentTouchConsumer(downHitTarget, false, null)); + } + } + + private TouchConsumer getCurrentTouchConsumer( + @HitTarget int downHitTarget, boolean forceToLauncher, VelocityTracker tracker) { + RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0); + + if (runningTaskInfo == null && !forceToLauncher) { + return mNoOpTouchConsumer; + } else if (forceToLauncher || + runningTaskInfo.topActivity.equals(mOverviewCommandHelper.overviewComponent)) { + return getOverviewConsumer(); + } else { + if (tracker == null) { + tracker = VelocityTracker.obtain(); + } + return new OtherActivityTouchConsumer(this, runningTaskInfo, mRecentsModel, + mOverviewCommandHelper.overviewIntent, + mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor, + mBackgroundThreadChoreographer, downHitTarget, mOverviewCallbacks, + mTaskOverlayFactory, tracker); + } + } + + private TouchConsumer getOverviewConsumer() { + ActivityControlHelper activityHelper = mOverviewCommandHelper.getActivityControlHelper(); + BaseDraggingActivity activity = activityHelper.getCreatedActivity(); + if (activity == null) { + return mNoOpTouchConsumer; + } + return new OverviewTouchConsumer(activityHelper, activity); + } + + private static class OverviewTouchConsumer + implements TouchConsumer { + + private final ActivityControlHelper mActivityHelper; + private final T mActivity; + private final BaseDragLayer mTarget; + private final int[] mLocationOnScreen = new int[2]; + private final PointF mDownPos = new PointF(); + private final int mTouchSlop; + private final QuickScrubController mQuickScrubController; + + private boolean mTrackingStarted = false; + private boolean mInvalidated = false; + + private float mLastProgress = 0; + private boolean mStartPending = false; + private boolean mEndPending = false; + + OverviewTouchConsumer(ActivityControlHelper activityHelper, T activity) { + mActivityHelper = activityHelper; + mActivity = activity; + mTarget = activity.getDragLayer(); + mTouchSlop = ViewConfiguration.get(mTarget.getContext()).getScaledTouchSlop(); + + mQuickScrubController = mActivity.getOverviewPanel() + .getQuickScrubController(); + } + + @Override + public void accept(MotionEvent ev) { + if (mInvalidated) { + return; + } + int action = ev.getActionMasked(); + if (action == ACTION_DOWN) { + mTrackingStarted = false; + mDownPos.set(ev.getX(), ev.getY()); + } else if (!mTrackingStarted) { + switch (action) { + case ACTION_POINTER_UP: + case ACTION_POINTER_DOWN: + if (!mTrackingStarted) { + mInvalidated = true; + } + break; + case ACTION_MOVE: { + float displacement = ev.getY() - mDownPos.y; + if (Math.abs(displacement) >= mTouchSlop) { + mTarget.getLocationOnScreen(mLocationOnScreen); + + // Send a down event only when mTouchSlop is crossed. + MotionEvent down = MotionEvent.obtain(ev); + down.setAction(ACTION_DOWN); + sendEvent(down); + down.recycle(); + mTrackingStarted = true; + } + } + } + } + + if (mTrackingStarted) { + sendEvent(ev); + } + + if (action == ACTION_UP || action == ACTION_CANCEL) { + mInvalidated = true; + } + } + + private void sendEvent(MotionEvent ev) { + int flags = ev.getEdgeFlags(); + ev.setEdgeFlags(flags | EDGE_NAV_BAR); + ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]); + if (!mTrackingStarted) { + mTarget.onInterceptTouchEvent(ev); + } + mTarget.onTouchEvent(ev); + ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]); + ev.setEdgeFlags(flags); + } + + @Override + public void onQuickStep(MotionEvent ev) { + if (mInvalidated) { + return; + } + OverviewCallbacks.get(mActivity).closeAllWindows(); + ActivityManagerWrapper.getInstance() + .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + } + + @Override + public void updateTouchTracking(int interactionType) { + if (mInvalidated) { + return; + } + if (interactionType == INTERACTION_QUICK_SCRUB) { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mInvalidated = true; + return; + } + OverviewCallbacks.get(mActivity).closeAllWindows(); + ActivityManagerWrapper.getInstance() + .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + + mStartPending = true; + Runnable action = () -> { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mInvalidated = true; + return; + } + mActivityHelper.onQuickInteractionStart(mActivity, null, true); + mQuickScrubController.onQuickScrubProgress(mLastProgress); + mStartPending = false; + + if (mEndPending) { + mQuickScrubController.onQuickScrubEnd(); + mEndPending = false; + } + }; + + mActivityHelper.executeOnWindowAvailable(mActivity, action); + } + } + + @Override + public void onQuickScrubEnd() { + if (mInvalidated) { + return; + } + if (mStartPending) { + mEndPending = true; + } else { + mQuickScrubController.onQuickScrubEnd(); + } + } + + @Override + public void onQuickScrubProgress(float progress) { + mLastProgress = progress; + if (mInvalidated || mStartPending) { + return; + } + mQuickScrubController.onQuickScrubProgress(progress); + } + + } + + private void initBackgroundChoreographer() { + if (sRemoteUiThread == null) { + sRemoteUiThread = new HandlerThread("remote-ui"); + sRemoteUiThread.start(); + } + new Handler(sRemoteUiThread.getLooper()).post(() -> + mBackgroundThreadChoreographer = ChoreographerCompat.getSfInstance()); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/WindowTransformSwipeHandler.java new file mode 100644 index 0000000000..0004a11f58 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/WindowTransformSwipeHandler.java @@ -0,0 +1,1102 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.os.UserHandle; +import android.support.annotation.AnyThread; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.View; +import android.view.ViewTreeObserver.OnDrawListener; +import android.view.WindowManager; +import android.view.animation.Interpolator; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.launcher3.util.TraceHelper; +import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.ActivityControlHelper.AnimationFactory; +import com.android.quickstep.ActivityControlHelper.LayoutListener; +import com.android.quickstep.TouchConsumer.InteractionType; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.util.TransformedRect; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.InputConsumerController; +import com.android.systemui.shared.system.LatencyTrackerCompat; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; +import com.android.systemui.shared.system.WindowCallbacksCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.StringJoiner; +import java.util.function.BiFunction; + +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; +import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION; +import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; +import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; +import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; + +@TargetApi(Build.VERSION_CODES.O) +public class WindowTransformSwipeHandler { + private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName(); + private static final boolean DEBUG_STATES = false; + + // Launcher UI related states + private static final int STATE_LAUNCHER_PRESENT = 1 << 0; + private static final int STATE_LAUNCHER_STARTED = 1 << 1; + private static final int STATE_LAUNCHER_DRAWN = 1 << 2; + private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 3; + + // Internal initialization states + private static final int STATE_APP_CONTROLLER_RECEIVED = 1 << 4; + + // Interaction finish states + private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 5; + private static final int STATE_SCALED_CONTROLLER_APP = 1 << 6; + + private static final int STATE_HANDLER_INVALIDATED = 1 << 7; + private static final int STATE_GESTURE_STARTED_QUICKSTEP = 1 << 8; + private static final int STATE_GESTURE_STARTED_QUICKSCRUB = 1 << 9; + private static final int STATE_GESTURE_CANCELLED = 1 << 10; + private static final int STATE_GESTURE_COMPLETED = 1 << 11; + + // States for quick switch/scrub + private static final int STATE_CURRENT_TASK_FINISHED = 1 << 12; + private static final int STATE_QUICK_SCRUB_START = 1 << 13; + private static final int STATE_QUICK_SCRUB_END = 1 << 14; + + private static final int STATE_CAPTURE_SCREENSHOT = 1 << 15; + private static final int STATE_SCREENSHOT_CAPTURED = 1 << 16; + + private static final int STATE_RESUME_LAST_TASK = 1 << 17; + private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 18; + + private static final int LAUNCHER_UI_STATES = + STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE + | STATE_LAUNCHER_STARTED; + + private static final int LONG_SWIPE_ENTER_STATE = + STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED + | STATE_APP_CONTROLLER_RECEIVED; + + private static final int LONG_SWIPE_START_STATE = + STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED + | STATE_APP_CONTROLLER_RECEIVED | STATE_SCREENSHOT_CAPTURED; + + // For debugging, keep in sync with above states + private static final String[] STATES = new String[] { + "STATE_LAUNCHER_PRESENT", + "STATE_LAUNCHER_STARTED", + "STATE_LAUNCHER_DRAWN", + "STATE_ACTIVITY_MULTIPLIER_COMPLETE", + "STATE_APP_CONTROLLER_RECEIVED", + "STATE_SCALED_CONTROLLER_RECENTS", + "STATE_SCALED_CONTROLLER_APP", + "STATE_HANDLER_INVALIDATED", + "STATE_GESTURE_STARTED_QUICKSTEP", + "STATE_GESTURE_STARTED_QUICKSCRUB", + "STATE_GESTURE_CANCELLED", + "STATE_GESTURE_COMPLETED", + "STATE_CURRENT_TASK_FINISHED", + "STATE_QUICK_SCRUB_START", + "STATE_QUICK_SCRUB_END", + "STATE_CAPTURE_SCREENSHOT", + "STATE_SCREENSHOT_CAPTURED", + "STATE_RESUME_LAST_TASK", + "STATE_ASSIST_DATA_RECEIVED", + }; + + public static final long MAX_SWIPE_DURATION = 350; + public static final long MIN_SWIPE_DURATION = 80; + public static final long MIN_OVERSHOOT_DURATION = 120; + + public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f; + private static final float SWIPE_DURATION_MULTIPLIER = + Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); + + private final ClipAnimationHelper mClipAnimationHelper = new ClipAnimationHelper(); + + protected Runnable mGestureEndCallback; + protected boolean mIsGoingToHome; + private DeviceProfile mDp; + private int mTransitionDragLength; + + // Shift in the range of [0, 1]. + // 0 => preview snapShot is completely visible, and hotseat is completely translated down + // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely + // visible. + private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); + + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + + // An increasing identifier per single instance of OtherActivityTouchConsumer. Generally one + // instance of OtherActivityTouchConsumer will only have one swipe handle, but sometimes we can + // end up with multiple handlers if we get recents command in the middle of a swipe gesture. + // This is used to match the corresponding activity manager callbacks in + // OtherActivityTouchConsumer + public final int id; + private final Context mContext; + private final ActivityControlHelper mActivityControlHelper; + private final ActivityInitListener mActivityInitListener; + + private final int mRunningTaskId; + private final RunningTaskInfo mRunningTaskInfo; + private ThumbnailData mTaskSnapshot; + + private MultiStateCallback mStateCallback; + private AnimatorPlaybackController mLauncherTransitionController; + + private T mActivity; + private LayoutListener mLayoutListener; + private RecentsView mRecentsView; + private SyncRtSurfaceTransactionApplier mSyncTransactionApplier; + private QuickScrubController mQuickScrubController; + private AnimationFactory mAnimationFactory = (t, i) -> { }; + + private Runnable mLauncherDrawnCallback; + + private boolean mWasLauncherAlreadyVisible; + + private boolean mPassedOverviewThreshold; + private boolean mGestureStarted; + private int mLogAction = Touch.SWIPE; + private float mCurrentQuickScrubProgress; + private boolean mQuickScrubBlocked; + + private @InteractionType int mInteractionType = INTERACTION_NORMAL; + + private InputConsumerController mInputConsumer = + InputConsumerController.getRecentsAnimationInputConsumer(); + + private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper(); + + private final long mTouchTimeMs; + private long mLauncherFrameDrawnTime; + + private boolean mBgLongSwipeMode = false; + private boolean mUiLongSwipeMode = false; + private float mLongSwipeDisplacement = 0; + private LongSwipeHelper mLongSwipeController; + + private Bundle mAssistData; + + WindowTransformSwipeHandler(int id, RunningTaskInfo runningTaskInfo, Context context, + long touchTimeMs, ActivityControlHelper controller) { + this.id = id; + mContext = context; + mRunningTaskInfo = runningTaskInfo; + mRunningTaskId = runningTaskInfo.id; + mTouchTimeMs = touchTimeMs; + mActivityControlHelper = controller; + mActivityInitListener = mActivityControlHelper + .createActivityInitListener(this::onActivityInit); + + initStateCallbacks(); + // Register the input consumer on the UI thread, to ensure that it runs after any pending + // unregister calls + executeOnUiThread(mInputConsumer::registerInputConsumer); + } + + private void initStateCallbacks() { + mStateCallback = new MultiStateCallback() { + @Override + public void setState(int stateFlag) { + debugNewState(stateFlag); + super.setState(stateFlag); + } + }; + + mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED_QUICKSCRUB, + this::initializeLauncherAnimationController); + mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED_QUICKSTEP, + this::initializeLauncherAnimationController); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN, + this::launcherFrameDrawn); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED_QUICKSTEP, + this::notifyGestureStartedAsync); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED_QUICKSCRUB, + this::notifyGestureStartedAsync); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED + | STATE_GESTURE_CANCELLED, + this::resetStateForAnimationCancel); + + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED, + this::sendRemoteAnimationsToAnimationFactory); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP, + this::resumeLastTaskForQuickstep); + mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED, + this::resumeLastTask); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED + | STATE_ACTIVITY_MULTIPLIER_COMPLETE + | STATE_CAPTURE_SCREENSHOT, + this::switchToScreenshot); + + mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED + | STATE_SCALED_CONTROLLER_RECENTS, + this::finishCurrentTransitionToHome); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED + | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS + | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED + | STATE_GESTURE_STARTED_QUICKSTEP, + this::setupLauncherUiAfterSwipeUpAnimation); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED + | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS + | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED + | STATE_GESTURE_STARTED_QUICKSTEP | STATE_ASSIST_DATA_RECEIVED, + this::preloadAssistData); + + mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, + this::invalidateHandlerWithLauncher); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED + | STATE_SCALED_CONTROLLER_APP, + this::notifyTransitionCancelled); + + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START + | STATE_APP_CONTROLLER_RECEIVED, this::onQuickScrubStart); + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START + | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub); + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_CURRENT_TASK_FINISHED + | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub); + + mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter); + mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart); + } + + private void executeOnUiThread(Runnable action) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { + action.run(); + } else { + postAsyncCallback(mMainThreadHandler, action); + } + } + + private void setStateOnUiThread(int stateFlag) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { + mStateCallback.setState(stateFlag); + } else { + postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); + } + } + + private void initTransitionEndpoints(DeviceProfile dp) { + mDp = dp; + + TransformedRect tempRect = new TransformedRect(); + mTransitionDragLength = mActivityControlHelper + .getSwipeUpDestinationAndLength(dp, mContext, mInteractionType, tempRect); + mClipAnimationHelper.updateTargetRect(tempRect); + } + + private long getFadeInDuration() { + if (mCurrentShift.getCurrentAnimation() != null) { + ObjectAnimator anim = mCurrentShift.getCurrentAnimation(); + long theirDuration = anim.getDuration() - anim.getCurrentPlayTime(); + + // TODO: Find a better heuristic + return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION)); + } else { + return MAX_SWIPE_DURATION; + } + } + + public void initWhenReady() { + mActivityInitListener.register(); + } + + private boolean onActivityInit(final T activity, Boolean alreadyOnHome) { + if (mActivity == activity) { + return true; + } + if (mActivity != null) { + // The launcher may have been recreated as a result of device rotation. + int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES; + initStateCallbacks(); + mStateCallback.setState(oldState); + mLayoutListener.setHandler(null); + } + mWasLauncherAlreadyVisible = alreadyOnHome; + mActivity = activity; + // Override the visibility of the activity until the gesture actually starts and we swipe + // up, or until we transition home and the home animation is composed + if (alreadyOnHome) { + mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); + } else { + mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); + } + + mRecentsView = activity.getOverviewPanel(); + mSyncTransactionApplier = new SyncRtSurfaceTransactionApplier(mRecentsView); + mQuickScrubController = mRecentsView.getQuickScrubController(); + mLayoutListener = mActivityControlHelper.createLayoutListener(mActivity); + + mStateCallback.setState(STATE_LAUNCHER_PRESENT); + if (alreadyOnHome) { + onLauncherStart(activity); + } else { + activity.setOnStartCallback(this::onLauncherStart); + } + return true; + } + + private void onLauncherStart(final T activity) { + if (mActivity != activity) { + return; + } + if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { + return; + } + + mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, + mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated); + AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible); + + if (mWasLauncherAlreadyVisible) { + mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN); + } else { + TraceHelper.beginSection("WTS-init"); + View dragLayer = activity.getDragLayer(); + mActivityControlHelper.getAlphaProperty(activity).setValue(0); + dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { + + @Override + public void onDraw() { + TraceHelper.endSection("WTS-init", "Launcher frame is drawn"); + dragLayer.post(() -> + dragLayer.getViewTreeObserver().removeOnDrawListener(this)); + if (activity != mActivity) { + return; + } + + mStateCallback.setState(STATE_LAUNCHER_DRAWN); + } + }); + } + + mRecentsView.showTask(mRunningTaskId); + mRecentsView.setRunningTaskHidden(true); + mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); + mLayoutListener.open(); + mStateCallback.setState(STATE_LAUNCHER_STARTED); + } + + public void setLauncherOnDrawCallback(Runnable callback) { + mLauncherDrawnCallback = callback; + } + + private void launcherFrameDrawn() { + AlphaProperty property = mActivityControlHelper.getAlphaProperty(mActivity); + if (property.getValue() < 1) { + if (mGestureStarted) { + final MultiStateCallback callback = mStateCallback; + ObjectAnimator animator = ObjectAnimator.ofFloat( + property, MultiValueAlpha.VALUE, 1); + animator.setDuration(getFadeInDuration()).addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); + } + }); + animator.start(); + } else { + property.setValue(1); + mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); + } + } + if (mLauncherDrawnCallback != null) { + mLauncherDrawnCallback.run(); + } + mLauncherFrameDrawnTime = SystemClock.uptimeMillis(); + } + + private void sendRemoteAnimationsToAnimationFactory() { + mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet); + } + + private void initializeLauncherAnimationController() { + mLayoutListener.setHandler(this); + buildAnimationController(); + + if (LatencyTrackerCompat.isEnabled(mContext)) { + LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs)); + } + + // This method is only called when STATE_GESTURE_STARTED_QUICKSTEP/ + // STATE_GESTURE_STARTED_QUICKSCRUB is set, so we can enable the high-res thumbnail loader + // here once we are sure that we will end up in an overview state + RecentsModel.getInstance(mContext).getRecentsTaskLoader() + .getHighResThumbnailLoader().setVisible(true); + } + + public void updateInteractionType(@InteractionType int interactionType) { + if (mInteractionType != INTERACTION_NORMAL) { + throw new IllegalArgumentException( + "Can't change interaction type from " + mInteractionType); + } + if (interactionType != INTERACTION_QUICK_SCRUB) { + throw new IllegalArgumentException( + "Can't change interaction type to " + interactionType); + } + mInteractionType = interactionType; + mRecentsAnimationWrapper.runOnInit(this::shiftAnimationDestinationForQuickscrub); + + setStateOnUiThread(STATE_QUICK_SCRUB_START | STATE_GESTURE_COMPLETED); + + // Start the window animation without waiting for launcher. + animateToProgress(mCurrentShift.value, 1f, QUICK_SCRUB_FROM_APP_START_DURATION, LINEAR, + true /* goingToHome */); + } + + private void shiftAnimationDestinationForQuickscrub() { + TransformedRect tempRect = new TransformedRect(); + mActivityControlHelper + .getSwipeUpDestinationAndLength(mDp, mContext, mInteractionType, tempRect); + mClipAnimationHelper.updateTargetRect(tempRect); + + float offsetY = + mActivityControlHelper.getTranslationYForQuickScrub(tempRect, mDp, mContext); + float scale, offsetX; + Resources res = mContext.getResources(); + + if (ActivityManagerWrapper.getInstance().getRecentTasks(2, UserHandle.myUserId()).size() + < 2) { + // There are not enough tasks, we don't need to shift + offsetX = 0; + scale = 1; + } else { + offsetX = res.getDimensionPixelSize(R.dimen.recents_page_spacing) + + tempRect.rect.width(); + float distanceToReachEdge = mDp.widthPx / 2 + tempRect.rect.width() / 2 + + res.getDimensionPixelSize(R.dimen.recents_page_spacing); + float interpolation = Math.min(1, offsetX / distanceToReachEdge); + scale = TaskView.getCurveScaleForInterpolation(interpolation); + } + mClipAnimationHelper.offsetTarget(scale, Utilities.isRtl(res) ? -offsetX : offsetX, offsetY, + QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR); + } + + @WorkerThread + public void updateDisplacement(float displacement) { + // We are moving in the negative x/y direction + displacement = -displacement; + if (displacement > mTransitionDragLength) { + mCurrentShift.updateValue(1); + + if (!mBgLongSwipeMode) { + mBgLongSwipeMode = true; + executeOnUiThread(this::onLongSwipeEnabledUi); + } + mLongSwipeDisplacement = displacement - mTransitionDragLength; + executeOnUiThread(this::onLongSwipeDisplacementUpdated); + } else { + if (mBgLongSwipeMode) { + mBgLongSwipeMode = false; + executeOnUiThread(this::onLongSwipeDisabledUi); + } + float translation = Math.max(displacement, 0); + float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; + mCurrentShift.updateValue(shift); + } + } + + /** + * Called by {@link #mLayoutListener} when launcher layout changes + */ + public void buildAnimationController() { + initTransitionEndpoints(mActivity.getDeviceProfile()); + mAnimationFactory.createActivityController(mTransitionDragLength, mInteractionType); + } + + private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) { + mLauncherTransitionController = anim; + mLauncherTransitionController.dispatchOnStart(); + mLauncherTransitionController.setPlayFraction(mCurrentShift.value); + } + + @WorkerThread + private void updateFinalShift() { + float shift = mCurrentShift.value; + + RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); + if (controller != null) { + + mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, shift, + Looper.myLooper() == mMainThreadHandler.getLooper() + ? mSyncTransactionApplier + : null); + + boolean passedThreshold = shift > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD; + mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold); + if (mActivityControlHelper.shouldMinimizeSplitScreen()) { + mRecentsAnimationWrapper.setSplitScreenMinimizedForTransaction(passedThreshold); + } + } + + executeOnUiThread(this::updateFinalShiftUi); + } + + private void updateFinalShiftUi() { + final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; + if (passed != mPassedOverviewThreshold) { + mPassedOverviewThreshold = passed; + if (mInteractionType == INTERACTION_NORMAL && mRecentsView != null) { + mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + } + + if (mLauncherTransitionController == null || mLauncherTransitionController + .getAnimationPlayer().isStarted()) { + return; + } + mLauncherTransitionController.setPlayFraction(mCurrentShift.value); + } + + public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller, + RemoteAnimationTargetSet targets, Rect homeContentInsets, Rect minimizedHomeBounds) { + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + InvariantDeviceProfile idp = appState == null ? + new InvariantDeviceProfile(mContext) : appState.getInvariantDeviceProfile(); + DeviceProfile dp = idp.getDeviceProfile(mContext); + final Rect overviewStackBounds; + RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mRunningTaskId); + + if (minimizedHomeBounds != null && runningTaskTarget != null) { + overviewStackBounds = mActivityControlHelper + .getOverviewWindowBounds(minimizedHomeBounds, runningTaskTarget); + dp = dp.getMultiWindowProfile(mContext, + new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height())); + dp.updateInsets(homeContentInsets); + } else { + if (mActivity != null) { + int loc[] = new int[2]; + View rootView = mActivity.getRootView(); + rootView.getLocationOnScreen(loc); + overviewStackBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), + loc[1] + rootView.getHeight()); + } else { + overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx); + } + // If we are not in multi-window mode, home insets should be same as system insets. + Rect insets = new Rect(); + WindowManagerWrapper.getInstance().getStableInsets(insets); + dp = dp.copy(mContext); + dp.updateInsets(insets); + } + dp.updateIsSeascape(mContext.getSystemService(WindowManager.class)); + + if (runningTaskTarget != null) { + mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); + } + mClipAnimationHelper.prepareAnimation(false /* isOpening */); + initTransitionEndpoints(dp); + + mRecentsAnimationWrapper.setController(controller, targets); + setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); + + mPassedOverviewThreshold = false; + } + + public void onRecentsAnimationCanceled() { + mRecentsAnimationWrapper.setController(null, null); + mActivityInitListener.unregister(); + setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); + } + + public void onGestureStarted() { + notifyGestureStartedAsync(); + setStateOnUiThread(mInteractionType == INTERACTION_NORMAL + ? STATE_GESTURE_STARTED_QUICKSTEP : STATE_GESTURE_STARTED_QUICKSCRUB); + mGestureStarted = true; + mRecentsAnimationWrapper.hideCurrentInputMethod(); + mRecentsAnimationWrapper.enableInputConsumer(); + } + + /** + * Notifies the launcher that the swipe gesture has started. This can be called multiple times + * on both background and UI threads + */ + @AnyThread + private void notifyGestureStartedAsync() { + final T curActivity = mActivity; + if (curActivity != null) { + // Once the gesture starts, we can no longer transition home through the button, so + // reset the force override of the activity visibility + mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); + } + } + + @WorkerThread + public void onGestureEnded(float endVelocity) { + float flingThreshold = mContext.getResources() + .getDimension(R.dimen.quickstep_fling_threshold_velocity); + boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; + setStateOnUiThread(STATE_GESTURE_COMPLETED); + + mLogAction = isFling ? Touch.FLING : Touch.SWIPE; + + if (mBgLongSwipeMode) { + executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling)); + } else { + handleNormalGestureEnd(endVelocity, isFling); + } + } + + private void handleNormalGestureEnd(float endVelocity, boolean isFling) { + float velocityPxPerMs = endVelocity / 1000; + long duration = MAX_SWIPE_DURATION; + float currentShift = mCurrentShift.value; + final boolean goingToHome; + float endShift; + final float startShift; + Interpolator interpolator = DEACCEL; + if (!isFling) { + goingToHome = currentShift >= MIN_PROGRESS_FOR_OVERVIEW && mGestureStarted; + endShift = goingToHome ? 1 : 0; + long expectedDuration = Math.abs(Math.round((endShift - currentShift) + * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); + duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); + startShift = currentShift; + interpolator = goingToHome ? OVERSHOOT_1_2 : DEACCEL; + } else { + goingToHome = endVelocity < 0; + endShift = goingToHome ? 1 : 0; + startShift = Utilities.boundToRange(currentShift - velocityPxPerMs + * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1); + float minFlingVelocity = mContext.getResources() + .getDimension(R.dimen.quickstep_fling_min_velocity); + if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { + if (goingToHome) { + Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams( + startShift, endShift, endShift, velocityPxPerMs, mTransitionDragLength); + endShift = overshoot.end; + interpolator = overshoot.interpolator; + duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION, + MAX_SWIPE_DURATION); + } else { + float distanceToTravel = (endShift - currentShift) * mTransitionDragLength; + + // 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. 2. + long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); + } + } + } + animateToProgress(startShift, endShift, duration, interpolator, goingToHome); + } + + private void doLogGesture(boolean toLauncher) { + DeviceProfile dp = mDp; + if (dp == null) { + // We probably never received an animation controller, skip logging. + return; + } + final int direction; + if (dp.isVerticalBarLayout()) { + direction = (dp.isSeascape() ^ toLauncher) ? Direction.LEFT : Direction.RIGHT; + } else { + direction = toLauncher ? Direction.UP : Direction.DOWN; + } + + int dstContainerType = toLauncher ? ContainerType.TASKSWITCHER : ContainerType.APP; + UserEventDispatcher.newInstance(mContext, dp).logStateChangeAction( + mLogAction, direction, + ContainerType.NAVBAR, ContainerType.APP, + dstContainerType, + 0); + } + + /** Animates to the given progress, where 0 is the current app and 1 is overview. */ + private void animateToProgress(float start, float end, long duration, + Interpolator interpolator, boolean goingToHome) { + mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration, + interpolator, goingToHome)); + } + + private void animateToProgressInternal(float start, float end, long duration, + Interpolator interpolator, boolean goingToHome) { + mIsGoingToHome = goingToHome; + ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration); + anim.setInterpolator(interpolator); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + setStateOnUiThread(mIsGoingToHome + ? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT) + : STATE_SCALED_CONTROLLER_APP); + } + }); + anim.start(); + long startMillis = SystemClock.uptimeMillis(); + executeOnUiThread(() -> { + // Animate the launcher components at the same time as the window, always on UI thread. + if (mLauncherTransitionController != null && !mWasLauncherAlreadyVisible + && start != end && duration > 0) { + // Adjust start progress and duration in case we are on a different thread. + long elapsedMillis = SystemClock.uptimeMillis() - startMillis; + elapsedMillis = Utilities.boundToRange(elapsedMillis, 0, duration); + float elapsedProgress = (float) elapsedMillis / duration; + float adjustedStart = Utilities.mapRange(elapsedProgress, start, end); + long adjustedDuration = duration - elapsedMillis; + // We want to use the same interpolator as the window, but need to adjust it to + // interpolate over the remaining progress (end - start). + mLauncherTransitionController.dispatchSetInterpolator(Interpolators.mapToProgress( + interpolator, adjustedStart, end)); + mLauncherTransitionController.getAnimationPlayer().setDuration(adjustedDuration); + mLauncherTransitionController.getAnimationPlayer().start(); + } + }); + } + + @UiThread + private void resumeLastTaskForQuickstep() { + setStateOnUiThread(STATE_RESUME_LAST_TASK); + doLogGesture(false /* toLauncher */); + reset(); + } + + @UiThread + private void resumeLastTask() { + mRecentsAnimationWrapper.finish(false /* toHome */, null); + } + + public void reset() { + if (mInteractionType != INTERACTION_QUICK_SCRUB) { + // Only invalidate the handler if we are not quick scrubbing, otherwise, it will be + // invalidated after the quick scrub ends + setStateOnUiThread(STATE_HANDLER_INVALIDATED); + } + } + + private void invalidateHandler() { + mCurrentShift.finishAnimation(); + + if (mGestureEndCallback != null) { + mGestureEndCallback.run(); + } + + mActivityInitListener.unregister(); + mInputConsumer.unregisterInputConsumer(); + mTaskSnapshot = null; + } + + private void invalidateHandlerWithLauncher() { + mLauncherTransitionController = null; + mLayoutListener.finish(); + mActivityControlHelper.getAlphaProperty(mActivity).setValue(1); + + mRecentsView.setRunningTaskHidden(false); + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, false /* animate */); + mQuickScrubController.cancelActiveQuickscrub(); + } + + private void notifyTransitionCancelled() { + mAnimationFactory.onTransitionCancelled(); + } + + private void resetStateForAnimationCancel() { + boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted; + mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible); + + // Leave the pending invisible flag, as it may be used by wallpaper open animation. + mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); + } + + public void layoutListenerClosed() { + if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) { + mLauncherTransitionController.setPlayFraction(1); + } + } + + private void switchToScreenshot() { + boolean finishTransitionPosted = false; + RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); + if (controller != null) { + // Update the screenshot of the task + if (mTaskSnapshot == null) { + mTaskSnapshot = controller.screenshotTask(mRunningTaskId); + } + TaskView taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot); + mRecentsView.setRunningTaskHidden(false); + if (taskView != null) { + // Defer finishing the animation until the next launcher frame with the + // new thumbnail + finishTransitionPosted = new WindowCallbacksCompat(taskView) { + + // The number of frames to defer until we actually finish the animation + private int mDeferFrameCount = 2; + + @Override + public void onPostDraw(Canvas canvas) { + if (mDeferFrameCount > 0) { + mDeferFrameCount--; + // Workaround, detach and reattach to invalidate the root node for + // another draw + detach(); + attach(); + taskView.invalidate(); + return; + } + + setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + detach(); + } + }.attach(); + } + } + if (!finishTransitionPosted) { + // If we haven't posted a draw callback, set the state immediately. + setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + } + } + + private void finishCurrentTransitionToHome() { + synchronized (mRecentsAnimationWrapper) { + mRecentsAnimationWrapper.finish(true /* toHome */, + () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); + } + } + + private void setupLauncherUiAfterSwipeUpAnimation() { + if (mLauncherTransitionController != null) { + mLauncherTransitionController.getAnimationPlayer().end(); + mLauncherTransitionController = null; + } + mActivityControlHelper.onSwipeUpComplete(mActivity); + + // Animate the first icon. + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, true /* animate */); + mRecentsView.setSwipeDownShouldLaunchApp(true); + + RecentsModel.getInstance(mContext).onOverviewShown(false, TAG); + + doLogGesture(true /* toLauncher */); + reset(); + } + + private void onQuickScrubStart() { + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mQuickScrubBlocked = true; + setStateOnUiThread(STATE_RESUME_LAST_TASK | STATE_HANDLER_INVALIDATED); + return; + } + if (mLauncherTransitionController != null) { + mLauncherTransitionController.getAnimationPlayer().end(); + mLauncherTransitionController = null; + } + + mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false); + + // Inform the last progress in case we skipped before. + mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress); + } + + private void onFinishedTransitionToQuickScrub() { + if (mQuickScrubBlocked) { + return; + } + mQuickScrubController.onFinishedTransitionToQuickScrub(); + + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, true /* animate */); + RecentsModel.getInstance(mContext).onOverviewShown(false, TAG); + } + + public void onQuickScrubProgress(float progress) { + mCurrentQuickScrubProgress = progress; + if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null + || mQuickScrubBlocked) { + return; + } + mQuickScrubController.onQuickScrubProgress(progress); + } + + public void onQuickScrubEnd() { + setStateOnUiThread(STATE_QUICK_SCRUB_END); + } + + private void switchToFinalAppAfterQuickScrub() { + if (mQuickScrubBlocked) { + return; + } + mQuickScrubController.onQuickScrubEnd(); + + // Normally this is handled in reset(), but since we are still scrubbing after the + // transition into recents, we need to defer the handler invalidation for quick scrub until + // after the gesture ends + setStateOnUiThread(STATE_HANDLER_INVALIDATED); + } + + private void debugNewState(int stateFlag) { + if (!DEBUG_STATES) { + return; + } + + int state = mStateCallback.getState(); + StringJoiner currentStateStr = new StringJoiner(", ", "[", "]"); + String stateFlagStr = "Unknown-" + stateFlag; + for (int i = 0; i < STATES.length; i++) { + if ((state & (i << i)) != 0) { + currentStateStr.add(STATES[i]); + } + if (stateFlag == (1 << i)) { + stateFlagStr = STATES[i] + " (" + stateFlag + ")"; + } + } + Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding " + stateFlagStr + " to " + + currentStateStr); + } + + public void setGestureEndCallback(Runnable gestureEndCallback) { + mGestureEndCallback = gestureEndCallback; + } + + // Handling long swipe + private void onLongSwipeEnabledUi() { + mUiLongSwipeMode = true; + checkLongSwipeCanEnter(); + checkLongSwipeCanStart(); + } + + private void onLongSwipeDisabledUi() { + mUiLongSwipeMode = false; + + if (mLongSwipeController != null) { + mLongSwipeController.destroy(); + setTargetAlphaProvider((t, a1) -> a1); + + // Rebuild animations + buildAnimationController(); + } + } + + private void onLongSwipeDisplacementUpdated() { + if (!mUiLongSwipeMode || mLongSwipeController == null) { + return; + } + + mLongSwipeController.onMove(mLongSwipeDisplacement); + } + + private void checkLongSwipeCanEnter() { + if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_ENTER_STATE) + || !mActivityControlHelper.supportsLongSwipe(mActivity)) { + return; + } + + // We are entering long swipe mode, make sure the screen shot is captured. + mStateCallback.setState(STATE_CAPTURE_SCREENSHOT); + + } + + private void checkLongSwipeCanStart() { + if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_START_STATE) + || !mActivityControlHelper.supportsLongSwipe(mActivity)) { + return; + } + + RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; + if (targetSet == null) { + // This can happen when cancelAnimation comes on the background thread, while we are + // processing the long swipe on the UI thread. + return; + } + + mLongSwipeController = mActivityControlHelper.getLongSwipeController( + mActivity, mRecentsAnimationWrapper.targetSet); + onLongSwipeDisplacementUpdated(); + setTargetAlphaProvider(mLongSwipeController::getTargetAlpha); + } + + private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) { + if (!mUiLongSwipeMode || mLongSwipeController == null) { + mUiLongSwipeMode = false; + handleNormalGestureEnd(velocity, isFling); + return; + } + mUiLongSwipeMode = false; + finishCurrentTransitionToHome(); + mLongSwipeController.end(velocity, isFling, + () -> setStateOnUiThread(STATE_HANDLER_INVALIDATED)); + + } + + private void setTargetAlphaProvider( + BiFunction provider) { + mClipAnimationHelper.setTaskAlphaCallback(provider); + updateFinalShift(); + } + + public void onAssistDataReceived(Bundle assistData) { + mAssistData = assistData; + setStateOnUiThread(STATE_ASSIST_DATA_RECEIVED); + } + + private void preloadAssistData() { + RecentsModel.getInstance(mContext).preloadAssistData(mRunningTaskId, mAssistData); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/FallbackRecentsView.java new file mode 100644 index 0000000000..4abf6e5358 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/FallbackRecentsView.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.fallback; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import com.android.launcher3.DeviceProfile; +import com.android.quickstep.RecentsActivity; +import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.views.RecentsView; + +public class FallbackRecentsView extends RecentsView { + + public FallbackRecentsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setOverviewStateEnabled(true); + getQuickScrubController().onFinishedTransitionToQuickScrub(); + } + + @Override + protected void startHome() { + mActivity.startHome(); + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + updateEmptyMessage(); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + updateEmptyMessage(); + } + + @Override + public void draw(Canvas canvas) { + maybeDrawEmptyMessage(canvas); + super.draw(canvas); + } + + @Override + protected void getTaskSize(DeviceProfile dp, Rect outRect) { + LayoutUtils.calculateFallbackTaskSize(getContext(), dp, outRect); + } + + @Override + public boolean shouldUseMultiWindowTaskSizeStrategy() { + // Just use the activity task size for multi-window as well. + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsRootView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsRootView.java new file mode 100644 index 0000000000..1c865a0894 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsRootView.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.fallback; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.R; +import com.android.launcher3.util.Themes; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.RecentsActivity; + +public class RecentsRootView extends BaseDragLayer { + + private final RecentsActivity mActivity; + + private final Point mLastKnownSize = new Point(10, 10); + + public RecentsRootView(Context context, AttributeSet attrs) { + super(context, attrs, 1 /* alphaChannelCount */); + mActivity = (RecentsActivity) BaseActivity.fromContext(context); + setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + public Point getLastKnownSize() { + return mLastKnownSize; + } + + public void setup() { + mControllers = new TouchController[] { new RecentsTaskController(mActivity) }; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Check size changes before the actual measure, to avoid multiple measure calls. + int width = View.MeasureSpec.getSize(widthMeasureSpec); + int height = View.MeasureSpec.getSize(heightMeasureSpec); + if (mLastKnownSize.x != width || mLastKnownSize.y != height) { + mLastKnownSize.set(width, height); + mActivity.onRootViewSizeChanged(); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @TargetApi(23) + @Override + protected boolean fitSystemWindows(Rect insets) { + // Update device profile before notifying the children. + mActivity.getDeviceProfile().updateInsets(insets); + setInsets(insets); + return true; // I'll take it from here + } + + @Override + public void setInsets(Rect insets) { + // If the insets haven't changed, this is a no-op. Avoid unnecessary layout caused by + // modifying child layout params. + if (!insets.equals(mInsets)) { + super.setInsets(insets); + } + setBackground(insets.top == 0 ? null + : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim)); + } + + public void dispatchInsets() { + mActivity.getDeviceProfile().updateInsets(mInsets); + super.setInsets(mInsets); + } +} \ No newline at end of file diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsTaskController.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsTaskController.java new file mode 100644 index 0000000000..635175638e --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/fallback/RecentsTaskController.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.fallback; + +import com.android.launcher3.uioverrides.TaskViewTouchController; +import com.android.quickstep.RecentsActivity; + +public class RecentsTaskController extends TaskViewTouchController { + + public RecentsTaskController(RecentsActivity activity) { + super(activity); + } + + @Override + protected boolean isRecentsInteractive() { + return mActivity.hasWindowFocus(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/logging/UserEventDispatcherExtension.java new file mode 100644 index 0000000000..b4b7f088ba --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/logging/UserEventDispatcherExtension.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.logging; + +import android.content.Context; +import android.util.Log; + +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.systemui.shared.system.MetricsLoggerCompat; + +import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent; +import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CANCEL_TARGET; +import static com.android.systemui.shared.system.LauncherEventUtil.DISMISS; +import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_QUICK_SCRUB_ONBOARDING_TIP; +import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_SWIPE_UP_ONBOARDING_TIP; +import static com.android.systemui.shared.system.LauncherEventUtil.VISIBLE; + +/** + * This class handles AOSP MetricsLogger function calls and logging around + * quickstep interactions. + */ +@SuppressWarnings("unused") +public class UserEventDispatcherExtension extends UserEventDispatcher { + + private static final String TAG = "UserEventDispatcher"; + + public UserEventDispatcherExtension(Context context) { } + + public void logStateChangeAction(int action, int dir, int srcChildTargetType, + int srcParentContainerType, int dstContainerType, + int pageIndex) { + new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY, + dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER); + super.logStateChangeAction(action, dir, srcChildTargetType, srcParentContainerType, + dstContainerType, pageIndex); + } + + public void logActionTip(int actionType, int viewType) { + LauncherLogProto.Action action = new LauncherLogProto.Action(); + LauncherLogProto.Target target = new LauncherLogProto.Target(); + switch(actionType) { + case VISIBLE: + action.type = LauncherLogProto.Action.Type.TIP; + target.type = LauncherLogProto.Target.Type.CONTAINER; + target.containerType = LauncherLogProto.ContainerType.TIP; + break; + case DISMISS: + action.type = LauncherLogProto.Action.Type.TOUCH; + action.touch = LauncherLogProto.Action.Touch.TAP; + target.type = LauncherLogProto.Target.Type.CONTROL; + target.controlType = CANCEL_TARGET; + break; + default: + Log.e(TAG, "Unexpected action type = " + actionType); + } + + switch(viewType) { + case RECENTS_QUICK_SCRUB_ONBOARDING_TIP: + target.tipType = LauncherLogProto.TipType.QUICK_SCRUB_TEXT; + break; + case RECENTS_SWIPE_UP_ONBOARDING_TIP: + target.tipType = LauncherLogProto.TipType.SWIPE_UP_TEXT; + break; + default: + Log.e(TAG, "Unexpected viewType = " + viewType); + } + LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target); + dispatchUserEvent(event, null); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/ClipAnimationHelper.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/ClipAnimationHelper.java new file mode 100644 index 0000000000..7a0d9931ca --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/ClipAnimationHelper.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.annotation.TargetApi; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Matrix.ScaleToFit; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.RemoteException; +import android.support.annotation.Nullable; +import android.view.animation.Interpolator; + +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.systemui.shared.recents.ISystemUiProxy; +import com.android.systemui.shared.recents.utilities.RectFEvaluator; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; +import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier.SurfaceParams; +import com.android.systemui.shared.system.TransactionCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.function.BiFunction; + +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; + +/** + * Utility class to handle window clip animation + */ +@TargetApi(Build.VERSION_CODES.P) +public class ClipAnimationHelper { + + // The bounds of the source app in device coordinates + private final Rect mSourceStackBounds = new Rect(); + // The insets of the source app + private final Rect mSourceInsets = new Rect(); + // The source app bounds with the source insets applied, in the source app window coordinates + private final RectF mSourceRect = new RectF(); + // The bounds of the task view in launcher window coordinates + private final RectF mTargetRect = new RectF(); + // Set when the final window destination is changed, such as offsetting for quick scrub + private final PointF mTargetOffset = new PointF(); + // The insets to be used for clipping the app window, which can be larger than mSourceInsets + // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In + // app window coordinates. + private final RectF mSourceWindowClipInsets = new RectF(); + + // The bounds of launcher (not including insets) in device coordinates + public final Rect mHomeStackBounds = new Rect(); + + // The clip rect in source app window coordinates + private final Rect mClipRect = new Rect(); + private final RectFEvaluator mRectFEvaluator = new RectFEvaluator(); + private final Matrix mTmpMatrix = new Matrix(); + private final RectF mTmpRectF = new RectF(); + + private float mTargetScale = 1f; + private float mOffsetScale = 1f; + private Interpolator mInterpolator = LINEAR; + // We translate y slightly faster than the rest of the animation for quick scrub. + private Interpolator mOffsetYInterpolator = LINEAR; + + // Whether to boost the opening animation target layers, or the closing + private int mBoostModeTargetLayers = -1; + // Wether or not applyTransform has been called yet since prepareAnimation() + private boolean mIsFirstFrame = true; + + private BiFunction mTaskAlphaCallback = + (t, a1) -> a1; + + private void updateSourceStack(RemoteAnimationTargetCompat target) { + mSourceInsets.set(target.contentInsets); + mSourceStackBounds.set(target.sourceContainerBounds); + + // TODO: Should sourceContainerBounds already have this offset? + mSourceStackBounds.offsetTo(target.position.x, target.position.y); + + } + + public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) { + mHomeStackBounds.set(homeStackBounds); + updateSourceStack(target); + } + + public void updateTargetRect(TransformedRect targetRect) { + mOffsetScale = targetRect.scale; + mSourceRect.set(mSourceInsets.left, mSourceInsets.top, + mSourceStackBounds.width() - mSourceInsets.right, + mSourceStackBounds.height() - mSourceInsets.bottom); + mTargetRect.set(targetRect.rect); + Utilities.scaleRectFAboutCenter(mTargetRect, targetRect.scale); + mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left, + mHomeStackBounds.top - mSourceStackBounds.top); + + // Calculate the clip based on the target rect (since the content insets and the + // launcher insets may differ, so the aspect ratio of the target rect can differ + // from the source rect. The difference between the target rect (scaled to the + // source rect) is the amount to clip on each edge. + RectF scaledTargetRect = new RectF(mTargetRect); + Utilities.scaleRectFAboutCenter(scaledTargetRect, + mSourceRect.width() / mTargetRect.width()); + scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top); + mSourceWindowClipInsets.set( + Math.max(scaledTargetRect.left, 0), + Math.max(scaledTargetRect.top, 0), + Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0), + Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0)); + mSourceRect.set(scaledTargetRect); + } + + public void prepareAnimation(boolean isOpening) { + mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING; + } + + public RectF applyTransform(RemoteAnimationTargetSet targetSet, float progress, + @Nullable SyncRtSurfaceTransactionApplier syncTransactionApplier) { + RectF currentRect; + mTmpRectF.set(mTargetRect); + Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale); + float offsetYProgress = mOffsetYInterpolator.getInterpolation(progress); + progress = mInterpolator.getInterpolation(progress); + currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF); + + synchronized (mTargetOffset) { + // Stay lined up with the center of the target, since it moves for quick scrub. + currentRect.offset(mTargetOffset.x * mOffsetScale * progress, + mTargetOffset.y * offsetYProgress); + } + + mClipRect.left = (int) (mSourceWindowClipInsets.left * progress); + mClipRect.top = (int) (mSourceWindowClipInsets.top * progress); + mClipRect.right = (int) + (mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress)); + mClipRect.bottom = (int) + (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress)); + + SurfaceParams[] params = new SurfaceParams[targetSet.unfilteredApps.length]; + for (int i = 0; i < targetSet.unfilteredApps.length; i++) { + RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i]; + mTmpMatrix.setTranslate(app.position.x, app.position.y); + Rect crop = app.sourceContainerBounds; + float alpha = 1f; + if (app.mode == targetSet.targetMode) { + if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL); + mTmpMatrix.postTranslate(app.position.x, app.position.y); + crop = mClipRect; + } + + if (app.isNotInRecents + || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + alpha = 1 - progress; + } + + alpha = mTaskAlphaCallback.apply(app, alpha); + } + + params[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, + RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers)); + } + applyParams(syncTransactionApplier, params); + return currentRect; + } + + private void applyParams(@Nullable SyncRtSurfaceTransactionApplier syncTransactionApplier, + SurfaceParams[] params) { + if (syncTransactionApplier != null) { + syncTransactionApplier.scheduleApply(params); + } else { + TransactionCompat t = new TransactionCompat(); + for (SurfaceParams param : params) { + SyncRtSurfaceTransactionApplier.applyParams(t, param); + } + t.setEarlyWakeup(); + t.apply(); + } + } + + public void setTaskAlphaCallback( + BiFunction callback) { + mTaskAlphaCallback = callback; + } + + public void offsetTarget(float scale, float offsetX, float offsetY, Interpolator interpolator) { + synchronized (mTargetOffset) { + mTargetOffset.set(offsetX, offsetY); + } + mTargetScale = scale; + mInterpolator = interpolator; + mOffsetYInterpolator = Interpolators.clampToProgress(mInterpolator, 0, + QUICK_SCRUB_TRANSLATION_Y_FACTOR); + } + + public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) { + fromTaskThumbnailView(ttv, rv, null); + } + + public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv, + @Nullable RemoteAnimationTargetCompat target) { + BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext()); + BaseDragLayer dl = activity.getDragLayer(); + + int[] pos = new int[2]; + dl.getLocationOnScreen(pos); + mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight()); + mHomeStackBounds.offset(pos[0], pos[1]); + + if (target != null) { + updateSourceStack(target); + } else if (rv.shouldUseMultiWindowTaskSizeStrategy()) { + updateStackBoundsToMultiWindowTaskSize(activity); + } else { + mSourceStackBounds.set(mHomeStackBounds); + mSourceInsets.set(activity.getDeviceProfile().getInsets()); + } + + TransformedRect targetRect = new TransformedRect(); + dl.getDescendantRectRelativeToSelf(ttv, targetRect.rect); + updateTargetRect(targetRect); + + if (target == null) { + // Transform the clip relative to the target rect. Only do this in the case where we + // aren't applying the insets to the app windows (where the clip should be in target app + // space) + float scale = mTargetRect.width() / mSourceRect.width(); + mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale; + mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale; + mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale; + mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale; + } + } + + private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + if (sysUiProxy != null) { + try { + mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds()); + return; + } catch (RemoteException e) { + // Use half screen size + } + } + + // Assume that the task size is half screen size (minus the insets and the divider size) + DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile(); + // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to + // account for system insets + int taskWidth = fullDp.availableWidthPx; + int taskHeight = fullDp.availableHeightPx; + int halfDividerSize = activity.getResources() + .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2; + + Rect insets = new Rect(); + WindowManagerWrapper.getInstance().getStableInsets(insets); + if (fullDp.isLandscape) { + taskWidth = taskWidth / 2 - halfDividerSize; + } else { + taskHeight = taskHeight / 2 - halfDividerSize; + } + + // Align the task to bottom left/right edge (closer to nav bar). + int left = activity.getDeviceProfile().isSeascape() ? insets.left + : (insets.left + fullDp.availableWidthPx - taskWidth); + mSourceStackBounds.set(0, 0, taskWidth, taskHeight); + mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight); + } + + public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) { + RectF currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect); + canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left, + mSourceStackBounds.top - mHomeStackBounds.top); + mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL); + + canvas.concat(mTmpMatrix); + canvas.translate(mTargetRect.left, mTargetRect.top); + + float insetProgress = (1 - progress); + ttv.drawOnCanvas(canvas, + -mSourceWindowClipInsets.left * insetProgress, + -mSourceWindowClipInsets.top * insetProgress, + ttv.getMeasuredWidth() + mSourceWindowClipInsets.right * insetProgress, + ttv.getMeasuredHeight() + mSourceWindowClipInsets.bottom * insetProgress, + ttv.getCornerRadius() * progress); + } + + public RectF getTargetRect() { + return mTargetRect; + } + + public RectF getSourceRect() { + return mSourceRect; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/LayoutUtils.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/LayoutUtils.java new file mode 100644 index 0000000000..4c7da40e69 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/LayoutUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.support.annotation.AnyThread; +import android.support.annotation.IntDef; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public class LayoutUtils { + + private static final int MULTI_WINDOW_STRATEGY_HALF_SCREEN = 1; + private static final int MULTI_WINDOW_STRATEGY_DEVICE_PROFILE = 2; + + @Retention(SOURCE) + @IntDef({MULTI_WINDOW_STRATEGY_HALF_SCREEN, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE}) + private @interface MultiWindowStrategy {} + + public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) { + float extraSpace; + if (dp.isVerticalBarLayout()) { + extraSpace = 0; + } else { + extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx; + } + calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_HALF_SCREEN, outRect); + } + + public static void calculateFallbackTaskSize(Context context, DeviceProfile dp, Rect outRect) { + calculateTaskSize(context, dp, 0, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect); + } + + @AnyThread + public static void calculateTaskSize(Context context, DeviceProfile dp, + float extraVerticalSpace, @MultiWindowStrategy int multiWindowStrategy, Rect outRect) { + float taskWidth, taskHeight, paddingHorz; + Resources res = context.getResources(); + Rect insets = dp.getInsets(); + + if (dp.isMultiWindowMode) { + if (multiWindowStrategy == MULTI_WINDOW_STRATEGY_HALF_SCREEN) { + DeviceProfile fullDp = dp.getFullScreenProfile(); + // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to + // account for system insets + taskWidth = fullDp.availableWidthPx; + taskHeight = fullDp.availableHeightPx; + float halfDividerSize = res.getDimension(R.dimen.multi_window_task_divider_size) + / 2; + + if (fullDp.isLandscape) { + taskWidth = taskWidth / 2 - halfDividerSize; + } else { + taskHeight = taskHeight / 2 - halfDividerSize; + } + } else { + // multiWindowStrategy == MULTI_WINDOW_STRATEGY_DEVICE_PROFILE + taskWidth = dp.widthPx; + taskHeight = dp.heightPx; + } + paddingHorz = res.getDimension(R.dimen.multi_window_task_card_horz_space); + } else { + taskWidth = dp.availableWidthPx; + taskHeight = dp.availableHeightPx; + paddingHorz = res.getDimension(dp.isVerticalBarLayout() + ? R.dimen.landscape_task_card_horz_space + : R.dimen.portrait_task_card_horz_space); + } + + float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin); + float paddingVert = res.getDimension(R.dimen.task_card_vert_space); + + // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless + // we override the insets ourselves. + int launcherVisibleWidth = dp.widthPx - insets.left - insets.right; + int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom; + + float availableHeight = launcherVisibleHeight + - topIconMargin - extraVerticalSpace - paddingVert; + float availableWidth = launcherVisibleWidth - paddingHorz; + + float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight); + float outWidth = scale * taskWidth; + float outHeight = scale * taskHeight; + + // Center in the visible space + float x = insets.left + (launcherVisibleWidth - outWidth) / 2; + float y = insets.top + Math.max(topIconMargin, + (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2); + outRect.set(Math.round(x), Math.round(y), + Math.round(x + outWidth), Math.round(y + outHeight)); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/MultiValueUpdateListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/MultiValueUpdateListener.java new file mode 100644 index 0000000000..f35f1552dd --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/MultiValueUpdateListener.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.animation.ValueAnimator; +import android.view.animation.Interpolator; + +import java.util.ArrayList; + +/** + * Utility class to update multiple values with different interpolators and durations during + * the same animation. + */ +public abstract class MultiValueUpdateListener implements ValueAnimator.AnimatorUpdateListener { + + private final ArrayList mAllProperties = new ArrayList<>(); + + @Override + public final void onAnimationUpdate(ValueAnimator animator) { + final float percent = animator.getAnimatedFraction(); + final float currentPlayTime = percent * animator.getDuration(); + + for (int i = mAllProperties.size() - 1; i >= 0; i--) { + FloatProp prop = mAllProperties.get(i); + float time = Math.max(0, currentPlayTime - prop.mDelay); + float newPercent = Math.min(1f, time / prop.mDuration); + newPercent = prop.mInterpolator.getInterpolation(newPercent); + prop.value = prop.mEnd * newPercent + prop.mStart * (1 - newPercent); + } + onUpdate(percent); + } + + public abstract void onUpdate(float percent); + + public final class FloatProp { + + public float value; + + private final float mStart; + private final float mEnd; + private final float mDelay; + private final float mDuration; + private final Interpolator mInterpolator; + + public FloatProp(float start, float end, float delay, float duration, Interpolator i) { + value = mStart = start; + mEnd = end; + mDelay = delay; + mDuration = duration; + mInterpolator = i; + + mAllProperties.add(this); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationProvider.java new file mode 100644 index 0000000000..e3c9f6ecab --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationProvider.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.animation.AnimatorSet; +import android.app.ActivityOptions; +import android.os.Handler; + +import com.android.launcher3.LauncherAnimationRunner; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +@FunctionalInterface +public interface RemoteAnimationProvider { + + static final int Z_BOOST_BASE = 800570000; + + AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targets); + + default ActivityOptions toActivityOptions(Handler handler, long duration) { + LauncherAnimationRunner runner = new LauncherAnimationRunner(handler, + false /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + result.setAnimation(createWindowAnimation(targetCompats)); + } + }; + return ActivityOptionsCompat.makeRemoteAnimation( + new RemoteAnimationAdapterCompat(runner, duration, 0)); + } + + /** + * Prepares the given {@param targets} for a remote animation, and should be called with the + * transaction from the first frame of animation. + * + * @param boostModeTargets The mode indicating which targets to boost in z-order above other + * targets. + */ + static void prepareTargetsForFirstFrame(RemoteAnimationTargetCompat[] targets, + TransactionCompat t, int boostModeTargets) { + for (RemoteAnimationTargetCompat target : targets) { + t.setLayer(target.leash, getLayer(target, boostModeTargets)); + t.show(target.leash); + } + } + + static int getLayer(RemoteAnimationTargetCompat target, int boostModeTarget) { + return target.mode == boostModeTarget + ? Z_BOOST_BASE + target.prefixOrderIndex + : target.prefixOrderIndex; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationTargetSet.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationTargetSet.java new file mode 100644 index 0000000000..d1097cdf16 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteAnimationTargetSet.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.ArrayList; + +/** + * Holds a collection of RemoteAnimationTargets, filtered by different properties. + */ +public class RemoteAnimationTargetSet { + + public final RemoteAnimationTargetCompat[] unfilteredApps; + public final RemoteAnimationTargetCompat[] apps; + public final int targetMode; + + public RemoteAnimationTargetSet(RemoteAnimationTargetCompat[] apps, int targetMode) { + ArrayList filteredApps = new ArrayList<>(); + if (apps != null) { + for (RemoteAnimationTargetCompat target : apps) { + if (target.mode == targetMode) { + filteredApps.add(target); + } + } + } + + this.unfilteredApps = apps; + this.apps = filteredApps.toArray(new RemoteAnimationTargetCompat[filteredApps.size()]); + this.targetMode = targetMode; + } + + public RemoteAnimationTargetCompat findTask(int taskId) { + for (RemoteAnimationTargetCompat target : apps) { + if (target.taskId == taskId) { + return target; + } + } + return null; + } + + public boolean isAnimatingHome() { + for (RemoteAnimationTargetCompat target : apps) { + if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + return true; + } + } + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteFadeOutAnimationListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteFadeOutAnimationListener.java new file mode 100644 index 0000000000..4607fdc240 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/RemoteFadeOutAnimationListener.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; + +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +import static com.android.quickstep.util.RemoteAnimationProvider.prepareTargetsForFirstFrame; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +/** + * Animation listener which fades out the closing targets + */ +public class RemoteFadeOutAnimationListener implements AnimatorUpdateListener { + + private final RemoteAnimationTargetSet mTarget; + private boolean mFirstFrame = true; + + public RemoteFadeOutAnimationListener(RemoteAnimationTargetCompat[] targets) { + mTarget = new RemoteAnimationTargetSet(targets, MODE_CLOSING); + } + + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + TransactionCompat t = new TransactionCompat(); + if (mFirstFrame) { + prepareTargetsForFirstFrame(mTarget.unfilteredApps, t, MODE_CLOSING); + mFirstFrame = false; + } + + float alpha = 1 - valueAnimator.getAnimatedFraction(); + for (RemoteAnimationTargetCompat app : mTarget.apps) { + t.setAlpha(app.leash, alpha); + } + t.apply(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TaskViewDrawable.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TaskViewDrawable.java new file mode 100644 index 0000000000..1d452866aa --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TaskViewDrawable.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.util.FloatProperty; +import android.view.View; + +import com.android.launcher3.Utilities; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.quickstep.views.TaskView; + +public class TaskViewDrawable extends Drawable { + + public static final FloatProperty PROGRESS = + new FloatProperty("progress") { + @Override + public void setValue(TaskViewDrawable taskViewDrawable, float v) { + taskViewDrawable.setProgress(v); + } + + @Override + public Float get(TaskViewDrawable taskViewDrawable) { + return taskViewDrawable.mProgress; + } + }; + + /** + * The progress at which we play the atomic icon scale animation. + */ + private static final float ICON_SCALE_THRESHOLD = 0.95f; + + private final RecentsView mParent; + private final View mIconView; + private final int[] mIconPos; + + private final TaskThumbnailView mThumbnailView; + + private final ClipAnimationHelper mClipAnimationHelper; + + private float mProgress = 1; + private boolean mPassedIconScaleThreshold; + private ValueAnimator mIconScaleAnimator; + private float mIconScale; + + public TaskViewDrawable(TaskView tv, RecentsView parent) { + mParent = parent; + mIconView = tv.getIconView(); + mIconPos = new int[2]; + mIconScale = mIconView.getScaleX(); + Utilities.getDescendantCoordRelativeToAncestor(mIconView, parent, mIconPos, true); + + mThumbnailView = tv.getThumbnail(); + mClipAnimationHelper = new ClipAnimationHelper(); + mClipAnimationHelper.fromTaskThumbnailView(mThumbnailView, parent); + } + + public void setProgress(float progress) { + mProgress = progress; + mParent.invalidate(); + boolean passedIconScaleThreshold = progress <= ICON_SCALE_THRESHOLD; + if (mPassedIconScaleThreshold != passedIconScaleThreshold) { + mPassedIconScaleThreshold = passedIconScaleThreshold; + animateIconScale(mPassedIconScaleThreshold ? 0 : 1); + } + } + + private void animateIconScale(float toScale) { + if (mIconScaleAnimator != null) { + mIconScaleAnimator.cancel(); + } + mIconScaleAnimator = ValueAnimator.ofFloat(mIconScale, toScale); + mIconScaleAnimator.addUpdateListener(valueAnimator -> { + mIconScale = (float) valueAnimator.getAnimatedValue(); + if (mProgress > ICON_SCALE_THRESHOLD) { + // Speed up the icon scale to ensure it is 1 when progress is 1. + float iconProgress = (mProgress - ICON_SCALE_THRESHOLD) / (1 - ICON_SCALE_THRESHOLD); + if (iconProgress > mIconScale) { + mIconScale = iconProgress; + } + } + invalidateSelf(); + }); + mIconScaleAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mIconScaleAnimator = null; + } + }); + mIconScaleAnimator.setDuration(TaskView.SCALE_ICON_DURATION); + mIconScaleAnimator.start(); + } + + @Override + public void draw(Canvas canvas) { + canvas.save(); + canvas.translate(mParent.getScrollX(), mParent.getScrollY()); + mClipAnimationHelper.drawForProgress(mThumbnailView, canvas, mProgress); + canvas.restore(); + + canvas.save(); + canvas.translate(mIconPos[0], mIconPos[1]); + canvas.scale(mIconScale, mIconScale, mIconView.getWidth() / 2, mIconView.getHeight() / 2); + mIconView.draw(canvas); + canvas.restore(); + } + + public ClipAnimationHelper getClipAnimationHelper() { + return mClipAnimationHelper; + } + + @Override + public void setAlpha(int i) { } + + @Override + public void setColorFilter(ColorFilter colorFilter) { } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TransformedRect.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TransformedRect.java new file mode 100644 index 0000000000..b1b0f4e0d8 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/util/TransformedRect.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.util; + +import android.graphics.Rect; + +/** + * A wrapper around {@link Rect} with additional transformation properties + */ +public class TransformedRect { + + public final Rect rect = new Rect(); + public float scale = 1; + + public void set(TransformedRect transformedRect) { + rect.set(transformedRect.rect); + scale = transformedRect.scale; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ClearAllButton.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ClearAllButton.java new file mode 100644 index 0000000000..a4e276cd2a --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ClearAllButton.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; + +import com.android.launcher3.Utilities; +import com.android.quickstep.views.RecentsView.PageCallbacks; +import com.android.quickstep.views.RecentsView.ScrollState; + +public class ClearAllButton extends Button implements PageCallbacks { + + private float mScrollAlpha = 1; + private float mContentAlpha = 1; + + private final boolean mIsRtl; + + private int mScrollOffset; + + public ClearAllButton(Context context, AttributeSet attrs) { + super(context, attrs); + mIsRtl = Utilities.isRtl(context.getResources()); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + RecentsView parent = (RecentsView) getParent(); + mScrollOffset = mIsRtl ? parent.getPaddingRight() / 2 : - parent.getPaddingLeft() / 2; + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public void setContentAlpha(float alpha) { + if (mContentAlpha != alpha) { + mContentAlpha = alpha; + updateAlpha(); + } + } + + @Override + public void onPageScroll(ScrollState scrollState) { + float width = getWidth(); + if (width == 0) { + return; + } + + float shift = Math.min(scrollState.scrollFromEdge, width); + setTranslationX(mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift)); + mScrollAlpha = 1 - shift / width; + updateAlpha(); + } + + private void updateAlpha() { + final float alpha = mScrollAlpha * mContentAlpha; + setAlpha(alpha); + setClickable(alpha == 1); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/IconView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/IconView.java new file mode 100644 index 0000000000..81f57681d9 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/IconView.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +/** + * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout + * when the drawable changes. + */ +public class IconView extends View { + + private Drawable mDrawable; + + public IconView(Context context) { + super(context); + } + + public IconView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public IconView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setDrawable(Drawable d) { + if (mDrawable != null) { + mDrawable.setCallback(null); + } + mDrawable = d; + if (mDrawable != null) { + mDrawable.setCallback(this); + mDrawable.setBounds(0, 0, getWidth(), getHeight()); + } + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mDrawable != null) { + mDrawable.setBounds(0, 0, w, h); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + final Drawable drawable = mDrawable; + if (drawable != null && drawable.isStateful() + && drawable.setState(getDrawableState())) { + invalidateDrawable(drawable); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDrawable != null) { + mDrawable.draw(canvas); + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherLayoutListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherLayoutListener.java new file mode 100644 index 0000000000..3a74851d03 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherLayoutListener.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.views; + +import android.graphics.Rect; +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Insettable; +import com.android.launcher3.Launcher; +import com.android.quickstep.ActivityControlHelper.LayoutListener; +import com.android.quickstep.WindowTransformSwipeHandler; + +import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK; +import static com.android.launcher3.states.RotationHelper.REQUEST_NONE; + +/** + * Floating view which shows the task snapshot allowing it to be dragged and placed. + */ +public class LauncherLayoutListener extends AbstractFloatingView + implements Insettable, LayoutListener { + + private final Launcher mLauncher; + private WindowTransformSwipeHandler mHandler; + + public LauncherLayoutListener(Launcher launcher) { + super(launcher, null); + mLauncher = launcher; + setVisibility(INVISIBLE); + + // For the duration of the gesture, lock the screen orientation to ensure that we do not + // rotate mid-quickscrub + launcher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK); + } + + @Override + public void setHandler(WindowTransformSwipeHandler handler) { + mHandler = handler; + } + + @Override + public void setInsets(Rect insets) { + if (mHandler != null) { + mHandler.buildAnimationController(); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + return false; + } + + @Override + protected void handleClose(boolean animate) { + if (mIsOpen) { + mIsOpen = false; + // We don't support animate. + mLauncher.getDragLayer().removeView(this); + + if (mHandler != null) { + mHandler.layoutListenerClosed(); + } + } + } + + @Override + public void open() { + if (!mIsOpen) { + mLauncher.getDragLayer().addView(this); + mIsOpen = true; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(1, 1); + } + + @Override + public void logActionCommand(int command) { + // We should probably log the weather + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_QUICKSTEP_PREVIEW) != 0; + } + + @Override + public void finish() { + setHandler(null); + close(false); + mLauncher.getRotationHelper().setStateHandlerRequest(REQUEST_NONE); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherRecentsView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherRecentsView.java new file mode 100644 index 0000000000..26f0bbfd02 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/LauncherRecentsView.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.views; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.view.View; +import android.view.ViewDebug; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.R; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.views.ScrimView; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.LayoutUtils; + +import static com.android.launcher3.LauncherAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN; +import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; + +/** + * {@link RecentsView} used in Launcher activity + */ +@TargetApi(Build.VERSION_CODES.O) +public class LauncherRecentsView extends RecentsView { + + public static final FloatProperty TRANSLATION_Y_FACTOR = + new FloatProperty("translationYFactor") { + + @Override + public void setValue(LauncherRecentsView view, float v) { + view.setTranslationYFactor(v); + } + + @Override + public Float get(LauncherRecentsView view) { + return view.mTranslationYFactor; + } + }; + + @ViewDebug.ExportedProperty(category = "launcher") + private float mTranslationYFactor; + + public LauncherRecentsView(Context context) { + this(context, null); + } + + public LauncherRecentsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LauncherRecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setContentAlpha(0); + } + + @Override + protected void startHome() { + mActivity.getStateManager().goToState(NORMAL); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + setTranslationYFactor(mTranslationYFactor); + } + + public void setTranslationYFactor(float translationFactor) { + mTranslationYFactor = translationFactor; + setTranslationY(computeTranslationYForFactor(mTranslationYFactor)); + } + + public float computeTranslationYForFactor(float translationYFactor) { + return translationYFactor * (getPaddingBottom() - getPaddingTop()); + } + + @Override + public void draw(Canvas canvas) { + maybeDrawEmptyMessage(canvas); + super.draw(canvas); + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + updateEmptyMessage(); + } + + @Override + protected void onTaskStackUpdated() { + // Lazily update the empty message only when the task stack is reapplied + updateEmptyMessage(); + } + + /** + * Animates adjacent tasks and translate hotseat off screen as well. + */ + @Override + public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv, + ClipAnimationHelper helper) { + AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv, helper); + + if (!OverviewInteractionState.getInstance(mActivity).isSwipeUpGestureEnabled()) { + // Hotseat doesn't move when opening recents with the button, + // so don't animate it here either. + return anim; + } + + float allAppsProgressOffscreen = ALL_APPS_PROGRESS_OFF_SCREEN; + LauncherState state = mActivity.getStateManager().getState(); + if ((state.getVisibleElements(mActivity) & ALL_APPS_HEADER_EXTRA) != 0) { + float maxShiftRange = mActivity.getDeviceProfile().heightPx; + float currShiftRange = mActivity.getAllAppsController().getShiftRange(); + allAppsProgressOffscreen = 1f + (maxShiftRange - currShiftRange) / maxShiftRange; + } + anim.play(ObjectAnimator.ofFloat( + mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen)); + + ObjectAnimator dragHandleAnim = ObjectAnimator.ofInt( + mActivity.findViewById(R.id.scrim_view), ScrimView.DRAG_HANDLE_ALPHA, 0); + dragHandleAnim.setInterpolator(Interpolators.ACCEL_2); + anim.play(dragHandleAnim); + + return anim; + } + + @Override + protected void getTaskSize(DeviceProfile dp, Rect outRect) { + LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect); + } + + @Override + protected void onTaskLaunched(boolean success) { + if (success) { + mActivity.getStateManager().goToState(NORMAL, false /* animate */); + } else { + LauncherState state = mActivity.getStateManager().getState(); + mActivity.getAllAppsController().setState(state); + } + super.onTaskLaunched(success); + } + + @Override + public boolean shouldUseMultiWindowTaskSizeStrategy() { + return mActivity.isInMultiWindowModeCompat(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/RecentsView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/RecentsView.java new file mode 100644 index 0000000000..e9c68cd35c --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/RecentsView.java @@ -0,0 +1,1367 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.os.UserHandle; +import android.support.annotation.Nullable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.ArraySet; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.SparseBooleanArray; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewDebug; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.ListView; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Insettable; +import com.android.launcher3.PagedView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.PropertyListBuilder; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.PendingAnimation; +import com.android.launcher3.util.Themes; +import com.android.quickstep.OverviewCallbacks; +import com.android.quickstep.QuickScrubController; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.TaskUtils; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.TaskViewDrawable; +import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; +import com.android.systemui.shared.recents.model.RecentsTaskLoader; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.TaskStack; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.PackageManagerWrapper; +import com.android.systemui.shared.system.TaskStackChangeListener; + +import java.util.ArrayList; +import java.util.function.Consumer; + +import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.ACCEL_2; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; +import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; +import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW; + +/** + * A list of recent tasks. + */ +@TargetApi(Build.VERSION_CODES.P) +public abstract class RecentsView extends PagedView implements Insettable { + + private static final String TAG = RecentsView.class.getSimpleName(); + + public static final FloatProperty CONTENT_ALPHA = + new FloatProperty("contentAlpha") { + @Override + public void setValue(RecentsView view, float v) { + view.setContentAlpha(v); + } + + @Override + public Float get(RecentsView view) { + return view.getContentAlpha(); + } + }; + + private final Rect mTempRect = new Rect(); + + private static final int DISMISS_TASK_DURATION = 300; + // The threshold at which we update the SystemUI flags when animating from the task into the app + public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f; + + private static final float[] sTempFloatArray = new float[3]; + + protected final T mActivity; + private final QuickScrubController mQuickScrubController; + private final float mFastFlingVelocity; + private final RecentsModel mModel; + private final int mTaskTopMargin; + private final ClearAllButton mClearAllButton; + private final Rect mClearAllButtonDeadZoneRect = new Rect(); + private final Rect mTaskViewDeadZoneRect = new Rect(); + + private final ScrollState mScrollState = new ScrollState(); + // Keeps track of the previously known visible tasks for purposes of loading/unloading task data + private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); + + /** + * TODO: Call reloadIdNeeded in onTaskStackChanged. + */ + private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { + @Override + public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) { + if (!mHandleTaskStackChanges) { + return; + } + updateThumbnail(taskId, snapshot); + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + if (!mHandleTaskStackChanges) { + return; + } + // Check this is for the right user + if (!checkCurrentOrManagedUserId(userId, getContext())) { + return; + } + + // Remove the task immediately from the task list + TaskView taskView = getTaskView(taskId); + if (taskView != null) { + removeView(taskView); + } + } + + @Override + public void onActivityUnpinned() { + if (!mHandleTaskStackChanges) { + return; + } + // TODO: Re-enable layout transitions for addition of the unpinned task + reloadIfNeeded(); + } + + @Override + public void onTaskRemoved(int taskId) { + if (!mHandleTaskStackChanges) { + return; + } + BackgroundExecutor.get().submit(() -> { + TaskView taskView = getTaskView(taskId); + if (taskView == null) { + return; + } + Handler handler = taskView.getHandler(); + if (handler == null) { + return; + } + + // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and + // remove all these checks + Task.TaskKey taskKey = taskView.getTask().key; + if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(), + taskKey.userId) == null) { + // The package was uninstalled + handler.post(() -> + dismissTask(taskView, true /* animate */, false /* removeTask */)); + } else { + RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getContext()); + RecentsTaskLoadPlan.PreloadOptions opts = + new RecentsTaskLoadPlan.PreloadOptions(); + opts.loadTitles = false; + loadPlan.preloadPlan(opts, mModel.getRecentsTaskLoader(), -1, + UserHandle.myUserId()); + if (loadPlan.getTaskStack().findTaskWithId(taskId) == null) { + // The task was removed from the recents list + handler.post(() -> + dismissTask(taskView, true /* animate */, false /* removeTask */)); + } + } + }); + } + + @Override + public void onPinnedStackAnimationStarted() { + // Needed for activities that auto-enter PiP, which will not trigger a remote + // animation to be created + mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); + } + }; + + private int mLoadPlanId = -1; + + // Only valid until the launcher state changes to NORMAL + private int mRunningTaskId = -1; + private boolean mRunningTaskTileHidden; + private Task mTmpRunningTask; + + private boolean mRunningTaskIconScaledDown = false; + + private boolean mOverviewStateEnabled; + private boolean mHandleTaskStackChanges; + private Runnable mNextPageSwitchRunnable; + private boolean mSwipeDownShouldLaunchApp; + private boolean mTouchDownToStartHome; + private final int mTouchSlop; + private int mDownX; + private int mDownY; + + private PendingAnimation mPendingAnimation; + + @ViewDebug.ExportedProperty(category = "launcher") + private float mContentAlpha = 1; + + // Keeps track of task views whose visual state should not be reset + private ArraySet mIgnoreResetTaskViews = new ArraySet<>(); + + // Variables for empty state + private final Drawable mEmptyIcon; + private final CharSequence mEmptyMessage; + private final TextPaint mEmptyMessagePaint; + private final Point mLastMeasureSize = new Point(); + private final int mEmptyMessagePadding; + private boolean mShowEmptyMessage; + private Layout mEmptyTextLayout; + + private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener = + (inMultiWindowMode) -> { + if (!inMultiWindowMode && mOverviewStateEnabled) { + // TODO: Re-enable layout transitions for addition of the unpinned task + reloadIfNeeded(); + } + }; + + public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); + enableFreeScroll(true); + + mFastFlingVelocity = getResources() + .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); + mActivity = (T) BaseActivity.fromContext(context); + mQuickScrubController = new QuickScrubController(mActivity, this); + mModel = RecentsModel.getInstance(context); + + mClearAllButton = (ClearAllButton) LayoutInflater.from(context) + .inflate(R.layout.overview_clear_all_button, this, false); + mClearAllButton.setOnClickListener(this::dismissAllTasks); + + mIsRtl = !Utilities.isRtl(getResources()); + setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); + mTaskTopMargin = getResources() + .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); + mEmptyIcon.setCallback(this); + mEmptyMessage = context.getText(R.string.recents_empty_message); + mEmptyMessagePaint = new TextPaint(); + mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); + mEmptyMessagePaint.setTextSize(getResources() + .getDimension(R.dimen.recents_empty_message_text_size)); + mEmptyMessagePadding = getResources() + .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); + setWillNotDraw(false); + updateEmptyMessage(); + } + + public boolean isRtl() { + return mIsRtl; + } + + public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) { + TaskView taskView = getTaskView(taskId); + if (taskView != null) { + taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData); + } + return taskView; + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + updateTaskStackListenerState(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + updateTaskStackListenerState(); + mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); + ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + updateTaskStackListenerState(); + mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); + ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + + // Clear the task data for the removed child if it was visible + if (child != mClearAllButton) { + Task task = ((TaskView) child).getTask(); + if (mHasVisibleTaskData.get(task.key.id)) { + mHasVisibleTaskData.delete(task.key.id); + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + } + } + + public boolean isTaskViewVisible(TaskView tv) { + // For now, just check if it's the active task or an adjacent task + return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; + } + + public TaskView getTaskView(int taskId) { + for (int i = 0; i < getTaskViewCount(); i++) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask().key.id == taskId) { + return tv; + } + } + return null; + } + + public void setOverviewStateEnabled(boolean enabled) { + mOverviewStateEnabled = enabled; + updateTaskStackListenerState(); + } + + public void setNextPageSwitchRunnable(Runnable r) { + mNextPageSwitchRunnable = r; + } + + @Override + protected void onPageEndTransition() { + super.onPageEndTransition(); + if (mNextPageSwitchRunnable != null) { + mNextPageSwitchRunnable.run(); + mNextPageSwitchRunnable = null; + } + if (getNextPage() > 0) { + setSwipeDownShouldLaunchApp(true); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + super.onTouchEvent(ev); + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + switch (ev.getAction()) { + case MotionEvent.ACTION_UP: + if (mShowEmptyMessage) { + onAllTasksRemoved(); + } + if (mTouchDownToStartHome) { + startHome(); + } + mTouchDownToStartHome = false; + break; + case MotionEvent.ACTION_CANCEL: + mTouchDownToStartHome = false; + break; + case MotionEvent.ACTION_MOVE: + // Passing the touch slop will not allow dismiss to home + if (mTouchDownToStartHome && Math.hypot(mDownX - x, mDownY - y) > mTouchSlop) { + mTouchDownToStartHome = false; + } + break; + case MotionEvent.ACTION_DOWN: + // Touch down anywhere but the deadzone around the visible clear all button and + // between the task views will start home on touch up + if (mTouchState == TOUCH_STATE_REST) { + updateDeadZoneRects(); + final boolean clearAllButtonDeadZoneConsumed = mClearAllButton.getAlpha() == 1 + && mClearAllButtonDeadZoneRect.contains(x, y); + if (!clearAllButtonDeadZoneConsumed + && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) { + mTouchDownToStartHome = true; + } + } + mDownX = x; + mDownY = y; + break; + } + + + // Do not let touch escape to siblings below this view. + return true; + } + + private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) { + if (mPendingAnimation != null) { + mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan)); + return; + } + TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null; + if (stack == null) { + removeAllViews(); + onTaskStackUpdated(); + return; + } + + int oldChildCount = getChildCount(); + + // Ensure there are as many views as there are tasks in the stack (adding and trimming as + // necessary) + final LayoutInflater inflater = LayoutInflater.from(getContext()); + final ArrayList tasks = new ArrayList<>(stack.getTasks()); + + final int requiredTaskCount = tasks.size(); + if (getTaskViewCount() != requiredTaskCount) { + if (oldChildCount > 0) { + removeView(mClearAllButton); + } + for (int i = getChildCount(); i < requiredTaskCount; i++) { + final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false); + addView(taskView); + } + while (getChildCount() > requiredTaskCount) { + final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1); + removeView(taskView); + } + if (requiredTaskCount > 0) { + addView(mClearAllButton); + } + } + + // Unload existing visible task data + unloadVisibleTaskData(); + + // Rebind and reset all task views + for (int i = requiredTaskCount - 1; i >= 0; i--) { + final int pageIndex = requiredTaskCount - i - 1; + final Task task = tasks.get(i); + final TaskView taskView = (TaskView) getChildAt(pageIndex); + taskView.bind(task); + } + resetTaskVisuals(); + + if (oldChildCount != getChildCount()) { + mQuickScrubController.snapToNextTaskIfAvailable(); + } + onTaskStackUpdated(); + } + + public int getTaskViewCount() { + // Account for the clear all button. + int childCount = getChildCount(); + return childCount == 0 ? 0 : childCount - 1; + } + + protected void onTaskStackUpdated() { } + + public void resetTaskVisuals() { + for (int i = getTaskViewCount() - 1; i >= 0; i--) { + TaskView taskView = (TaskView) getChildAt(i); + if (!mIgnoreResetTaskViews.contains(taskView)) { + taskView.resetVisualProperties(); + } + } + if (mRunningTaskTileHidden) { + setRunningTaskHidden(mRunningTaskTileHidden); + } + applyIconScale(false /* animate */); + + updateCurveProperties(); + // Update the set of visible task's data + loadVisibleTaskData(); + } + + private void updateTaskStackListenerState() { + boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() + && getWindowVisibility() == VISIBLE; + if (handleTaskStackChanges != mHandleTaskStackChanges) { + mHandleTaskStackChanges = handleTaskStackChanges; + if (handleTaskStackChanges) { + reloadIfNeeded(); + } + } + } + + @Override + public void setInsets(Rect insets) { + mInsets.set(insets); + DeviceProfile dp = mActivity.getDeviceProfile(); + getTaskSize(dp, mTempRect); + + // Keep this logic in sync with ActivityControlHelper.getTranslationYForQuickScrub. + mTempRect.top -= mTaskTopMargin; + setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, + dp.availableWidthPx + mInsets.left - mTempRect.right, + dp.availableHeightPx + mInsets.top - mTempRect.bottom); + } + + protected abstract void getTaskSize(DeviceProfile dp, Rect outRect); + + public void getTaskSize(Rect outRect) { + getTaskSize(mActivity.getDeviceProfile(), outRect); + } + + @Override + protected boolean computeScrollHelper() { + boolean scrolling = super.computeScrollHelper(); + boolean isFlingingFast = false; + updateCurveProperties(); + if (scrolling || (mTouchState == TOUCH_STATE_SCROLLING)) { + if (scrolling) { + // Check if we are flinging quickly to disable high res thumbnail loading + isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity; + } + + // After scrolling, update the visible task's data + loadVisibleTaskData(); + } + + // Update the high res thumbnail loader + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + loader.getHighResThumbnailLoader().setFlingingFast(isFlingingFast); + return scrolling; + } + + /** + * Scales and adjusts translation of adjacent pages as if on a curved carousel. + */ + public void updateCurveProperties() { + if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) { + return; + } + int scrollX = getScrollX(); + final int halfPageWidth = getNormalChildWidth() / 2; + final int screenCenter = mInsets.left + getPaddingLeft() + scrollX + halfPageWidth; + final int halfScreenWidth = getMeasuredWidth() / 2; + final int pageSpacing = mPageSpacing; + mScrollState.scrollFromEdge = mIsRtl ? scrollX : (mMaxScrollX - scrollX); + + final int pageCount = getPageCount(); + for (int i = 0; i < pageCount; i++) { + View page = getPageAt(i); + float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth; + float distanceFromScreenCenter = screenCenter - pageCenter; + float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing; + mScrollState.linearInterpolation = Math.min(1, + Math.abs(distanceFromScreenCenter) / distanceToReachEdge); + ((PageCallbacks) page).onPageScroll(mScrollState); + } + } + + /** + * Iterates through all thet asks, and loads the associated task data for newly visible tasks, + * and unloads the associated task data for tasks that are no longer visible. + */ + public void loadVisibleTaskData() { + if (!mOverviewStateEnabled) { + // Skip loading visible task data if we've already left the overview state + return; + } + + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + int centerPageIndex = getPageNearestToCenterOfScreen(); + int numChildren = getTaskViewCount(); + int lower = Math.max(0, centerPageIndex - 2); + int upper = Math.min(centerPageIndex + 2, numChildren - 1); + + // Update the task data for the in/visible children + for (int i = 0; i < numChildren; i++) { + TaskView taskView = (TaskView) getChildAt(i); + Task task = taskView.getTask(); + boolean visible = lower <= i && i <= upper; + if (visible) { + if (task == mTmpRunningTask) { + // Skip loading if this is the task that we are animating into + continue; + } + if (!mHasVisibleTaskData.get(task.key.id)) { + loader.loadTaskData(task); + loader.getHighResThumbnailLoader().onTaskVisible(task); + } + mHasVisibleTaskData.put(task.key.id, visible); + } else { + if (mHasVisibleTaskData.get(task.key.id)) { + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + mHasVisibleTaskData.delete(task.key.id); + } + } + } + + /** + * Unloads any associated data from the currently visible tasks + */ + private void unloadVisibleTaskData() { + RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); + for (int i = 0; i < mHasVisibleTaskData.size(); i++) { + if (mHasVisibleTaskData.valueAt(i)) { + TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); + Task task = taskView.getTask(); + loader.unloadTaskData(task); + loader.getHighResThumbnailLoader().onTaskInvisible(task); + } + } + mHasVisibleTaskData.clear(); + } + + protected void onAllTasksRemoved() { + startHome(); + } + + protected abstract void startHome(); + + public void reset() { + mRunningTaskId = -1; + mRunningTaskTileHidden = false; + + unloadVisibleTaskData(); + setCurrentPage(0); + + OverviewCallbacks.get(getContext()).onResetOverview(); + } + + /** + * Reloads the view if anything in recents changed. + */ + public void reloadIfNeeded() { + if (!mModel.isLoadPlanValid(mLoadPlanId)) { + mLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan); + } + } + + /** + * Ensures that the first task in the view represents {@param task} and reloads the view + * if needed. This allows the swipe-up gesture to assume that the first tile always + * corresponds to the correct task. + * All subsequent calls to reload will keep the task as the first item until {@link #reset()} + * is called. + * Also scrolls the view to this task + */ + public void showTask(int runningTaskId) { + if (getChildCount() == 0) { + // Add an empty view for now until the task plan is loaded and applied + final TaskView taskView = (TaskView) LayoutInflater.from(getContext()) + .inflate(R.layout.task, this, false); + addView(taskView); + addView(mClearAllButton); + + // The temporary running task is only used for the duration between the start of the + // gesture and the task list is loaded and applied + mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(), + new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0, + false, true, false, false, new ActivityManager.TaskDescription(), 0, + new ComponentName("", ""), false); + taskView.bind(mTmpRunningTask); + } + setCurrentTask(runningTaskId); + } + + /** + * Hides the tile associated with {@link #mRunningTaskId} + */ + public void setRunningTaskHidden(boolean isHidden) { + mRunningTaskTileHidden = isHidden; + TaskView runningTask = getTaskView(mRunningTaskId); + if (runningTask != null) { + runningTask.setAlpha(isHidden ? 0 : mContentAlpha); + } + } + + /** + * Similar to {@link #showTask(int)} but does not put any restrictions on the first tile. + */ + public void setCurrentTask(int runningTaskId) { + boolean runningTaskTileHidden = mRunningTaskTileHidden; + boolean runningTaskIconScaledDown = mRunningTaskIconScaledDown; + + setRunningTaskIconScaledDown(false, false); + setRunningTaskHidden(false); + mRunningTaskId = runningTaskId; + setRunningTaskIconScaledDown(runningTaskIconScaledDown, false); + setRunningTaskHidden(runningTaskTileHidden); + + setCurrentPage(0); + + // Load the tasks (if the loading is already + mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan); + } + + public void showNextTask() { + TaskView runningTaskView = getTaskView(mRunningTaskId); + if (runningTaskView == null) { + // Launch the first task + if (getTaskViewCount() > 0) { + ((TaskView) getChildAt(0)).launchTask(true /* animate */); + } + } else { + // Get the next launch task + int runningTaskIndex = indexOfChild(runningTaskView); + int nextTaskIndex = Math.max(0, Math.min(getTaskViewCount() - 1, runningTaskIndex + 1)); + if (nextTaskIndex < getTaskViewCount()) { + ((TaskView) getChildAt(nextTaskIndex)).launchTask(true /* animate */); + } + } + } + + public QuickScrubController getQuickScrubController() { + return mQuickScrubController; + } + + public void setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate) { + if (mRunningTaskIconScaledDown == isScaledDown) { + return; + } + mRunningTaskIconScaledDown = isScaledDown; + applyIconScale(animate); + } + + private void applyIconScale(boolean animate) { + float scale = mRunningTaskIconScaledDown ? 0 : 1; + TaskView firstTask = getTaskView(mRunningTaskId); + if (firstTask != null) { + if (animate) { + firstTask.animateIconToScaleAndDim(scale); + } else { + firstTask.setIconScaleAndDim(scale); + } + } + } + + public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) { + mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp; + } + + public boolean shouldSwipeDownLaunchApp() { + return mSwipeDownShouldLaunchApp; + } + + public interface PageCallbacks { + + /** + * Updates the page UI based on scroll params. + */ + default void onPageScroll(ScrollState scrollState) {} + } + + public static class ScrollState { + + /** + * The progress from 0 to 1, where 0 is the center + * of the screen and 1 is the edge of the screen. + */ + public float linearInterpolation; + + /** + * The amount by which all the content is scrolled relative to the end of the list. + */ + public float scrollFromEdge; + } + + public void addIgnoreResetTask(TaskView taskView) { + mIgnoreResetTaskViews.add(taskView); + } + + public void removeIgnoreResetTask(TaskView taskView) { + mIgnoreResetTaskViews.remove(taskView); + } + + private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) { + addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); + addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), + duration, LINEAR, anim); + } + + private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, + boolean shouldLog) { + if (task != null) { + ActivityManagerWrapper.getInstance().removeTask(task.key.id); + if (shouldLog) { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( + onEndListener.logAction, Direction.UP, index, + TaskUtils.getLaunchComponentKeyForTask(task.key)); + } + } + } + + public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, + boolean shouldRemoveTask, long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + AnimatorSet anim = new AnimatorSet(); + PendingAnimation pendingAnimation = new PendingAnimation(anim); + + int count = getPageCount(); + if (count == 0) { + return pendingAnimation; + } + + int[] oldScroll = new int[count]; + getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); + + int[] newScroll = new int[count]; + getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView); + + int taskCount = getTaskViewCount(); + int scrollDiffPerPage = 0; + if (count > 1) { + scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); + } + int draggedIndex = indexOfChild(taskView); + + boolean needsCurveUpdates = false; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child == taskView) { + if (animateTaskView) { + addDismissedTaskAnimations(taskView, anim, duration); + } + } else { + // If we just take newScroll - oldScroll, everything to the right of dragged task + // translates to the left. We need to offset this in some cases: + // - In RTL, add page offset to all pages, since we want pages to move to the right + // Additionally, add a page offset if: + // - Current page is rightmost page (leftmost for RTL) + // - Dragging an adjacent page on the left side (right side for RTL) + int offset = mIsRtl ? scrollDiffPerPage : 0; + if (mCurrentPage == draggedIndex) { + int lastPage = taskCount - 1; + if (mCurrentPage == lastPage) { + offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; + } + } else { + // Dragging an adjacent page. + int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) + if (draggedIndex == negativeAdjacent) { + offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; + } + } + int scrollDiff = newScroll[i] - oldScroll[i] + offset; + if (scrollDiff != 0) { + addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), + duration, ACCEL, anim); + needsCurveUpdates = true; + } + } + } + + if (needsCurveUpdates) { + ValueAnimator va = ValueAnimator.ofFloat(0, 1); + va.addUpdateListener((a) -> updateCurveProperties()); + anim.play(va); + } + + // Add a tiny bit of translation Z, so that it draws on top of other views + if (animateTaskView) { + taskView.setTranslationZ(0.1f); + } + + mPendingAnimation = pendingAnimation; + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + if (shouldRemoveTask) { + removeTask(taskView.getTask(), draggedIndex, onEndListener, true); + } + int pageToSnapTo = mCurrentPage; + if (draggedIndex < pageToSnapTo || pageToSnapTo == (getTaskViewCount() - 1)) { + pageToSnapTo -= 1; + } + removeView(taskView); + + if (getTaskViewCount() == 0) { + removeView(mClearAllButton); + onAllTasksRemoved(); + } else { + snapToPageImmediately(pageToSnapTo); + } + } + resetTaskVisuals(); + mPendingAnimation = null; + }); + return pendingAnimation; + } + + public PendingAnimation createAllTasksDismissAnimation(long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + AnimatorSet anim = new AnimatorSet(); + PendingAnimation pendingAnimation = new PendingAnimation(anim); + + int count = getTaskViewCount(); + for (int i = 0; i < count; i++) { + addDismissedTaskAnimations(getChildAt(i), anim, duration); + } + + mPendingAnimation = pendingAnimation; + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + int taskViewCount = getTaskViewCount(); + for (int i = 0; i < taskViewCount; i++) { + removeTask(getTaskViewAt(i).getTask(), -1, onEndListener, false); + } + removeAllViews(); + onAllTasksRemoved(); + } + mPendingAnimation = null; + }); + return pendingAnimation; + } + + private static void addAnim(ObjectAnimator anim, long duration, + TimeInterpolator interpolator, AnimatorSet set) { + anim.setDuration(duration).setInterpolator(interpolator); + set.play(anim); + } + + private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) { + if (pageCount == 0) { + return false; + } + final int newPageUnbound = getNextPage() + delta; + if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) { + return false; + } + snapToPage((newPageUnbound + pageCount) % pageCount); + getChildAt(getNextPage()).requestFocus(); + return true; + } + + private void runDismissAnimation(PendingAnimation pendingAnim) { + AnimatorPlaybackController controller = AnimatorPlaybackController.wrap( + pendingAnim.anim, DISMISS_TASK_DURATION); + controller.dispatchOnStart(); + controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE)); + controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); + controller.start(); + } + + public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { + runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask, + DISMISS_TASK_DURATION)); + } + + @SuppressWarnings("unused") + private void dismissAllTasks(View view) { + runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); + } + + private void dismissCurrentTask() { + TaskView taskView = getTaskView(getNextPage()); + if (taskView != null) { + dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_TAB: + return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1, + event.isAltPressed() /* cycle */); + case KeyEvent.KEYCODE_DPAD_RIGHT: + return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */); + case KeyEvent.KEYCODE_DPAD_LEFT: + return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */); + case KeyEvent.KEYCODE_DEL: + case KeyEvent.KEYCODE_FORWARD_DEL: + dismissCurrentTask(); + return true; + case KeyEvent.KEYCODE_NUMPAD_DOT: + if (event.isAltPressed()) { + // Numpad DEL pressed while holding Alt. + dismissCurrentTask(); + return true; + } + } + } + return super.dispatchKeyEvent(event); + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, + @Nullable Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (gainFocus && getChildCount() > 0) { + switch (direction) { + case FOCUS_FORWARD: + setCurrentPage(0); + break; + case FOCUS_BACKWARD: + case FOCUS_RIGHT: + case FOCUS_LEFT: + setCurrentPage(getChildCount() - 1); + break; + } + } + } + + public float getContentAlpha() { + return mContentAlpha; + } + + public void setContentAlpha(float alpha) { + if (alpha == mContentAlpha) { + return; + } + alpha = Utilities.boundToRange(alpha, 0, 1); + mContentAlpha = alpha; + for (int i = getTaskViewCount() - 1; i >= 0; i--) { + TaskView child = getTaskViewAt(i); + if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) { + getChildAt(i).setAlpha(alpha); + } + } + mClearAllButton.setContentAlpha(mContentAlpha); + + int alphaInt = Math.round(alpha * 255); + mEmptyMessagePaint.setAlpha(alphaInt); + mEmptyIcon.setAlpha(alphaInt); + + setVisibility(alpha > 0 ? VISIBLE : GONE); + } + + private float[] getAdjacentScaleAndTranslation(TaskView currTask, + float currTaskToScale, float currTaskToTranslationY) { + float displacement = currTask.getWidth() * (currTaskToScale - currTask.getCurveScale()); + sTempFloatArray[0] = currTaskToScale; + sTempFloatArray[1] = mIsRtl ? -displacement : displacement; + sTempFloatArray[2] = currTaskToTranslationY; + return sTempFloatArray; + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + child.setAlpha(mContentAlpha); + } + + public TaskView getTaskViewAt(int index) { + View child = getChildAt(index); + return child == mClearAllButton ? null : (TaskView) child; + } + + public void updateEmptyMessage() { + boolean isEmpty = getChildCount() == 0; + boolean hasSizeChanged = mLastMeasureSize.x != getWidth() + || mLastMeasureSize.y != getHeight(); + if (isEmpty == mShowEmptyMessage && !hasSizeChanged) { + return; + } + setContentDescription(isEmpty ? mEmptyMessage : ""); + mShowEmptyMessage = isEmpty; + updateEmptyStateUi(hasSizeChanged); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateEmptyStateUi(changed); + + // Set the pivot points to match the task preview center + setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin) + + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2); + setPivotX(((mInsets.left + getPaddingLeft()) + + (getWidth() - mInsets.right - getPaddingRight())) / 2); + } + + private void updateDeadZoneRects() { + // Get the deadzone rect surrounding the clear all button to not dismiss overview to home + mClearAllButtonDeadZoneRect.setEmpty(); + if (mClearAllButton.getWidth() > 0) { + int verticalMargin = getResources() + .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin); + mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect); + mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin); + } + + // Get the deadzone rect between the task views + mTaskViewDeadZoneRect.setEmpty(); + int count = getTaskViewCount(); + if (count > 0) { + final View taskView = getTaskViewAt(0); + getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect); + mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(), + taskView.getBottom()); + } + } + + private void updateEmptyStateUi(boolean sizeChanged) { + boolean hasValidSize = getWidth() > 0 && getHeight() > 0; + if (sizeChanged && hasValidSize) { + mEmptyTextLayout = null; + mLastMeasureSize.set(getWidth(), getHeight()); + } + + if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { + int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; + mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), + mEmptyMessagePaint, availableWidth) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .build(); + int totalHeight = mEmptyTextLayout.getHeight() + + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight(); + + int top = (mLastMeasureSize.y - totalHeight) / 2; + int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2; + mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(), + top + mEmptyIcon.getIntrinsicHeight()); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon); + } + + protected void maybeDrawEmptyMessage(Canvas canvas) { + if (mShowEmptyMessage && mEmptyTextLayout != null) { + // Offset to center in the visible (non-padded) part of RecentsView + mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), + mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); + canvas.save(); + canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, + (mTempRect.top - mTempRect.bottom) / 2); + mEmptyIcon.draw(canvas); + canvas.translate(mEmptyMessagePadding, + mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); + mEmptyTextLayout.draw(canvas); + canvas.restore(); + } + } + + /** + * Animate adjacent tasks off screen while scaling up. + * + * If launching one of the adjacent tasks, parallax the center task and other adjacent task + * to the right. + */ + public AnimatorSet createAdjacentPageAnimForTaskLaunch( + TaskView tv, ClipAnimationHelper clipAnimationHelper) { + AnimatorSet anim = new AnimatorSet(); + + int taskIndex = indexOfChild(tv); + int centerTaskIndex = getCurrentPage(); + boolean launchingCenterTask = taskIndex == centerTaskIndex; + + float toScale = clipAnimationHelper.getSourceRect().width() + / clipAnimationHelper.getTargetRect().width(); + float toTranslationY = clipAnimationHelper.getSourceRect().centerY() + - clipAnimationHelper.getTargetRect().centerY(); + if (launchingCenterTask) { + TaskView centerTask = getTaskViewAt(centerTaskIndex); + if (taskIndex - 1 >= 0) { + TaskView adjacentTask = getTaskViewAt(taskIndex - 1); + float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask, + toScale, toTranslationY); + scaleAndTranslation[1] = -scaleAndTranslation[1]; + anim.play(createAnimForChild(adjacentTask, scaleAndTranslation)); + } + if (taskIndex + 1 < getTaskViewCount()) { + TaskView adjacentTask = getTaskViewAt(taskIndex + 1); + float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask, + toScale, toTranslationY); + anim.play(createAnimForChild(adjacentTask, scaleAndTranslation)); + } + } else { + // We are launching an adjacent task, so parallax the center and other adjacent task. + float displacementX = tv.getWidth() * (toScale - tv.getCurveScale()); + anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X, + mIsRtl ? -displacementX : displacementX)); + + int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex); + if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) { + anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex), + new PropertyListBuilder() + .translationX(mIsRtl ? -displacementX : displacementX) + .scale(1) + .build())); + } + } + return anim; + } + + private Animator createAnimForChild(TaskView child, float[] toScaleAndTranslation) { + AnimatorSet anim = new AnimatorSet(); + anim.play(ObjectAnimator.ofFloat(child, TaskView.ZOOM_SCALE, toScaleAndTranslation[0])); + anim.play(ObjectAnimator.ofPropertyValuesHolder(child, + new PropertyListBuilder() + .translationX(toScaleAndTranslation[1]) + .translationY(toScaleAndTranslation[2]) + .build())); + return anim; + } + + public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + + int count = getChildCount(); + if (count == 0) { + return new PendingAnimation(new AnimatorSet()); + } + + tv.setVisibility(INVISIBLE); + int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); + TaskViewDrawable drawable = new TaskViewDrawable(tv, this); + getOverlay().add(drawable); + + final boolean[] passedOverviewThreshold = new boolean[] {false}; + ObjectAnimator drawableAnim = + ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0); + drawableAnim.setInterpolator(LINEAR); + drawableAnim.addUpdateListener((animator) -> { + // Once we pass a certain threshold, update the sysui flags to match the target tasks' + // flags + mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, + animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD + ? targetSysUiFlags + : 0); + + // Passing the threshold from taskview to fullscreen app will vibrate + final boolean passed = animator.getAnimatedFraction() >= MIN_PROGRESS_FOR_OVERVIEW; + if (passed != passedOverviewThreshold[0]) { + passedOverviewThreshold[0] = passed; + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } + }); + + AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, + drawable.getClipAnimationHelper()); + anim.play(drawableAnim); + anim.setDuration(duration); + + Consumer onTaskLaunchFinish = (result) -> { + onTaskLaunched(result); + tv.setVisibility(VISIBLE); + getOverlay().remove(drawable); + }; + + mPendingAnimation = new PendingAnimation(anim); + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + Consumer onLaunchResult = (result) -> { + onTaskLaunchFinish.accept(result); + if (!result) { + tv.notifyTaskLaunchFailed(TAG); + } + }; + tv.launchTask(false, onLaunchResult, getHandler()); + Task task = tv.getTask(); + if (task != null) { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( + onEndListener.logAction, Direction.DOWN, indexOfChild(tv), + TaskUtils.getLaunchComponentKeyForTask(task.key)); + } + } else { + onTaskLaunchFinish.accept(false); + } + mPendingAnimation = null; + }); + return mPendingAnimation; + } + + public abstract boolean shouldUseMultiWindowTaskSizeStrategy(); + + protected void onTaskLaunched(boolean success) { + resetTaskVisuals(); + } + + @Override + protected void notifyPageSwitchListener(int prevPage) { + super.notifyPageSwitchListener(prevPage); + loadVisibleTaskData(); + } + + @Override + protected String getCurrentPageDescription() { + return ""; + } + + @Override + public void addChildrenForAccessibility(ArrayList outChildren) { + // Add children in reverse order + for (int i = getChildCount() - 1; i >= 0; --i) { + outChildren.add(getChildAt(i)); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + final AccessibilityNodeInfo.CollectionInfo + collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( + 1, getTaskViewCount(), false, + AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); + info.setCollectionInfo(collectionInfo); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + final int taskViewCount = getTaskViewCount(); + event.setScrollable(taskViewCount > 0); + + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + final int[] visibleTasks = getVisibleChildrenRange(); + event.setFromIndex(taskViewCount - visibleTasks[1] - 1); + event.setToIndex(taskViewCount - visibleTasks[0] - 1); + event.setItemCount(taskViewCount); + } + } + + @Override + public CharSequence getAccessibilityClassName() { + // To hear position-in-list related feedback from Talkback. + return ListView.class.getName(); + } + + @Override + protected boolean isPageOrderFlipped() { + return true; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ShelfScrimView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ShelfScrimView.java new file mode 100644 index 0000000000..524724cc8a --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/ShelfScrimView.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.quickstep.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Path.Direction; +import android.graphics.Path.Op; +import android.util.AttributeSet; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ScrimView; + +import static android.support.v4.graphics.ColorUtils.setAlphaComponent; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.LINEAR; + +/** + * Scrim used for all-apps and shelf in Overview + * In transposed layout, it behaves as a simple color scrim. + * In portrait layout, it draws a rounded rect such that + * From normal state to overview state, the shelf just fades in and does not move + * From overview state to all-apps state the shelf moves up and fades in to cover the screen + */ +public class ShelfScrimView extends ScrimView { + + // If the progress is more than this, shelf follows the finger, otherwise it moves faster to + // cover the whole screen + private static final float SCRIM_CATCHUP_THRESHOLD = 0.2f; + + // In transposed layout, we simply draw a flat color. + private boolean mDrawingFlatColor; + + // For shelf mode + private final int mEndAlpha; + private final float mRadius; + private final int mMaxScrimAlpha; + private final Paint mPaint; + + // Mid point where the alpha changes + private int mMidAlpha; + private float mMidProgress; + + private float mShiftRange; + + private final float mShelfOffset; + private float mTopOffset; + private float mShelfTop; + private float mShelfTopAtThreshold; + + private int mShelfColor; + private int mRemainingScreenColor; + + private final Path mTempPath = new Path(); + private final Path mRemainingScreenPath = new Path(); + private boolean mRemainingScreenPathValid = false; + + public ShelfScrimView(Context context, AttributeSet attrs) { + super(context, attrs); + mMaxScrimAlpha = Math.round(OVERVIEW.getWorkspaceScrimAlpha(mLauncher) * 255); + + mEndAlpha = Color.alpha(mEndScrim); + mRadius = mLauncher.getResources().getDimension(R.dimen.shelf_surface_radius); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mShelfOffset = context.getResources().getDimension(R.dimen.shelf_surface_offset); + // Just assume the easiest UI for now, until we have the proper layout information. + mDrawingFlatColor = true; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mRemainingScreenPathValid = false; + } + + @Override + public void reInitUi() { + DeviceProfile dp = mLauncher.getDeviceProfile(); + mDrawingFlatColor = dp.isVerticalBarLayout(); + + if (!mDrawingFlatColor) { + mRemainingScreenPathValid = false; + mShiftRange = mLauncher.getAllAppsController().getShiftRange(); + + mMidProgress = OVERVIEW.getVerticalProgress(mLauncher); + mMidAlpha = mMidProgress >= 1 ? 0 + : Themes.getAttrInteger(getContext(), R.attr.allAppsInterimScrimAlpha); + + mTopOffset = dp.getInsets().top - mShelfOffset; + mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset; + updateColors(); + } + updateDragHandleAlpha(); + invalidate(); + } + + @Override + public void updateColors() { + super.updateColors(); + if (mDrawingFlatColor) { + mDragHandleOffset = 0; + return; + } + + mDragHandleOffset = mShelfOffset - mDragHandleSize; + if (mProgress >= SCRIM_CATCHUP_THRESHOLD) { + mShelfTop = mShiftRange * mProgress + mTopOffset; + } else { + mShelfTop = Utilities.mapRange(mProgress / SCRIM_CATCHUP_THRESHOLD, -mRadius, + mShelfTopAtThreshold); + } + + if (mProgress >= 1) { + mRemainingScreenColor = 0; + mShelfColor = 0; + } else if (mProgress >= mMidProgress) { + mRemainingScreenColor = 0; + + int alpha = Math.round(Utilities.mapToRange( + mProgress, mMidProgress, 1, mMidAlpha, 0, ACCEL)); + mShelfColor = setAlphaComponent(mEndScrim, alpha); + } else { + mDragHandleOffset += mShiftRange * (mMidProgress - mProgress); + + // Note that these ranges and interpolators are inverted because progress goes 1 to 0. + int alpha = Math.round( + Utilities.mapToRange(mProgress, (float) 0, mMidProgress, (float) mEndAlpha, + (float) mMidAlpha, Interpolators.clampToProgress(ACCEL, 0.5f, 1f))); + mShelfColor = setAlphaComponent(mEndScrim, alpha); + + int remainingScrimAlpha = Math.round( + Utilities.mapToRange(mProgress, (float) 0, mMidProgress, mMaxScrimAlpha, + (float) 0, LINEAR)); + mRemainingScreenColor = setAlphaComponent(mScrimColor, remainingScrimAlpha); + } + } + + @Override + protected void onDraw(Canvas canvas) { + drawBackground(canvas); + drawDragHandle(canvas); + } + + private void drawBackground(Canvas canvas) { + if (mDrawingFlatColor) { + if (mCurrentFlatColor != 0) { + canvas.drawColor(mCurrentFlatColor); + } + return; + } + + if (Color.alpha(mShelfColor) == 0) { + return; + } else if (mProgress <= 0) { + canvas.drawColor(mShelfColor); + return; + } + + int height = getHeight(); + int width = getWidth(); + // Draw the scrim over the remaining screen if needed. + if (mRemainingScreenColor != 0) { + if (!mRemainingScreenPathValid) { + mTempPath.reset(); + // Using a arbitrary '+10' in the bottom to avoid any left-overs at the + // corners due to rounding issues. + mTempPath.addRoundRect(0, height - mRadius, width, height + mRadius + 10, + mRadius, mRadius, Direction.CW); + mRemainingScreenPath.reset(); + mRemainingScreenPath.addRect(0, 0, width, height, Direction.CW); + mRemainingScreenPath.op(mTempPath, Op.DIFFERENCE); + } + + float offset = height - mRadius - mShelfTop; + canvas.translate(0, -offset); + mPaint.setColor(mRemainingScreenColor); + canvas.drawPath(mRemainingScreenPath, mPaint); + canvas.translate(0, offset); + } + + mPaint.setColor(mShelfColor); + canvas.drawRoundRect(0, mShelfTop, width, height + mRadius, mRadius, mRadius, mPaint); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskMenuView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskMenuView.java new file mode 100644 index 0000000000..2c8a86fae8 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskMenuView.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.R; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.TaskSystemShortcut; +import com.android.quickstep.TaskUtils; + +import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA; + +/** + * Contains options for a recent task when long-pressing its icon. + */ +public class TaskMenuView extends AbstractFloatingView { + + private static final Rect sTempRect = new Rect(); + + /** Note that these will be shown in order from top to bottom, if available for the task. */ + public static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[] { + new TaskSystemShortcut.AppInfo(), + new TaskSystemShortcut.SplitScreen(), + new TaskSystemShortcut.Pin(), + new TaskSystemShortcut.Install(), + }; + + private static final int REVEAL_OPEN_DURATION = 150; + private static final int REVEAL_CLOSE_DURATION = 100; + + private BaseDraggingActivity mActivity; + private TextView mTaskIconAndName; + private AnimatorSet mOpenCloseAnimator; + private TaskView mTaskView; + private LinearLayout mOptionLayout; + + public TaskMenuView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + mActivity = BaseDraggingActivity.fromContext(context); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTaskIconAndName = findViewById(R.id.task_icon_and_name); + mOptionLayout = findViewById(R.id.menu_option_layout); + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + BaseDragLayer dl = mActivity.getDragLayer(); + if (!dl.isEventOverView(this, ev)) { + // TODO: log this once we have a new container type for it? + close(true); + return true; + } + } + return false; + } + + @Override + protected void handleClose(boolean animate) { + if (animate) { + animateClose(); + } else { + closeComplete(); + } + } + + @Override + public void logActionCommand(int command) { + // TODO + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_TASK_MENU) != 0; + } + + public static boolean showForTask(TaskView taskView) { + BaseDraggingActivity activity = BaseDraggingActivity.fromContext(taskView.getContext()); + final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate( + R.layout.task_menu, activity.getDragLayer(), false); + return taskMenuView.populateAndShowForTask(taskView); + } + + private boolean populateAndShowForTask(TaskView taskView) { + if (isAttachedToWindow()) { + return false; + } + mActivity.getDragLayer().addView(this); + mTaskView = taskView; + addMenuOptions(mTaskView); + orientAroundTaskView(mTaskView); + post(this::animateOpen); + return true; + } + + private void addMenuOptions(TaskView taskView) { + Drawable icon = taskView.getTask().icon.getConstantState().newDrawable(); + int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size); + icon.setBounds(0, 0, iconSize, iconSize); + mTaskIconAndName.setCompoundDrawables(null, icon, null, null); + mTaskIconAndName.setText(TaskUtils.getTitle(getContext(), taskView.getTask())); + mTaskIconAndName.setOnClickListener(v -> close(true)); + + // Move the icon and text up half an icon size to lay over the TaskView + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mTaskIconAndName.getLayoutParams(); + params.topMargin = (int) -getResources().getDimension(R.dimen.task_thumbnail_top_margin); + mTaskIconAndName.setLayoutParams(params); + + for (TaskSystemShortcut menuOption : MENU_OPTIONS) { + View.OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, taskView); + if (onClickListener != null) { + addMenuOption(menuOption, onClickListener); + } + } + } + + private void addMenuOption(TaskSystemShortcut menuOption, View.OnClickListener onClickListener) { + ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate( + R.layout.task_view_menu_option, this, false); + menuOptionView.findViewById(R.id.icon).setBackgroundResource(menuOption.iconResId); + ((TextView) menuOptionView.findViewById(R.id.text)).setText(menuOption.labelResId); + menuOptionView.setOnClickListener(onClickListener); + mOptionLayout.addView(menuOptionView); + } + + private void orientAroundTaskView(TaskView taskView) { + measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect); + Rect insets = mActivity.getDragLayer().getInsets(); + BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); + params.width = sTempRect.width(); + params.gravity = Gravity.LEFT; + setLayoutParams(params); + setX(sTempRect.left - insets.left); + setY(sTempRect.top + getResources().getDimension(R.dimen.task_thumbnail_top_margin) + - insets.top); + } + + private void animateOpen() { + animateOpenOrClosed(false); + mIsOpen = true; + } + + private void animateClose() { + animateOpenOrClosed(true); + } + + private void animateOpenOrClosed(boolean closing) { + if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { + return; + } + mOpenCloseAnimator = LauncherAnimUtils.createAnimatorSet(); + + final Animator revealAnimator = createOpenCloseOutlineProvider() + .createRevealAnimator(this, closing); + revealAnimator.setInterpolator(Interpolators.DEACCEL); + mOpenCloseAnimator.play(revealAnimator); + mOpenCloseAnimator.play(ObjectAnimator.ofFloat(mTaskView.getThumbnail(), DIM_ALPHA, + closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA)); + mOpenCloseAnimator.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationStart(Animator animation) { + setVisibility(VISIBLE); + } + + @Override + public void onAnimationSuccess(Animator animator) { + if (closing) { + closeComplete(); + } + } + }); + mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); + mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION); + mOpenCloseAnimator.start(); + } + + private void closeComplete() { + mIsOpen = false; + mActivity.getDragLayer().removeView(this); + } + + private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { + float radius = getResources().getDimension(R.dimen.task_corner_radius); + Rect fromRect = new Rect(0, 0, getWidth(), 0); + Rect toRect = new Rect(0, 0, getWidth(), getHeight()); + return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskThumbnailView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskThumbnailView.java new file mode 100644 index 0000000000..0a0c27333f --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskThumbnailView.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LightingColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.Property; +import android.view.View; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.SystemUiController; +import com.android.launcher3.util.Themes; +import com.android.quickstep.TaskOverlayFactory; +import com.android.quickstep.TaskOverlayFactory.TaskOverlay; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; + +/** + * A task in the Recents view. + */ +public class TaskThumbnailView extends View { + + private static final LightingColorFilter[] sDimFilterCache = new LightingColorFilter[256]; + private static final LightingColorFilter[] sHighlightFilterCache = new LightingColorFilter[256]; + + public static final Property DIM_ALPHA_MULTIPLIER = + new FloatProperty("dimAlphaMultiplier") { + @Override + public void setValue(TaskThumbnailView thumbnail, float dimAlphaMultiplier) { + thumbnail.setDimAlphaMultipler(dimAlphaMultiplier); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mDimAlphaMultiplier; + } + }; + + public static final Property DIM_ALPHA = + new FloatProperty("dimAlpha") { + @Override + public void setValue(TaskThumbnailView thumbnail, float dimAlpha) { + thumbnail.setDimAlpha(dimAlpha); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mDimAlpha; + } + }; + + private final float mCornerRadius; + + private final BaseActivity mActivity; + private final TaskOverlay mOverlay; + private final boolean mIsDarkTextTheme; + private final Paint mPaint = new Paint(); + private final Paint mBackgroundPaint = new Paint(); + + private final Matrix mMatrix = new Matrix(); + + private float mClipBottom = -1; + + private Task mTask; + private ThumbnailData mThumbnailData; + protected BitmapShader mBitmapShader; + + private float mDimAlpha = 1f; + private float mDimAlphaMultiplier = 1f; + + public TaskThumbnailView(Context context) { + this(context, null); + } + + public TaskThumbnailView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mCornerRadius = getResources().getDimension(R.dimen.task_corner_radius); + mOverlay = TaskOverlayFactory.get(context).createOverlay(this); + mPaint.setFilterBitmap(true); + mBackgroundPaint.setColor(Color.WHITE); + mActivity = BaseActivity.fromContext(context); + mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText); + } + + public void bind() { + mOverlay.reset(); + } + + /** + * Updates this thumbnail. + */ + public void setThumbnail(Task task, ThumbnailData thumbnailData) { + mTask = task; + int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; + mPaint.setColor(color); + mBackgroundPaint.setColor(color); + + if (thumbnailData != null && thumbnailData.thumbnail != null) { + Bitmap bm = thumbnailData.thumbnail; + bm.prepareToDraw(); + mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + mPaint.setShader(mBitmapShader); + mThumbnailData = thumbnailData; + updateThumbnailMatrix(); + } else { + mBitmapShader = null; + mThumbnailData = null; + mPaint.setShader(null); + mOverlay.reset(); + } + updateThumbnailPaintFilter(); + } + + public void setDimAlphaMultipler(float dimAlphaMultipler) { + mDimAlphaMultiplier = dimAlphaMultipler; + setDimAlpha(mDimAlpha); + } + + /** + * Sets the alpha of the dim layer on top of this view. + * + * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black. + */ + public void setDimAlpha(float dimAlpha) { + mDimAlpha = dimAlpha; + updateThumbnailPaintFilter(); + } + + public float getDimAlpha() { + return mDimAlpha; + } + + public Rect getInsets() { + if (mThumbnailData != null) { + return mThumbnailData.insets; + } + return new Rect(); + } + + public int getSysUiStatusNavFlags() { + if (mThumbnailData != null) { + int flags = 0; + flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0 + ? SystemUiController.FLAG_LIGHT_STATUS + : SystemUiController.FLAG_DARK_STATUS; + flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0 + ? SystemUiController.FLAG_LIGHT_NAV + : SystemUiController.FLAG_DARK_NAV; + return flags; + } + return 0; + } + + @Override + protected void onDraw(Canvas canvas) { + drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius); + } + + public float getCornerRadius() { + return mCornerRadius; + } + + public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, + float cornerRadius) { + // Draw the background in all cases, except when the thumbnail data is opaque + final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null + || mThumbnailData == null; + if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) { + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint); + if (drawBackgroundOnly) { + return; + } + } + + if (mClipBottom > 0) { + canvas.save(); + canvas.clipRect(x, y, width, mClipBottom); + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); + canvas.restore(); + } else { + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); + } + } + + private void updateThumbnailPaintFilter() { + int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255); + if (mBitmapShader != null) { + LightingColorFilter filter = getDimmingColorFilter(mul, mIsDarkTextTheme); + mPaint.setColorFilter(filter); + mBackgroundPaint.setColorFilter(filter); + } else { + mPaint.setColorFilter(null); + mPaint.setColor(Color.argb(255, mul, mul, mul)); + } + invalidate(); + } + + private void updateThumbnailMatrix() { + boolean rotate = false; + mClipBottom = -1; + if (mBitmapShader != null && mThumbnailData != null) { + float scale = mThumbnailData.scale; + Rect thumbnailInsets = mThumbnailData.insets; + final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() - + (thumbnailInsets.left + thumbnailInsets.right) * scale; + final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() - + (thumbnailInsets.top + thumbnailInsets.bottom) * scale; + + final float thumbnailScale; + final DeviceProfile profile = mActivity.getDeviceProfile(); + + if (getMeasuredWidth() == 0) { + // If we haven't measured , skip the thumbnail drawing and only draw the background + // color + thumbnailScale = 0f; + } else { + final Configuration configuration = + getContext().getResources().getConfiguration(); + // Rotate the screenshot if not in multi-window mode + rotate = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION && + configuration.orientation != mThumbnailData.orientation && + !mActivity.isInMultiWindowModeCompat() && + mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN; + // Scale the screenshot to always fit the width of the card. + thumbnailScale = rotate + ? getMeasuredWidth() / thumbnailHeight + : getMeasuredWidth() / thumbnailWidth; + } + + if (rotate) { + int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1; + mMatrix.setRotate(90 * rotationDir); + int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top; + int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right; + mMatrix.postTranslate(-newLeftInset * scale, -newTopInset * scale); + if (rotationDir == -1) { + // Crop the right/bottom side of the screenshot rather than left/top + float excessHeight = thumbnailWidth * thumbnailScale - getMeasuredHeight(); + mMatrix.postTranslate(0, -excessHeight); + } + // Move the screenshot to the thumbnail window (rotation moved it out). + if (rotationDir == 1) { + mMatrix.postTranslate(mThumbnailData.thumbnail.getHeight(), 0); + } else { + mMatrix.postTranslate(0, mThumbnailData.thumbnail.getWidth()); + } + } else { + mMatrix.setTranslate(-mThumbnailData.insets.left * scale, + -mThumbnailData.insets.top * scale); + } + mMatrix.postScale(thumbnailScale, thumbnailScale); + mBitmapShader.setLocalMatrix(mMatrix); + + float bitmapHeight = Math.max((rotate ? thumbnailWidth : thumbnailHeight) + * thumbnailScale, 0); + if (Math.round(bitmapHeight) < getMeasuredHeight()) { + mClipBottom = bitmapHeight; + } + mPaint.setShader(mBitmapShader); + } + + if (rotate) { + // The overlay doesn't really work when the screenshot is rotated, so don't add it. + mOverlay.reset(); + } else { + mOverlay.setTaskInfo(mTask, mThumbnailData, mMatrix); + } + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateThumbnailMatrix(); + } + + private static LightingColorFilter getDimmingColorFilter(int intensity, boolean shouldLighten) { + intensity = Utilities.boundToRange(intensity, 0, 255); + if (intensity == 255) { + return null; + } + if (shouldLighten) { + if (sHighlightFilterCache[intensity] == null) { + int colorAdd = 255 - intensity; + sHighlightFilterCache[intensity] = new LightingColorFilter( + Color.argb(255, intensity, intensity, intensity), + Color.argb(255, colorAdd, colorAdd, colorAdd)); + } + return sHighlightFilterCache[intensity]; + } else { + if (sDimFilterCache[intensity] == null) { + sDimFilterCache[intensity] = new LightingColorFilter( + Color.argb(255, intensity, intensity, intensity), 0); + } + return sDimFilterCache[intensity]; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskView.java b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskView.java new file mode 100644 index 0000000000..67cdef4076 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/quickstep/views/TaskView.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.quickstep.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Outline; +import android.os.Bundle; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.Log; +import android.util.Property; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.Toast; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.R; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.quickstep.TaskSystemShortcut; +import com.android.quickstep.TaskUtils; +import com.android.quickstep.views.RecentsView.PageCallbacks; +import com.android.quickstep.views.RecentsView.ScrollState; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.Task.TaskCallbacks; +import com.android.systemui.shared.recents.model.ThumbnailData; +import com.android.systemui.shared.system.ActivityManagerWrapper; + +import java.util.function.Consumer; + +import static android.widget.Toast.LENGTH_SHORT; +import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA_MULTIPLIER; + +/** + * A task in the Recents view. + */ +public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks { + + private static final String TAG = TaskView.class.getSimpleName(); + + /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */ + private static final TimeInterpolator CURVE_INTERPOLATOR + = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f; + + /** + * The alpha of a black scrim on a page in the carousel as it leaves the screen. + * In the resting position of the carousel, the adjacent pages have about half this scrim. + */ + public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f; + + /** + * How much to scale down pages near the edge of the screen. + */ + private static final float EDGE_SCALE_DOWN_FACTOR = 0.03f; + + public static final long SCALE_ICON_DURATION = 120; + private static final long DIM_ANIM_DURATION = 700; + + public static final Property ZOOM_SCALE = + new FloatProperty("zoomScale") { + @Override + public void setValue(TaskView taskView, float v) { + taskView.setZoomScale(v); + } + + @Override + public Float get(TaskView taskView) { + return taskView.mZoomScale; + } + }; + + private Task mTask; + private TaskThumbnailView mSnapshotView; + private IconView mIconView; + private float mCurveScale; + private float mZoomScale; + private Animator mDimAlphaAnim; + + public TaskView(Context context) { + this(context, null); + } + + public TaskView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setOnClickListener((view) -> { + if (getTask() == null) { + return; + } + launchTask(true /* animate */); + BaseActivity.fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss( + Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this), + TaskUtils.getLaunchComponentKeyForTask(getTask().key)); + }); + setOutlineProvider(new TaskOutlineProvider(getResources())); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSnapshotView = findViewById(R.id.snapshot); + mIconView = findViewById(R.id.icon); + } + + /** + * Updates this task view to the given {@param task}. + */ + public void bind(Task task) { + if (mTask != null) { + mTask.removeCallback(this); + } + mTask = task; + mSnapshotView.bind(); + task.addCallback(this); + setContentDescription(task.titleDescription); + } + + public Task getTask() { + return mTask; + } + + public TaskThumbnailView getThumbnail() { + return mSnapshotView; + } + + public IconView getIconView() { + return mIconView; + } + + public void launchTask(boolean animate) { + launchTask(animate, (result) -> { + if (!result) { + notifyTaskLaunchFailed(TAG); + } + }, getHandler()); + } + + public void launchTask(boolean animate, Consumer resultCallback, + Handler resultCallbackHandler) { + if (mTask != null) { + final ActivityOptions opts; + if (animate) { + opts = BaseDraggingActivity.fromContext(getContext()) + .getActivityLaunchOptions(this); + } else { + opts = ActivityOptions.makeCustomAnimation(getContext(), 0, 0); + } + ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key, + opts, resultCallback, resultCallbackHandler); + } + } + + @Override + public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) { + mSnapshotView.setThumbnail(task, thumbnailData); + mIconView.setDrawable(task.icon); + mIconView.setOnClickListener(icon -> TaskMenuView.showForTask(this)); + mIconView.setOnLongClickListener(icon -> { + requestDisallowInterceptTouchEvent(true); + return TaskMenuView.showForTask(this); + }); + } + + @Override + public void onTaskDataUnloaded() { + mSnapshotView.setThumbnail(null, null); + mIconView.setDrawable(null); + mIconView.setOnLongClickListener(null); + } + + @Override + public void onTaskWindowingModeChanged() { + // Do nothing + } + + public void animateIconToScaleAndDim(float scale) { + mIconView.animate().scaleX(scale).scaleY(scale).setDuration(SCALE_ICON_DURATION).start(); + mDimAlphaAnim = ObjectAnimator.ofFloat(mSnapshotView, DIM_ALPHA_MULTIPLIER, 1 - scale, + scale); + mDimAlphaAnim.setDuration(DIM_ANIM_DURATION); + mDimAlphaAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDimAlphaAnim = null; + } + }); + mDimAlphaAnim.start(); + } + + protected void setIconScaleAndDim(float iconScale) { + mIconView.animate().cancel(); + mIconView.setScaleX(iconScale); + mIconView.setScaleY(iconScale); + if (mDimAlphaAnim != null) { + mDimAlphaAnim.cancel(); + } + mSnapshotView.setDimAlphaMultipler(iconScale); + } + + public void resetVisualProperties() { + setZoomScale(1); + setTranslationX(0f); + setTranslationY(0f); + setTranslationZ(0); + setAlpha(1f); + setIconScaleAndDim(1); + } + + @Override + public void onPageScroll(ScrollState scrollState) { + float curveInterpolation = + CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation); + + mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA); + setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + setPivotX((right - left) * 0.5f); + setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f); + } + + public static float getCurveScaleForInterpolation(float linearInterpolation) { + float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation); + return getCurveScaleForCurveInterpolation(curveInterpolation); + } + + private static float getCurveScaleForCurveInterpolation(float curveInterpolation) { + return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR; + } + + private void setCurveScale(float curveScale) { + mCurveScale = curveScale; + onScaleChanged(); + } + + public float getCurveScale() { + return mCurveScale; + } + + public void setZoomScale(float adjacentScale) { + mZoomScale = adjacentScale; + onScaleChanged(); + } + + private void onScaleChanged() { + float scale = mCurveScale * mZoomScale; + setScaleX(scale); + setScaleY(scale); + } + + @Override + public boolean hasOverlappingRendering() { + // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. + return false; + } + + private static final class TaskOutlineProvider extends ViewOutlineProvider { + + private final int mMarginTop; + private final float mRadius; + + TaskOutlineProvider(Resources res) { + mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); + mRadius = res.getDimension(R.dimen.task_corner_radius); + } + + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, mMarginTop, view.getWidth(), + view.getHeight(), mRadius); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + info.addAction( + new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task, + getContext().getText(R.string.accessibility_close_task))); + + final Context context = getContext(); + final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(context); + for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { + OnClickListener onClickListener = menuOption.getOnClickListener(activity, this); + if (onClickListener != null) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(menuOption.labelResId, + context.getText(menuOption.labelResId))); + } + } + + final RecentsView recentsView = getRecentsView(); + final AccessibilityNodeInfo.CollectionItemInfo itemInfo = + AccessibilityNodeInfo.CollectionItemInfo.obtain( + 0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1, + false); + info.setCollectionItemInfo(itemInfo); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (action == R.string.accessibility_close_task) { + getRecentsView().dismissTask(this, true /*animateTaskView*/, + true /*removeTask*/); + return true; + } + + for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { + if (action == menuOption.labelResId) { + OnClickListener onClickListener = menuOption.getOnClickListener( + BaseDraggingActivity.fromContext(getContext()), this); + if (onClickListener != null) { + onClickListener.onClick(this); + } + return true; + } + } + + return super.performAccessibilityAction(action, arguments); + } + + private RecentsView getRecentsView() { + return (RecentsView) getParent(); + } + + public void notifyTaskLaunchFailed(String tag) { + String msg = "Failed to launch task"; + if (mTask != null) { + msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")"; + } + Log.w(tag, msg); + Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/AllAppsState.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/AllAppsState.java new file mode 100644 index 0000000000..3e49925cb5 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/AllAppsState.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; + +import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS; +import static com.android.launcher3.anim.Interpolators.DEACCEL_2; + +/** + * Definition for AllApps state + */ +public class AllAppsState extends LauncherState { + + private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY; + + private static final PageAlphaProvider PAGE_ALPHA_PROVIDER = new PageAlphaProvider(DEACCEL_2) { + @Override + public float getPageAlpha(int pageIndex) { + return 0; + } + }; + + public AllAppsState(int id) { + super(id, ContainerType.ALLAPPS, ALL_APPS_TRANSITION_MS, STATE_FLAGS); + } + + @Override + public void onStateEnabled(Launcher launcher) { + AbstractFloatingView.closeAllOpenViews(launcher); + dispatchWindowStateChanged(launcher); + } + + @Override + public String getDescription(Launcher launcher) { + AllAppsContainerView appsView = launcher.getAppsView(); + return appsView.getDescription(); + } + + @Override + public float getVerticalProgress(Launcher launcher) { + return 0f; + } + + @Override + public float[] getWorkspaceScaleAndTranslation(Launcher launcher) { + float[] scaleAndTranslation = LauncherState.OVERVIEW.getWorkspaceScaleAndTranslation( + launcher); + scaleAndTranslation[0] = 1; + return scaleAndTranslation; + } + + @Override + public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { + return PAGE_ALPHA_PROVIDER; + } + + @Override + public int getVisibleElements(Launcher launcher) { + return ALL_APPS_HEADER | ALL_APPS_HEADER_EXTRA | ALL_APPS_CONTENT; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + return new float[] {0.9f, -0.2f}; + } + + @Override + public LauncherState getHistoryForState(LauncherState previousState) { + return previousState == OVERVIEW ? OVERVIEW : NORMAL; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/BackButtonAlphaHandler.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/BackButtonAlphaHandler.java new file mode 100644 index 0000000000..f2917f7707 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/BackButtonAlphaHandler.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.uioverrides; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.quickstep.OverviewInteractionState; + +public class BackButtonAlphaHandler implements LauncherStateManager.StateHandler { + + private static final String TAG = "BackButtonAlphaHandler"; + + private final Launcher mLauncher; + private final OverviewInteractionState mOverviewInteractionState; + + public BackButtonAlphaHandler(Launcher launcher) { + mLauncher = launcher; + mOverviewInteractionState = OverviewInteractionState.getInstance(mLauncher); + } + + @Override + public void setState(LauncherState state) { + UiFactory.onLauncherStateOrFocusChanged(mLauncher); + } + + @Override + public void setStateWithAnimation(LauncherState toState, + AnimatorSetBuilder builder, LauncherStateManager.AnimationConfig config) { + if (!config.playNonAtomicComponent()) { + return; + } + float fromAlpha = mOverviewInteractionState.getBackButtonAlpha(); + float toAlpha = toState.hideBackButton ? 0 : 1; + if (Float.compare(fromAlpha, toAlpha) != 0) { + ValueAnimator anim = ValueAnimator.ofFloat(fromAlpha, toAlpha); + anim.setDuration(config.duration); + anim.addUpdateListener(valueAnimator -> { + final float alpha = (float) valueAnimator.getAnimatedValue(); + mOverviewInteractionState.setBackButtonAlpha(alpha, false); + }); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Reapply the final alpha in case some state (e.g. window focus) changed. + UiFactory.onLauncherStateOrFocusChanged(mLauncher); + } + }); + builder.play(anim); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/DisplayRotationListener.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/DisplayRotationListener.java new file mode 100644 index 0000000000..c51fb8f8b2 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/DisplayRotationListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.content.Context; +import android.os.Handler; + +import com.android.systemui.shared.system.RotationWatcher; + +/** + * Utility class for listening for rotation changes + */ +public class DisplayRotationListener extends RotationWatcher { + + private final Runnable mCallback; + private Handler mHandler; + + public DisplayRotationListener(Context context, Runnable callback) { + super(context); + mCallback = callback; + } + + @Override + public void enable() { + if (mHandler == null) { + mHandler = new Handler(); + } + super.enable(); + } + + @Override + protected void onRotationChanged(int i) { + mHandler.post(mCallback); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/FastOverviewState.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/FastOverviewState.java new file mode 100644 index 0000000000..1da29d2dd3 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/FastOverviewState.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.quickstep.QuickScrubController; +import com.android.quickstep.views.RecentsView; + +/** + * Extension of overview state used for QuickScrub + */ +public class FastOverviewState extends OverviewState { + + private static final float MAX_PREVIEW_SCALE_UP = 1.3f; + /** + * Vertical transition of the task previews relative to the full container. + */ + public static final float OVERVIEW_TRANSLATION_FACTOR = 0.4f; + + private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_DISABLE_INTERACTION + | FLAG_OVERVIEW_UI | FLAG_HIDE_BACK_BUTTON | FLAG_DISABLE_ACCESSIBILITY; + + public FastOverviewState(int id) { + super(id, QuickScrubController.QUICK_SCRUB_FROM_HOME_START_DURATION, STATE_FLAGS); + } + + @Override + public void onStateTransitionEnd(Launcher launcher) { + super.onStateTransitionEnd(launcher); + RecentsView recentsView = launcher.getOverviewPanel(); + recentsView.getQuickScrubController().onFinishedTransitionToQuickScrub(); + } + + @Override + public int getVisibleElements(Launcher launcher) { + return NONE; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + RecentsView recentsView = launcher.getOverviewPanel(); + recentsView.getTaskSize(sTempRect); + + return new float[] {getOverviewScale(launcher.getDeviceProfile(), sTempRect, launcher), + OVERVIEW_TRANSLATION_FACTOR}; + } + + public static float getOverviewScale(DeviceProfile dp, Rect taskRect, Context context) { + if (dp.isVerticalBarLayout()) { + return 1f; + } + + Resources res = context.getResources(); + float usedHeight = taskRect.height() + res.getDimension(R.dimen.task_thumbnail_top_margin); + float usedWidth = taskRect.width() + 2 * (res.getDimension(R.dimen.recents_page_spacing) + + res.getDimension(R.dimen.quickscrub_adjacent_visible_width)); + return Math.min(Math.min(dp.availableHeightPx / usedHeight, + dp.availableWidthPx / usedWidth), MAX_PREVIEW_SCALE_UP); + } + + @Override + public void onStateDisabled(Launcher launcher) { + super.onStateDisabled(launcher); + launcher.getOverviewPanel().getQuickScrubController().cancelActiveQuickscrub(); + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/LandscapeEdgeSwipeController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/LandscapeEdgeSwipeController.java new file mode 100644 index 0000000000..1d00e4ec17 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/LandscapeEdgeSwipeController.java @@ -0,0 +1,79 @@ +package foundation.e.blisslauncher.uioverrides; + +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationComponents; +import com.android.launcher3.touch.AbstractStateChangeTouchController; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.quickstep.RecentsModel; + +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR; + +/** + * Touch controller for handling edge swipes in landscape/seascape UI + */ +public class LandscapeEdgeSwipeController extends AbstractStateChangeTouchController { + + private static final String TAG = "LandscapeEdgeSwipeCtrl"; + + public LandscapeEdgeSwipeController(Launcher l) { + super(l, SwipeDetector.HORIZONTAL); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + return mLauncher.isInState(NORMAL) && (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + boolean draggingFromNav = mLauncher.getDeviceProfile().isSeascape() != isDragTowardPositive; + return draggingFromNav ? OVERVIEW : NORMAL; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return LauncherLogProto.ContainerType.NAVBAR; + } + + @Override + protected float getShiftRange() { + return mLauncher.getDragLayer().getWidth(); + } + + @Override + protected float initCurrentAnimation(@AnimationComponents int animComponent) { + float range = getShiftRange(); + long maxAccuracy = (long) (2 * range); + mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState, + maxAccuracy, animComponent); + return (mLauncher.getDeviceProfile().isSeascape() ? 2 : -2) / range; + } + + @Override + protected int getDirectionForLog() { + return mLauncher.getDeviceProfile().isSeascape() ? Direction.RIGHT : Direction.LEFT; + } + + @Override + protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { + super.onSwipeInteractionCompleted(targetState, logAction); + if (mStartState == NORMAL && targetState == OVERVIEW) { + RecentsModel.getInstance(mLauncher).onOverviewShown(true, TAG); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewState.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewState.java new file mode 100644 index 0000000000..026c58f4f5 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewState.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.view.View; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.R; +import com.android.launcher3.Workspace; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.views.RecentsView; + +import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; +import static com.android.launcher3.anim.Interpolators.DEACCEL_2; +import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE; + +/** + * Definition for overview state + */ +public class OverviewState extends LauncherState { + + private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED + | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY; + + public OverviewState(int id) { + this(id, OVERVIEW_TRANSITION_MS, STATE_FLAGS); + } + + protected OverviewState(int id, int transitionDuration, int stateFlags) { + super(id, ContainerType.TASKSWITCHER, transitionDuration, stateFlags); + } + + @Override + public float[] getWorkspaceScaleAndTranslation(Launcher launcher) { + RecentsView recentsView = launcher.getOverviewPanel(); + Workspace workspace = launcher.getWorkspace(); + View workspacePage = workspace.getPageAt(workspace.getCurrentPage()); + float workspacePageWidth = workspacePage != null && workspacePage.getWidth() != 0 + ? workspacePage.getWidth() : launcher.getDeviceProfile().availableWidthPx; + recentsView.getTaskSize(sTempRect); + float scale = (float) sTempRect.width() / workspacePageWidth; + float parallaxFactor = 0.5f; + return new float[]{scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor}; + } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + return new float[] {1f, 0f}; + } + + @Override + public void onStateEnabled(Launcher launcher) { + RecentsView rv = launcher.getOverviewPanel(); + rv.setOverviewStateEnabled(true); + AbstractFloatingView.closeAllOpenViews(launcher); + } + + @Override + public void onStateDisabled(Launcher launcher) { + RecentsView rv = launcher.getOverviewPanel(); + rv.setOverviewStateEnabled(false); + RecentsModel.getInstance(launcher).resetAssistCache(); + } + + @Override + public void onStateTransitionEnd(Launcher launcher) { + launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE); + DiscoveryBounce.showForOverviewIfNeeded(launcher); + } + + public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { + return new PageAlphaProvider(DEACCEL_2) { + @Override + public float getPageAlpha(int pageIndex) { + return 0; + } + }; + } + + @Override + public int getVisibleElements(Launcher launcher) { + if (launcher.getDeviceProfile().isVerticalBarLayout()) { + return VERTICAL_SWIPE_INDICATOR; + } else { + return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | + (launcher.getAppsView().getFloatingHeaderView().hasVisibleContent() + ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS); + } + } + + @Override + public float getWorkspaceScrimAlpha(Launcher launcher) { + return 0.5f; + } + + @Override + public float getVerticalProgress(Launcher launcher) { + if ((getVisibleElements(launcher) & ALL_APPS_HEADER_EXTRA) == 0) { + // We have no all apps content, so we're still at the fully down progress. + return super.getVerticalProgress(launcher); + } + return 1 - (getDefaultSwipeHeight(launcher) + / launcher.getAllAppsController().getShiftRange()); + } + + @Override + public String getDescription(Launcher launcher) { + return launcher.getString(R.string.accessibility_desc_recent_apps); + } + + public static float getDefaultSwipeHeight(Launcher launcher) { + DeviceProfile dp = launcher.getDeviceProfile(); + return dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewToAllAppsTouchController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewToAllAppsTouchController.java new file mode 100644 index 0000000000..b988ab86de --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/OverviewToAllAppsTouchController.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.quickstep.TouchInteractionService; +import com.android.quickstep.views.RecentsView; + +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; + +/** + * Touch controller from going from OVERVIEW to ALL_APPS. + * + * This is used in landscape mode. It is also used in portrait mode for the fallback recents. + */ +public class OverviewToAllAppsTouchController extends PortraitStatesTouchController { + + public OverviewToAllAppsTouchController(Launcher l) { + super(l); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + if (mLauncher.isInState(ALL_APPS)) { + // In all-apps only listen if the container cannot scroll itself + return mLauncher.getAppsView().shouldContainerScroll(ev); + } else if (mLauncher.isInState(NORMAL)) { + return true; + } else if (mLauncher.isInState(OVERVIEW)) { + RecentsView rv = mLauncher.getOverviewPanel(); + return ev.getY() > (rv.getBottom() - rv.getPaddingBottom()); + } else { + return false; + } + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + if (fromState == ALL_APPS && !isDragTowardPositive) { + // Should swipe down go to OVERVIEW instead? + return TouchInteractionService.isConnected() ? + mLauncher.getStateManager().getLastState() : NORMAL; + } else if (isDragTowardPositive) { + return ALL_APPS; + } + return fromState; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return LauncherLogProto.ContainerType.WORKSPACE; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/PortraitStatesTouchController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/PortraitStatesTouchController.java new file mode 100644 index 0000000000..acbde49fda --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/PortraitStatesTouchController.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.view.MotionEvent; +import android.view.animation.Interpolator; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationComponents; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.touch.AbstractStateChangeTouchController; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.TouchInteractionService; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.launcher3.anim.Interpolators.LINEAR; + +/** + * Touch controller for handling various state transitions in portrait UI. + */ +public class PortraitStatesTouchController extends AbstractStateChangeTouchController { + + private static final String TAG = "PortraitStatesTouchCtrl"; + + /** + * The progress at which all apps content will be fully visible when swiping up from overview. + */ + private static final float ALL_APPS_CONTENT_FADE_THRESHOLD = 0.08f; + + /** + * The progress at which recents will begin fading out when swiping up from overview. + */ + private static final float RECENTS_FADE_THRESHOLD = 0.88f; + + private InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper(); + + // If true, we will finish the current animation instantly on second touch. + private boolean mFinishFastOnSecondTouch; + + + public PortraitStatesTouchController(Launcher l) { + super(l, SwipeDetector.VERTICAL); + } + + @Override + protected boolean canInterceptTouch(MotionEvent ev) { + if (mCurrentAnimation != null) { + if (mFinishFastOnSecondTouch) { + // TODO: Animate to finish instead. + mCurrentAnimation.getAnimationPlayer().end(); + } + + AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); + if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()) { + // If we are already animating from a previous state, we can intercept as long as + // the touch is below the current all apps progress (to allow for double swipe). + return true; + } + // Otherwise, make sure everything is settled and don't intercept so they can scroll + // recents, dismiss a task, etc. + if (mAtomicAnim != null) { + mAtomicAnim.end(); + } + return false; + } + if (mLauncher.isInState(ALL_APPS)) { + // In all-apps only listen if the container cannot scroll itself + if (!mLauncher.getAppsView().shouldContainerScroll(ev)) { + return false; + } + } else { + // For all other states, only listen if the event originated below the hotseat height + DeviceProfile dp = mLauncher.getDeviceProfile(); + int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom; + if (ev.getY() < (mLauncher.getDragLayer().getHeight() - hotseatHeight)) { + return false; + } + } + if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { + return false; + } + return true; + } + + @Override + protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { + if (fromState == ALL_APPS && !isDragTowardPositive) { + // Should swipe down go to OVERVIEW instead? + return TouchInteractionService.isConnected() ? + mLauncher.getStateManager().getLastState() : NORMAL; + } else if (fromState == OVERVIEW) { + return isDragTowardPositive ? ALL_APPS : NORMAL; + } else if (fromState == NORMAL && isDragTowardPositive) { + return TouchInteractionService.isConnected() ? OVERVIEW : ALL_APPS; + } + return fromState; + } + + @Override + protected int getLogContainerTypeForNormalState() { + return ContainerType.HOTSEAT; + } + + private AnimatorSetBuilder getNormalToOverviewAnimation() { + mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR; + + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper); + return builder; + } + + public static AnimatorSetBuilder getOverviewToAllAppsAnimation() { + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL, + 0, ALL_APPS_CONTENT_FADE_THRESHOLD)); + builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(DEACCEL, + RECENTS_FADE_THRESHOLD, 1)); + return builder; + } + + private AnimatorSetBuilder getAllAppsToOverviewAnimation() { + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL, + 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1)); + builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(ACCEL, + 0f, 1 - RECENTS_FADE_THRESHOLD)); + return builder; + } + + @Override + protected AnimatorSetBuilder getAnimatorSetBuilderForStates(LauncherState fromState, + LauncherState toState) { + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + if (fromState == NORMAL && toState == OVERVIEW) { + builder = getNormalToOverviewAnimation(); + } else if (fromState == OVERVIEW && toState == ALL_APPS) { + builder = getOverviewToAllAppsAnimation(); + } else if (fromState == ALL_APPS && toState == OVERVIEW) { + builder = getAllAppsToOverviewAnimation(); + } + return builder; + } + + @Override + protected float initCurrentAnimation(@AnimationComponents int animComponents) { + float range = getShiftRange(); + long maxAccuracy = (long) (2 * range); + + float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range; + float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range; + + float totalShift = endVerticalShift - startVerticalShift; + + final AnimatorSetBuilder builder = totalShift == 0 ? new AnimatorSetBuilder() + : getAnimatorSetBuilderForStates(mFromState, mToState); + + cancelPendingAnim(); + + RecentsView recentsView = mLauncher.getOverviewPanel(); + TaskView taskView = recentsView.getTaskViewAt(recentsView.getNextPage()); + if (recentsView.shouldSwipeDownLaunchApp() && mFromState == OVERVIEW && mToState == NORMAL + && taskView != null) { + // Reset the state manager, when changing the interaction mode + mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */); + mPendingAnimation = recentsView.createTaskLauncherAnimation(taskView, maxAccuracy); + mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN); + + Runnable onCancelRunnable = () -> { + cancelPendingAnim(); + clearState(); + }; + mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation.anim, maxAccuracy, + onCancelRunnable); + mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation); + } else { + mCurrentAnimation = mLauncher.getStateManager() + .createAnimationToNewWorkspace(mToState, builder, maxAccuracy, this::clearState, + animComponents); + } + + if (totalShift == 0) { + totalShift = Math.signum(mFromState.ordinal - mToState.ordinal) + * OverviewState.getDefaultSwipeHeight(mLauncher); + } + return 1 / totalShift; + } + + private void cancelPendingAnim() { + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + } + + @Override + protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, + LauncherState targetState, float velocity, boolean isFling) { + super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, + velocity, isFling); + handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling); + } + + private void handleFirstSwipeToOverview(final ValueAnimator animator, + final long expectedDuration, final LauncherState targetState, final float velocity, + final boolean isFling) { + if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) { + mFinishFastOnSecondTouch = true; + if (isFling && expectedDuration != 0) { + // Update all apps interpolator to add a bit of overshoot starting from currFraction + final float currFraction = mCurrentAnimation.getProgressFraction(); + mAllAppsInterpolatorWrapper.baseInterpolator = Interpolators.clampToProgress( + Interpolators.overshootInterpolatorForVelocity(velocity), currFraction, 1); + animator.setDuration(Math.min(expectedDuration, ATOMIC_DURATION)) + .setInterpolator(LINEAR); + } + } else { + mFinishFastOnSecondTouch = false; + } + } + + @Override + protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { + super.onSwipeInteractionCompleted(targetState, logAction); + if (mStartState == NORMAL && targetState == OVERVIEW) { + RecentsModel.getInstance(mLauncher).onOverviewShown(true, TAG); + } + } + + private static class InterpolatorWrapper implements Interpolator { + + public TimeInterpolator baseInterpolator = LINEAR; + + @Override + public float getInterpolation(float v) { + return baseInterpolator.getInterpolation(v); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/RecentsViewStateController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/RecentsViewStateController.java new file mode 100644 index 0000000000..807cbcb54e --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/RecentsViewStateController.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.animation.ValueAnimator; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.animation.Interpolator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationConfig; +import com.android.launcher3.LauncherStateManager.StateHandler; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.anim.PropertySetter; +import com.android.quickstep.views.LauncherRecentsView; + +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.FAST_OVERVIEW; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE; +import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; +import static com.android.quickstep.views.LauncherRecentsView.TRANSLATION_Y_FACTOR; +import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; + +@TargetApi(Build.VERSION_CODES.O) +public class RecentsViewStateController implements StateHandler { + + private final Launcher mLauncher; + private final LauncherRecentsView mRecentsView; + + public RecentsViewStateController(Launcher launcher) { + mLauncher = launcher; + mRecentsView = launcher.getOverviewPanel(); + } + + @Override + public void setState(LauncherState state) { + mRecentsView.setContentAlpha(state.overviewUi ? 1 : 0); + float[] scaleTranslationYFactor = state.getOverviewScaleAndTranslationYFactor(mLauncher); + SCALE_PROPERTY.set(mRecentsView, scaleTranslationYFactor[0]); + mRecentsView.setTranslationYFactor(scaleTranslationYFactor[1]); + if (state.overviewUi) { + mRecentsView.updateEmptyMessage(); + mRecentsView.resetTaskVisuals(); + } + } + + @Override + public void setStateWithAnimation(final LauncherState toState, + AnimatorSetBuilder builder, AnimationConfig config) { + if (!config.playAtomicComponent()) { + // The entire recents animation is played atomically. + return; + } + PropertySetter setter = config.getPropertySetter(builder); + float[] scaleTranslationYFactor = toState.getOverviewScaleAndTranslationYFactor(mLauncher); + Interpolator scaleAndTransYInterpolator = builder.getInterpolator( + ANIM_OVERVIEW_SCALE, LINEAR); + if (mLauncher.getStateManager().getState() == OVERVIEW && toState == FAST_OVERVIEW) { + scaleAndTransYInterpolator = Interpolators.clampToProgress( + QUICK_SCRUB_START_INTERPOLATOR, 0, QUICK_SCRUB_TRANSLATION_Y_FACTOR); + } + setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleTranslationYFactor[0], + scaleAndTransYInterpolator); + setter.setFloat(mRecentsView, TRANSLATION_Y_FACTOR, scaleTranslationYFactor[1], + scaleAndTransYInterpolator); + setter.setFloat(mRecentsView, CONTENT_ALPHA, toState.overviewUi ? 1 : 0, + builder.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT)); + + if (!toState.overviewUi) { + builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals); + } + + if (toState.overviewUi) { + ValueAnimator updateAnim = ValueAnimator.ofFloat(0, 1); + updateAnim.addUpdateListener(valueAnimator -> { + // While animating into recents, update the visible task data as needed + mRecentsView.loadVisibleTaskData(); + }); + updateAnim.setDuration(config.duration); + builder.play(updateAnim); + mRecentsView.updateEmptyMessage(); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/StatusBarTouchController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/StatusBarTouchController.java new file mode 100644 index 0000000000..2730234f10 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/StatusBarTouchController.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.uioverrides; + +import android.content.SharedPreferences; +import android.os.RemoteException; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.Utilities; +import com.android.launcher3.touch.TouchEventTranslator; +import com.android.launcher3.util.TouchController; +import com.android.quickstep.RecentsModel; +import com.android.systemui.shared.recents.ISystemUiProxy; + +public class StatusBarTouchController implements TouchController { + private static final String TAG = StatusBarTouchController.class.getSimpleName(); + private static final String PREF_STATUSBAR_EXPAND = "pref_expand_statusbar"; + + private boolean mCanIntercept; + private ISystemUiProxy mSysUiProxy; + + private final Launcher mLauncher; + private final SharedPreferences mSharedPreferences; + private final float mTouchSlop; + + protected final TouchEventTranslator mTranslator = + new TouchEventTranslator(this::dispatchTouchEvent); + + public StatusBarTouchController(Launcher launcher) { + mLauncher = launcher; + mSharedPreferences = Utilities.getPrefs(launcher); + mTouchSlop = ViewConfiguration.get(launcher).getScaledTouchSlop() * 2; + } + + private void dispatchTouchEvent(MotionEvent ev) { + try { + if (mSysUiProxy != null) { + mSysUiProxy.onStatusBarMotionEvent(ev); + } + } catch (RemoteException e) { + Log.e(TAG, "Remote exception on sysUiProxy.", e); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + int action = ev.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + mCanIntercept = canInterceptTouch(ev); + if (!mCanIntercept) { + return false; + } + mTranslator.reset(); + mTranslator.setDownParameters(0, ev); + } else if (action == MotionEvent.ACTION_POINTER_DOWN) { + mTranslator.setDownParameters(ev.getActionIndex(), ev); + } + + if (mCanIntercept && action == MotionEvent.ACTION_MOVE) { + float dy = ev.getY() - mTranslator.getDownY(); + float dx = ev.getX() - mTranslator.getDownX(); + if (dy > mTouchSlop && dy > Math.abs(dx)) { + mTranslator.dispatchDownEvents(ev); + mTranslator.processMotionEvent(ev); + return true; + } else if (Math.abs(dx) > mTouchSlop) { + mCanIntercept = false; + } + } + return false; + } + + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + mTranslator.processMotionEvent(ev); + return true; + } + + private boolean canInterceptTouch(MotionEvent ev) { + if (!mSharedPreferences.getBoolean(PREF_STATUSBAR_EXPAND, true)) { + return false; + } + + if (mLauncher.isInState(LauncherState.NORMAL)) { + if (AbstractFloatingView.getTopOpenViewWithType( + mLauncher, AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) == null) { + if (ev.getY() > mLauncher.getDragLayer().getHeight() - + mLauncher.getDeviceProfile().getInsets().bottom) { + return false; + } + mSysUiProxy = RecentsModel.getInstance(mLauncher).getSystemUiProxy(); + return mSysUiProxy != null; + } + } + return false; + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/TaskViewTouchController.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/TaskViewTouchController.java new file mode 100644 index 0000000000..8444ed2b15 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/TaskViewTouchController.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.uioverrides; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.MotionEvent; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.FlingBlockCheck; +import com.android.launcher3.util.PendingAnimation; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; + +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; + +/** + * Touch controller for handling task view card swipes + */ +public abstract class TaskViewTouchController + extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener { + + private static final String TAG = "OverviewSwipeController"; + + // Progress after which the transition is assumed to be a success in case user does not fling + private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; + + protected final T mActivity; + private final SwipeDetector mDetector; + private final RecentsView mRecentsView; + private final int[] mTempCords = new int[2]; + + private PendingAnimation mPendingAnimation; + private AnimatorPlaybackController mCurrentAnimation; + private boolean mCurrentAnimationIsGoingUp; + + private boolean mNoIntercept; + + private float mDisplacementShift; + private float mProgressMultiplier; + private float mEndDisplacement; + private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); + + private TaskView mTaskBeingDragged; + + public TaskViewTouchController(T activity) { + mActivity = activity; + mRecentsView = activity.getOverviewPanel(); + mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL); + } + + private boolean canInterceptTouch() { + if (mCurrentAnimation != null) { + // If we are already animating from a previous state, we can intercept. + return true; + } + if (AbstractFloatingView.getTopOpenView(mActivity) != null) { + return false; + } + return isRecentsInteractive(); + } + + protected abstract boolean isRecentsInteractive(); + + protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + } + + @Override + public void onAnimationCancel(Animator animation) { + if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) { + clearState(); + } + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mNoIntercept = !canInterceptTouch(); + if (mNoIntercept) { + return false; + } + + // Now figure out which direction scroll events the controller will start + // calling the callbacks. + int directionsToDetectScroll = 0; + boolean ignoreSlopWhenSettling = false; + if (mCurrentAnimation != null) { + directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; + ignoreSlopWhenSettling = true; + } else { + mTaskBeingDragged = null; + + for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) { + TaskView view = mRecentsView.getTaskViewAt(i); + if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer() + .isEventOverView(view, ev)) { + mTaskBeingDragged = view; + if (!OverviewInteractionState.getInstance(mActivity) + .isSwipeUpGestureEnabled()) { + // Don't allow swipe down to open if we don't support swipe up + // to enter overview. + directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; + } else { + // The task can be dragged up to dismiss it, + // and down to open if it's the current page. + directionsToDetectScroll = i == mRecentsView.getCurrentPage() + ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE; + } + break; + } + } + if (mTaskBeingDragged == null) { + mNoIntercept = true; + return false; + } + } + + mDetector.setDetectableScrollConditions( + directionsToDetectScroll, ignoreSlopWhenSettling); + } + + if (mNoIntercept) { + return false; + } + + onControllerTouchEvent(ev); + return mDetector.isDraggingOrSettling(); + } + + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + return mDetector.onTouchEvent(ev); + } + + private void reInitAnimationController(boolean goingUp) { + if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) { + // No need to init + return; + } + int scrollDirections = mDetector.getScrollDirections(); + if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0) + || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) { + // Trying to re-init in an unsupported direction. + return; + } + if (mCurrentAnimation != null) { + mCurrentAnimation.setPlayFraction(0); + } + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + + mCurrentAnimationIsGoingUp = goingUp; + BaseDragLayer dl = mActivity.getDragLayer(); + long maxDuration = (long) (2 * dl.getHeight()); + + if (goingUp) { + mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged, + true /* animateTaskView */, true /* removeTask */, maxDuration); + + mEndDisplacement = -mTaskBeingDragged.getHeight(); + } else { + mPendingAnimation = mRecentsView.createTaskLauncherAnimation( + mTaskBeingDragged, maxDuration); + mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN); + + mTempCords[1] = mTaskBeingDragged.getHeight(); + dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords); + mEndDisplacement = dl.getHeight() - mTempCords[1]; + } + + if (mCurrentAnimation != null) { + mCurrentAnimation.setOnCancelRunnable(null); + } + mCurrentAnimation = AnimatorPlaybackController + .wrap(mPendingAnimation.anim, maxDuration, this::clearState); + onUserControlledAnimationCreated(mCurrentAnimation); + mCurrentAnimation.getTarget().addListener(this); + mCurrentAnimation.dispatchOnStart(); + mProgressMultiplier = 1 / mEndDisplacement; + } + + @Override + public void onDragStart(boolean start) { + if (mCurrentAnimation == null) { + reInitAnimationController(mDetector.wasInitialTouchPositive()); + mDisplacementShift = 0; + } else { + mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier; + mCurrentAnimation.pause(); + } + mFlingBlockCheck.unblockFling(); + } + + @Override + public boolean onDrag(float displacement, float velocity) { + float totalDisplacement = displacement + mDisplacementShift; + boolean isGoingUp = + totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0; + if (isGoingUp != mCurrentAnimationIsGoingUp) { + reInitAnimationController(isGoingUp); + mFlingBlockCheck.blockFling(); + } else { + mFlingBlockCheck.onEvent(); + } + mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier); + return true; + } + + @Override + public void onDragEnd(float velocity, boolean fling) { + final boolean goingToEnd; + final int logAction; + boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); + if (blockedFling) { + fling = false; + } + float progress = mCurrentAnimation.getProgressFraction(); + float interpolatedProgress = mCurrentAnimation.getInterpolator().getInterpolation(progress); + if (fling) { + logAction = Touch.FLING; + boolean goingUp = velocity < 0; + goingToEnd = goingUp == mCurrentAnimationIsGoingUp; + } else { + logAction = Touch.SWIPE; + goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS; + } + long animationDuration = SwipeDetector.calculateDuration( + velocity, goingToEnd ? (1 - progress) : progress); + if (blockedFling && !goingToEnd) { + animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); + } + + float nextFrameProgress = Utilities.boundToRange( + progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f); + + mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction)); + + ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); + anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f); + anim.setDuration(animationDuration); + anim.setInterpolator(scrollInterpolatorForVelocity(velocity)); + anim.start(); + } + + private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) { + if (mPendingAnimation != null) { + mPendingAnimation.finish(wasSuccess, logAction); + mPendingAnimation = null; + } + clearState(); + } + + private void clearState() { + mDetector.finishedScrolling(); + mDetector.setDetectableScrollConditions(0, false); + mTaskBeingDragged = null; + mCurrentAnimation = null; + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/UiFactory.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/UiFactory.java new file mode 100644 index 0000000000..dc93d2548e --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/UiFactory.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.uioverrides; + +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.content.Context; +import android.os.CancellationSignal; +import android.util.Base64; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppTransitionManagerImpl; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.LauncherStateManager.StateHandler; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.util.TouchController; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.util.RemoteFadeOutAnimationListener; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.ActivityCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.zip.Deflater; + +import static android.view.View.VISIBLE; +import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN; +import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN; + +public class UiFactory { + + public static TouchController[] createTouchControllers(Launcher launcher) { + boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher) + .isSwipeUpGestureEnabled(); + if (!swipeUpEnabled) { + return new TouchController[] { + launcher.getDragController(), + new OverviewToAllAppsTouchController(launcher), + new LauncherTaskViewController(launcher), + new StatusBarTouchController(launcher)}; + } + if (launcher.getDeviceProfile().isVerticalBarLayout()) { + return new TouchController[] { + launcher.getDragController(), + new OverviewToAllAppsTouchController(launcher), + new LandscapeEdgeSwipeController(launcher), + new LauncherTaskViewController(launcher), + new StatusBarTouchController(launcher)}; + } else { + return new TouchController[] { + launcher.getDragController(), + new PortraitStatesTouchController(launcher), + new LauncherTaskViewController(launcher), + new StatusBarTouchController(launcher)}; + } + } + + public static void setOnTouchControllersChangedListener(Context context, Runnable listener) { + OverviewInteractionState.getInstance(context).setOnSwipeUpSettingChangedListener(listener); + } + + public static StateHandler[] getStateHandler(Launcher launcher) { + return new StateHandler[] {launcher.getAllAppsController(), launcher.getWorkspace(), + new RecentsViewStateController(launcher), new BackButtonAlphaHandler(launcher)}; + } + + /** + * Sets the back button visibility based on the current state/window focus. + */ + public static void onLauncherStateOrFocusChanged(Launcher launcher) { + boolean shouldBackButtonBeHidden = launcher != null + && launcher.getStateManager().getState().hideBackButton + && launcher.hasWindowFocus(); + if (shouldBackButtonBeHidden) { + // Show the back button if there is a floating view visible. + shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(launcher, + TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null; + } + OverviewInteractionState.getInstance(launcher) + .setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */); + } + + public static void resetOverview(Launcher launcher) { + RecentsView recents = launcher.getOverviewPanel(); + recents.reset(); + } + + public static void onCreate(Launcher launcher) { + if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) { + launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { + @Override + public void onStateSetImmediately(LauncherState state) { + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher) + .isSwipeUpGestureEnabled(); + LauncherState prevState = launcher.getStateManager().getLastState(); + + if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled + && finalState == ALL_APPS && prevState == NORMAL))) { + launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply(); + launcher.getStateManager().removeStateListener(this); + } + } + }); + } + + if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) { + launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { + @Override + public void onStateSetImmediately(LauncherState state) { + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + LauncherState prevState = launcher.getStateManager().getLastState(); + + if (finalState == ALL_APPS && prevState == OVERVIEW) { + launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply(); + launcher.getStateManager().removeStateListener(this); + } + } + }); + } + } + + public static void onStart(Context context) { + RecentsModel model = RecentsModel.getInstance(context); + if (model != null) { + model.onStart(); + } + } + + public static void onEnterAnimationComplete(Context context) { + // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled + // as a part of quickstep/scrub, so that high-res thumbnails can load the next time we + // enter overview + RecentsModel.getInstance(context).getRecentsTaskLoader() + .getHighResThumbnailLoader().setVisible(true); + } + + public static void onLauncherStateOrResumeChanged(Launcher launcher) { + LauncherState state = launcher.getStateManager().getState(); + DeviceProfile profile = launcher.getDeviceProfile(); + WindowManagerWrapper.getInstance().setShelfHeight( + (state == NORMAL || state == OVERVIEW) && launcher.isUserActive() + && !profile.isVerticalBarLayout(), + profile.hotseatBarSizePx); + + if (state == NORMAL) { + launcher.getOverviewPanel().setSwipeDownShouldLaunchApp(false); + } + } + + public static void onTrimMemory(Context context, int level) { + RecentsModel model = RecentsModel.getInstance(context); + if (model != null) { + model.onTrimMemory(level); + } + } + + public static void useFadeOutAnimationForLauncherStart(Launcher launcher, + CancellationSignal cancellationSignal) { + LauncherAppTransitionManagerImpl appTransitionManager = + (LauncherAppTransitionManagerImpl) launcher.getAppTransitionManager(); + appTransitionManager.setRemoteAnimationProvider((targets) -> { + + // On the first call clear the reference. + cancellationSignal.cancel(); + + ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0); + fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(targets)); + AnimatorSet anim = new AnimatorSet(); + anim.play(fadeAnimation); + return anim; + }, cancellationSignal); + } + + public static boolean dumpActivity(Activity activity, PrintWriter writer) { + if (!Utilities.IS_DEBUG_DEVICE) { + return false; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + if (!(new ActivityCompat(activity).encodeViewHierarchy(out))) { + return false; + } + + Deflater deflater = new Deflater(); + deflater.setInput(out.toByteArray()); + deflater.finish(); + + out.reset(); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); // returns the generated code... index + out.write(buffer, 0, count); + } + + writer.println("--encoded-view-dump-v0--"); + writer.println(Base64.encodeToString( + out.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING)); + return true; + } + + public static void prepareToShowOverview(Launcher launcher) { + RecentsView overview = launcher.getOverviewPanel(); + if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) { + SCALE_PROPERTY.set(overview, 1.33f); + } + } + + private static class LauncherTaskViewController extends TaskViewTouchController { + + public LauncherTaskViewController(Launcher activity) { + super(activity); + } + + @Override + protected boolean isRecentsInteractive() { + return mActivity.isInState(OVERVIEW); + } + + @Override + protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + mActivity.getStateManager().setCurrentUserControlledAnimation(animController); + } + } +} diff --git a/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/WallpaperColorInfo.java b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/WallpaperColorInfo.java new file mode 100644 index 0000000000..76f35f5253 --- /dev/null +++ b/quickstep/src/main/java/foundation/e/blisslauncher/uioverrides/WallpaperColorInfo.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.uioverrides; + +import android.annotation.TargetApi; +import android.app.WallpaperColors; +import android.app.WallpaperManager; +import android.app.WallpaperManager.OnColorsChangedListener; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import com.android.systemui.shared.system.TonalCompat; +import com.android.systemui.shared.system.TonalCompat.ExtractionInfo; + +import java.util.ArrayList; + +import static android.app.WallpaperManager.FLAG_SYSTEM; + +@TargetApi(Build.VERSION_CODES.P) +public class WallpaperColorInfo implements OnColorsChangedListener { + + private static final Object sInstanceLock = new Object(); + private static WallpaperColorInfo sInstance; + + public static WallpaperColorInfo getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + sInstance = new WallpaperColorInfo(context.getApplicationContext()); + } + return sInstance; + } + } + + private final ArrayList mListeners = new ArrayList<>(); + private final WallpaperManager mWallpaperManager; + private final TonalCompat mTonalCompat; + + private ExtractionInfo mExtractionInfo; + + private OnChangeListener[] mTempListeners = new OnChangeListener[0]; + + private WallpaperColorInfo(Context context) { + mWallpaperManager = context.getSystemService(WallpaperManager.class); + mTonalCompat = new TonalCompat(context); + + mWallpaperManager.addOnColorsChangedListener(this, new Handler(Looper.getMainLooper())); + update(mWallpaperManager.getWallpaperColors(FLAG_SYSTEM)); + } + + public int getMainColor() { + return mExtractionInfo.mainColor; + } + + public int getSecondaryColor() { + return mExtractionInfo.secondaryColor; + } + + public boolean isDark() { + return mExtractionInfo.supportsDarkTheme; + } + + public boolean supportsDarkText() { + return mExtractionInfo.supportsDarkText; + } + + @Override + public void onColorsChanged(WallpaperColors colors, int which) { + if ((which & FLAG_SYSTEM) != 0) { + update(colors); + notifyChange(); + } + } + + private void update(WallpaperColors wallpaperColors) { + mExtractionInfo = mTonalCompat.extractDarkColors(wallpaperColors); + } + + public void addOnChangeListener(OnChangeListener listener) { + mListeners.add(listener); + } + + public void removeOnChangeListener(OnChangeListener listener) { + mListeners.remove(listener); + } + + private void notifyChange() { + // Create a new array to avoid concurrent modification when the activity destroys itself. + mTempListeners = mListeners.toArray(mTempListeners); + for (OnChangeListener listener : mTempListeners) { + if (listener != null) { + listener.onExtractedColorsChanged(this); + } + } + } + + public interface OnChangeListener { + void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo); + } +} diff --git a/quickstep/src/main/res/values/strings.xml b/quickstep/src/main/res/values/strings.xml new file mode 100644 index 0000000000..32728d8764 --- /dev/null +++ b/quickstep/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Quickstep + diff --git a/quickstep/src/test/java/foundation/e/blisslauncher/quickstep/ExampleUnitTest.java b/quickstep/src/test/java/foundation/e/blisslauncher/quickstep/ExampleUnitTest.java new file mode 100644 index 0000000000..f4ecc5fa5f --- /dev/null +++ b/quickstep/src/test/java/foundation/e/blisslauncher/quickstep/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package foundation.e.blisslauncher.quickstep; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100755 index e70334ca2e..0000000000 --- a/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -include ':app' -include ':data' -include ':domain' \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100755 index 0000000000..4baf46bf45 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +include(":app", ":common", ":blisslauncherv2", ":data-bridge", ":data", ":domain") \ No newline at end of file -- GitLab From b525f2ae761d8a219e8f7222312fd5c9df4ba94a Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 2 Apr 2020 23:47:30 +0530 Subject: [PATCH 03/23] Add generic reactive repository to manage entities and Rename project level dictionaries to blisslauncher.xml --- .../{amit.xml => blisslauncher.xml} | 2 + .../domain/repository/LauncherRepository.kt | 15 ++-- .../domain/repository/Repository.kt | 76 +++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) rename .idea/dictionaries/{amit.xml => blisslauncher.xml} (86%) create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt diff --git a/.idea/dictionaries/amit.xml b/.idea/dictionaries/blisslauncher.xml similarity index 86% rename from .idea/dictionaries/amit.xml rename to .idea/dictionaries/blisslauncher.xml index 09877e869f..6f5d5759a9 100644 --- a/.idea/dictionaries/amit.xml +++ b/.idea/dictionaries/blisslauncher.xml @@ -1,10 +1,12 @@ + amit badging flowable interactor interactors + kumar unsuspend diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt index 193dbb15dc..a3b170b90a 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt @@ -5,15 +5,15 @@ import foundation.e.blisslauncher.domain.entity.ApplicationItem import foundation.e.blisslauncher.domain.entity.LauncherItem /** - * Loads and update all the LauncherItems + * Repository to manage [LauncherItem] */ -interface LauncherRepository { +interface LauncherRepository: Repository - fun getAllActivities(user: UserHandle, quietMode: Boolean): List +/*fun getAllActivities(user: UserHandle, quietMode: Boolean): List - /** - * Functions to fetch/add/update/remove AppsRepository - */ + *//** + * Functions to fetch/add/update/remove AppsRepository + *//* fun add(packageName: String, user: UserHandle, quietMode: Boolean): List fun remove(packageName: String, user: UserHandle) @@ -32,5 +32,4 @@ interface LauncherRepository { fun makePackagesUnavailable(packages: Array, user: UserHandle): List - fun removePackages(packages: Array, user: UserHandle): List -} \ No newline at end of file + fun removePackages(packages: Array, user: UserHandle): List*/ \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt new file mode 100644 index 0000000000..783cccd882 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt @@ -0,0 +1,76 @@ +package foundation.e.blisslauncher.domain.repository + +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Single + +/** + * Generic Reactive Repository interface which captures the domain type to manage with generic CRUD operations. + * + * @param the domain type which the repository manages. + * @param the type of the id of the entity which the repository manages. + * + * @author Amit Kumar + */ +interface Repository { + + /** + * Saves a given entity and return the instance for further operations. + * + * @param entity to be saved. + * @return [Single] emitting the saved entity. + */ + fun save(entity: S): Single + + /** + * Saves all given entities. + * + * @param entities to be saved. + * @return [Flowable] emitting the saved entities. + */ + fun saveAll(entities: Iterable): Flowable + + /** + * Retrieves an entity by its id. + * + * @param id of the entity. + * @return [Maybe] emitting the entity with the given id or {@link Maybe#empty()} if none found. + */ + fun findById(id: ID): Maybe + + /** + * Return all entities of this type. + * + * @return [Flowable] emitting all entities. + */ + fun findAll(): Flowable + + /** + * Deletes a given entity. + * + * @return [Completable] signaling when operation has completed. + */ + fun delete(entity: T) + + /** + * Deletes the entity with the given id. + * + * @return [Completable] signaling when operation has completed. + */ + fun deleteById(id: ID): Completable + + /** + * Deletes all entities managed by this repository. + * + * @return [Completable] signaling when operation has completed. + */ + fun deleteAll() + + /** + * Deletes the given entities. + * + * @return [Completable] signaling when operation has completed. + */ + fun deleteAll(entities: Iterable) +} \ No newline at end of file -- GitLab From b7fefaadcbd4054540a1f4a72bcf46fecf16a108 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 3 Apr 2020 16:20:29 +0530 Subject: [PATCH 04/23] More changes in presentation layer and data layer. Add PackageManagerHelper and support of nested state changes --- .gitignore | 1 + .idea/codeStyles/Project.xml | 6 -- .idea/dictionaries/blisslauncher.xml | 2 +- .../core/blur/ShaderBlurDrawable.kt | 1 - .../features/launcher/LauncherActivity.java | 3 +- blisslauncherv2/build.gradle | 14 ++- .../blisslauncher/ExampleInstrumentedTest.kt | 24 ----- .../base/presentation/BaseIntent.kt | 11 +- .../base/presentation/BaseView.kt | 6 +- .../base/presentation/BaseViewModel.kt | 12 ++- .../e/blisslauncher/common/Functions.kt | 2 +- .../common/util/SystemUiController.kt | 1 - .../blisslauncher/common/util/TraceHelper.kt | 4 +- .../features/launcher/LauncherActivity.kt | 21 +++- .../features/launcher/LauncherState.kt | 22 ++-- .../features/launcher/LauncherView.kt | 2 +- .../features/launcher/LauncherViewEvent.kt | 4 +- .../features/launcher/LauncherViewModel.kt | 42 ++++---- .../inject/ActivityBindsModule.kt | 2 - .../e/blisslauncher/ExampleUnitTest.kt | 17 --- .../base/presentation/BaseViewModelTest.kt | 55 ++++++++++ common.gradle | 4 + .../amitkma/common/ExampleInstrumentedTest.kt | 24 ----- .../common/executors/AppExecutors.kt | 1 - .../common/util/MultiHashMap.java | 52 +++++++++ .../com/amitkma/common/ExampleUnitTest.kt | 17 --- .../databridge/ExampleInstrumentedTest.kt | 24 ----- .../databridge/ExampleUnitTest.kt | 17 --- .../data/LauncherDatabaseGateway.kt | 5 + .../data/LauncherRepositoryImpl.kt | 64 +++++++++++ .../data/PackageManagerHelper.kt | 102 ++++++++++++++++++ .../data/database/IconDatabase.kt | 2 +- .../e/blisslauncher/data/icon/IconCache.kt | 6 +- .../blisslauncher/data/inject/CompatModule.kt | 2 +- .../data/inject/DataComponent.kt | 2 +- .../data/inject/DataRepoBindingModule.kt | 2 +- .../LauncherAppsChangedCallbackCompat.kt | 2 +- .../data/receiver/SessionCommitReceiver.kt | 1 - .../data/widgets/WidgetsRepositoryImpl.kt | 4 +- .../domain/entity/ApplicationItem.kt | 7 +- .../domain/entity/WorkspaceItem.kt | 4 +- .../domain/interactor/AddPackages.kt | 2 +- .../domain/interactor/DeleteComponents.kt | 2 +- .../domain/interactor/LoadLauncher.kt | 10 +- .../interactor/ObserveAddedLauncherItems.kt | 1 - .../interactor/ObserveRemovedLauncherItems.kt | 1 - .../domain/interactor/RemovePackages.kt | 2 +- .../domain/interactor/UpdateLauncher.kt | 27 +++-- .../domain/interactor/UpdatePackages.kt | 6 +- .../domain/repository/Repository.kt | 4 +- 50 files changed, 413 insertions(+), 236 deletions(-) delete mode 100644 blisslauncherv2/src/androidTest/java/foundation/e/blisslauncher/ExampleInstrumentedTest.kt delete mode 100644 blisslauncherv2/src/test/java/foundation/e/blisslauncher/ExampleUnitTest.kt create mode 100644 blisslauncherv2/src/test/java/foundation/e/blisslauncher/base/presentation/BaseViewModelTest.kt delete mode 100644 common/src/androidTest/java/com/amitkma/common/ExampleInstrumentedTest.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java delete mode 100644 common/src/test/java/com/amitkma/common/ExampleUnitTest.kt delete mode 100644 data-bridge/src/androidTest/java/foundation/e/blisslauncher/databridge/ExampleInstrumentedTest.kt delete mode 100644 data-bridge/src/test/java/foundation/e/blisslauncher/databridge/ExampleUnitTest.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/LauncherRepositoryImpl.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt rename data/src/main/java/foundation/e/blisslauncher/data/{ => launcher}/LauncherAppsChangedCallbackCompat.kt (98%) diff --git a/.gitignore b/.gitignore index 5c038db904..1575777d10 100755 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ build/ .idea/tasks.xml .idea/modules.xml .idea/assetWizardSettings.xml +.idea/markdown* gradle.xml .classpath diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 2e191f6759..60d3be05fc 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,8 +1,5 @@ - - - - diff --git a/.idea/dictionaries/blisslauncher.xml b/.idea/dictionaries/blisslauncher.xml index 6f5d5759a9..39d51360c2 100644 --- a/.idea/dictionaries/blisslauncher.xml +++ b/.idea/dictionaries/blisslauncher.xml @@ -1,5 +1,5 @@ - + amit badging diff --git a/app/src/main/java/foundation/e/blisslauncher/core/blur/ShaderBlurDrawable.kt b/app/src/main/java/foundation/e/blisslauncher/core/blur/ShaderBlurDrawable.kt index 2578758dfd..5ba6050252 100644 --- a/app/src/main/java/foundation/e/blisslauncher/core/blur/ShaderBlurDrawable.kt +++ b/app/src/main/java/foundation/e/blisslauncher/core/blur/ShaderBlurDrawable.kt @@ -51,7 +51,6 @@ class ShaderBlurDrawable internal constructor(private val blurWallpaperProvider: } blurBitmap = if (blurBitmap!!.height > (blurBounds.bottom.toInt() - blurBounds.top.toInt())) { - Bitmap.createBitmap( blurBitmap!!, blurBounds.left.toInt(), blurBounds.top.toInt(), diff --git a/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java b/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java index cfe9cd778a..850584b81a 100755 --- a/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java @@ -475,7 +475,8 @@ public class LauncherActivity extends AppCompatActivity implements ManagedProfileBroadcastReceiver.unregister(this, managedProfileReceiver); LocalBroadcastManager.getInstance(this).unregisterReceiver(mWeatherReceiver); getCompositeDisposable().dispose(); - events.unsubscribe(); + if (events != null) + events.unsubscribe(); BlissLauncher.getApplication(this).getAppProvider().clear(); } diff --git a/blisslauncherv2/build.gradle b/blisslauncherv2/build.gradle index bfda6f31c6..9971f993ae 100644 --- a/blisslauncherv2/build.gradle +++ b/blisslauncherv2/build.gradle @@ -61,7 +61,17 @@ dependencies { implementation Libs.timber - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' + testImplementation Libs.junit + testImplementation Libs.mockK androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + androidTestImplementation 'androidx.test:core:1.0.0' + + // AndroidJUnitRunner and JUnit Rules + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test:rules:1.1.0' + + // Assertions + androidTestImplementation 'androidx.test.ext:junit:1.0.0' + androidTestImplementation 'androidx.test.ext:truth:1.0.0' } diff --git a/blisslauncherv2/src/androidTest/java/foundation/e/blisslauncher/ExampleInstrumentedTest.kt b/blisslauncherv2/src/androidTest/java/foundation/e/blisslauncher/ExampleInstrumentedTest.kt deleted file mode 100644 index 89eecd711b..0000000000 --- a/blisslauncherv2/src/androidTest/java/foundation/e/blisslauncher/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package foundation.e.blisslauncher - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("foundation.e.blisslauncher", appContext.packageName) - } -} diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt index 9794914a68..9d5fdfbd58 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt @@ -1,14 +1,21 @@ package foundation.e.blisslauncher.base.presentation +/** + * Intent that are used to change state. + * It can either reduce to a new state or another intent which resolves the new state. + */ interface BaseIntent { fun reduce(oldState: T): T } +typealias StateReducer = T.() -> T +typealias UnitReducer = T.() -> Unit + /** * * NOTE: Magic of extension functions, (T)->T and T.()->T interchangeable. */ -fun intent(block: T.() -> T): BaseIntent = object : +fun intent(block: StateReducer): BaseIntent = object : BaseIntent { override fun reduce(oldState: T): T = block(oldState) } @@ -20,7 +27,7 @@ fun intent(block: T.() -> T): BaseIntent = object : * * Use the `sideEffect {}` DSL function for those situations. */ -fun sideEffect(block: T.() -> Unit): BaseIntent = object : +fun sideEffect(block: UnitReducer): BaseIntent = object : BaseIntent { override fun reduce(oldState: T): T = oldState.apply(block) } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt index b41591b82e..1b0473c9f6 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt @@ -1,6 +1,10 @@ package foundation.e.blisslauncher.base.presentation -interface BaseView { +import io.reactivex.Observable + +interface BaseView { + + fun intents(): Observable> fun render(state: State) } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt index 42ca7956ce..9260f96459 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt @@ -5,10 +5,12 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.subjects.PublishSubject import timber.log.Timber -abstract class BaseViewModel(initialState: S) : +abstract class BaseViewModel(initialState: S) : Model { - // State reducers + /** + * Used to process events and state reducers + */ private val intents = PublishSubject.create>() private val store = intents @@ -21,5 +23,11 @@ abstract class BaseViewModel(initialState: S) : override fun process(intent: BaseIntent) = intents.onNext(intent) + fun newState(onStateUpdate: S.() -> S) { + process(intent(onStateUpdate)) + } + override fun states(): Observable = store + + abstract fun toIntent(event: E): BaseIntent } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt index 0b182d953c..a5601c75e3 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt @@ -5,4 +5,4 @@ import io.reactivex.Observable import io.reactivex.disposables.Disposable fun Observable.subscribeToState(onNext: (state: S) -> Unit): Disposable = - this.subscribe(onNext) \ No newline at end of file + this.subscribe(onNext) diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt index 8a79d48e6d..2693475ad0 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt @@ -59,6 +59,5 @@ class SystemUiController @Inject constructor(private val window: Window) { const val FLAG_DARK_NAV = 1 shl 1 const val FLAG_LIGHT_STATUS = 1 shl 2 const val FLAG_DARK_STATUS = 1 shl 3 - } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt index a692f60eaf..c811c3c374 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt @@ -35,7 +35,7 @@ class TraceHelper { } val now = SystemClock.uptimeMillis() - Log.d(sectionName, "${partition} : ${now - time}") + Log.d(sectionName, "$partition : ${now - time}") time = now } } @@ -52,7 +52,7 @@ class TraceHelper { } Log.d( sectionName, - "${msg} : ${(SystemClock.uptimeMillis() - time)}" + "$msg : ${(SystemClock.uptimeMillis() - time)}" ) } } diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt index b4dc36ce15..6fc8b690ba 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt @@ -8,13 +8,16 @@ import android.os.StrictMode.VmPolicy import android.view.View import dagger.android.AndroidInjection import foundation.e.blisslauncher.base.BaseDraggingActivity +import foundation.e.blisslauncher.base.presentation.BaseIntent import foundation.e.blisslauncher.common.subscribeToState import foundation.e.blisslauncher.common.util.TraceHelper import foundation.e.blisslauncher.domain.entity.LauncherItem import foundation.e.blisslauncher.domain.interactor.LoadLauncher import foundation.e.blisslauncher.domain.keys.PackageUserKey +import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign +import io.reactivex.subjects.BehaviorSubject import javax.inject.Inject class LauncherActivity : BaseDraggingActivity(), LauncherView { @@ -25,6 +28,8 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { private val compositeDisposable: CompositeDisposable = CompositeDisposable() + private val loadLauncherIntentPublisher = BehaviorSubject.create() + @Inject lateinit var launcherViewModel: LauncherViewModel @@ -56,9 +61,20 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { oldConfig = Configuration(resources.configuration) - compositeDisposable += (launcherViewModel.states().subscribeToState { render(it) }) + compositeDisposable += launcherViewModel.states().subscribeToState { render(it) } //TODO set model and state here - launcherViewModel.loadLauncher() + + compositeDisposable += intents().subscribe { launcherViewModel::process } + } + + override fun intents(): Observable> { + return Observable.merge(initialIntent(), + loadLauncherIntentPublisher.map { launcherViewModel.toIntent(it) } + ) + } + + private fun initialIntent(): Observable> { + return loadLauncherIntentPublisher.map { launcherViewModel.toIntent(it) } } override fun getRootView(): View { @@ -111,6 +127,5 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { } override fun render(state: LauncherState) { - } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt index 6e8e016134..664fa9a77d 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt @@ -234,14 +234,13 @@ data class LauncherState @Inject constructor( } LauncherConstants.ItemType.APPLICATION, LauncherConstants.ItemType.SHORTCUT -> { - if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP - || item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT + if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP || + item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT ) { mutableAllItems.add(item) } else { if (newItem) { if (!folders.containsKey(item.container)) { - } } else { findOrMakeFolder(item.container).add(item as WorkspaceItem, false) @@ -291,7 +290,7 @@ data class LauncherState @Inject constructor( return null } - fun add(item: ApplicationItem, info: LauncherActivityInfo): LauncherState { + /*fun add(item: ApplicationItem, info: LauncherActivityInfo): LauncherState { if (findApplicationItem(item.componentName, item.user) != null) { return } @@ -301,7 +300,7 @@ data class LauncherState @Inject constructor( return copy(data = mutableData) data.add(item) addItem(item, true) - } + }*/ @Synchronized fun updateDisabledFlags( @@ -387,9 +386,9 @@ data class LauncherState @Inject constructor( user: UserHandle ): ApplicationItem? { for (item in itemsIdMap) { - if (item is ApplicationItem - && componentName == item.componentName - && user == item.user + if (item is ApplicationItem && + componentName == item.componentName && + user == item.user ) { return item } @@ -397,7 +396,8 @@ data class LauncherState @Inject constructor( return null } - fun addItem(item: LauncherItem, + fun addItem( + item: LauncherItem, mutableData: MutableList, mutableAllItems: MutableList, newItem: Boolean @@ -411,8 +411,8 @@ data class LauncherState @Inject constructor( } LauncherConstants.ItemType.APPLICATION, LauncherConstants.ItemType.SHORTCUT -> { - if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP - || item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT + if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP || + item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT ) { mutableAllItems.add(item) } else { diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt index f337917ef7..a95ebb6225 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt @@ -3,7 +3,7 @@ package foundation.e.blisslauncher.features.launcher import foundation.e.blisslauncher.base.presentation.BaseView import foundation.e.blisslauncher.domain.keys.PackageUserKey -interface LauncherView: +interface LauncherView : BaseView { fun updateIconBadges(updatedBadges: Set) diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt index ae00b0028a..67b0951afe 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt @@ -2,6 +2,6 @@ package foundation.e.blisslauncher.features.launcher import foundation.e.blisslauncher.base.presentation.BaseViewEvent -sealed class LauncherViewEvent: BaseViewEvent { - object LoadLauncher: LauncherViewEvent() +sealed class LauncherViewEvent : BaseViewEvent { + object LoadLauncher : LauncherViewEvent() } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt index b0ce46ded2..4ff4e38021 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt @@ -14,7 +14,7 @@ class LauncherViewModel @Inject constructor( private val launcherStateInteractor: LauncherStateInteractor, private val observeAddedApps: ObserveAddedApps, private val loadLauncher: LoadLauncher -) : BaseViewModel( +) : BaseViewModel( LauncherState( itemsIdMap = LongArrayMap(), allItems = emptyList(), @@ -32,32 +32,26 @@ class LauncherViewModel @Inject constructor( } } - fun loadLauncher() { - process(loadLauncherIntent()) - } - - private fun loadLauncherIntent(): BaseIntent { - return intent { - loadLauncher( - onSuccess = { list -> - process(intent { - val mutableAllItems = allItems.toMutableList() - mutableAllItems.addAll(list) - copy(data = list, allItems = mutableAllItems) - }) - }, - onError = { - it.printStackTrace() - process(intent { copy(data = emptyList()) }) - } - ) - copy() - } - } - fun terminate() { launcherStateInteractor(LauncherStateInteractor.Command.TERMINATE) disposable.dispose() observeAddedApps.dispose() } + + override fun toIntent(event: LauncherViewEvent): BaseIntent { + return when (event) { + is LauncherViewEvent.LoadLauncher -> intent { + loadLauncher( + onSuccess = { + newState { copy(data = emptyList()) } + }, + onError = { + it.printStackTrace() + copy(data = emptyList()) + } + ) + copy() + } + } + } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt index b14bb2c153..9ca4d45613 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/ActivityBindsModule.kt @@ -12,6 +12,4 @@ abstract class ActivityBindsModule { @PerActivity @ContributesAndroidInjector(modules = [LauncherActivityModule::class]) abstract fun contributesLauncherActivity(): LauncherActivity - - } \ No newline at end of file diff --git a/blisslauncherv2/src/test/java/foundation/e/blisslauncher/ExampleUnitTest.kt b/blisslauncherv2/src/test/java/foundation/e/blisslauncher/ExampleUnitTest.kt deleted file mode 100644 index 65638904ca..0000000000 --- a/blisslauncherv2/src/test/java/foundation/e/blisslauncher/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package foundation.e.blisslauncher - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/blisslauncherv2/src/test/java/foundation/e/blisslauncher/base/presentation/BaseViewModelTest.kt b/blisslauncherv2/src/test/java/foundation/e/blisslauncher/base/presentation/BaseViewModelTest.kt new file mode 100644 index 0000000000..65831f3f08 --- /dev/null +++ b/blisslauncherv2/src/test/java/foundation/e/blisslauncher/base/presentation/BaseViewModelTest.kt @@ -0,0 +1,55 @@ +package foundation.e.blisslauncher.base.presentation + +import foundation.e.blisslauncher.common.subscribeToState +import io.mockk.mockk +import org.junit.Before +import org.junit.Test + + +fun main() { + print("Amit") +} +class BaseViewModelTest { + + private lateinit var viewModel: BaseViewModel + + @Before + fun setUp() { + viewModel = mockk() + viewModel.states().subscribeToState {it.print()} + } + + @Test + fun testEvent() { + var event = DummyEvent.Event1 + if(event == DummyEvent.Event1) { + viewModel.process(intentForEvent1()) + } + } + + private fun processEvent1(onSuccess: (String) -> Unit) { + onSuccess("Title changed from Event1") + } + + private fun intentForEvent1(): BaseIntent { + return intent { + processEvent1 { + viewModel.process(intent { + copy(title = it) + }) + } + copy() + } + } +} + +data class DummyState(val id: Int, val title: String, val description: String): BaseViewState { + fun print() { + println("Id: $id, Title: $title, Desc: $description") + } +} + +sealed class DummyEvent: BaseViewEvent { + object Event1: DummyEvent() + object Event2: DummyEvent() +} \ No newline at end of file diff --git a/common.gradle b/common.gradle index 94b0822752..12b0f27adf 100644 --- a/common.gradle +++ b/common.gradle @@ -21,6 +21,10 @@ android { kotlinOptions { jvmTarget = "1.8" } + + lintOptions { + abortOnError false + } } dependencies { diff --git a/common/src/androidTest/java/com/amitkma/common/ExampleInstrumentedTest.kt b/common/src/androidTest/java/com/amitkma/common/ExampleInstrumentedTest.kt deleted file mode 100644 index cb70abd82c..0000000000 --- a/common/src/androidTest/java/com/amitkma/common/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.amitkma.common - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.amitkma.common.test", appContext.packageName) - } -} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt b/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt index 137b115745..fb312f2111 100644 --- a/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/executors/AppExecutors.kt @@ -3,4 +3,3 @@ package foundation.e.blisslauncher.common.executors import java.util.concurrent.Executor data class AppExecutors(val io: Executor, val computation: Executor, val main: Executor) - diff --git a/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java b/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java new file mode 100644 index 0000000000..8626a5b1c3 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.e.blisslauncher.common.util; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A utility map from keys to an ArrayList of values. + */ +public class MultiHashMap extends HashMap> { + + public MultiHashMap() { } + + public MultiHashMap(int size) { + super(size); + } + + public void addToList(K key, V value) { + ArrayList list = get(key); + if (list == null) { + list = new ArrayList<>(); + list.add(value); + put(key, list); + } else { + list.add(value); + } + } + + @Override + public MultiHashMap clone() { + MultiHashMap map = new MultiHashMap<>(size()); + for (Entry> entry : entrySet()) { + map.put(entry.getKey(), new ArrayList(entry.getValue())); + } + return map; + } +} diff --git a/common/src/test/java/com/amitkma/common/ExampleUnitTest.kt b/common/src/test/java/com/amitkma/common/ExampleUnitTest.kt deleted file mode 100644 index 2be77c0772..0000000000 --- a/common/src/test/java/com/amitkma/common/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.amitkma.common - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/data-bridge/src/androidTest/java/foundation/e/blisslauncher/databridge/ExampleInstrumentedTest.kt b/data-bridge/src/androidTest/java/foundation/e/blisslauncher/databridge/ExampleInstrumentedTest.kt deleted file mode 100644 index a019b8502c..0000000000 --- a/data-bridge/src/androidTest/java/foundation/e/blisslauncher/databridge/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package foundation.e.blisslauncher.databridge - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("foundation.e.blisslauncher.databridge.test", appContext.packageName) - } -} diff --git a/data-bridge/src/test/java/foundation/e/blisslauncher/databridge/ExampleUnitTest.kt b/data-bridge/src/test/java/foundation/e/blisslauncher/databridge/ExampleUnitTest.kt deleted file mode 100644 index c40869b2a3..0000000000 --- a/data-bridge/src/test/java/foundation/e/blisslauncher/databridge/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package foundation.e.blisslauncher.databridge - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt new file mode 100644 index 0000000000..22c1d0706f --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -0,0 +1,5 @@ +package foundation.e.blisslauncher.data + +interface LauncherDatabaseGateway { + +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherRepositoryImpl.kt new file mode 100644 index 0000000000..4becc3bb66 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherRepositoryImpl.kt @@ -0,0 +1,64 @@ +package foundation.e.blisslauncher.data + +import android.content.Context +import android.os.UserHandle +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.util.MultiHashMap +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.repository.LauncherRepository +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Single +import timber.log.Timber +import javax.inject.Inject + +class LauncherRepositoryImpl +@Inject constructor( + private val context: Context, + private val launcherApps: LauncherAppsCompat, + private val launcherDatabase: LauncherDatabaseGateway +) : + LauncherRepository { + override fun save(entity: S): Single { + TODO("Not yet implemented") + } + + override fun saveAll(entities: Iterable): Flowable> { + TODO("Not yet implemented") + } + + override fun findById(id: Long): Maybe { + TODO("Not yet implemented") + } + + override fun findAll(): Flowable> { + val pmHelper = PackageManagerHelper(context, launcherApps) + val isSafeMode = pmHelper.isSafeMode + val isSdCardReady = Utilities.isBootCompleted() + val pendingPackages = MultiHashMap() + var clearDb = false + + //TODO: GridSize Migration Task + if(clearDb) { + + } + } + + override fun delete(entity: LauncherItem) { + TODO("Not yet implemented") + } + + override fun deleteById(id: Long): Completable { + TODO("Not yet implemented") + } + + override fun deleteAll() { + TODO("Not yet implemented") + } + + override fun deleteAll(entities: Iterable) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt new file mode 100644 index 0000000000..a62203dcec --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt @@ -0,0 +1,102 @@ +package foundation.e.blisslauncher.data + +import android.app.AppOpsManager +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.os.Build +import android.os.UserHandle +import android.text.TextUtils +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import javax.inject.Inject + +class PackageManagerHelper( + context: Context, + private val launcherApps: LauncherAppsCompat +) { + private val pm = context.packageManager + + val isSafeMode + get() = pm.isSafeMode + + /** + * Returns true if the app can possibly be on the SDCard. This is just a workaround and doesn't + * guarantee that the app is on SD card. + */ + fun isAppOnSdcard( + packageName: String, + user: UserHandle + ): Boolean { + val info = launcherApps.getApplicationInfo( + packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, user + ) + return info != null && info.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0 + } + + /** + * Returns whether the target app is suspended for a given user as per + * [android.app.admin.DevicePolicyManager.isPackageSuspended]. + */ + fun isAppSuspended( + packageName: String, + user: UserHandle + ): Boolean { + val info = launcherApps.getApplicationInfo(packageName, 0, user) + return info != null && info.flags and ApplicationInfo.FLAG_SUSPENDED != 0 + } + + fun getAppLaunchIntent(pkg: String, user: UserHandle): Intent? { + val activities = launcherApps.getActivityList(pkg, user) + return if (activities.isEmpty()) null else + ApplicationItem.makeLaunchIntent(activities[0]) + } + + /** + * Returns true if {@param srcPackage} has the permission required to start the activity from + * {@param intent}. If {@param srcPackage} is null, then the activity should not need + * any permissions + */ + fun hasPermissionForActivity( + intent: Intent?, + srcPackage: String? + ): Boolean { + val target: ResolveInfo = pm.resolveActivity(intent, 0) + ?: // Not a valid target + return false + if (TextUtils.isEmpty(target.activityInfo.permission)) { + // No permission is needed + return true + } + if (TextUtils.isEmpty(srcPackage)) { + // The activity requires some permission but there is no source. + return false + } + + // Source does not have sufficient permissions. + if (pm.checkPermission(target.activityInfo.permission, srcPackage) != + PackageManager.PERMISSION_GRANTED + ) { + return false + } + + // On M and above also check AppOpsManager for compatibility mode permissions. + if (TextUtils.isEmpty(AppOpsManager.permissionToOp(target.activityInfo.permission))) { + // There is no app-op for this permission, which could have been disabled. + return true + } + + // There is no direct way to check if the app-op is allowed for a particular app. Since + // app-op is only enabled for apps running in compatibility mode, simply block such apps. + try { + return pm.getApplicationInfo( + srcPackage, + 0 + ).targetSdkVersion >= Build.VERSION_CODES.M + } catch (e: PackageManager.NameNotFoundException) { + } + return false + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt index 72e4e6fa5a..267447bd4c 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt @@ -4,6 +4,6 @@ import androidx.room.Database import androidx.room.RoomDatabase @Database(entities = [IconDatabase::class], version = 1) -abstract class IconDatabase: RoomDatabase() { +abstract class IconDatabase : RoomDatabase() { abstract fun iconDao(): IconDao } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt index badc42ac60..e68bee3f5a 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt @@ -138,8 +138,8 @@ class IconCache @Inject constructor( ) { val forDeletion = HashSet() for (key in cache.keys) { - if (key.componentName.packageName == packageName - && key.user == user + if (key.componentName.packageName == packageName && + key.user == user ) { forDeletion.add(key) } @@ -182,7 +182,7 @@ class IconCache @Inject constructor( ) { removeFromMemCacheLocked(packageName, user) val userSerial: Long = userManager.getSerialNumberForUser(user) - iconDao.delete("${packageName}/%", userSerial.toInt()) + iconDao.delete("$packageName/%", userSerial.toInt()) } fun updateDbIcons(ignorePackagesForMainUser: Set) { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt index 8a7b59b17f..7ec34b369f 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/CompatModule.kt @@ -9,7 +9,7 @@ import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.domain.repository.UserManagerRepository import foundation.e.blisslauncher.common.executors.AppExecutors import foundation.e.blisslauncher.common.executors.MainThreadExecutor -import foundation.e.blisslauncher.data.LauncherAppsChangedCallbackCompat +import foundation.e.blisslauncher.data.launcher.LauncherAppsChangedCallbackCompat import foundation.e.blisslauncher.data.compat.LauncherAppsCompatVL import foundation.e.blisslauncher.data.compat.LauncherAppsCompatVO import foundation.e.blisslauncher.data.compat.UserManagerCompatVN diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt index 9b0fdadfc7..3565750fe0 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataComponent.kt @@ -13,7 +13,7 @@ import javax.inject.Singleton DataRepoBindingModule::class ] ) -interface DataComponent: DomainComponent { +interface DataComponent : DomainComponent { @Component.Factory interface Factory { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt index 83caa02ce7..1250b1df3f 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt @@ -22,7 +22,7 @@ class DataRepoBindingModule { fun bindLauncherStateManager(launcherStateManagerImpl: LauncherStateManagerImpl): LauncherStateManager = launcherStateManagerImpl @Provides - fun bindAppsRepository(appsRepositoryImpl: AppsRepositoryImpl): AppsRepository = appsRepositoryImpl + fun bindAppsRepository(appsRepositoryImpl: AppsRepositoryImpl): AppsRepository = appsRepositoryImpl @Provides fun bindShortcutRepository(shortcutRepositoryImpl: ShortcutsRepositoryImpl): ShortcutRepository = shortcutRepositoryImpl diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherAppsChangedCallbackCompat.kt b/data/src/main/java/foundation/e/blisslauncher/data/launcher/LauncherAppsChangedCallbackCompat.kt similarity index 98% rename from data/src/main/java/foundation/e/blisslauncher/data/LauncherAppsChangedCallbackCompat.kt rename to data/src/main/java/foundation/e/blisslauncher/data/launcher/LauncherAppsChangedCallbackCompat.kt index 21cd8b2fba..1055d7304d 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherAppsChangedCallbackCompat.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/launcher/LauncherAppsChangedCallbackCompat.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.data +package foundation.e.blisslauncher.data.launcher import android.os.UserHandle import foundation.e.blisslauncher.common.compat.LauncherAppsCompat diff --git a/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt index 3dfc63e5c8..cd59306e0e 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/receiver/SessionCommitReceiver.kt @@ -6,7 +6,6 @@ import android.os.UserHandle class SessionCommitReceiver { companion object { fun queueAppIconAddition(context: Context, it: String, user: UserHandle) { - } } } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt index f23ecbc30c..bd7208b055 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt @@ -3,6 +3,4 @@ package foundation.e.blisslauncher.data.widgets import foundation.e.blisslauncher.domain.repository.WidgetsRepository import javax.inject.Inject -class WidgetsRepositoryImpl @Inject constructor(): WidgetsRepository { - -} \ No newline at end of file +class WidgetsRepositoryImpl @Inject constructor() : WidgetsRepository \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt index c4fa4a2494..634dc71c2f 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt @@ -55,7 +55,8 @@ open class ApplicationItem : WorkspaceItem { } fun updateRuntimeFlagsForActivityTarget( - info: LauncherItemWithIcon, lai: LauncherActivityInfo + info: LauncherItemWithIcon, + lai: LauncherActivityInfo ) { val appInfo = lai.applicationInfo /*if (PackageManagerHelper.isAppSuspended(appInfo)) { @@ -63,8 +64,8 @@ open class ApplicationItem : WorkspaceItem { }*/ info.runtimeStatusFlags = info.runtimeStatusFlags or if (appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0) FLAG_SYSTEM_NO else FLAG_SYSTEM_YES - if (Utilities.ATLEAST_OREO - && appInfo.targetSdkVersion >= Build.VERSION_CODES.O && Process.myUserHandle() == lai.user + if (Utilities.ATLEAST_OREO && + appInfo.targetSdkVersion >= Build.VERSION_CODES.O && Process.myUserHandle() == lai.user ) { // The icon for a non-primary user is badged, hence it's not exactly an adaptive icon. info.runtimeStatusFlags = info.runtimeStatusFlags or FLAG_ADAPTIVE_ICON } diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt index 4310af50db..a12c52ebe3 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt @@ -62,8 +62,8 @@ open class WorkspaceItem : LauncherItemWithIcon { override fun getTargetComponent(): ComponentName? { val cn = super.getTargetComponent() - if (cn == null && (itemType == LauncherConstants.ItemType.SHORTCUT - || hasStatusFlag(FLAG_SUPPORTS_WEB_UI)) + if (cn == null && (itemType == LauncherConstants.ItemType.SHORTCUT || + hasStatusFlag(FLAG_SUPPORTS_WEB_UI)) ) { // Legacy shortcuts and promise icons with web UI may not have a componentName but just // a packageName. In that case create a dummy componentName instead of adding additional diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt index 44bc2c20e6..1d1f200f6f 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt @@ -25,7 +25,7 @@ class AddPackages @Inject constructor( launcherRepository.add(it, params.user, userManager.isQuietModeEnabled(params.user)) //TODO: Add SessionCommitReceiver for below O devices } - }/*.doOnComplete { + } /*.doOnComplete { observeAddedApps(Unit) }*/ } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt index 5e5625d08f..dcda2e83b1 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/DeleteComponents.kt @@ -5,7 +5,7 @@ import foundation.e.blisslauncher.domain.ItemInfoMatcher import io.reactivex.Completable import java.util.concurrent.Executor -class DeleteComponents(appExecutors: AppExecutors): CompletableInteractor() { +class DeleteComponents(appExecutors: AppExecutors) : CompletableInteractor() { override val subscribeExecutor: Executor = appExecutors.io override fun doWork(params: ItemInfoMatcher): Completable { diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt index 279077c4c5..8f8397e892 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt @@ -3,6 +3,7 @@ package foundation.e.blisslauncher.domain.interactor import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.executors.AppExecutors import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.repository.LauncherRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository import io.reactivex.Single import java.util.concurrent.Executor @@ -11,7 +12,7 @@ import javax.inject.Singleton @Singleton class LoadLauncher @Inject constructor( - private val launcherApps: LauncherAppsCompat, + private val launcherRepository: LauncherRepository, private val userManager: UserManagerRepository, appExecutors: AppExecutors ) : ResultInteractor>() { @@ -20,11 +21,6 @@ class LoadLauncher @Inject constructor( override val observeExecutor: Executor = appExecutors.main override fun doWork(params: Unit?): Single> { - return Single.just( - userManager.userProfiles.map { user -> - launcherApps.getActivityList(null, user) - .map { ApplicationItem(it, user, userManager.isQuietModeEnabled(user)) } - }.flatten() - ) + return launcherRepository.findAll() } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt index 4f6126ed1f..00df62b05e 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedLauncherItems.kt @@ -1,2 +1 @@ package foundation.e.blisslauncher.domain.interactor - diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt index 4f6126ed1f..00df62b05e 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveRemovedLauncherItems.kt @@ -1,2 +1 @@ package foundation.e.blisslauncher.domain.interactor - diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt index dda7ce499c..ef5f562d75 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt @@ -21,6 +21,6 @@ class RemovePackages @Inject constructor( launcherRepository.removePackages(params.packages, params.user) }.doOnComplete { - observeAddedApps(Unit) + //observeAddedApps(Unit) } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt index 9a5c97d128..53ab0456b9 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt @@ -11,16 +11,14 @@ import foundation.e.blisslauncher.domain.ItemInfoMatcher import foundation.e.blisslauncher.domain.Matcher import foundation.e.blisslauncher.domain.and import foundation.e.blisslauncher.domain.entity.ApplicationItem -import foundation.e.blisslauncher.domain.entity.LauncherConstants import foundation.e.blisslauncher.domain.entity.WorkspaceItem import foundation.e.blisslauncher.domain.or -import foundation.e.blisslauncher.domain.repository.AppsRepository +import foundation.e.blisslauncher.domain.repository.LauncherRepository import io.reactivex.Completable import java.util.concurrent.Executor class UpdateLauncher( appExecutors: AppExecutors, - private val appsRepository: AppsRepository, private val launcherRepository: LauncherRepository, private val launcherAppsCompat: LauncherAppsCompat, private val deleteComponents: DeleteComponents @@ -30,11 +28,11 @@ class UpdateLauncher( override fun doWork(params: Params): Completable = Completable.fromAction { val addedOrUpdated = ArrayList() - addedOrUpdated.addAll(appsRepository.getModifiedApps()) - addedOrUpdated.addAll(appsRepository.getAddedApps()) + /* addedOrUpdated.addAll(appsRepository.getModifiedApps()) + addedOrUpdated.addAll(appsRepository.getAddedApps()) - val removedApps = ArrayList(appsRepository.getRemovedApps()) - appsRepository.clear() + val removedApps = ArrayList(appsRepository.getRemovedApps()) + appsRepository.clear()*/ val addedOrUpdatedApps = ArrayMap() if (addedOrUpdated.isNotEmpty()) { @@ -49,8 +47,9 @@ class UpdateLauncher( val isNewApkAvailable = params.command == Command.ADD || params.command == Command.UPDATE val updatedItems = ArrayList() - val map = launcherRepository.allItemsMap() - map.forEach { + //TODO: Uncomment it after successful presentation test. + //val map = launcherRepository.allItemsMap() + /*map.forEach { if (it is WorkspaceItem && params.user == it.user) it.let { workspaceItem -> var itemUpdated = false var shortcutUpdated = false @@ -101,7 +100,7 @@ class UpdateLauncher( } //TODO: Update launcher widgets here - } + }*/ // TODO: Update Shortcut here if (!removedItems.isEmpty) { @@ -121,11 +120,11 @@ class UpdateLauncher( removedPackages.add(it) } } - - // Update removedComponents because some packages can get removed during package update - removedApps.forEach { + //TODO + //Update removedComponents because some packages can get removed during package update + /*removedApps.forEach { removedComponents.add(it.componentName) - } + }*/ } if (removedPackages.isNotEmpty() || removedComponents.isNotEmpty()) { diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt index 6b56e1d8ac..2294ace20e 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdatePackages.kt @@ -4,7 +4,6 @@ import android.content.Context import android.os.UserHandle import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.repository.AppsRepository import io.reactivex.Completable import java.util.concurrent.Executor import javax.inject.Inject @@ -12,7 +11,6 @@ import javax.inject.Inject class UpdatePackages @Inject constructor( appExecutors: AppExecutors, private val context: Context, - private val appsRepository: AppsRepository, private val observeAddedApps: ObserveAddedApps, private val launcherAppsCompat: LauncherAppsCompat ) : CompletableInteractor() { @@ -24,10 +22,10 @@ class UpdatePackages @Inject constructor( override fun doWork(params: Params): Completable = Completable.fromAction { params.packages.forEach { // TODO: update icon cache - appsRepository.updateApp(context, it, params.user) + //appsRepository.updateApp(context, it, params.user) //TODO: Remove from widget cache } }.doOnComplete { - observeAddedApps(Unit) + //observeAddedApps(Unit) } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt index 783cccd882..c5806ef421 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt @@ -29,7 +29,7 @@ interface Repository { * @param entities to be saved. * @return [Flowable] emitting the saved entities. */ - fun saveAll(entities: Iterable): Flowable + fun saveAll(entities: Iterable): Flowable> /** * Retrieves an entity by its id. @@ -44,7 +44,7 @@ interface Repository { * * @return [Flowable] emitting all entities. */ - fun findAll(): Flowable + fun findAll(): Flowable> /** * Deletes a given entity. -- GitLab From 822db37eb65429dbca1c1bb8ea418823afc863d6 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 7 Apr 2020 03:41:25 +0530 Subject: [PATCH 05/23] More changes to data and domain layer --- .gitignore | 1 + .../features/launcher/LauncherActivity.java | 3 +- .../data/LauncherDatabaseGateway.kt | 18 +++- .../data/LauncherItemRepositoryImpl.kt | 93 +++++++++++++++++++ .../data/LauncherRepositoryImpl.kt | 64 ------------- .../data/compat/UserManagerCompatVN.kt | 9 +- .../data/database/WorkspaceLauncherItem.kt | 29 ++++++ .../data/database/WorkspaceScreen.kt | 17 ++++ .../data/inject/DataRepoBindingModule.kt | 4 +- .../domain/dto/WorkspaceModel.kt | 15 +++ .../domain/entity/WorkspaceScreen.kt | 6 ++ .../domain/inject/DomainComponent.kt | 4 +- .../domain/interactor/AddPackages.kt | 6 +- .../interactor/ChangeUserAvailability.kt | 6 +- .../domain/interactor/LoadLauncher.kt | 65 +++++++++++-- .../interactor/MakePackageUnavailable.kt | 6 +- .../domain/interactor/ObserveAddedApps.kt | 4 +- .../domain/interactor/RemovePackages.kt | 6 +- .../domain/interactor/SuspendPackages.kt | 6 +- .../domain/interactor/UnsuspendPackages.kt | 6 +- .../domain/interactor/UpdateLauncher.kt | 4 +- ...epository.kt => LauncherItemRepository.kt} | 2 +- .../domain/repository/Repository.kt | 33 ++----- .../repository/WorkspaceScreenRepository.kt | 7 ++ .../interactor/LoadAllAppsInteractorTest.kt | 14 +-- 25 files changed, 292 insertions(+), 136 deletions(-) create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt delete mode 100644 data/src/main/java/foundation/e/blisslauncher/data/LauncherRepositoryImpl.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceScreen.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceScreen.kt rename domain/src/main/java/foundation/e/blisslauncher/domain/repository/{LauncherRepository.kt => LauncherItemRepository.kt} (94%) create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt diff --git a/.gitignore b/.gitignore index 1575777d10..34ccb659b1 100755 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ build/ .idea/assetWizardSettings.xml .idea/markdown* gradle.xml +projectFilesBackup/ .classpath .project diff --git a/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java b/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java index 850584b81a..17278e5835 100755 --- a/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java +++ b/app/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.java @@ -261,8 +261,7 @@ public class LauncherActivity extends AppCompatActivity implements mAppWidgetManager = BlissLauncher.getApplication(this).getAppWidgetManager(); mAppWidgetHost = BlissLauncher.getApplication(this).getAppWidgetHost(); - mLauncherView = LayoutInflater.from(this).inflate( - foundation.e.blisslauncher.R.layout.activity_main, null); + mLauncherView = LayoutInflater.from(this).inflate(R.layout.activity_main, null); setContentView(mLauncherView); setupViews(); diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt index 22c1d0706f..2468808055 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -1,5 +1,21 @@ package foundation.e.blisslauncher.data +import foundation.e.blisslauncher.domain.entity.LauncherItem + interface LauncherDatabaseGateway { - + fun createEmptyDatabase() + + fun generateNewItemId() + + fun generateNewScreenId() + + fun deleteEmptyFolders() + + fun loadDefaultWorkspace() + + fun loadAllLauncherItems(): List + + fun loadWorkspaceScreensInOrder(): List + + fun markDeleted(id: Long) } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt new file mode 100644 index 0000000000..86066b5ce2 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt @@ -0,0 +1,93 @@ +package foundation.e.blisslauncher.data + +import android.content.Context +import android.os.UserHandle +import android.util.LongSparseArray +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.util.MultiHashMap +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import javax.inject.Inject + +class LauncherItemRepositoryImpl +@Inject constructor( + private val context: Context, + private val launcherApps: LauncherAppsCompat, + private val launcherDatabase: LauncherDatabaseGateway, + private val userManager: UserManagerRepository +) : LauncherItemRepository { + + private val TAG = "LauncherRepositoryImpl" + + override fun save(entity: S): S { + TODO("Not yet implemented") + } + + override fun saveAll(entities: Iterable): Iterable { + TODO("Not yet implemented") + } + + override fun findById(id: Long): LauncherItem? { + TODO("Not yet implemented") + } + + override fun findAll(): Iterable { + val pmHelper = PackageManagerHelper(context, launcherApps) + val isSafeMode = pmHelper.isSafeMode + val isSdCardReady = Utilities.isBootCompleted() + val pendingPackages = MultiHashMap() + + val allUsers:LongSparseArray = LongSparseArray() + val quietMode:LongSparseArray = LongSparseArray() + val unlockedUsers:LongSparseArray = LongSparseArray() + + userManager.userProfiles.forEach { + val serialNo = userManager.getSerialNumberForUser(it) + allUsers.put(serialNo, it) + quietMode.put(serialNo, userManager.isQuietModeEnabled(it)) + + val userUnlocked = userManager.isUserUnlocked(it) + unlockedUsers.put(serialNo, userUnlocked) + } + + //Populate item from database and fill necessary details based on users. + val launcherItems = launcherDatabase.loadAllLauncherItems() + + launcherItems.forEach {item -> + if(item.user == null) { + launcherDatabase.markDeleted(item.id) + } + + when(item.itemType) { + LauncherConstants.ItemType.APPLICATION, LauncherConstants.ItemType.SHORTCUT -> { + val intent = item.getIntent() + if(intent == null) { + launcherDatabase.markDeleted(item.id) + return@forEach + } + } + } + } + + return launcherItems + } + + override fun delete(entity: LauncherItem) { + TODO("Not yet implemented") + } + + override fun deleteById(id: Long) { + TODO("Not yet implemented") + } + + override fun deleteAll() { + TODO("Not yet implemented") + } + + override fun deleteAll(entities: Iterable) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherRepositoryImpl.kt deleted file mode 100644 index 4becc3bb66..0000000000 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherRepositoryImpl.kt +++ /dev/null @@ -1,64 +0,0 @@ -package foundation.e.blisslauncher.data - -import android.content.Context -import android.os.UserHandle -import foundation.e.blisslauncher.common.Utilities -import foundation.e.blisslauncher.common.compat.LauncherAppsCompat -import foundation.e.blisslauncher.common.util.MultiHashMap -import foundation.e.blisslauncher.domain.entity.LauncherItem -import foundation.e.blisslauncher.domain.repository.LauncherRepository -import io.reactivex.Completable -import io.reactivex.Flowable -import io.reactivex.Maybe -import io.reactivex.Single -import timber.log.Timber -import javax.inject.Inject - -class LauncherRepositoryImpl -@Inject constructor( - private val context: Context, - private val launcherApps: LauncherAppsCompat, - private val launcherDatabase: LauncherDatabaseGateway -) : - LauncherRepository { - override fun save(entity: S): Single { - TODO("Not yet implemented") - } - - override fun saveAll(entities: Iterable): Flowable> { - TODO("Not yet implemented") - } - - override fun findById(id: Long): Maybe { - TODO("Not yet implemented") - } - - override fun findAll(): Flowable> { - val pmHelper = PackageManagerHelper(context, launcherApps) - val isSafeMode = pmHelper.isSafeMode - val isSdCardReady = Utilities.isBootCompleted() - val pendingPackages = MultiHashMap() - var clearDb = false - - //TODO: GridSize Migration Task - if(clearDb) { - - } - } - - override fun delete(entity: LauncherItem) { - TODO("Not yet implemented") - } - - override fun deleteById(id: Long): Completable { - TODO("Not yet implemented") - } - - override fun deleteAll() { - TODO("Not yet implemented") - } - - override fun deleteAll(entities: Iterable) { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt index 593dd82f9c..a6bd9536a5 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt @@ -12,10 +12,12 @@ import java.util.ArrayList open class UserManagerCompatVN(context: Context) : UserManagerRepository { - protected val userManager: UserManager = context.getSystemService(Context.USER_SERVICE) as UserManager + protected val userManager: UserManager = + context.getSystemService(Context.USER_SERVICE) as UserManager private val pm: PackageManager = context.packageManager private lateinit var users: LongArrayMap + // Create a separate reverse map as LongArrayMap.indexOfValue checks if objects are same // and not {@link Object#equals} private lateinit var userToSerialMap: ArrayMap @@ -24,8 +26,7 @@ open class UserManagerCompatVN(context: Context) : UserManagerRepository { synchronized(this) { users = LongArrayMap() userToSerialMap = ArrayMap() - val _users: List = userManager.userProfiles - _users.forEach { + userManager.userProfiles.forEach { val serial = userManager.getSerialNumberForUser(it) users.put(serial, it) userToSerialMap[it] = serial @@ -37,7 +38,7 @@ open class UserManagerCompatVN(context: Context) : UserManagerRepository { get() { synchronized(this) { if (::users.isInitialized) { - return ArrayList(userToSerialMap.keys) + return ArrayList(userToSerialMap.keys) } } return userManager.userProfiles diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt new file mode 100644 index 0000000000..2100081ba7 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt @@ -0,0 +1,29 @@ +package foundation.e.blisslauncher.data.database + +import androidx.annotation.NonNull +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "launcherItems") +data class WorkspaceLauncherItem( + @PrimaryKey + val _id: Int, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT) + val title: String, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT) + val intent: String, + val container: Int, + val screen: Int, + val cellX: Int, + val cellY: Int, + val itemType: Int, + @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) + @NonNull + val modified: Int, + @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) + @NonNull + val rank: Int, + + val profileId: Int +) \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceScreen.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceScreen.kt new file mode 100644 index 0000000000..7e69d9e33e --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceScreen.kt @@ -0,0 +1,17 @@ +package foundation.e.blisslauncher.data.database + +import androidx.annotation.NonNull +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "workspaceScreens") +data class WorkspaceScreen( + @PrimaryKey + val id: Long, + @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) + val screenRank: Int, + @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) + @NonNull + val modified: Int +) \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt index 1250b1df3f..03c2a677a6 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt @@ -10,7 +10,7 @@ import foundation.e.blisslauncher.data.widgets.WidgetsRepositoryImpl import foundation.e.blisslauncher.data.workspace.WorkspaceRepositoryImpl import foundation.e.blisslauncher.domain.manager.LauncherStateManager import foundation.e.blisslauncher.domain.repository.AppsRepository -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.ShortcutRepository import foundation.e.blisslauncher.domain.repository.WidgetsRepository import foundation.e.blisslauncher.domain.repository.WorkspaceRepository @@ -28,7 +28,7 @@ class DataRepoBindingModule { fun bindShortcutRepository(shortcutRepositoryImpl: ShortcutsRepositoryImpl): ShortcutRepository = shortcutRepositoryImpl @Provides - fun bindLauncherRepository(launcherRepositoryImpl: LauncherRepositoryImpl): LauncherRepository = launcherRepositoryImpl + fun bindLauncherRepository(launcherRepositoryImpl: LauncherRepositoryImpl): LauncherItemRepository = launcherRepositoryImpl @Provides fun bindWorkspaceRepository(workspaceRepositoryImpl: WorkspaceRepositoryImpl): WorkspaceRepository = workspaceRepositoryImpl diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt new file mode 100644 index 0000000000..02f4f419ee --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt @@ -0,0 +1,15 @@ +package foundation.e.blisslauncher.domain.dto + +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.entity.FolderItem +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.WorkspaceScreen + +data class WorkspaceModel( + val itemsIdMap: LongArrayMap = LongArrayMap(), + val workspaceItems: ArrayList = ArrayList(), + val folders: LongArrayMap = LongArrayMap(), + val workspaceScreens: ArrayList = ArrayList() + + +) \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceScreen.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceScreen.kt new file mode 100644 index 0000000000..d4fe0bdd08 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceScreen.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.domain.entity + +data class WorkspaceScreen( + val id: Long, + val screenRank: Int +) \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt index 09466cc517..52e1502656 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt @@ -3,7 +3,7 @@ package foundation.e.blisslauncher.domain.inject import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.executors.AppExecutors import foundation.e.blisslauncher.domain.manager.LauncherStateManager -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository /** * Interface that lists all public repositories and data access layer components which are needed @@ -13,7 +13,7 @@ interface DomainComponent { fun launcherStateManager(): LauncherStateManager - fun launcherRepository(): LauncherRepository + fun launcherRepository(): LauncherItemRepository fun appExecutors(): AppExecutors diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt index 1d1f200f6f..56309236bc 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt @@ -2,7 +2,7 @@ package foundation.e.blisslauncher.domain.interactor import android.os.UserHandle import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository import io.reactivex.Completable import java.util.concurrent.Executor @@ -10,7 +10,7 @@ import javax.inject.Inject class AddPackages @Inject constructor( appExecutors: AppExecutors, - private val launcherRepository: LauncherRepository, + private val launcherItemRepository: LauncherItemRepository, private val userManager: UserManagerRepository, private val observeAddedApps: ObserveAddedApps ) : CompletableInteractor() { @@ -22,7 +22,7 @@ class AddPackages @Inject constructor( override fun doWork(params: Params): Completable = Completable.fromAction { params.packages.forEach { //TODO: Update icons cache - launcherRepository.add(it, params.user, userManager.isQuietModeEnabled(params.user)) + launcherItemRepository.add(it, params.user, userManager.isQuietModeEnabled(params.user)) //TODO: Add SessionCommitReceiver for below O devices } } /*.doOnComplete { diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt index d1b2661e64..03b92be8ef 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt @@ -2,7 +2,7 @@ package foundation.e.blisslauncher.domain.interactor import android.os.UserHandle import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository import io.reactivex.Completable import java.util.concurrent.Executor @@ -10,7 +10,7 @@ import javax.inject.Inject class ChangeUserAvailability @Inject constructor( appExecutors: AppExecutors, - private val launcherRepository: LauncherRepository, + private val launcherItemRepository: LauncherItemRepository, private val userManager: UserManagerRepository, private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems ) : CompletableInteractor() { @@ -19,7 +19,7 @@ class ChangeUserAvailability @Inject constructor( override fun doWork(params: UserHandle): Completable = Completable.fromAction { observeUpdatedLauncherItems( - launcherRepository.updateUserAvailability( + launcherItemRepository.updateUserAvailability( params, userManager.isQuietModeEnabled(params) ) diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt index 8f8397e892..ea45170595 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt @@ -1,26 +1,75 @@ package foundation.e.blisslauncher.domain.interactor -import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.entity.ApplicationItem -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.dto.WorkspaceModel +import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_DESKTOP +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceScreenRepository import io.reactivex.Single +import timber.log.Timber +import java.util.ArrayList import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Singleton @Singleton class LoadLauncher @Inject constructor( - private val launcherRepository: LauncherRepository, - private val userManager: UserManagerRepository, + private val launcherItemRepository: LauncherItemRepository, + private val workspaceScreenRepository: WorkspaceScreenRepository, appExecutors: AppExecutors -) : ResultInteractor>() { +) : ResultInteractor() { override val subscribeExecutor: Executor = appExecutors.io override val observeExecutor: Executor = appExecutors.main - override fun doWork(params: Unit?): Single> { - return launcherRepository.findAll() + override fun doWork(params: Unit?): Single { + return Single.just(WorkspaceModel()) + .map { workspaceModel -> + var clearDb = false + + //TODO: GridSize Migration Task + /*if (!clearDb && GridSizeMigrationTask.ENABLED && + !GridSizeMigrationTask.migrateGridIfNeeded(context) + ) { + // Migration failed. Clear workspace. + clearDb = true + }*/ + + if (clearDb) { + Timber.d("loadLauncher: resetting launcher database") + clearAllDbs() + } + + workspaceModel.workspaceScreens.addAll( + workspaceScreenRepository.findAllOrderedByScreenRank() + .map { it.id }) + + val launcherItems = launcherItemRepository.findAll() + + // Remove any empty screens + val unusedScreens: ArrayList = + ArrayList(workspaceModel.workspaceScreens) + for (item in workspaceModel.itemsIdMap) { + val screenId: Long = item.screenId + if (item.container == CONTAINER_DESKTOP.toLong() && + unusedScreens.contains(screenId) + ) { + unusedScreens.remove(screenId) + } + } + + // If there are any empty screens remove them, and update. + if (unusedScreens.size != 0) { + workspaceModel.workspaceScreens.removeAll(unusedScreens) + //TODO: update workspace screen order in database. + } + workspaceModel + } + } + + private fun clearAllDbs() { + workspaceScreenRepository.deleteAll() + launcherItemRepository.deleteAll() } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt index dc2fa424ad..3ea778097b 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt @@ -2,14 +2,14 @@ package foundation.e.blisslauncher.domain.interactor import android.os.UserHandle import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.reactivex.Completable import java.util.concurrent.Executor import javax.inject.Inject class MakePackageUnavailable @Inject constructor( appExecutors: AppExecutors, - private val launcherRepository: LauncherRepository, + private val launcherItemRepository: LauncherItemRepository, private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems ) : CompletableInteractor() { @@ -19,7 +19,7 @@ class MakePackageUnavailable @Inject constructor( override fun doWork(params: Params): Completable = Completable.fromAction { observeUpdatedLauncherItems( - launcherRepository.makePackagesUnavailable( + launcherItemRepository.makePackagesUnavailable( params.packages, params.user ) diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt index a11f116818..cd00cd8d71 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveAddedApps.kt @@ -2,7 +2,7 @@ package foundation.e.blisslauncher.domain.interactor import foundation.e.blisslauncher.common.executors.AppExecutors import foundation.e.blisslauncher.domain.entity.ApplicationItem -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.reactivex.Flowable import java.util.concurrent.Executor import javax.inject.Inject @@ -11,7 +11,7 @@ import javax.inject.Singleton @Singleton class ObserveAddedApps @Inject constructor( appExecutors: AppExecutors, - private val launcherRepository: LauncherRepository + private val launcherItemRepository: LauncherItemRepository ) : PublishSubjectInteractor, List>() { override val subscribeExecutor: Executor = appExecutors.io diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt index ef5f562d75..ed4caf8aba 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt @@ -2,14 +2,14 @@ package foundation.e.blisslauncher.domain.interactor import android.os.UserHandle import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.reactivex.Completable import java.util.concurrent.Executor import javax.inject.Inject class RemovePackages @Inject constructor( appExecutors: AppExecutors, - private val launcherRepository: LauncherRepository, + private val launcherItemRepository: LauncherItemRepository, private val observeAddedApps: ObserveAddedApps ) : CompletableInteractor() { @@ -19,7 +19,7 @@ class RemovePackages @Inject constructor( override fun doWork(params: Params): Completable = Completable.fromAction { - launcherRepository.removePackages(params.packages, params.user) + launcherItemRepository.removePackages(params.packages, params.user) }.doOnComplete { //observeAddedApps(Unit) } diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt index db453f6c24..f1fd183d15 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt @@ -2,14 +2,14 @@ package foundation.e.blisslauncher.domain.interactor import android.os.UserHandle import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.reactivex.Completable import java.util.concurrent.Executor import javax.inject.Inject class SuspendPackages @Inject constructor( appExecutors: AppExecutors, - private val launcherRepository: LauncherRepository, + private val launcherItemRepository: LauncherItemRepository, private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems ) : CompletableInteractor() { @@ -19,7 +19,7 @@ class SuspendPackages @Inject constructor( override fun doWork(params: Params): Completable = Completable.fromAction { observeUpdatedLauncherItems( - launcherRepository.suspendPackages( + launcherItemRepository.suspendPackages( params.packages, params.user ) diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt index 94c16db8b8..06e6ee541f 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt @@ -2,14 +2,14 @@ package foundation.e.blisslauncher.domain.interactor import android.os.UserHandle import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.reactivex.Completable import java.util.concurrent.Executor import javax.inject.Inject class UnsuspendPackages @Inject constructor( appExecutors: AppExecutors, - private val launcherRepository: LauncherRepository, + private val launcherItemRepository: LauncherItemRepository, private val observeUpdatedLauncherItems: ObserveUpdatedLauncherItems ) : CompletableInteractor() { @@ -19,7 +19,7 @@ class UnsuspendPackages @Inject constructor( override fun doWork(params: Params): Completable = Completable.fromAction { observeUpdatedLauncherItems( - launcherRepository.unsuspendPackages( + launcherItemRepository.unsuspendPackages( params.packages, params.user ) diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt index 53ab0456b9..8b02fd0578 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt @@ -13,13 +13,13 @@ import foundation.e.blisslauncher.domain.and import foundation.e.blisslauncher.domain.entity.ApplicationItem import foundation.e.blisslauncher.domain.entity.WorkspaceItem import foundation.e.blisslauncher.domain.or -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.reactivex.Completable import java.util.concurrent.Executor class UpdateLauncher( appExecutors: AppExecutors, - private val launcherRepository: LauncherRepository, + private val launcherItemRepository: LauncherItemRepository, private val launcherAppsCompat: LauncherAppsCompat, private val deleteComponents: DeleteComponents ) : CompletableInteractor() { diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt similarity index 94% rename from domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt rename to domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt index a3b170b90a..19a91d9116 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherRepository.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt @@ -7,7 +7,7 @@ import foundation.e.blisslauncher.domain.entity.LauncherItem /** * Repository to manage [LauncherItem] */ -interface LauncherRepository: Repository +interface LauncherItemRepository: Repository /*fun getAllActivities(user: UserHandle, quietMode: Boolean): List diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt index c5806ef421..0ae0cd1fd9 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt @@ -1,12 +1,7 @@ package foundation.e.blisslauncher.domain.repository -import io.reactivex.Completable -import io.reactivex.Flowable -import io.reactivex.Maybe -import io.reactivex.Single - /** - * Generic Reactive Repository interface which captures the domain type to manage with generic CRUD operations. + * Generic Repository interface which captures the domain type to manage with generic CRUD operations. * * @param the domain type which the repository manages. * @param the type of the id of the entity which the repository manages. @@ -19,58 +14,50 @@ interface Repository { * Saves a given entity and return the instance for further operations. * * @param entity to be saved. - * @return [Single] emitting the saved entity. + * @return [S] entity */ - fun save(entity: S): Single + fun save(entity: S): S /** * Saves all given entities. * * @param entities to be saved. - * @return [Flowable] emitting the saved entities. + * @return [Iterable] with the saved entities. */ - fun saveAll(entities: Iterable): Flowable> + fun saveAll(entities: Iterable): Iterable /** * Retrieves an entity by its id. * * @param id of the entity. - * @return [Maybe] emitting the entity with the given id or {@link Maybe#empty()} if none found. + * @return [T] the entity with the given id or null if none found. */ - fun findById(id: ID): Maybe + fun findById(id: ID): T? /** * Return all entities of this type. * - * @return [Flowable] emitting all entities. + * @return [Iterable] with all entities. */ - fun findAll(): Flowable> + fun findAll(): Iterable /** * Deletes a given entity. - * - * @return [Completable] signaling when operation has completed. */ fun delete(entity: T) /** * Deletes the entity with the given id. - * - * @return [Completable] signaling when operation has completed. */ - fun deleteById(id: ID): Completable + fun deleteById(id: ID) /** * Deletes all entities managed by this repository. - * - * @return [Completable] signaling when operation has completed. */ fun deleteAll() /** * Deletes the given entities. - * - * @return [Completable] signaling when operation has completed. */ fun deleteAll(entities: Iterable) } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt new file mode 100644 index 0000000000..60e103b7ad --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt @@ -0,0 +1,7 @@ +package foundation.e.blisslauncher.domain.repository + +import foundation.e.blisslauncher.domain.entity.WorkspaceScreen + +interface WorkspaceScreenRepository : Repository { + fun findAllOrderedByScreenRank(): Iterable +} \ No newline at end of file diff --git a/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt b/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt index 08855b1042..b0486ed161 100644 --- a/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt +++ b/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt @@ -2,7 +2,7 @@ package foundation.e.blisslauncher.domain.interactor import foundation.e.blisslauncher.common.executors.AppExecutors import foundation.e.blisslauncher.common.executors.MainThreadExecutor -import foundation.e.blisslauncher.domain.repository.LauncherRepository +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -18,7 +18,7 @@ import java.util.concurrent.Executors class LoadAllAppsInteractorTest { private lateinit var loadAllAppsInteractor: LoadAllAppsInteractor - lateinit var launcherRepository: LauncherRepository + lateinit var launcherItemRepository: LauncherItemRepository lateinit var appExecutors: AppExecutors @MockK @@ -26,7 +26,7 @@ class LoadAllAppsInteractorTest { @Before fun setUp() { - launcherRepository = mockk { + launcherItemRepository = mockk { every { getAllApps() } returns Single.just(listOf( @@ -48,7 +48,7 @@ class LoadAllAppsInteractorTest { Executors.newSingleThreadExecutor(), mainThreadExecutor ) - loadAllAppsInteractor = LoadAllAppsInteractor(launcherRepository, appExecutors) + loadAllAppsInteractor = LoadAllAppsInteractor(launcherItemRepository, appExecutors) } @After @@ -57,7 +57,7 @@ class LoadAllAppsInteractorTest { @Test fun doWorkCallsRepository() { - launcherRepository = mockk { + launcherItemRepository = mockk { every { getAllApps() } returns Single.just(listOf( @@ -74,14 +74,14 @@ class LoadAllAppsInteractorTest { )) } loadAllAppsInteractor.doWork() - verify(exactly = 1) { launcherRepository.getAllApps() } + verify(exactly = 1) { launcherItemRepository.getAllApps() } } @Test fun invokeCallsRepositoryAndCompletes() { loadAllAppsInteractor() - verify(exactly = 1) { launcherRepository.getAllApps() } + verify(exactly = 1) { launcherItemRepository.getAllApps() } loadAllAppsInteractor(onSuccess = { Assert.assertEquals(2, it.size) }) } -- GitLab From 26bee8182a7939b2634af5a2487fb36868974a1e Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 7 Apr 2020 21:39:11 +0530 Subject: [PATCH 06/23] Add filter operation --- .../data/LauncherDatabaseGateway.kt | 3 +- .../data/LauncherItemRepositoryImpl.kt | 105 +++++++++++++++--- .../data/database/WorkspaceLauncherItem.kt | 23 +++- 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt index 2468808055..7b48c9680c 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -1,5 +1,6 @@ package foundation.e.blisslauncher.data +import foundation.e.blisslauncher.data.database.WorkspaceLauncherItem import foundation.e.blisslauncher.domain.entity.LauncherItem interface LauncherDatabaseGateway { @@ -13,7 +14,7 @@ interface LauncherDatabaseGateway { fun loadDefaultWorkspace() - fun loadAllLauncherItems(): List + fun getAllWorkspaceItems(): List fun loadWorkspaceScreensInOrder(): List diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt index 86066b5ce2..b45b02d5ce 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt @@ -1,13 +1,17 @@ package foundation.e.blisslauncher.data import android.content.Context +import android.os.Process import android.os.UserHandle +import android.text.TextUtils import android.util.LongSparseArray import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.util.MultiHashMap +import foundation.e.blisslauncher.data.database.WorkspaceLauncherItem import foundation.e.blisslauncher.domain.entity.LauncherConstants import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository import javax.inject.Inject @@ -17,7 +21,8 @@ class LauncherItemRepositoryImpl private val context: Context, private val launcherApps: LauncherAppsCompat, private val launcherDatabase: LauncherDatabaseGateway, - private val userManager: UserManagerRepository + private val userManager: UserManagerRepository, + private val packageManagerHelper: PackageManagerHelper ) : LauncherItemRepository { private val TAG = "LauncherRepositoryImpl" @@ -40,9 +45,9 @@ class LauncherItemRepositoryImpl val isSdCardReady = Utilities.isBootCompleted() val pendingPackages = MultiHashMap() - val allUsers:LongSparseArray = LongSparseArray() - val quietMode:LongSparseArray = LongSparseArray() - val unlockedUsers:LongSparseArray = LongSparseArray() + val allUsers: LongSparseArray = LongSparseArray() + val quietMode: LongSparseArray = LongSparseArray() + val unlockedUsers: LongSparseArray = LongSparseArray() userManager.userProfiles.forEach { val serialNo = userManager.getSerialNumberForUser(it) @@ -54,25 +59,91 @@ class LauncherItemRepositoryImpl } //Populate item from database and fill necessary details based on users. - val launcherItems = launcherDatabase.loadAllLauncherItems() + val launcherItems = ArrayList() + launcherDatabase.getAllWorkspaceItems().asSequence() + .filter { checkAndValidate(allUsers, quietMode, unlockedUsers) } + .map { } - launcherItems.forEach {item -> - if(item.user == null) { - launcherDatabase.markDeleted(item.id) - } - when(item.itemType) { - LauncherConstants.ItemType.APPLICATION, LauncherConstants.ItemType.SHORTCUT -> { - val intent = item.getIntent() - if(intent == null) { - launcherDatabase.markDeleted(item.id) - return@forEach + launcherDatabase.getAllWorkspaceItems().forEach { item -> + } + + return launcherItems + } + + private fun checkAndValidate( + item: WorkspaceLauncherItem, + allUsers: LongSparseArray, + quietMode: LongSparseArray, + unlockedUsers: LongSparseArray, + isSdcardReady: Boolean + ): Boolean { + // Load necessary properties. + val itemType = item.itemType + val container = item.container + val id = item._id + val serialNumber = item.profileId + + //val restoreFlag = getInt(restoredIndex) + val user = allUsers[item.profileId] + if (user == null) { + launcherDatabase.markDeleted(id) + return false + } + + when (itemType) { + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT, + LauncherConstants.ItemType.DEEP_SHORTCUT -> { + val intent = item.getParsedIntent() + if (intent == null) { + launcherDatabase.markDeleted(id) + return false + } + + val disabledState = + if (quietMode[serialNumber]) LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER + else 0 + val componentName = intent.component + val targetPackage = + if (componentName == null) intent.`package` else componentName.packageName + if (Process.myUserHandle() != user) { + if (itemType == LauncherConstants.ItemType.SHORTCUT) { + launcherDatabase.markDeleted(id) + return false + } + } + + if (targetPackage.isNullOrEmpty() and (itemType != LauncherConstants.ItemType.SHORTCUT)) { + launcherDatabase.markDeleted(id) + return false + } + + // If there is no target package, its an implicit intent + // (legacy shortcut) which is always valid + val validTarget = targetPackage.isNullOrEmpty() || + launcherApps.isPackageEnabledForProfile(targetPackage, user) + if (componentName != null && validTarget) { + if (!launcherApps.isActivityEnabledForProfile(componentName, user)) { + launcherDatabase.markDeleted(id) + return false + } + } + + // else if componentName == null => can't infer much, leave it + // else if !validPackage => could be restored icon or missing sd-card + + if (targetPackage?.isNotEmpty() == true && !validTarget) { + // Points to a valid app (superset of componentName != null) but the apk + // is not available. + if(!packageManagerHelper.isAppOnSdcard(targetPackage, user) and isSdcardReady) { + launcherDatabase.markDeleted(id) + return false } } } } - - return launcherItems + return true } override fun delete(entity: LauncherItem) { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt index 2100081ba7..cdb633daec 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt @@ -1,14 +1,21 @@ package foundation.e.blisslauncher.data.database +import android.content.Intent +import android.os.UserHandle +import android.text.TextUtils +import android.util.LongSparseArray import androidx.annotation.NonNull import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey +import timber.log.Timber +import java.net.URISyntaxException @Entity(tableName = "launcherItems") data class WorkspaceLauncherItem( @PrimaryKey - val _id: Int, + val _id: Long, @ColumnInfo(typeAffinity = ColumnInfo.TEXT) val title: String, @ColumnInfo(typeAffinity = ColumnInfo.TEXT) @@ -24,6 +31,14 @@ data class WorkspaceLauncherItem( @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) @NonNull val rank: Int, - - val profileId: Int -) \ No newline at end of file + val profileId: Long +) { + fun getParsedIntent(): Intent? { + try { + return if (intent.isNullOrEmpty()) null else Intent.parseUri(intent, 0) + } catch (e: URISyntaxException) { + Timber.e(e) + return null + } + } +} \ No newline at end of file -- GitLab From c0f7f129fa65d72211a71826b96b981ac4ca42b3 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 8 Apr 2020 03:48:02 +0530 Subject: [PATCH 07/23] Map database entities to domain entities --- .../data/LauncherDatabaseGateway.kt | 2 + .../data/LauncherItemRepositoryImpl.kt | 155 +++++++++++++----- .../data/database/WorkspaceLauncherItem.kt | 42 ++++- .../domain/entity/ApplicationItem.kt | 4 +- .../domain/entity/LauncherItemWithIcon.kt | 1 + .../domain/entity/WorkspaceItem.kt | 9 +- 6 files changed, 158 insertions(+), 55 deletions(-) diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt index 7b48c9680c..dca10f271b 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -19,4 +19,6 @@ interface LauncherDatabaseGateway { fun loadWorkspaceScreensInOrder(): List fun markDeleted(id: Long) + + fun markDeleted(item: WorkspaceLauncherItem) } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt index b45b02d5ce..f8e18ff106 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt @@ -3,17 +3,20 @@ package foundation.e.blisslauncher.data import android.content.Context import android.os.Process import android.os.UserHandle -import android.text.TextUtils import android.util.LongSparseArray import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.util.MultiHashMap import foundation.e.blisslauncher.data.database.WorkspaceLauncherItem +import foundation.e.blisslauncher.domain.entity.FolderItem import foundation.e.blisslauncher.domain.entity.LauncherConstants import foundation.e.blisslauncher.domain.entity.LauncherItem import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.WorkspaceItem import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import timber.log.Timber +import java.lang.RuntimeException import javax.inject.Inject class LauncherItemRepositoryImpl @@ -61,71 +64,64 @@ class LauncherItemRepositoryImpl //Populate item from database and fill necessary details based on users. val launcherItems = ArrayList() launcherDatabase.getAllWorkspaceItems().asSequence() - .filter { checkAndValidate(allUsers, quietMode, unlockedUsers) } - .map { } - - - launcherDatabase.getAllWorkspaceItems().forEach { item -> - } + .onEach { + it.apply { + user = allUsers[profileId] + validTarget = + targetPackage.isNullOrEmpty() or launcherApps.isPackageEnabledForProfile( + targetPackage, + user + ) + } + } + .filter { checkAndValidate(it, unlockedUsers, isSdCardReady) } + .map { convertToLauncherItem(it, quietMode, isSdCardReady) } + .toCollection(launcherItems) return launcherItems } private fun checkAndValidate( item: WorkspaceLauncherItem, - allUsers: LongSparseArray, - quietMode: LongSparseArray, unlockedUsers: LongSparseArray, isSdcardReady: Boolean ): Boolean { - // Load necessary properties. - val itemType = item.itemType - val container = item.container - val id = item._id - val serialNumber = item.profileId //val restoreFlag = getInt(restoredIndex) - val user = allUsers[item.profileId] - if (user == null) { - launcherDatabase.markDeleted(id) + + if (item.user == null) { + launcherDatabase.markDeleted(item) return false } - when (itemType) { + when (item.itemType) { LauncherConstants.ItemType.APPLICATION, LauncherConstants.ItemType.SHORTCUT, LauncherConstants.ItemType.DEEP_SHORTCUT -> { - val intent = item.getParsedIntent() - if (intent == null) { - launcherDatabase.markDeleted(id) + if (item.intent == null) { + launcherDatabase.markDeleted(item) return false } - val disabledState = - if (quietMode[serialNumber]) LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER - else 0 - val componentName = intent.component - val targetPackage = - if (componentName == null) intent.`package` else componentName.packageName - if (Process.myUserHandle() != user) { - if (itemType == LauncherConstants.ItemType.SHORTCUT) { - launcherDatabase.markDeleted(id) + if (Process.myUserHandle() != item.user) { + if (item.itemType == LauncherConstants.ItemType.SHORTCUT) { + launcherDatabase.markDeleted(item) return false } } - if (targetPackage.isNullOrEmpty() and (itemType != LauncherConstants.ItemType.SHORTCUT)) { - launcherDatabase.markDeleted(id) + if (item.targetPackage.isNullOrEmpty() and (item.itemType != LauncherConstants.ItemType.SHORTCUT)) { + launcherDatabase.markDeleted(item) return false } // If there is no target package, its an implicit intent // (legacy shortcut) which is always valid - val validTarget = targetPackage.isNullOrEmpty() || - launcherApps.isPackageEnabledForProfile(targetPackage, user) - if (componentName != null && validTarget) { - if (!launcherApps.isActivityEnabledForProfile(componentName, user)) { - launcherDatabase.markDeleted(id) + val validTarget = item.targetPackage.isNullOrEmpty() || + launcherApps.isPackageEnabledForProfile(item.targetPackage, item.user) + if (item.componentName != null && validTarget) { + if (!launcherApps.isActivityEnabledForProfile(item.componentName, item.user)) { + launcherDatabase.markDeleted(item) return false } } @@ -133,11 +129,15 @@ class LauncherItemRepositoryImpl // else if componentName == null => can't infer much, leave it // else if !validPackage => could be restored icon or missing sd-card - if (targetPackage?.isNotEmpty() == true && !validTarget) { + if (item.targetPackage?.isNotEmpty() == true && !validTarget) { // Points to a valid app (superset of componentName != null) but the apk // is not available. - if(!packageManagerHelper.isAppOnSdcard(targetPackage, user) and isSdcardReady) { - launcherDatabase.markDeleted(id) + if (!packageManagerHelper.isAppOnSdcard( + item.targetPackage, + item.user!! + ) and isSdcardReady + ) { + launcherDatabase.markDeleted(item) return false } } @@ -146,6 +146,81 @@ class LauncherItemRepositoryImpl return true } + private fun convertToLauncherItem( + item: WorkspaceLauncherItem, + quietMode: LongSparseArray, + isSdcardReady: Boolean, + isSafeMode: Boolean, + pendingPackages: MultiHashMap + ): LauncherItem? { + var allowMissingTarget = false + // Load necessary properties. + val itemType = item.itemType + val container = item.container + val id = item._id + val serialNumber = item.profileId + + var disabledState = + if (quietMode[item.profileId]) LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER else 0 + //val restoreFlag = getInt(restoredIndex) + return when (itemType) { + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT, + LauncherConstants.ItemType.DEEP_SHORTCUT -> { + if (item.targetPackage?.isNotEmpty() == true and !item.validTarget) { + // Points to a valid app but the apk is not available. + if (packageManagerHelper.isAppOnSdcard(item.targetPackage, item.user!!)) { + disabledState = + disabledState or LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE + allowMissingTarget = true + } else if (!isSdcardReady) { + Timber.d("Missing Package ${item.targetPackage}, will be checked later") + pendingPackages.addToList(item.user, item.targetPackage) + allowMissingTarget = true + } + } + + var launcherItem: WorkspaceItem? = null + if (item.itemType == LauncherConstants.ItemType.APPLICATION) { + + } else if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { + + } else { + launcherItem = WorkspaceItem() + .apply { + this.user = item.user!! + this.itemType = item.itemType + this.title = if (item.title.isEmpty()) "" else item.title + // TODO: Set Icon here + } + } + if (launcherItem != null) { + launcherItem.apply { + item.applyCommonProperties(this) + } + launcherItem.intent = item.intent + launcherItem.rank = item.rank + launcherItem.runtimeStatusFlags = + launcherItem.runtimeStatusFlags or disabledState + if (!isSafeMode && !Utilities.isSystemApp(context, item.intent)) { + launcherItem.runtimeStatusFlags = + launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SAFEMODE + } + launcherItem + } else throw RuntimeException("Unexpected null LauncherItem") + } + LauncherConstants.ItemType.FOLDER -> { + val folderItem = FolderItem() + .apply { + item.applyCommonProperties(this) + title = item.title + } + folderItem + } + else -> null + } + } + override fun delete(entity: LauncherItem) { TODO("Not yet implemented") } diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt index cdb633daec..66ddf1f893 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt @@ -1,14 +1,15 @@ package foundation.e.blisslauncher.data.database +import android.content.ComponentName import android.content.Intent import android.os.UserHandle -import android.text.TextUtils -import android.util.LongSparseArray import androidx.annotation.NonNull import androidx.room.ColumnInfo import androidx.room.Entity -import androidx.room.Ignore import androidx.room.PrimaryKey +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon import timber.log.Timber import java.net.URISyntaxException @@ -18,10 +19,10 @@ data class WorkspaceLauncherItem( val _id: Long, @ColumnInfo(typeAffinity = ColumnInfo.TEXT) val title: String, - @ColumnInfo(typeAffinity = ColumnInfo.TEXT) - val intent: String, - val container: Int, - val screen: Int, + @ColumnInfo(typeAffinity = ColumnInfo.TEXT, name = "intent") + val intentStr: String?, + val container: Long, + val screen: Long, val cellX: Int, val cellY: Int, val itemType: Int, @@ -33,12 +34,35 @@ data class WorkspaceLauncherItem( val rank: Int, val profileId: Long ) { - fun getParsedIntent(): Intent? { + // Properties to initialise for proper validation + val targetPackage: String? + val intent: Intent? + val componentName: ComponentName? + var user: UserHandle? = null + var validTarget: Boolean = false + + init { + intent = getParsedIntent() + componentName = intent?.component + targetPackage = componentName?.packageName ?: intent?.`package` + } + + private fun getParsedIntent(): Intent? { try { - return if (intent.isNullOrEmpty()) null else Intent.parseUri(intent, 0) + return if (intentStr.isNullOrEmpty()) null else Intent.parseUri(intentStr, 0) } catch (e: URISyntaxException) { Timber.e(e) return null } } + + fun applyCommonProperties( + destItem: LauncherItem + ) { + destItem.id = _id + destItem.container = container + destItem.screenId = screen + destItem.cellX = cellX + destItem.cellY = cellY + } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt index 634dc71c2f..5a492fcabb 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt @@ -28,7 +28,7 @@ open class ApplicationItem : WorkspaceItem { this.container = NO_ID.toLong() this.user = user this.title = info.label - actionIntent = makeLaunchIntent(componentName) + intent = makeLaunchIntent(componentName) if (quietModeEnabled) { runtimeStatusFlags = runtimeStatusFlags or FLAG_DISABLED_QUIET_USER @@ -36,7 +36,7 @@ open class ApplicationItem : WorkspaceItem { updateRuntimeFlagsForActivityTarget(this, info) } - override fun getIntent(): Intent? = actionIntent + override fun getIntent(): Intent? = intent fun toComponentKey(): ComponentKey = ComponentKey( diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt index b62c30910c..a8a5bfe6e0 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt @@ -1,5 +1,6 @@ package foundation.e.blisslauncher.domain.entity +import android.content.Intent import android.graphics.Bitmap /** diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt index a12c52ebe3..e5e22d1f74 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt @@ -13,7 +13,8 @@ open class WorkspaceItem : LauncherItemWithIcon { /** * The intent used to start the application. */ - var actionIntent: Intent? = null + @get:JvmName("_getIntent") + var intent: Intent? = null /** * If isShortcut=true and customIcon=false, this contains a reference to the @@ -44,14 +45,14 @@ open class WorkspaceItem : LauncherItemWithIcon { constructor(item: WorkspaceItem) : super(item) { title = item.title - actionIntent = item.getIntent() + intent = item.getIntent() iconResource = item.iconResource status = item.status installProgress = item.installProgress } override fun getIntent(): Intent? { - return actionIntent + return intent } fun hasStatusFlag(flag: Int) = status and flag != 0 @@ -68,7 +69,7 @@ open class WorkspaceItem : LauncherItemWithIcon { // Legacy shortcuts and promise icons with web UI may not have a componentName but just // a packageName. In that case create a dummy componentName instead of adding additional // check everywhere. - val pkg: String? = actionIntent?.getPackage() + val pkg: String? = intent?.getPackage() return if (pkg == null) null else ComponentName(pkg, ".") } return cn -- GitLab From 35d031e40ac27a37960b09c104e9adb75c6da410 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 9 Apr 2020 01:47:53 +0530 Subject: [PATCH 08/23] Add support for Application Item --- .../features/launcher/LauncherState.kt | 6 +- .../data/LauncherItemRepositoryImpl.kt | 74 +++++++++++++++---- .../{WorkspaceItem.kt => AppShortcutItem.kt} | 9 ++- .../domain/entity/ApplicationItem.kt | 5 +- .../blisslauncher/domain/entity/FolderItem.kt | 12 +-- .../domain/interactor/UpdateLauncher.kt | 4 +- 6 files changed, 78 insertions(+), 32 deletions(-) rename domain/src/main/java/foundation/e/blisslauncher/domain/entity/{WorkspaceItem.kt => AppShortcutItem.kt} (92%) diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt index 664fa9a77d..fffeaa7f39 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt @@ -15,7 +15,7 @@ import foundation.e.blisslauncher.domain.entity.FolderItem import foundation.e.blisslauncher.domain.entity.LauncherConstants import foundation.e.blisslauncher.domain.entity.LauncherItem import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon -import foundation.e.blisslauncher.domain.entity.WorkspaceItem +import foundation.e.blisslauncher.domain.entity.AppShortcutItem import foundation.e.blisslauncher.domain.removeFlag import javax.inject.Inject @@ -243,7 +243,7 @@ data class LauncherState @Inject constructor( if (!folders.containsKey(item.container)) { } } else { - findOrMakeFolder(item.container).add(item as WorkspaceItem, false) + findOrMakeFolder(item.container).add(item as AppShortcutItem, false) } } } @@ -421,7 +421,7 @@ data class LauncherState @Inject constructor( } } else { findOrMakeFolder(item.container).add( - item as WorkspaceItem, + item as AppShortcutItem, false ) } diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt index f8e18ff106..f86568c73b 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt @@ -1,6 +1,7 @@ package foundation.e.blisslauncher.data import android.content.Context +import android.content.Intent import android.os.Process import android.os.UserHandle import android.util.LongSparseArray @@ -8,15 +9,15 @@ import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.util.MultiHashMap import foundation.e.blisslauncher.data.database.WorkspaceLauncherItem +import foundation.e.blisslauncher.domain.entity.AppShortcutItem +import foundation.e.blisslauncher.domain.entity.ApplicationItem import foundation.e.blisslauncher.domain.entity.FolderItem import foundation.e.blisslauncher.domain.entity.LauncherConstants import foundation.e.blisslauncher.domain.entity.LauncherItem import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon -import foundation.e.blisslauncher.domain.entity.WorkspaceItem import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository import timber.log.Timber -import java.lang.RuntimeException import javax.inject.Inject class LauncherItemRepositoryImpl @@ -28,8 +29,6 @@ class LauncherItemRepositoryImpl private val packageManagerHelper: PackageManagerHelper ) : LauncherItemRepository { - private val TAG = "LauncherRepositoryImpl" - override fun save(entity: S): S { TODO("Not yet implemented") } @@ -75,7 +74,15 @@ class LauncherItemRepositoryImpl } } .filter { checkAndValidate(it, unlockedUsers, isSdCardReady) } - .map { convertToLauncherItem(it, quietMode, isSdCardReady) } + .mapNotNull { + convertToLauncherItem( + it, + quietMode, + isSdCardReady, + isSafeMode, + pendingPackages + ) + } .toCollection(launcherItems) return launcherItems @@ -156,10 +163,6 @@ class LauncherItemRepositoryImpl var allowMissingTarget = false // Load necessary properties. val itemType = item.itemType - val container = item.container - val id = item._id - val serialNumber = item.profileId - var disabledState = if (quietMode[item.profileId]) LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER else 0 //val restoreFlag = getInt(restoredIndex) @@ -180,19 +183,35 @@ class LauncherItemRepositoryImpl } } - var launcherItem: WorkspaceItem? = null + var launcherItem: AppShortcutItem? = null if (item.itemType == LauncherConstants.ItemType.APPLICATION) { - + launcherItem = getApplicationItem( + item.user!!, + quietMode[item.profileId], + item.intent!!, + allowMissingTarget + ) } else if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { } else { - launcherItem = WorkspaceItem() + launcherItem = AppShortcutItem() .apply { this.user = item.user!! this.itemType = item.itemType this.title = if (item.title.isEmpty()) "" else item.title // TODO: Set Icon here } + val intent = item.intent!! + if (intent.action != null && + intent.categories != null && + intent.action == Intent.ACTION_MAIN && + intent.categories.contains(Intent.CATEGORY_LAUNCHER) + ) { + item.intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + ) + } } if (launcherItem != null) { launcherItem.apply { @@ -206,8 +225,8 @@ class LauncherItemRepositoryImpl launcherItem.runtimeStatusFlags = launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SAFEMODE } - launcherItem - } else throw RuntimeException("Unexpected null LauncherItem") + } + launcherItem } LauncherConstants.ItemType.FOLDER -> { val folderItem = FolderItem() @@ -217,8 +236,33 @@ class LauncherItemRepositoryImpl } folderItem } - else -> null + else -> throw RuntimeException("Unexpected type of LauncherItem encountered") + } + } + + private fun getApplicationItem( + user: UserHandle, + quietMode: Boolean, + intent: Intent, + allowMissingTarget: Boolean + ): AppShortcutItem? { + val componentName = intent.component + if (componentName == null) { + Timber.d("Missing component found in getApplicationItem") + return null } + val newIntent = Intent(Intent.ACTION_MAIN, null) + .apply { + component = componentName + } + newIntent.addCategory(Intent.CATEGORY_LAUNCHER) + val lai = launcherApps.resolveActivity(newIntent, user) + if ((lai == null) && !allowMissingTarget) { + Timber.d("Missing activity found in getApplicationItem") + return null + } + + return ApplicationItem(lai!!, user, quietMode) } override fun delete(entity: LauncherItem) { diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt similarity index 92% rename from domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt rename to domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt index e5e22d1f74..afe751efd8 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/WorkspaceItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt @@ -5,10 +5,11 @@ import android.content.Intent import android.content.Intent.ShortcutIconResource /** - * Represents an item in workspace and inside folder + * Represents an item in workspace and inside folder. + * It can be an Application, Shortcut or Deep Shortcut. * Also used for pinned and dynamic shortcuts of the apps. */ -open class WorkspaceItem : LauncherItemWithIcon { +open class AppShortcutItem : LauncherItemWithIcon { /** * The intent used to start the application. @@ -43,7 +44,7 @@ open class WorkspaceItem : LauncherItemWithIcon { itemType = LauncherConstants.ItemType.SHORTCUT } - constructor(item: WorkspaceItem) : super(item) { + constructor(item: AppShortcutItem) : super(item) { title = item.title intent = item.getIntent() iconResource = item.iconResource @@ -82,7 +83,7 @@ open class WorkspaceItem : LauncherItemWithIcon { * The shortcut was restored from a backup and it not ready to be used. This is automatically * set during backup/restore */ - const val FLAG_RESTORED_ICON = 1 + const val FLAG_RESTORED_ICON = 1 /** * The icon was added as an auto-install app, and is not ready to be used. This flag can't diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt index 5a492fcabb..e5386e16aa 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt @@ -11,9 +11,10 @@ import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.domain.keys.ComponentKey /** - * Represents an app in AllAppsStore + * Represents an app in Launcher Workspace */ -open class ApplicationItem : WorkspaceItem { +open class ApplicationItem : AppShortcutItem { + /** * The intent used to start the application. */ diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt index d9fdd302ca..aa36a342c0 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt @@ -7,7 +7,7 @@ class FolderItem : LauncherItem() { var options: Int = 0 - val contents = mutableListOf() + val contents = mutableListOf() val listeners = ArrayList() @@ -21,14 +21,14 @@ class FolderItem : LauncherItem() { * * @param item */ - fun add(item: WorkspaceItem, animate: Boolean) { + fun add(item: AppShortcutItem, animate: Boolean) { add(item, contents.size, animate) } /** * Add an app or shortcut for a specified rank. */ - fun add(item: WorkspaceItem, rank: Int, animate: Boolean) { + fun add(item: AppShortcutItem, rank: Int, animate: Boolean) { var rank = rank rank = Utilities.boundToRange(rank, 0, contents.size) contents.add(rank, item) @@ -43,7 +43,7 @@ class FolderItem : LauncherItem() { * * @param item */ - fun remove(item: WorkspaceItem, animate: Boolean) { + fun remove(item: AppShortcutItem, animate: Boolean) { contents.remove(item) /*for (i in listeners.indices) { listeners.get(i).onRemove(item) @@ -72,8 +72,8 @@ class FolderItem : LauncherItem() { } interface FolderListener { - fun onAdd(item: WorkspaceItem, rank: Int) - fun onRemove(item: WorkspaceItem) + fun onAdd(item: AppShortcutItem, rank: Int) + fun onRemove(item: AppShortcutItem) fun onTitleChanged(title: CharSequence) fun onItemsChanged(animate: Boolean) fun prepareAutoUpdate() diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt index 8b02fd0578..ece469f8b6 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt @@ -11,7 +11,7 @@ import foundation.e.blisslauncher.domain.ItemInfoMatcher import foundation.e.blisslauncher.domain.Matcher import foundation.e.blisslauncher.domain.and import foundation.e.blisslauncher.domain.entity.ApplicationItem -import foundation.e.blisslauncher.domain.entity.WorkspaceItem +import foundation.e.blisslauncher.domain.entity.AppShortcutItem import foundation.e.blisslauncher.domain.or import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.reactivex.Completable @@ -46,7 +46,7 @@ class UpdateLauncher( val removedItems = LongArrayMap() val isNewApkAvailable = params.command == Command.ADD || params.command == Command.UPDATE - val updatedItems = ArrayList() + val updatedItems = ArrayList() //TODO: Uncomment it after successful presentation test. //val map = launcherRepository.allItemsMap() /*map.forEach { -- GitLab From fe3a983d15ba5cd27b21c7bf0818bc6467271bfb Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 10 Apr 2020 13:15:43 +0530 Subject: [PATCH 09/23] Add PinnedShortcutManager --- .../common/compat/ShortcutInfoCompat.kt | 4 +- .../data/LauncherDatabaseGateway.kt | 7 +- .../data/LauncherItemRepositoryImpl.kt | 14 +- .../data/database/IconDatabase.kt | 1 + .../data/database/{ => dao}/IconDao.kt | 3 +- .../database/{ => roomentity}/IconEntity.kt | 2 +- .../LauncherItemRoomEntity.kt} | 6 +- .../e/blisslauncher/data/icon/IconCache.kt | 2 +- .../data/shortcuts/PinnedShortcutManager.kt | 158 ++++++++++++++++++ .../data/shortcuts/ShortcutsRepositoryImpl.kt | 12 -- .../blisslauncher/domain/keys/ShortcutKey.kt | 34 ++++ 11 files changed, 212 insertions(+), 31 deletions(-) rename data/src/main/java/foundation/e/blisslauncher/data/database/{ => dao}/IconDao.kt (82%) rename data/src/main/java/foundation/e/blisslauncher/data/database/{ => roomentity}/IconEntity.kt (96%) rename data/src/main/java/foundation/e/blisslauncher/data/database/{WorkspaceLauncherItem.kt => roomentity/LauncherItemRoomEntity.kt} (89%) create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/shortcuts/PinnedShortcutManager.kt delete mode 100644 data/src/main/java/foundation/e/blisslauncher/data/shortcuts/ShortcutsRepositoryImpl.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/keys/ShortcutKey.kt diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt index 37849abfc0..99d10d7fa7 100644 --- a/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt @@ -46,9 +46,9 @@ class ShortcutInfoCompat(private val shortcutInfo: ShortcutInfo) { fun getLastChangedTimestamp(): Long = shortcutInfo.getLastChangedTimestamp() - fun getActivity(): ComponentName? = shortcutInfo.getActivity() + fun getActivity(): ComponentName = shortcutInfo.getActivity() - fun getUserHandle(): UserHandle? = shortcutInfo.getUserHandle() + fun getUserHandle(): UserHandle = shortcutInfo.getUserHandle() fun hasKeyFieldsOnly(): Boolean = shortcutInfo.hasKeyFieldsOnly() diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt index dca10f271b..c9c29bda26 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -1,7 +1,6 @@ package foundation.e.blisslauncher.data -import foundation.e.blisslauncher.data.database.WorkspaceLauncherItem -import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity interface LauncherDatabaseGateway { fun createEmptyDatabase() @@ -14,11 +13,11 @@ interface LauncherDatabaseGateway { fun loadDefaultWorkspace() - fun getAllWorkspaceItems(): List + fun getAllWorkspaceItems(): List fun loadWorkspaceScreensInOrder(): List fun markDeleted(id: Long) - fun markDeleted(item: WorkspaceLauncherItem) + fun markDeleted(item: LauncherItemRoomEntity) } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt index f86568c73b..5059df0557 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt @@ -8,13 +8,14 @@ import android.util.LongSparseArray import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.util.MultiHashMap -import foundation.e.blisslauncher.data.database.WorkspaceLauncherItem +import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity import foundation.e.blisslauncher.domain.entity.AppShortcutItem import foundation.e.blisslauncher.domain.entity.ApplicationItem import foundation.e.blisslauncher.domain.entity.FolderItem import foundation.e.blisslauncher.domain.entity.LauncherConstants import foundation.e.blisslauncher.domain.entity.LauncherItem import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.keys.ShortcutKey import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository import timber.log.Timber @@ -62,7 +63,8 @@ class LauncherItemRepositoryImpl //Populate item from database and fill necessary details based on users. val launcherItems = ArrayList() - launcherDatabase.getAllWorkspaceItems().asSequence() + launcherDatabase.getAllWorkspaceItems() + .filter { checkAndValidate(it, unlockedUsers, isSdCardReady) } .onEach { it.apply { user = allUsers[profileId] @@ -73,7 +75,6 @@ class LauncherItemRepositoryImpl ) } } - .filter { checkAndValidate(it, unlockedUsers, isSdCardReady) } .mapNotNull { convertToLauncherItem( it, @@ -89,7 +90,7 @@ class LauncherItemRepositoryImpl } private fun checkAndValidate( - item: WorkspaceLauncherItem, + item: LauncherItemRoomEntity, unlockedUsers: LongSparseArray, isSdcardReady: Boolean ): Boolean { @@ -154,7 +155,7 @@ class LauncherItemRepositoryImpl } private fun convertToLauncherItem( - item: WorkspaceLauncherItem, + item: LauncherItemRoomEntity, quietMode: LongSparseArray, isSdcardReady: Boolean, isSafeMode: Boolean, @@ -192,7 +193,7 @@ class LauncherItemRepositoryImpl allowMissingTarget ) } else if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { - + val shortcutKey = ShortcutKey.fromIntent(item.intent!!, item.user!!) } else { launcherItem = AppShortcutItem() .apply { @@ -263,6 +264,7 @@ class LauncherItemRepositoryImpl } return ApplicationItem(lai!!, user, quietMode) + .apply { itemType = LauncherConstants.ItemType.APPLICATION } } override fun delete(entity: LauncherItem) { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt index 267447bd4c..f928838e00 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt @@ -2,6 +2,7 @@ package foundation.e.blisslauncher.data.database import androidx.room.Database import androidx.room.RoomDatabase +import foundation.e.blisslauncher.data.database.dao.IconDao @Database(entities = [IconDatabase::class], version = 1) abstract class IconDatabase : RoomDatabase() { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt similarity index 82% rename from data/src/main/java/foundation/e/blisslauncher/data/database/IconDao.kt rename to data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt index 94a94a452d..bca5ee25c7 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDao.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt @@ -1,9 +1,10 @@ -package foundation.e.blisslauncher.data.database +package foundation.e.blisslauncher.data.database.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import foundation.e.blisslauncher.data.database.roomentity.IconEntity @Dao interface IconDao { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconEntity.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt similarity index 96% rename from data/src/main/java/foundation/e/blisslauncher/data/database/IconEntity.kt rename to data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt index 22c8c87642..4ace013058 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/IconEntity.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.data.database +package foundation.e.blisslauncher.data.database.roomentity import androidx.room.ColumnInfo import androidx.room.Entity diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt similarity index 89% rename from data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt rename to data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt index 66ddf1f893..d0ce2f338b 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceLauncherItem.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.data.database +package foundation.e.blisslauncher.data.database.roomentity import android.content.ComponentName import android.content.Intent @@ -7,14 +7,12 @@ import androidx.annotation.NonNull import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import foundation.e.blisslauncher.common.util.LongArrayMap import foundation.e.blisslauncher.domain.entity.LauncherItem -import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon import timber.log.Timber import java.net.URISyntaxException @Entity(tableName = "launcherItems") -data class WorkspaceLauncherItem( +data class LauncherItemRoomEntity( @PrimaryKey val _id: Long, @ColumnInfo(typeAffinity = ColumnInfo.TEXT) diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt index e68bee3f5a..a39b922d37 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt @@ -18,7 +18,7 @@ import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.domain.repository.UserManagerRepository import foundation.e.blisslauncher.data.InvariantDeviceProfile -import foundation.e.blisslauncher.data.database.IconDao +import foundation.e.blisslauncher.data.database.dao.IconDao import foundation.e.blisslauncher.data.graphics.BitmapInfo import foundation.e.blisslauncher.data.graphics.BitmapRenderer import foundation.e.blisslauncher.domain.keys.ComponentKey diff --git a/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/PinnedShortcutManager.kt b/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/PinnedShortcutManager.kt new file mode 100644 index 0000000000..64bf9a8eaa --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/PinnedShortcutManager.kt @@ -0,0 +1,158 @@ +package foundation.e.blisslauncher.data.shortcuts + +import android.annotation.TargetApi +import android.content.ComponentName +import android.content.Context +import android.content.pm.LauncherApps +import android.content.pm.LauncherApps.ShortcutQuery +import android.content.pm.ShortcutInfo +import android.graphics.drawable.Drawable +import android.os.UserHandle +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.domain.keys.ShortcutKey +import timber.log.Timber +import javax.inject.Inject + +/** + * Manages shortcuts, such as querying for them, pinning them, etc. + */ +class PinnedShortcutManager @Inject constructor( + private val context: Context +) { + private var wasLastCallSuccess = false + private val launcherApps: LauncherApps = + context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + + fun wasLastCallSuccess() = wasLastCallSuccess + + /** + * Removes the given shortcut from the current list of pinned shortcuts. + * (Runs on background thread) + */ + @TargetApi(25) + fun unpinShortcut(key: ShortcutKey) { + if (Utilities.ATLEAST_NOUGAT_MR1) { + val packageName = key.componentName.getPackageName() + val id = key.getId() + val user = key.user + val pinnedIds = + extractIds(queryForPinnedShortcuts(packageName, user)) + pinnedIds.remove(id) + try { + launcherApps.pinShortcuts(packageName, pinnedIds, user) + wasLastCallSuccess = true + } catch (e: SecurityException) { + Timber.e(e, "Failed to unpin shortcut") + wasLastCallSuccess = false + } catch (e: IllegalStateException) { + Timber.e(e, "Failed to unpin shortcut") + wasLastCallSuccess = false + } + } + } + + /** + * Adds the given shortcut to the current list of pinned shortcuts. + * (Runs on background thread) + */ + @TargetApi(25) + fun pinShortcut(key: ShortcutKey) { + if (Utilities.ATLEAST_NOUGAT_MR1) { + val packageName = key.componentName.getPackageName() + val id = key.getId() + val user = key.user + val pinnedIds = + extractIds(queryForPinnedShortcuts(packageName, user)) + pinnedIds.add(id) + try { + launcherApps.pinShortcuts(packageName, pinnedIds, user) + wasLastCallSuccess = true + } catch (e: SecurityException) { + Timber.e(e, "Failed to pin shortcut") + wasLastCallSuccess = false + } catch (e: IllegalStateException) { + Timber.e(e, "Failed to pin shortcut") + wasLastCallSuccess = false + } + } + } + + @TargetApi(25) + fun getShortcutIconDrawable(shortcutInfo: ShortcutInfoCompat, density: Int): Drawable? { + if (Utilities.ATLEAST_NOUGAT_MR1) { + try { + val icon: Drawable = launcherApps.getShortcutIconDrawable( + shortcutInfo.getShortcutInfo(), density + ) + wasLastCallSuccess = true + return icon + } catch (e: SecurityException) { + Timber.e(e, "Failed to get shortcut icon") + wasLastCallSuccess = false + } catch (e: java.lang.IllegalStateException) { + Timber.e(e, "Failed to get shortcut icon") + wasLastCallSuccess = false + } + } + return null + } + + fun queryForPinnedShortcuts( + packageName: String?, + user: UserHandle? + ): List { + return query(ShortcutQuery.FLAG_MATCH_PINNED, packageName, null, null, user) + } + + @TargetApi(25) + private fun query( + flags: Int, + packageName: String?, + activity: ComponentName?, + shortcutIds: List?, + user: UserHandle? + ): List { + return if (Utilities.ATLEAST_NOUGAT_MR1) { + val q = ShortcutQuery() + q.setQueryFlags(flags) + if (packageName != null) { + q.setPackage(packageName) + q.setActivity(activity) + q.setShortcutIds(shortcutIds) + } + var shortcutInfos: List? = null + try { + shortcutInfos = launcherApps.getShortcuts(q, user) + wasLastCallSuccess = true + } catch (e: SecurityException) { + Timber.e(e, "Failed to query for shortcuts") + wasLastCallSuccess = false + } catch (e: java.lang.IllegalStateException) { + Timber.e(e, "Failed to query for shortcuts") + wasLastCallSuccess = false + } + shortcutInfos?.map { ShortcutInfoCompat(it) } ?: emptyList() + } else { + emptyList() + } + } + + @TargetApi(25) + fun hasHostPermission(): Boolean { + if (Utilities.ATLEAST_NOUGAT_MR1) { + try { + return launcherApps.hasShortcutHostPermission() + } catch (e: SecurityException) { + Timber.e(e, "Failed to make shortcut manager call") + } catch (e: java.lang.IllegalStateException) { + Timber.e(e, "Failed to make shortcut manager call") + } + } + return false + } + + private fun extractIds(shortcuts: List): MutableList { + return shortcuts.map { it.getId() }.toMutableList() + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/ShortcutsRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/ShortcutsRepositoryImpl.kt deleted file mode 100644 index c2ab24a5cf..0000000000 --- a/data/src/main/java/foundation/e/blisslauncher/data/shortcuts/ShortcutsRepositoryImpl.kt +++ /dev/null @@ -1,12 +0,0 @@ -package foundation.e.blisslauncher.data.shortcuts - -import android.content.pm.ShortcutInfo -import foundation.e.blisslauncher.domain.repository.ShortcutRepository -import io.reactivex.Single -import javax.inject.Inject - -class ShortcutsRepositoryImpl @Inject constructor() : ShortcutRepository { - override fun getAllShortcuts(): Single> { - return Single.just(emptyList()) - } -} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ShortcutKey.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ShortcutKey.kt new file mode 100644 index 0000000000..604972a988 --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/keys/ShortcutKey.kt @@ -0,0 +1,34 @@ +package foundation.e.blisslauncher.domain.keys + +import android.content.ComponentName +import android.content.Intent +import android.os.UserHandle +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.domain.entity.LauncherItem + +class ShortcutKey(componentName: ComponentName, user: UserHandle) : + ComponentKey(componentName, user) { + + constructor(packageName: String, user: UserHandle, id: String) : this( + ComponentName( + packageName, + id + ), user + ) + + fun getId() = componentName.className + + companion object { + fun fromShortcutInfoCompat(shortcutInfo: ShortcutInfoCompat): ShortcutKey = ShortcutKey( + shortcutInfo.getPackage(), shortcutInfo.getUserHandle(), shortcutInfo.getId() + ) + + fun fromIntent(intent: Intent, user: UserHandle) = ShortcutKey( + intent.`package`!!, + user, + intent.getStringExtra(ShortcutInfoCompat.EXTRA_SHORTCUT_ID) + ) + + fun fromLauncherItem(item: LauncherItem) = fromIntent(item.getIntent()!!, item.user) + } +} \ No newline at end of file -- GitLab From 699e2c874c67be54975aefef3ed04125bd6b2db0 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 10 Apr 2020 20:21:06 +0530 Subject: [PATCH 10/23] Map PinnedShortcutItem from database to domain --- .../data/LauncherItemRepositoryImpl.kt | 128 ++++++++++++++++-- .../data/PackageManagerHelper.kt | 5 +- .../domain/entity/ApplicationItem.kt | 5 +- 3 files changed, 120 insertions(+), 18 deletions(-) diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt index 5059df0557..4ab1df7db0 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt @@ -7,8 +7,10 @@ import android.os.UserHandle import android.util.LongSparseArray import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat import foundation.e.blisslauncher.common.util.MultiHashMap import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity +import foundation.e.blisslauncher.data.shortcuts.PinnedShortcutManager import foundation.e.blisslauncher.domain.entity.AppShortcutItem import foundation.e.blisslauncher.domain.entity.ApplicationItem import foundation.e.blisslauncher.domain.entity.FolderItem @@ -27,7 +29,8 @@ class LauncherItemRepositoryImpl private val launcherApps: LauncherAppsCompat, private val launcherDatabase: LauncherDatabaseGateway, private val userManager: UserManagerRepository, - private val packageManagerHelper: PackageManagerHelper + private val packageManagerHelper: PackageManagerHelper, + private val shortcutManager: PinnedShortcutManager ) : LauncherItemRepository { override fun save(entity: S): S { @@ -47,24 +50,37 @@ class LauncherItemRepositoryImpl val isSafeMode = pmHelper.isSafeMode val isSdCardReady = Utilities.isBootCompleted() val pendingPackages = MultiHashMap() - + val shortcutKeyToPinnedShortcuts = HashMap() val allUsers: LongSparseArray = LongSparseArray() val quietMode: LongSparseArray = LongSparseArray() val unlockedUsers: LongSparseArray = LongSparseArray() - userManager.userProfiles.forEach { - val serialNo = userManager.getSerialNumberForUser(it) - allUsers.put(serialNo, it) - quietMode.put(serialNo, userManager.isQuietModeEnabled(it)) + userManager.userProfiles.forEach { user -> + val serialNo = userManager.getSerialNumberForUser(user) + allUsers.put(serialNo, user) + quietMode.put(serialNo, userManager.isQuietModeEnabled(user)) + + var userUnlocked = userManager.isUserUnlocked(user) - val userUnlocked = userManager.isUserUnlocked(it) + // Query for pinned shortcuts only when user is unlocked. + if (userUnlocked) { + val pinnedShortcuts = + shortcutManager.queryForPinnedShortcuts(null, user) + if (shortcutManager.wasLastCallSuccess()) { + pinnedShortcuts.map { shortcut -> ShortcutKey.fromShortcutInfoCompat(shortcut) to shortcut } + .toMap(shortcutKeyToPinnedShortcuts) + } else { + // Shortcut Manager can fail due to various reasons. + // Consider this condition as user locked. + userUnlocked = false + } + } unlockedUsers.put(serialNo, userUnlocked) } //Populate item from database and fill necessary details based on users. val launcherItems = ArrayList() launcherDatabase.getAllWorkspaceItems() - .filter { checkAndValidate(it, unlockedUsers, isSdCardReady) } .onEach { it.apply { user = allUsers[profileId] @@ -75,13 +91,23 @@ class LauncherItemRepositoryImpl ) } } + .filter { + checkAndValidate( + it, + unlockedUsers, + shortcutKeyToPinnedShortcuts, + isSdCardReady + ) + } .mapNotNull { convertToLauncherItem( it, quietMode, isSdCardReady, isSafeMode, - pendingPackages + unlockedUsers, + pendingPackages, + shortcutKeyToPinnedShortcuts ) } .toCollection(launcherItems) @@ -92,6 +118,7 @@ class LauncherItemRepositoryImpl private fun checkAndValidate( item: LauncherItemRoomEntity, unlockedUsers: LongSparseArray, + shortcutKeyToPinnedShortcuts: HashMap, isSdcardReady: Boolean ): Boolean { @@ -149,6 +176,19 @@ class LauncherItemRepositoryImpl return false } } + + if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { + val shortcutKey = ShortcutKey.fromIntent(item.intent, item.user!!) + if (unlockedUsers.get(item.profileId)) { + val pinnedShortcut: ShortcutInfoCompat? = + shortcutKeyToPinnedShortcuts.get(shortcutKey) + if (pinnedShortcut == null) { + // The shortcut is no longer valid. + launcherDatabase.markDeleted(item) + return false + } + } + } } } return true @@ -159,7 +199,9 @@ class LauncherItemRepositoryImpl quietMode: LongSparseArray, isSdcardReady: Boolean, isSafeMode: Boolean, - pendingPackages: MultiHashMap + unlockedUsers: LongSparseArray, + pendingPackages: MultiHashMap, + shortcutKeyToPinnedShortcuts: HashMap ): LauncherItem? { var allowMissingTarget = false // Load necessary properties. @@ -190,10 +232,44 @@ class LauncherItemRepositoryImpl item.user!!, quietMode[item.profileId], item.intent!!, - allowMissingTarget + allowMissingTarget, + item.title ) } else if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) { val shortcutKey = ShortcutKey.fromIntent(item.intent!!, item.user!!) + if (unlockedUsers.get(item.profileId)) { + val pinnedShortcut = shortcutKeyToPinnedShortcuts[shortcutKey] + if (pinnedShortcut != null) { + launcherItem = AppShortcutItem() + .apply { + this.user = item.user!! + this.itemType = LauncherConstants.ItemType.DEEP_SHORTCUT + this.intent = pinnedShortcut.makeIntent() + this.title = pinnedShortcut.getShortLabel() + this.runtimeStatusFlags = if (pinnedShortcut.isEnabled()) { + this.runtimeStatusFlags and LauncherItemWithIcon.FLAG_DISABLED_BY_PUBLISHER.inv() + } else { + this.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_BY_PUBLISHER + } + this.disabledMessage = pinnedShortcut.getDisabledMessage() + } + //TODO: Set Icon here + if(packageManagerHelper.isAppSuspended(pinnedShortcut.getPackage(), launcherItem.user)) { + launcherItem.runtimeStatusFlags = + launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED + } + } + } else { + launcherItem = AppShortcutItem() + .apply { + this.user = item.user!! + this.itemType = item.itemType + this.title = if (item.title.isEmpty()) "" else item.title + // TODO: Set Icon here + } + launcherItem.runtimeStatusFlags = + launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_LOCKED_USER + } } else { launcherItem = AppShortcutItem() .apply { @@ -202,6 +278,7 @@ class LauncherItemRepositoryImpl this.title = if (item.title.isEmpty()) "" else item.title // TODO: Set Icon here } + val intent = item.intent!! if (intent.action != null && intent.categories != null && @@ -245,7 +322,8 @@ class LauncherItemRepositoryImpl user: UserHandle, quietMode: Boolean, intent: Intent, - allowMissingTarget: Boolean + allowMissingTarget: Boolean, + title: String? ): AppShortcutItem? { val componentName = intent.component if (componentName == null) { @@ -263,8 +341,30 @@ class LauncherItemRepositoryImpl return null } - return ApplicationItem(lai!!, user, quietMode) - .apply { itemType = LauncherConstants.ItemType.APPLICATION } + + return if (lai != null) { + val applicationItem = ApplicationItem(lai, user, quietMode) + .apply { itemType = LauncherConstants.ItemType.APPLICATION } + val isSuspended = packageManagerHelper.isAppSuspended(lai.applicationInfo) + if (isSuspended) { + applicationItem.runtimeStatusFlags = + applicationItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED + } + applicationItem + } else { + val applicationItem = ApplicationItem() + .apply { + this.user = user + this.intent = newIntent + this.componentName = componentName + this.title = title + } + + if (applicationItem.title == null) { + applicationItem.title = componentName.className + } + applicationItem + } } override fun delete(entity: LauncherItem) { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt index a62203dcec..acfc915215 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt @@ -9,9 +9,9 @@ import android.content.pm.ResolveInfo import android.os.Build import android.os.UserHandle import android.text.TextUtils +import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.domain.entity.ApplicationItem -import javax.inject.Inject class PackageManagerHelper( context: Context, @@ -48,6 +48,9 @@ class PackageManagerHelper( return info != null && info.flags and ApplicationInfo.FLAG_SUSPENDED != 0 } + fun isAppSuspended(info: ApplicationInfo): Boolean = + info.flags and ApplicationInfo.FLAG_SUSPENDED != 0 + fun getAppLaunchIntent(pkg: String, user: UserHandle): Intent? { val activities = launcherApps.getActivityList(pkg, user) return if (activities.isEmpty()) null else diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt index e5386e16aa..d32f19b3e3 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt @@ -34,6 +34,7 @@ open class ApplicationItem : AppShortcutItem { if (quietModeEnabled) { runtimeStatusFlags = runtimeStatusFlags or FLAG_DISABLED_QUIET_USER } + updateRuntimeFlagsForActivityTarget(this, info) } @@ -60,9 +61,7 @@ open class ApplicationItem : AppShortcutItem { lai: LauncherActivityInfo ) { val appInfo = lai.applicationInfo - /*if (PackageManagerHelper.isAppSuspended(appInfo)) { - info.runtimeStatusFlags = info.runtimeStatusFlags or FLAG_DISABLED_SUSPENDED - }*/ + info.runtimeStatusFlags = info.runtimeStatusFlags or if (appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0) FLAG_SYSTEM_NO else FLAG_SYSTEM_YES if (Utilities.ATLEAST_OREO && -- GitLab From 7b29dfc668687732fdbc4443a2950b25d39ddf16 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 10 Apr 2020 21:05:25 +0530 Subject: [PATCH 11/23] Make domain layer build passing --- .gitlab-ci.yml | 11 ++++------- .../blisslauncher/common/compat/ShortcutInfoCompat.kt | 2 +- .../e/blisslauncher/domain/dto/WorkspaceModel.kt | 2 -- .../e/blisslauncher/domain/entity/AppShortcutItem.kt | 2 +- .../domain/entity/LauncherItemWithIcon.kt | 1 - .../e/blisslauncher/domain/interactor/AddPackages.kt | 2 +- .../domain/interactor/ChangeUserAvailability.kt | 4 ++-- .../e/blisslauncher/domain/interactor/LoadLauncher.kt | 1 - .../domain/interactor/MakePackageUnavailable.kt | 4 ++-- .../blisslauncher/domain/interactor/RemovePackages.kt | 2 +- .../domain/interactor/SuspendPackages.kt | 4 ++-- .../domain/interactor/UnsuspendPackages.kt | 4 ++-- .../domain/repository/LauncherItemRepository.kt | 4 +--- 13 files changed, 17 insertions(+), 26 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0601958f2e..bc136dc558 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ image: "registry.gitlab.e.foundation:5000/e/apps/docker-android-apps-cicd:latest" stages: -- build +- build-domain before_script: - export GRADLE_USER_HOME=$(pwd)/.gradle @@ -12,10 +12,7 @@ cache: paths: - .gradle/ -build: - stage: build +build-domain: + stage: build-domain script: - - ./gradlew build - artifacts: - paths: - - app/build/outputs/apk + - ./gradlew :domain:build diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt index 99d10d7fa7..e2553548fa 100644 --- a/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt @@ -46,7 +46,7 @@ class ShortcutInfoCompat(private val shortcutInfo: ShortcutInfo) { fun getLastChangedTimestamp(): Long = shortcutInfo.getLastChangedTimestamp() - fun getActivity(): ComponentName = shortcutInfo.getActivity() + fun getActivity(): ComponentName? = shortcutInfo.getActivity() fun getUserHandle(): UserHandle = shortcutInfo.getUserHandle() diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt index 02f4f419ee..6d3e8e34a6 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt @@ -3,7 +3,6 @@ package foundation.e.blisslauncher.domain.dto import foundation.e.blisslauncher.common.util.LongArrayMap import foundation.e.blisslauncher.domain.entity.FolderItem import foundation.e.blisslauncher.domain.entity.LauncherItem -import foundation.e.blisslauncher.domain.entity.WorkspaceScreen data class WorkspaceModel( val itemsIdMap: LongArrayMap = LongArrayMap(), @@ -11,5 +10,4 @@ data class WorkspaceModel( val folders: LongArrayMap = LongArrayMap(), val workspaceScreens: ArrayList = ArrayList() - ) \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt index afe751efd8..98d712d359 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt @@ -83,7 +83,7 @@ open class AppShortcutItem : LauncherItemWithIcon { * The shortcut was restored from a backup and it not ready to be used. This is automatically * set during backup/restore */ - const val FLAG_RESTORED_ICON = 1 + const val FLAG_RESTORED_ICON = 1 /** * The icon was added as an auto-install app, and is not ready to be used. This flag can't diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt index a8a5bfe6e0..b62c30910c 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItemWithIcon.kt @@ -1,6 +1,5 @@ package foundation.e.blisslauncher.domain.entity -import android.content.Intent import android.graphics.Bitmap /** diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt index 56309236bc..1bbe2d4a6f 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/AddPackages.kt @@ -22,7 +22,7 @@ class AddPackages @Inject constructor( override fun doWork(params: Params): Completable = Completable.fromAction { params.packages.forEach { //TODO: Update icons cache - launcherItemRepository.add(it, params.user, userManager.isQuietModeEnabled(params.user)) + //launcherItemRepository.add(it, params.user, userManager.isQuietModeEnabled(params.user)) //TODO: Add SessionCommitReceiver for below O devices } } /*.doOnComplete { diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt index 03b92be8ef..fd88941047 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ChangeUserAvailability.kt @@ -18,11 +18,11 @@ class ChangeUserAvailability @Inject constructor( override val subscribeExecutor: Executor = appExecutors.io override fun doWork(params: UserHandle): Completable = Completable.fromAction { - observeUpdatedLauncherItems( + /*observeUpdatedLauncherItems( launcherItemRepository.updateUserAvailability( params, userManager.isQuietModeEnabled(params) ) - ) + )*/ } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt index ea45170595..617e6d2ab8 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt @@ -4,7 +4,6 @@ import foundation.e.blisslauncher.common.executors.AppExecutors import foundation.e.blisslauncher.domain.dto.WorkspaceModel import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_DESKTOP import foundation.e.blisslauncher.domain.repository.LauncherItemRepository -import foundation.e.blisslauncher.domain.repository.UserManagerRepository import foundation.e.blisslauncher.domain.repository.WorkspaceScreenRepository import io.reactivex.Single import timber.log.Timber diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt index 3ea778097b..27c462b677 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/MakePackageUnavailable.kt @@ -18,11 +18,11 @@ class MakePackageUnavailable @Inject constructor( override val subscribeExecutor: Executor = appExecutors.io override fun doWork(params: Params): Completable = Completable.fromAction { - observeUpdatedLauncherItems( + /*observeUpdatedLauncherItems( launcherItemRepository.makePackagesUnavailable( params.packages, params.user ) - ) + )*/ } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt index ed4caf8aba..edc2554d10 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/RemovePackages.kt @@ -19,7 +19,7 @@ class RemovePackages @Inject constructor( override fun doWork(params: Params): Completable = Completable.fromAction { - launcherItemRepository.removePackages(params.packages, params.user) + //launcherItemRepository.removePackages(params.packages, params.user) }.doOnComplete { //observeAddedApps(Unit) } diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt index f1fd183d15..0ce960ccf2 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/SuspendPackages.kt @@ -18,11 +18,11 @@ class SuspendPackages @Inject constructor( override val subscribeExecutor: Executor = appExecutors.io override fun doWork(params: Params): Completable = Completable.fromAction { - observeUpdatedLauncherItems( + /*observeUpdatedLauncherItems( launcherItemRepository.suspendPackages( params.packages, params.user ) - ) + )*/ } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt index 06e6ee541f..c09428bdac 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UnsuspendPackages.kt @@ -18,11 +18,11 @@ class UnsuspendPackages @Inject constructor( override val subscribeExecutor: Executor = appExecutors.io override fun doWork(params: Params): Completable = Completable.fromAction { - observeUpdatedLauncherItems( + /*observeUpdatedLauncherItems( launcherItemRepository.unsuspendPackages( params.packages, params.user ) - ) + )*/ } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt index 19a91d9116..5d6beaaa0d 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/LauncherItemRepository.kt @@ -1,13 +1,11 @@ package foundation.e.blisslauncher.domain.repository -import android.os.UserHandle -import foundation.e.blisslauncher.domain.entity.ApplicationItem import foundation.e.blisslauncher.domain.entity.LauncherItem /** * Repository to manage [LauncherItem] */ -interface LauncherItemRepository: Repository +interface LauncherItemRepository : Repository /*fun getAllActivities(user: UserHandle, quietMode: Boolean): List -- GitLab From 9ee37bd9313e0caea9ce03010bf2abed41d9e5f6 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 10 Apr 2020 21:09:36 +0530 Subject: [PATCH 12/23] Delete test as of now --- .../interactor/LoadAllAppsInteractorTest.kt | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt diff --git a/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt b/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt deleted file mode 100644 index b0486ed161..0000000000 --- a/domain/src/test/java/foundation/e/blisslauncher/domain/interactor/LoadAllAppsInteractorTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package foundation.e.blisslauncher.domain.interactor - -import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.common.executors.MainThreadExecutor -import foundation.e.blisslauncher.domain.repository.LauncherItemRepository -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.verify -import io.reactivex.Single -import org.junit.After -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import java.util.concurrent.Executors - -class LoadAllAppsInteractorTest { - - private lateinit var loadAllAppsInteractor: LoadAllAppsInteractor - lateinit var launcherItemRepository: LauncherItemRepository - lateinit var appExecutors: AppExecutors - - @MockK - lateinit var mainThreadExecutor: MainThreadExecutor - - @Before - fun setUp() { - launcherItemRepository = mockk { - every { - getAllApps() - } returns Single.just(listOf( - mockk { - every { - title = "App1" - } - }, - mockk { - every { - title = "App2" - } - } - )) - } - MockKAnnotations.init(this) - appExecutors = AppExecutors( - Executors.newSingleThreadExecutor(), - Executors.newSingleThreadExecutor(), - mainThreadExecutor - ) - loadAllAppsInteractor = LoadAllAppsInteractor(launcherItemRepository, appExecutors) - } - - @After - fun tearDown() { - } - - @Test - fun doWorkCallsRepository() { - launcherItemRepository = mockk { - every { - getAllApps() - } returns Single.just(listOf( - mockk { - every { - title = "App1" - } - }, - mockk { - every { - title = "App2" - } - } - )) - } - loadAllAppsInteractor.doWork() - verify(exactly = 1) { launcherItemRepository.getAllApps() } - } - - @Test - fun invokeCallsRepositoryAndCompletes() { - - loadAllAppsInteractor() - verify(exactly = 1) { launcherItemRepository.getAllApps() } - - loadAllAppsInteractor(onSuccess = { Assert.assertEquals(2, it.size) }) - } -} \ No newline at end of file -- GitLab From c80e4ff079848ca5ac8994e8c4eab134ca33639c Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 10 Apr 2020 21:15:56 +0530 Subject: [PATCH 13/23] Delete test entites as of now --- .../data/inject/DataRepoBindingModule.kt | 23 +------------------ .../domain/entity/LauncherItemTest.kt | 18 --------------- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 domain/src/test/java/foundation/e/blisslauncher/domain/entity/LauncherItemTest.kt diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt index 03c2a677a6..c9d6d76486 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt @@ -3,17 +3,8 @@ package foundation.e.blisslauncher.data.inject import dagger.Module import dagger.Provides import foundation.e.blisslauncher.data.LauncherStateManagerImpl -import foundation.e.blisslauncher.data.apps.AppsRepositoryImpl -import foundation.e.blisslauncher.data.launcher.LauncherRepositoryImpl -import foundation.e.blisslauncher.data.shortcuts.ShortcutsRepositoryImpl -import foundation.e.blisslauncher.data.widgets.WidgetsRepositoryImpl -import foundation.e.blisslauncher.data.workspace.WorkspaceRepositoryImpl import foundation.e.blisslauncher.domain.manager.LauncherStateManager -import foundation.e.blisslauncher.domain.repository.AppsRepository import foundation.e.blisslauncher.domain.repository.LauncherItemRepository -import foundation.e.blisslauncher.domain.repository.ShortcutRepository -import foundation.e.blisslauncher.domain.repository.WidgetsRepository -import foundation.e.blisslauncher.domain.repository.WorkspaceRepository @Module class DataRepoBindingModule { @@ -22,17 +13,5 @@ class DataRepoBindingModule { fun bindLauncherStateManager(launcherStateManagerImpl: LauncherStateManagerImpl): LauncherStateManager = launcherStateManagerImpl @Provides - fun bindAppsRepository(appsRepositoryImpl: AppsRepositoryImpl): AppsRepository = appsRepositoryImpl - - @Provides - fun bindShortcutRepository(shortcutRepositoryImpl: ShortcutsRepositoryImpl): ShortcutRepository = shortcutRepositoryImpl - - @Provides - fun bindLauncherRepository(launcherRepositoryImpl: LauncherRepositoryImpl): LauncherItemRepository = launcherRepositoryImpl - - @Provides - fun bindWorkspaceRepository(workspaceRepositoryImpl: WorkspaceRepositoryImpl): WorkspaceRepository = workspaceRepositoryImpl - - @Provides - fun bindWidgetsRepository(widgetsRepositoryImpl: WidgetsRepositoryImpl): WidgetsRepository = widgetsRepositoryImpl + fun bindLauncherRepository(launcherRepositoryImpl: LauncherItemRepository): LauncherItemRepository = launcherRepositoryImpl } \ No newline at end of file diff --git a/domain/src/test/java/foundation/e/blisslauncher/domain/entity/LauncherItemTest.kt b/domain/src/test/java/foundation/e/blisslauncher/domain/entity/LauncherItemTest.kt deleted file mode 100644 index b787b515c2..0000000000 --- a/domain/src/test/java/foundation/e/blisslauncher/domain/entity/LauncherItemTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package foundation.e.blisslauncher.domain.entity - -import org.junit.Test - -class LauncherItemTest { - - lateinit var launcherItem: LauncherItem - - @org.junit.Before - fun setUp() { - launcherItem = LauncherItem() - } - - @Test - fun testDumpProperties() { - launcherItem.toString() - } -} \ No newline at end of file -- GitLab From 501859f7e32c416daa678d7f10916ee43a567d56 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 10 Apr 2020 21:40:52 +0530 Subject: [PATCH 14/23] Make data layer passing for initial run --- .../data/LauncherDatabaseGateway.kt | 23 +++++++++++-------- .../data/LauncherItemRepositoryImpl.kt | 9 +++++--- .../data/PackageManagerHelper.kt | 1 - .../data/database/BlissLauncherDatabase.kt | 8 +++++-- .../data/database/IconDatabase.kt | 3 ++- .../data/database/dao/IconDao.kt | 2 +- .../data/database/dao/LauncherItemDao.kt | 6 +++++ .../roomentity/LauncherItemRoomEntity.kt | 7 ++++++ .../data/widgets/WidgetsRepositoryImpl.kt | 6 ----- .../domain/inject/DomainComponent.kt | 4 ---- .../interactor/ObserveUpdatedLauncherItems.kt | 3 ++- 11 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherItemDao.kt delete mode 100644 data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt index c9c29bda26..a3a001fcf7 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -1,23 +1,26 @@ package foundation.e.blisslauncher.data import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity +import javax.inject.Inject -interface LauncherDatabaseGateway { - fun createEmptyDatabase() +class LauncherDatabaseGateway @Inject constructor() { + fun createEmptyDatabase() {} - fun generateNewItemId() + fun generateNewItemId() {} - fun generateNewScreenId() + fun generateNewScreenId() {} - fun deleteEmptyFolders() + fun deleteEmptyFolders() {} - fun loadDefaultWorkspace() + fun loadDefaultWorkspace() {} - fun getAllWorkspaceItems(): List + fun getAllWorkspaceItems(): List = emptyList() - fun loadWorkspaceScreensInOrder(): List + fun loadWorkspaceScreensInOrder(): List = emptyList() - fun markDeleted(id: Long) + fun markDeleted(id: Long) {} - fun markDeleted(item: LauncherItemRoomEntity) + fun markDeleted(item: LauncherItemRoomEntity) { + markDeleted(item._id) + } } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt index 4ab1df7db0..299b400690 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt @@ -254,8 +254,12 @@ class LauncherItemRepositoryImpl this.disabledMessage = pinnedShortcut.getDisabledMessage() } //TODO: Set Icon here - if(packageManagerHelper.isAppSuspended(pinnedShortcut.getPackage(), launcherItem.user)) { - launcherItem.runtimeStatusFlags = + if (packageManagerHelper.isAppSuspended( + pinnedShortcut.getPackage(), + launcherItem.user + ) + ) { + launcherItem.runtimeStatusFlags = launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED } } @@ -341,7 +345,6 @@ class LauncherItemRepositoryImpl return null } - return if (lai != null) { val applicationItem = ApplicationItem(lai, user, quietMode) .apply { itemType = LauncherConstants.ItemType.APPLICATION } diff --git a/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt index acfc915215..4ab291eed2 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt @@ -9,7 +9,6 @@ import android.content.pm.ResolveInfo import android.os.Build import android.os.UserHandle import android.text.TextUtils -import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.domain.entity.ApplicationItem diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt index 69c0266a93..dcecf2e6a1 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt @@ -2,6 +2,10 @@ package foundation.e.blisslauncher.data.database import androidx.room.Database import androidx.room.RoomDatabase +import foundation.e.blisslauncher.data.database.dao.LauncherItemDao +import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity -@Database(entities = [], version = 1) -abstract class BlissLauncherDatabase : RoomDatabase() \ No newline at end of file +@Database(entities = [LauncherItemRoomEntity::class], version = 1) +abstract class BlissLauncherDatabase : RoomDatabase() { + abstract fun launcherDao(): LauncherItemDao +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt index f928838e00..931166761b 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/IconDatabase.kt @@ -3,8 +3,9 @@ package foundation.e.blisslauncher.data.database import androidx.room.Database import androidx.room.RoomDatabase import foundation.e.blisslauncher.data.database.dao.IconDao +import foundation.e.blisslauncher.data.database.roomentity.IconEntity -@Database(entities = [IconDatabase::class], version = 1) +@Database(entities = [IconEntity::class], version = 1) abstract class IconDatabase : RoomDatabase() { abstract fun iconDao(): IconDao } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt index bca5ee25c7..9a0a9506ef 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt @@ -11,7 +11,7 @@ interface IconDao { @Query("DELETE FROM icons WHERE componentName LIKE :componentName AND profileId = :userSerial") fun delete(componentName: String, userSerial: Int) - @Query("DROP TABLE IF EXISTS icons") + @Query("DELETE FROM icons") fun clear() @Query("SELECT * FROM icons WHERE profileId = :userSerial") diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherItemDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherItemDao.kt new file mode 100644 index 0000000000..3715283269 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherItemDao.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.data.database.dao + +import androidx.room.Dao + +@Dao +interface LauncherItemDao \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt index d0ce2f338b..e6cb7e5652 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt @@ -6,6 +6,7 @@ import android.os.UserHandle import androidx.annotation.NonNull import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey import foundation.e.blisslauncher.domain.entity.LauncherItem import timber.log.Timber @@ -33,10 +34,16 @@ data class LauncherItemRoomEntity( val profileId: Long ) { // Properties to initialise for proper validation + + @Ignore val targetPackage: String? + @Ignore val intent: Intent? + @Ignore val componentName: ComponentName? + @Ignore var user: UserHandle? = null + @Ignore var validTarget: Boolean = false init { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt deleted file mode 100644 index bd7208b055..0000000000 --- a/data/src/main/java/foundation/e/blisslauncher/data/widgets/WidgetsRepositoryImpl.kt +++ /dev/null @@ -1,6 +0,0 @@ -package foundation.e.blisslauncher.data.widgets - -import foundation.e.blisslauncher.domain.repository.WidgetsRepository -import javax.inject.Inject - -class WidgetsRepositoryImpl @Inject constructor() : WidgetsRepository \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt index 52e1502656..e7ed7c2f5d 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt @@ -11,10 +11,6 @@ import foundation.e.blisslauncher.domain.repository.LauncherItemRepository */ interface DomainComponent { - fun launcherStateManager(): LauncherStateManager - - fun launcherRepository(): LauncherItemRepository - fun appExecutors(): AppExecutors fun launcherAppsCompat(): LauncherAppsCompat diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt index 417032036c..4006bcb241 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/ObserveUpdatedLauncherItems.kt @@ -4,8 +4,9 @@ import foundation.e.blisslauncher.common.executors.AppExecutors import foundation.e.blisslauncher.domain.entity.LauncherItem import io.reactivex.Flowable import java.util.concurrent.Executor +import javax.inject.Inject -class ObserveUpdatedLauncherItems(appExecutors: AppExecutors) : +class ObserveUpdatedLauncherItems @Inject constructor(appExecutors: AppExecutors) : PublishSubjectInteractor, List>() { override val subscribeExecutor: Executor = appExecutors.io override val observeExecutor: Executor = appExecutors.main -- GitLab From 84aac9ea4e1e5c1551656f505d530cf68de96c0f Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 10 Apr 2020 21:41:55 +0530 Subject: [PATCH 15/23] Add data layer build script into gitlab CI --- .gitlab-ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bc136dc558..8df729f106 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ image: "registry.gitlab.e.foundation:5000/e/apps/docker-android-apps-cicd:latest stages: - build-domain +- build-data before_script: - export GRADLE_USER_HOME=$(pwd)/.gradle @@ -16,3 +17,8 @@ build-domain: stage: build-domain script: - ./gradlew :domain:build + +build-data: + stage: build-data + script: + - ./gradlew :data:build -- GitLab From 32b6a18ab6cfe74424c42d786637a8858e0f4e92 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 10 Apr 2020 21:43:00 +0530 Subject: [PATCH 16/23] Fix spotless violation --- .../foundation/e/blisslauncher/domain/inject/DomainComponent.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt index e7ed7c2f5d..9d23f26b78 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt @@ -2,8 +2,6 @@ package foundation.e.blisslauncher.domain.inject import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.domain.manager.LauncherStateManager -import foundation.e.blisslauncher.domain.repository.LauncherItemRepository /** * Interface that lists all public repositories and data access layer components which are needed -- GitLab From fd4d0d2d42a07de675ce97fbb7c8488ab6ff210a Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 29 Apr 2020 12:24:46 +0530 Subject: [PATCH 17/23] Move MVI to separate layer --- mvicore/.gitignore | 1 + mvicore/build.gradle | 14 +++++ .../e/blisslauncher/mvicore/MyClass.kt | 4 ++ .../blisslauncher/mvicore/component/Actor.kt | 5 ++ .../mvicore/component/BaseModel.kt | 62 +++++++++++++++++++ .../mvicore/component/EventPublisher.kt | 4 ++ .../mvicore/component/IntentToAction.kt | 3 + .../blisslauncher/mvicore/component/Model.kt | 9 +++ .../mvicore/component/MviView.kt | 10 +++ .../mvicore/component/Reducer.kt | 6 ++ .../mvicore/util/SameThreadVerifier.kt | 16 +++++ settings.gradle.kts | 2 +- 12 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 mvicore/.gitignore create mode 100644 mvicore/build.gradle create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/MyClass.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Actor.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseModel.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/EventPublisher.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/IntentToAction.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Model.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Reducer.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/util/SameThreadVerifier.kt diff --git a/mvicore/.gitignore b/mvicore/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/mvicore/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mvicore/build.gradle b/mvicore/build.gradle new file mode 100644 index 0000000000..fc229d6e47 --- /dev/null +++ b/mvicore/build.gradle @@ -0,0 +1,14 @@ +import foundation.e.blisslauncher.buildsrc.Libs + +apply plugin: 'java' +apply plugin: 'kotlin' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation Libs.Kotlin.stdlib + + implementation Libs.RxJava.rxKotlin +} + +sourceCompatibility = "8" +targetCompatibility = "8" diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/MyClass.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/MyClass.kt new file mode 100644 index 0000000000..c32bf2537f --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/MyClass.kt @@ -0,0 +1,4 @@ +package foundation.e.blisslauncher.mvicore + +public class MyClass { +} diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Actor.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Actor.kt new file mode 100644 index 0000000000..2fab8461f7 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Actor.kt @@ -0,0 +1,5 @@ +package foundation.e.blisslauncher.mvicore.component + +import io.reactivex.Observable + +typealias Actor = (State, Action) -> Observable \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseModel.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseModel.kt new file mode 100644 index 0000000000..91459c8662 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseModel.kt @@ -0,0 +1,62 @@ +package foundation.e.blisslauncher.mvicore.component + +import foundation.e.blisslauncher.mvicore.util.SameThreadVerifier +import io.reactivex.Observer +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.functions.Consumer +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject + +open class BaseModel( + initialState: State, + private val intentToAction: IntentToAction, + actor: Actor, + reducer: Reducer, + eventPublisher: EventPublisher? = null +) : Model, Disposable { + + private val threadVerifier = SameThreadVerifier() + private val actionSubject = PublishSubject.create() + private val stateSubject = PublishSubject.create() + private val eventSubject = PublishSubject.create() + + private val disposable = CompositeDisposable() + + private val eventPublisherWrapper = eventPublisher?.let { + EventPublisherWrapper(eventPublisher, eventSubject) + } + + init { + disposable += eventPublisherWrapper + } + + override fun accept(t: Intent) { + TODO("Not yet implemented") + } + + override fun subscribe(observer: Observer) { + TODO("Not yet implemented") + } + + override val state: State + get() = TODO("Not yet implemented") + + override fun isDisposed(): Boolean = disposable.isDisposed + + override fun dispose() = disposable.dispose() + + private class EventPublisherWrapper( + private val eventPublisher: EventPublisher, + private val events: Subject + ) : Consumer>, Disposable { + + override fun accept(t: Triple) { + val (action, effect, state) = t + eventPublisher.invoke(action, effect, state)?.let { + events.onNext(it) + } + } + } +} \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/EventPublisher.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/EventPublisher.kt new file mode 100644 index 0000000000..c4c0cf84c2 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/EventPublisher.kt @@ -0,0 +1,4 @@ +package foundation.e.blisslauncher.mvicore.component + +typealias EventPublisher = + (action: Action, effect: Effect, state: State) -> Event? \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/IntentToAction.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/IntentToAction.kt new file mode 100644 index 0000000000..d7e33a3f21 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/IntentToAction.kt @@ -0,0 +1,3 @@ +package foundation.e.blisslauncher.mvicore.component + +typealias IntentToAction = (intent: Intent) -> Action \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Model.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Model.kt new file mode 100644 index 0000000000..cd1efe6b14 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Model.kt @@ -0,0 +1,9 @@ +package foundation.e.blisslauncher.mvicore.component + +import io.reactivex.ObservableSource +import io.reactivex.functions.Consumer + +interface Model : Consumer, + ObservableSource { + val state: State +} \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt new file mode 100644 index 0000000000..8dc156898a --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.mvicore.component + +import io.reactivex.Observable + +interface MviView { + + val events: Observable + + fun render(model: ViewModel) +} \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Reducer.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Reducer.kt new file mode 100644 index 0000000000..c04b4eb8d6 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Reducer.kt @@ -0,0 +1,6 @@ +package foundation.e.blisslauncher.mvicore.component + +/** + * Reducer function which takes current state, applies an effect to it and produce a new state. + */ +typealias Reducer = (state: State, effect: Effect) -> State \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/util/SameThreadVerifier.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/util/SameThreadVerifier.kt new file mode 100644 index 0000000000..1e18521c2b --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/util/SameThreadVerifier.kt @@ -0,0 +1,16 @@ +package foundation.e.blisslauncher.mvicore.util + +class SameThreadVerifier { + + companion object { + var isEnabled: Boolean = true + } + + private val originalThread = Thread.currentThread().id + + fun verify() { + if (isEnabled && (Thread.currentThread().id != originalThread)) { + throw AssertionError("Not on same thread as previous verification") + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4baf46bf45..4657f28498 100755 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -include(":app", ":common", ":blisslauncherv2", ":data-bridge", ":data", ":domain") \ No newline at end of file +include(":app", ":common", ":blisslauncherv2", ":data-bridge", ":data", ":domain", ":mvicore") \ No newline at end of file -- GitLab From 866946313fcc4c8acc772b38c5b1e990705a213d Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 5 May 2020 15:53:41 +0530 Subject: [PATCH 18/23] Move MVI to separate module --- blisslauncherv2/build.gradle | 1 + .../e/blisslauncher/base/BaseActivity.kt | 2 +- .../base/presentation/BaseIntent.kt | 33 --------- .../base/presentation/BaseView.kt | 10 --- .../base/presentation/BaseViewEvent.kt | 3 - .../base/presentation/BaseViewModel.kt | 33 --------- .../base/presentation/BaseViewState.kt | 3 - .../blisslauncher/base/presentation/Model.kt | 22 ------ .../e/blisslauncher/common/Functions.kt | 8 --- .../e/blisslauncher/features/LauncherStore.kt | 32 +++++++++ .../features/launcher/LauncherActivity.kt | 2 +- .../launcher/LauncherActivityModule.kt | 2 +- .../features/launcher/LauncherState.kt | 5 +- .../features/launcher/LauncherView.kt | 11 +-- .../features/launcher/LauncherViewEvent.kt | 7 -- .../features/launcher/LauncherViewModel.kt | 57 --------------- .../launcher/{ => views}/PagedView.java | 4 +- .../pageindicators/PageIndicator.kt | 2 +- .../pageindicators/PageIndicatorDots.kt | 2 +- .../util => utils}/SystemUiController.kt | 2 +- .../{common/util => utils}/TraceHelper.kt | 2 +- .../{common/util => utils}/LongArrayMap.kt | 2 +- .../{common/util => utils}/MultiHashMap.java | 2 +- .../data/LauncherItemRepositoryImpl.kt | 2 +- .../data/compat/UserManagerCompatVN.kt | 2 +- .../e/blisslauncher/domain/Functions.kt | 2 +- .../domain/dto/WorkspaceModel.kt | 2 +- .../domain/interactor/UpdateLauncher.kt | 2 +- .../e/blisslauncher/mvicore/MyClass.kt | 4 -- .../blisslauncher/mvicore/component/Actor.kt | 5 -- .../mvicore/component/BaseModel.kt | 62 ---------------- .../mvicore/component/BaseStore.kt | 70 +++++++++++++++++++ .../mvicore/component/EventPublisher.kt | 4 -- .../mvicore/component/IntentToAction.kt | 3 - .../mvicore/component/MviView.kt | 5 +- .../mvicore/component/Reducer.kt | 6 -- .../mvicore/component/{Model.kt => Store.kt} | 6 +- .../mvicore/component/Typealiases.kt | 24 +++++++ 38 files changed, 159 insertions(+), 287 deletions(-) delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewEvent.kt delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewState.kt delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/Model.kt delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/{ => views}/PagedView.java (99%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/{ => views}/pageindicators/PageIndicator.kt (73%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/{ => views}/pageindicators/PageIndicatorDots.kt (99%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{common/util => utils}/SystemUiController.kt (97%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{common/util => utils}/TraceHelper.kt (97%) rename common/src/main/java/foundation/e/blisslauncher/{common/util => utils}/LongArrayMap.kt (94%) rename common/src/main/java/foundation/e/blisslauncher/{common/util => utils}/MultiHashMap.java (96%) delete mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/MyClass.kt delete mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Actor.kt delete mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseModel.kt create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt delete mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/EventPublisher.kt delete mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/IntentToAction.kt delete mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Reducer.kt rename mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/{Model.kt => Store.kt} (50%) create mode 100644 mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Typealiases.kt diff --git a/blisslauncherv2/build.gradle b/blisslauncherv2/build.gradle index 9971f993ae..ab09ee0144 100644 --- a/blisslauncherv2/build.gradle +++ b/blisslauncherv2/build.gradle @@ -43,6 +43,7 @@ dependencies { implementation project(path: ':common') implementation project(path: ':data-bridge') implementation project(path: ':domain') + implementation project(path: ':mvicore') implementation Libs.Kotlin.stdlib implementation Libs.AndroidX.appcompat implementation Libs.AndroidX.recyclerview diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt index a84288f1ad..c4f2e60d64 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import android.content.ContextWrapper import androidx.annotation.IntDef -import foundation.e.blisslauncher.common.util.SystemUiController +import foundation.e.blisslauncher.utils.SystemUiController import javax.inject.Inject open class BaseActivity : Activity() { diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt deleted file mode 100644 index 9d5fdfbd58..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseIntent.kt +++ /dev/null @@ -1,33 +0,0 @@ -package foundation.e.blisslauncher.base.presentation - -/** - * Intent that are used to change state. - * It can either reduce to a new state or another intent which resolves the new state. - */ -interface BaseIntent { - fun reduce(oldState: T): T -} - -typealias StateReducer = T.() -> T -typealias UnitReducer = T.() -> Unit - -/** - * - * NOTE: Magic of extension functions, (T)->T and T.()->T interchangeable. - */ -fun intent(block: StateReducer): BaseIntent = object : - BaseIntent { - override fun reduce(oldState: T): T = block(oldState) -} - -/** - * By delegating work to other models, repositories or services, we - * end up with situations where we don't need to update our ModelStore - * state until the delegated work completes. - * - * Use the `sideEffect {}` DSL function for those situations. - */ -fun sideEffect(block: UnitReducer): BaseIntent = object : - BaseIntent { - override fun reduce(oldState: T): T = oldState.apply(block) -} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt deleted file mode 100644 index 1b0473c9f6..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseView.kt +++ /dev/null @@ -1,10 +0,0 @@ -package foundation.e.blisslauncher.base.presentation - -import io.reactivex.Observable - -interface BaseView { - - fun intents(): Observable> - - fun render(state: State) -} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewEvent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewEvent.kt deleted file mode 100644 index 37b674ab70..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package foundation.e.blisslauncher.base.presentation - -interface BaseViewEvent \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt deleted file mode 100644 index 9260f96459..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -package foundation.e.blisslauncher.base.presentation - -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.subjects.PublishSubject -import timber.log.Timber - -abstract class BaseViewModel(initialState: S) : - Model { - - /** - * Used to process events and state reducers - */ - private val intents = PublishSubject.create>() - - private val store = intents - .observeOn(AndroidSchedulers.mainThread()) - .scan(initialState) { oldState, intent -> intent.reduce(oldState) } - .replay(1) - .apply { connect() } - - private val internalLogger = store.subscribe({ Timber.i("$it") }, { throw it }) - - override fun process(intent: BaseIntent) = intents.onNext(intent) - - fun newState(onStateUpdate: S.() -> S) { - process(intent(onStateUpdate)) - } - - override fun states(): Observable = store - - abstract fun toIntent(event: E): BaseIntent -} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewState.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewState.kt deleted file mode 100644 index f11510aaca..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/BaseViewState.kt +++ /dev/null @@ -1,3 +0,0 @@ -package foundation.e.blisslauncher.base.presentation - -interface BaseViewState \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/Model.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/Model.kt deleted file mode 100644 index 83f55229d0..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/presentation/Model.kt +++ /dev/null @@ -1,22 +0,0 @@ -package foundation.e.blisslauncher.base.presentation - -import io.reactivex.Observable - -interface Model { - - /** - * Model will receive intents to be processed via this function - * - * Model State is immutable. Processed intents will copy and create a new modified state. - */ - fun process(intent: BaseIntent) - - /** - * Observable stream of changes to the Model state - * - * Every time a model state is replaced by a new one, this observable will emit that. - * - * Views should only subscribe to this. - */ - fun states(): Observable -} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt deleted file mode 100644 index a5601c75e3..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/Functions.kt +++ /dev/null @@ -1,8 +0,0 @@ -package foundation.e.blisslauncher.common - -import foundation.e.blisslauncher.base.presentation.BaseViewState -import io.reactivex.Observable -import io.reactivex.disposables.Disposable - -fun Observable.subscribeToState(onNext: (state: S) -> Unit): Disposable = - this.subscribe(onNext) diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt new file mode 100644 index 0000000000..16b3c00375 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt @@ -0,0 +1,32 @@ +package foundation.e.blisslauncher.features + +import foundation.e.blisslauncher.features.LauncherStore.Intent +import foundation.e.blisslauncher.features.LauncherStore.Action +import foundation.e.blisslauncher.features.LauncherStore.Effect +import foundation.e.blisslauncher.features.LauncherStore.State +import foundation.e.blisslauncher.features.LauncherStore.News +import foundation.e.blisslauncher.mvicore.component.BaseStore + +class LauncherStore : + BaseStore() { + + sealed class Intent { + + } + + sealed class Effect { + + } + + sealed class News { + + } + + sealed class Action { + + } + + data class State(val name: String) { + + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt index 6fc8b690ba..fa37d8b538 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt @@ -10,7 +10,7 @@ import dagger.android.AndroidInjection import foundation.e.blisslauncher.base.BaseDraggingActivity import foundation.e.blisslauncher.base.presentation.BaseIntent import foundation.e.blisslauncher.common.subscribeToState -import foundation.e.blisslauncher.common.util.TraceHelper +import foundation.e.blisslauncher.utils.TraceHelper import foundation.e.blisslauncher.domain.entity.LauncherItem import foundation.e.blisslauncher.domain.interactor.LoadLauncher import foundation.e.blisslauncher.domain.keys.PackageUserKey diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt index da745e6cb8..a2acd0a171 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt @@ -2,7 +2,7 @@ package foundation.e.blisslauncher.features.launcher import dagger.Module import dagger.Provides -import foundation.e.blisslauncher.common.util.SystemUiController +import foundation.e.blisslauncher.utils.SystemUiController @Module class LauncherActivityModule { diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt index fffeaa7f39..95a4eb6986 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt @@ -5,8 +5,7 @@ import android.content.Context import android.content.pm.LauncherActivityInfo import android.os.UserHandle import androidx.core.util.set -import foundation.e.blisslauncher.base.presentation.BaseViewState -import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.utils.LongArrayMap import foundation.e.blisslauncher.domain.ItemInfoMatcher import foundation.e.blisslauncher.domain.Matcher import foundation.e.blisslauncher.domain.addFlag @@ -48,7 +47,7 @@ data class LauncherState @Inject constructor( /** The list of all apps. */ val data: List -) : BaseViewState { +) { @Synchronized fun clear() { diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt index a95ebb6225..10206f2d9f 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt @@ -1,10 +1,11 @@ package foundation.e.blisslauncher.features.launcher -import foundation.e.blisslauncher.base.presentation.BaseView -import foundation.e.blisslauncher.domain.keys.PackageUserKey +import foundation.e.blisslauncher.mvicore.component.MviView -interface LauncherView : - BaseView { +interface LauncherView : MviView { + data class LauncherViewModel(private val name: String) - fun updateIconBadges(updatedBadges: Set) + sealed class LauncherViewEvent { + + } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt deleted file mode 100644 index 67b0951afe..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package foundation.e.blisslauncher.features.launcher - -import foundation.e.blisslauncher.base.presentation.BaseViewEvent - -sealed class LauncherViewEvent : BaseViewEvent { - object LoadLauncher : LauncherViewEvent() -} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt deleted file mode 100644 index 4ff4e38021..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherViewModel.kt +++ /dev/null @@ -1,57 +0,0 @@ -package foundation.e.blisslauncher.features.launcher - -import foundation.e.blisslauncher.base.presentation.BaseIntent -import foundation.e.blisslauncher.base.presentation.BaseViewModel -import foundation.e.blisslauncher.base.presentation.intent -import foundation.e.blisslauncher.common.util.LongArrayMap -import foundation.e.blisslauncher.domain.interactor.LauncherStateInteractor -import foundation.e.blisslauncher.domain.interactor.LoadLauncher -import foundation.e.blisslauncher.domain.interactor.ObserveAddedApps -import io.reactivex.disposables.CompositeDisposable -import javax.inject.Inject - -class LauncherViewModel @Inject constructor( - private val launcherStateInteractor: LauncherStateInteractor, - private val observeAddedApps: ObserveAddedApps, - private val loadLauncher: LoadLauncher -) : BaseViewModel( - LauncherState( - itemsIdMap = LongArrayMap(), - allItems = emptyList(), - folders = LongArrayMap(), - workspaceScreen = emptyList(), - data = emptyList() - ) -) { - private val disposable = CompositeDisposable() - - init { - launcherStateInteractor(LauncherStateInteractor.Command.INIT) - - observeAddedApps.observe { - } - } - - fun terminate() { - launcherStateInteractor(LauncherStateInteractor.Command.TERMINATE) - disposable.dispose() - observeAddedApps.dispose() - } - - override fun toIntent(event: LauncherViewEvent): BaseIntent { - return when (event) { - is LauncherViewEvent.LoadLauncher -> intent { - loadLauncher( - onSuccess = { - newState { copy(data = emptyList()) } - }, - onError = { - it.printStackTrace() - copy(data = emptyList()) - } - ) - copy() - } - } - } -} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/PagedView.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/PagedView.java similarity index 99% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/PagedView.java rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/PagedView.java index 3aa58ade47..bb933c4b98 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/PagedView.java +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/PagedView.java @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.features.launcher; +package foundation.e.blisslauncher.features.launcher.views; /* * Copyright (C) 2012 The Android Open Source Project @@ -44,7 +44,7 @@ import java.util.ArrayList; import foundation.e.blisslauncher.R; import foundation.e.blisslauncher.common.Utilities; -import foundation.e.blisslauncher.features.launcher.pageindicators.PageIndicator; +import foundation.e.blisslauncher.features.launcher.views.pageindicators.PageIndicator; import foundation.e.blisslauncher.touch.OverScroll; /** diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicator.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicator.kt similarity index 73% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicator.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicator.kt index dd4f689ee5..e7b02e9c37 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicator.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicator.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.features.launcher.pageindicators +package foundation.e.blisslauncher.features.launcher.views.pageindicators /** * Base class for a page indicator. diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicatorDots.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicatorDots.kt similarity index 99% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicatorDots.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicatorDots.kt index eef6dc5573..cb3ee7bbad 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/pageindicators/PageIndicatorDots.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicatorDots.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.features.launcher.pageindicators +package foundation.e.blisslauncher.features.launcher.views.pageindicators import android.animation.Animator import android.animation.AnimatorListenerAdapter diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/SystemUiController.kt similarity index 97% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/SystemUiController.kt index 2693475ad0..21b0e83476 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/SystemUiController.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.common.util +package foundation.e.blisslauncher.utils import android.view.View import android.view.Window diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/TraceHelper.kt similarity index 97% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/TraceHelper.kt index c811c3c374..148bca426d 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/TraceHelper.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.common.util +package foundation.e.blisslauncher.utils import android.os.SystemClock import android.os.Trace diff --git a/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt b/common/src/main/java/foundation/e/blisslauncher/utils/LongArrayMap.kt similarity index 94% rename from common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt rename to common/src/main/java/foundation/e/blisslauncher/utils/LongArrayMap.kt index 1f01029528..52bb4d6c9b 100644 --- a/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt +++ b/common/src/main/java/foundation/e/blisslauncher/utils/LongArrayMap.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.common.util +package foundation.e.blisslauncher.utils import android.util.LongSparseArray diff --git a/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java b/common/src/main/java/foundation/e/blisslauncher/utils/MultiHashMap.java similarity index 96% rename from common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java rename to common/src/main/java/foundation/e/blisslauncher/utils/MultiHashMap.java index 8626a5b1c3..732dff451e 100644 --- a/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java +++ b/common/src/main/java/foundation/e/blisslauncher/utils/MultiHashMap.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package foundation.e.blisslauncher.common.util; +package foundation.e.blisslauncher.utils; import java.util.ArrayList; import java.util.HashMap; diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt index 299b400690..eba6895108 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt @@ -8,7 +8,7 @@ import android.util.LongSparseArray import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat -import foundation.e.blisslauncher.common.util.MultiHashMap +import foundation.e.blisslauncher.utils.MultiHashMap import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity import foundation.e.blisslauncher.data.shortcuts.PinnedShortcutManager import foundation.e.blisslauncher.domain.entity.AppShortcutItem diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt index a6bd9536a5..edb9baf067 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt @@ -6,7 +6,7 @@ import android.os.Process import android.os.UserHandle import android.os.UserManager import android.util.ArrayMap -import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.utils.LongArrayMap import foundation.e.blisslauncher.domain.repository.UserManagerRepository import java.util.ArrayList diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt index a301468699..3603b6c7c5 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt @@ -2,7 +2,7 @@ package foundation.e.blisslauncher.domain import android.content.ComponentName import android.os.UserHandle -import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.utils.LongArrayMap import foundation.e.blisslauncher.domain.entity.LauncherItem typealias ItemInfoMatcher = (item: LauncherItem, cn: ComponentName) -> Boolean diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt index 6d3e8e34a6..b365596a8a 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt @@ -1,6 +1,6 @@ package foundation.e.blisslauncher.domain.dto -import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.utils.LongArrayMap import foundation.e.blisslauncher.domain.entity.FolderItem import foundation.e.blisslauncher.domain.entity.LauncherItem diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt index ece469f8b6..e5706e0ad9 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt @@ -6,7 +6,7 @@ import android.util.ArrayMap import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.utils.LongArrayMap import foundation.e.blisslauncher.domain.ItemInfoMatcher import foundation.e.blisslauncher.domain.Matcher import foundation.e.blisslauncher.domain.and diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/MyClass.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/MyClass.kt deleted file mode 100644 index c32bf2537f..0000000000 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/MyClass.kt +++ /dev/null @@ -1,4 +0,0 @@ -package foundation.e.blisslauncher.mvicore - -public class MyClass { -} diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Actor.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Actor.kt deleted file mode 100644 index 2fab8461f7..0000000000 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Actor.kt +++ /dev/null @@ -1,5 +0,0 @@ -package foundation.e.blisslauncher.mvicore.component - -import io.reactivex.Observable - -typealias Actor = (State, Action) -> Observable \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseModel.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseModel.kt deleted file mode 100644 index 91459c8662..0000000000 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseModel.kt +++ /dev/null @@ -1,62 +0,0 @@ -package foundation.e.blisslauncher.mvicore.component - -import foundation.e.blisslauncher.mvicore.util.SameThreadVerifier -import io.reactivex.Observer -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable -import io.reactivex.functions.Consumer -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject - -open class BaseModel( - initialState: State, - private val intentToAction: IntentToAction, - actor: Actor, - reducer: Reducer, - eventPublisher: EventPublisher? = null -) : Model, Disposable { - - private val threadVerifier = SameThreadVerifier() - private val actionSubject = PublishSubject.create() - private val stateSubject = PublishSubject.create() - private val eventSubject = PublishSubject.create() - - private val disposable = CompositeDisposable() - - private val eventPublisherWrapper = eventPublisher?.let { - EventPublisherWrapper(eventPublisher, eventSubject) - } - - init { - disposable += eventPublisherWrapper - } - - override fun accept(t: Intent) { - TODO("Not yet implemented") - } - - override fun subscribe(observer: Observer) { - TODO("Not yet implemented") - } - - override val state: State - get() = TODO("Not yet implemented") - - override fun isDisposed(): Boolean = disposable.isDisposed - - override fun dispose() = disposable.dispose() - - private class EventPublisherWrapper( - private val eventPublisher: EventPublisher, - private val events: Subject - ) : Consumer>, Disposable { - - override fun accept(t: Triple) { - val (action, effect, state) = t - eventPublisher.invoke(action, effect, state)?.let { - events.onNext(it) - } - } - } -} \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt new file mode 100644 index 0000000000..270c8221b7 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt @@ -0,0 +1,70 @@ +package foundation.e.blisslauncher.mvicore.component + +import foundation.e.blisslauncher.mvicore.util.SameThreadVerifier +import io.reactivex.ObservableSource +import io.reactivex.Observer +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.functions.Consumer +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject + +open class BaseStore( + initialState: State, + private val intentToAction: IntentToAction, + private val actor: Actor, + private val reducer: Reducer, + private val newsPublisher: NewsPublisher? = null +) : Store, Disposable { + + private val threadVerifier = SameThreadVerifier() + private val actionSubject = PublishSubject.create() + private val stateSubject = BehaviorSubject.createDefault(initialState) + private val newsSubject = PublishSubject.create() + + private val disposable = CompositeDisposable() + + private val news: ObservableSource + get() = newsSubject + + override val state: State + get() = stateSubject.value!! + + init { + disposable += actionSubject.subscribe { invokeActor(state, it) } + } + + override fun accept(intent: Intent) { + val action = intentToAction(intent) + actionSubject.onNext(action) + } + + override fun subscribe(observer: Observer) { + stateSubject.subscribe(observer) + } + + override fun isDisposed(): Boolean = disposable.isDisposed + + override fun dispose() = disposable.dispose() + + private fun invokeActor(state: State, action: Action) { + if (isDisposed) return + + disposable += actor(state, action) + .subscribe { invokeReducer(stateSubject.value!!, action, it) } + } + + private fun invokeReducer(state: State, action: Action, effect: Effect) { + if(isDisposed) return + + threadVerifier.verify() + + val newState = reducer.invoke(state, effect) + stateSubject.onNext(newState) + newsPublisher?.invoke(action, effect, state)?.let { + newsSubject.onNext(it) + } + } +} \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/EventPublisher.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/EventPublisher.kt deleted file mode 100644 index c4c0cf84c2..0000000000 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/EventPublisher.kt +++ /dev/null @@ -1,4 +0,0 @@ -package foundation.e.blisslauncher.mvicore.component - -typealias EventPublisher = - (action: Action, effect: Effect, state: State) -> Event? \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/IntentToAction.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/IntentToAction.kt deleted file mode 100644 index d7e33a3f21..0000000000 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/IntentToAction.kt +++ /dev/null @@ -1,3 +0,0 @@ -package foundation.e.blisslauncher.mvicore.component - -typealias IntentToAction = (intent: Intent) -> Action \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt index 8dc156898a..63cfceee63 100644 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/MviView.kt @@ -1,10 +1,11 @@ package foundation.e.blisslauncher.mvicore.component import io.reactivex.Observable +import io.reactivex.functions.Consumer -interface MviView { +interface MviView { - val events: Observable + val events: Observable fun render(model: ViewModel) } \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Reducer.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Reducer.kt deleted file mode 100644 index c04b4eb8d6..0000000000 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Reducer.kt +++ /dev/null @@ -1,6 +0,0 @@ -package foundation.e.blisslauncher.mvicore.component - -/** - * Reducer function which takes current state, applies an effect to it and produce a new state. - */ -typealias Reducer = (state: State, effect: Effect) -> State \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Model.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt similarity index 50% rename from mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Model.kt rename to mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt index cd1efe6b14..f37ef4d4e6 100644 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Model.kt +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt @@ -3,7 +3,11 @@ package foundation.e.blisslauncher.mvicore.component import io.reactivex.ObservableSource import io.reactivex.functions.Consumer -interface Model : Consumer, +/** + * Store manages the state of the application, similar to the Model + * and are bound to a particular domain. + */ +interface Store : Consumer, ObservableSource { val state: State } \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Typealiases.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Typealiases.kt new file mode 100644 index 0000000000..5975227fd0 --- /dev/null +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Typealiases.kt @@ -0,0 +1,24 @@ +package foundation.e.blisslauncher.mvicore.component + +import io.reactivex.Observable + +/** + * Function which maps Intent to Action. + */ +typealias IntentToAction = (intent: Intent) -> Action + +/** + * Actor function which takes current state, action and returns a stream of effects. + */ +typealias Actor = (State, Action) -> Observable + +/** + * Reducer function which takes current state, applies an effect to it and returns a new state. + */ +typealias Reducer = (state: State, effect: Effect) -> State + +/** + * Publisher used to publish Single Events (aka News) + */ +typealias NewsPublisher = + (action: Action, effect: Effect, state: State) -> News? -- GitLab From 603f1f769f2f998a725440fb22df8b32f0051de2 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 22 May 2020 22:06:20 +0530 Subject: [PATCH 19/23] Add PagedView --- .idea/codeStyles/Project.xml | 6 +- .../{ => features}/base/BaseActivity.kt | 2 +- .../base/BaseDraggingActivity.kt | 2 +- .../features/launcher/LauncherActivity.kt | 4 +- .../features/launcher/views/PagedView.java | 1595 ----------------- .../e/blisslauncher/widget/PagedView.kt | 1343 ++++++++++++++ .../pageindicators/PageIndicator.kt | 4 +- .../pageindicators/PageIndicatorDots.kt | 123 +- .../src/main/res/values/colors.xml | 3 +- .../base/presentation/BaseViewModelTest.kt | 2 +- 10 files changed, 1418 insertions(+), 1666 deletions(-) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{ => features}/base/BaseActivity.kt (97%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{ => features}/base/BaseDraggingActivity.kt (99%) delete mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/PagedView.java create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{features/launcher/views => widget}/pageindicators/PageIndicator.kt (61%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{features/launcher/views => widget}/pageindicators/PageIndicatorDots.kt (75%) rename blisslauncherv2/src/test/java/foundation/e/blisslauncher/{ => features}/base/presentation/BaseViewModelTest.kt (95%) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 60d3be05fc..1a654515e5 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -53,7 +53,7 @@ .*:id - http://schemas.android.com/apk/res/android + http://schemas.android.com/apk/res/android\n @@ -64,7 +64,7 @@ .*:name - http://schemas.android.com/apk/res/android + http://schemas.android.com/apk/res/android\n @@ -109,7 +109,7 @@ .* - http://schemas.android.com/apk/res/android + http://schemas.android.com/apk/res/android\n ANDROID_ATTRIBUTE_ORDER diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt similarity index 97% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt index c4f2e60d64..b7bf0845c7 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.base +package foundation.e.blisslauncher.features.base import android.app.Activity import android.content.Context diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseDraggingActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseDraggingActivity.kt similarity index 99% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseDraggingActivity.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseDraggingActivity.kt index 140caeb728..7c3397f074 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/base/BaseDraggingActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseDraggingActivity.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.base +package foundation.e.blisslauncher.features.base import android.app.ActivityOptions import android.content.ActivityNotFoundException diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt index fa37d8b538..59c185d16f 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt @@ -7,8 +7,8 @@ import android.os.StrictMode import android.os.StrictMode.VmPolicy import android.view.View import dagger.android.AndroidInjection -import foundation.e.blisslauncher.base.BaseDraggingActivity -import foundation.e.blisslauncher.base.presentation.BaseIntent +import foundation.e.blisslauncher.features.base.BaseDraggingActivity +import foundation.e.blisslauncher.features.base.presentation.BaseIntent import foundation.e.blisslauncher.common.subscribeToState import foundation.e.blisslauncher.utils.TraceHelper import foundation.e.blisslauncher.domain.entity.LauncherItem diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/PagedView.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/PagedView.java deleted file mode 100644 index bb933c4b98..0000000000 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/PagedView.java +++ /dev/null @@ -1,1595 +0,0 @@ -package foundation.e.blisslauncher.features.launcher.views; - -/* - * 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. - */ - -import android.animation.LayoutTransition; -import android.animation.TimeInterpolator; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.os.Bundle; -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.ViewDebug; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.animation.Interpolator; -import android.widget.ScrollView; -import android.widget.Scroller; - -import java.util.ArrayList; - -import foundation.e.blisslauncher.R; -import foundation.e.blisslauncher.common.Utilities; -import foundation.e.blisslauncher.features.launcher.views.pageindicators.PageIndicator; -import foundation.e.blisslauncher.touch.OverScroll; - -/** - * An abstraction of the original Workspace which supports browsing through a - * sequential list of "pages" - */ -public abstract class PagedView extends ViewGroup { - private static final String TAG = "PagedView"; - private static final boolean DEBUG = false; - - protected static final int INVALID_PAGE = -1; - protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE; - - public static final int PAGE_SNAP_ANIMATION_DURATION = 750; - public static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; - - // OverScroll constants - private final static int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270; - - private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; - // The page is moved more than halfway, automatically move to the next page on touch up. - private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; - - private static final float MAX_SCROLL_PROGRESS = 1.0f; - - // The following constants need to be scaled based on density. The scaled versions will be - // assigned to the corresponding member variables below. - private static final int FLING_THRESHOLD_VELOCITY = 500; - private static final int MIN_SNAP_VELOCITY = 1500; - private static final int MIN_FLING_VELOCITY = 250; - - public static final int INVALID_RESTORE_PAGE = -1001; - - private boolean mFreeScroll = false; - private boolean mSettleOnPageInFreeScroll = false; - - protected int mFlingThresholdVelocity; - protected int mMinFlingVelocity; - protected int mMinSnapVelocity; - - protected boolean mFirstLayout = true; - - @ViewDebug.ExportedProperty(category = "launcher") - protected int mCurrentPage; - - @ViewDebug.ExportedProperty(category = "launcher") - protected int mNextPage = INVALID_PAGE; - protected int mMaxScrollX; - public Scroller mScroller; - private Interpolator mDefaultInterpolator; - private VelocityTracker mVelocityTracker; - protected int mPageSpacing = 0; - - private float mDownMotionX; - private float mDownMotionY; - private float mLastMotionX; - private float mLastMotionXRemainder; - private float mTotalMotionX; - - protected int[] mPageScrolls; - - 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 int mTouchState = TOUCH_STATE_REST; - - protected int mTouchSlop; - private int mMaximumVelocity; - protected boolean mAllowOverScroll = true; - - protected static final int INVALID_POINTER = -1; - - protected int mActivePointerId = INVALID_POINTER; - - protected boolean mIsPageInTransition = false; - - protected boolean mWasInOverscroll = false; - - // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise - // it is equal to the scaled overscroll position. We use a separate value so as to prevent - // the screens from continuing to translate beyond the normal bounds. - protected int mOverScrollX; - - protected int mUnboundedScrollX; - - // Page Indicator - int mPageIndicatorViewId; - protected T mPageIndicator; - - // Convenience/caching - private static final Rect sTmpRect = new Rect(); - - protected final Rect mInsets = new Rect(); - protected boolean mIsRtl; - - // Similar to the platform implementation of isLayoutValid(); - protected boolean mIsLayoutValid; - - private int[] mTmpIntPair = new int[2]; - - 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); - mPageIndicatorViewId = a.getResourceId(R.styleable.PagedView_pageIndicator, -1); - a.recycle(); - - setHapticFeedbackEnabled(false); - mIsRtl = false; - init(); - } - - /** - * Initializes various states for this workspace. - */ - protected void init() { - mScroller = new Scroller(getContext()); - mCurrentPage = 0; - - final ViewConfiguration configuration = ViewConfiguration.get(getContext()); - mTouchSlop = configuration.getScaledPagingTouchSlop(); - mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); - - float density = getResources().getDisplayMetrics().density; - mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * density); - mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * density); - mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density); - - if (Utilities.ATLEAST_OREO) { - setDefaultFocusHighlightEnabled(false); - } - } - - public void initParentViews(View parent) { - if (mPageIndicatorViewId > -1) { - mPageIndicator = parent.findViewById(mPageIndicatorViewId); - mPageIndicator.setMarkersCount(getChildCount()); - } - } - - public T getPageIndicator() { - return mPageIndicator; - } - - /** - * Returns the index of the currently displayed page. When in free scroll mode, this is the page - * that the user was on before entering free scroll mode (e.g. the home screen page they - * long-pressed on to enter the overview). Try using {@link #getPageNearestToCenterOfScreen()} - * to get the page the user is currently scrolling over. - */ - public int getCurrentPage() { - return mCurrentPage; - } - - /** - * Returns the index of page to be shown immediately afterwards. - */ - public int getNextPage() { - return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; - } - - public int getPageCount() { - return getChildCount(); - } - - public 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()) { - newX = getScrollForPage(mCurrentPage); - } - scrollTo(newX, 0); - mScroller.setFinalX(newX); - forceFinishScroller(true); - } - - public void abortScrollerAnimation(boolean resetNextPage) { - mScroller.abortAnimation(); - // We need to clean up the next page here to avoid computeScrollHelper from - // updating current page on the pass. - if (resetNextPage) { - mNextPage = INVALID_PAGE; - pageEndTransition(); - } - } - - private void forceFinishScroller(boolean resetNextPage) { - mScroller.forceFinished(true); - // We need to clean up the next page here to avoid computeScrollHelper from - // updating current page on the pass. - if (resetNextPage) { - mNextPage = INVALID_PAGE; - pageEndTransition(); - } - } - - private int validateNewPage(int newPage) { - // Ensure that it is clamped by the actual set of children in all cases - return Utilities.boundToRange(newPage, 0, getPageCount() - 1); - } - - /** - * Sets the current page. - */ - public void setCurrentPage(int currentPage) { - if (!mScroller.isFinished()) { - abortScrollerAnimation(true); - } - // don't introduce any checks like mCurrentPage == currentPage here-- if we change the - // the default - if (getChildCount() == 0) { - return; - } - int prevPage = mCurrentPage; - mCurrentPage = validateNewPage(currentPage); - updateCurrentPageScroll(); - notifyPageSwitchListener(prevPage); - invalidate(); - } - - /** - * Should be called whenever the page changes. In the case of a scroll, we wait until the page - * has settled. - */ - protected void notifyPageSwitchListener(int prevPage) { - updatePageIndicator(); - } - - private void updatePageIndicator() { - if (mPageIndicator != null) { - mPageIndicator.setActiveMarker(getNextPage()); - } - } - protected void pageBeginTransition() { - if (!mIsPageInTransition) { - mIsPageInTransition = true; - onPageBeginTransition(); - } - } - - protected void pageEndTransition() { - if (mIsPageInTransition) { - mIsPageInTransition = false; - onPageEndTransition(); - } - } - - protected boolean isPageInTransition() { - return mIsPageInTransition; - } - - /** - * Called when the page starts moving as part of the scroll. Subclasses can override this - * to provide custom behavior during animation. - */ - protected void onPageBeginTransition() { - } - - /** - * Called when the page ends moving as part of the scroll. Subclasses can override this - * to provide custom behavior during animation. - */ - protected void onPageEndTransition() { - mWasInOverscroll = false; - } - - protected int getUnboundedScrollX() { - return mUnboundedScrollX; - } - - @Override - public void scrollBy(int x, int y) { - scrollTo(getUnboundedScrollX() + x, getScrollY() + y); - } - - @Override - public void scrollTo(int x, int y) { - // In free scroll mode, we clamp the scrollX - if (mFreeScroll) { - // If the scroller is trying to move to a location beyond the maximum allowed - // in the free scroll mode, we make sure to end the scroll operation. - if (!mScroller.isFinished() && (x > mMaxScrollX || x < 0)) { - forceFinishScroller(false); - } - - x = Utilities.boundToRange(x, 0, mMaxScrollX); - } - - mUnboundedScrollX = x; - - boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < 0); - boolean isXAfterLastPage = mIsRtl ? (x < 0) : (x > mMaxScrollX); - if (isXBeforeFirstPage) { - super.scrollTo(mIsRtl ? mMaxScrollX : 0, y); - if (mAllowOverScroll) { - mWasInOverscroll = true; - if (mIsRtl) { - overScroll(x - mMaxScrollX); - } else { - overScroll(x); - } - } - } else if (isXAfterLastPage) { - super.scrollTo(mIsRtl ? 0 : mMaxScrollX, y); - if (mAllowOverScroll) { - mWasInOverscroll = true; - if (mIsRtl) { - overScroll(x); - } else { - overScroll(x - mMaxScrollX); - } - } - } else { - if (mWasInOverscroll) { - overScroll(0); - mWasInOverscroll = false; - } - mOverScrollX = x; - super.scrollTo(x, y); - } - } - - private void sendScrollAccessibilityEvent() { - } - - // we moved this functionality to a helper function so SmoothPagedView can reuse it - protected boolean computeScrollHelper() { - return computeScrollHelper(true); - } - - protected void announcePageForAccessibility() { - } - - protected boolean computeScrollHelper(boolean shouldInvalidate) { - if (mScroller.computeScrollOffset()) { - // Don't bother scrolling if the page does not need to be moved - if (getUnboundedScrollX() != mScroller.getCurrX() - || getScrollY() != mScroller.getCurrY() - || mOverScrollX != mScroller.getCurrX()) { - scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); - } - if (shouldInvalidate) { - invalidate(); - } - return true; - } else if (mNextPage != INVALID_PAGE && shouldInvalidate) { - sendScrollAccessibilityEvent(); - - int prevPage = mCurrentPage; - mCurrentPage = validateNewPage(mNextPage); - mNextPage = INVALID_PAGE; - notifyPageSwitchListener(prevPage); - - // 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) { - pageEndTransition(); - } - - if (canAnnouncePageDescription()) { - announcePageForAccessibility(); - } - } - return false; - } - - @Override - public void computeScroll() { - computeScrollHelper(); - } - - public int getExpectedHeight() { - return getMeasuredHeight(); - } - - public int getNormalChildHeight() { - return getExpectedHeight() - getPaddingTop() - getPaddingBottom() - - mInsets.top - mInsets.bottom; - } - - public int getExpectedWidth() { - return getMeasuredWidth(); - } - - public int getNormalChildWidth() { - return getExpectedWidth() - getPaddingLeft() - getPaddingRight() - - mInsets.left - mInsets.right; - } - - @Override - public void requestLayout() { - mIsLayoutValid = false; - super.requestLayout(); - } - - @Override - public void forceLayout() { - mIsLayoutValid = false; - super.forceLayout(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (getChildCount() == 0) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - - // We measure the dimensions of the PagedView to be larger than the pages so that when we - // zoom out (and scale down), the view is still contained in the parent - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - - if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - - // Return early if we aren't given a proper dimension - if (widthSize <= 0 || heightSize <= 0) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - - // 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); - - int myWidthSpec = MeasureSpec.makeMeasureSpec( - widthSize - mInsets.left - mInsets.right, MeasureSpec.EXACTLY); - int myHeightSpec = MeasureSpec.makeMeasureSpec( - heightSize - mInsets.top - mInsets.bottom, MeasureSpec.EXACTLY); - - // measureChildren takes accounts for content padding, we only need to care about extra - // space due to insets. - measureChildren(myWidthSpec, myHeightSpec); - setMeasuredDimension(widthSize, heightSize); - } - - @SuppressLint("DrawAllocation") - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - mIsLayoutValid = true; - final int childCount = getChildCount(); - boolean pageScrollChanged = false; - if (mPageScrolls == null || childCount != mPageScrolls.length) { - mPageScrolls = new int[childCount]; - pageScrollChanged = true; - } - - if (childCount == 0) { - return; - } - - if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); - - if (getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC)) { - pageScrollChanged = true; - } - - final LayoutTransition transition = getLayoutTransition(); - // If the transition is running defer updating max scroll, as some empty pages could - // still be present, and a max scroll change could cause sudden jumps in scroll. - if (transition != null && transition.isRunning()) { - transition.addTransitionListener(new LayoutTransition.TransitionListener() { - - @Override - public void startTransition(LayoutTransition transition, ViewGroup container, - View view, int transitionType) { } - - @Override - public void endTransition(LayoutTransition transition, ViewGroup container, - View view, int transitionType) { - // Wait until all transitions are complete. - if (!transition.isRunning()) { - transition.removeTransitionListener(this); - updateMaxScrollX(); - } - } - }); - } else { - updateMaxScrollX(); - } - - if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) { - updateCurrentPageScroll(); - mFirstLayout = false; - } - - if (mScroller.isFinished() && pageScrollChanged) { - setCurrentPage(getNextPage()); - } - } - - /** - * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length - * of {@code outPageScrolls} should be same as the the childCount - * - */ - protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren, - ComputePageScrollsLogic scrollLogic) { - final int childCount = getChildCount(); - - final int startIndex = mIsRtl ? childCount - 1 : 0; - final int endIndex = mIsRtl ? -1 : childCount; - final int delta = mIsRtl ? -1 : 1; - - final int verticalCenter = (getPaddingTop() + getMeasuredHeight() + mInsets.top - - mInsets.bottom - getPaddingBottom()) / 2; - - final int scrollOffsetLeft = mInsets.left + getPaddingLeft(); - final int scrollOffsetRight = getWidth() - getPaddingRight() - mInsets.right; - boolean pageScrollChanged = false; - - for (int i = startIndex, childLeft = scrollOffsetLeft; i != endIndex; i += delta) { - final View child = getPageAt(i); - if (scrollLogic.shouldIncludeView(child)) { - final int childWidth = child.getMeasuredWidth(); - final int childRight = childLeft + childWidth; - - if (layoutChildren) { - final int childHeight = child.getMeasuredHeight(); - final int childTop = verticalCenter - childHeight / 2; - child.layout(childLeft, childTop, childRight, childTop + childHeight); - } - - // In case the pages are of different width, align the page to left or right edge - // based on the orientation. - final int pageScroll = mIsRtl - ? (childLeft - scrollOffsetLeft) - : Math.max(0, childRight - scrollOffsetRight); - if (outPageScrolls[i] != pageScroll) { - pageScrollChanged = true; - outPageScrolls[i] = pageScroll; - } - - childLeft += childWidth + mPageSpacing + getChildGap(); - } - } - return pageScrollChanged; - } - - protected int getChildGap() { - return 0; - } - - private void updateMaxScrollX() { - mMaxScrollX = computeMaxScrollX(); - } - - protected int computeMaxScrollX() { - int childCount = getChildCount(); - if (childCount > 0) { - final int index = mIsRtl ? 0 : childCount - 1; - return getScrollForPage(index); - } else { - return 0; - } - } - - public void setPageSpacing(int pageSpacing) { - mPageSpacing = pageSpacing; - requestLayout(); - } - - private void dispatchPageCountChanged() { - if (mPageIndicator != null) { - mPageIndicator.setMarkersCount(getChildCount()); - } - // This ensures that when children are added, they get the correct transforms / alphas - // in accordance with any scroll effects. - invalidate(); - } - - @Override - public void onViewAdded(View child) { - super.onViewAdded(child); - dispatchPageCountChanged(); - } - - @Override - public void onViewRemoved(View child) { - super.onViewRemoved(child); - mCurrentPage = validateNewPage(mCurrentPage); - dispatchPageCountChanged(); - } - - protected int getChildOffset(int index) { - if (index < 0 || index > getChildCount() - 1) return 0; - return getPageAt(index).getLeft(); - } - - @Override - public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { - int page = indexToPage(indexOfChild(child)); - if (page != mCurrentPage || !mScroller.isFinished()) { - if (immediate) { - setCurrentPage(page); - } else { - 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) { - if (super.dispatchUnhandledMove(focused, direction)) { - return true; - } - - if (mIsRtl) { - if (direction == View.FOCUS_LEFT) { - direction = View.FOCUS_RIGHT; - } else if (direction == View.FOCUS_RIGHT) { - direction = View.FOCUS_LEFT; - } - } - if (direction == View.FOCUS_LEFT) { - if (getCurrentPage() > 0) { - snapToPage(getCurrentPage() - 1); - getChildAt(getCurrentPage() - 1).requestFocus(direction); - return true; - } - } else if (direction == View.FOCUS_RIGHT) { - if (getCurrentPage() < getPageCount() - 1) { - snapToPage(getCurrentPage() + 1); - getChildAt(getCurrentPage() + 1).requestFocus(direction); - return true; - } - } - return false; - } - - @Override - public void addFocusables(ArrayList views, int direction, int focusableMode) { - if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) { - return; - } - - // 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); - if(currentPage != null) { - currentPage.cancelLongPress(); - } - } - super.requestDisallowInterceptTouchEvent(disallowIntercept); - } - - /** Returns whether x and y originated within the buffered viewport */ - private boolean isTouchPointInViewportWithBuffer(int x, int y) { - sTmpRect.set(-getMeasuredWidth() / 2, 0, 3 * getMeasuredWidth() / 2, getMeasuredHeight()); - return sTmpRect.contains(x, y); - } - - @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); - } - // 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) - break; - } - - case MotionEvent.ACTION_DOWN: { - final float x = ev.getX(); - final float y = ev.getY(); - // Remember location of down touch - mDownMotionX = x; - mDownMotionY = y; - mLastMotionX = x; - mLastMotionXRemainder = 0; - mTotalMotionX = 0; - mActivePointerId = ev.getPointerId(0); - - /* - * 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 / 3); - - if (finishedScrolling) { - mTouchState = TOUCH_STATE_REST; - if (!mScroller.isFinished() && !mFreeScroll) { - setCurrentPage(getNextPage()); - pageEndTransition(); - } - } else { - if (isTouchPointInViewportWithBuffer((int) mDownMotionX, (int) mDownMotionY)) { - mTouchState = TOUCH_STATE_SCROLLING; - } else { - mTouchState = TOUCH_STATE_REST; - } - } - - break; - } - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - resetTouchState(); - 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; - } - - public boolean isHandlingTouch() { - 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) { - // Disallow scrolling if we don't have a valid pointer index - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - if (pointerIndex == -1) return; - - // Disallow scrolling if we started the gesture from outside the viewport - final float x = ev.getX(pointerIndex); - final float y = ev.getY(pointerIndex); - if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return; - - final int xDiff = (int) Math.abs(x - mLastMotionX); - - final int touchSlop = Math.round(touchSlopScale * mTouchSlop); - boolean xMoved = xDiff > touchSlop; - - if (xMoved) { - // Scroll if the user moved far enough along the X axis - mTouchState = TOUCH_STATE_SCROLLING; - mTotalMotionX += Math.abs(mLastMotionX - x); - mLastMotionX = x; - mLastMotionXRemainder = 0; - onScrollInteractionBegin(); - pageBeginTransition(); - // Stop listening for things like pinches. - requestDisallowInterceptTouchEvent(true); - } - } - - protected void cancelCurrentPageLongPress() { - // 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 delta = screenCenter - (getScrollForPage(page) + halfScreenSize); - int count = getChildCount(); - - final int totalDistance; - - int adjacentPage = page + 1; - if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) { - adjacentPage = page - 1; - } - - if (adjacentPage < 0 || adjacentPage > count - 1) { - totalDistance = v.getMeasuredWidth() + mPageSpacing; - } else { - totalDistance = Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page)); - } - - float scrollProgress = delta / (totalDistance * 1.0f); - scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS); - scrollProgress = Math.max(scrollProgress, - MAX_SCROLL_PROGRESS); - return scrollProgress; - } - - public int getScrollForPage(int index) { - if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { - return 0; - } else { - return mPageScrolls[index]; - } - } - - // While layout transitions are occurring, a child's position may stray from its baseline - // position. This method returns the magnitude of this stray at any given time. - public int getLayoutTransitionOffsetForPage(int index) { - if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { - return 0; - } else { - View child = getChildAt(index); - - int scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft(); - int baselineX = mPageScrolls[index] + scrollOffset; - return (int) (child.getX() - baselineX); - } - } - - protected void dampedOverScroll(float amount) { - if (Float.compare(amount, 0f) == 0) return; - - int overScrollAmount = OverScroll.dampedScroll(amount, getMeasuredWidth()); - if (amount < 0) { - mOverScrollX = overScrollAmount; - super.scrollTo(mOverScrollX, getScrollY()); - } else { - mOverScrollX = mMaxScrollX + overScrollAmount; - super.scrollTo(mOverScrollX, getScrollY()); - } - invalidate(); - } - - protected void overScroll(float amount) { - dampedOverScroll(amount); - } - - - protected void enableFreeScroll(boolean settleOnPageInFreeScroll) { - setEnableFreeScroll(true); - mSettleOnPageInFreeScroll = settleOnPageInFreeScroll; - } - - private void setEnableFreeScroll(boolean freeScroll) { - boolean wasFreeScroll = mFreeScroll; - mFreeScroll = freeScroll; - - if (mFreeScroll) { - setCurrentPage(getNextPage()); - } else if (wasFreeScroll) { - snapToPage(getNextPage()); - } - - setEnableOverscroll(!freeScroll); - } - - protected void setEnableOverscroll(boolean enable) { - mAllowOverScroll = enable; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - super.onTouchEvent(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()) { - abortScrollerAnimation(false); - } - - // Remember where the motion event started - mDownMotionX = mLastMotionX = ev.getX(); - mDownMotionY = ev.getY(); - mLastMotionXRemainder = 0; - mTotalMotionX = 0; - mActivePointerId = ev.getPointerId(0); - - if (mTouchState == TOUCH_STATE_SCROLLING) { - onScrollInteractionBegin(); - pageBeginTransition(); - } - break; - - case MotionEvent.ACTION_MOVE: - if (mTouchState == TOUCH_STATE_SCROLLING) { - // Scroll to follow the motion event - final int pointerIndex = ev.findPointerIndex(mActivePointerId); - - if (pointerIndex == -1) return true; - - 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) { - scrollBy((int) deltaX, 0); - 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 = getPageAt(mCurrentPage).getMeasuredWidth(); - boolean isSignificantMove = Math.abs(deltaX) > pageWidth * - SIGNIFICANT_MOVE_THRESHOLD; - - mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); - boolean isFling = mTotalMotionX > mTouchSlop && shouldFlingForVelocity(velocityX); - - if (!mFreeScroll) { - // 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. - boolean isDeltaXLeft = mIsRtl ? deltaX > 0 : deltaX < 0; - boolean isVelocityXLeft = mIsRtl ? velocityX > 0 : velocityX < 0; - if (((isSignificantMove && !isDeltaXLeft && !isFling) || - (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { - finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; - 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 (!mScroller.isFinished()) { - abortScrollerAnimation(true); - } - - float scaleX = getScaleX(); - int vX = (int) (-velocityX * scaleX); - int initialScrollX = (int) (getScrollX() * scaleX); - - mScroller.fling(initialScrollX, - getScrollY(), vX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); - int unscaledScrollX = (int) (mScroller.getFinalX() / scaleX); - mNextPage = getPageNearestToCenterOfScreen(unscaledScrollX); - int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1); - int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0); - if (mSettleOnPageInFreeScroll && unscaledScrollX > 0 - && unscaledScrollX < mMaxScrollX) { - // If scrolling ends in the half of the added space that is closer to the - // end, settle to the end. Otherwise snap to the nearest page. - // If flinging past one of the ends, don't change the velocity as it will - // get stopped at the end anyway. - final int finalX = unscaledScrollX < firstPageScroll / 2 ? - 0 : - unscaledScrollX > (lastPageScroll + mMaxScrollX) / 2 ? - mMaxScrollX : - getScrollForPage(mNextPage); - - mScroller.setFinalX((int) (finalX * getScaleX())); - // Ensure the scroll/snap doesn't happen too fast; - int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION - - mScroller.getDuration(); - if (extraScrollDuration > 0) { - mScroller.extendDuration(extraScrollDuration); - } - } - invalidate(); - } - onScrollInteractionEnd(); - } 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(); - } - } - - // End any intermediate reordering states - resetTouchState(); - break; - - case MotionEvent.ACTION_CANCEL: - if (mTouchState == TOUCH_STATE_SCROLLING) { - snapToDestination(); - onScrollInteractionEnd(); - } - resetTouchState(); - break; - - case MotionEvent.ACTION_POINTER_UP: - onSecondaryPointerUp(ev); - releaseVelocityTracker(); - break; - } - - return true; - } - - protected boolean shouldFlingForVelocity(int velocityX) { - return Math.abs(velocityX) > mFlingThresholdVelocity; - } - - private void resetTouchState() { - releaseVelocityTracker(); - mTouchState = TOUCH_STATE_REST; - mActivePointerId = INVALID_POINTER; - } - - /** - * Triggered by scrolling via touch - */ - protected void onScrollInteractionBegin() { - } - - protected void onScrollInteractionEnd() { - } - - @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 = mIsRtl ? (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.clear(); - 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); - mLastMotionXRemainder = 0; - mActivePointerId = ev.getPointerId(newPointerIndex); - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - } - } - } - - @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); - } - } - - public int getPageNearestToCenterOfScreen() { - return getPageNearestToCenterOfScreen(getScrollX()); - } - - private int getPageNearestToCenterOfScreen(int scaledScrollX) { - int screenCenter = scaledScrollX + (getMeasuredWidth() / 2); - int minDistanceFromScreenCenter = Integer.MAX_VALUE; - int minDistanceFromScreenCenterIndex = -1; - final int childCount = getChildCount(); - for (int i = 0; i < childCount; ++i) { - View layout = getPageAt(i); - int childWidth = layout.getMeasuredWidth(); - 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(), getPageSnapDuration()); - } - - protected boolean isInOverScroll() { - return (mOverScrollX > mMaxScrollX || mOverScrollX < 0); - } - - protected int getPageSnapDuration() { - if (isInOverScroll()) { - return OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION; - } - return PAGE_SNAP_ANIMATION_DURATION; - } - - // 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. - private float distanceInfluenceForSnapDuration(float f) { - f -= 0.5f; // center the values about 0. - f *= 0.3f * Math.PI / 2.0f; - return (float) Math.sin(f); - } - - protected boolean snapToPageWithVelocity(int whichPage, int velocity) { - whichPage = validateNewPage(whichPage); - int halfScreenSize = getMeasuredWidth() / 2; - - final int newX = getScrollForPage(whichPage); - int delta = newX - getUnboundedScrollX(); - 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 - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); - } - - // 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)); - - return snapToPage(whichPage, delta, duration); - } - - public boolean snapToPage(int whichPage) { - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); - } - - public boolean snapToPageImmediately(int whichPage) { - return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null); - } - - public boolean snapToPage(int whichPage, int duration) { - return snapToPage(whichPage, duration, false, null); - } - - public boolean snapToPage(int whichPage, int duration, TimeInterpolator interpolator) { - return snapToPage(whichPage, duration, false, interpolator); - } - - protected boolean snapToPage(int whichPage, int duration, boolean immediate, - TimeInterpolator interpolator) { - whichPage = validateNewPage(whichPage); - - int newX = getScrollForPage(whichPage); - final int delta = newX - getUnboundedScrollX(); - return snapToPage(whichPage, delta, duration, immediate, interpolator); - } - - protected boolean snapToPage(int whichPage, int delta, int duration) { - return snapToPage(whichPage, delta, duration, false, null); - } - - protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate, - TimeInterpolator interpolator) { - if (mFirstLayout) { - setCurrentPage(whichPage); - return false; - } - - whichPage = validateNewPage(whichPage); - - mNextPage = whichPage; - - awakenScrollBars(duration); - if (immediate) { - duration = 0; - } else if (duration == 0) { - duration = Math.abs(delta); - } - - if (duration != 0) { - pageBeginTransition(); - } - - if (!mScroller.isFinished()) { - abortScrollerAnimation(false); - } - - mScroller.startScroll(getUnboundedScrollX(), 0, delta, 0, duration); - - updatePageIndicator(); - - // Trigger a compute() to finish switching pages if necessary - if (immediate) { - computeScroll(); - pageEndTransition(); - } - - invalidate(); - return Math.abs(delta) > 0; - } - - public boolean scrollLeft() { - if (getNextPage() > 0) { - snapToPage(getNextPage() - 1); - return true; - } - return false; - } - - public boolean scrollRight() { - if (getNextPage() < getChildCount() - 1) { - snapToPage(getNextPage() + 1); - return true; - } - return false; - } - - @Override - public CharSequence getAccessibilityClassName() { - // Some accessibility services have special logic for ScrollView. Since we provide same - // accessibility info as ScrollView, inform the service to handle use the same way. - return ScrollView.class.getName(); - } - - protected boolean isPageOrderFlipped() { - return false; - } - - /* Accessibility */ - @SuppressWarnings("deprecation") - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - final boolean pagesFlipped = isPageOrderFlipped(); - info.setScrollable(getPageCount() > 1); - if (getCurrentPage() < getPageCount() - 1) { - info.addAction(pagesFlipped ? AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD - : AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); - } - if (getCurrentPage() > 0) { - info.addAction(pagesFlipped ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD - : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); - } - - // Accessibility-wise, PagedView doesn't support long click, so disabling it. - // Besides disabling the accessibility long-click, this also prevents this view from getting - // accessibility focus. - info.setLongClickable(false); - info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); - } - - @Override - public void sendAccessibilityEvent(int eventType) { - // Don't let the view send real scroll events. - if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED) { - super.sendAccessibilityEvent(eventType); - } - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setScrollable(getPageCount() > 1); - } - - @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (super.performAccessibilityAction(action, arguments)) { - return true; - } - final boolean pagesFlipped = isPageOrderFlipped(); - switch (action) { - case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { - if (pagesFlipped ? scrollLeft() : scrollRight()) { - return true; - } - } break; - case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { - if (pagesFlipped ? scrollRight() : scrollLeft()) { - return true; - } - } - break; - } - return false; - } - - protected boolean canAnnouncePageDescription() { - return true; - } - - protected String getCurrentPageDescription() { - return getContext().getString(R.string.default_scroll_format, - getNextPage() + 1, getChildCount()); - } - - protected float getDownMotionX() { - return mDownMotionX; - } - - protected float getDownMotionY() { - return mDownMotionY; - } - - protected interface ComputePageScrollsLogic { - - boolean shouldIncludeView(View view); - } - - public int[] getVisibleChildrenRange() { - float visibleLeft = 0; - float visibleRight = visibleLeft + getMeasuredWidth(); - float scaleX = getScaleX(); - if (scaleX < 1 && scaleX > 0) { - float mid = getMeasuredWidth() / 2; - visibleLeft = mid - ((mid - visibleLeft) / scaleX); - visibleRight = mid + ((visibleRight - mid) / scaleX); - } - - int leftChild = -1; - int rightChild = -1; - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getPageAt(i); - - float left = child.getLeft() + child.getTranslationX() - getScrollX(); - if (left <= visibleRight && (left + child.getMeasuredWidth()) >= visibleLeft) { - if (leftChild == -1) { - leftChild = i; - } - rightChild = i; - } - } - mTmpIntPair[0] = leftChild; - mTmpIntPair[1] = rightChild; - return mTmpIntPair; - } - - public boolean isScrollerFinished() { - return mScroller.isFinished(); - } -} diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt new file mode 100644 index 0000000000..339877cd5f --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt @@ -0,0 +1,1343 @@ +package foundation.e.blisslauncher.widget + +import android.animation.LayoutTransition +import android.animation.TimeInterpolator +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Matrix +import android.graphics.Rect +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.widget.Scroller +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.touch.OverScroll +import foundation.e.blisslauncher.widget.pageindicators.PageIndicatorDots +import java.util.ArrayList +import kotlin.math.sin + +typealias ComputePageScrollsLogic = (View) -> Boolean + +abstract class PagedView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ViewGroup(context, attrs, defStyleAttr) { + + private var freeScroll = false + private var settleOnPageInFreeScroll = false + + private val flingThresholdVelocity: Int + private val minFlingVelocity: Int + private val minSnapVelocity: Int + + protected var firstLayout = true + + private var currentPage = 0 + set(value) { + if (!scroller.isFinished) { + abortScrollerAnimation(true) + } + // don't introduce any checks like currentPage == currentPage here-- if we change the + // the default + if (childCount == 0) { + return + } + val prevPage: Int = currentPage + field = validateNewPage(value) + updateCurrentPageScroll() + notifyPageSwitchListener(prevPage) + invalidate() + } + + protected var nextPage: Int = INVALID_PAGE + get() { + return if (field != INVALID_PAGE) field else currentPage + } + + protected var maxScrollX = 0 + protected val scroller: Scroller = Scroller(context) + private var velocityTracker: VelocityTracker? = null + protected var pageSpacing = 0 + set(value) { + field = value + requestLayout() + } + + private var downMotionX = 0f + private var downMotionY = 0f + private var lastMotionX = 0f + private var lastMotionXRemainder = 0f + private var totalMotionX = 0f + + protected var pageScrolls: IntArray = IntArray(0) + + protected var touchState: Int = TOUCH_STATE_REST + + private val touchSlop: Int + private val maximumVelocity: Int + protected var mAllowOverScroll = true + + protected val INVALID_POINTER = -1 + + protected var activePointerId = INVALID_POINTER + + protected var isPageInTransition = false + + protected var wasInOverscroll = false + + protected var overScrollX = 0 + + protected var unboundedScrollX = 0 + + protected var pageIndicator: PageIndicatorDots? = null + + // Convenience/caching + private val sTmpInvMatrix = Matrix() + private val sTmpPoint = FloatArray(2) + private val sTmpRect = Rect() + + protected val insets = Rect() + protected var isRtl = false + + // Similar to the platform implementation of isLayoutValid(); + protected var mIsLayoutValid = false + + init { + isHapticFeedbackEnabled = false + currentPage = 0 + val configuration = ViewConfiguration.get(getContext()) + touchSlop = configuration.scaledPagingTouchSlop + maximumVelocity = configuration.scaledMaximumFlingVelocity + + val density = resources.displayMetrics.density + flingThresholdVelocity = (FLING_THRESHOLD_VELOCITY * density).toInt() + minFlingVelocity = (MIN_FLING_VELOCITY * density).toInt() + minSnapVelocity = (MIN_SNAP_VELOCITY * density).toInt() + } + + fun getPageCount() = childCount + + fun getPageAt(index: Int) = getChildAt(index) + + fun indexToPage(index: Int) = index + + fun scrollAndForceFinish(scrollX: Int) { + scrollTo(scrollX, 0) + scroller.finalX = scrollX + forceFinishScroller(true) + } + + private fun forceFinishScroller(resetNextPage: Boolean) { + scroller.forceFinished(true) + // We need to clean up the next page here to avoid computeScrollHelper from + // updating current page on the pass. + if (resetNextPage) { + nextPage = INVALID_PAGE + pageEndTransition() + } + } + + /** + * 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. + */ + private fun updateCurrentPageScroll() { + // If the current page is invalid, just reset the scroll position to zero + var newX = 0 + if (0 <= currentPage && currentPage < getPageCount()) { + newX = getScrollForPage(currentPage) + } + scrollAndForceFinish(newX) + } + + private fun abortScrollerAnimation(resetNextPage: Boolean) { + scroller.abortAnimation() + // We need to clean up the next page here to avoid computeScrollHelper from + // updating current page on the pass. + if (resetNextPage) { + nextPage = INVALID_PAGE + pageEndTransition() + } + } + + private fun validateNewPage(newPage: Int): Int { + // Ensure that it is clamped by the actual set of children in all cases + return Utilities.boundToRange(newPage, 0, getPageCount() - 1) + } + + /** + * Should be called whenever the page changes. In the case of a scroll, we wait until the page + * has settled. + */ + protected fun notifyPageSwitchListener(prevPage: Int) { + updatePageIndicator() + } + + private fun updatePageIndicator() { + if (pageIndicator != null) { + pageIndicator!!.setActiveMarker(nextPage) + } + } + + protected fun pageBeginTransition() { + if (!isPageInTransition) { + isPageInTransition = true + onPageBeginTransition() + } + } + + protected fun pageEndTransition() { + if (isPageInTransition) { + isPageInTransition = false + onPageEndTransition() + } + } + + /** + * Called when the page starts moving as part of the scroll. Subclasses can override this + * to provide custom behavior during animation. + */ + protected fun onPageBeginTransition() {} + + /** + * Called when the page ends moving as part of the scroll. Subclasses can override this + * to provide custom behavior during animation. + */ + protected fun onPageEndTransition() { + wasInOverscroll = false + } + + override fun scrollBy(x: Int, y: Int) { + scrollTo(unboundedScrollX + x, scrollY + y) + } + + override fun scrollTo(x: Int, y: Int) { + // In free scroll mode, we clamp the scrollX + var x = x + if (freeScroll) { + // If the scroller is trying to move to a location beyond the maximum allowed + // in the free scroll mode, we make sure to end the scroll operation. + if (!scroller.isFinished && (x > maxScrollX || x < 0)) { + forceFinishScroller(false) + } + x = Utilities.boundToRange(x, 0, maxScrollX) + } + unboundedScrollX = x + val isXBeforeFirstPage = if (isRtl) x > maxScrollX else x < 0 + val isXAfterLastPage = if (isRtl) x < 0 else x > maxScrollX + if (isXBeforeFirstPage) { + super.scrollTo(if (isRtl) maxScrollX else 0, y) + if (mAllowOverScroll) { + wasInOverscroll = true + if (isRtl) { + overScroll(x - maxScrollX.toFloat()) + } else { + overScroll(x.toFloat()) + } + } + } else if (isXAfterLastPage) { + super.scrollTo(if (isRtl) 0 else maxScrollX, y) + if (mAllowOverScroll) { + wasInOverscroll = true + if (isRtl) { + overScroll(x.toFloat()) + } else { + overScroll(x - maxScrollX.toFloat()) + } + } + } else { + if (wasInOverscroll) { + overScroll(0f) + wasInOverscroll = false + } + overScrollX = x + super.scrollTo(x, y) + } + } + + // we moved this functionality to a helper function so SmoothPagedView can reuse it + protected fun computeScrollHelper(): Boolean { + return computeScrollHelper(true) + } + + protected fun computeScrollHelper(shouldInvalidate: Boolean): Boolean { + if (scroller.computeScrollOffset()) { + // Don't bother scrolling if the page does not need to be moved + if (unboundedScrollX != scroller.getCurrX() || scrollY != scroller.getCurrY() || overScrollX != scroller.getCurrX() + ) { + scrollTo(scroller.getCurrX(), scroller.getCurrY()) + } + if (shouldInvalidate) { + invalidate() + } + return true + } else if (nextPage != INVALID_PAGE && shouldInvalidate) { + val prevPage: Int = currentPage + currentPage = validateNewPage(nextPage) + nextPage = INVALID_PAGE + notifyPageSwitchListener(prevPage) + + // We don't want to trigger a page end moving unless the page has settled + // and the user has stopped scrolling + if (touchState == TOUCH_STATE_REST) { + pageEndTransition() + } + } + return false + } + + override fun computeScroll() { + computeScrollHelper() + } + + fun getExpectedHeight(): Int { + return measuredHeight + } + + fun getNormalChildHeight(): Int { + return (getExpectedHeight() - paddingTop - paddingBottom + - insets.top - insets.bottom) + } + + fun getExpectedWidth(): Int { + return measuredWidth + } + + fun getNormalChildWidth(): Int { + return (getExpectedWidth() - paddingLeft - paddingRight + - insets.left - insets.right) + } + + override fun requestLayout() { + mIsLayoutValid = false + super.requestLayout() + } + + override fun forceLayout() { + mIsLayoutValid = false + super.forceLayout() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (childCount == 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + + // We measure the dimensions of the PagedView to be larger than the pages so that when we + // zoom out (and scale down), the view is still contained in the parent + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + + // Return early if we aren't given a proper dimension + if (widthSize <= 0 || heightSize <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + + Log.d( + TAG, + "PagedView.onMeasure(): $widthSize, $heightSize" + ) + val myWidthSpec = MeasureSpec.makeMeasureSpec( + widthSize - insets.left - insets.right, MeasureSpec.EXACTLY + ) + val myHeightSpec = MeasureSpec.makeMeasureSpec( + heightSize - insets.top - insets.bottom, MeasureSpec.EXACTLY + ) + + // measureChildren takes accounts for content padding, we only need to care about extra + // space due to insets. + measureChildren(myWidthSpec, myHeightSpec) + setMeasuredDimension(widthSize, heightSize) + } + + protected fun restoreScrollOnLayout() { + currentPage = nextPage + } + + @SuppressLint("DrawAllocation") + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + mIsLayoutValid = true + val childCount = childCount + var pageScrollChanged = false + if (childCount != pageScrolls.size) { + pageScrolls = IntArray(childCount) + pageScrollChanged = true + } + if (childCount == 0) { + return + } + Log.d(TAG, "PagedView.onLayout()") + if (getPageScrolls(pageScrolls, true, SIMPLE_SCROLL_LOGIC)) { + pageScrollChanged = true + } + val transition = layoutTransition + // If the transition is running defer updating max scroll, as some empty pages could + // still be present, and a max scroll change could cause sudden jumps in scroll. + if (transition != null && transition.isRunning) { + transition.addTransitionListener(object : LayoutTransition.TransitionListener { + override fun startTransition( + transition: LayoutTransition, container: ViewGroup, + view: View, transitionType: Int + ) { + } + + override fun endTransition( + transition: LayoutTransition, container: ViewGroup, + view: View, transitionType: Int + ) { + // Wait until all transitions are complete. + if (!transition.isRunning) { + transition.removeTransitionListener(this) + updateMaxScrollX() + } + } + }) + } else { + updateMaxScrollX() + } + if (firstLayout && currentPage >= 0 && currentPage < childCount) { + updateCurrentPageScroll() + firstLayout = false + } + if (scroller.isFinished() && pageScrollChanged) { + restoreScrollOnLayout() + } + } + + val SIMPLE_SCROLL_LOGIC: ComputePageScrollsLogic = { v -> v.visibility != View.GONE } + + /** + * Initializes `outPageScrolls` with scroll positions for view at that index. The length + * of `outPageScrolls` should be same as the the childCount + * + */ + protected fun getPageScrolls( + outPageScrolls: IntArray, layoutChildren: Boolean, + scrollLogic: ComputePageScrollsLogic + ): Boolean { + val childCount = childCount + val startIndex = if (isRtl) childCount - 1 else 0 + val endIndex = if (isRtl) -1 else childCount + val delta = if (isRtl) -1 else 1 + val verticalCenter = + (paddingTop + measuredHeight + insets.top - insets.bottom - paddingBottom) / 2 + val scrollOffsetLeft = insets.left + paddingLeft + var pageScrollChanged = false + var i = startIndex + var childLeft = scrollOffsetLeft + offsetForPageScrolls() + while (i != endIndex) { + val child = getPageAt(i) + if (scrollLogic(child)) { + val childTop = verticalCenter - child.measuredHeight / 2 + val childWidth = child.measuredWidth + if (layoutChildren) { + val childHeight = child.measuredHeight + child.layout( + childLeft, childTop, + childLeft + child.measuredWidth, childTop + childHeight + ) + } + val pageScroll = childLeft - scrollOffsetLeft + if (outPageScrolls[i] != pageScroll) { + pageScrollChanged = true + outPageScrolls[i] = pageScroll + } + childLeft += childWidth + pageSpacing + getChildGap() + } + i += delta + } + return pageScrollChanged + } + + protected fun getChildGap(): Int { + return 0 + } + + private fun updateMaxScrollX() { + maxScrollX = computeMaxScrollX() + } + + protected fun computeMaxScrollX(): Int { + val childCount = childCount + return if (childCount > 0) { + val index = if (isRtl) 0 else childCount - 1 + getScrollForPage(index) + } else { + 0 + } + } + + protected fun offsetForPageScrolls(): Int { + return 0 + } + + private fun dispatchPageCountChanged() { + if (pageIndicator != null) { + pageIndicator!!.setMarkersCount(childCount) + } + // This ensures that when children are added, they get the correct transforms / alphas + // in accordance with any scroll effects. + invalidate() + } + + override fun onViewAdded(child: View?) { + super.onViewAdded(child) + dispatchPageCountChanged() + } + + override fun onViewRemoved(child: View?) { + super.onViewRemoved(child) + currentPage = validateNewPage(currentPage) + dispatchPageCountChanged() + } + + protected fun getChildOffset(index: Int): Int { + return if (index < 0 || index > childCount - 1) 0 else getPageAt(index).left + } + + override fun requestChildRectangleOnScreen( + child: View?, + rectangle: Rect?, + immediate: Boolean + ): Boolean { + val page = indexToPage(indexOfChild(child)) + if (page != currentPage || !scroller.isFinished()) { + if (immediate) { + currentPage = page + } else { + snapToPage(page) + } + return true + } + return false + } + + override fun onRequestFocusInDescendants( + direction: Int, + previouslyFocusedRect: Rect? + ): Boolean { + val focusablePage: Int + if (nextPage != INVALID_PAGE) { + focusablePage = nextPage + } else { + focusablePage = currentPage + } + val v = getPageAt(focusablePage) + return v?.requestFocus(direction, previouslyFocusedRect) ?: false + } + + override fun dispatchUnhandledMove(focused: View?, direction: Int): Boolean { + var direction = direction + if (super.dispatchUnhandledMove(focused, direction)) { + return true + } + if (isRtl) { + if (direction == View.FOCUS_LEFT) { + direction = View.FOCUS_RIGHT + } else if (direction == View.FOCUS_RIGHT) { + direction = View.FOCUS_LEFT + } + } + if (direction == View.FOCUS_LEFT) { + if (currentPage > 0) { + snapToPage(currentPage - 1) + return true + } + } else if (direction == View.FOCUS_RIGHT) { + if (currentPage < getPageCount() - 1) { + snapToPage(currentPage + 1) + return true + } + } + return false + } + + override fun addFocusables( + views: ArrayList?, + direction: Int, + focusableMode: Int + ) { + if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) { + return + } + + // XXX-RTL: This will be fixed in a future CL + if (currentPage >= 0 && currentPage < getPageCount()) { + getPageAt(currentPage).addFocusables(views, direction, focusableMode) + } + if (direction == View.FOCUS_LEFT) { + if (currentPage > 0) { + getPageAt(currentPage - 1).addFocusables(views, direction, focusableMode) + } + } else if (direction == View.FOCUS_RIGHT) { + if (currentPage < getPageCount() - 1) { + getPageAt(currentPage + 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 fun focusableViewAvailable(focused: View) { + val current = getPageAt(currentPage) + var v = focused + while (true) { + if (v === current) { + super.focusableViewAvailable(focused) + return + } + if (v === this) { + return + } + val parent = v.parent + v = if (parent is View) { + v.parent as View + } else { + return + } + } + } + + /** + * {@inheritDoc} + */ + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + if (disallowIntercept) { + // We need to make sure to cancel our long press if + // a scrollable widget takes over touch events + val currentPage = getPageAt(currentPage) + currentPage.cancelLongPress() + } + super.requestDisallowInterceptTouchEvent(disallowIntercept) + } + + /** Returns whether x and y originated within the buffered viewport */ + private fun isTouchPointInViewportWithBuffer(x: Int, y: Int): Boolean { + sTmpRect.set( + -measuredWidth / 2, + 0, + 3 * measuredWidth / 2, + measuredHeight + ) + return sTmpRect.contains(x, y) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + /* + * 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 (childCount <= 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. + */ + val action = ev.action + if (action == MotionEvent.ACTION_MOVE && + touchState == TOUCH_STATE_SCROLLING + ) { + return true + } + when (action and MotionEvent.ACTION_MASK) { + 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 (activePointerId != INVALID_POINTER) { + determineScrollingStart(ev) + } + } + MotionEvent.ACTION_DOWN -> { + val x = ev.x + val y = ev.y + // Remember location of down touch + downMotionX = x + downMotionY = y + lastMotionX = x + lastMotionXRemainder = 0f + totalMotionX = 0f + activePointerId = ev.getPointerId(0) + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. scroller.isFinished should be false when + * being flinged. + */ + val xDist: Int = Math.abs(scroller.getFinalX() - scroller.getCurrX()) + val finishedScrolling = + scroller.isFinished() || xDist < touchSlop / 3 + if (finishedScrolling) { + touchState = TOUCH_STATE_REST + if (!scroller.isFinished() && !freeScroll) { + currentPage = nextPage + pageEndTransition() + } + } else { + touchState = if (isTouchPointInViewportWithBuffer( + downMotionX.toInt(), + downMotionY.toInt() + ) + ) { + TOUCH_STATE_SCROLLING + } else { + TOUCH_STATE_REST + } + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> resetTouchState() + MotionEvent.ACTION_POINTER_UP -> { + onSecondaryPointerUp(ev) + releaseVelocityTracker() + } + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */return touchState != TOUCH_STATE_REST + } + + fun isHandlingTouch(): Boolean { + return touchState != TOUCH_STATE_REST + } + + protected fun determineScrollingStart(ev: MotionEvent) { + 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 fun determineScrollingStart( + ev: MotionEvent, + touchSlopScale: Float + ) { + // Disallow scrolling if we don't have a valid pointer index + val pointerIndex = ev.findPointerIndex(activePointerId) + if (pointerIndex == -1) return + + // Disallow scrolling if we started the gesture from outside the viewport + val x = ev.getX(pointerIndex) + val y = ev.getY(pointerIndex) + if (!isTouchPointInViewportWithBuffer(x.toInt(), y.toInt())) return + val xDiff = Math.abs(x - lastMotionX).toInt() + val touchSlop = Math.round(touchSlopScale * touchSlop).toInt() + val xMoved = xDiff > touchSlop + if (xMoved) { + // Scroll if the user moved far enough along the X axis + touchState = TOUCH_STATE_SCROLLING + totalMotionX += Math.abs(lastMotionX - x) + lastMotionX = x + lastMotionXRemainder = 0f + onScrollInteractionBegin() + pageBeginTransition() + // Stop listening for things like pinches. + requestDisallowInterceptTouchEvent(true) + } + } + + protected fun cancelCurrentPageLongPress() { + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + val currentPage = getPageAt(currentPage) + currentPage?.cancelLongPress() + } + + protected fun getScrollProgress( + screenCenter: Int, + v: View, + page: Int + ): Float { + val halfScreenSize = measuredWidth / 2 + val delta = screenCenter - (getScrollForPage(page) + halfScreenSize) + val count = childCount + val totalDistance: Int + var adjacentPage = page + 1 + if (delta < 0 && !isRtl || delta > 0 && isRtl) { + adjacentPage = page - 1 + } + totalDistance = if (adjacentPage < 0 || adjacentPage > count - 1) { + v.measuredWidth + pageSpacing + } else { + Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page)) + } + var scrollProgress = delta / (totalDistance * 1.0f) + scrollProgress = + Math.min(scrollProgress, MAX_SCROLL_PROGRESS) + scrollProgress = + Math.max(scrollProgress, -MAX_SCROLL_PROGRESS) + return scrollProgress + } + + fun getScrollForPage(index: Int): Int { + return if (index >= pageScrolls.size || index < 0) { + 0 + } else { + pageScrolls[index] + } + } + + // While layout transitions are occurring, a child's position may stray from its baseline + // position. This method returns the magnitude of this stray at any given time. + fun getLayoutTransitionOffsetForPage(index: Int): Int { + return if (index >= pageScrolls.size || index < 0) { + 0 + } else { + val child = getChildAt(index) + val scrollOffset = if (isRtl) paddingRight else paddingLeft + val baselineX = pageScrolls[index] + scrollOffset + (child.x - baselineX).toInt() + } + } + + protected fun dampedOverScroll(amount: Float) { + if (java.lang.Float.compare(amount, 0f) == 0) return + val overScrollAmount: Int = OverScroll.dampedScroll(amount, measuredWidth) + if (amount < 0) { + overScrollX = overScrollAmount + super.scrollTo(overScrollX, scrollY) + } else { + overScrollX = maxScrollX + overScrollAmount + super.scrollTo(overScrollX, scrollY) + } + invalidate() + } + + protected fun overScroll(amount: Float) { + dampedOverScroll(amount) + } + + protected fun enableFreeScroll(settleOnPageInFreeScroll: Boolean) { + setEnableFreeScroll(true) + this.settleOnPageInFreeScroll = settleOnPageInFreeScroll + } + + private fun setEnableFreeScroll(freeScroll: Boolean) { + val wasFreeScroll = this.freeScroll + this.freeScroll = freeScroll + if (this.freeScroll) { + currentPage = nextPage + } else if (wasFreeScroll) { + snapToPage(nextPage) + } + setEnableOverscroll(!freeScroll) + } + + protected fun setEnableOverscroll(enable: Boolean) { + mAllowOverScroll = enable + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + super.onTouchEvent(ev) + + // Skip touch handling if there are no pages to swipe + if (childCount <= 0) return super.onTouchEvent(ev) + acquireVelocityTrackerAndAddMovement(ev) + val action = ev.action + when (action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN -> { + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */if (!scroller.isFinished()) { + abortScrollerAnimation(false) + } + run { + lastMotionX = ev.x + downMotionX = lastMotionX + } + downMotionY = ev.y + lastMotionXRemainder = 0f + totalMotionX = 0f + activePointerId = ev.getPointerId(0) + if (touchState == TOUCH_STATE_SCROLLING) { + onScrollInteractionBegin() + pageBeginTransition() + } + } + MotionEvent.ACTION_MOVE -> if (touchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + val pointerIndex = ev.findPointerIndex(activePointerId) + if (pointerIndex == -1) return true + val x = ev.getX(pointerIndex) + val deltaX = lastMotionX + lastMotionXRemainder - x + totalMotionX += 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) { + scrollBy(deltaX.toInt(), 0) + lastMotionX = x + lastMotionXRemainder = deltaX - deltaX.toInt() + } else { + awakenScrollBars() + } + } else { + determineScrollingStart(ev) + } + MotionEvent.ACTION_UP -> { + if (touchState == TOUCH_STATE_SCROLLING) { + val activePointerId = activePointerId + val pointerIndex = ev.findPointerIndex(activePointerId) + val x = ev.getX(pointerIndex) + val velocityTracker = velocityTracker!! + velocityTracker.computeCurrentVelocity(1000, maximumVelocity.toFloat()) + val velocityX = velocityTracker.getXVelocity(activePointerId).toInt() + val deltaX = (x - downMotionX).toInt() + val pageWidth = getPageAt(currentPage).measuredWidth + val isSignificantMove: Boolean = Math.abs(deltaX) > pageWidth * + SIGNIFICANT_MOVE_THRESHOLD + totalMotionX += Math.abs(lastMotionX + lastMotionXRemainder - x) + val isFling = + totalMotionX > touchSlop && shouldFlingForVelocity(velocityX) + if (!freeScroll) { + // 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. + var returnToOriginalPage = false + if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && Math.signum( + velocityX.toFloat() + ) != Math.signum( + deltaX.toFloat() + ) && isFling + ) { + returnToOriginalPage = true + } + val finalPage: Int + // 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. + val isDeltaXLeft = if (isRtl) deltaX > 0 else deltaX < 0 + val isVelocityXLeft = + if (isRtl) velocityX > 0 else velocityX < 0 + if ((isSignificantMove && !isDeltaXLeft && !isFling || + isFling && !isVelocityXLeft) && currentPage > 0 + ) { + finalPage = if (returnToOriginalPage) currentPage else currentPage - 1 + snapToPageWithVelocity(finalPage, velocityX) + } else if ((isSignificantMove && isDeltaXLeft && !isFling || + isFling && isVelocityXLeft) && + currentPage < childCount - 1 + ) { + finalPage = if (returnToOriginalPage) currentPage else currentPage + 1 + snapToPageWithVelocity(finalPage, velocityX) + } else { + snapToDestination() + } + } else { + if (!scroller.isFinished()) { + abortScrollerAnimation(true) + } + val scaleX = scaleX + val vX = (-velocityX * scaleX).toInt() + val initialScrollX = (scrollX * scaleX).toInt() + scroller.fling( + initialScrollX, + scrollY, + vX, + 0, + Int.MIN_VALUE, + Int.MAX_VALUE, + 0, + 0 + ) + val unscaledScrollX = (scroller.getFinalX() / scaleX) as Int + nextPage = getPageNearestToCenterOfScreen(unscaledScrollX) + val firstPageScroll = + getScrollForPage(if (!isRtl) 0 else getPageCount() - 1) + val lastPageScroll = + getScrollForPage(if (!isRtl) getPageCount() - 1 else 0) + if (settleOnPageInFreeScroll && unscaledScrollX > 0 && unscaledScrollX < maxScrollX + ) { + // If scrolling ends in the half of the added space that is closer to the + // end, settle to the end. Otherwise snap to the nearest page. + // If flinging past one of the ends, don't change the velocity as it will + // get stopped at the end anyway. + val finalX = + if (unscaledScrollX < firstPageScroll / 2) 0 else if (unscaledScrollX > (lastPageScroll + maxScrollX) / 2) maxScrollX else getScrollForPage( + nextPage + ) + scroller.setFinalX((finalX * getScaleX()).toInt()) + // Ensure the scroll/snap doesn't happen too fast; + val extraScrollDuration: Int = + (OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION + - scroller.getDuration()) + if (extraScrollDuration > 0) { + scroller.extendDuration(extraScrollDuration) + } + } + invalidate() + } + onScrollInteractionEnd() + } else if (touchState == 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 + val nextPage = Math.max(0, currentPage - 1) + if (nextPage != currentPage) { + snapToPage(nextPage) + } else { + snapToDestination() + } + } else if (touchState == 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 + val nextPage = Math.min(childCount - 1, currentPage + 1) + if (nextPage != currentPage) { + snapToPage(nextPage) + } else { + snapToDestination() + } + } + + // End any intermediate reordering states + resetTouchState() + } + MotionEvent.ACTION_CANCEL -> { + if (touchState == TOUCH_STATE_SCROLLING) { + snapToDestination() + onScrollInteractionEnd() + } + resetTouchState() + } + MotionEvent.ACTION_POINTER_UP -> { + onSecondaryPointerUp(ev) + releaseVelocityTracker() + } + } + return true + } + + protected fun shouldFlingForVelocity(velocityX: Int): Boolean { + return Math.abs(velocityX) > flingThresholdVelocity + } + + private fun resetTouchState() { + releaseVelocityTracker() + touchState = TOUCH_STATE_REST + activePointerId = INVALID_POINTER + } + + /** + * Triggered by scrolling via touch + */ + protected fun onScrollInteractionBegin() {} + + protected fun onScrollInteractionEnd() {} + + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + when (event.action) { + MotionEvent.ACTION_SCROLL -> { + + // Handle mouse (or ext. device) by shifting the page depending on the scroll + val vscroll: Float + val hscroll: Float + if (event.metaState and KeyEvent.META_SHIFT_ON != 0) { + vscroll = 0f + hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + } else { + vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL) + hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) + } + if (hscroll != 0f || vscroll != 0f) { + val isForwardScroll = + if (isRtl) hscroll < 0 || vscroll < 0 else hscroll > 0 || vscroll > 0 + if (isForwardScroll) { + scrollRight() + } else { + scrollLeft() + } + return true + } + } + } + } + return super.onGenericMotionEvent(event) + } + + private fun acquireVelocityTrackerAndAddMovement(ev: MotionEvent) { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + velocityTracker!!.addMovement(ev) + } + + private fun releaseVelocityTracker() { + if (velocityTracker != null) { + velocityTracker!!.clear() + velocityTracker!!.recycle() + velocityTracker = null + } + } + + private fun onSecondaryPointerUp(ev: MotionEvent) { + val pointerIndex = + ev.action and MotionEvent.ACTION_POINTER_INDEX_MASK shr + MotionEvent.ACTION_POINTER_INDEX_SHIFT + val pointerId = ev.getPointerId(pointerIndex) + if (pointerId == activePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + val newPointerIndex = if (pointerIndex == 0) 1 else 0 + downMotionX = ev.getX(newPointerIndex) + lastMotionX = downMotionX + lastMotionXRemainder = 0f + activePointerId = ev.getPointerId(newPointerIndex) + if (velocityTracker != null) { + velocityTracker!!.clear() + } + } + } + + override fun requestChildFocus( + child: View?, + focused: View? + ) { + super.requestChildFocus(child, focused) + val page = indexToPage(indexOfChild(child)) + if (page >= 0 && page != currentPage && !isInTouchMode) { + snapToPage(page) + } + } + + fun getPageNearestToCenterOfScreen(): Int { + return getPageNearestToCenterOfScreen(scrollX) + } + + private fun getPageNearestToCenterOfScreen(scaledScrollX: Int): Int { + val screenCenter = scaledScrollX + measuredWidth / 2 + var minDistanceFromScreenCenter = Int.MAX_VALUE + var minDistanceFromScreenCenterIndex = -1 + val childCount = childCount + for (i in 0 until childCount) { + val layout = getPageAt(i) + val childWidth = layout.measuredWidth + val halfChildWidth = childWidth / 2 + val childCenter: Int = getChildOffset(i) + halfChildWidth + val distanceFromScreenCenter = Math.abs(childCenter - screenCenter) + if (distanceFromScreenCenter < minDistanceFromScreenCenter) { + minDistanceFromScreenCenter = distanceFromScreenCenter + minDistanceFromScreenCenterIndex = i + } + } + return minDistanceFromScreenCenterIndex + } + + protected fun snapToDestination() { + snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration()) + } + + protected fun isInOverScroll(): Boolean { + return overScrollX > maxScrollX || overScrollX < 0 + } + + protected fun getPageSnapDuration(): Int { + return if (isInOverScroll()) { + OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION + } else PAGE_SNAP_ANIMATION_DURATION + } + + // 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. + private fun distanceInfluenceForSnapDuration(f: Float): Float { + var f = f + f -= 0.5f // center the values about 0. + f *= (0.3f * Math.PI / 2.0f).toFloat() + return sin(f.toDouble()).toFloat() + } + + protected fun snapToPageWithVelocity(whichPage: Int, velocity: Int): Boolean { + var whichPage = whichPage + var velocity = velocity + whichPage = validateNewPage(whichPage) + val halfScreenSize = measuredWidth / 2 + val newX: Int = getScrollForPage(whichPage) + val delta: Int = newX - unboundedScrollX + var duration = 0 + if (Math.abs(velocity) < minFlingVelocity) { + // If the velocity is low enough, then treat this more as an automatic page advance + // as opposed to an apparent physical response to flinging + return snapToPage( + whichPage, + PAGE_SNAP_ANIMATION_DURATION + ) + } + + // 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. + val distanceRatio = + Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)) + val distance = halfScreenSize + halfScreenSize * + distanceInfluenceForSnapDuration(distanceRatio) + velocity = Math.abs(velocity) + velocity = Math.max(minSnapVelocity, 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)) + return snapToPage(whichPage, delta, duration) + } + + fun snapToPage(whichPage: Int): Boolean { + return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION) + } + + fun snapToPageImmediately(whichPage: Int): Boolean { + return snapToPage( + whichPage, + PAGE_SNAP_ANIMATION_DURATION, + true, + null + ) + } + + fun snapToPage(whichPage: Int, duration: Int): Boolean { + return snapToPage(whichPage, duration, false, null) + } + + fun snapToPage( + whichPage: Int, + duration: Int, + interpolator: TimeInterpolator? + ): Boolean { + return snapToPage(whichPage, duration, false, interpolator) + } + + protected fun snapToPage( + whichPage: Int, duration: Int, immediate: Boolean, + interpolator: TimeInterpolator? + ): Boolean { + var whichPage = whichPage + whichPage = validateNewPage(whichPage) + val newX: Int = getScrollForPage(whichPage) + val delta: Int = newX - unboundedScrollX + return snapToPage(whichPage, delta, duration, immediate, interpolator) + } + + protected fun snapToPage(whichPage: Int, delta: Int, duration: Int): Boolean { + return snapToPage(whichPage, delta, duration, false, null) + } + + protected fun snapToPage( + whichPage: Int, delta: Int, duration: Int, immediate: Boolean, + interpolator: TimeInterpolator? + ): Boolean { + var whichPage = whichPage + var duration = duration + if (firstLayout) { + currentPage = whichPage + return false + } + whichPage = validateNewPage(whichPage) + nextPage = whichPage + awakenScrollBars(duration) + if (immediate) { + duration = 0 + } else if (duration == 0) { + duration = Math.abs(delta) + } + if (duration != 0) { + pageBeginTransition() + } + if (!scroller.isFinished()) { + abortScrollerAnimation(false) + } + scroller.startScroll(unboundedScrollX, 0, delta, 0, duration) + updatePageIndicator() + + // Trigger a compute() to finish switching pages if necessary + if (immediate) { + computeScroll() + pageEndTransition() + } + invalidate() + return Math.abs(delta) > 0 + } + + fun scrollLeft(): Boolean { + if (nextPage > 0) { + snapToPage(nextPage - 1) + return true + } + return false + } + + fun scrollRight(): Boolean { + if (nextPage < childCount - 1) { + snapToPage(nextPage + 1) + return true + } + return false + } + + companion object { + private const val TAG = "PagedView" + + const val INVALID_PAGE = -1 + + const val PAGE_SNAP_ANIMATION_DURATION = 750 + private const val OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270 + + private const val RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f + + // Move to next page on touch up if page is moved more than this threshold + private const val SIGNIFICANT_MOVE_THRESHOLD = 0.4f + + private const val MAX_SCROLL_PROGRESS = 1.0f + + // Scaled based on density + private const val FLING_THRESHOLD_VELOCITY = 500 + private const val MIN_SNAP_VELOCITY = 1500 + private const val MIN_FLING_VELOCITY = 250 + + private const val TOUCH_STATE_REST = 0 + private const val TOUCH_STATE_SCROLLING = 1 + private const val TOUCH_STATE_PREV_PAGE = 2 + private const val TOUCH_STATE_NEXT_PAGE = 3 + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicator.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicator.kt similarity index 61% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicator.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicator.kt index e7b02e9c37..5541ba8777 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicator.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicator.kt @@ -1,7 +1,7 @@ -package foundation.e.blisslauncher.features.launcher.views.pageindicators +package foundation.e.blisslauncher.widget.pageindicators /** - * Base class for a page indicator. + * Interface for a page indicator. */ interface PageIndicator { fun setScroll(currentScroll: Int, totalScroll: Int) diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicatorDots.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicatorDots.kt similarity index 75% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicatorDots.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicatorDots.kt index cb3ee7bbad..970c790486 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/views/pageindicators/PageIndicatorDots.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicatorDots.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.features.launcher.views.pageindicators +package foundation.e.blisslauncher.widget.pageindicators import android.animation.Animator import android.animation.AnimatorListenerAdapter @@ -23,15 +23,21 @@ import kotlin.math.abs * [PageIndicator] which shows dots per page. The active page is shown with the current * accent color. */ -class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : - View(context, attrs, defStyleAttr), PageIndicator { - private val mCirclePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) - private val mDotRadius: Float - private val mActiveColor: Int - private val mInActiveColor: Int - private val mIsRtl: Boolean = false - private var mNumPages = 0 - private var mActivePage = 0 +class PageIndicatorDots @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr), PageIndicator { + private val circlePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val dotRadius: Float + private val activeColor: Int + private val inActiveColor: Int + private val isRtl: Boolean = false + private val sTempRect: RectF = RectF() + + private var numPages = 0 + private var activePage = 0 + /** * The current position of the active dot including the animation progress. * For ex: @@ -46,17 +52,23 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I private var mAnimator: ObjectAnimator? = null private var mEntryAnimationRadiusFactors: FloatArray? = null - constructor(context: Context?) : this(context, null) - constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) + init { + circlePaint.style = Paint.Style.FILL + dotRadius = resources.getDimension(R.dimen.dotSize) / 2 + outlineProvider = MyOutlineProver() + activeColor = resources.getColor(R.color.dot_on_color) + inActiveColor = resources.getColor(R.color.dot_on_color) + //mIsRtl = Utilities.isRtl(getResources()) + } override fun setScroll(currentScroll: Int, totalScroll: Int) { var currentScroll = currentScroll - if (mNumPages > 1) { + if (numPages > 1) { // Ignore this as of now. - if (mIsRtl) { + if (isRtl) { currentScroll = totalScroll - currentScroll } - val scrollPerPage = totalScroll / (mNumPages - 1) + val scrollPerPage = totalScroll / (numPages - 1) val pageToLeft = currentScroll / scrollPerPage val pageToLeftScroll = pageToLeft * scrollPerPage val pageToRightScroll = pageToLeftScroll + scrollPerPage @@ -83,12 +95,9 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I } if (mAnimator == null && mCurrentPosition.compareTo(mFinalPosition) != 0) { val positionForThisAnim = - if (mCurrentPosition > mFinalPosition) mCurrentPosition - SHIFT_PER_ANIMATION else mCurrentPosition + SHIFT_PER_ANIMATION - mAnimator = ObjectAnimator.ofFloat( - this, - CURRENT_POSITION, - positionForThisAnim - ).apply { + if (mCurrentPosition > mFinalPosition) mCurrentPosition - SHIFT_PER_ANIMATION + else mCurrentPosition + SHIFT_PER_ANIMATION + mAnimator = ObjectAnimator.ofFloat(this, CURRENT_POSITION, positionForThisAnim).apply { addListener(AnimationCycleListener()) duration = ANIMATION_DURATION } @@ -101,7 +110,7 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I mAnimator!!.cancel() mAnimator = null } - mFinalPosition = mActivePage.toFloat() + mFinalPosition = activePage.toFloat() CURRENT_POSITION.set(this, mFinalPosition) } @@ -110,7 +119,7 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I * [.playEntryAnimation] must be called after this. */ fun prepareEntryAnimation() { - mEntryAnimationRadiusFactors = FloatArray(mNumPages) + mEntryAnimationRadiusFactors = FloatArray(numPages) invalidate() } @@ -148,59 +157,62 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I } override fun setActiveMarker(activePage: Int) { - if (mActivePage != activePage) { - mActivePage = activePage + if (this.activePage != activePage) { + this.activePage = activePage } } override fun setMarkersCount(numMarkers: Int) { - mNumPages = numMarkers + numPages = numMarkers requestLayout() } override fun onMeasure( widthMeasureSpec: Int, heightMeasureSpec: Int - ) { // Add extra spacing of mDotRadius on all sides so that entry animation could be run. + ) { + // Add extra spacing of mDotRadius on all sides so that entry animation could be run. val width = if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) MeasureSpec.getSize( widthMeasureSpec - ) else ((mNumPages * 3 + 2) * mDotRadius).toInt() + ) else ((numPages * 3 + 2) * dotRadius).toInt() val height = if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) MeasureSpec.getSize( heightMeasureSpec - ) else (4 * mDotRadius).toInt() + ) else (4 * dotRadius).toInt() setMeasuredDimension(width, height) } - override fun onDraw(canvas: Canvas) { // Draw all page indicators; - var circleGap = 3 * mDotRadius - val startX = (width - mNumPages * circleGap + mDotRadius) / 2 - var x = startX + mDotRadius + override fun onDraw(canvas: Canvas) { + // Draw all page indicators; + var circleGap = 3 * dotRadius + val startX = (width - numPages * circleGap + dotRadius) / 2 + var x = startX + dotRadius val y = height / 2.toFloat() - if (mEntryAnimationRadiusFactors != null) { // During entry animation, only draw the circles - if (mIsRtl) { + if (mEntryAnimationRadiusFactors != null) { + // During entry animation, only draw the circles + if (isRtl) { x = getWidth() - x circleGap = -circleGap } for (i in mEntryAnimationRadiusFactors!!.indices) { - mCirclePaint.setColor(if (i == mActivePage) mActiveColor else mInActiveColor) + circlePaint.setColor(if (i == activePage) activeColor else inActiveColor) canvas.drawCircle( x, y, - mDotRadius * mEntryAnimationRadiusFactors!![i], - mCirclePaint + dotRadius * mEntryAnimationRadiusFactors!![i], + circlePaint ) x += circleGap } } else { - mCirclePaint.color = mInActiveColor - for (i in 0 until mNumPages) { - canvas.drawCircle(x, y, mDotRadius, mCirclePaint) + circlePaint.color = inActiveColor + for (i in 0 until numPages) { + canvas.drawCircle(x, y, dotRadius, circlePaint) x += circleGap } - mCirclePaint.setColor(mActiveColor) - canvas.drawRoundRect(activeRect, mDotRadius, mDotRadius, mCirclePaint) + circlePaint.color = activeColor + canvas.drawRoundRect(activeRect, dotRadius, dotRadius, circlePaint) } } // Dot is leaving the left circle. @@ -209,11 +221,11 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I get() { val startCircle: Float = mCurrentPosition var delta = mCurrentPosition - startCircle - val diameter = 2 * mDotRadius - val circleGap = 3 * mDotRadius - val startX = (width - mNumPages * circleGap + mDotRadius) / 2 - sTempRect!!.top = height * 0.5f - mDotRadius - sTempRect.bottom = height * 0.5f + mDotRadius + val diameter = 2 * dotRadius + val circleGap = 3 * dotRadius + val startX = (width - numPages * circleGap + dotRadius) / 2 + sTempRect!!.top = height * 0.5f - dotRadius + sTempRect.bottom = height * 0.5f + dotRadius sTempRect.left = startX + startCircle * circleGap sTempRect.right = sTempRect.left + diameter @@ -224,7 +236,7 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I delta -= SHIFT_PER_ANIMATION sTempRect.left += delta * circleGap * 2 } - if (mIsRtl) { + if (isRtl) { val rectWidth = sTempRect.width() sTempRect.right = width - sTempRect.left @@ -243,7 +255,7 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I activeRect.top.toInt(), activeRect.right.toInt(), activeRect.bottom.toInt(), - mDotRadius + dotRadius ) } } @@ -273,9 +285,9 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I private const val ENTER_ANIMATION_START_DELAY = 300 private const val ENTER_ANIMATION_STAGGERED_DELAY = 150 private const val ENTER_ANIMATION_DURATION = 400 + // This value approximately overshoots to 1.5 times the original size. private const val ENTER_ANIMATION_OVERSHOOT_TENSION = 4.9f - private val sTempRect: RectF? = RectF() private val CURRENT_POSITION: Property = object : Property( Float::class.java, "current_position" @@ -291,13 +303,4 @@ class PageIndicatorDots(context: Context?, attrs: AttributeSet?, defStyleAttr: I } } } - - init { - mCirclePaint.style = Paint.Style.FILL - mDotRadius = resources.getDimension(R.dimen.dotSize) / 2 - outlineProvider = MyOutlineProver() - mActiveColor = resources.getColor(R.color.dot_on_color) - mInActiveColor = resources.getColor(R.color.dot_on_color) - //mIsRtl = Utilities.isRtl(getResources()) - } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/values/colors.xml b/blisslauncherv2/src/main/res/values/colors.xml index 2566ee833e..f6fbd75afb 100644 --- a/blisslauncherv2/src/main/res/values/colors.xml +++ b/blisslauncherv2/src/main/res/values/colors.xml @@ -3,5 +3,6 @@ #008577 #00574B #D81B60 - #f00 + #88FFFFFF + #FFFFFFFF diff --git a/blisslauncherv2/src/test/java/foundation/e/blisslauncher/base/presentation/BaseViewModelTest.kt b/blisslauncherv2/src/test/java/foundation/e/blisslauncher/features/base/presentation/BaseViewModelTest.kt similarity index 95% rename from blisslauncherv2/src/test/java/foundation/e/blisslauncher/base/presentation/BaseViewModelTest.kt rename to blisslauncherv2/src/test/java/foundation/e/blisslauncher/features/base/presentation/BaseViewModelTest.kt index 65831f3f08..8189767028 100644 --- a/blisslauncherv2/src/test/java/foundation/e/blisslauncher/base/presentation/BaseViewModelTest.kt +++ b/blisslauncherv2/src/test/java/foundation/e/blisslauncher/features/base/presentation/BaseViewModelTest.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.base.presentation +package foundation.e.blisslauncher.features.base.presentation import foundation.e.blisslauncher.common.subscribeToState import io.mockk.mockk -- GitLab From cf484d28a082dcd7698d8982a618a262b98b8836 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 28 May 2020 12:15:02 +0530 Subject: [PATCH 20/23] Add hotseat and default hotseat parser --- .idea/dictionaries/amit.xml | 7 + blisslauncherv2/src/main/AndroidManifest.xml | 3 +- .../WallpaperChangeReceiver.java | 48 ++ .../e/blisslauncher}/badge/BadgeRenderer.java | 2 +- .../util}/SystemUiController.kt | 2 +- .../{utils => common/util}/TraceHelper.kt | 2 +- .../e/blisslauncher/features/LauncherStore.kt | 58 ++- .../features/base/BaseActivity.kt | 2 +- .../features/launcher/Hotseat.kt | 33 ++ .../features/launcher/LauncherActivity.kt | 108 +++-- .../launcher/LauncherActivityModule.kt | 2 +- .../features/launcher/LauncherState.kt | 427 +---------------- .../features/launcher/LauncherStateTemp.kt | 433 ++++++++++++++++++ .../features/launcher/LauncherView.kt | 9 +- .../features/launcher/Workspace.kt | 422 +++++++++++++++++ .../e/blisslauncher/inject/AppModule.kt | 5 + .../e/blisslauncher/widget/Insettable.kt | 10 + .../widget/InsettableFrameLayout.kt | 97 ++++ .../e/blisslauncher/widget/PagedView.kt | 56 ++- .../src/main/res/layout/activity_launcher.xml | 20 + .../src/main/res/layout/activity_main.xml | 3 - .../res/layout/layout_workspace_screen.xml | 5 + blisslauncherv2/src/main/res/values/attrs.xml | 3 + .../src/main/res/values/styles.xml | 10 + .../base/presentation/BaseViewModelTest.kt | 55 --- common/src/main/AndroidManifest.xml | 2 +- .../e/blisslauncher/common}/DeviceProfile.kt | 46 +- .../common}/InvariantDeviceProfile.kt | 155 +++---- .../blisslauncher/common/LauncherConstants.kt | 36 ++ .../common/util/LabelComparator.kt | 25 + .../{utils => common/util}/LongArrayMap.kt | 2 +- .../{utils => common/util}/MultiHashMap.java | 2 +- common/src/main/res/values/attrs.xml | 2 +- common/src/main/res/values/config.xml | 2 + common/src/main/res/values/dimens.xml | 30 ++ .../main/res/xml/default_workspace_4x4.xml | 47 -- .../main/res/xml/default_workspace_5x5.xml | 48 -- .../main/res/xml/default_workspace_5x6.xml | 37 -- common/src/main/res/xml/device_profiles.xml | 47 +- common/src/main/res/xml/dw_hotseat_3.xml | 53 +++ ...ult_workspace_3x3.xml => dw_hotseat_4.xml} | 71 +-- ...{dw_phone_hotseat.xml => dw_hotseat_5.xml} | 17 +- ...dw_tablet_hotseat.xml => dw_hotseat_6.xml} | 46 +- .../data/LauncherDatabaseGateway.kt | 136 +++++- .../data/PackageManagerHelper.kt | 3 +- ...toryImpl.kt => WorkspaceRepositoryImpl.kt} | 227 ++++++--- .../data/WorkspaceScreenRepositoryImpl.kt | 50 ++ .../data/compat/UserManagerCompatVN.kt | 2 +- .../data/database/BlissLauncherDatabase.kt | 9 +- .../data/database/dao/LauncherDao.kt | 38 ++ .../data/database/dao/LauncherItemDao.kt | 6 - ...cherItemRoomEntity.kt => WorkspaceItem.kt} | 10 +- .../{ => roomentity}/WorkspaceScreen.kt | 4 +- .../e/blisslauncher/data/icon/IconCache.kt | 2 +- .../data/inject/DataRepoBindingModule.kt | 24 +- .../data/parser/AppShortcutParser.kt | 71 +++ .../data/parser/AppShortcutWithUriParser.kt | 102 +++++ .../data/parser/DefaultHotseatParser.kt | 165 +++++++ .../blisslauncher/data/parser/ParseResult.kt | 12 + .../data/parser/ResolveParser.kt | 40 ++ .../e/blisslauncher/data/parser/TagParser.kt | 15 + .../data/util/LauncherItemComparator.kt | 33 ++ domain/build.gradle | 1 + .../e/blisslauncher/domain/Functions.kt | 2 +- .../domain/dto/WorkspaceModel.kt | 94 +++- .../domain/entity/ApplicationItem.kt | 2 +- .../blisslauncher/domain/entity/FolderItem.kt | 12 +- .../domain/entity/LauncherConstants.kt | 6 +- .../domain/entity/LauncherItem.kt | 2 +- .../{AppShortcutItem.kt => ShortcutItem.kt} | 4 +- .../domain/inject/DomainComponent.kt | 5 + .../domain/interactor/Interactor.kt | 11 +- .../domain/interactor/LoadLauncher.kt | 42 +- .../domain/interactor/UpdateLauncher.kt | 6 +- .../domain/repository/Repository.kt | 10 +- .../domain/repository/WorkspaceRepository.kt | 7 + .../repository/WorkspaceScreenRepository.kt | 4 +- .../mvicore/component/BaseStore.kt | 4 +- .../mvicore/component/MviView.kt | 5 +- .../blisslauncher/mvicore/component/Store.kt | 2 +- 80 files changed, 2604 insertions(+), 1054 deletions(-) create mode 100644 .idea/dictionaries/amit.xml create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/WallpaperChangeReceiver.java rename {data/src/main/java/foundation/e/blisslauncher/data => blisslauncherv2/src/main/java/foundation/e/blisslauncher}/badge/BadgeRenderer.java (98%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{utils => common/util}/SystemUiController.kt (97%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{utils => common/util}/TraceHelper.kt (97%) create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherStateTemp.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/Insettable.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/InsettableFrameLayout.kt create mode 100644 blisslauncherv2/src/main/res/layout/activity_launcher.xml delete mode 100644 blisslauncherv2/src/main/res/layout/activity_main.xml create mode 100644 blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml delete mode 100644 blisslauncherv2/src/test/java/foundation/e/blisslauncher/features/base/presentation/BaseViewModelTest.kt rename {data/src/main/java/foundation/e/blisslauncher/data => common/src/main/java/foundation/e/blisslauncher/common}/DeviceProfile.kt (97%) rename {data/src/main/java/foundation/e/blisslauncher/data => common/src/main/java/foundation/e/blisslauncher/common}/InvariantDeviceProfile.kt (81%) create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/LauncherConstants.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/util/LabelComparator.kt rename common/src/main/java/foundation/e/blisslauncher/{utils => common/util}/LongArrayMap.kt (94%) rename common/src/main/java/foundation/e/blisslauncher/{utils => common/util}/MultiHashMap.java (96%) create mode 100644 common/src/main/res/values/dimens.xml delete mode 100644 common/src/main/res/xml/default_workspace_4x4.xml delete mode 100644 common/src/main/res/xml/default_workspace_5x5.xml delete mode 100644 common/src/main/res/xml/default_workspace_5x6.xml create mode 100644 common/src/main/res/xml/dw_hotseat_3.xml rename common/src/main/res/xml/{default_workspace_3x3.xml => dw_hotseat_4.xml} (57%) rename common/src/main/res/xml/{dw_phone_hotseat.xml => dw_hotseat_5.xml} (82%) rename common/src/main/res/xml/{dw_tablet_hotseat.xml => dw_hotseat_6.xml} (72%) rename data/src/main/java/foundation/e/blisslauncher/data/{LauncherItemRepositoryImpl.kt => WorkspaceRepositoryImpl.kt} (67%) create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/WorkspaceScreenRepositoryImpl.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt delete mode 100644 data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherItemDao.kt rename data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/{LauncherItemRoomEntity.kt => WorkspaceItem.kt} (92%) rename data/src/main/java/foundation/e/blisslauncher/data/database/{ => roomentity}/WorkspaceScreen.kt (83%) create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutParser.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutWithUriParser.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/parser/DefaultHotseatParser.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/parser/ParseResult.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/parser/ResolveParser.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/parser/TagParser.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/util/LauncherItemComparator.kt rename domain/src/main/java/foundation/e/blisslauncher/domain/entity/{AppShortcutItem.kt => ShortcutItem.kt} (96%) create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceRepository.kt diff --git a/.idea/dictionaries/amit.xml b/.idea/dictionaries/amit.xml new file mode 100644 index 0000000000..217011ced4 --- /dev/null +++ b/.idea/dictionaries/amit.xml @@ -0,0 +1,7 @@ + + + + hotseat + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/AndroidManifest.xml b/blisslauncherv2/src/main/AndroidManifest.xml index bf1b03f521..79c8b664f5 100644 --- a/blisslauncherv2/src/main/AndroidManifest.xml +++ b/blisslauncherv2/src/main/AndroidManifest.xml @@ -10,7 +10,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> - + diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/WallpaperChangeReceiver.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/WallpaperChangeReceiver.java new file mode 100644 index 0000000000..0d4c0c14a8 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/WallpaperChangeReceiver.java @@ -0,0 +1,48 @@ +package foundation.e.blisslauncher; + +import android.app.WallpaperManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.IBinder; +import android.view.View; + +import static android.content.Context.WALLPAPER_SERVICE; + +public class WallpaperChangeReceiver extends BroadcastReceiver { + private final Context mContext; + private IBinder mWindowToken; + private boolean mRegistered; + private View mWorkspace; + + public WallpaperChangeReceiver(View workspace){ + this.mWorkspace = workspace; + this.mContext = mWorkspace.getContext(); + } + + @Override + public void onReceive(Context context, Intent intent) { + //BlurWallpaperProvider.Companion.getInstance(context).updateAsync(); + updateOffset(); + } + + public void setWindowToken(IBinder token) { + mWindowToken = token; + if (mWindowToken == null && mRegistered) { + mWorkspace.getContext().unregisterReceiver(this); + mRegistered = false; + } else if (mWindowToken != null && !mRegistered) { + mWorkspace.getContext() + .registerReceiver(this, new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)); + onReceive(mWorkspace.getContext(), null); + mRegistered = true; + } + } + + private void updateOffset() { + WallpaperManager wm = (WallpaperManager) mContext.getSystemService(WALLPAPER_SERVICE); + wm.setWallpaperOffsets(mWindowToken, 0f, 0.5f); + wm.setWallpaperOffsetSteps(0.0f, 0.0f); + } +} diff --git a/data/src/main/java/foundation/e/blisslauncher/data/badge/BadgeRenderer.java b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/badge/BadgeRenderer.java similarity index 98% rename from data/src/main/java/foundation/e/blisslauncher/data/badge/BadgeRenderer.java rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/badge/BadgeRenderer.java index f094ea978f..bd71869f3d 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/badge/BadgeRenderer.java +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/badge/BadgeRenderer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package foundation.e.blisslauncher.data.badge; +package foundation.e.blisslauncher.badge; import android.graphics.Canvas; import android.graphics.Paint; diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/SystemUiController.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt similarity index 97% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/SystemUiController.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt index 21b0e83476..2693475ad0 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/SystemUiController.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/SystemUiController.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.utils +package foundation.e.blisslauncher.common.util import android.view.View import android.view.Window diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/TraceHelper.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt similarity index 97% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/TraceHelper.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt index 148bca426d..c811c3c374 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/utils/TraceHelper.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/common/util/TraceHelper.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.utils +package foundation.e.blisslauncher.common.util import android.os.SystemClock import android.os.Trace diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt index 16b3c00375..32ce658f39 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/LauncherStore.kt @@ -1,32 +1,70 @@ package foundation.e.blisslauncher.features -import foundation.e.blisslauncher.features.LauncherStore.Intent +import foundation.e.blisslauncher.domain.dto.WorkspaceModel +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.interactor.LoadLauncher +import foundation.e.blisslauncher.features.LauncherStore.LauncherIntent import foundation.e.blisslauncher.features.LauncherStore.Action import foundation.e.blisslauncher.features.LauncherStore.Effect -import foundation.e.blisslauncher.features.LauncherStore.State import foundation.e.blisslauncher.features.LauncherStore.News +import foundation.e.blisslauncher.features.launcher.LauncherState +import foundation.e.blisslauncher.mvicore.component.Actor import foundation.e.blisslauncher.mvicore.component.BaseStore +import foundation.e.blisslauncher.mvicore.component.IntentToAction +import foundation.e.blisslauncher.mvicore.component.Reducer +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import timber.log.Timber +import javax.inject.Inject -class LauncherStore : - BaseStore() { - - sealed class Intent { +class LauncherStore @Inject constructor(loadLauncher: LoadLauncher) : + BaseStore( + LauncherState.Loading, + IntentToActionImpl(), + ActorImpl(loadLauncher), + ReducerImpl() + ) { + sealed class LauncherIntent { + object InitialIntent : LauncherIntent() } sealed class Effect { + object Loading : Effect() + object ErrorLoading : Effect() + data class LoadedResponse(val workspaceModel: WorkspaceModel) : Effect() } - sealed class News { - - } + sealed class News sealed class Action { + object LoadLauncher : Action() + } + class IntentToActionImpl : IntentToAction { + override fun invoke(intent: LauncherIntent): Action = when (intent) { + is LauncherIntent.InitialIntent -> Action.LoadLauncher + } } - data class State(val name: String) { + class ActorImpl(private val loadLauncher: LoadLauncher) : Actor { + override fun invoke(state: LauncherState, action: Action): Observable { + return loadLauncher().toObservable() + .observeOn(AndroidSchedulers.mainThread()) + .map { Effect.LoadedResponse(it) as Effect } + .startWith(Effect.Loading) + .onErrorReturn { Effect.ErrorLoading } + } + } + class ReducerImpl : Reducer { + override fun invoke(state: LauncherState, effect: Effect): LauncherState { + return when(effect) { + Effect.Loading -> LauncherState.Loading + Effect.ErrorLoading -> LauncherState.Error + is Effect.LoadedResponse -> LauncherState.Loaded(effect.workspaceModel) + } + } } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt index b7bf0845c7..c351f3d8dd 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/base/BaseActivity.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import android.content.ContextWrapper import androidx.annotation.IntDef -import foundation.e.blisslauncher.utils.SystemUiController +import foundation.e.blisslauncher.common.util.SystemUiController import javax.inject.Inject open class BaseActivity : Activity() { diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.kt new file mode 100644 index 0000000000..86dd22b2ee --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Hotseat.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.features.launcher + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.GridLayout +import foundation.e.blisslauncher.widget.Insettable + +class Hotseat @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : GridLayout(context, attrs, defStyleAttr), Insettable { + + private val launcher: LauncherActivity = LauncherActivity.getLauncher(context) + + override fun setInsets(insets: Rect) { + val lp = getLayoutParams() as FrameLayout.LayoutParams + val idp = launcher.deviceProfile + lp.gravity = Gravity.BOTTOM + lp.width = ViewGroup.LayoutParams.MATCH_PARENT + lp.height = idp.hotseatBarSizePx + insets!!.bottom + + val padding: Rect = idp.hotseatLayoutPadding + setPadding(padding.left, padding.top, padding.right, padding.bottom) + + layoutParams = lp + + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt index 59c185d16f..2f0c002cba 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt @@ -1,40 +1,56 @@ package foundation.e.blisslauncher.features.launcher import android.app.ActivityOptions +import android.content.Context +import android.content.ContextWrapper import android.content.res.Configuration import android.os.Bundle import android.os.StrictMode import android.os.StrictMode.VmPolicy import android.view.View +import android.widget.GridLayout +import android.widget.ImageView import dagger.android.AndroidInjection -import foundation.e.blisslauncher.features.base.BaseDraggingActivity -import foundation.e.blisslauncher.features.base.presentation.BaseIntent -import foundation.e.blisslauncher.common.subscribeToState -import foundation.e.blisslauncher.utils.TraceHelper +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.common.DeviceProfile +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.common.util.TraceHelper +import foundation.e.blisslauncher.domain.dto.WorkspaceModel import foundation.e.blisslauncher.domain.entity.LauncherItem -import foundation.e.blisslauncher.domain.interactor.LoadLauncher -import foundation.e.blisslauncher.domain.keys.PackageUserKey +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.features.LauncherStore +import foundation.e.blisslauncher.features.base.BaseDraggingActivity import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign -import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import kotlinx.android.synthetic.main.activity_launcher.* +import timber.log.Timber +import java.util.ArrayList import javax.inject.Inject class LauncherActivity : BaseDraggingActivity(), LauncherView { private lateinit var oldConfig: Configuration - private lateinit var loadLauncher: LoadLauncher - private val compositeDisposable: CompositeDisposable = CompositeDisposable() - private val loadLauncherIntentPublisher = BehaviorSubject.create() + private val intentSubject = PublishSubject.create() + + override val events: Observable + get() = intentSubject @Inject - lateinit var launcherViewModel: LauncherViewModel + lateinit var launcherStore: LauncherStore + + @Inject + lateinit var idp: InvariantDeviceProfile + + lateinit var deviceProfile: DeviceProfile override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) + deviceProfile = idp.getDeviceProfile(this) if (DEBUG_STRICT_MODE) { StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder() @@ -54,6 +70,8 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { ) } + setContentView(R.layout.activity_launcher) + TraceHelper.beginSection("Launcher-onCreate") super.onCreate(savedInstanceState) @@ -61,20 +79,11 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { oldConfig = Configuration(resources.configuration) - compositeDisposable += launcherViewModel.states().subscribeToState { render(it) } + compositeDisposable += Observable.wrap(launcherStore).subscribe { render(it) } //TODO set model and state here + compositeDisposable += events.subscribe(launcherStore) - compositeDisposable += intents().subscribe { launcherViewModel::process } - } - - override fun intents(): Observable> { - return Observable.merge(initialIntent(), - loadLauncherIntentPublisher.map { launcherViewModel.toIntent(it) } - ) - } - - private fun initialIntent(): Observable> { - return loadLauncherIntentPublisher.map { launcherViewModel.toIntent(it) } + intentSubject.onNext(LauncherStore.LauncherIntent.InitialIntent) } override fun getRootView(): View { @@ -91,7 +100,6 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { override fun onDestroy() { super.onDestroy() - launcherViewModel.terminate() } companion object { @@ -112,20 +120,68 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { // Type: int private const val RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen" + // Type: int private const val RUNTIME_STATE = "launcher.state" + // Type: PendingRequestArgs private const val RUNTIME_STATE_PENDING_REQUEST_ARGS = "launcher.request_args" + // Type: ActivityResultInfo private const val RUNTIME_STATE_PENDING_ACTIVITY_RESULT = "launcher.activity_result" + // Type: SparseArray private const val RUNTIME_STATE_WIDGET_PANEL = "launcher.widget_panel" - } - override fun updateIconBadges(updatedBadges: Set) { + fun getLauncher(context: Context): LauncherActivity { + return if (context is LauncherActivity) { + context + } else (context as ContextWrapper).baseContext as LauncherActivity + } } override fun render(state: LauncherState) { + Timber.d("Current state is $state") + if (state is LauncherState.Loaded) { + val model = state.workspaceModel + bindScreens(model) + + bindWorkspaceItems(model.workspaceItems) + } + } + + private fun bindScreens(model: WorkspaceModel) { + workspace.addExtraEmptyScreen() + model.workspaceScreens.forEach { + workspace.insertNewWorkspaceScreenBeforeEmptyScreen(it) + } + + Timber.d("Total child in workspace is: ${workspace.childCount}") + } + + private fun bindWorkspaceItems(workspaceItems: ArrayList) { + workspaceItems.forEach { + if (it is LauncherItemWithIcon) { + val view = ImageView(this) + view.setImageBitmap(it.iconBitmap) + val lp = GridLayout.LayoutParams() + lp.width = deviceProfile.iconSizePx + lp.height = deviceProfile.iconSizePx + if (it.screenId >= 0) { + workspace.addInScreenFromBind(view, it) + } else { + var currentScreen = workspace.getChildAt(workspace.childCount - 1) as GridLayout + if (currentScreen.childCount < deviceProfile.inv.numRows * deviceProfile.inv.numColumns) { + currentScreen.addView(view) + } else { + workspace.insertNewWorkspaceScreen(workspace.childCount.toLong()) + currentScreen = + workspace.getChildAt(workspace.childCount - 1) as GridLayout + currentScreen.addView(view) + } + } + } + } } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt index a2acd0a171..da745e6cb8 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivityModule.kt @@ -2,7 +2,7 @@ package foundation.e.blisslauncher.features.launcher import dagger.Module import dagger.Provides -import foundation.e.blisslauncher.utils.SystemUiController +import foundation.e.blisslauncher.common.util.SystemUiController @Module class LauncherActivityModule { diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt index 95a4eb6986..a07a04d2b4 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherState.kt @@ -1,435 +1,30 @@ package foundation.e.blisslauncher.features.launcher -import android.content.ComponentName -import android.content.Context -import android.content.pm.LauncherActivityInfo -import android.os.UserHandle -import androidx.core.util.set -import foundation.e.blisslauncher.utils.LongArrayMap -import foundation.e.blisslauncher.domain.ItemInfoMatcher -import foundation.e.blisslauncher.domain.Matcher -import foundation.e.blisslauncher.domain.addFlag -import foundation.e.blisslauncher.domain.entity.ApplicationItem -import foundation.e.blisslauncher.domain.entity.FolderItem -import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.dto.WorkspaceModel import foundation.e.blisslauncher.domain.entity.LauncherItem -import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon -import foundation.e.blisslauncher.domain.entity.AppShortcutItem -import foundation.e.blisslauncher.domain.removeFlag -import javax.inject.Inject -/** - * Stores data related to Launcher in memory. - */ -data class LauncherState @Inject constructor( - /*val context: Context, - val launcherApps: LauncherAppsCompat,*/ - /** - * Map of all the items (apps, shortcuts, folder or widgets) to their ids - */ - val itemsIdMap: LongArrayMap, - - /** - * List of all apps, folders, shortcuts and widgets directly on screen - * (no apps, shortcuts within folders). - */ - val allItems: List, - - /** - * Map of all the folders to their ids - */ - val folders: LongArrayMap, - - /** - * Ordered list of workspace screen ids - */ - val workspaceScreen: List, - - /** The list of all apps. */ - val data: List -) { - - @Synchronized - fun clear() { - itemsIdMap.clear() - folders.clear() - } - - /*override fun getAllActivities( - user: UserHandle, - quietMode: Boolean - ): List { - val apps = launcherApps.getActivityList(null, user) - if (apps.isNotEmpty()) { - apps.forEach { - add(ApplicationItem(it, user, quietMode), it) - } - } - return data - } - - override fun add( - packageName: String, - user: UserHandle, - quietMode: Boolean - ): ArrayList { - val addedPackageApps = ArrayList() - val matches = launcherApps.getActivityList(packageName, user) - matches.forEach { info -> - add(ApplicationItem( - info, user, quietMode - ).apply { - id = System.nanoTime() - }.let { - addedPackageApps.add(it) - it - }, - info - ) - } - return addedPackageApps - } -*/ - fun remove(packageName: String, user: UserHandle) { - val data = data - val iterator = data.iterator() - /*while (iterator.hasNext()) { - val item = iterator.next() - if (item.componentName.packageName == packageName && item.user == user) { - removed.add(item) - iterator.remove() - } - }*/ - } - - /*override fun updatedPackages( - packages: Array, - user: UserHandle, - quietMode: Boolean - ): List { - //TODO: Update icon cache for packages - val addedApps = ArrayList() - val modifiedApps = ArrayList() - - val removedPackages = HashSet() - val removedComponents = HashSet() - - packages.forEach { - if (!launcherApps.isPackageEnabledForProfile(it, user)) { - removedPackages.add(it) - } else { - val matches = launcherApps.getActivityList(it, user) - if (matches.isNotEmpty()) { - removedComponents.addAll( - removeIfNoActivityFound( - context, - matches, - it, - user - ) - ) - - matches.forEach { - var applicationItem = - findApplicationItem(it.componentName, user) - if (applicationItem == null) { - applicationItem = - ApplicationItem(it, user, quietMode) - add(applicationItem, it) - addedApps.add(applicationItem) - } else { - //TODO: update icon and title - modifiedApps.add(applicationItem) - } - } - } else { - removedPackages.add(it) - } - } - } - val flagOp = removeFlag(LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE) - val matcher = Matcher.ofPackages(packages.toHashSet(), user) - itemsIdMap.forEach { - //TODO: If user and packageSet of icon resource equals, - //TODO: Update item flag here. - } - return modifiedApps - }*/ - - fun suspendPackages( - packages: Array, - user: UserHandle - ): List { - val matcher: ItemInfoMatcher = Matcher.ofPackages(packages.toHashSet(), user) - return updateDisabledFlags( - matcher, - addFlag(LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED) - ) - } - - fun unsuspendPackages( - packages: Array, - user: UserHandle - ): List { - val matcher: ItemInfoMatcher = Matcher.ofPackages(packages.toHashSet(), user) - return updateDisabledFlags( - matcher, - removeFlag(LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED) - ) - } - - fun updateUserAvailability(user: UserHandle, quietMode: Boolean): List { - val matcher: ItemInfoMatcher = Matcher.ofUser(user) - val flagOp = if (quietMode) addFlag else removeFlag - return updateDisabledFlags( - matcher, - flagOp(LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER) - ) - } - - fun makePackagesUnavailable( - packages: Array, - user: UserHandle - ): List { - val matcher = Matcher.ofPackages(packages.toHashSet(), user) - return updateDisabledFlags( - matcher, - addFlag(LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE) - ) - } - - fun removePackages(packages: Array, user: UserHandle): List { - val removedPackages = packages.toHashSet() - val matcher = Matcher.ofPackages(removedPackages, user) - // Remove any queued items from the install queue - //TODO: InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser) - return removePackages(matcher) - } - - @Synchronized - fun removeItem(context: Context, vararg items: LauncherItem) { - items.forEach { - when (it.itemType) { - LauncherConstants.ItemType.FOLDER -> { - folders.remove(it.id) - //allItems.remove(it) - } - LauncherConstants.ItemType.APPLICATION, - LauncherConstants.ItemType.SHORTCUT -> { - //allItems.remove(it) - } - } - itemsIdMap.remove(it.id) - } - } - - @Synchronized - fun addItem(item: LauncherItem, newItem: Boolean): LauncherState { - val mutableAllItems = allItems.toMutableList() - when (item.itemType) { - LauncherConstants.ItemType.FOLDER -> { - folders.put(item.id, item as FolderItem) - mutableAllItems.add(item) - } - LauncherConstants.ItemType.APPLICATION, - LauncherConstants.ItemType.SHORTCUT -> { - if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP || - item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT - ) { - mutableAllItems.add(item) - } else { - if (newItem) { - if (!folders.containsKey(item.container)) { - } - } else { - findOrMakeFolder(item.container).add(item as AppShortcutItem, false) - } - } - } - } - return copy(allItems = mutableAllItems) - } - - fun findOrMakeFolder(id: Long): FolderItem { - var folderItem: FolderItem? = folders[id] - if (folderItem == null) { - folderItem = FolderItem() - folders[id] = folderItem - } - return folderItem - } +sealed class LauncherState { /** - * Returns whether *apps* contains *component*. + * Very initial launcher state when there is nothing going on. */ - fun checkForComponent( - apps: List, - component: ComponentName - ): Boolean { - for (info in apps) { - if (info.componentName == component) { - return true - } - } - return false - } + object Empty : LauncherState() /** - * Finds an application item corresponding to the given component name and user. + * Launcher State when launcher is loading its content */ - fun findApplicationItem( - componentName: ComponentName, - user: UserHandle - ): ApplicationItem? { - for (item in data) { - if (componentName == item.componentName && user == item.user) { - return item - } - } - return null - } - - /*fun add(item: ApplicationItem, info: LauncherActivityInfo): LauncherState { - if (findApplicationItem(item.componentName, item.user) != null) { - return - } - // TODO: Update icon from IconCache - val mutableData = data.toMutableList() - mutableData.add(item) - return copy(data = mutableData) - data.add(item) - addItem(item, true) - }*/ - - @Synchronized - fun updateDisabledFlags( - matcher: ItemInfoMatcher, - flagOp: (oldFlags: Int) -> Int - ): List { - val updatedItems = ArrayList() - itemsIdMap.filter { - it is LauncherItemWithIcon && matcher(it, it.getTargetComponent()!!) - }.forEach { - it as LauncherItemWithIcon - val oldFlags = it.runtimeStatusFlags - it.apply { flagOp(runtimeStatusFlags) } - if (it.runtimeStatusFlags != oldFlags) - updatedItems.add(it) - } - return updatedItems - } - - @Synchronized - fun removePackages( - matches: (item: LauncherItem, cn: ComponentName) -> Boolean - ): List { - val removedItems = HashSet() - itemsIdMap.forEach { - if (it is LauncherItemWithIcon) it.let { - val cn = it.getTargetComponent() - if (cn != null && matches(it, cn)) removedItems.add(it) - } else if (it is FolderItem) it.let { folder -> - folder.contents.forEach { - val cn = it.getTargetComponent() - if (cn != null && matches(it, cn)) removedItems.add(it) - } - } - } - //TODO: delete items from database sequentially and remove them from itemsIdMap - return removedItems.toList() - } + object Loading : LauncherState() + object Error : LauncherState() { - @Synchronized - fun removeIfNoActivityFound( - context: Context, - matches: List, - packageName: String, - user: UserHandle - ): HashSet { - val removedComponents = HashSet() - val modified = ArrayList() - itemsIdMap.filter { it.itemType == LauncherConstants.ItemType.APPLICATION } - .forEach { - it as ApplicationItem - if (it.user == user && packageName == it.componentName.packageName) { - if (!findActivity(matches, it.componentName)) { - removedComponents.add(it.componentName) - } - } - } - return removedComponents } /** - * Returns whether *apps* contains *component*. + * Launcher State when launcher finished its loading and available to show its data */ - private fun findActivity( - apps: List, - component: ComponentName - ): Boolean { - for (info in apps) { - if (info.componentName == component) { - return true - } - } - return false - } + data class Loaded(val workspaceModel: WorkspaceModel) : LauncherState() /** - * Find an AppInfo object for the given componentName - * - * @return the corresponding AppInfo or null + * Launcher State when launcher swipe to search down is invoked. */ - fun findAppInfo( - componentName: ComponentName, - user: UserHandle - ): ApplicationItem? { - for (item in itemsIdMap) { - if (item is ApplicationItem && - componentName == item.componentName && - user == item.user - ) { - return item - } - } - return null - } - - fun addItem( - item: LauncherItem, - mutableData: MutableList, - mutableAllItems: MutableList, - newItem: Boolean - ) { - mutableData.add(item as ApplicationItem) - itemsIdMap.put(item.id, item) - when (item.itemType) { - LauncherConstants.ItemType.FOLDER -> { - folders.put(item.id, item as FolderItem) - mutableAllItems.add(item) - } - LauncherConstants.ItemType.APPLICATION, - LauncherConstants.ItemType.SHORTCUT -> { - if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP || - item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT - ) { - mutableAllItems.add(item) - } else { - if (newItem) { - if (!folders.containsKey(item.container)) { - } - } else { - findOrMakeFolder(item.container).add( - item as AppShortcutItem, - false - ) - } - } - } - } - } - - companion object { - const val TAG = "LauncherModelStore" - } + data class Search(val searchQuery: String) : LauncherState() } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherStateTemp.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherStateTemp.kt new file mode 100644 index 0000000000..790f5a9791 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherStateTemp.kt @@ -0,0 +1,433 @@ +package foundation.e.blisslauncher.features.launcher + +import android.content.ComponentName +import android.content.Context +import android.content.pm.LauncherActivityInfo +import android.os.UserHandle +import androidx.core.util.set +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.ItemInfoMatcher +import foundation.e.blisslauncher.domain.Matcher +import foundation.e.blisslauncher.domain.addFlag +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.FolderItem +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.removeFlag + +/** + * Stores data related to Launcher in memory. + */ +sealed class LauncherStateTemp constructor( + /*val context: Context, + val launcherApps: LauncherAppsCompat,*/ + /** + * Map of all the items (apps, shortcuts, folder or widgets) to their ids + */ + val itemsIdMap: LongArrayMap, + + /** + * List of all apps, folders, shortcuts and widgets directly on screen + * (no apps, shortcuts within folders). + */ + val allItems: List, + + /** + * Map of all the folders to their ids + */ + val folders: LongArrayMap, + + /** + * Ordered list of workspace screen ids + */ + val workspaceScreen: List, + + /** The list of all apps. */ + val data: List +) { + + @Synchronized + fun clear() { + itemsIdMap.clear() + folders.clear() + } + + /*override fun getAllActivities( + user: UserHandle, + quietMode: Boolean + ): List { + val apps = launcherApps.getActivityList(null, user) + if (apps.isNotEmpty()) { + apps.forEach { + add(ApplicationItem(it, user, quietMode), it) + } + } + return data + } + + override fun add( + packageName: String, + user: UserHandle, + quietMode: Boolean + ): ArrayList { + val addedPackageApps = ArrayList() + val matches = launcherApps.getActivityList(packageName, user) + matches.forEach { info -> + add(ApplicationItem( + info, user, quietMode + ).apply { + id = System.nanoTime() + }.let { + addedPackageApps.add(it) + it + }, + info + ) + } + return addedPackageApps + } +*/ + fun remove(packageName: String, user: UserHandle) { + val data = data + val iterator = data.iterator() + /*while (iterator.hasNext()) { + val item = iterator.next() + if (item.componentName.packageName == packageName && item.user == user) { + removed.add(item) + iterator.remove() + } + }*/ + } + + /*override fun updatedPackages( + packages: Array, + user: UserHandle, + quietMode: Boolean + ): List { + //TODO: Update icon cache for packages + val addedApps = ArrayList() + val modifiedApps = ArrayList() + + val removedPackages = HashSet() + val removedComponents = HashSet() + + packages.forEach { + if (!launcherApps.isPackageEnabledForProfile(it, user)) { + removedPackages.add(it) + } else { + val matches = launcherApps.getActivityList(it, user) + if (matches.isNotEmpty()) { + removedComponents.addAll( + removeIfNoActivityFound( + context, + matches, + it, + user + ) + ) + + matches.forEach { + var applicationItem = + findApplicationItem(it.componentName, user) + if (applicationItem == null) { + applicationItem = + ApplicationItem(it, user, quietMode) + add(applicationItem, it) + addedApps.add(applicationItem) + } else { + //TODO: update icon and title + modifiedApps.add(applicationItem) + } + } + } else { + removedPackages.add(it) + } + } + } + val flagOp = removeFlag(LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE) + val matcher = Matcher.ofPackages(packages.toHashSet(), user) + itemsIdMap.forEach { + //TODO: If user and packageSet of icon resource equals, + //TODO: Update item flag here. + } + return modifiedApps + }*/ + + fun suspendPackages( + packages: Array, + user: UserHandle + ): List { + val matcher: ItemInfoMatcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + addFlag(LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED) + ) + } + + fun unsuspendPackages( + packages: Array, + user: UserHandle + ): List { + val matcher: ItemInfoMatcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + removeFlag(LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED) + ) + } + + fun updateUserAvailability(user: UserHandle, quietMode: Boolean): List { + val matcher: ItemInfoMatcher = Matcher.ofUser(user) + val flagOp = if (quietMode) addFlag else removeFlag + return updateDisabledFlags( + matcher, + flagOp(LauncherItemWithIcon.FLAG_DISABLED_QUIET_USER) + ) + } + + fun makePackagesUnavailable( + packages: Array, + user: UserHandle + ): List { + val matcher = Matcher.ofPackages(packages.toHashSet(), user) + return updateDisabledFlags( + matcher, + addFlag(LauncherItemWithIcon.FLAG_DISABLED_NOT_AVAILABLE) + ) + } + + fun removePackages(packages: Array, user: UserHandle): List { + val removedPackages = packages.toHashSet() + val matcher = Matcher.ofPackages(removedPackages, user) + // Remove any queued items from the install queue + //TODO: InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser) + return removePackages(matcher) + } + + @Synchronized + fun removeItem(context: Context, vararg items: LauncherItem) { + items.forEach { + when (it.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.remove(it.id) + //allItems.remove(it) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + //allItems.remove(it) + } + } + itemsIdMap.remove(it.id) + } + } + + @Synchronized + fun addItem(item: LauncherItem, newItem: Boolean): LauncherStateTemp { + val mutableAllItems = allItems.toMutableList() + when (item.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.put(item.id, item as FolderItem) + mutableAllItems.add(item) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + /*if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP || + item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT + ) { + mutableAllItems.add(item) + } else { + if (newItem) { + if (!folders.containsKey(item.container)) { + } + } else { + findOrMakeFolder(item.container).add(item as AppShortcutItem, false) + } + }*/ + } + } + return this + } + + fun findOrMakeFolder(id: Long): FolderItem { + var folderItem: FolderItem? = folders[id] + if (folderItem == null) { + folderItem = FolderItem() + folders[id] = folderItem + } + return folderItem + } + + /** + * Returns whether *apps* contains *component*. + */ + fun checkForComponent( + apps: List, + component: ComponentName + ): Boolean { + for (info in apps) { + if (info.componentName == component) { + return true + } + } + return false + } + + /** + * Finds an application item corresponding to the given component name and user. + */ + fun findApplicationItem( + componentName: ComponentName, + user: UserHandle + ): ApplicationItem? { + for (item in data) { + if (componentName == item.componentName && user == item.user) { + return item + } + } + return null + } + + /*fun add(item: ApplicationItem, info: LauncherActivityInfo): LauncherState { + if (findApplicationItem(item.componentName, item.user) != null) { + return + } + // TODO: Update icon from IconCache + val mutableData = data.toMutableList() + mutableData.add(item) + return copy(data = mutableData) + data.add(item) + addItem(item, true) + }*/ + + @Synchronized + fun updateDisabledFlags( + matcher: ItemInfoMatcher, + flagOp: (oldFlags: Int) -> Int + ): List { + val updatedItems = ArrayList() + itemsIdMap.filter { + it is LauncherItemWithIcon && matcher(it, it.getTargetComponent()!!) + }.forEach { + it as LauncherItemWithIcon + val oldFlags = it.runtimeStatusFlags + it.apply { flagOp(runtimeStatusFlags) } + if (it.runtimeStatusFlags != oldFlags) + updatedItems.add(it) + } + return updatedItems + } + + @Synchronized + fun removePackages( + matches: (item: LauncherItem, cn: ComponentName) -> Boolean + ): List { + val removedItems = HashSet() + itemsIdMap.forEach { + if (it is LauncherItemWithIcon) it.let { + val cn = it.getTargetComponent() + if (cn != null && matches(it, cn)) removedItems.add(it) + } else if (it is FolderItem) it.let { folder -> + folder.contents.forEach { + val cn = it.getTargetComponent() + if (cn != null && matches(it, cn)) removedItems.add(it) + } + } + } + //TODO: delete items from database sequentially and remove them from itemsIdMap + return removedItems.toList() + } + + @Synchronized + fun removeIfNoActivityFound( + context: Context, + matches: List, + packageName: String, + user: UserHandle + ): HashSet { + val removedComponents = HashSet() + val modified = ArrayList() + itemsIdMap.filter { it.itemType == LauncherConstants.ItemType.APPLICATION } + .forEach { + it as ApplicationItem + if (it.user == user && packageName == it.componentName.packageName) { + if (!findActivity(matches, it.componentName)) { + removedComponents.add(it.componentName) + } + } + } + return removedComponents + } + + /** + * Returns whether *apps* contains *component*. + */ + private fun findActivity( + apps: List, + component: ComponentName + ): Boolean { + for (info in apps) { + if (info.componentName == component) { + return true + } + } + return false + } + + /** + * Find an AppInfo object for the given componentName + * + * @return the corresponding AppInfo or null + */ + fun findAppInfo( + componentName: ComponentName, + user: UserHandle + ): ApplicationItem? { + for (item in itemsIdMap) { + if (item is ApplicationItem && + componentName == item.componentName && + user == item.user + ) { + return item + } + } + return null + } + + fun addItem( + item: LauncherItem, + mutableData: MutableList, + mutableAllItems: MutableList, + newItem: Boolean + ) { + mutableData.add(item as ApplicationItem) + itemsIdMap.put(item.id, item) + when (item.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.put(item.id, item as FolderItem) + mutableAllItems.add(item) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + /*if (item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_DESKTOP || + item.container.toInt() == LauncherConstants.ContainerType.CONTAINER_HOTSEAT + ) { + mutableAllItems.add(item) + } else { + if (newItem) { + if (!folders.containsKey(item.container)) { + } + } else { + findOrMakeFolder(item.container).add( + item as AppShortcutItem, + false + ) + } + }*/ + } + } + } + + companion object { + const val TAG = "LauncherModelStore" + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt index 10206f2d9f..566df94a6b 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherView.kt @@ -1,11 +1,6 @@ package foundation.e.blisslauncher.features.launcher +import foundation.e.blisslauncher.features.LauncherStore import foundation.e.blisslauncher.mvicore.component.MviView -interface LauncherView : MviView { - data class LauncherViewModel(private val name: String) - - sealed class LauncherViewEvent { - - } -} \ No newline at end of file +interface LauncherView : MviView \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt new file mode 100644 index 0000000000..b73ccb6f12 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt @@ -0,0 +1,422 @@ +package foundation.e.blisslauncher.features.launcher + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.LayoutTransition +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder +import android.app.WallpaperManager +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.GridLayout +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.WallpaperChangeReceiver +import foundation.e.blisslauncher.common.DeviceProfile +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.common.util.LongArrayMap +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.widget.Insettable +import foundation.e.blisslauncher.widget.PagedView +import timber.log.Timber +import javax.inject.Inject + +class Workspace @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : PagedView(context, attrs, defStyleAttr), Insettable { + + private val mTempXY: IntArray = IntArray(2) + private var mYDown: Float = 0.0f + private var mXDown: Float = 0.0f + private val FADE_EMPTY_SCREEN_DURATION: Int = 150 + private val SNAP_OFF_EMPTY_SCREEN_DURATION: Int = 400 + + private lateinit var wallpaperReceiver: WallpaperChangeReceiver + + @get:JvmName("getLayoutTransition_") + private val layoutTransition: LayoutTransition by lazy { + LayoutTransition().apply { + this.enableTransitionType(LayoutTransition.DISAPPEARING) + this.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING) + this.disableTransitionType(LayoutTransition.APPEARING) + this.disableTransitionType(LayoutTransition.CHANGE_APPEARING) + } + } + + private val wallpaperManager: WallpaperManager + + @Inject + lateinit var invariantDeviceProfile: InvariantDeviceProfile + lateinit var deviceProfile: DeviceProfile + + private var maxDistanceForFolderCreation: Float = 0.0f + private val screenOrder = ArrayList() + private val workspaceScreens = LongArrayMap() + + init { + + deviceProfile = LauncherActivity.getLauncher(context).deviceProfile + invariantDeviceProfile = deviceProfile.inv + + wallpaperReceiver = WallpaperChangeReceiver(this) + isHapticFeedbackEnabled = false + + wallpaperManager = WallpaperManager.getInstance(context) + currentPage = DEFAULT_PAGE + clipToPadding = false + + setLayoutTransition(layoutTransition) + + + // Set the wallpaper dimensions when Launcher starts up + setWallpaperDimension() + isMotionEventSplittingEnabled = true + + //TODO: Set touch listener if required + + } + + private fun setupLayoutTransition() { + // We want to show layout transitions when pages are deleted, to close the gap. + val layoutTransition = LayoutTransition() + layoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING) + layoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING) + layoutTransition.disableTransitionType(LayoutTransition.APPEARING) + layoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING) + } + + fun enableLayoutTransitions() { + setLayoutTransition(layoutTransition) + } + + fun disableLayoutTransitions() { + setLayoutTransition(null) + } + + private fun setWallpaperDimension() { + //TODO: Run it on a separate thread + val size: Point = invariantDeviceProfile.defaultWallpaperSize + if (size.x != wallpaperManager.desiredMinimumWidth || + size.y != wallpaperManager.desiredMinimumHeight + ) { + wallpaperManager.suggestDesiredDimensions(size.x, size.y) + } + } + + override fun setInsets(insets: Rect) { + this.insets.set(insets) + maxDistanceForFolderCreation = 0.55f * deviceProfile.iconSizePx + + val padding: Rect = deviceProfile.workspacePadding + setPadding(padding.left, padding.top, padding.right, padding.bottom) + } + + override fun onViewAdded(child: View?) { + require(child is GridLayout) { "A Workspace can only have CellLayout children." } + val gridlayout = child as GridLayout + super.onViewAdded(child) + } + + val isTouchActive: Boolean + get() = touchState != TOUCH_STATE_REST + + fun removeAllWorkspaceScreens() { + disableLayoutTransitions() + //removeFolderListners() + screenOrder.clear() + workspaceScreens.clear() + enableLayoutTransitions() + } + + fun insertNewWorkspaceScreenBeforeEmptyScreen(screenId: Long) { + var insertIndex = screenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID) + if (insertIndex < 0) { + insertIndex = screenOrder.size + } + + insertNewWorkspaceScreen(screenId, insertIndex) + } + + fun insertNewWorkspaceScreen(screenId: Long) { + insertNewWorkspaceScreen(screenId, childCount) + } + + fun insertNewWorkspaceScreen(screenId: Long, insertIndex: Int): GridLayout { + if (workspaceScreens.containsKey(screenId)) { + throw RuntimeException("Screen id $screenId already exists!") + } + + val newScreen = LayoutInflater.from(context) + .inflate(R.layout.layout_workspace_screen, this, false) as GridLayout + // TODO: Set padding if needed + newScreen.columnCount = invariantDeviceProfile.numColumns + newScreen.rowCount = invariantDeviceProfile.numRows + workspaceScreens.put(screenId, newScreen) + screenOrder.add(insertIndex, screenId) + addView(newScreen, insertIndex) + + // TODO: Apply state transition animation if needed + // TODO: Enable accessibilty + return newScreen + } + + fun addExtraEmptyScreenOnDrag() { + //TODO: Add once drag layer is done + } + + fun addExtraEmptyScreen(): Boolean { + if (!workspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) { + insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID) + return true + } + return false + } + + fun convertFinalScreenToEmptyScreenIfNecessary() { + //TODO: Return if early if workspace is loading + + if (hasExtraEmptyScreen || screenOrder.size == 0) return + val finalScreenId = screenOrder[screenOrder.size - 1] + val finalScreen = workspaceScreens[finalScreenId] + // TODO: If the final screen is empty, convert it to extra empty screen + } + + fun removeExtraEmptyScreen( + animate: Boolean, + stripEmptyScreens: Boolean + ) { + removeExtraEmptyScreenDelayed(animate, null, 0, stripEmptyScreens) + } + + fun removeExtraEmptyScreenDelayed( + animate: Boolean, + onComplete: Runnable?, + delay: Int, + stripEmptyScreens: Boolean + ) { + //TODO: Return if early if workspace is loading + + if (delay > 0) { + postDelayed({ + removeExtraEmptyScreenDelayed(animate, onComplete, 0, stripEmptyScreens) + }, delay.toLong()) + return + } + + convertFinalScreenToEmptyScreenIfNecessary() + + if (hasExtraEmptyScreen) { + val emptyIndex: Int = + screenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID) + if (nextPage == emptyIndex) { + snapToPage( + nextPage - 1, + SNAP_OFF_EMPTY_SCREEN_DURATION + ) + fadeAndRemoveEmptyScreen( + SNAP_OFF_EMPTY_SCREEN_DURATION, + FADE_EMPTY_SCREEN_DURATION, + onComplete, + stripEmptyScreens + ) + } else { + snapToPage(nextPage, 0) + fadeAndRemoveEmptyScreen( + 0, FADE_EMPTY_SCREEN_DURATION, + onComplete, stripEmptyScreens + ) + } + return + } else if (stripEmptyScreens) { + // If we're not going to strip the empty screens after removing + // the extra empty screen, do it right away. + stripEmptyScreens() + } + onComplete?.run() + } + + private fun fadeAndRemoveEmptyScreen( + delay: Int, + duration: Int, + onComplete: Runnable?, + stripEmptyScreens: Boolean + ) { + // XXX: Do we need to update LM workspace screens below? + val alpha = PropertyValuesHolder.ofFloat("alpha", 0f) + val bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", 0f) + val gl: GridLayout = + workspaceScreens.get(EXTRA_EMPTY_SCREEN_ID) + val oa: ObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(gl, alpha, bgAlpha) + oa.duration = duration.toLong() + oa.startDelay = delay.toLong() + oa.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + Runnable { + if (hasExtraEmptyScreen) { + workspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID) + screenOrder.remove(EXTRA_EMPTY_SCREEN_ID) + removeView(gl) + if (stripEmptyScreens) { + stripEmptyScreens() + } + // Update the page indicator to reflect the removed page. + //showPageIndicatorAtCurrentScroll() + } + }.run() + onComplete?.run() + } + }) + oa.start() + } + + val hasExtraEmptyScreen: Boolean + get() = workspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) && childCount > 1 + + fun commitExtraEmptyScreen(): Long { + // TODO: Return -1 if launcher is loading + + // TODO: Update the model here + TODO() + } + + fun getScreenWithId(screenId: Long) = workspaceScreens[screenId] + + fun getIdForScreen(screen: GridLayout): Long { + val index = workspaceScreens.indexOfValue(screen) + return if (index != -1) workspaceScreens.keyAt(index) else -1 + } + + fun getPageIndexForScreen(screenId: Long) = indexOfChild(workspaceScreens[screenId]) + + fun getScreenIdForPageIndex(index: Int): Long { + if (0 <= index && index < screenOrder.size) { + screenOrder[index] + } + return -1 + } + + fun getScreenOrder() = screenOrder + + fun stripEmptyScreens() { + // TODO: Return early if launcher is loading + } + + fun addInScreenFromBind(child: View, item: LauncherItem) { + val x = item.cellX + val y = item.cellY + addInScreen(child, item.container, item.screenId, x, y) + } + + fun addInScreen(child: View, item: LauncherItem) { + addInScreen(child, item.container, item.screenId, item.cellX, item.cellY) + } + + private fun addInScreen(child: View, container: Long, screenId: Long, x: Int, y: Int) { + if (container == LauncherConstants.ContainerType.CONTAINER_DESKTOP) { + if (getScreenWithId(screenId) == null) { + Timber.e("Skipping child, screenId $screenId not found") + // DEBUGGING - Print out the stack trace to see where we are adding from + Throwable().printStackTrace() + return + } + } + + if (screenId == EXTRA_EMPTY_SCREEN_ID) { + throw RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID") + } + + if (container == LauncherConstants.ContainerType.CONTAINER_HOTSEAT) { + //TODO: Hide folder title in hotseat + } else { + } + // TODO: Add view to gridlayout here + + child.isHapticFeedbackEnabled = false + child.setOnLongClickListener(null) + } + + private fun shouldConsumeTouch(v: View): Boolean { + return (!workspaceIconsCanBeDragged || + !workspaceInModalState && indexOfChild(v) != currentPage) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (ev.actionMasked == MotionEvent.ACTION_DOWN) { + mXDown = ev.x + mYDown = ev.y + } + return super.onInterceptTouchEvent(ev) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + wallpaperReceiver.setWindowToken(windowToken) + //TODO: Set window token to drag layer + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + wallpaperReceiver.setWindowToken(null) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + //TODO: Update page alpha values + } + + override fun getDescendantFocusability(): Int { + if (workspaceInModalState) { + return ViewGroup.FOCUS_BLOCK_DESCENDANTS + } + return super.getDescendantFocusability() + } + + val workspaceInModalState = false //TODO: Change it with the state of launcher + + val workspaceIconsCanBeDragged = true // TODO: Change with the launcher State + + private fun updateChildrenLayersEnabled() { + /*val enableChildrenLayers = mIsSwitchingState || isPageInTransition() + + if (enableChildrenLayers != mChildrenLayersEnabled) { + mChildrenLayersEnabled = enableChildrenLayers + if (mChildrenLayersEnabled) { + enableHwLayersOnVisiblePages() + } else { + for (i in 0 until getPageCount()) { + val cl: CellLayout = getChildAt(i) as CellLayout + cl.enableHardwareLayer(false) + } + } + }*/ + } + + private fun enableHwLayersOnVisiblePages() { + } + + fun onWallpaperTap(ev: MotionEvent) { + val position: IntArray = mTempXY + getLocationOnScreen(position) + val pointerIndex = ev.actionIndex + position[0] += ev.getX(pointerIndex).toInt() + position[1] += ev.getY(pointerIndex).toInt() + wallpaperManager.sendWallpaperCommand( + windowToken, + if (ev.action == MotionEvent.ACTION_UP) WallpaperManager.COMMAND_TAP + else WallpaperManager.COMMAND_SECONDARY_TAP, + position[0], position[1], 0, null + ) + } + + companion object { + const val DEFAULT_PAGE = 0 + const val EXTRA_EMPTY_SCREEN_ID: Long = -201 + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt index 7e083ee310..b5c26eb446 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/inject/AppModule.kt @@ -4,10 +4,15 @@ import android.content.Context import dagger.Module import dagger.Provides import foundation.e.blisslauncher.BlissLauncher +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.features.LauncherStore @Module class AppModule { @Provides fun provideContext(application: BlissLauncher): Context = application.applicationContext + + @Provides + fun provideIdp(context: Context): InvariantDeviceProfile = InvariantDeviceProfile(context) } \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/Insettable.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/Insettable.kt new file mode 100644 index 0000000000..f803b5ede2 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/Insettable.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.widget + +import android.graphics.Rect +/** + * Allows the implementing [View] to not draw underneath system bars. + * e.g., notification bar on top and home key area on the bottom. + */ +interface Insettable { + fun setInsets(insets: Rect) +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/InsettableFrameLayout.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/InsettableFrameLayout.kt new file mode 100644 index 0000000000..4152b2deda --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/InsettableFrameLayout.kt @@ -0,0 +1,97 @@ +package foundation.e.blisslauncher.widget + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.widget.FrameLayout +import foundation.e.blisslauncher.R +import timber.log.Timber + +class InsettableFrameLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), Insettable { + + private val insets = Rect() + + override fun setInsets(newInsets: Rect) { + val n = childCount + for (i in 0 until n) { + val child = getChildAt(i) + setFrameLayoutChildInsets(child, insets, newInsets) + } + insets.set(insets) + } + + override fun onApplyWindowInsets(newInsets: WindowInsets): WindowInsets { + Timber.d("this is called") + insets.set(0, newInsets.systemWindowInsetTop, 0, newInsets.systemWindowInsetBottom) + setInsets(insets) + return newInsets + } + + fun setFrameLayoutChildInsets( + child: View, + newInsets: Rect, + oldInsets: Rect + ) { + val lp: LayoutParams = + child.layoutParams as LayoutParams + if (child is Insettable) { + (child as Insettable).setInsets(newInsets) + } else if (!lp.ignoreInsets) { + lp.topMargin += newInsets.top - oldInsets.top + lp.leftMargin += newInsets.left - oldInsets.left + lp.rightMargin += newInsets.right - oldInsets.right + lp.bottomMargin += newInsets.bottom - oldInsets.bottom + } + child.layoutParams = lp + } + + override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams { + return LayoutParams(context, attrs) + } + + override fun generateDefaultLayoutParams(): LayoutParams { + return LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + // Override to allow type-checking of LayoutParams. + override fun checkLayoutParams(p: ViewGroup.LayoutParams): Boolean { + return p is LayoutParams + } + + override fun generateLayoutParams(p: ViewGroup.LayoutParams): LayoutParams { + return LayoutParams(p) + } + + class LayoutParams : FrameLayout.LayoutParams { + var ignoreInsets = false + + constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { + val a = c.obtainStyledAttributes( + attrs, + R.styleable.InsettableFrameLayout_Layout + ) + ignoreInsets = a.getBoolean( + R.styleable.InsettableFrameLayout_Layout_layout_ignoreInsets, false + ) + a.recycle() + } + + constructor(width: Int, height: Int) : super(width, height) {} + constructor(lp: ViewGroup.LayoutParams?) : super(lp) {} + } + + override fun onViewAdded(child: View?) { + super.onViewAdded(child) + setFrameLayoutChildInsets(child!!, insets, Rect()) + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt index 339877cd5f..bb8d163672 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt @@ -24,7 +24,7 @@ import kotlin.math.sin typealias ComputePageScrollsLogic = (View) -> Boolean -abstract class PagedView @JvmOverloads constructor( +open class PagedView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -39,7 +39,7 @@ abstract class PagedView @JvmOverloads constructor( protected var firstLayout = true - private var currentPage = 0 + var currentPage = 0 set(value) { if (!scroller.isFinished) { abortScrollerAnimation(true) @@ -56,7 +56,7 @@ abstract class PagedView @JvmOverloads constructor( invalidate() } - protected var nextPage: Int = INVALID_PAGE + var nextPage: Int = INVALID_PAGE get() { return if (field != INVALID_PAGE) field else currentPage } @@ -95,7 +95,7 @@ abstract class PagedView @JvmOverloads constructor( protected var overScrollX = 0 protected var unboundedScrollX = 0 - + protected var pageIndicator: PageIndicatorDots? = null // Convenience/caching @@ -108,7 +108,7 @@ abstract class PagedView @JvmOverloads constructor( // Similar to the platform implementation of isLayoutValid(); protected var mIsLayoutValid = false - + init { isHapticFeedbackEnabled = false currentPage = 0 @@ -303,8 +303,8 @@ abstract class PagedView @JvmOverloads constructor( } fun getNormalChildHeight(): Int { - return (getExpectedHeight() - paddingTop - paddingBottom - - insets.top - insets.bottom) + return (getExpectedHeight() - paddingTop - paddingBottom - + insets.top - insets.bottom) } fun getExpectedWidth(): Int { @@ -312,8 +312,8 @@ abstract class PagedView @JvmOverloads constructor( } fun getNormalChildWidth(): Int { - return (getExpectedWidth() - paddingLeft - paddingRight - - insets.left - insets.right) + return (getExpectedWidth() - paddingLeft - paddingRight - + insets.left - insets.right) } override fun requestLayout() { @@ -398,14 +398,18 @@ abstract class PagedView @JvmOverloads constructor( if (transition != null && transition.isRunning) { transition.addTransitionListener(object : LayoutTransition.TransitionListener { override fun startTransition( - transition: LayoutTransition, container: ViewGroup, - view: View, transitionType: Int + transition: LayoutTransition, + container: ViewGroup, + view: View, + transitionType: Int ) { } override fun endTransition( - transition: LayoutTransition, container: ViewGroup, - view: View, transitionType: Int + transition: LayoutTransition, + container: ViewGroup, + view: View, + transitionType: Int ) { // Wait until all transitions are complete. if (!transition.isRunning) { @@ -434,7 +438,8 @@ abstract class PagedView @JvmOverloads constructor( * */ protected fun getPageScrolls( - outPageScrolls: IntArray, layoutChildren: Boolean, + outPageScrolls: IntArray, + layoutChildren: Boolean, scrollLogic: ComputePageScrollsLogic ): Boolean { val childCount = childCount @@ -735,7 +740,7 @@ abstract class PagedView @JvmOverloads constructor( return touchState != TOUCH_STATE_REST } - protected fun determineScrollingStart(ev: MotionEvent) { + fun determineScrollingStart(ev: MotionEvent) { determineScrollingStart(ev, 1.0f) } @@ -997,8 +1002,8 @@ abstract class PagedView @JvmOverloads constructor( scroller.setFinalX((finalX * getScaleX()).toInt()) // Ensure the scroll/snap doesn't happen too fast; val extraScrollDuration: Int = - (OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION - - scroller.getDuration()) + (OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION - + scroller.getDuration()) if (extraScrollDuration > 0) { scroller.extendDuration(extraScrollDuration) } @@ -1249,7 +1254,9 @@ abstract class PagedView @JvmOverloads constructor( } protected fun snapToPage( - whichPage: Int, duration: Int, immediate: Boolean, + whichPage: Int, + duration: Int, + immediate: Boolean, interpolator: TimeInterpolator? ): Boolean { var whichPage = whichPage @@ -1264,7 +1271,10 @@ abstract class PagedView @JvmOverloads constructor( } protected fun snapToPage( - whichPage: Int, delta: Int, duration: Int, immediate: Boolean, + whichPage: Int, + delta: Int, + duration: Int, + immediate: Boolean, interpolator: TimeInterpolator? ): Boolean { var whichPage = whichPage @@ -1335,9 +1345,9 @@ abstract class PagedView @JvmOverloads constructor( private const val MIN_SNAP_VELOCITY = 1500 private const val MIN_FLING_VELOCITY = 250 - private const val TOUCH_STATE_REST = 0 - private const val TOUCH_STATE_SCROLLING = 1 - private const val TOUCH_STATE_PREV_PAGE = 2 - private const val TOUCH_STATE_NEXT_PAGE = 3 + const val TOUCH_STATE_REST = 0 + const val TOUCH_STATE_SCROLLING = 1 + const val TOUCH_STATE_PREV_PAGE = 2 + const val TOUCH_STATE_NEXT_PAGE = 3 } } \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/layout/activity_launcher.xml b/blisslauncherv2/src/main/res/layout/activity_launcher.xml new file mode 100644 index 0000000000..fef2bd7684 --- /dev/null +++ b/blisslauncherv2/src/main/res/layout/activity_launcher.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/layout/activity_main.xml b/blisslauncherv2/src/main/res/layout/activity_main.xml deleted file mode 100644 index 95459fce8c..0000000000 --- a/blisslauncherv2/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml b/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml new file mode 100644 index 0000000000..9b058ad54a --- /dev/null +++ b/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/values/attrs.xml b/blisslauncherv2/src/main/res/values/attrs.xml index 0bee177dd8..d7ad834d3f 100644 --- a/blisslauncherv2/src/main/res/values/attrs.xml +++ b/blisslauncherv2/src/main/res/values/attrs.xml @@ -6,4 +6,7 @@ + + + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/values/styles.xml b/blisslauncherv2/src/main/res/values/styles.xml index 5885930df6..10e52c8434 100644 --- a/blisslauncherv2/src/main/res/values/styles.xml +++ b/blisslauncherv2/src/main/res/values/styles.xml @@ -8,4 +8,14 @@ @color/colorAccent + + diff --git a/blisslauncherv2/src/test/java/foundation/e/blisslauncher/features/base/presentation/BaseViewModelTest.kt b/blisslauncherv2/src/test/java/foundation/e/blisslauncher/features/base/presentation/BaseViewModelTest.kt deleted file mode 100644 index 8189767028..0000000000 --- a/blisslauncherv2/src/test/java/foundation/e/blisslauncher/features/base/presentation/BaseViewModelTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package foundation.e.blisslauncher.features.base.presentation - -import foundation.e.blisslauncher.common.subscribeToState -import io.mockk.mockk -import org.junit.Before -import org.junit.Test - - -fun main() { - print("Amit") -} -class BaseViewModelTest { - - private lateinit var viewModel: BaseViewModel - - @Before - fun setUp() { - viewModel = mockk() - viewModel.states().subscribeToState {it.print()} - } - - @Test - fun testEvent() { - var event = DummyEvent.Event1 - if(event == DummyEvent.Event1) { - viewModel.process(intentForEvent1()) - } - } - - private fun processEvent1(onSuccess: (String) -> Unit) { - onSuccess("Title changed from Event1") - } - - private fun intentForEvent1(): BaseIntent { - return intent { - processEvent1 { - viewModel.process(intent { - copy(title = it) - }) - } - copy() - } - } -} - -data class DummyState(val id: Int, val title: String, val description: String): BaseViewState { - fun print() { - println("Id: $id, Title: $title, Desc: $description") - } -} - -sealed class DummyEvent: BaseViewEvent { - object Event1: DummyEvent() - object Event2: DummyEvent() -} \ No newline at end of file diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml index 8e4906f2de..146618cd5d 100644 --- a/common/src/main/AndroidManifest.xml +++ b/common/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ + package="foundation.e.blisslauncher.common" /> diff --git a/data/src/main/java/foundation/e/blisslauncher/data/DeviceProfile.kt b/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt similarity index 97% rename from data/src/main/java/foundation/e/blisslauncher/data/DeviceProfile.kt rename to common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt index 538e754eef..2d5d4a0291 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/DeviceProfile.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package foundation.e.blisslauncher.data +package foundation.e.blisslauncher.common import android.appwidget.AppWidgetHostView import android.content.ComponentName @@ -26,9 +26,6 @@ import android.graphics.Rect import android.util.DisplayMetrics import android.view.Surface import android.view.WindowManager -import foundation.e.blisslauncher.common.Utilities -import foundation.e.blisslauncher.data.badge.BadgeRenderer -import foundation.e.blisslauncher.domain.entity.LauncherConstants class DeviceProfile( context: Context, @@ -41,11 +38,13 @@ class DeviceProfile( isMultiWindowMode: Boolean ) { val inv: InvariantDeviceProfile + // Device properties val isTablet: Boolean val isLargeTablet: Boolean val isPhone: Boolean val transposeLayoutWithOrientation: Boolean + // Device properties in current orientation val isLandscape: Boolean val isMultiWindowMode: Boolean @@ -53,6 +52,7 @@ class DeviceProfile( val heightPx: Int var availableWidthPx = 0 var availableHeightPx = 0 + // Workspace val desiredWorkspaceLeftRightMarginPx: Int val cellLayoutPaddingLeftRightPx: Int @@ -63,8 +63,10 @@ class DeviceProfile( private val topWorkspacePadding: Int var workspaceSpringLoadShrinkFactor = 0f val workspaceSpringLoadedBottomSpace: Int + // Drag handle val verticalDragHandleSizePx: Int + // Workspace icons var iconSizePx = 0 var iconTextSizePx = 0 @@ -73,39 +75,46 @@ class DeviceProfile( var cellWidthPx = 0 var cellHeightPx = 0 var workspaceCellPaddingXPx: Int + // Folder var folderIconSizePx = 0 var folderIconOffsetYPx = 0 + // Folder cell var folderCellWidthPx = 0 var folderCellHeightPx = 0 + // Folder child var folderChildIconSizePx = 0 var folderChildTextSizePx = 0 var folderChildDrawablePaddingPx = 0 + // Hotseat var hotseatCellHeightPx = 0 + // In portrait: size = height, in landscape: size = width var hotseatBarSizePx: Int val hotseatBarTopPaddingPx: Int val hotseatBarBottomPaddingPx: Int val hotseatBarSidePaddingPx: Int + // All apps var allAppsCellHeightPx = 0 var allAppsIconSizePx = 0 var allAppsIconDrawablePaddingPx = 0 var allAppsIconTextSizePx = 0f + // Widgets val appWidgetScale = PointF(1.0f, 1.0f) + // Drop Target var dropTargetBarSizePx: Int + // Insets val insets = Rect() val workspacePadding = Rect() private val mHotseatPadding = Rect() private var mIsSeascape = false - // Icon badges - var mBadgeRenderer: BadgeRenderer fun copy(context: Context): DeviceProfile { val size = @@ -152,8 +161,8 @@ class DeviceProfile( * Inverse of [.getMultiWindowProfile] * @return device profile corresponding to the current orientation in non multi-window mode. */ - val fullScreenProfile: DeviceProfile? - get() = if (isLandscape) inv.landscapeProfile else inv.portraitProfile + val fullScreenProfile: DeviceProfile? = null + //get() = if (isLandscape) inv.landscapeProfile else inv.portraitProfile /** * Adjusts the profile so that the labels on the Workspace are hidden. @@ -170,8 +179,8 @@ class DeviceProfile( allAppsIconDrawablePaddingPx * if (isVerticalBarLayout) 2 else 1 allAppsCellHeightPx = (allAppsIconSizePx + allAppsIconDrawablePaddingPx + Utilities.calculateTextHeight( - allAppsIconTextSizePx - ) + + allAppsIconTextSizePx + ) + topBottomPadding * 2) } @@ -211,8 +220,8 @@ class DeviceProfile( iconDrawablePaddingPx = (iconDrawablePaddingOriginalPx * scale).toInt() cellHeightPx = (iconSizePx + iconDrawablePaddingPx + Utilities.calculateTextHeight( - iconTextSizePx.toFloat() - )) + iconTextSizePx.toFloat() + )) val cellYPadding = (cellSize.y - cellHeightPx) / 2 if (iconDrawablePaddingPx > cellYPadding && !isVerticalLayout && !isMultiWindowMode @@ -262,10 +271,10 @@ class DeviceProfile( (res.getDimensionPixelSize(R.dimen.folder_label_padding_top) + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom) + Utilities.calculateTextHeight( - res.getDimension( - R.dimen.folder_label_text_size - ) - )) + res.getDimension( + R.dimen.folder_label_text_size + ) + )) updateFolderCellSize(1f, dm, res) // Don't let the folder get too close to the edges of the screen. val folderMargin = edgeMarginPx @@ -472,7 +481,7 @@ class DeviceProfile( return isVerticalBarLayout || isLargeTablet } - fun getCellHeight(containerType: Int): Int { + fun getCellHeight(containerType: Long): Int { return when (containerType) { LauncherConstants.ContainerType.CONTAINER_DESKTOP -> cellHeightPx LauncherConstants.ContainerType.CONTAINER_HOTSEAT -> hotseatCellHeightPx @@ -501,6 +510,7 @@ class DeviceProfile( */ private const val MAX_HORIZONTAL_PADDING_PERCENT = 0.14f private const val TALL_DEVICE_ASPECT_RATIO_THRESHOLD = 2.0f + fun calculateCellWidth(width: Int, countX: Int): Int { return width / countX } @@ -611,6 +621,6 @@ class DeviceProfile( } updateWorkspacePadding() // This is done last, after iconSizePx is calculated above. - mBadgeRenderer = BadgeRenderer(iconSizePx) + //TODO: mBadgeRenderer = BadgeRenderer(iconSizePx) } } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/InvariantDeviceProfile.kt b/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt similarity index 81% rename from data/src/main/java/foundation/e/blisslauncher/data/InvariantDeviceProfile.kt rename to common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt index 2c7398831a..e563c06e5b 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/InvariantDeviceProfile.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt @@ -13,16 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package foundation.e.blisslauncher.data +package foundation.e.blisslauncher.common import android.content.Context import android.content.res.Configuration import android.graphics.Point import android.util.DisplayMetrics +import android.util.Log import android.util.Xml import android.view.WindowManager -import com.amitkma.common.R -import foundation.e.blisslauncher.common.Utilities import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import java.io.IOException @@ -37,11 +36,13 @@ open class InvariantDeviceProfile { var name: String? = null var minWidthDps = 0f var minHeightDps = 0f + /** * Number of icons per row and column in the workspace. */ var numRows = 0 var numColumns = 0 + /** * Number of icons per row and column in the folder. */ @@ -53,53 +54,18 @@ open class InvariantDeviceProfile { var iconBitmapSize = 0 var fillResIconDpi = 0 var iconTextSize = 0f + /** * Number of icons inside the hotseat area. */ var numHotseatIcons = 0 var defaultLayoutId = 0 var demoModeLayoutId = 0 - var landscapeProfile: DeviceProfile? = null - var portraitProfile: DeviceProfile? = null - var defaultWallpaperSize: Point? = null + lateinit var landscapeProfile: DeviceProfile + lateinit var portraitProfile: DeviceProfile + lateinit var defaultWallpaperSize: Point constructor() - private constructor(p: InvariantDeviceProfile) : this( - p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, - p.numFolderRows, p.numFolderColumns, - p.iconSize, p.landscapeIconSize, p.iconTextSize, p.numHotseatIcons, - p.defaultLayoutId, p.demoModeLayoutId - ) - - private constructor( - n: String?, - w: Float, - h: Float, - r: Int, - c: Int, - fr: Int, - fc: Int, - `is`: Float, - lis: Float, - its: Float, - hs: Int, - dlId: Int, - dmlId: Int - ) { - name = n - minWidthDps = w - minHeightDps = h - numRows = r - numColumns = c - numFolderRows = fr - numFolderColumns = fc - iconSize = `is` - landscapeIconSize = lis - iconTextSize = its - numHotseatIcons = hs - defaultLayoutId = dlId - demoModeLayoutId = dmlId - } @Inject constructor(context: Context) { @@ -131,6 +97,7 @@ open class InvariantDeviceProfile { val interpolatedDeviceProfileOut = invDistWeightedInterpolate(minWidthDps, minHeightDps, closestProfiles) val closestProfile = closestProfiles[0] + Log.d("InvariantDevice", "rows and col: ${closestProfile.numRows} * ${closestProfile.numColumns}") numRows = closestProfile.numRows numColumns = closestProfile.numColumns numHotseatIcons = closestProfile.numHotseatIcons @@ -155,25 +122,52 @@ open class InvariantDeviceProfile { val largeSide = Math.max(realSize.x, realSize.y) landscapeProfile = DeviceProfile( context, this, smallestSize, largestSize, - largeSide, smallSide, true /* isLandscape */, false /* isMultiWindowMode */ + largeSide, smallSide, true, false ) portraitProfile = DeviceProfile( context, this, smallestSize, largestSize, - smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */ + smallSide, largeSide, false, false ) // We need to ensure that there is enough extra space in the wallpaper // for the intended parallax effects - defaultWallpaperSize = if (context.resources.configuration.smallestScreenWidthDp >= 720) { - Point( - (largeSide * wallpaperTravelToScreenWidthRatio( - largeSide, - smallSide - )).toInt(), - largeSide - ) - } else { - Point(Math.max(smallSide * 2, largeSide), largeSide) - } + defaultWallpaperSize = Point(smallSide, largeSide) + } + + private constructor(p: InvariantDeviceProfile) : this( + p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns, + p.numFolderRows, p.numFolderColumns, + p.iconSize, p.landscapeIconSize, p.iconTextSize, p.numHotseatIcons, + p.defaultLayoutId, p.demoModeLayoutId + ) + + private constructor( + n: String?, + w: Float, + h: Float, + r: Int, + c: Int, + fr: Int, + fc: Int, + `is`: Float, + lis: Float, + its: Float, + hs: Int, + dlId: Int, + dmlId: Int + ) { + name = n + minWidthDps = w + minHeightDps = h + numRows = r + numColumns = c + numFolderRows = fr + numFolderColumns = fc + iconSize = `is` + landscapeIconSize = lis + iconTextSize = its + numHotseatIcons = hs + defaultLayoutId = dlId + demoModeLayoutId = dmlId } private fun getPredefinedDeviceProfiles(context: Context): ArrayList { @@ -199,12 +193,15 @@ open class InvariantDeviceProfile { ) val numColumns = a.getInt( R.styleable.InvariantDeviceProfile_numColumns, - 0 + 5 ) + Log.d("Invariant", "Num columns here: $numColumns") val iconSize = a.getFloat( R.styleable.InvariantDeviceProfile_iconSize, 0f ) + val name = a.getString(R.styleable.InvariantDeviceProfile_name) + Log.d("Invariant", "Parsing profile name: $name") profiles.add( InvariantDeviceProfile( a.getString(R.styleable.InvariantDeviceProfile_name), @@ -283,18 +280,6 @@ open class InvariantDeviceProfile { return density } - /** - * Apply any Partner customization grid overrides. - * - * Currently we support: all apps row / column count. - */ - /*private void applyPartnerDeviceProfileOverrides(Context context, DisplayMetrics dm) { - Partner p = Partner.get(context.getPackageManager()); - if (p != null) { - p.applyInvariantDeviceProfileOverrides(this, dm); - } - }*/ - fun dist(x0: Float, y0: Float, x1: Float, y1: Float): Float { return hypot(x1 - x0.toDouble(), y1 - y0.toDouble()).toFloat() } @@ -334,7 +319,8 @@ open class InvariantDeviceProfile { val out = InvariantDeviceProfile() var i = 0 while (i < points.size && i < KNEARESTNEIGHBOR) { - p = InvariantDeviceProfile(points[i]) + p = + InvariantDeviceProfile(points[i]) val w = weight( width, height, @@ -369,7 +355,7 @@ open class InvariantDeviceProfile { return rank == allAppsButtonRank } - fun getDeviceProfile(context: Context): DeviceProfile? { + fun getDeviceProfile(context: Context): DeviceProfile { return if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE ) landscapeProfile else portraitProfile @@ -395,38 +381,13 @@ open class InvariantDeviceProfile { // This is a static that we use for the default icon size on a 4/5-inch phone private const val DEFAULT_ICON_SIZE_DP = 60f private const val ICON_SIZE_DEFINED_IN_APP_DP = 48f + // Constants that affects the interpolation curve between statically defined device profile // buckets. private const val KNEARESTNEIGHBOR = 3f private const val WEIGHT_POWER = 5f + // used to offset float not being able to express extremely small weights in extreme cases. private const val WEIGHT_EFFICIENT = 100000f - - /** - * As a ratio of screen height, the total distance we want the parallax effect to span - * horizontally - */ - private fun wallpaperTravelToScreenWidthRatio(width: Int, height: Int): Float { - val aspectRatio = width / height.toFloat() - // 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: - val ASPECT_RATIO_LANDSCAPE = 16 / 10f - val ASPECT_RATIO_PORTRAIT = 10 / 16f - val WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f - val 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: - val x = - (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / - (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT) - val y = - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT - return x * aspectRatio + y - } } } \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/LauncherConstants.kt b/common/src/main/java/foundation/e/blisslauncher/common/LauncherConstants.kt new file mode 100644 index 0000000000..1615945e13 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/LauncherConstants.kt @@ -0,0 +1,36 @@ +package foundation.e.blisslauncher.common + +class LauncherConstants { + + object ItemType { + + const val APPLICATION = 0 + const val SHORTCUT = 1 + const val FOLDER = 2 + const val APPWIDGET = 4 + const val CUSTOM_APPWIDGET = 5 + const val DEEP_SHORTCUT = 6 + + fun itemTypeToString(type: Int): String = when (type) { + APPLICATION -> "APP" + SHORTCUT -> "SHORTCUT" + FOLDER -> "FOLDER" + APPWIDGET -> "WIDGET" + CUSTOM_APPWIDGET -> "CUSTOMWIDGET" + DEEP_SHORTCUT -> "DEEPSHORTCUT" + else -> type.toString() + } + } + + object ContainerType { + + const val CONTAINER_DESKTOP: Long = -100 + const val CONTAINER_HOTSEAT: Long = -101 + + fun containerToString(container: Long): String = when (container) { + CONTAINER_DESKTOP -> "desktop" + CONTAINER_HOTSEAT -> "hotseat" + else -> container.toString() + } + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/util/LabelComparator.kt b/common/src/main/java/foundation/e/blisslauncher/common/util/LabelComparator.kt new file mode 100644 index 0000000000..c15827349f --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/util/LabelComparator.kt @@ -0,0 +1,25 @@ +package foundation.e.blisslauncher.common.util + +import java.text.Collator + +class LabelComparator : Comparator { + + private val collator = Collator.getInstance() + override fun compare(titleA: String, titleB: String): Int { + + // Ensure that we de-prioritize any titles that don't start with a + // linguistic letter or digit + val aStartsWithLetter = titleA.isNotEmpty() && + Character.isLetterOrDigit(titleA.codePointAt(0)) + val bStartsWithLetter = titleB.isNotEmpty() && + Character.isLetterOrDigit(titleB.codePointAt(0)) + if (aStartsWithLetter && !bStartsWithLetter) { + return -1 + } else if (!aStartsWithLetter && bStartsWithLetter) { + return 1 + } + + // Order by the title in the current locale + return collator.compare(titleA, titleB) + } +} \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/utils/LongArrayMap.kt b/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt similarity index 94% rename from common/src/main/java/foundation/e/blisslauncher/utils/LongArrayMap.kt rename to common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt index 52bb4d6c9b..1f01029528 100644 --- a/common/src/main/java/foundation/e/blisslauncher/utils/LongArrayMap.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/util/LongArrayMap.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.utils +package foundation.e.blisslauncher.common.util import android.util.LongSparseArray diff --git a/common/src/main/java/foundation/e/blisslauncher/utils/MultiHashMap.java b/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java similarity index 96% rename from common/src/main/java/foundation/e/blisslauncher/utils/MultiHashMap.java rename to common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java index 732dff451e..8626a5b1c3 100644 --- a/common/src/main/java/foundation/e/blisslauncher/utils/MultiHashMap.java +++ b/common/src/main/java/foundation/e/blisslauncher/common/util/MultiHashMap.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package foundation.e.blisslauncher.utils; +package foundation.e.blisslauncher.common.util; import java.util.ArrayList; import java.util.HashMap; diff --git a/common/src/main/res/values/attrs.xml b/common/src/main/res/values/attrs.xml index efc02f494b..c30c90dbd4 100644 --- a/common/src/main/res/values/attrs.xml +++ b/common/src/main/res/values/attrs.xml @@ -1,5 +1,5 @@ - + diff --git a/common/src/main/res/values/config.xml b/common/src/main/res/values/config.xml index 1aed339bc6..b5c1d0b20a 100644 --- a/common/src/main/res/values/config.xml +++ b/common/src/main/res/values/config.xml @@ -7,4 +7,6 @@ true + 90 + \ No newline at end of file diff --git a/common/src/main/res/values/dimens.xml b/common/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..ade66cc90b --- /dev/null +++ b/common/src/main/res/values/dimens.xml @@ -0,0 +1,30 @@ + + + + 8dp + 1dp + 8dp + 8dp + 8dp + + 8dp + + 5.5dp + 0dp + 8dp + + 8dp + 2dp + 80dp + 0dp + + 9dp + 6dp + 13sp + 4dp + 12dp + 14sp + 48dp + 24dp + + \ No newline at end of file diff --git a/common/src/main/res/xml/default_workspace_4x4.xml b/common/src/main/res/xml/default_workspace_4x4.xml deleted file mode 100644 index 979a1b4c84..0000000000 --- a/common/src/main/res/xml/default_workspace_4x4.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/src/main/res/xml/default_workspace_5x5.xml b/common/src/main/res/xml/default_workspace_5x5.xml deleted file mode 100644 index f9cc0e789b..0000000000 --- a/common/src/main/res/xml/default_workspace_5x5.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/src/main/res/xml/default_workspace_5x6.xml b/common/src/main/res/xml/default_workspace_5x6.xml deleted file mode 100644 index 8493c265e2..0000000000 --- a/common/src/main/res/xml/default_workspace_5x6.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/common/src/main/res/xml/device_profiles.xml b/common/src/main/res/xml/device_profiles.xml index 6d250a27de..73d8156a4d 100644 --- a/common/src/main/res/xml/device_profiles.xml +++ b/common/src/main/res/xml/device_profiles.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/common/src/main/res/xml/dw_hotseat_3.xml b/common/src/main/res/xml/dw_hotseat_3.xml new file mode 100644 index 0000000000..3306993fa1 --- /dev/null +++ b/common/src/main/res/xml/dw_hotseat_3.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/xml/default_workspace_3x3.xml b/common/src/main/res/xml/dw_hotseat_4.xml similarity index 57% rename from common/src/main/res/xml/default_workspace_3x3.xml rename to common/src/main/res/xml/dw_hotseat_4.xml index 2aee3d82df..a3139d361d 100644 --- a/common/src/main/res/xml/default_workspace_3x3.xml +++ b/common/src/main/res/xml/dw_hotseat_4.xml @@ -1,5 +1,4 @@ - - - - - - - + + + - - - - - - - - - - + launcher:y="0"> - - - - - - - - + launcher:y="0"> + + + + + - - - - - - + launcher:y="0"> + launcher:container="-101" + launcher:screen="3" + launcher:x="3" + launcher:y="0"> - + diff --git a/common/src/main/res/xml/dw_phone_hotseat.xml b/common/src/main/res/xml/dw_hotseat_5.xml similarity index 82% rename from common/src/main/res/xml/dw_phone_hotseat.xml rename to common/src/main/res/xml/dw_hotseat_5.xml index 031d0d7a11..74f7386948 100644 --- a/common/src/main/res/xml/dw_phone_hotseat.xml +++ b/common/src/main/res/xml/dw_hotseat_5.xml @@ -14,9 +14,9 @@ limitations under the License. --> - - - + + + - + + + + - + diff --git a/common/src/main/res/xml/dw_tablet_hotseat.xml b/common/src/main/res/xml/dw_hotseat_6.xml similarity index 72% rename from common/src/main/res/xml/dw_tablet_hotseat.xml rename to common/src/main/res/xml/dw_hotseat_6.xml index 671ccba3c5..ce33eadefd 100644 --- a/common/src/main/res/xml/dw_tablet_hotseat.xml +++ b/common/src/main/res/xml/dw_hotseat_6.xml @@ -1,5 +1,4 @@ - - - - - + + + + + launcher:y="0"> @@ -33,7 +33,7 @@ launcher:container="-101" launcher:screen="1" launcher:x="1" - launcher:y="0" > + launcher:y="0"> @@ -42,37 +42,35 @@ launcher:container="-101" launcher:screen="2" launcher:x="2" - launcher:y="0" > - + launcher:y="0"> + - - - + launcher:screen="3" + launcher:x="3" + launcher:y="0"> + + + launcher:screen="4" + launcher:x="4" + launcher:y="0"> + launcher:screen="5" + launcher:x="5" + launcher:y="0"> - + diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt index a3a001fcf7..090f0387d9 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -1,26 +1,144 @@ package foundation.e.blisslauncher.data -import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity +import android.content.Context +import android.content.SharedPreferences +import androidx.room.Room +import androidx.room.RoomDatabase.Callback +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteDatabase +import foundation.e.blisslauncher.data.database.BlissLauncherDatabase +import foundation.e.blisslauncher.data.database.BlissLauncherFiles +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.ArrayList import javax.inject.Inject -class LauncherDatabaseGateway @Inject constructor() { - fun createEmptyDatabase() {} +class LauncherDatabaseGateway @Inject constructor( + context: Context, + private val sharedPrefs: SharedPreferences +) { - fun generateNewItemId() {} + private val db: BlissLauncherDatabase - fun generateNewScreenId() {} + private var maxItemId: Long = -1 + private var maxScreenId: Long = -1 - fun deleteEmptyFolders() {} + init { + db = Room.databaseBuilder( + context, + BlissLauncherDatabase::class.java, + BlissLauncherFiles.LAUNCHER_DB + ).addCallback( + object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + Timber.d("Room database created") + maxItemId = 0 + maxScreenId = 0 + sharedPrefs.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit() + } + + override fun onOpen(db: SupportSQLiteDatabase) { + super.onOpen(db) + Timber.d("Room database opened") + initIds() + } + } + ).build() + } + + private fun initIds() { + if (maxItemId == -1L) { + initializeMaxItemId() + } + + if (maxScreenId == -1L) { + initializeMaxScreenId() + } + } + + private fun initializeMaxItemId() { + getMaxId(db, "launcherItems") + } + + private fun initializeMaxScreenId() { + getMaxId(db, "workspaceScreens") + } + + private fun getMaxId( + db: BlissLauncherDatabase, + tableName: String + ) { + Observable.fromCallable { + db.launcherDao().getMaxIdInTable(SimpleSQLiteQuery("SELECT MAX(_id) FROM $tableName")) + }.subscribeOn(Schedulers.io()) + .subscribe { + if (it == -1L) { + throw RuntimeException("Error: could not query max id in $tableName") + } + if (tableName == "launcherItems") + maxItemId = it + else if (tableName == "workspaceScreens") + maxScreenId = it + else throw IllegalArgumentException("Error: specified table doesn't match") + } + } + + fun createEmptyDatabase() { + db.launcherDao().createEmptyDb() + } + + fun insertAndCheck(item: WorkspaceItem): Long { + checkItemId(item) + return db.launcherDao().insert(item) + } + + fun checkItemId(item: WorkspaceItem) { + val id = item._id + maxItemId = Math.max(id, maxItemId) + } + + fun generateNewItemId(): Long { + if (maxItemId < 0) { + throw RuntimeException("Error: max item id was not initialized") + } + maxItemId += 1 + return maxItemId + } + + fun generateNewScreenId(): Long { + if (maxScreenId < 0) { + throw RuntimeException("Error: max screen id was not initialized") + } - fun loadDefaultWorkspace() {} + maxScreenId += 1 + return maxScreenId + } - fun getAllWorkspaceItems(): List = emptyList() + fun deleteEmptyFolders() {} + + fun getAllWorkspaceItems(): List = db.launcherDao().getAllWorkspaceItems() fun loadWorkspaceScreensInOrder(): List = emptyList() fun markDeleted(id: Long) {} - fun markDeleted(item: LauncherItemRoomEntity) { + fun markDeleted(item: WorkspaceItem) { markDeleted(item._id) } + + fun saveAll(items: ArrayList) { + db.launcherDao().insertAll(items) + } + + // Helper function to initialise the database + fun createDbIfNotExist() { + db.openHelper.readableDatabase + } + + companion object { + const val EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED" + } } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt index 4ab291eed2..556b004781 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/PackageManagerHelper.kt @@ -11,8 +11,9 @@ import android.os.UserHandle import android.text.TextUtils import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.domain.entity.ApplicationItem +import javax.inject.Inject -class PackageManagerHelper( +class PackageManagerHelper @Inject constructor( context: Context, private val launcherApps: LauncherAppsCompat ) { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt similarity index 67% rename from data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt rename to data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt index eba6895108..365394ef58 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherItemRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt @@ -1,51 +1,52 @@ package foundation.e.blisslauncher.data +import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.LauncherActivityInfo import android.os.Process import android.os.UserHandle import android.util.LongSparseArray +import androidx.core.graphics.drawable.toBitmap +import foundation.e.blisslauncher.common.InvariantDeviceProfile import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat -import foundation.e.blisslauncher.utils.MultiHashMap -import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity +import foundation.e.blisslauncher.common.util.MultiHashMap +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.parser.DefaultHotseatParser import foundation.e.blisslauncher.data.shortcuts.PinnedShortcutManager -import foundation.e.blisslauncher.domain.entity.AppShortcutItem +import foundation.e.blisslauncher.data.util.LauncherItemComparator +import foundation.e.blisslauncher.domain.dto.WorkspaceModel +import foundation.e.blisslauncher.domain.entity.ShortcutItem import foundation.e.blisslauncher.domain.entity.ApplicationItem import foundation.e.blisslauncher.domain.entity.FolderItem import foundation.e.blisslauncher.domain.entity.LauncherConstants import foundation.e.blisslauncher.domain.entity.LauncherItem import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon import foundation.e.blisslauncher.domain.keys.ShortcutKey -import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository import timber.log.Timber +import java.util.Collections import javax.inject.Inject -class LauncherItemRepositoryImpl +class WorkspaceRepositoryImpl @Inject constructor( private val context: Context, private val launcherApps: LauncherAppsCompat, private val launcherDatabase: LauncherDatabaseGateway, private val userManager: UserManagerRepository, private val packageManagerHelper: PackageManagerHelper, - private val shortcutManager: PinnedShortcutManager -) : LauncherItemRepository { + private val shortcutManager: PinnedShortcutManager, + private val sharedPrefs: SharedPreferences, + private val idp: InvariantDeviceProfile, + private val launcherItemComparator: LauncherItemComparator +) : WorkspaceRepository { - override fun save(entity: S): S { - TODO("Not yet implemented") - } - - override fun saveAll(entities: Iterable): Iterable { - TODO("Not yet implemented") - } - - override fun findById(id: Long): LauncherItem? { - TODO("Not yet implemented") - } - - override fun findAll(): Iterable { + override fun loadWorkspace(): WorkspaceModel { + val workspaceModel = WorkspaceModel() val pmHelper = PackageManagerHelper(context, launcherApps) val isSafeMode = pmHelper.isSafeMode val isSdCardReady = Utilities.isBootCompleted() @@ -55,6 +56,23 @@ class LauncherItemRepositoryImpl val quietMode: LongSparseArray = LongSparseArray() val unlockedUsers: LongSparseArray = LongSparseArray() + var clearDb = false + + //TODO: GridSize Migration Task + /*if (!clearDb && GridSizeMigrationTask.ENABLED && + !GridSizeMigrationTask.migrateGridIfNeeded(context) + ) { + // Migration failed. Clear workspace. + clearDb = true + }*/ + + if (clearDb) { + Timber.d("loadLauncher: resetting launcher database") + clearAllDbs() + } + + val allAppsMap = hashMapOf() + userManager.userProfiles.forEach { user -> val serialNo = userManager.getSerialNumberForUser(user) allUsers.put(serialNo, user) @@ -76,12 +94,18 @@ class LauncherItemRepositoryImpl } } unlockedUsers.put(serialNo, userUnlocked) + launcherApps.getActivityList(null, user).forEach { + allAppsMap[it.componentName] = it + } } + loadDefaultWorkspaceIfNecessary(allAppsMap) + //Populate item from database and fill necessary details based on users. - val launcherItems = ArrayList() - launcherDatabase.getAllWorkspaceItems() - .onEach { + val launcherDatabaseItems = launcherDatabase.getAllWorkspaceItems() + Timber.d("LauncherDatabase size is ${launcherDatabaseItems.size}") + launcherDatabaseItems + .forEach { it.apply { user = allUsers[profileId] validTarget = @@ -90,17 +114,17 @@ class LauncherItemRepositoryImpl user ) } - } - .filter { - checkAndValidate( - it, - unlockedUsers, - shortcutKeyToPinnedShortcuts, - isSdCardReady + + if (!checkAndValidate( + it, + unlockedUsers, + shortcutKeyToPinnedShortcuts, + isSdCardReady + ) ) - } - .mapNotNull { - convertToLauncherItem( + return@forEach + + val launcherItem = convertToLauncherItem( it, quietMode, isSdCardReady, @@ -109,14 +133,92 @@ class LauncherItemRepositoryImpl pendingPackages, shortcutKeyToPinnedShortcuts ) + if (launcherItem != null && checkAndAddItem(launcherItem, workspaceModel)) { + if (launcherItem.itemType == LauncherConstants.ItemType.APPLICATION) { + launcherItem.getTargetComponent()?.let { componentName -> + allAppsMap.remove(componentName) + } + } + } + + } + + Timber.d( + "Size of launcherItems before processing remaining app items: " + + "${workspaceModel.workspaceItems.size}" + ) + allAppsMap.values.forEach { info -> + val applicationItem = ApplicationItem( + info, + info.user, + quietMode[userManager.getSerialNumberForUser(info.user)] + ) + applicationItem.iconBitmap = info.getBadgedIcon(0).toBitmap() + checkAndAddItem(applicationItem, workspaceModel) + } + + sortWorkspaceItems(workspaceModel.workspaceItems) + + return workspaceModel + } + + private fun checkAndAddItem( + launcherItem: LauncherItem, + workspaceModel: WorkspaceModel + ): Boolean { + workspaceModel.addItem(context, launcherItem, false) + return true + } + + private fun loadDefaultWorkspaceIfNecessary(allAppsMap: HashMap) { + launcherDatabase.createDbIfNotExist() + if (sharedPrefs.getBoolean(LauncherDatabaseGateway.EMPTY_DATABASE_CREATED, false)) { + val hotseatParser = + DefaultHotseatParser(launcherDatabase, idp.defaultLayoutId, context, userManager) + val count = hotseatParser.loadDefaultLayout() + Timber.d("Hotseat count is $count") + val existingWorkspaceItems = launcherDatabase.getAllWorkspaceItems() + val apps = ArrayList() + + allAppsMap.values.forEach { + if (!isApplicationAlreadyAdded(existingWorkspaceItems, it.componentName)) { + val intent = Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(it.componentName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + + apps.add( + WorkspaceItem( + launcherDatabase.generateNewItemId(), + it.label.toString(), + intent.toUri(0), + LauncherConstants.ContainerType.CONTAINER_DESKTOP, + -1, -1, -1, LauncherConstants.ItemType.APPLICATION + , 0, userManager.getSerialNumberForUser(Process.myUserHandle()) + ) + ) + } } - .toCollection(launcherItems) - return launcherItems + launcherDatabase.saveAll(apps) + } + } + + private fun isApplicationAlreadyAdded( + existingWorkspaceItems: List, + componentName: ComponentName + ): Boolean { + for (i in existingWorkspaceItems.indices) { + val item = existingWorkspaceItems[i] + if (item.componentName != null && item.componentName == componentName) { + return true + } + } + return false } private fun checkAndValidate( - item: LauncherItemRoomEntity, + item: WorkspaceItem, unlockedUsers: LongSparseArray, shortcutKeyToPinnedShortcuts: HashMap, isSdcardReady: Boolean @@ -194,8 +296,33 @@ class LauncherItemRepositoryImpl return true } + private fun sortWorkspaceItems(launcherItems: List) { + val screenCols = idp.numColumns + val screenRows = idp.numRows + val screenCellCount = screenCols * screenRows + Collections.sort(launcherItems, { lhs, rhs -> + if (lhs.container == rhs.container) { + when (lhs.container) { + LauncherConstants.ContainerType.CONTAINER_DESKTOP -> { + val lr = lhs.screenId * screenCellCount + lhs.cellY * screenCols + lhs.cellX + val rr = rhs.screenId * screenCellCount + rhs.cellY * screenCols + rhs.cellX + val result = lr.compareTo(rr) + if (result != 0) result + else launcherItemComparator.compare(lhs, rhs) + } + LauncherConstants.ContainerType.CONTAINER_HOTSEAT -> { + lhs.screenId.compareTo(rhs.screenId) + } + else -> throw RuntimeException("Unexpected container type when sorting: ${lhs}") + } + } else { + lhs.container.compareTo(rhs.container) + } + }) + } + private fun convertToLauncherItem( - item: LauncherItemRoomEntity, + item: WorkspaceItem, quietMode: LongSparseArray, isSdcardReady: Boolean, isSafeMode: Boolean, @@ -226,7 +353,7 @@ class LauncherItemRepositoryImpl } } - var launcherItem: AppShortcutItem? = null + var launcherItem: ShortcutItem? = null if (item.itemType == LauncherConstants.ItemType.APPLICATION) { launcherItem = getApplicationItem( item.user!!, @@ -240,7 +367,7 @@ class LauncherItemRepositoryImpl if (unlockedUsers.get(item.profileId)) { val pinnedShortcut = shortcutKeyToPinnedShortcuts[shortcutKey] if (pinnedShortcut != null) { - launcherItem = AppShortcutItem() + launcherItem = ShortcutItem() .apply { this.user = item.user!! this.itemType = LauncherConstants.ItemType.DEEP_SHORTCUT @@ -264,7 +391,7 @@ class LauncherItemRepositoryImpl } } } else { - launcherItem = AppShortcutItem() + launcherItem = ShortcutItem() .apply { this.user = item.user!! this.itemType = item.itemType @@ -275,7 +402,7 @@ class LauncherItemRepositoryImpl launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_LOCKED_USER } } else { - launcherItem = AppShortcutItem() + launcherItem = ShortcutItem() .apply { this.user = item.user!! this.itemType = item.itemType @@ -328,7 +455,7 @@ class LauncherItemRepositoryImpl intent: Intent, allowMissingTarget: Boolean, title: String? - ): AppShortcutItem? { + ): ApplicationItem? { val componentName = intent.component if (componentName == null) { Timber.d("Missing component found in getApplicationItem") @@ -370,19 +497,7 @@ class LauncherItemRepositoryImpl } } - override fun delete(entity: LauncherItem) { - TODO("Not yet implemented") - } - - override fun deleteById(id: Long) { - TODO("Not yet implemented") - } - - override fun deleteAll() { - TODO("Not yet implemented") - } - - override fun deleteAll(entities: Iterable) { - TODO("Not yet implemented") + private fun clearAllDbs() { + launcherDatabase.createEmptyDatabase() } } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceScreenRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceScreenRepositoryImpl.kt new file mode 100644 index 0000000000..2954dabf6d --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceScreenRepositoryImpl.kt @@ -0,0 +1,50 @@ +package foundation.e.blisslauncher.data + +import foundation.e.blisslauncher.domain.entity.WorkspaceScreen +import foundation.e.blisslauncher.domain.repository.WorkspaceScreenRepository +import javax.inject.Inject + +class WorkspaceScreenRepositoryImpl +@Inject constructor(private val launcherDatabase: LauncherDatabaseGateway) : + WorkspaceScreenRepository { + + override fun findAllOrderedByScreenRank(): List { + return launcherDatabase.loadWorkspaceScreensInOrder() + } + + override fun generateNewScreenId(): Long { + return launcherDatabase.generateNewScreenId() + } + + override fun save(entity: S): S { + TODO("Not yet implemented") + } + + override fun saveAll(entities: List): List { + TODO("Not yet implemented") + } + + override fun findById(id: Long): WorkspaceScreen? { + TODO("Not yet implemented") + } + + override fun findAll(): List { + TODO("Not yet implemented") + } + + override fun delete(entity: WorkspaceScreen) { + TODO("Not yet implemented") + } + + override fun deleteById(id: Long) { + TODO("Not yet implemented") + } + + override fun deleteAll() { + TODO("Not yet implemented") + } + + override fun deleteAll(entities: List) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt index edb9baf067..a6bd9536a5 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/compat/UserManagerCompatVN.kt @@ -6,7 +6,7 @@ import android.os.Process import android.os.UserHandle import android.os.UserManager import android.util.ArrayMap -import foundation.e.blisslauncher.utils.LongArrayMap +import foundation.e.blisslauncher.common.util.LongArrayMap import foundation.e.blisslauncher.domain.repository.UserManagerRepository import java.util.ArrayList diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt index dcecf2e6a1..56d7c21e7a 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/BlissLauncherDatabase.kt @@ -2,10 +2,11 @@ package foundation.e.blisslauncher.data.database import androidx.room.Database import androidx.room.RoomDatabase -import foundation.e.blisslauncher.data.database.dao.LauncherItemDao -import foundation.e.blisslauncher.data.database.roomentity.LauncherItemRoomEntity +import foundation.e.blisslauncher.data.database.dao.LauncherDao +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceScreen -@Database(entities = [LauncherItemRoomEntity::class], version = 1) +@Database(entities = [WorkspaceItem::class, WorkspaceScreen::class], version = 1) abstract class BlissLauncherDatabase : RoomDatabase() { - abstract fun launcherDao(): LauncherItemDao + abstract fun launcherDao(): LauncherDao } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt new file mode 100644 index 0000000000..ec2f699b30 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt @@ -0,0 +1,38 @@ +package foundation.e.blisslauncher.data.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.RawQuery +import androidx.room.Transaction +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem + +@Dao +abstract class LauncherDao { + + @RawQuery + abstract fun getMaxIdInTable(query: SupportSQLiteQuery): Long + + @Insert + abstract fun insert(workspaceItem: WorkspaceItem): Long + + @Insert + abstract fun insertAll(workspaceItems: List) + + @Transaction + open fun createEmptyDb() { + dropWorkspaceItemTable() + dropWorkspaceScreenTable() + } + + @Query("SELECT * FROM launcherItems") + abstract fun getAllWorkspaceItems(): List + + @Query("DELETE FROM launcherItems") + abstract fun dropWorkspaceItemTable() + + @Query("DELETE FROM workspaceScreens") + abstract fun dropWorkspaceScreenTable() +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherItemDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherItemDao.kt deleted file mode 100644 index 3715283269..0000000000 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherItemDao.kt +++ /dev/null @@ -1,6 +0,0 @@ -package foundation.e.blisslauncher.data.database.dao - -import androidx.room.Dao - -@Dao -interface LauncherItemDao \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt similarity index 92% rename from data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt rename to data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt index e6cb7e5652..5f2f00d578 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/LauncherItemRoomEntity.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt @@ -13,7 +13,7 @@ import timber.log.Timber import java.net.URISyntaxException @Entity(tableName = "launcherItems") -data class LauncherItemRoomEntity( +data class WorkspaceItem( @PrimaryKey val _id: Long, @ColumnInfo(typeAffinity = ColumnInfo.TEXT) @@ -27,22 +27,24 @@ data class LauncherItemRoomEntity( val itemType: Int, @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) @NonNull - val modified: Int, - @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) - @NonNull val rank: Int, + @ColumnInfo val profileId: Long ) { // Properties to initialise for proper validation @Ignore val targetPackage: String? + @Ignore val intent: Intent? + @Ignore val componentName: ComponentName? + @Ignore var user: UserHandle? = null + @Ignore var validTarget: Boolean = false diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceScreen.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt similarity index 83% rename from data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceScreen.kt rename to data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt index 7e69d9e33e..8d5e7bd092 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/WorkspaceScreen.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.data.database +package foundation.e.blisslauncher.data.database.roomentity import androidx.annotation.NonNull import androidx.room.ColumnInfo @@ -8,7 +8,7 @@ import androidx.room.PrimaryKey @Entity(tableName = "workspaceScreens") data class WorkspaceScreen( @PrimaryKey - val id: Long, + val _id: Long, @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) val screenRank: Int, @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt index a39b922d37..b205b32da4 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt @@ -17,7 +17,7 @@ import android.util.Log import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.domain.repository.UserManagerRepository -import foundation.e.blisslauncher.data.InvariantDeviceProfile +import foundation.e.blisslauncher.common.InvariantDeviceProfile import foundation.e.blisslauncher.data.database.dao.IconDao import foundation.e.blisslauncher.data.graphics.BitmapInfo import foundation.e.blisslauncher.data.graphics.BitmapRenderer diff --git a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt index c9d6d76486..4c2c15c4f5 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/inject/DataRepoBindingModule.kt @@ -1,17 +1,37 @@ package foundation.e.blisslauncher.data.inject +import android.content.Context +import android.content.SharedPreferences import dagger.Module import dagger.Provides +import foundation.e.blisslauncher.data.WorkspaceRepositoryImpl import foundation.e.blisslauncher.data.LauncherStateManagerImpl +import foundation.e.blisslauncher.data.WorkspaceScreenRepositoryImpl +import foundation.e.blisslauncher.data.database.BlissLauncherFiles import foundation.e.blisslauncher.domain.manager.LauncherStateManager import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceScreenRepository @Module class DataRepoBindingModule { @Provides - fun bindLauncherStateManager(launcherStateManagerImpl: LauncherStateManagerImpl): LauncherStateManager = launcherStateManagerImpl + fun bindLauncherStateManager(launcherStateManagerImpl: LauncherStateManagerImpl): LauncherStateManager = + launcherStateManagerImpl @Provides - fun bindLauncherRepository(launcherRepositoryImpl: LauncherItemRepository): LauncherItemRepository = launcherRepositoryImpl + fun bindLauncherRepository(workspaceRepositoryImpl: WorkspaceRepositoryImpl): WorkspaceRepository = + workspaceRepositoryImpl + + @Provides + fun bindWorkspaceScreenRepository(workspaceScreenRepositoryImpl: WorkspaceScreenRepositoryImpl): WorkspaceScreenRepository = + workspaceScreenRepositoryImpl + + @Provides + fun provideSharedPreferences(context: Context): SharedPreferences = + context.getSharedPreferences( + BlissLauncherFiles.SHARED_PREFERENCES_KEY, + Context.MODE_PRIVATE + ) } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutParser.kt new file mode 100644 index 0000000000..ae82c45700 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutParser.kt @@ -0,0 +1,71 @@ +package foundation.e.blisslauncher.data.parser + +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.res.XmlResourceParser +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import timber.log.Timber + +open class AppShortcutParser(context: Context) : TagParser { + + val packageManager = context.packageManager + + override fun parseAndAdd(parser: XmlResourceParser): ParseResult { + val packageName: String? = DefaultHotseatParser.getAttributeValue( + parser, + DefaultHotseatParser.ATTR_PACKAGE_NAME + ) + val className: String? = DefaultHotseatParser.getAttributeValue( + parser, + DefaultHotseatParser.ATTR_CLASS_NAME + ) + + return if (!packageName.isNullOrEmpty() && !className.isNullOrEmpty()) { + var info: ActivityInfo + try { + var cn: ComponentName? + try { + cn = ComponentName(packageName, className) + info = packageManager.getActivityInfo(cn, 0) + } catch (nnfe: PackageManager.NameNotFoundException) { + val packages: Array = + packageManager.currentToCanonicalPackageNames( + arrayOf(packageName) + ) + cn = ComponentName(packages[0], className) + info = packageManager.getActivityInfo(cn, 0) + } + val intent = + Intent(Intent.ACTION_MAIN, null) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(cn) + .setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + ) + return ParseResult( + 1, Triple( + info.loadLabel(packageManager).toString(), + intent, LauncherConstants.ItemType.APPLICATION + ) + ) + } catch (e: PackageManager.NameNotFoundException) { + Timber.e("Favorite not found: $packageName/$className") + return ParseResult(-1) + } + } else { + return invalidPackageOrClass(parser) + } + } + + /** + * Helper method to allow extending the parser capabilities + */ + protected open fun invalidPackageOrClass(parser: XmlResourceParser): ParseResult { + Timber.w("Skipping invalid with no component") + return ParseResult(-1) + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutWithUriParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutWithUriParser.kt new file mode 100644 index 0000000000..62ccdb27ea --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/AppShortcutWithUriParser.kt @@ -0,0 +1,102 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.res.XmlResourceParser +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import timber.log.Timber +import java.net.URISyntaxException + +class AppShortcutWithUriParser(context: Context) : AppShortcutParser(context) { + + override fun invalidPackageOrClass(parser: XmlResourceParser): ParseResult { + val uri: String? = + DefaultHotseatParser.getAttributeValue(parser, DefaultHotseatParser.ATTR_URI) + if (uri.isNullOrEmpty()) { + Timber.e("Skipping invalid with no component or uri") + return ParseResult(-1) + } + val metaIntent: Intent + metaIntent = try { + Intent.parseUri(uri, 0) + } catch (e: URISyntaxException) { + Timber.e("Unable to add meta-favorite: $uri") + return ParseResult(-1) + } + var resolved: ResolveInfo = packageManager.resolveActivity( + metaIntent, + PackageManager.MATCH_DEFAULT_ONLY + ) + val appList: List = packageManager.queryIntentActivities( + metaIntent, PackageManager.MATCH_DEFAULT_ONLY + ) + + // Verify that the result is an app and not just the resolver dialog asking which + // app to use. + if (wouldLaunchResolverActivity(resolved, appList)) { + // If only one of the results is a system app then choose that as the default. + val systemApp = getSingleSystemActivity(appList) + if (systemApp == null) { + // There is no logical choice for this meta-favorite, so rather than making + // a bad choice just add nothing. + Timber.w( + "No preference or single system activity found for " + + metaIntent.toString() + ) + return ParseResult(-1) + } + resolved = systemApp + } + val info = resolved.activityInfo + val intent: Intent = + packageManager.getLaunchIntentForPackage(info.packageName) + ?: return ParseResult(-1) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + return ParseResult( + 1, Triple( + info.loadLabel(packageManager).toString(), intent, + LauncherConstants.ItemType.APPLICATION + ) + ) + } + + private fun getSingleSystemActivity(appList: List): ResolveInfo? { + var systemResolve: ResolveInfo? = null + val N = appList.size + for (i in 0 until N) { + try { + val info: ApplicationInfo = packageManager.getApplicationInfo( + appList[i].activityInfo.packageName, 0 + ) + Timber.d("$info") + if (info.flags and ApplicationInfo.FLAG_SYSTEM != 0) { + Timber.d("True for $info") + return appList[i] + } + } catch (e: PackageManager.NameNotFoundException) { + Timber.w(e, "Unable to get info about resolve results") + return null + } + } + return systemResolve + } + + private fun wouldLaunchResolverActivity( + resolved: ResolveInfo, + appList: List + ): Boolean { + // If the list contains the above resolved activity, then it can't be + // ResolverActivity itself. + for (i in appList.indices) { + val tmp = appList[i] + if (tmp.activityInfo.name == resolved.activityInfo.name && tmp.activityInfo.packageName == resolved.activityInfo.packageName) { + return false + } + } + return true + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/DefaultHotseatParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/DefaultHotseatParser.kt new file mode 100644 index 0000000000..0b9efd24c2 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/DefaultHotseatParser.kt @@ -0,0 +1,165 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Context +import android.content.res.Resources +import android.content.res.XmlResourceParser +import android.os.Process +import android.util.ArrayMap +import foundation.e.blisslauncher.data.LauncherDatabaseGateway +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import timber.log.Timber +import java.io.IOException + +class DefaultHotseatParser ( + private val dbGateway: LauncherDatabaseGateway, + private val defaultHotseatId: Int, + private val context: Context, + private val userManagerRepository: UserManagerRepository +) { + + private val resources: Resources = context.resources + + /** + * Loads the default hotseat layout and returns number of entries added to the desktop + */ + fun loadDefaultLayout(): Int = try { + parseLayout(defaultHotseatId) + } catch (e: Exception) { + Timber.e(e, "Error parsing layout") + -1 + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun parseLayout(defaultHotseatId: Int): Int { + val parser = resources.getXml(defaultHotseatId) + beginDocument(parser, ROOT_TAG) + val depth = parser.depth + Timber.d("Depth of parser is: $depth") + var type: Int + val tagParserMap = getLayoutElementsMap() + var count = 0 + while ((parser.next().also { type = it } != XmlPullParser.END_TAG || parser.depth > depth) + && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue + } + count += parseAndAddNode(parser, tagParserMap) + } + return count + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun parseAndAddNode( + parser: XmlResourceParser, + tagParserMap: ArrayMap + ): Int { + Timber.d("Parser is ${parser.name}") + val container = getAttributeValue(parser, ATTR_CONTAINER)?.toLong() + val screenId = getAttributeValue(parser, ATTR_SCREEN)?.toLong() + val rank = screenId?.toInt() + val x = getAttributeValue(parser, ATTR_X)?.toInt() + val y = getAttributeValue(parser, ATTR_Y)?.toInt() + val tagParser = tagParserMap.get(parser.name) + if (tagParser == null) { + Timber.d("Ignoring unknown element tag: ${parser.name}" ) + return 0 + } + + val parsedResult = tagParser.parseAndAdd(parser) + return if (parsedResult.result >= 0 && parsedResult.dataTriplet != null) { + val triple = parsedResult.dataTriplet + WorkspaceItem( + _id = dbGateway.generateNewItemId(), title = triple.first, + intentStr = triple.second.toUri(0), container = container!!, screen = screenId!!, + cellX = x!!, cellY = y!!, itemType = triple.third, rank = rank!!, + profileId = userManagerRepository.getSerialNumberForUser(Process.myUserHandle()) + ).let { + Timber.d("Parsed Item: $it") + dbGateway.insertAndCheck(it) + 1 + } + } else 0 + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun beginDocument( + parser: XmlPullParser, + firstElementName: String + ) { + var type: Int + while (parser.next().also { type = it } != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT + ); + if (type != XmlPullParser.START_TAG) { + throw XmlPullParserException("No start tag found") + } + if (parser.name != firstElementName) { + throw XmlPullParserException( + "Unexpected start tag: found " + parser.name + + ", expected " + firstElementName + ) + } + } + + private fun getLayoutElementsMap(): ArrayMap { + val parsers = ArrayMap() + parsers[TAG_RESOLVE] = ResolveParser(context) + return parsers + } + + companion object { + const val TAG_RESOLVE = "resolve" + const val TAG_FAVORITE = "favorite" + const val ATTR_URI = "uri" + const val ATTR_CONTAINER = "container" + const val ATTR_SCREEN = "screen" + const val ATTR_PACKAGE_NAME = "packageName" + const val ATTR_CLASS_NAME = "className" + + const val ROOT_TAG = "hotseat" + + const val ATTR_X = "x" + const val ATTR_Y = "y" + + /** + * Return attribute value, attempting launcher-specific namespace first + * before falling back to anonymous attribute. + */ + fun getAttributeValue( + parser: XmlResourceParser, + attribute: String + ): String? { + Timber.d("Attribute is $attribute") + var value = parser.getAttributeValue( + "http://schemas.android.com/apk/res-auto", attribute + ) + if (value == null) { + value = parser.getAttributeValue(null, attribute) + } + Timber.d("Value is $value") + + return value + } + + /** + * Return attribute resource value, attempting launcher-specific namespace + * first before falling back to anonymous attribute. + */ + fun getAttributeResourceValue( + parser: XmlResourceParser, attribute: String?, + defaultValue: Int + ): Int { + var value = parser.getAttributeResourceValue( + "http://schemas.android.com/apk/res-auto/foundation.e.blisslauncher", attribute, + defaultValue + ) + if (value == defaultValue) { + value = parser.getAttributeResourceValue(null, attribute, defaultValue) + } + return value + } + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/ParseResult.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/ParseResult.kt new file mode 100644 index 0000000000..72a3257b03 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/ParseResult.kt @@ -0,0 +1,12 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Intent + +/** + * Result return after parsing the node. + * If [result] == 1, the xml node is parsed successfully and it contains additional data in + * [dataTriplet] + * + * If their is any error or no package/shortcut is found [result] is set to -1. + */ +data class ParseResult(val result: Int, val dataTriplet: Triple? = null) \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/ResolveParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/ResolveParser.kt new file mode 100644 index 0000000000..e7924c2279 --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/ResolveParser.kt @@ -0,0 +1,40 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Context +import android.content.res.XmlResourceParser +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import timber.log.Timber +import java.io.IOException + +/** + * Contains a list of nodes, and accepts the first successfully parsed node. + */ +class ResolveParser(context: Context) : TagParser { + private val mChildParser: AppShortcutWithUriParser = AppShortcutWithUriParser(context) + + @Throws(XmlPullParserException::class, IOException::class) + override fun parseAndAdd(parser: XmlResourceParser): ParseResult { + val groupDepth = parser.depth + var type: Int + var parseResult = ParseResult(-1) + while (parser.next().also { type = it } != XmlPullParser.END_TAG || + parser.depth > groupDepth + ) { + Timber.d("Parsing Type is $type") + if (type != XmlPullParser.START_TAG || parseResult.result > -1) { + continue + } + val fallbackItemName = parser.name + Timber.d("FallbackItem $fallbackItemName") + if (DefaultHotseatParser.TAG_FAVORITE == fallbackItemName) { + parseResult = mChildParser.parseAndAdd(parser) + return parseResult + } else { + Timber.e("Fallback groups can contain only favorites, found " + + "$fallbackItemName") + } + } + return parseResult + } +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/parser/TagParser.kt b/data/src/main/java/foundation/e/blisslauncher/data/parser/TagParser.kt new file mode 100644 index 0000000000..caf3ef381a --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/parser/TagParser.kt @@ -0,0 +1,15 @@ +package foundation.e.blisslauncher.data.parser + +import android.content.Intent +import android.content.res.XmlResourceParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +interface TagParser { + /** + * Parses the tag and adds to the db + * @return the id of the row added or -1; + */ + @Throws(XmlPullParserException::class, IOException::class) + fun parseAndAdd(parser: XmlResourceParser): ParseResult +} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/util/LauncherItemComparator.kt b/data/src/main/java/foundation/e/blisslauncher/data/util/LauncherItemComparator.kt new file mode 100644 index 0000000000..949e34b8fc --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/util/LauncherItemComparator.kt @@ -0,0 +1,33 @@ +package foundation.e.blisslauncher.data.util + +import android.os.Process +import foundation.e.blisslauncher.common.util.LabelComparator +import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import javax.inject.Inject + +class LauncherItemComparator +@Inject constructor( + private val userManager: UserManagerRepository +) : + Comparator { + + private val myUser = Process.myUserHandle() + private val labelComparator = LabelComparator() + + override fun compare(itemA: LauncherItem, itemB: LauncherItem): Int { + // Order by the title in the current locale + var result: Int = labelComparator.compare(itemA.title.toString(), itemB.title.toString()) + if (result != 0) { + return result + } + + return if (myUser == itemA.user) { + -1 + } else { + val aUserSerial: Long = userManager.getSerialNumberForUser(itemA.user) + val bUserSerial: Long = userManager.getSerialNumberForUser(itemB.user) + aUserSerial.compareTo(bUserSerial) + } + } +} \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle index 7fb0e35af2..812cffbad8 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -6,6 +6,7 @@ dependencies { implementation project(path: ":common") implementation Libs.RxJava.rxJava implementation Libs.RxJava.rxKotlin + implementation "androidx.room:room-rxjava2:2.2.3" implementation Libs.timber diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt index 3603b6c7c5..a301468699 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/Functions.kt @@ -2,7 +2,7 @@ package foundation.e.blisslauncher.domain import android.content.ComponentName import android.os.UserHandle -import foundation.e.blisslauncher.utils.LongArrayMap +import foundation.e.blisslauncher.common.util.LongArrayMap import foundation.e.blisslauncher.domain.entity.LauncherItem typealias ItemInfoMatcher = (item: LauncherItem, cn: ComponentName) -> Boolean diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt index b365596a8a..3658064174 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/dto/WorkspaceModel.kt @@ -1,13 +1,103 @@ package foundation.e.blisslauncher.domain.dto -import foundation.e.blisslauncher.utils.LongArrayMap +import android.content.Context +import foundation.e.blisslauncher.common.util.LongArrayMap import foundation.e.blisslauncher.domain.entity.FolderItem +import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_DESKTOP +import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_HOTSEAT import foundation.e.blisslauncher.domain.entity.LauncherItem +import foundation.e.blisslauncher.domain.entity.ShortcutItem data class WorkspaceModel( + + /** + * Map of all the Items (shortcuts, folders, and widgets) created by + * LoadLauncher Interactor to their ids + */ val itemsIdMap: LongArrayMap = LongArrayMap(), + + /** + * List of all the folders and shortcuts directly on the home screen (no widgets + * or shortcuts within folders). + */ val workspaceItems: ArrayList = ArrayList(), + + /** + * Map of id to FolderItems of all the folders created by LauncherModel + */ val folders: LongArrayMap = LongArrayMap(), + + /** + * Ordered list of workspace screens ids. + */ val workspaceScreens: ArrayList = ArrayList() +) { + + /** + * Clear all data that this model holds + */ + @Synchronized + fun clear() { + workspaceItems.clear() + workspaceScreens.clear() + itemsIdMap.clear() + folders.clear() + } + + @Synchronized + fun removeItem(context: Context, items: List) { + items.forEach { + when (it.itemType) { + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + workspaceItems.remove(it) + } + LauncherConstants.ItemType.FOLDER -> { + folders.remove(it.id) + //TODO: Add debug log if folder contains some items + } + } + itemsIdMap.remove(it.id) + } + } + + @Synchronized + fun addItem(context: Context, item: LauncherItem, newItem: Boolean) { + itemsIdMap.put(item.id, item) + when (item.itemType) { + LauncherConstants.ItemType.FOLDER -> { + folders.put(item.id, item as FolderItem) + workspaceItems.add(item) + } + LauncherConstants.ItemType.APPLICATION, + LauncherConstants.ItemType.SHORTCUT -> { + if (item.container == CONTAINER_DESKTOP || item.container == CONTAINER_HOTSEAT) { + workspaceItems.add(item) + } else { + if (newItem) { + if (!folders.containsKey(item.container)) { + // Adding an item to a folder that doesn't exist. + val msg = + "adding item: " + item + " to a folder that " + + " doesn't exist" + } + } else { + findOrMakeFolder(item.container).add(item as ShortcutItem, false) + } + } + } + } + } -) \ No newline at end of file + private fun findOrMakeFolder(id: Long): FolderItem { + // See if a placeholder was created for us already + var folderInfo: FolderItem? = folders[id] + if (folderInfo == null) { + // No placeholder -- create a new instance + folderInfo = FolderItem() + folders.put(id, folderInfo) + } + return folderInfo + } +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt index d32f19b3e3..f9bc1b8861 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ApplicationItem.kt @@ -13,7 +13,7 @@ import foundation.e.blisslauncher.domain.keys.ComponentKey /** * Represents an app in Launcher Workspace */ -open class ApplicationItem : AppShortcutItem { +open class ApplicationItem : ShortcutItem { /** * The intent used to start the application. diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt index aa36a342c0..721184b08a 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/FolderItem.kt @@ -7,7 +7,7 @@ class FolderItem : LauncherItem() { var options: Int = 0 - val contents = mutableListOf() + val contents = mutableListOf() val listeners = ArrayList() @@ -21,14 +21,14 @@ class FolderItem : LauncherItem() { * * @param item */ - fun add(item: AppShortcutItem, animate: Boolean) { + fun add(item: ShortcutItem, animate: Boolean) { add(item, contents.size, animate) } /** * Add an app or shortcut for a specified rank. */ - fun add(item: AppShortcutItem, rank: Int, animate: Boolean) { + fun add(item: ShortcutItem, rank: Int, animate: Boolean) { var rank = rank rank = Utilities.boundToRange(rank, 0, contents.size) contents.add(rank, item) @@ -43,7 +43,7 @@ class FolderItem : LauncherItem() { * * @param item */ - fun remove(item: AppShortcutItem, animate: Boolean) { + fun remove(item: ShortcutItem, animate: Boolean) { contents.remove(item) /*for (i in listeners.indices) { listeners.get(i).onRemove(item) @@ -72,8 +72,8 @@ class FolderItem : LauncherItem() { } interface FolderListener { - fun onAdd(item: AppShortcutItem, rank: Int) - fun onRemove(item: AppShortcutItem) + fun onAdd(item: ShortcutItem, rank: Int) + fun onRemove(item: ShortcutItem) fun onTitleChanged(title: CharSequence) fun onItemsChanged(animate: Boolean) fun prepareAutoUpdate() diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt index 5e5af323af..d3589fafd9 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherConstants.kt @@ -24,10 +24,10 @@ class LauncherConstants { object ContainerType { - const val CONTAINER_DESKTOP = -100 - const val CONTAINER_HOTSEAT = -101 + const val CONTAINER_DESKTOP: Long = -100 + const val CONTAINER_HOTSEAT: Long = -101 - fun containerToString(container: Int): String = when (container) { + fun containerToString(container: Long): String = when (container) { CONTAINER_DESKTOP -> "desktop" CONTAINER_HOTSEAT -> "hotseat" else -> container.toString() diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt index 5c63431fd6..cd5676e82d 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/LauncherItem.kt @@ -125,7 +125,7 @@ open class LauncherItem : Entity { protected open fun dumpProperties(): String? { return ("id=" + id + " type=" + LauncherConstants.ItemType.itemTypeToString(itemType) + - " container=" + LauncherConstants.ContainerType.containerToString(container.toInt()) + + " container=" + LauncherConstants.ContainerType.containerToString(container) + " screen=" + screenId + " cell(" + cellX + "," + cellY + ")" + " span(" + spanX + "," + spanY + ")" + diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ShortcutItem.kt similarity index 96% rename from domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt rename to domain/src/main/java/foundation/e/blisslauncher/domain/entity/ShortcutItem.kt index 98d712d359..a4969a90dc 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/AppShortcutItem.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/ShortcutItem.kt @@ -9,7 +9,7 @@ import android.content.Intent.ShortcutIconResource * It can be an Application, Shortcut or Deep Shortcut. * Also used for pinned and dynamic shortcuts of the apps. */ -open class AppShortcutItem : LauncherItemWithIcon { +open class ShortcutItem : LauncherItemWithIcon { /** * The intent used to start the application. @@ -44,7 +44,7 @@ open class AppShortcutItem : LauncherItemWithIcon { itemType = LauncherConstants.ItemType.SHORTCUT } - constructor(item: AppShortcutItem) : super(item) { + constructor(item: ShortcutItem) : super(item) { title = item.title intent = item.getIntent() iconResource = item.iconResource diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt index 9d23f26b78..03ff953914 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/inject/DomainComponent.kt @@ -2,6 +2,8 @@ package foundation.e.blisslauncher.domain.inject import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.executors.AppExecutors +import foundation.e.blisslauncher.domain.repository.LauncherItemRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository /** * Interface that lists all public repositories and data access layer components which are needed @@ -13,6 +15,9 @@ interface DomainComponent { fun launcherAppsCompat(): LauncherAppsCompat + fun workspaceRepository(): WorkspaceRepository + + companion object { @Volatile @JvmStatic diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt index f26dbe2938..a922d0d828 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/Interactor.kt @@ -64,14 +64,9 @@ abstract class ResultInteractor : ObservableInteractor

() { abstract fun doWork(params: P? = null): Single operator fun invoke( - params: P? = null, - onSuccess: (result: T) -> Unit = {}, - onError: (e: Throwable) -> Unit = {} - ) { - disposables += this.doWork(params) - .subscribeOn(Schedulers.from(subscribeExecutor)) - .observeOn(Schedulers.from(observeExecutor)) - .subscribe(onSuccess, onError) + params: P? = null + ): Single { + return this.doWork(params) } } diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt index 617e6d2ab8..75464a441e 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt @@ -2,20 +2,17 @@ package foundation.e.blisslauncher.domain.interactor import foundation.e.blisslauncher.common.executors.AppExecutors import foundation.e.blisslauncher.domain.dto.WorkspaceModel -import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_DESKTOP -import foundation.e.blisslauncher.domain.repository.LauncherItemRepository -import foundation.e.blisslauncher.domain.repository.WorkspaceScreenRepository +import foundation.e.blisslauncher.domain.repository.WorkspaceRepository import io.reactivex.Single +import io.reactivex.schedulers.Schedulers import timber.log.Timber -import java.util.ArrayList import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Singleton @Singleton class LoadLauncher @Inject constructor( - private val launcherItemRepository: LauncherItemRepository, - private val workspaceScreenRepository: WorkspaceScreenRepository, + private val workspaceRepository: WorkspaceRepository, appExecutors: AppExecutors ) : ResultInteractor() { @@ -23,35 +20,27 @@ class LoadLauncher @Inject constructor( override val observeExecutor: Executor = appExecutors.main override fun doWork(params: Unit?): Single { + Timber.d("This is invoked") + return Single.just(WorkspaceModel()) .map { workspaceModel -> - var clearDb = false - - //TODO: GridSize Migration Task - /*if (!clearDb && GridSizeMigrationTask.ENABLED && - !GridSizeMigrationTask.migrateGridIfNeeded(context) - ) { - // Migration failed. Clear workspace. - clearDb = true - }*/ - if (clearDb) { - Timber.d("loadLauncher: resetting launcher database") - clearAllDbs() - } + Timber.d("Current working thread is ${Thread.currentThread().name}") - workspaceModel.workspaceScreens.addAll( + /*workspaceModel.workspaceScreens.addAll( workspaceScreenRepository.findAllOrderedByScreenRank() - .map { it.id }) + ) val launcherItems = launcherItemRepository.findAll() + var count = 0 + launcherItems.forEach { count++ } // Remove any empty screens val unusedScreens: ArrayList = ArrayList(workspaceModel.workspaceScreens) for (item in workspaceModel.itemsIdMap) { val screenId: Long = item.screenId - if (item.container == CONTAINER_DESKTOP.toLong() && + if (item.container == CONTAINER_DESKTOP && unusedScreens.contains(screenId) ) { unusedScreens.remove(screenId) @@ -63,12 +52,9 @@ class LoadLauncher @Inject constructor( workspaceModel.workspaceScreens.removeAll(unusedScreens) //TODO: update workspace screen order in database. } - workspaceModel + val sortedList = sortWorkspaceItems(launcherItems)*/ + workspaceRepository.loadWorkspace() } - } - - private fun clearAllDbs() { - workspaceScreenRepository.deleteAll() - launcherItemRepository.deleteAll() + .subscribeOn(Schedulers.from(subscribeExecutor)) } } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt index e5706e0ad9..17aa60b3ce 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/UpdateLauncher.kt @@ -6,12 +6,12 @@ import android.util.ArrayMap import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.executors.AppExecutors -import foundation.e.blisslauncher.utils.LongArrayMap +import foundation.e.blisslauncher.common.util.LongArrayMap import foundation.e.blisslauncher.domain.ItemInfoMatcher import foundation.e.blisslauncher.domain.Matcher import foundation.e.blisslauncher.domain.and import foundation.e.blisslauncher.domain.entity.ApplicationItem -import foundation.e.blisslauncher.domain.entity.AppShortcutItem +import foundation.e.blisslauncher.domain.entity.ShortcutItem import foundation.e.blisslauncher.domain.or import foundation.e.blisslauncher.domain.repository.LauncherItemRepository import io.reactivex.Completable @@ -46,7 +46,7 @@ class UpdateLauncher( val removedItems = LongArrayMap() val isNewApkAvailable = params.command == Command.ADD || params.command == Command.UPDATE - val updatedItems = ArrayList() + val updatedItems = ArrayList() //TODO: Uncomment it after successful presentation test. //val map = launcherRepository.allItemsMap() /*map.forEach { diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt index 0ae0cd1fd9..269e3da42c 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/Repository.kt @@ -22,9 +22,9 @@ interface Repository { * Saves all given entities. * * @param entities to be saved. - * @return [Iterable] with the saved entities. + * @return [List] with the saved entities. */ - fun saveAll(entities: Iterable): Iterable + fun saveAll(entities: List): List /** * Retrieves an entity by its id. @@ -37,9 +37,9 @@ interface Repository { /** * Return all entities of this type. * - * @return [Iterable] with all entities. + * @return [List] with all entities. */ - fun findAll(): Iterable + fun findAll(): List /** * Deletes a given entity. @@ -59,5 +59,5 @@ interface Repository { /** * Deletes the given entities. */ - fun deleteAll(entities: Iterable) + fun deleteAll(entities: List) } \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceRepository.kt new file mode 100644 index 0000000000..ade788383e --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceRepository.kt @@ -0,0 +1,7 @@ +package foundation.e.blisslauncher.domain.repository + +import foundation.e.blisslauncher.domain.dto.WorkspaceModel + +interface WorkspaceRepository { + fun loadWorkspace(): WorkspaceModel +} \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt index 60e103b7ad..3439004892 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/repository/WorkspaceScreenRepository.kt @@ -3,5 +3,7 @@ package foundation.e.blisslauncher.domain.repository import foundation.e.blisslauncher.domain.entity.WorkspaceScreen interface WorkspaceScreenRepository : Repository { - fun findAllOrderedByScreenRank(): Iterable + fun findAllOrderedByScreenRank(): List + + fun generateNewScreenId(): Long } \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt index 270c8221b7..0be5054088 100644 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/BaseStore.kt @@ -5,11 +5,9 @@ import io.reactivex.ObservableSource import io.reactivex.Observer import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable -import io.reactivex.functions.Consumer import io.reactivex.rxkotlin.plusAssign import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject open class BaseStore( initialState: State, @@ -57,7 +55,7 @@ open class BaseStore { +interface MviView { val events: Observable - fun render(model: ViewModel) + fun render(state: State) } \ No newline at end of file diff --git a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt index f37ef4d4e6..2e5ab5595d 100644 --- a/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt +++ b/mvicore/src/main/java/foundation/e/blisslauncher/mvicore/component/Store.kt @@ -7,7 +7,7 @@ import io.reactivex.functions.Consumer * Store manages the state of the application, similar to the Model * and are bound to a particular domain. */ -interface Store : Consumer, +interface Store : Consumer, ObservableSource { val state: State } \ No newline at end of file -- GitLab From c8359ff510e7818c5d25abcf412f345470ad02b1 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 2 Jun 2020 15:25:54 +0530 Subject: [PATCH 21/23] Temp commit --- .gitignore | 1 + .../features/launcher/LauncherActivity.kt | 27 +-- .../features/launcher/Workspace.kt | 18 +- .../data/LauncherDatabaseGateway.kt | 65 ++--- .../data/WorkspaceRepositoryImpl.kt | 224 +++++++++++++++--- .../data/database/dao/LauncherDao.kt | 9 + .../data/database/roomentity/WorkspaceItem.kt | 10 +- .../database/roomentity/WorkspaceScreen.kt | 5 +- .../domain/interactor/LoadLauncher.kt | 39 +-- 9 files changed, 272 insertions(+), 126 deletions(-) diff --git a/.gitignore b/.gitignore index 34ccb659b1..68fa747df2 100755 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ build/ # IntelliJ *iml .idea/workspace.xml +.idea/jarRepositories.xml .idea/libraries .idea/caches .idea/navEditor.xml diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt index 2f0c002cba..3697bcc114 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/LauncherActivity.kt @@ -10,6 +10,8 @@ import android.os.StrictMode.VmPolicy import android.view.View import android.widget.GridLayout import android.widget.ImageView +import android.widget.TextView +import androidx.core.graphics.drawable.toDrawable import dagger.android.AndroidInjection import foundation.e.blisslauncher.R import foundation.e.blisslauncher.common.DeviceProfile @@ -146,7 +148,6 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { if (state is LauncherState.Loaded) { val model = state.workspaceModel bindScreens(model) - bindWorkspaceItems(model.workspaceItems) } } @@ -154,33 +155,19 @@ class LauncherActivity : BaseDraggingActivity(), LauncherView { private fun bindScreens(model: WorkspaceModel) { workspace.addExtraEmptyScreen() model.workspaceScreens.forEach { - workspace.insertNewWorkspaceScreenBeforeEmptyScreen(it) + workspace.insertNewWorkspaceScreen(it) } Timber.d("Total child in workspace is: ${workspace.childCount}") } private fun bindWorkspaceItems(workspaceItems: ArrayList) { + Timber.d("Total workspace items: ${workspaceItems.size}") workspaceItems.forEach { if (it is LauncherItemWithIcon) { - val view = ImageView(this) - view.setImageBitmap(it.iconBitmap) - val lp = GridLayout.LayoutParams() - lp.width = deviceProfile.iconSizePx - lp.height = deviceProfile.iconSizePx - if (it.screenId >= 0) { - workspace.addInScreenFromBind(view, it) - } else { - var currentScreen = workspace.getChildAt(workspace.childCount - 1) as GridLayout - if (currentScreen.childCount < deviceProfile.inv.numRows * deviceProfile.inv.numColumns) { - currentScreen.addView(view) - } else { - workspace.insertNewWorkspaceScreen(workspace.childCount.toLong()) - currentScreen = - workspace.getChildAt(workspace.childCount - 1) as GridLayout - currentScreen.addView(view) - } - } + val view = TextView(this) + view.setText(it.title) + workspace.addInScreenFromBind(view, it) } } } diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt index b73ccb6f12..bf45889ab4 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/features/launcher/Workspace.kt @@ -74,13 +74,11 @@ class Workspace @JvmOverloads constructor( setLayoutTransition(layoutTransition) - // Set the wallpaper dimensions when Launcher starts up setWallpaperDimension() isMotionEventSplittingEnabled = true //TODO: Set touch listener if required - } private fun setupLayoutTransition() { @@ -311,6 +309,8 @@ class Workspace @JvmOverloads constructor( fun addInScreenFromBind(child: View, item: LauncherItem) { val x = item.cellX val y = item.cellY + Timber.d("Child title is: ${item.title}") + Timber.d("Child screen is: ${item.screenId}") addInScreen(child, item.container, item.screenId, x, y) } @@ -332,12 +332,24 @@ class Workspace @JvmOverloads constructor( throw RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID") } + var layout: GridLayout if (container == LauncherConstants.ContainerType.CONTAINER_HOTSEAT) { //TODO: Hide folder title in hotseat + layout = LauncherActivity.getLauncher(context).findViewById(R.id.hotseat) } else { + layout = getScreenWithId(screenId) } - // TODO: Add view to gridlayout here + val rowSpec = + GridLayout.spec(GridLayout.UNDEFINED) + val colSpec = + GridLayout.spec(GridLayout.UNDEFINED) + val lp = GridLayout.LayoutParams(rowSpec, colSpec) + lp.width = deviceProfile.iconSizePx + lp.height = deviceProfile.iconSizePx + child.layoutParams = lp + layout.addView(child, lp) + child.visibility = View.VISIBLE child.isHapticFeedbackEnabled = false child.setOnLongClickListener(null) } diff --git a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt index 090f0387d9..c2eb0ad959 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/LauncherDatabaseGateway.kt @@ -9,46 +9,22 @@ import androidx.sqlite.db.SupportSQLiteDatabase import foundation.e.blisslauncher.data.database.BlissLauncherDatabase import foundation.e.blisslauncher.data.database.BlissLauncherFiles import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceScreen import io.reactivex.Observable import io.reactivex.schedulers.Schedulers import timber.log.Timber -import java.util.ArrayList import javax.inject.Inject class LauncherDatabaseGateway @Inject constructor( - context: Context, + private val context: Context, private val sharedPrefs: SharedPreferences ) { - private val db: BlissLauncherDatabase + private lateinit var db: BlissLauncherDatabase private var maxItemId: Long = -1 private var maxScreenId: Long = -1 - init { - db = Room.databaseBuilder( - context, - BlissLauncherDatabase::class.java, - BlissLauncherFiles.LAUNCHER_DB - ).addCallback( - object : Callback() { - override fun onCreate(db: SupportSQLiteDatabase) { - super.onCreate(db) - Timber.d("Room database created") - maxItemId = 0 - maxScreenId = 0 - sharedPrefs.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit() - } - - override fun onOpen(db: SupportSQLiteDatabase) { - super.onOpen(db) - Timber.d("Room database opened") - initIds() - } - } - ).build() - } - private fun initIds() { if (maxItemId == -1L) { initializeMaxItemId() @@ -121,7 +97,8 @@ class LauncherDatabaseGateway @Inject constructor( fun getAllWorkspaceItems(): List = db.launcherDao().getAllWorkspaceItems() - fun loadWorkspaceScreensInOrder(): List = emptyList() + fun loadWorkspaceScreensInOrder(): List = + db.launcherDao().getAllWorkspaceScreens().map { it._id } fun markDeleted(id: Long) {} @@ -135,9 +112,41 @@ class LauncherDatabaseGateway @Inject constructor( // Helper function to initialise the database fun createDbIfNotExist() { + db = Room.databaseBuilder( + context, + BlissLauncherDatabase::class.java, + BlissLauncherFiles.LAUNCHER_DB + ).addCallback( + object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + Timber.d("Room database created") + maxItemId = 0 + maxScreenId = 0 + sharedPrefs.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit() + } + + override fun onOpen(db: SupportSQLiteDatabase) { + super.onOpen(db) + Timber.d("Room database opened") + initIds() + } + } + ).build() db.openHelper.readableDatabase } + fun updateWorkspaceScreenOrder(screenIds: ArrayList) { + val list = ArrayList() + for(i in screenIds.indices) { + val id = screenIds[i] + if(id >= 0) { + list.add(WorkspaceScreen(id, i)) + } + } + db.launcherDao().insertAllWorkspaceScreens(list) + } + companion object { const val EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED" } diff --git a/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt index 365394ef58..fea8012556 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt @@ -8,23 +8,24 @@ import android.content.pm.LauncherActivityInfo import android.os.Process import android.os.UserHandle import android.util.LongSparseArray -import androidx.core.graphics.drawable.toBitmap import foundation.e.blisslauncher.common.InvariantDeviceProfile import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.common.util.LabelComparator import foundation.e.blisslauncher.common.util.MultiHashMap import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem import foundation.e.blisslauncher.data.parser.DefaultHotseatParser import foundation.e.blisslauncher.data.shortcuts.PinnedShortcutManager import foundation.e.blisslauncher.data.util.LauncherItemComparator import foundation.e.blisslauncher.domain.dto.WorkspaceModel -import foundation.e.blisslauncher.domain.entity.ShortcutItem import foundation.e.blisslauncher.domain.entity.ApplicationItem import foundation.e.blisslauncher.domain.entity.FolderItem import foundation.e.blisslauncher.domain.entity.LauncherConstants +import foundation.e.blisslauncher.domain.entity.LauncherConstants.ContainerType.CONTAINER_DESKTOP import foundation.e.blisslauncher.domain.entity.LauncherItem import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.ShortcutItem import foundation.e.blisslauncher.domain.keys.ShortcutKey import foundation.e.blisslauncher.domain.repository.UserManagerRepository import foundation.e.blisslauncher.domain.repository.WorkspaceRepository @@ -101,9 +102,12 @@ class WorkspaceRepositoryImpl loadDefaultWorkspaceIfNecessary(allAppsMap) + workspaceModel.workspaceScreens.addAll(launcherDatabase.loadWorkspaceScreensInOrder()) + //Populate item from database and fill necessary details based on users. val launcherDatabaseItems = launcherDatabase.getAllWorkspaceItems() Timber.d("LauncherDatabase size is ${launcherDatabaseItems.size}") + Timber.d("Workspace screen size is ${workspaceModel.workspaceScreens.size}") launcherDatabaseItems .forEach { it.apply { @@ -147,26 +151,128 @@ class WorkspaceRepositoryImpl "Size of launcherItems before processing remaining app items: " + "${workspaceModel.workspaceItems.size}" ) - allAppsMap.values.forEach { info -> - val applicationItem = ApplicationItem( - info, - info.user, - quietMode[userManager.getSerialNumberForUser(info.user)] + + sortWorkspaceItems(workspaceModel.workspaceItems) + + // Processing newly added apps. + // These apps are added when launcher was not running + val apps = ArrayList() + allAppsMap.values.map { + val intent = Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(it.componentName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + + WorkspaceItem( + launcherDatabase.generateNewItemId(), + it.label.toString(), + intent.toUri(0), + CONTAINER_DESKTOP, + -1, + -1, + -1, + LauncherConstants.ItemType.APPLICATION, + -1, + userManager.getSerialNumberForUser(it.user) ) - applicationItem.iconBitmap = info.getBadgedIcon(0).toBitmap() - checkAndAddItem(applicationItem, workspaceModel) } - sortWorkspaceItems(workspaceModel.workspaceItems) + var (cellX, cellY, rank) = lastItemPositionAndRank(workspaceModel.workspaceItems) + val labelComparator = LabelComparator() + apps.sortWith(Comparator { item1, item2 -> + labelComparator.compare(item1.title, item2.title) + }) + + var currentScreenId = + workspaceModel.workspaceScreens[workspaceModel.workspaceScreens.size - 1] + + apps.forEach { + if (rank == idp.numRows * idp.numColumns) { + currentScreenId = launcherDatabase.generateNewScreenId() + workspaceModel.workspaceScreens.add(currentScreenId) + cellX = 0 + cellY = 0 + rank = calculateRank(cellX, cellY, idp.numRows) + } + it.screen = currentScreenId + it.cellX = cellX + it.cellY = cellY + it.rank = rank + val (cX, cY) = generateNewCell(cellX, cellY, idp.numColumns) + cellX = cX + cellY = cY + } + launcherDatabase.saveAll(apps) + launcherDatabase.updateWorkspaceScreenOrder(workspaceModel.workspaceScreens) + + apps.forEach { + val item = convertToLauncherItem( + it, + quietMode, + isSdCardReady, + isSafeMode, + unlockedUsers, + pendingPackages, + shortcutKeyToPinnedShortcuts + ) + if (item != null) { + checkAndAddItem(item, workspaceModel) + } + } + + // Remove any empty screens + val unusedScreens: ArrayList = + ArrayList(workspaceModel.workspaceScreens) + for (item in workspaceModel.itemsIdMap) { + val screenId: Long = item.screenId + if (item.container == CONTAINER_DESKTOP && unusedScreens.contains(screenId)) { + unusedScreens.remove(screenId) + } + } + + // If there are any empty screens remove them, and update. + if (unusedScreens.size != 0) { + workspaceModel.workspaceScreens.removeAll(unusedScreens) + launcherDatabase.updateWorkspaceScreenOrder(workspaceModel.workspaceScreens) + } return workspaceModel } + private fun lastItemPositionAndRank(workspaceItems: ArrayList): Triple { + val size = workspaceItems.size + val lastItem = workspaceItems[size - 1] + var screenId = lastItem.screenId + val x = lastItem.cellX + val y = lastItem.cellY + val rank = calculateRank(x, y, idp.numColumns) + Timber.d("${rank + 1} items on last screens having id $screenId") + return Triple(x, y, rank) + } + private fun checkAndAddItem( - launcherItem: LauncherItem, + item: LauncherItem, + workspaceModel: WorkspaceModel + ): Boolean { + if (checkItemPlacement(item, workspaceModel)) { + workspaceModel.addItem(context, item, false) + return true + } else { + launcherDatabase.markDeleted(item.id) + return false + } + } + + private fun checkItemPlacement( + item: LauncherItem, workspaceModel: WorkspaceModel ): Boolean { - workspaceModel.addItem(context, launcherItem, false) + if (item.container == CONTAINER_DESKTOP) { + if (!workspaceModel.workspaceScreens.contains(item.screenId)) { + launcherDatabase.markDeleted(item.id) + return false + } + } return true } @@ -179,7 +285,7 @@ class WorkspaceRepositoryImpl Timber.d("Hotseat count is $count") val existingWorkspaceItems = launcherDatabase.getAllWorkspaceItems() val apps = ArrayList() - + val screenIds = ArrayList() allAppsMap.values.forEach { if (!isApplicationAlreadyAdded(existingWorkspaceItems, it.componentName)) { val intent = Intent(Intent.ACTION_MAIN) @@ -192,18 +298,80 @@ class WorkspaceRepositoryImpl launcherDatabase.generateNewItemId(), it.label.toString(), intent.toUri(0), - LauncherConstants.ContainerType.CONTAINER_DESKTOP, - -1, -1, -1, LauncherConstants.ItemType.APPLICATION - , 0, userManager.getSerialNumberForUser(Process.myUserHandle()) + CONTAINER_DESKTOP, + -1, + -1, + -1, + LauncherConstants.ItemType.APPLICATION, + -1, + userManager.getSerialNumberForUser(it.user) ) ) } } + val labelComparator = LabelComparator() + apps.sortWith(Comparator { item1, item2 -> + labelComparator.compare(item1.title, item2.title) + }) + + populateScreensAndCells(apps, screenIds) + launcherDatabase.saveAll(apps) + launcherDatabase.updateWorkspaceScreenOrder(screenIds) + clearFlagEmptyDbCreated() + } + } + + private fun populateScreensAndCells( + apps: ArrayList, + screenIds: ArrayList + ) { + var currentScreenId = launcherDatabase.generateNewScreenId() + screenIds.add(currentScreenId) + var cellX = 0 + var cellY = 0 + val cols = idp.numColumns + val rows = idp.numRows + apps.forEach { + var rank = calculateRank(cellX, cellY, cols) + Timber.d("Cellx, celly, rank of ${it.title} is $cellX, $cellY, $rank") + if (rank >= rows * cols) { + currentScreenId = launcherDatabase.generateNewScreenId() + screenIds.add(currentScreenId) + cellX = 0 + cellY = 0 + rank = calculateRank(cellX, cellY, cols) + Timber.d("Cellx, celly, rank of ${it.title} is $cellX, $cellY, $rank") + } + it.screen = currentScreenId + it.cellX = cellX + it.cellY = cellY + it.rank = rank + val (cX, cY) = generateNewCell(cellX, cellY, cols) + cellX = cX + cellY = cY } } + private fun generateNewCell(cellX: Int, cellY: Int, cols: Int): Pair { + var tempX = cellX + 1 + var tempY = cellY + if (tempX >= cols) { + tempX = 0 + tempY += 1 + } + return Pair(tempX, tempY) + } + + private fun calculateRank(cellX: Int, cellY: Int, cols: Int): Int = cellY * cols + cellX + + private fun clearFlagEmptyDbCreated() { + sharedPrefs.edit().putBoolean(LauncherDatabaseGateway.EMPTY_DATABASE_CREATED, false) + .commit() + } + + private fun isApplicationAlreadyAdded( existingWorkspaceItems: List, componentName: ComponentName @@ -300,10 +468,10 @@ class WorkspaceRepositoryImpl val screenCols = idp.numColumns val screenRows = idp.numRows val screenCellCount = screenCols * screenRows - Collections.sort(launcherItems, { lhs, rhs -> + Collections.sort(launcherItems) { lhs, rhs -> if (lhs.container == rhs.container) { when (lhs.container) { - LauncherConstants.ContainerType.CONTAINER_DESKTOP -> { + CONTAINER_DESKTOP -> { val lr = lhs.screenId * screenCellCount + lhs.cellY * screenCols + lhs.cellX val rr = rhs.screenId * screenCellCount + rhs.cellY * screenCols + rhs.cellX val result = lr.compareTo(rr) @@ -318,7 +486,7 @@ class WorkspaceRepositoryImpl } else { lhs.container.compareTo(rhs.container) } - }) + } } private fun convertToLauncherItem( @@ -422,17 +590,15 @@ class WorkspaceRepositoryImpl ) } } - if (launcherItem != null) { - launcherItem.apply { - item.applyCommonProperties(this) - } - launcherItem.intent = item.intent - launcherItem.rank = item.rank - launcherItem.runtimeStatusFlags = - launcherItem.runtimeStatusFlags or disabledState + launcherItem?.apply { + item.applyCommonProperties(this) + this.intent = item.intent + this.rank = item.rank + this.runtimeStatusFlags = + this.runtimeStatusFlags or disabledState if (!isSafeMode && !Utilities.isSystemApp(context, item.intent)) { - launcherItem.runtimeStatusFlags = - launcherItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SAFEMODE + this.runtimeStatusFlags = + this.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SAFEMODE } } launcherItem diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt index ec2f699b30..095f4f76d6 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/LauncherDao.kt @@ -2,12 +2,14 @@ package foundation.e.blisslauncher.data.database.dao import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.RawQuery import androidx.room.Transaction import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.database.roomentity.WorkspaceScreen @Dao abstract class LauncherDao { @@ -21,6 +23,10 @@ abstract class LauncherDao { @Insert abstract fun insertAll(workspaceItems: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertAllWorkspaceScreens(screens: List) + @Transaction open fun createEmptyDb() { dropWorkspaceItemTable() @@ -35,4 +41,7 @@ abstract class LauncherDao { @Query("DELETE FROM workspaceScreens") abstract fun dropWorkspaceScreenTable() + + @Query("SELECT * FROM workspaceScreens") + abstract fun getAllWorkspaceScreens(): List } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt index 5f2f00d578..242fc2b1e2 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceItem.kt @@ -20,14 +20,14 @@ data class WorkspaceItem( val title: String, @ColumnInfo(typeAffinity = ColumnInfo.TEXT, name = "intent") val intentStr: String?, - val container: Long, - val screen: Long, - val cellX: Int, - val cellY: Int, + var container: Long, + var screen: Long, + var cellX: Int, + var cellY: Int, val itemType: Int, @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) @NonNull - val rank: Int, + var rank: Int, @ColumnInfo val profileId: Long ) { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt index 8d5e7bd092..86d1b84bb8 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/WorkspaceScreen.kt @@ -10,8 +10,5 @@ data class WorkspaceScreen( @PrimaryKey val _id: Long, @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) - val screenRank: Int, - @ColumnInfo(defaultValue = "0", typeAffinity = ColumnInfo.INTEGER) - @NonNull - val modified: Int + val screenRank: Int ) \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt index 75464a441e..17268e9285 100644 --- a/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/interactor/LoadLauncher.kt @@ -19,42 +19,7 @@ class LoadLauncher @Inject constructor( override val subscribeExecutor: Executor = appExecutors.io override val observeExecutor: Executor = appExecutors.main - override fun doWork(params: Unit?): Single { - Timber.d("This is invoked") - - return Single.just(WorkspaceModel()) - .map { workspaceModel -> - - Timber.d("Current working thread is ${Thread.currentThread().name}") - - /*workspaceModel.workspaceScreens.addAll( - workspaceScreenRepository.findAllOrderedByScreenRank() - ) - - val launcherItems = launcherItemRepository.findAll() - var count = 0 - launcherItems.forEach { count++ } - - // Remove any empty screens - val unusedScreens: ArrayList = - ArrayList(workspaceModel.workspaceScreens) - for (item in workspaceModel.itemsIdMap) { - val screenId: Long = item.screenId - if (item.container == CONTAINER_DESKTOP && - unusedScreens.contains(screenId) - ) { - unusedScreens.remove(screenId) - } - } - - // If there are any empty screens remove them, and update. - if (unusedScreens.size != 0) { - workspaceModel.workspaceScreens.removeAll(unusedScreens) - //TODO: update workspace screen order in database. - } - val sortedList = sortWorkspaceItems(launcherItems)*/ - workspaceRepository.loadWorkspace() - } + override fun doWork(params: Unit?): Single = + Single.fromCallable { workspaceRepository.loadWorkspace() } .subscribeOn(Schedulers.from(subscribeExecutor)) - } } \ No newline at end of file -- GitLab From fdae8e3620229b47c04fa3d1cb7125f877d06ac6 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 7 Jun 2020 20:21:37 +0530 Subject: [PATCH 22/23] Icon related work --- .idea/dictionaries/amit.xml | 1 + blisslauncherv2/src/main/AndroidManifest.xml | 1 + .../features/launcher/Hotseat.kt | 8 +- .../features/launcher/LauncherActivity.kt | 14 +- .../features/launcher/Workspace.kt | 46 +- .../blisslauncher/graphics/ColorExtractor.kt | 112 ++ .../blisslauncher/graphics/DrawableFactory.kt | 99 ++ .../graphics/DynamicDrawableFactory.kt | 10 + .../graphics/FastBitmapDrawable.kt | 309 +++++ .../touch/CheckLongPressHelper.kt | 88 ++ .../e/blisslauncher/views/CellLayout.kt | 91 ++ .../e/blisslauncher/views/IconTextView.kt | 90 ++ .../{widget => views}/Insettable.kt | 2 +- .../InsettableFrameLayout.kt | 2 +- .../{widget => views}/PagedView.kt | 4 +- .../pageindicators/PageIndicator.kt | 2 +- .../pageindicators/PageIndicatorDots.kt | 2 +- .../main/res/drawable/ic_baseline_cake_24.xml | 10 + .../src/main/res/layout/activity_launcher.xml | 4 +- .../src/main/res/layout/cell_view.xml | 4 + .../res/layout/layout_workspace_screen.xml | 6 +- .../src/main/res/values/dimens.xml | 1 + common.gradle | 1 + .../e/blisslauncher/common}/BitmapRenderer.kt | 14 +- .../e/blisslauncher/common/DeviceProfile.kt | 315 +---- .../common/InvariantDeviceProfile.kt | 4 +- .../common/compat/AdaptiveIconCompat.java | 1096 +++++++++++++++++ .../common/compat/ShortcutInfoCompat.kt | 2 +- .../data/WorkspaceRepositoryImpl.kt | 24 +- .../data/database/dao/IconDao.kt | 5 +- .../data/database/roomentity/IconEntity.kt | 15 +- .../blisslauncher/data/graphics/BitmapInfo.kt | 28 - .../e/blisslauncher/data/icon/IconCache.kt | 164 ++- .../e/blisslauncher/data/icon/IconProvider.kt | 4 +- .../blisslauncher/data/icon/LauncherIcons.kt | 306 +++++ data/src/main/res/values/dimens.xml | 1 + .../domain/entity/PackageItem.kt | 3 + 37 files changed, 2517 insertions(+), 371 deletions(-) create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/ColorExtractor.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DrawableFactory.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DynamicDrawableFactory.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/FastBitmapDrawable.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/CheckLongPressHelper.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/CellLayout.kt create mode 100644 blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/IconTextView.kt rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{widget => views}/Insettable.kt (84%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{widget => views}/InsettableFrameLayout.kt (98%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{widget => views}/PagedView.kt (99%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{widget => views}/pageindicators/PageIndicator.kt (78%) rename blisslauncherv2/src/main/java/foundation/e/blisslauncher/{widget => views}/pageindicators/PageIndicatorDots.kt (99%) create mode 100644 blisslauncherv2/src/main/res/drawable/ic_baseline_cake_24.xml create mode 100644 blisslauncherv2/src/main/res/layout/cell_view.xml rename {data/src/main/java/foundation/e/blisslauncher/data/graphics => common/src/main/java/foundation/e/blisslauncher/common}/BitmapRenderer.kt (86%) create mode 100755 common/src/main/java/foundation/e/blisslauncher/common/compat/AdaptiveIconCompat.java delete mode 100644 data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapInfo.kt create mode 100644 data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt create mode 100644 domain/src/main/java/foundation/e/blisslauncher/domain/entity/PackageItem.kt diff --git a/.idea/dictionaries/amit.xml b/.idea/dictionaries/amit.xml index 217011ced4..e990825d10 100644 --- a/.idea/dictionaries/amit.xml +++ b/.idea/dictionaries/amit.xml @@ -2,6 +2,7 @@ hotseat + unbadged \ No newline at end of file diff --git a/blisslauncherv2/src/main/AndroidManifest.xml b/blisslauncherv2/src/main/AndroidManifest.xml index 79c8b664f5..866623c858 100644 --- a/blisslauncherv2/src/main/AndroidManifest.xml +++ b/blisslauncherv2/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + () - private val workspaceScreens = LongArrayMap() + private val workspaceScreens = LongArrayMap() init { @@ -117,8 +118,8 @@ class Workspace @JvmOverloads constructor( } override fun onViewAdded(child: View?) { - require(child is GridLayout) { "A Workspace can only have CellLayout children." } - val gridlayout = child as GridLayout + require(child is CellLayout) { "A Workspace can only have CellLayout children." } + val CellLayout = child as CellLayout super.onViewAdded(child) } @@ -146,16 +147,20 @@ class Workspace @JvmOverloads constructor( insertNewWorkspaceScreen(screenId, childCount) } - fun insertNewWorkspaceScreen(screenId: Long, insertIndex: Int): GridLayout { + fun insertNewWorkspaceScreen(screenId: Long, insertIndex: Int): CellLayout { if (workspaceScreens.containsKey(screenId)) { throw RuntimeException("Screen id $screenId already exists!") } val newScreen = LayoutInflater.from(context) - .inflate(R.layout.layout_workspace_screen, this, false) as GridLayout + .inflate(R.layout.layout_workspace_screen, this, false) as CellLayout // TODO: Set padding if needed newScreen.columnCount = invariantDeviceProfile.numColumns newScreen.rowCount = invariantDeviceProfile.numRows + + val paddingLeftRight: Int = deviceProfile.cellLayoutPaddingLeftRightPx + val paddingBottom: Int = deviceProfile.cellLayoutBottomPaddingPx + newScreen.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom) workspaceScreens.put(screenId, newScreen) screenOrder.add(insertIndex, screenId) addView(newScreen, insertIndex) @@ -249,7 +254,7 @@ class Workspace @JvmOverloads constructor( // XXX: Do we need to update LM workspace screens below? val alpha = PropertyValuesHolder.ofFloat("alpha", 0f) val bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", 0f) - val gl: GridLayout = + val gl: CellLayout = workspaceScreens.get(EXTRA_EMPTY_SCREEN_ID) val oa: ObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(gl, alpha, bgAlpha) oa.duration = duration.toLong() @@ -286,7 +291,7 @@ class Workspace @JvmOverloads constructor( fun getScreenWithId(screenId: Long) = workspaceScreens[screenId] - fun getIdForScreen(screen: GridLayout): Long { + fun getIdForScreen(screen: CellLayout): Long { val index = workspaceScreens.indexOfValue(screen) return if (index != -1) workspaceScreens.keyAt(index) else -1 } @@ -332,26 +337,19 @@ class Workspace @JvmOverloads constructor( throw RuntimeException("Screen id should not be EXTRA_EMPTY_SCREEN_ID") } - var layout: GridLayout + var layout: CellLayout if (container == LauncherConstants.ContainerType.CONTAINER_HOTSEAT) { //TODO: Hide folder title in hotseat - layout = LauncherActivity.getLauncher(context).findViewById(R.id.hotseat) + //layout = LauncherActivity.getLauncher(context).hotseat } else { layout = getScreenWithId(screenId) + layout.addView(child) + child.visibility = View.VISIBLE + child.isHapticFeedbackEnabled = false + child.setOnLongClickListener(null) } - val rowSpec = - GridLayout.spec(GridLayout.UNDEFINED) - val colSpec = - GridLayout.spec(GridLayout.UNDEFINED) - val lp = GridLayout.LayoutParams(rowSpec, colSpec) - lp.width = deviceProfile.iconSizePx - lp.height = deviceProfile.iconSizePx - child.layoutParams = lp - layout.addView(child, lp) - child.visibility = View.VISIBLE - child.isHapticFeedbackEnabled = false - child.setOnLongClickListener(null) + val genericLp = child.layoutParams } private fun shouldConsumeTouch(v: View): Boolean { diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/ColorExtractor.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/ColorExtractor.kt new file mode 100644 index 0000000000..057d77446e --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/ColorExtractor.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.graphics + +import android.graphics.Bitmap +import android.graphics.Color +import android.util.SparseArray + +/** + * Utility class for extracting colors from a bitmap. + */ +object ColorExtractor { + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + * @param samples The approximate max number of samples to use. + */ + @JvmOverloads + fun findDominantColorByHue(bitmap: Bitmap, samples: Int = 20): Int { + val height = bitmap.height + val width = bitmap.width + var sampleStride = Math.sqrt(height * width / samples.toDouble()).toInt() + if (sampleStride < 1) { + sampleStride = 1 + } + + // This is an out-param, for getting the hsv values for an rgb + val hsv = FloatArray(3) + + // First get the best hue, by creating a histogram over 360 hue buckets, + // where each pixel contributes a score weighted by saturation, value, and alpha. + val hueScoreHistogram = FloatArray(360) + var highScore = -1f + var bestHue = -1 + val pixels = IntArray(samples) + var pixelCount = 0 + var y = 0 + while (y < height) { + var x = 0 + while (x < width) { + val argb = bitmap.getPixel(x, y) + val alpha = 0xFF and (argb shr 24) + if (alpha < 0x80) { + // Drop mostly-transparent pixels. + x += sampleStride + continue + } + // Remove the alpha channel. + val rgb = argb or -0x1000000 + Color.colorToHSV(rgb, hsv) + // Bucket colors by the 360 integer hues. + val hue = hsv[0].toInt() + if (hue < 0 || hue >= hueScoreHistogram.size) { + // Defensively avoid array bounds violations. + x += sampleStride + continue + } + if (pixelCount < samples) { + pixels[pixelCount++] = rgb + } + val score = hsv[1] * hsv[2] + hueScoreHistogram[hue] += score + if (hueScoreHistogram[hue] > highScore) { + highScore = hueScoreHistogram[hue] + bestHue = hue + } + x += sampleStride + } + y += sampleStride + } + val rgbScores = SparseArray() + var bestColor = -0x1000000 + highScore = -1f + // Go back over the RGB colors that match the winning hue, + // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. + // The highest-scoring RGB color wins. + for (i in 0 until pixelCount) { + val rgb = pixels[i] + Color.colorToHSV(rgb, hsv) + val hue = hsv[0].toInt() + if (hue == bestHue) { + val s = hsv[1] + val v = hsv[2] + val bucket = (s * 100).toInt() + (v * 10000).toInt() + // Score by cumulative saturation * value. + val score = s * v + val oldTotal = rgbScores[bucket] + val newTotal = if (oldTotal == null) score else oldTotal + score + rgbScores.put(bucket, newTotal) + if (newTotal > highScore) { + highScore = newTotal + // All the colors in the winning bucket are very similar. Last in wins. + bestColor = rgb + } + } + } + return bestColor + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DrawableFactory.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DrawableFactory.kt new file mode 100644 index 0000000000..82c7842995 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DrawableFactory.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.graphics + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Process +import android.os.UserHandle +import android.util.ArrayMap +import foundation.e.blisslauncher.R +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Factory for creating new drawables. + */ +@Singleton +open class DrawableFactory @Inject constructor(context: Context){ + private val myUser: UserHandle = Process.myUserHandle() + private val userBadges = ArrayMap() + + /** + * Returns a FastBitmapDrawable with the icon. + */ + fun newIcon(info: LauncherItemWithIcon): FastBitmapDrawable { + val drawable = FastBitmapDrawable(info) + drawable.setIsDisabled(info.isDisabled()) + return drawable + } + + /** + * Returns a drawable that can be used as a badge for the user or null. + */ + fun getBadgeForUser(user: UserHandle, context: Context): Drawable? { + if (myUser == user) { + return null + } + val badgeBitmap = getUserBadge(user, context) + val d = FastBitmapDrawable(badgeBitmap) + d.isFilterBitmap = true + d.setBounds(0, 0, badgeBitmap!!.width, badgeBitmap.height) + return d + } + + @Synchronized + fun getUserBadge( + user: UserHandle, + context: Context + ): Bitmap? { + var badgeBitmap = userBadges[user] + if (badgeBitmap != null) { + return badgeBitmap + } + val res = context.applicationContext.resources + val badgeSize = + res.getDimensionPixelSize(R.dimen.profile_badge_size) + badgeBitmap = Bitmap.createBitmap( + badgeSize, + badgeSize, + Bitmap.Config.ARGB_8888 + ) + val drawable = context.packageManager.getUserBadgedDrawableForDensity( + BitmapDrawable(res, badgeBitmap), + user, + Rect(0, 0, badgeSize, badgeSize), + 0 + ) + if (drawable is BitmapDrawable) { + badgeBitmap = drawable.bitmap + } else { + badgeBitmap.eraseColor(Color.TRANSPARENT) + val c = Canvas(badgeBitmap) + drawable.setBounds(0, 0, badgeSize, badgeSize) + drawable.draw(c) + c.setBitmap(null) + } + userBadges[user] = badgeBitmap + return badgeBitmap + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DynamicDrawableFactory.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DynamicDrawableFactory.kt new file mode 100644 index 0000000000..fcd74b2290 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/DynamicDrawableFactory.kt @@ -0,0 +1,10 @@ +package foundation.e.blisslauncher.graphics + +import android.content.Context +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DynamicDrawableFactory @Inject constructor(context: Context) : DrawableFactory(context) { + +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/FastBitmapDrawable.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/FastBitmapDrawable.kt new file mode 100644 index 0000000000..9120651731 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/FastBitmapDrawable.kt @@ -0,0 +1,309 @@ +/* + * 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 foundation.e.blisslauncher.graphics + +import android.R +import android.animation.ObjectAnimator +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.Property +import android.util.SparseArray +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import kotlin.math.floor + +open class FastBitmapDrawable constructor( + protected var bitmap: Bitmap? +) : + Drawable() { + protected val mPaint = + Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG) + private var mIsPressed = false + private var mIsDisabled = false + private var mScaleAnimation: ObjectAnimator? = null + private var mScale = 1f + + // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and + // as a result, can be used to compose the key for the cached ColorMatrixColorFilters + private var mDesaturation = 0 + private var mBrightness = 0 + private var mAlpha = 255 + private var mPrevUpdateKey = Int.MAX_VALUE + + constructor(info: LauncherItemWithIcon) : this(info.iconBitmap) + + override fun draw(canvas: Canvas) { + if (mScaleAnimation != null) { + val count = canvas.save() + val bounds = bounds + canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY()) + drawInternal(canvas, bounds) + canvas.restoreToCount(count) + } else { + drawInternal(canvas, bounds) + } + } + + protected fun drawInternal( + canvas: Canvas, + bounds: Rect? + ) { + canvas.drawBitmap(bitmap, null, bounds, mPaint) + } + + override fun setColorFilter(cf: ColorFilter) { + // No op + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSLUCENT + } + + override fun setAlpha(alpha: Int) { + mAlpha = alpha + mPaint.alpha = alpha + } + + override fun setFilterBitmap(filterBitmap: Boolean) { + mPaint.isFilterBitmap = filterBitmap + mPaint.isAntiAlias = filterBitmap + } + + override fun getAlpha(): Int { + return mAlpha + } + + val animatedScale: Float + get() = if (mScaleAnimation == null) 1f else mScale + + override fun getIntrinsicWidth(): Int { + return bitmap!!.width + } + + override fun getIntrinsicHeight(): Int { + return bitmap!!.height + } + + override fun getMinimumWidth(): Int { + return bounds.width() + } + + override fun getMinimumHeight(): Int { + return bounds.height() + } + + override fun isStateful(): Boolean { + return true + } + + override fun getColorFilter(): ColorFilter { + return mPaint.colorFilter + } + + override fun onStateChange(state: IntArray): Boolean { + var isPressed = false + for (s in state) { + if (s == R.attr.state_pressed) { + isPressed = true + break + } + } + if (mIsPressed != isPressed) { + mIsPressed = isPressed + if (mScaleAnimation != null) { + mScaleAnimation!!.cancel() + mScaleAnimation = null + } + if (mIsPressed) { + // Animate when going to pressed state + mScaleAnimation = ObjectAnimator.ofFloat( + this, + SCALE, + PRESSED_SCALE + ) + mScaleAnimation!!.duration = CLICK_FEEDBACK_DURATION.toLong() + mScaleAnimation!!.start() + } else { + mScale = 1f + invalidateSelf() + } + return true + } + return false + } + + private fun invalidateDesaturationAndBrightness() { + desaturation = if (mIsDisabled) DISABLED_DESATURATION else 0f + brightness = if (mIsDisabled) DISABLED_BRIGHTNESS else 0f + } + + fun setIsDisabled(isDisabled: Boolean) { + if (mIsDisabled != isDisabled) { + mIsDisabled = isDisabled + invalidateDesaturationAndBrightness() + } + } + + /** + * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] + */ + var desaturation: Float + get() = mDesaturation.toFloat() / REDUCED_FILTER_VALUE_SPACE + private set(desaturation) { + val newDesaturation = + Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE.toDouble()).toInt() + if (mDesaturation != newDesaturation) { + mDesaturation = newDesaturation + updateFilter() + } + } + + /** + * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious] + */ + private var brightness: Float + private get() = mBrightness.toFloat() / REDUCED_FILTER_VALUE_SPACE + private set(brightness) { + val newBrightness = + floor(brightness * REDUCED_FILTER_VALUE_SPACE.toDouble()).toInt() + if (mBrightness != newBrightness) { + mBrightness = newBrightness + updateFilter() + } + } + + /** + * Updates the paint to reflect the current brightness and saturation. + */ + private fun updateFilter() { + var usePorterDuffFilter = false + var key = -1 + if (mDesaturation > 0) { + key = mDesaturation shl 16 or mBrightness + } else if (mBrightness > 0) { + // Compose a key with a fully saturated icon if we are just animating brightness + key = 1 shl 16 or mBrightness + + // We found that in L, ColorFilters cause drawing artifacts with shadows baked into + // icons, so just use a PorterDuff filter when we aren't animating saturation + usePorterDuffFilter = true + } + + // Debounce multiple updates on the same frame + if (key == mPrevUpdateKey) { + return + } + mPrevUpdateKey = key + if (key != -1) { + var filter = sCachedFilter[key] + if (filter == null) { + val brightnessF = brightness + val brightnessI = (255 * brightnessF).toInt() + if (usePorterDuffFilter) { + filter = PorterDuffColorFilter( + Color.argb(brightnessI, 255, 255, 255), + PorterDuff.Mode.SRC_ATOP + ) + } else { + val saturationF = 1f - desaturation + sTempFilterMatrix.setSaturation(saturationF) + if (mBrightness > 0) { + // Brightness: C-new = C-old*(1-amount) + amount + val scale = 1f - brightnessF + val mat = + sTempBrightnessMatrix.array + mat[0] = scale + mat[6] = scale + mat[12] = scale + mat[4] = brightnessI.toFloat() + mat[9] = brightnessI.toFloat() + mat[14] = brightnessI.toFloat() + sTempFilterMatrix.preConcat(sTempBrightnessMatrix) + } + filter = ColorMatrixColorFilter(sTempFilterMatrix) + } + sCachedFilter.append(key, filter) + } + mPaint.colorFilter = filter + } else { + mPaint.colorFilter = null + } + invalidateSelf() + } + + override fun getConstantState(): ConstantState { + return MyConstantState(bitmap) + } + + protected class MyConstantState(protected val mBitmap: Bitmap?) : + ConstantState() { + override fun newDrawable(): Drawable { + return FastBitmapDrawable(mBitmap) + } + + override fun getChangingConfigurations(): Int { + return 0 + } + } + + companion object { + private const val PRESSED_SCALE = 1.1f + private const val DISABLED_DESATURATION = 1f + private const val DISABLED_BRIGHTNESS = 0.5f + const val CLICK_FEEDBACK_DURATION = 200 + + // Since we don't need 256^2 values for combinations of both the brightness and saturation, we + // reduce the value space to a smaller value V, which reduces the number of cached + // ColorMatrixColorFilters that we need to keep to V^2 + private const val REDUCED_FILTER_VALUE_SPACE = 48 + + // A cache of ColorFilters for optimizing brightness and saturation animations + private val sCachedFilter = SparseArray() + + // Temporary matrices used for calculation + private val sTempBrightnessMatrix = ColorMatrix() + private val sTempFilterMatrix = ColorMatrix() + + // Animator and properties for the fast bitmap drawable's scale + private val SCALE: Property = object : + Property(java.lang.Float.TYPE, "scale") { + override fun get(fastBitmapDrawable: FastBitmapDrawable): Float { + return fastBitmapDrawable.mScale + } + + override fun set( + fastBitmapDrawable: FastBitmapDrawable, + value: Float + ) { + fastBitmapDrawable.mScale = value + fastBitmapDrawable.invalidateSelf() + } + } + } + + init { + isFilterBitmap = true + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/CheckLongPressHelper.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/CheckLongPressHelper.kt new file mode 100644 index 0000000000..d77aeea69f --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/touch/CheckLongPressHelper.kt @@ -0,0 +1,88 @@ +/* + * 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 foundation.e.blisslauncher.touch + +import android.view.View +import android.view.View.OnLongClickListener + +class CheckLongPressHelper { + var mView: View + var mListener: OnLongClickListener? = null + var mHasPerformedLongPress = false + private var mLongPressTimeout = + DEFAULT_LONG_PRESS_TIMEOUT + private var mPendingCheckForLongPress: CheckForLongPress? = + null + + internal inner class CheckForLongPress : Runnable { + override fun run() { + if (mView.parent != null && mView.hasWindowFocus() + && !mHasPerformedLongPress + ) { + val handled: Boolean = if (mListener != null) { + mListener!!.onLongClick(mView) + } else { + mView.performLongClick() + } + if (handled) { + mView.isPressed = false + mHasPerformedLongPress = true + } + } + } + } + + constructor(v: View) { + mView = v + } + + constructor(v: View, listener: OnLongClickListener?) { + mView = v + mListener = listener + } + + /** + * Overrides the default long press timeout. + */ + fun setLongPressTimeout(longPressTimeout: Int) { + mLongPressTimeout = longPressTimeout + } + + fun postCheckForLongPress() { + mHasPerformedLongPress = false + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = + CheckForLongPress() + } + mView.postDelayed(mPendingCheckForLongPress, mLongPressTimeout.toLong()) + } + + fun cancelLongPress() { + mHasPerformedLongPress = false + if (mPendingCheckForLongPress != null) { + mView.removeCallbacks(mPendingCheckForLongPress) + mPendingCheckForLongPress = null + } + } + + fun hasPerformedLongPress(): Boolean { + return mHasPerformedLongPress + } + + companion object { + const val DEFAULT_LONG_PRESS_TIMEOUT = 300 + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/CellLayout.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/CellLayout.kt new file mode 100644 index 0000000000..e739ba7b0f --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/CellLayout.kt @@ -0,0 +1,91 @@ +package foundation.e.blisslauncher.views + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.widget.GridLayout +import foundation.e.blisslauncher.common.DeviceProfile +import foundation.e.blisslauncher.common.LauncherConstants +import foundation.e.blisslauncher.features.launcher.LauncherActivity + +open class CellLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : GridLayout(context, attrs, defStyleAttr), Insettable { + + private val TAG = "CellLayout" + + private val launcher: LauncherActivity = LauncherActivity.getLauncher(context) + private val dp = launcher.deviceProfile + val countX = dp.inv.numColumns + val countY = dp.inv.numRows + + open var containerType = LauncherConstants.ContainerType.CONTAINER_DESKTOP + + private var cellWidth: Int = 0 + private var cellHeight: Int = 0 + + init { + setWillNotDraw(false) + clipToPadding = false + } + + override fun setInsets(insets: Rect) { + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + super.onMeasure(widthSpec, heightSpec) + Log.d( + TAG, + "$this onMeasure() called with: widthSpec = $widthSpec, heightSpec = $heightSpec" + ) + val widthSpecMode = MeasureSpec.getMode(widthSpec) + val heightSpecMode = MeasureSpec.getMode(heightSpec) + val widthSize = MeasureSpec.getSize(widthSpec) + val heightSize = MeasureSpec.getSize(heightSpec) + val childWidthSize = widthSize - (paddingLeft + paddingRight) + val childHeightSize = heightSize - (paddingTop + paddingBottom) + cellWidth = DeviceProfile.calculateCellWidth(childWidthSize, countX) + cellHeight = DeviceProfile.calculateCellHeight(childHeightSize, countY) + Log.d(TAG, "cellWidth: $cellWidth") + setMeasuredDimension(widthSize, heightSize) + for (i in 0 until childCount) { + val child = getChildAt(i) + if (child.visibility != View.GONE) { + measureChild(child) + } + } + } + + private fun measureChild(child: View) { + val lp = child.layoutParams as LayoutParams + lp.rowSpec = spec(UNDEFINED) + lp.columnSpec = spec(UNDEFINED) + lp.width = cellWidth + lp.height = cellHeight + // Center the icon/folder + val cHeight: Int = dp.cellHeightPx + val cellPaddingY = 0f.coerceAtLeast((lp.height - cHeight) / 2f).toInt() + var cellPaddingX: Int + if (containerType == LauncherConstants.ContainerType.CONTAINER_DESKTOP) { + cellPaddingX = dp.workspaceCellPaddingXPx + } else { + cellPaddingX = (dp.edgeMarginPx / 2f).toInt() + } + child.setPadding(cellPaddingX, cellPaddingY, cellPaddingX, 0) + val childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY) + val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY) + child.measure(childWidthMeasureSpec, childHeightMeasureSpec) + } + + override fun onViewAdded(child: View?) { + super.onViewAdded(child) + Log.d(TAG, "onViewAdded() called with: child = $child") + } + + fun addViewToCellLayout(child: View, index: Int, childId: Int, params: LayoutParams) { + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/IconTextView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/IconTextView.kt new file mode 100644 index 0000000000..c628756508 --- /dev/null +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/IconTextView.kt @@ -0,0 +1,90 @@ +package foundation.e.blisslauncher.views + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.text.TextUtils.TruncateAt +import android.util.TypedValue +import android.widget.TextView +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.ShortcutItem +import foundation.e.blisslauncher.features.launcher.LauncherActivity +import foundation.e.blisslauncher.graphics.DrawableFactory +import kotlin.math.ceil + +/** + * A text view which displays an icon on top side of it. + */ +@SuppressLint("AppCompatCustomView") +class IconTextView @JvmOverloads constructor(context: Context) : TextView(context) { + + private val launcher: LauncherActivity = LauncherActivity.getLauncher(context) + private val dp = launcher.deviceProfile + val defaultIconSize = dp.iconSizePx + + private var disableRelayout = false + private var mIcon: Drawable? = null + + init { + setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.iconTextSizePx.toFloat()) + compoundDrawablePadding = dp.iconDrawablePaddingPx + ellipsize = TruncateAt.END + } + + override fun onFocusChanged( + focused: Boolean, + direction: Int, + previouslyFocusedRect: Rect? + ) { + // Disable marques when not focused to that, so that updating text does not cause relayout. + ellipsize = if (focused) TruncateAt.MARQUEE else TruncateAt.END + super.onFocusChanged(focused, direction, previouslyFocusedRect) + } + + fun reset() {} + + fun applyFromShortcutItem(item: ShortcutItem) { + applyIconAndLabel(item) + tag = item + applyBadgeState(item, false) + } + + private fun applyIconAndLabel(item: LauncherItemWithIcon) { + val icon = DrawableFactory(context).newIcon(item) + disableRelayout = mIcon != null + icon.setBounds(0, 0, defaultIconSize, defaultIconSize) + setCompoundDrawables(null, icon, null, null) + disableRelayout = false + mIcon = icon + text = item.title + } + + private fun applyBadgeState(item: ShortcutItem, animate: Boolean) { + } + + override fun setTag(tag: Any?) { + if (tag != null) { + //TODO: Check Item info locked + } + super.setTag(tag) + } + + override fun requestLayout() { + if (!disableRelayout) { + super.requestLayout() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val fm = paint.fontMetrics + val cellHeightPx: Int = defaultIconSize + compoundDrawablePadding + + ceil(fm.bottom - fm.top.toDouble()).toInt() + val height = MeasureSpec.getSize(heightMeasureSpec) + setPadding( + paddingLeft, (height - cellHeightPx) / 2, paddingRight, + paddingBottom + ) + } +} \ No newline at end of file diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/Insettable.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/Insettable.kt similarity index 84% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/Insettable.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/Insettable.kt index f803b5ede2..f3238fb57c 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/Insettable.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/Insettable.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.widget +package foundation.e.blisslauncher.views import android.graphics.Rect /** diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/InsettableFrameLayout.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/InsettableFrameLayout.kt similarity index 98% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/InsettableFrameLayout.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/InsettableFrameLayout.kt index 4152b2deda..1392b1140a 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/InsettableFrameLayout.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/InsettableFrameLayout.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.widget +package foundation.e.blisslauncher.views import android.content.Context import android.graphics.Rect diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/PagedView.kt similarity index 99% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/PagedView.kt index bb8d163672..ff48ca2fc0 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/PagedView.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/PagedView.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.widget +package foundation.e.blisslauncher.views import android.animation.LayoutTransition import android.animation.TimeInterpolator @@ -18,7 +18,7 @@ import android.view.ViewGroup import android.widget.Scroller import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.touch.OverScroll -import foundation.e.blisslauncher.widget.pageindicators.PageIndicatorDots +import foundation.e.blisslauncher.views.pageindicators.PageIndicatorDots import java.util.ArrayList import kotlin.math.sin diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicator.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicator.kt similarity index 78% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicator.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicator.kt index 5541ba8777..cd4af6494c 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicator.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicator.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.widget.pageindicators +package foundation.e.blisslauncher.views.pageindicators /** * Interface for a page indicator. diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicatorDots.kt b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicatorDots.kt similarity index 99% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicatorDots.kt rename to blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicatorDots.kt index 970c790486..e3f2e1b38b 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/widget/pageindicators/PageIndicatorDots.kt +++ b/blisslauncherv2/src/main/java/foundation/e/blisslauncher/views/pageindicators/PageIndicatorDots.kt @@ -1,4 +1,4 @@ -package foundation.e.blisslauncher.widget.pageindicators +package foundation.e.blisslauncher.views.pageindicators import android.animation.Animator import android.animation.AnimatorListenerAdapter diff --git a/blisslauncherv2/src/main/res/drawable/ic_baseline_cake_24.xml b/blisslauncherv2/src/main/res/drawable/ic_baseline_cake_24.xml new file mode 100644 index 0000000000..4150bf8dc6 --- /dev/null +++ b/blisslauncherv2/src/main/res/drawable/ic_baseline_cake_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/blisslauncherv2/src/main/res/layout/activity_launcher.xml b/blisslauncherv2/src/main/res/layout/activity_launcher.xml index fef2bd7684..ee197b7fc3 100644 --- a/blisslauncherv2/src/main/res/layout/activity_launcher.xml +++ b/blisslauncherv2/src/main/res/layout/activity_launcher.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/layout/cell_view.xml b/blisslauncherv2/src/main/res/layout/cell_view.xml new file mode 100644 index 0000000000..352d797925 --- /dev/null +++ b/blisslauncherv2/src/main/res/layout/cell_view.xml @@ -0,0 +1,4 @@ + + diff --git a/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml b/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml index 9b058ad54a..516a786d90 100644 --- a/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml +++ b/blisslauncherv2/src/main/res/layout/layout_workspace_screen.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/blisslauncherv2/src/main/res/values/dimens.xml b/blisslauncherv2/src/main/res/values/dimens.xml index 86d89a2827..fe49c304ad 100644 --- a/blisslauncherv2/src/main/res/values/dimens.xml +++ b/blisslauncherv2/src/main/res/values/dimens.xml @@ -1,4 +1,5 @@ 8dp + 24dp \ No newline at end of file diff --git a/common.gradle b/common.gradle index 12b0f27adf..9189c8ec5d 100644 --- a/common.gradle +++ b/common.gradle @@ -30,6 +30,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation Libs.Kotlin.stdlib + implementation Libs.AndroidX.coreKtx // Dagger implementation Libs.Dagger.dagger diff --git a/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapRenderer.kt b/common/src/main/java/foundation/e/blisslauncher/common/BitmapRenderer.kt similarity index 86% rename from data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapRenderer.kt rename to common/src/main/java/foundation/e/blisslauncher/common/BitmapRenderer.kt index 54be67afbb..707123794f 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapRenderer.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/BitmapRenderer.kt @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package foundation.e.blisslauncher.data.graphics +package foundation.e.blisslauncher.common import android.annotation.TargetApi import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Picture import android.os.Build -import foundation.e.blisslauncher.common.Utilities object BitmapRenderer { - val USE_HARDWARE_BITMAP: Boolean = Utilities.ATLEAST_P + val USE_HARDWARE_BITMAP = Utilities.ATLEAST_P + fun createSoftwareBitmap( width: Int, height: Int, @@ -45,7 +45,11 @@ object BitmapRenderer { renderer: Renderer ): Bitmap { if (!USE_HARDWARE_BITMAP) { - return createSoftwareBitmap(width, height, renderer) + return createSoftwareBitmap( + width, + height, + renderer + ) } val picture = Picture() renderer.draw(picture.beginRecording(width, height)) @@ -57,6 +61,6 @@ object BitmapRenderer { * Interface representing a bitmap draw operation. */ interface Renderer { - fun draw(out: Canvas?) + fun draw(out: Canvas) } } \ No newline at end of file diff --git a/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt b/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt index 2d5d4a0291..28545b913f 100644 --- a/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/DeviceProfile.kt @@ -24,8 +24,7 @@ import android.graphics.Point import android.graphics.PointF import android.graphics.Rect import android.util.DisplayMetrics -import android.view.Surface -import android.view.WindowManager +import android.util.Log class DeviceProfile( context: Context, @@ -33,9 +32,7 @@ class DeviceProfile( minSize: Point, maxSize: Point, width: Int, - height: Int, - isLandscape: Boolean, - isMultiWindowMode: Boolean + height: Int ) { val inv: InvariantDeviceProfile @@ -45,9 +42,6 @@ class DeviceProfile( val isPhone: Boolean val transposeLayoutWithOrientation: Boolean - // Device properties in current orientation - val isLandscape: Boolean - val isMultiWindowMode: Boolean val widthPx: Int val heightPx: Int var availableWidthPx = 0 @@ -61,11 +55,6 @@ class DeviceProfile( val defaultWidgetPadding: Rect val defaultPageSpacingPx: Int private val topWorkspacePadding: Int - var workspaceSpringLoadShrinkFactor = 0f - val workspaceSpringLoadedBottomSpace: Int - - // Drag handle - val verticalDragHandleSizePx: Int // Workspace icons var iconSizePx = 0 @@ -98,18 +87,14 @@ class DeviceProfile( val hotseatBarBottomPaddingPx: Int val hotseatBarSidePaddingPx: Int - // All apps - var allAppsCellHeightPx = 0 - var allAppsIconSizePx = 0 - var allAppsIconDrawablePaddingPx = 0 - var allAppsIconTextSizePx = 0f - // Widgets val appWidgetScale = PointF(1.0f, 1.0f) // Drop Target var dropTargetBarSizePx: Int + private val TAG = "DeviceProfile" + // Insets val insets = Rect() val workspacePadding = Rect() @@ -120,69 +105,16 @@ class DeviceProfile( val size = Point(availableWidthPx, availableHeightPx) return DeviceProfile( - context, inv, size, size, widthPx, heightPx, isLandscape, - isMultiWindowMode + context, inv, size, size, widthPx, heightPx ) } - fun getMultiWindowProfile( - context: Context, - mwSize: Point - ): DeviceProfile { - // We take the minimum sizes of this profile and it's multi-window variant to ensure that - // the system decor is always excluded. - mwSize[Math.min(availableWidthPx, mwSize.x)] = Math.min(availableHeightPx, mwSize.y) - // In multi-window mode, we can have widthPx = availableWidthPx - // and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles' - // widthPx and heightPx values where it's needed. - val profile = - DeviceProfile( - context, inv, mwSize, mwSize, mwSize.x, mwSize.y, - isLandscape, true - ) - // If there isn't enough vertical cell padding with the labels displayed, hide the labels. - val workspaceCellPaddingY = (profile.cellSize.y - profile.iconSizePx - - iconDrawablePaddingPx - profile.iconTextSizePx).toFloat() - if (workspaceCellPaddingY < profile.iconDrawablePaddingPx * 2) { - profile.adjustToHideWorkspaceLabels() - } - // We use these scales to measure and layout the widgets using their full invariant profile - // sizes and then draw them scaled and centered to fit in their multi-window mode cellspans. - val appWidgetScaleX = - profile.cellSize.x.toFloat() / cellSize.x - val appWidgetScaleY = - profile.cellSize.y.toFloat() / cellSize.y - profile.appWidgetScale[appWidgetScaleX] = appWidgetScaleY - profile.updateWorkspacePadding() - return profile - } - /** * Inverse of [.getMultiWindowProfile] * @return device profile corresponding to the current orientation in non multi-window mode. */ - val fullScreenProfile: DeviceProfile? = null - //get() = if (isLandscape) inv.landscapeProfile else inv.portraitProfile - - /** - * Adjusts the profile so that the labels on the Workspace are hidden. - * It is important to call this method after the All Apps variables have been set. - */ - private fun adjustToHideWorkspaceLabels() { - iconTextSizePx = 0 - iconDrawablePaddingPx = 0 - cellHeightPx = iconSizePx - // In normal cases, All Apps cell height should equal the Workspace cell height. - // Since we are removing labels from the Workspace, we need to manually compute the - // All Apps cell height. - val topBottomPadding = - allAppsIconDrawablePaddingPx * if (isVerticalBarLayout) 2 else 1 - allAppsCellHeightPx = (allAppsIconSizePx + allAppsIconDrawablePaddingPx + - Utilities.calculateTextHeight( - allAppsIconTextSizePx - ) + - topBottomPadding * 2) - } + var fullScreenProfile: DeviceProfile? = null + get() = inv.portraitProfile private fun updateAvailableDimensions( dm: DisplayMetrics, @@ -204,10 +136,7 @@ class DeviceProfile( res: Resources, dm: DisplayMetrics ) { - // Workspace - val isVerticalLayout = isVerticalBarLayout - val invIconSizePx = - if (isVerticalLayout) inv.landscapeIconSize else inv.iconSize + val invIconSizePx = inv.iconSize iconSizePx = (Utilities.pxFromDp( invIconSizePx, @@ -217,47 +146,19 @@ class DeviceProfile( inv.iconTextSize, dm ) * scale).toInt() - iconDrawablePaddingPx = (iconDrawablePaddingOriginalPx * scale).toInt() - cellHeightPx = (iconSizePx + iconDrawablePaddingPx + - Utilities.calculateTextHeight( - iconTextSizePx.toFloat() - )) + iconDrawablePaddingPx = + (availableWidthPx - iconSizePx * inv.numColumns) / (inv.numColumns + 1) + cellHeightPx = (iconSizePx + iconDrawablePaddingPx + + Utilities.calculateTextHeight(iconTextSizePx.toFloat())) + val cellYPadding = (cellSize.y - cellHeightPx) / 2 - if (iconDrawablePaddingPx > cellYPadding && !isVerticalLayout && - !isMultiWindowMode - ) { - // Ensures that the label is closer to its corresponding icon. This is not an issue - // with vertical bar layout or multi-window mode since the issue is handled separately - // with their calls to {@link #adjustToHideWorkspaceLabels}. + if (iconDrawablePaddingPx > cellYPadding) { cellHeightPx -= iconDrawablePaddingPx - cellYPadding iconDrawablePaddingPx = cellYPadding } cellWidthPx = iconSizePx + iconDrawablePaddingPx - // All apps - allAppsIconTextSizePx = iconTextSizePx.toFloat() - allAppsIconSizePx = iconSizePx - allAppsIconDrawablePaddingPx = iconDrawablePaddingPx - allAppsCellHeightPx = cellSize.y - if (isVerticalLayout) { // Always hide the Workspace text with vertical bar layout. - adjustToHideWorkspaceLabels() - } - // Hotseat - if (isVerticalLayout) { - hotseatBarSizePx = iconSizePx - } hotseatCellHeightPx = iconSizePx - workspaceSpringLoadShrinkFactor = if (!isVerticalLayout) { - val expectedWorkspaceHeight = (availableHeightPx - hotseatBarSizePx - - verticalDragHandleSizePx - topWorkspacePadding) - val minRequiredHeight = - dropTargetBarSizePx + workspaceSpringLoadedBottomSpace.toFloat() - Math.min( - res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f, - 1 - minRequiredHeight / expectedWorkspaceHeight - ) - } else { - res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f - } + // Folder icon folderIconSizePx = iconSizePx folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2 @@ -327,13 +228,10 @@ class DeviceProfile( updateWorkspacePadding() } - // Since we are only concerned with the overall padding, layout direction does -// not matter. + // Since we are only concerned with the overall padding, layout direction does not matter. val cellSize: Point get() { val result = Point() - // Since we are only concerned with the overall padding, layout direction does -// not matter. val padding = totalWorkspacePadding result.x = calculateCellWidth( @@ -363,70 +261,43 @@ class DeviceProfile( */ private fun updateWorkspacePadding() { val padding = workspacePadding - if (isVerticalBarLayout) { - padding.top = 0 - padding.bottom = edgeMarginPx - padding.left = hotseatBarSidePaddingPx - padding.right = hotseatBarSidePaddingPx - if (isSeascape) { - padding.left += hotseatBarSizePx - padding.right += verticalDragHandleSizePx - } else { - padding.left += verticalDragHandleSizePx - padding.right += hotseatBarSizePx - } - } else { - val paddingBottom = hotseatBarSizePx + verticalDragHandleSizePx - if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing -// between all icons -// The amount of screen space available for left/right padding. - var availablePaddingX = Math.max( - 0, widthPx - (inv.numColumns * cellWidthPx + - (inv.numColumns - 1) * cellWidthPx) - ) - availablePaddingX = Math.min( - availablePaddingX.toFloat(), - widthPx * MAX_HORIZONTAL_PADDING_PERCENT - ).toInt() - val availablePaddingY = Math.max( - 0, heightPx - topWorkspacePadding - paddingBottom - - 2 * inv.numRows * cellHeightPx - hotseatBarTopPaddingPx - - hotseatBarBottomPaddingPx - ) - padding[availablePaddingX / 2, topWorkspacePadding + availablePaddingY / 2, availablePaddingX / 2] = - paddingBottom + availablePaddingY / 2 - } else { // Pad the top and bottom of the workspace with search/hotseat bar sizes - padding[desiredWorkspaceLeftRightMarginPx, topWorkspacePadding, desiredWorkspaceLeftRightMarginPx] = - paddingBottom - } + val paddingBottom = hotseatBarSizePx + if (isTablet) { // Pad the left and right of the workspace to ensure consistent spacing + // between all icons + // The amount of screen space available for left/right padding. + var availablePaddingX = Math.max( + 0, widthPx - (inv.numColumns * cellWidthPx + + (inv.numColumns - 1) * cellWidthPx) + ) + availablePaddingX = Math.min( + availablePaddingX.toFloat(), + widthPx * MAX_HORIZONTAL_PADDING_PERCENT + ).toInt() + val availablePaddingY = Math.max( + 0, heightPx - topWorkspacePadding - paddingBottom - + 2 * inv.numRows * cellHeightPx - hotseatBarTopPaddingPx - + hotseatBarBottomPaddingPx + ) + padding[availablePaddingX / 2, topWorkspacePadding + availablePaddingY / 2, availablePaddingX / 2] = + paddingBottom + availablePaddingY / 2 + } else { // Pad the top and bottom of the workspace with search/hotseat bar sizes + padding[desiredWorkspaceLeftRightMarginPx, topWorkspacePadding, desiredWorkspaceLeftRightMarginPx] = + paddingBottom } } - // We want the edges of the hotseat to line up with the edges of the workspace, but the -// icons in the hotseat are a different size, and so don't line up perfectly. To account -// for this, we pad the left and right of the hotseat with half of the difference of a -// workspace cell vs a hotseat cell. val hotseatLayoutPadding: Rect get() { - if (isVerticalBarLayout) { - if (isSeascape) { - mHotseatPadding[insets.left, insets.top, hotseatBarSidePaddingPx] = - insets.bottom - } else { - mHotseatPadding[hotseatBarSidePaddingPx, insets.top, insets.right] = - insets.bottom - } - } else { // We want the edges of the hotseat to line up with the edges of the workspace, but the -// icons in the hotseat are a different size, and so don't line up perfectly. To account -// for this, we pad the left and right of the hotseat with half of the difference of a -// workspace cell vs a hotseat cell. - val workspaceCellWidth = widthPx.toFloat() / inv.numColumns - val hotseatCellWidth = widthPx.toFloat() / inv.numHotseatIcons - val hotseatAdjustment = - Math.round((workspaceCellWidth - hotseatCellWidth) / 2) - mHotseatPadding[hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx, hotseatBarTopPaddingPx, hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx] = - hotseatBarBottomPaddingPx + insets.bottom + cellLayoutBottomPaddingPx - } + // We want the edges of the hotseat to line up with the edges of the workspace, but the + // icons in the hotseat are a different size, and so don't line up perfectly. To account + // for this, we pad the left and right of the hotseat with half of the difference of a + // workspace cell vs a hotseat cell. + val workspaceCellWidth = widthPx.toFloat() / inv.numColumns + val hotseatCellWidth = widthPx.toFloat() / inv.numHotseatIcons + val hotseatAdjustment = + Math.round((workspaceCellWidth - hotseatCellWidth) / 2) + mHotseatPadding[hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx, hotseatBarTopPaddingPx, hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx] = + hotseatBarBottomPaddingPx + insets.bottom + cellLayoutBottomPaddingPx return mHotseatPadding } // Folders should only appear below the drop target bar and above the hotseat// Folders should only appear right of the drop target bar and left of the hotseat @@ -434,52 +305,12 @@ class DeviceProfile( * @return the bounds for which the open folders should be contained within */ val absoluteOpenFolderBounds: Rect - get() = if (isVerticalBarLayout) { // Folders should only appear right of the drop target bar and left of the hotseat - Rect( - insets.left + dropTargetBarSizePx + edgeMarginPx, - insets.top, - insets.left + availableWidthPx - hotseatBarSizePx - edgeMarginPx, - insets.top + availableHeightPx - ) - } else { // Folders should only appear below the drop target bar and above the hotseat - Rect( - insets.left + edgeMarginPx, - insets.top + dropTargetBarSizePx + edgeMarginPx, - insets.left + availableWidthPx - edgeMarginPx, - insets.top + availableHeightPx - hotseatBarSizePx - - verticalDragHandleSizePx - edgeMarginPx - ) - } - - /** - * When `true`, the device is in landscape mode and the hotseat is on the right column. - * When `false`, either device is in portrait mode or the device is in landscape mode and - * the hotseat is on the bottom row. - */ - val isVerticalBarLayout: Boolean - get() = isLandscape && transposeLayoutWithOrientation - - /** - * Updates orientation information and returns true if it has changed from the previous value. - */ - fun updateIsSeascape(wm: WindowManager): Boolean { - if (isVerticalBarLayout) { - val isSeascape = - wm.defaultDisplay.rotation == Surface.ROTATION_270 - if (mIsSeascape != isSeascape) { - mIsSeascape = isSeascape - return true - } - } - return false - } - - val isSeascape: Boolean - get() = isVerticalBarLayout && mIsSeascape - - fun shouldFadeAdjacentWorkspaceScreens(): Boolean { - return isVerticalBarLayout || isLargeTablet - } + get() = Rect( + insets.left + edgeMarginPx, + insets.top + dropTargetBarSizePx + edgeMarginPx, + insets.left + availableWidthPx - edgeMarginPx, + insets.top + availableHeightPx - hotseatBarSizePx - -edgeMarginPx + ) fun getCellHeight(containerType: Long): Int { return when (containerType) { @@ -533,8 +364,6 @@ class DeviceProfile( init { var context = context this.inv = inv - this.isLandscape = isLandscape - this.isMultiWindowMode = isMultiWindowMode var res = context.resources val dm = res.displayMetrics // Constants from resources @@ -548,8 +377,7 @@ class DeviceProfile( res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation) context = getContext( - context, - if (isVerticalBarLayout) Configuration.ORIENTATION_LANDSCAPE else Configuration.ORIENTATION_PORTRAIT + context, Configuration.ORIENTATION_PORTRAIT ) res = context.resources val cn = ComponentName( @@ -559,14 +387,11 @@ class DeviceProfile( defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null) edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin) - desiredWorkspaceLeftRightMarginPx = if (isVerticalBarLayout) 0 else edgeMarginPx + desiredWorkspaceLeftRightMarginPx = edgeMarginPx cellLayoutPaddingLeftRightPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding) cellLayoutBottomPaddingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_bottom_padding) - verticalDragHandleSizePx = res.getDimensionPixelSize( - R.dimen.vertical_drag_handle_size - ) defaultPageSpacingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing) topWorkspacePadding = @@ -575,8 +400,6 @@ class DeviceProfile( res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding) dropTargetBarSizePx = res.getDimensionPixelSize(R.dimen.dynamic_grid_drop_target_size) - workspaceSpringLoadedBottomSpace = - res.getDimensionPixelSize(R.dimen.dynamic_grid_min_spring_loaded_space) workspaceCellPaddingXPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_x) hotseatBarTopPaddingPx = @@ -586,21 +409,12 @@ class DeviceProfile( hotseatBarSidePaddingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding) hotseatBarSizePx = - if (isVerticalBarLayout) Utilities.pxFromDp( - inv.iconSize, - dm - ) else res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_size) - +hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_size) + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx // Determine sizes. widthPx = width heightPx = height - if (isLandscape) { - availableWidthPx = maxSize.x - availableHeightPx = minSize.y - } else { - availableWidthPx = minSize.x - availableHeightPx = maxSize.y - } + availableWidthPx = minSize.x + availableHeightPx = maxSize.y // Calculate all of the remaining variables. updateAvailableDimensions(dm, res) // Now that we have all of the variables calculated, we can tune certain sizes. @@ -610,12 +424,13 @@ class DeviceProfile( heightPx ) val isTallDevice = aspectRatio.compareTo(TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0 - if (!isVerticalBarLayout && isPhone && isTallDevice) { // We increase the hotseat size when there is extra space. -// ie. For a display with a large aspect ratio, we can keep the icons on the workspace -// in portrait mode closer together by adding more height to the hotseat. -// Note: This calculation was created after noticing a pattern in the design spec. + if (isPhone && isTallDevice) { + // We increase the hotseat size when there is extra space. + // ie. For a display with a large aspect ratio, we can keep the icons on the workspace + // in portrait mode closer together by adding more height to the hotseat. + // Note: This calculation was created after noticing a pattern in the design spec. val extraSpace = cellSize.y - iconSizePx - iconDrawablePaddingPx - hotseatBarSizePx += extraSpace - verticalDragHandleSizePx + hotseatBarSizePx += extraSpace // Recalculate the available dimensions using the new hotseat size. updateAvailableDimensions(dm, res) } diff --git a/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt b/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt index e563c06e5b..b265b4da2c 100644 --- a/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/InvariantDeviceProfile.kt @@ -122,11 +122,11 @@ open class InvariantDeviceProfile { val largeSide = Math.max(realSize.x, realSize.y) landscapeProfile = DeviceProfile( context, this, smallestSize, largestSize, - largeSide, smallSide, true, false + largeSide, smallSide ) portraitProfile = DeviceProfile( context, this, smallestSize, largestSize, - smallSide, largeSide, false, false + smallSide, largeSide ) // We need to ensure that there is enough extra space in the wallpaper // for the intended parallax effects diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/AdaptiveIconCompat.java b/common/src/main/java/foundation/e/blisslauncher/common/compat/AdaptiveIconCompat.java new file mode 100755 index 0000000000..8c7b00de29 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/AdaptiveIconCompat.java @@ -0,0 +1,1096 @@ +package foundation.e.blisslauncher.common.compat; + +import android.annotation.SuppressLint; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff.Mode; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Shader; +import android.graphics.Shader.TileMode; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import androidx.core.graphics.PathParser; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + *

This class can also be created via XML inflation using <adaptive-icon> tag + * in addition to dynamic creation. + * + *

This drawable supports two drawable layers: foreground and background. The layers are clipped + * when rendering using the mask defined in the device configuration. + * + *

    + *
  • Both foreground and background layers should be sized at 108 x 108 dp.
  • + *
  • The inner 72 x 72 dp of the icon appears within the masked viewport.
  • + *
  • The outer 18 dp on each of the 4 sides of the layers is reserved for use by the system UI + * surfaces to create interesting visual effects, such as parallax or pulsing.
  • + *
+ *

+ * Such motion effect is achieved by internally setting the bounds of the foreground and + * background layer as following: + *

+ * Rect(getBounds().left - getBounds().getWidth() * #getExtraInsetFraction(),
+ *      getBounds().top - getBounds().getHeight() * #getExtraInsetFraction(),
+ *      getBounds().right + getBounds().getWidth() * #getExtraInsetFraction(),
+ *      getBounds().bottom + getBounds().getHeight() * #getExtraInsetFraction())
+ * 
+ */ +public class AdaptiveIconCompat extends Drawable implements Drawable.Callback { + + private static final String path = "M142,180H38c-21,0 -38,-17 -38,-38V38C0,17 17,0 38,0h104c21,0 38,17 38,38v104C180,163 163,180 142,180z"; + + /** + * Mask path is defined inside device configuration in following dimension: [100 x 100] + */ + public static float MASK_SIZE = 180f; + + /** + * Launcher icons design guideline + */ + private static final float SAFEZONE_SCALE = 66f / 72f; + + /** + * All four sides of the layers are padded with extra inset so as to provide + * extra content to reveal within the clip path when performing affine transformations on the + * layers. + *

+ * Each layers will reserve 25% of it's width and height. + *

+ * As a result, the view port of the layers is smaller than their intrinsic width and height. + */ + private static final float EXTRA_INSET_PERCENTAGE = 1 / 4f; + private static final float DEFAULT_VIEW_PORT_SCALE = 1f / (1 + 2 * EXTRA_INSET_PERCENTAGE); + + /** + * Clip path defined in R.string.config_icon_mask. + */ + private static Path sMask; + + /** + * Scaled mask based on the view bounds. + */ + private final Path mMask; + private final Matrix mMaskMatrix; + private final Region mTransparentRegion; + + private Bitmap mMaskBitmap; + + private static final int BACKGROUND_ID = 0; + private static final int FOREGROUND_ID = 1; + + /** + * State variable that maintains the {@link ChildDrawable} array. + */ + LayerState mLayerState; + + private Shader mLayersShader; + private Bitmap mLayersBitmap; + + private final Rect mTmpOutRect = new Rect(); + private Rect mHotspotBounds; + private boolean mMutated; + + private boolean mSuspendChildInvalidation; + private boolean mChildRequestedInvalidation; + private final Canvas mCanvas; + private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | + Paint.FILTER_BITMAP_FLAG); + + private Method methodCreatePathFromPathData; + private Method methodExtractThemeAttrs; + + private boolean mUseMyUglyWorkaround = true; + + private static final String TAG = "AdaptiveIconDrawable"; + + /** + * Constructor used for xml inflation. + */ + public AdaptiveIconCompat() { + this((LayerState) null, null); + } + + /** + * The one constructor to rule them all. This is called by all public + * constructors to set the state and initialize local properties. + */ + AdaptiveIconCompat(LayerState state, Resources res) { + initReflections(); + + mLayerState = createConstantState(state, res); + + if (sMask == null) { + sMask = PathParser.createPathFromPathData(getMaskPath()); + } + mMask = PathParser.createPathFromPathData(getMaskPath()); + //mMask = DeviceProfile.path; + mMaskMatrix = new Matrix(); + mCanvas = new Canvas(); + mTransparentRegion = new Region(); + } + + @SuppressLint("PrivateApi") + private void initReflections() { + try { + Class pathParser = getClass().getClassLoader().loadClass("android.util.PathParser"); + methodCreatePathFromPathData = pathParser.getDeclaredMethod("createPathFromPathData", + String.class); + methodExtractThemeAttrs = TypedArray.class.getDeclaredMethod("extractThemeAttrs"); + } catch (ClassNotFoundException | NoSuchMethodException e) { + e.printStackTrace(); + } + } + + private int getInt(Field field, Object obj) { + try { + return field.getInt(obj); + } catch (IllegalAccessException e) { + return 0; + } + } + + private T invoke(Method method, Object obj, Object... params) { + try { + return (T) method.invoke(obj, params); + } catch (IllegalAccessException | InvocationTargetException e) { + return null; + } + } + + private String getMaskPath() { + return path; + } + + private ChildDrawable createChildDrawable(Drawable drawable) { + final ChildDrawable layer = new ChildDrawable(mLayerState.mDensity); + layer.mDrawable = drawable; + layer.mDrawable.setCallback(this); + mLayerState.mChildrenChangingConfigurations |= + layer.mDrawable.getChangingConfigurations(); + return layer; + } + + LayerState createConstantState(LayerState state, Resources res) { + return new LayerState(state, this, res); + } + + /** + * Constructor used to dynamically create this drawable. + * + * @param backgroundDrawable drawable that should be rendered in the background + * @param foregroundDrawable drawable that should be rendered in the foreground + */ + public AdaptiveIconCompat(Drawable backgroundDrawable, + Drawable foregroundDrawable) { + this(backgroundDrawable, foregroundDrawable, true); + } + + public AdaptiveIconCompat(Drawable backgroundDrawable, + Drawable foregroundDrawable, boolean useMyUglyWorkaround) { + this((LayerState) null, null); + if (backgroundDrawable != null) { + addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable)); + } + if (foregroundDrawable != null) { + addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable)); + } + mUseMyUglyWorkaround = useMyUglyWorkaround; + } + + /** + * Sets the layer to the {@param index} and invalidates cache. + * + * @param index The index of the layer. + * @param layer The layer to add. + */ + private void addLayer(int index, ChildDrawable layer) { + mLayerState.mChildren[index] = layer; + mLayerState.invalidateCache(); + } + + @Override + public void inflate(Resources r, XmlPullParser parser, + AttributeSet attrs, Theme theme) + throws XmlPullParserException, IOException { + super.inflate(r, parser, attrs, theme); + + final LayerState state = mLayerState; + if (state == null) { + return; + } + + // The density may have changed since the last update. This will + // apply scaling to any existing constant state properties. + final int deviceDensity = resolveDensity(r, 0); + //state.setDensity(deviceDensity); + + final ChildDrawable[] array = state.mChildren; + for (int i = 0; i < state.mChildren.length; i++) { + final ChildDrawable layer = array[i]; + //layer.setDensity(deviceDensity); + } + + inflateLayers(r, parser, attrs, theme); + } + + static int resolveDensity(Resources r, int parentDensity) { + final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi; + return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; + } + + /** + * All four sides of the layers are padded with extra inset so as to provide + * extra content to reveal within the clip path when performing affine transformations on the + * layers. + * + * @see #getForeground() and #getBackground() for more info on how this value is used + */ + public static float getExtraInsetFraction() { + return EXTRA_INSET_PERCENTAGE; + } + + public static float getExtraInsetPercentage() { + return EXTRA_INSET_PERCENTAGE; + } + + /** + * When called before the bound is set, the returned path is identical to + * R.string.config_icon_mask. After the bound is set, the + * returned path's computed bound is same as the #getBounds(). + * + * @return the mask path object used to clip the drawable + */ + public Path getIconMask() { + return mMask; + } + + /** + * Returns the foreground drawable managed by this class. The bound of this drawable is + * extended by {@link #getExtraInsetFraction()} * getBounds().width on left/right sides and by + * {@link #getExtraInsetFraction()} * getBounds().height on top/bottom sides. + * + * @return the foreground drawable managed by this drawable + */ + public Drawable getForeground() { + return mLayerState.mChildren[FOREGROUND_ID].mDrawable; + } + + /** + * Returns the foreground drawable managed by this class. The bound of this drawable is + * extended by {@link #getExtraInsetFraction()} * getBounds().width on left/right sides and by + * {@link #getExtraInsetFraction()} * getBounds().height on top/bottom sides. + * + * @return the background drawable managed by this drawable + */ + public Drawable getBackground() { + return mLayerState.mChildren[BACKGROUND_ID].mDrawable; + } + + @Override + protected void onBoundsChange(Rect bounds) { + if (bounds.isEmpty()) { + return; + } + updateLayerBounds(bounds); + } + + private void updateLayerBounds(Rect bounds) { + try { + suspendChildInvalidation(); + updateLayerBoundsInternal(bounds); + updateMaskBoundsInternal(bounds); + } finally { + resumeChildInvalidation(); + } + } + + /** + * Set the child layer bounds bigger than the view port size by {@link #DEFAULT_VIEW_PORT_SCALE} + */ + private void updateLayerBoundsInternal(Rect bounds) { + int cX = bounds.width() / 2; + int cY = bounds.height() / 2; + + for (int i = 0, count = LayerState.N_CHILDREN; i < count; i++) { + final ChildDrawable r = mLayerState.mChildren[i]; + if (r == null) { + continue; + } + final Drawable d = r.mDrawable; + if (d == null) { + continue; + } + + int insetWidth = (int) (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2)); + int insetHeight = (int) (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2)); + final Rect outRect = mTmpOutRect; + outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight); + + d.setBounds(outRect); + } + } + + private void updateMaskBoundsInternal(Rect b) { + mMaskMatrix.setScale(b.width() / MASK_SIZE, b.height() / MASK_SIZE); + sMask.transform(mMaskMatrix, mMask); + + if (mMaskBitmap == null || mMaskBitmap.getWidth() != b.width() || + mMaskBitmap.getHeight() != b.height()) { + mMaskBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ALPHA_8); + mLayersBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888); + } + // mMaskBitmap bound [0, w] x [0, h] + mCanvas.setBitmap(mMaskBitmap); + mPaint.setShader(null); + mPaint.setColor(0xFFFFFFFF); + mCanvas.drawPath(mMask, mPaint); + + // mMask bound [left, top, right, bottom] + mMaskMatrix.postTranslate(b.left, b.top); + mMask.reset(); + sMask.transform(mMaskMatrix, mMask); + // reset everything that depends on the view bounds + mTransparentRegion.setEmpty(); + mLayersShader = null; + } + + @Override + public void draw(Canvas canvas) { + if (mLayersBitmap == null) { + return; + } + if (mLayersShader == null) { + mCanvas.setBitmap(mLayersBitmap); + mCanvas.drawColor(Color.BLACK); + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + if (mLayerState.mChildren[i] == null) { + continue; + } + final Drawable dr = mLayerState.mChildren[i].mDrawable; + if (dr != null) { + dr.draw(mCanvas); + } + } + mLayersShader = new BitmapShader(mLayersBitmap, TileMode.CLAMP, TileMode.CLAMP); + if (mUseMyUglyWorkaround) { + // TODO: remove this ugly and slow code + if (mMaskBitmap != null) { + int width = mLayersBitmap.getWidth(); + int height = mLayersBitmap.getHeight(); + int[] colors = new int[width * height]; + int[] alphas = new int[width * height]; + mLayersBitmap.getPixels(colors, 0, width, 0, 0, width, height); + mMaskBitmap.getPixels(alphas, 0, width, 0, 0, width, height); + int color, alpha, index; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + index = i * height + j; + color = colors[index]; + alpha = alphas[index]; + colors[index] = color & 0x00FFFFFF | alpha & 0xFF000000; + } + } + mLayersBitmap.setPixels(colors, 0, width, 0, 0, width, height); + } + } else { + mPaint.setShader(mLayersShader); + } + } + if (mMaskBitmap != null) { + Rect bounds = getBounds(); + canvas.drawBitmap(mUseMyUglyWorkaround ? mLayersBitmap : mMaskBitmap, bounds.left, + bounds.top, mPaint); + } + } + + @Override + public void invalidateSelf() { + mLayersShader = null; + super.invalidateSelf(); + } + + @Override + public void getOutline(Outline outline) { + outline.setConvexPath(mMask); + } + + public Region getSafeZone() { + mMaskMatrix.reset(); + mMaskMatrix.setScale(SAFEZONE_SCALE, SAFEZONE_SCALE, getBounds().centerX(), + getBounds().centerY()); + Path p = new Path(); + mMask.transform(mMaskMatrix, p); + Region safezoneRegion = new Region(getBounds()); + safezoneRegion.setPath(p, safezoneRegion); + return safezoneRegion; + } + + @Override + public Region getTransparentRegion() { + if (mTransparentRegion.isEmpty()) { + mMask.toggleInverseFillType(); + mTransparentRegion.set(getBounds()); + mTransparentRegion.setPath(mMask, mTransparentRegion); + mMask.toggleInverseFillType(); + } + return mTransparentRegion; + } + + /** + * Inflates child layers using the specified parser. + */ + private void inflateLayers(Resources r, XmlPullParser parser, + AttributeSet attrs, Theme theme) + throws XmlPullParserException, IOException { + final LayerState state = mLayerState; + + final int innerDepth = parser.getDepth() + 1; + int type; + int depth; + int childIndex; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + if (depth > innerDepth) { + continue; + } + String tagName = parser.getName(); + switch (tagName) { + case "background": + childIndex = BACKGROUND_ID; + break; + case "foreground": + childIndex = FOREGROUND_ID; + break; + default: + continue; + } + + final ChildDrawable layer = new ChildDrawable(state.mDensity); + final TypedArray a = obtainAttributes(r, theme, attrs, + new int[]{android.R.attr.drawable}); + updateLayerFromTypedArray(layer, a); + a.recycle(); + + // If the layer doesn't have a drawable or unresolved theme + // attribute for a drawable, attempt to parse one from the child + // element. If multiple child elements exist, we'll only use the + // first one. + if (layer.mDrawable == null && (layer.mThemeAttrs == null)) { + while ((type = parser.next()) == XmlPullParser.TEXT) { + } + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException(parser.getPositionDescription() + + ": or tag requires a 'drawable'" + + "attribute or child tag defining a drawable"); + } + + // We found a child drawable. Take ownership. + layer.mDrawable = Drawable.createFromXmlInner(r, parser, attrs, theme); + layer.mDrawable.setCallback(this); + state.mChildrenChangingConfigurations |= + layer.mDrawable.getChangingConfigurations(); + } + addLayer(childIndex, layer); + } + } + + private void updateLayerFromTypedArray(ChildDrawable layer, TypedArray a) { + final LayerState state = mLayerState; + + // Account for any configuration changes. + state.mChildrenChangingConfigurations |= a.getChangingConfigurations(); + + // Extract the theme attributes, if any. + layer.mThemeAttrs = invoke(methodExtractThemeAttrs, a); + + @SuppressLint("ResourceType") Drawable dr = getDrawable(a, 0); + if (dr != null) { + if (layer.mDrawable != null) { + // It's possible that a drawable was already set, in which case + // we should clear the callback. We may have also integrated the + // drawable's changing configurations, but we don't have enough + // information to revert that change. + layer.mDrawable.setCallback(null); + } + + // Take ownership of the new drawable. + layer.mDrawable = dr; + layer.mDrawable.setCallback(this); + state.mChildrenChangingConfigurations |= + layer.mDrawable.getChangingConfigurations(); + } + } + + private Drawable getDrawable(TypedArray a, int index) { + final TypedValue value = new TypedValue(); + a.getValue(index, value); + if (value.resourceId != 0) { + return a.getResources().getDrawableForDensity(value.resourceId, DisplayMetrics.DENSITY_DEFAULT, null); + } + return null; + } + + @Override + public boolean canApplyTheme() { + return (mLayerState != null && mLayerState.canApplyTheme()) || super.canApplyTheme(); + } + + /** + * Temporarily suspends child invalidation. + * + * @see #resumeChildInvalidation() + */ + private void suspendChildInvalidation() { + mSuspendChildInvalidation = true; + } + + /** + * Resumes child invalidation after suspension, immediately performing an + * invalidation if one was requested by a child during suspension. + * + * @see #suspendChildInvalidation() + */ + private void resumeChildInvalidation() { + mSuspendChildInvalidation = false; + + if (mChildRequestedInvalidation) { + mChildRequestedInvalidation = false; + invalidateSelf(); + } + } + + @Override + public void invalidateDrawable(Drawable who) { + if (mSuspendChildInvalidation) { + mChildRequestedInvalidation = true; + } else { + invalidateSelf(); + } + } + + @Override + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + + @Override + public int getChangingConfigurations() { + return super.getChangingConfigurations() | mLayerState.getChangingConfigurations(); + } + + @Override + public void setHotspot(float x, float y) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setHotspot(x, y); + } + } + } + + @Override + public void setHotspotBounds(int left, int top, int right, int bottom) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setHotspotBounds(left, top, right, bottom); + } + } + + if (mHotspotBounds == null) { + mHotspotBounds = new Rect(left, top, right, bottom); + } else { + mHotspotBounds.set(left, top, right, bottom); + } + } + + @Override + public void getHotspotBounds(Rect outRect) { + if (mHotspotBounds != null) { + outRect.set(mHotspotBounds); + } else { + super.getHotspotBounds(outRect); + } + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + final boolean changed = super.setVisible(visible, restart); + final ChildDrawable[] array = mLayerState.mChildren; + + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setVisible(visible, restart); + } + } + + return changed; + } + + @Override + public void setDither(boolean dither) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setDither(dither); + } + } + } + + @Override + public void setAlpha(int alpha) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setAlpha(alpha); + } + } + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setColorFilter(colorFilter); + } + } + } + + @Override + public void setTintList(ColorStateList tint) { + final ChildDrawable[] array = mLayerState.mChildren; + final int N = LayerState.N_CHILDREN; + for (int i = 0; i < N; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setTintList(tint); + } + } + } + + @Override + public void setTintMode(Mode tintMode) { + final ChildDrawable[] array = mLayerState.mChildren; + final int N = LayerState.N_CHILDREN; + for (int i = 0; i < N; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setTintMode(tintMode); + } + } + } + + public void setOpacity(int opacity) { + mLayerState.mOpacityOverride = opacity; + } + + @Override + public int getOpacity() { + if (mLayerState.mOpacityOverride != PixelFormat.UNKNOWN) { + return mLayerState.mOpacityOverride; + } + return mLayerState.getOpacity(); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + mLayerState.mAutoMirrored = mirrored; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setAutoMirrored(mirrored); + } + } + } + + @Override + public boolean isAutoMirrored() { + return mLayerState.mAutoMirrored; + } + + @Override + public void jumpToCurrentState() { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.jumpToCurrentState(); + } + } + } + + @Override + public boolean isStateful() { + return mLayerState.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + boolean changed = false; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.isStateful() && dr.setState(state)) { + changed = true; + } + } + + if (changed) { + updateLayerBounds(getBounds()); + } + + return changed; + } + + @Override + protected boolean onLevelChange(int level) { + boolean changed = false; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.setLevel(level)) { + changed = true; + } + } + + if (changed) { + updateLayerBounds(getBounds()); + } + + return changed; + } + + @Override + public int getIntrinsicWidth() { + return (int) (getMaxIntrinsicWidth() * DEFAULT_VIEW_PORT_SCALE); + } + + private int getMaxIntrinsicWidth() { + int width = -1; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final ChildDrawable r = mLayerState.mChildren[i]; + if (r.mDrawable == null) { + continue; + } + final int w = r.mDrawable.getIntrinsicWidth(); + if (w > width) { + width = w; + } + } + return width; + } + + @Override + public int getIntrinsicHeight() { + return (int) (getMaxIntrinsicHeight() * DEFAULT_VIEW_PORT_SCALE); + } + + private int getMaxIntrinsicHeight() { + int height = -1; + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final ChildDrawable r = mLayerState.mChildren[i]; + if (r.mDrawable == null) { + continue; + } + final int h = r.mDrawable.getIntrinsicHeight(); + if (h > height) { + height = h; + } + } + return height; + } + + @Override + public ConstantState getConstantState() { + if (mLayerState.canConstantState()) { + mLayerState.mChangingConfigurations = getChangingConfigurations(); + return mLayerState; + } + return null; + } + + + @Override + public Drawable mutate() { + if (!mMutated && super.mutate() == this) { + mLayerState = createConstantState(mLayerState, null); + for (int i = 0; i < LayerState.N_CHILDREN; i++) { + final Drawable dr = mLayerState.mChildren[i].mDrawable; + if (dr != null) { + dr.mutate(); + } + } + mMutated = true; + } + return this; + } + + protected static TypedArray obtainAttributes(Resources res, + Theme theme, AttributeSet set, int[] attrs) { + if (theme == null) { + return res.obtainAttributes(set, attrs); + } + return theme.obtainStyledAttributes(set, attrs, 0, 0); + } + + static class ChildDrawable { + public Drawable mDrawable; + public int[] mThemeAttrs; + public int mDensity; + + ChildDrawable(int density) { + mDensity = density; + } + + ChildDrawable(ChildDrawable orig, AdaptiveIconCompat owner, + Resources res) { + + final Drawable dr = orig.mDrawable; + final Drawable clone; + if (dr != null) { + final ConstantState cs = dr.getConstantState(); + if (cs == null) { + clone = dr; + } else if (res != null) { + clone = cs.newDrawable(res); + } else { + clone = cs.newDrawable(); + } + clone.setCallback(owner); + clone.setBounds(dr.getBounds()); + clone.setLevel(dr.getLevel()); + } else { + clone = null; + } + + mDrawable = clone; + mThemeAttrs = orig.mThemeAttrs; + + mDensity = resolveDensity(res, orig.mDensity); + } + + public boolean canApplyTheme() { + return mThemeAttrs != null + || (mDrawable != null && mDrawable.canApplyTheme()); + } + + public final void setDensity(int targetDensity) { + if (mDensity != targetDensity) { + mDensity = targetDensity; + } + } + } + + static class LayerState extends ConstantState { + private int[] mThemeAttrs; + + final static int N_CHILDREN = 2; + ChildDrawable[] mChildren; + + // The density at which to render the drawable and its children. + int mDensity; + + // The density to use when inflating/looking up the children drawables. A value of 0 means + // use the system's density. + int mSrcDensityOverride = 0; + + int mOpacityOverride = PixelFormat.UNKNOWN; + + int mChangingConfigurations; + int mChildrenChangingConfigurations; + + private boolean mCheckedOpacity; + private int mOpacity; + + private boolean mCheckedStateful; + private boolean mIsStateful; + private boolean mAutoMirrored = false; + + LayerState(LayerState orig, AdaptiveIconCompat owner, + Resources res) { + mDensity = resolveDensity(res, orig != null ? orig.mDensity : 0); + mChildren = new ChildDrawable[N_CHILDREN]; + if (orig != null) { + final ChildDrawable[] origChildDrawable = orig.mChildren; + + mChangingConfigurations = orig.mChangingConfigurations; + mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations; + + for (int i = 0; i < N_CHILDREN; i++) { + final ChildDrawable or = origChildDrawable[i]; + mChildren[i] = new ChildDrawable(or, owner, res); + } + + mCheckedOpacity = orig.mCheckedOpacity; + mOpacity = orig.mOpacity; + mCheckedStateful = orig.mCheckedStateful; + mIsStateful = orig.mIsStateful; + mAutoMirrored = orig.mAutoMirrored; + mThemeAttrs = orig.mThemeAttrs; + mOpacityOverride = orig.mOpacityOverride; + mSrcDensityOverride = orig.mSrcDensityOverride; + } else { + for (int i = 0; i < N_CHILDREN; i++) { + mChildren[i] = new ChildDrawable(mDensity); + } + } + } + + public final void setDensity(int targetDensity) { + if (mDensity != targetDensity) { + mDensity = targetDensity; + } + } + + @Override + public boolean canApplyTheme() { + if (mThemeAttrs != null || super.canApplyTheme()) { + return true; + } + + final ChildDrawable[] array = mChildren; + for (int i = 0; i < N_CHILDREN; i++) { + final ChildDrawable layer = array[i]; + if (layer.canApplyTheme()) { + return true; + } + } + return false; + } + + + @Override + public Drawable newDrawable() { + return new AdaptiveIconCompat(this, null); + } + + + @Override + public Drawable newDrawable(Resources res) { + return new AdaptiveIconCompat(this, null); + } + + @Override + public int getChangingConfigurations() { + return mChangingConfigurations + | mChildrenChangingConfigurations; + } + + public final int getOpacity() { + if (mCheckedOpacity) { + return mOpacity; + } + + final ChildDrawable[] array = mChildren; + + // Seek to the first non-null drawable. + int firstIndex = -1; + for (int i = 0; i < N_CHILDREN; i++) { + if (array[i].mDrawable != null) { + firstIndex = i; + break; + } + } + + int op; + if (firstIndex >= 0) { + op = array[firstIndex].mDrawable.getOpacity(); + } else { + op = PixelFormat.TRANSPARENT; + } + + // Merge all remaining non-null drawables. + for (int i = firstIndex + 1; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + op = Drawable.resolveOpacity(op, dr.getOpacity()); + } + } + + mOpacity = op; + mCheckedOpacity = true; + return op; + } + + public final boolean isStateful() { + if (mCheckedStateful) { + return mIsStateful; + } + + final ChildDrawable[] array = mChildren; + boolean isStateful = false; + for (int i = 0; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.isStateful()) { + isStateful = true; + break; + } + } + + mIsStateful = isStateful; + mCheckedStateful = true; + return isStateful; + } + + public final boolean canConstantState() { + final ChildDrawable[] array = mChildren; + for (int i = 0; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.getConstantState() == null) { + return false; + } + } + + // Don't cache the result, this method is not called very often. + return true; + } + + public void invalidateCache() { + mCheckedOpacity = false; + mCheckedStateful = false; + } + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt index e2553548fa..3b34c4d7a7 100644 --- a/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/compat/ShortcutInfoCompat.kt @@ -29,7 +29,7 @@ class ShortcutInfoCompat(private val shortcutInfo: ShortcutInfo) { //@RequiresApi(Build.VERSION_CODES.N_MR1) fun getBadgePackage(context: Context): String? { val whitelistedPkg = "" - return if (whitelistedPkg == getPackage() && shortcutInfo.getExtras().containsKey( + return if (whitelistedPkg == getPackage() && shortcutInfo.extras.containsKey( EXTRA_BADGEPKG ) ) { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt index fea8012556..f522029ebe 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/WorkspaceRepositoryImpl.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.LauncherActivityInfo +import android.os.Build import android.os.Process import android.os.UserHandle import android.util.LongSparseArray @@ -15,6 +16,7 @@ import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat import foundation.e.blisslauncher.common.util.LabelComparator import foundation.e.blisslauncher.common.util.MultiHashMap import foundation.e.blisslauncher.data.database.roomentity.WorkspaceItem +import foundation.e.blisslauncher.data.icon.LauncherIcons import foundation.e.blisslauncher.data.parser.DefaultHotseatParser import foundation.e.blisslauncher.data.shortcuts.PinnedShortcutManager import foundation.e.blisslauncher.data.util.LauncherItemComparator @@ -43,7 +45,8 @@ class WorkspaceRepositoryImpl private val shortcutManager: PinnedShortcutManager, private val sharedPrefs: SharedPreferences, private val idp: InvariantDeviceProfile, - private val launcherItemComparator: LauncherItemComparator + private val launcherItemComparator: LauncherItemComparator, + private val launcherIcons: LauncherIcons ) : WorkspaceRepository { override fun loadWorkspace(): WorkspaceModel { @@ -371,7 +374,6 @@ class WorkspaceRepositoryImpl .commit() } - private fun isApplicationAlreadyAdded( existingWorkspaceItems: List, componentName: ComponentName @@ -640,8 +642,16 @@ class WorkspaceRepositoryImpl return if (lai != null) { val applicationItem = ApplicationItem(lai, user, quietMode) - .apply { itemType = LauncherConstants.ItemType.APPLICATION } + .apply { + itemType = LauncherConstants.ItemType.APPLICATION + iconBitmap = launcherIcons.createBadgedIconBitmap( + lai.getBadgedIcon(0), + user, + Build.VERSION.SDK_INT + ) + } val isSuspended = packageManagerHelper.isAppSuspended(lai.applicationInfo) + Timber.d("$applicationItem is $isSuspended") if (isSuspended) { applicationItem.runtimeStatusFlags = applicationItem.runtimeStatusFlags or LauncherItemWithIcon.FLAG_DISABLED_SUSPENDED @@ -654,6 +664,14 @@ class WorkspaceRepositoryImpl this.intent = newIntent this.componentName = componentName this.title = title + iconBitmap = launcherIcons.createBadgedIconBitmap( + context.packageManager.getPackageInfo( + this.componentName.packageName, + 0 + ).applicationInfo.loadIcon(context.packageManager), + user, + Build.VERSION.SDK_INT + ) } if (applicationItem.title == null) { diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt index 9a0a9506ef..e1c075afa6 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt @@ -11,11 +11,14 @@ interface IconDao { @Query("DELETE FROM icons WHERE componentName LIKE :componentName AND profileId = :userSerial") fun delete(componentName: String, userSerial: Int) + @Query("DELETE FROM icons WHERE componentName in (:components)") + fun delete(components: List) + @Query("DELETE FROM icons") fun clear() @Query("SELECT * FROM icons WHERE profileId = :userSerial") - fun query(userSerial: Int): IconEntity + fun query(userSerial: Long): List @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrReplace(iconEntity: IconEntity) diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt index 4ace013058..3225d7613b 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/roomentity/IconEntity.kt @@ -7,16 +7,13 @@ import androidx.room.Entity data class IconEntity( @ColumnInfo(name = "componentName", typeAffinity = ColumnInfo.TEXT) val componentName: String, - @ColumnInfo(name = "profileId", typeAffinity = ColumnInfo.INTEGER) - val profileId: Int, - @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) - val lastUpdated: Int = 0, + @ColumnInfo(name = "profileId") + val profileId: Long, + val lastUpdated: Long = 0, @ColumnInfo(typeAffinity = ColumnInfo.INTEGER) val version: Int = 0, @ColumnInfo(typeAffinity = ColumnInfo.BLOB) val icon: ByteArray, - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - val iconLowRes: ByteArray, @ColumnInfo(typeAffinity = ColumnInfo.TEXT) val label: String, @ColumnInfo(typeAffinity = ColumnInfo.TEXT) @@ -33,7 +30,6 @@ data class IconEntity( if (lastUpdated != other.lastUpdated) return false if (version != other.version) return false if (!icon.contentEquals(other.icon)) return false - if (!iconLowRes.contentEquals(other.iconLowRes)) return false if (label != other.label) return false if (systemState != other.systemState) return false @@ -42,11 +38,10 @@ data class IconEntity( override fun hashCode(): Int { var result = componentName.hashCode() - result = 31 * result + profileId - result = 31 * result + lastUpdated + result = 31 * result + profileId.hashCode() + result = 31 * result + lastUpdated.hashCode() result = 31 * result + version result = 31 * result + icon.contentHashCode() - result = 31 * result + iconLowRes.contentHashCode() result = 31 * result + label.hashCode() result = 31 * result + systemState.hashCode() return result diff --git a/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapInfo.kt b/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapInfo.kt deleted file mode 100644 index 335539f772..0000000000 --- a/data/src/main/java/foundation/e/blisslauncher/data/graphics/BitmapInfo.kt +++ /dev/null @@ -1,28 +0,0 @@ -package foundation.e.blisslauncher.data.graphics - -import android.graphics.Bitmap -import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon - -open class BitmapInfo { - - var icon: Bitmap? = null - var color = 0 - fun applyTo(info: LauncherItemWithIcon) { - info.iconBitmap = icon - info.iconColor = color - } - - fun applyTo(info: BitmapInfo) { - info.icon = icon - info.color = color - } - - companion object { - fun fromBitmap(bitmap: Bitmap?): BitmapInfo { - val info = BitmapInfo() - info.icon = bitmap - //info.color = ColorExtractor.findDominantColorByHue(bitmap) - return info - } - } -} \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt index b205b32da4..621e4d6dc6 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt @@ -5,6 +5,7 @@ import android.annotation.SuppressLint import android.content.ComponentName import android.content.Context import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo import android.content.pm.LauncherActivityInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager @@ -12,17 +13,20 @@ import android.content.res.Resources import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Process import android.os.UserHandle import android.util.Log +import foundation.e.blisslauncher.common.BitmapRenderer +import foundation.e.blisslauncher.common.InvariantDeviceProfile import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat -import foundation.e.blisslauncher.domain.repository.UserManagerRepository -import foundation.e.blisslauncher.common.InvariantDeviceProfile import foundation.e.blisslauncher.data.database.dao.IconDao -import foundation.e.blisslauncher.data.graphics.BitmapInfo -import foundation.e.blisslauncher.data.graphics.BitmapRenderer +import foundation.e.blisslauncher.data.database.roomentity.IconEntity import foundation.e.blisslauncher.domain.keys.ComponentKey +import foundation.e.blisslauncher.domain.repository.UserManagerRepository import java.util.HashSet +import java.util.Stack import javax.inject.Inject import javax.inject.Singleton @@ -33,16 +37,17 @@ class IconCache @Inject constructor( private val iconProvider: IconProvider, private val launcherApps: LauncherAppsCompat, private val userManager: UserManagerRepository, - private val iconDao: IconDao + private val iconDao: IconDao, + private val launcherIcons: LauncherIcons ) { - inner class CacheEntry : BitmapInfo() { - var title: CharSequence = "" + data class CacheEntry( + var bitmap: Bitmap, + var title: CharSequence? = "", var contentDescription: CharSequence = "" - var isLowResIcon = false - } + ) - private val mDefaultIcons: HashMap = HashMap() + private val mDefaultIcons: HashMap = HashMap() private val packageManager: PackageManager = context.packageManager private val cache = HashMap() private val iconDpi: Int = inv.fillResIconDpi @@ -87,7 +92,7 @@ class IconCache @Inject constructor( } } - fun getFullResIcon(info: ActivityInfo): Drawable? { + fun getFullResIcon(info: ActivityInfo): Drawable { return try { packageManager.getResourcesForApplication( info.applicationInfo @@ -112,14 +117,11 @@ class IconCache @Inject constructor( return iconProvider.getIcon(info, iconDpi, flattenDrawable) } - //TODO - /*protected fun makeDefaultIcon(user: UserHandle?): BitmapInfo? { - LauncherIcons.obtain(mContext).use({ li -> - return li.createBadgedIconBitmap( - getFullResDefaultActivityIcon(), user, VERSION.SDK_INT - ) - }) - }*/ + protected fun makeDefaultIcon(user: UserHandle?): Bitmap { + return launcherIcons.createBadgedIconBitmap( + getFullResDefaultActivityIcon(), user, Build.VERSION.SDK_INT + ) + } /** * Remove any records for the supplied ComponentName. @@ -161,17 +163,65 @@ class IconCache @Inject constructor( try { val info: PackageInfo = packageManager.getPackageInfo( packageName, - PackageManager.GET_UNINSTALLED_PACKAGES + PackageManager.MATCH_UNINSTALLED_PACKAGES ) val userSerial: Long = userManager.getSerialNumberForUser(user) for (app in launcherApps.getActivityList(packageName, user)) { - //addIconToDBAndMemCache(app, info, userSerial, false /*replace existing*/) + addIconToDBAndMemCache(app, info, userSerial, false /*replace existing*/) } } catch (e: PackageManager.NameNotFoundException) { Log.d(TAG, "Package not found", e) } } + private fun addIconToDBAndMemCache( + app: LauncherActivityInfo, + info: PackageInfo, + userSerial: Long, + replaceExisting: Boolean + ) { + val componentKey = ComponentKey(app.componentName, app.user) + var entry: CacheEntry? = null + if (!replaceExisting) { + entry = cache[componentKey] + if (entry?.bitmap == null) { + entry == null + } + } + + if (entry == null) { + entry = launcherIcons.createBadgedIconBitmap( + getFullResIcon(app), + app.user, + app.applicationInfo.targetSdkVersion + ).let { + CacheEntry(it, app.label, userManager.getBadgedLabelForUser(app.label, app.user)) + } + } + cache.put(componentKey, entry) + addIconToDB(entry, app.componentName, app.applicationInfo.packageName, info, userSerial) + } + + private fun addIconToDB( + entry: CacheEntry, + componentName: ComponentName, + packageName: String, + info: PackageInfo, + userSerial: Long + ) { + val iconEntity = IconEntity( + componentName.flattenToString(), + userSerial, + info.lastUpdateTime, + info.versionCode, + Utilities.flattenBitmap(entry.bitmap), + entry.title.toString(), + iconProvider.getIconSystemState(packageName) + ) + + iconDao.insertOrReplace(iconEntity) + } + /** * Removes the entries related to the given package in memory and persistent DB. */ @@ -200,11 +250,77 @@ class IconCache @Inject constructor( } // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} // is called by the icon cache when the job is complete. - /*updateDBIcons( + updateDBIcons( user, apps, - if (Process.myUserHandle() == user) ignorePackagesForMainUser else emptySet() - )*/ + if (Process.myUserHandle() == user) ignorePackagesForMainUser else emptySet() + ) + } + } + + private fun updateDBIcons( + user: UserHandle, + apps: List, + ignorePackages: Set + ) { + val userSerial = userManager.getSerialNumberForUser(user) + val pm = context.packageManager + val pkgInfoMap = HashMap() + pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES).forEach { + pkgInfoMap[it.packageName] = it + } + + val componentMap = HashMap() + for (app in apps) { + componentMap[app.componentName] = app + } + + val itemsToRemove = HashSet() + val appsToUpdate = Stack() + + iconDao.query(userSerial).forEach { + val cn = it.componentName + val component = ComponentName.unflattenFromString(cn) + val info = pkgInfoMap[component.packageName] + if (info == null) { + if (!ignorePackages.contains(component.packageName)) { + remove(component, user) + itemsToRemove.add(cn) + } + return@forEach + } + + if (info.applicationInfo.flags and ApplicationInfo.FLAG_IS_DATA_ONLY != 0) { + return@forEach + } + + val app = componentMap.remove(component) + if (it.version == info.versionCode && it.lastUpdated == info.lastUpdateTime + && it.systemState == iconProvider.getIconSystemState(info.packageName) + ) { + return@forEach + } + + if (app == null) { + remove(component, user) + itemsToRemove.add(cn) + } else { + appsToUpdate.add(app) + } + } + + if (itemsToRemove.isNotEmpty()) { + iconDao.delete(itemsToRemove.toList()) + } + + if (componentMap.isNotEmpty() || appsToUpdate.isNotEmpty()) { + val appsToAdd = + Stack() + appsToAdd.addAll(componentMap.values) + /*SerializedIconUpdateTask( + userSerial, pkgInfoMap, + appsToAdd, appsToUpdate + ).scheduleNext()*/ } } diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt index ee57d23e11..b6b355bd6b 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconProvider.kt @@ -9,7 +9,7 @@ import javax.inject.Singleton @Singleton class IconProvider @Inject constructor(context: Context) { - private var mSystemState: String? = null + private var mSystemState: String = "" init { updateSystemStateString(context) @@ -20,7 +20,7 @@ class IconProvider @Inject constructor(context: Context) { mSystemState = locale + "," + Build.VERSION.SDK_INT } - fun getIconSystemState(packageName: String?): String? { + fun getIconSystemState(packageName: String): String { return mSystemState } diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt new file mode 100644 index 0000000000..a92e89797c --- /dev/null +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package foundation.e.blisslauncher.data.icon + +import android.content.Context +import android.content.Intent +import android.content.Intent.ShortcutIconResource +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PaintFlagsDrawFilter +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.PaintDrawable +import android.os.Process +import android.os.UserHandle +import foundation.e.blisslauncher.common.BitmapRenderer +import foundation.e.blisslauncher.common.InvariantDeviceProfile +import foundation.e.blisslauncher.common.Utilities +import foundation.e.blisslauncher.common.compat.ShortcutInfoCompat +import foundation.e.blisslauncher.data.R +import foundation.e.blisslauncher.data.shortcuts.PinnedShortcutManager +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.PackageItem +import javax.inject.Inject + +/** + * Helper methods for generating various launcher icons + */ +class LauncherIcons @Inject constructor( + context: Context, + idp: InvariantDeviceProfile, + val pinnedShortcutManager: PinnedShortcutManager +) : AutoCloseable { + + override fun close() { + } + + private val mOldBounds = Rect() + private val mContext: Context = context.applicationContext + private val mCanvas: Canvas + private val mPm: PackageManager + private val mFillResIconDpi: Int + private val mIconBitmapSize: Int + private var mWrapperIcon: Drawable? = null + private var mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND + + // sometimes we store linked lists of these things + private var next: LauncherIcons? = null + + /** + * Returns a bitmap suitable for the all apps view. If the package or the resource do not + * exist, it returns null. + */ + fun createIconBitmap(iconRes: ShortcutIconResource): Bitmap? { + try { + val resources = + mPm.getResourcesForApplication(iconRes.packageName) + if (resources != null) { + val id = resources.getIdentifier(iconRes.resourceName, null, null) + // do not stamp old legacy shortcuts as the app may have already forgotten about it + return createBadgedIconBitmap( + resources.getDrawableForDensity(id, mFillResIconDpi), + Process.myUserHandle() /* only available on primary user */, + 0 /* do not apply legacy treatment */ + ) + } + } catch (e: Exception) { + // Icon not found. + } + return null + } + + /** + * Returns a bitmap which is of the appropriate size to be displayed as an icon + */ + fun createIconBitmap(icon: Bitmap): Bitmap { + return if (mIconBitmapSize == icon.width && mIconBitmapSize == icon.height) { + icon + } else createIconBitmap(BitmapDrawable(mContext.resources, icon)) + } + + /** + * Returns a bitmap suitable for displaying as an icon at various launcher UIs like all apps + * view or workspace. The icon is badged for {@param user}. + * The bitmap is also visually normalized with other icons. + */ + @JvmOverloads + fun createBadgedIconBitmap( + icon: Drawable?, user: UserHandle?, iconAppTargetSdk: Int + ): Bitmap { + var icon = icon + icon = normalizeAndWrapToAdaptiveIcon(icon!!, iconAppTargetSdk, null) + val bitmap = createIconBitmap(icon!!) + if (Utilities.ATLEAST_OREO && icon is AdaptiveIconDrawable) { + mCanvas.setBitmap(bitmap) + mCanvas.setBitmap(null) + } + val result: Bitmap + result = if (user != null && Process.myUserHandle() != user) { + val drawable: BitmapDrawable = FixedSizeBitmapDrawable(bitmap) + val badged = mPm.getUserBadgedIcon(drawable, user) + if (badged is BitmapDrawable) { + badged.bitmap + } else { + createIconBitmap(badged) + } + } else { + bitmap + } + return result + } + + /** + * Creates a normalized bitmap suitable for the all apps view. The bitmap is also visually + * normalized with other icons and has enough spacing to add shadow. + */ + fun createBitmapWithoutShadow( + icon: Drawable?, + iconAppTargetSdk: Int + ): Bitmap { + var icon = icon + val iconBounds = RectF() + icon = normalizeAndWrapToAdaptiveIcon(icon!!, iconAppTargetSdk, iconBounds) + return createIconBitmap(icon) + } + + /** + * Sets the background color used for wrapped adaptive icon + */ + fun setWrapperBackgroundColor(color: Int) { + mWrapperBackgroundColor = + if (Color.alpha(color) < 255) DEFAULT_WRAPPER_BACKGROUND else color + } + + private fun normalizeAndWrapToAdaptiveIcon( + icon: Drawable, iconAppTargetSdk: Int, + outIconBounds: RectF? + ): Drawable { + // Ignore icon processing as of now. + return icon + } + + /** + * Adds the {@param badge} on top of {@param target} using the badge dimensions. + */ + fun badgeWithDrawable(target: Bitmap?, badge: Drawable) { + mCanvas.setBitmap(target) + badgeWithDrawable(mCanvas, badge) + mCanvas.setBitmap(null) + } + + /** + * Adds the {@param badge} on top of {@param target} using the badge dimensions. + */ + private fun badgeWithDrawable(target: Canvas, badge: Drawable) { + val badgeSize = mContext.resources + .getDimensionPixelSize(R.dimen.profile_badge_size) + badge.setBounds( + mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize, + mIconBitmapSize, mIconBitmapSize + ) + badge.draw(target) + } + + private fun createIconBitmap(icon: Drawable): Bitmap { + var width = mIconBitmapSize + var height = mIconBitmapSize + if (icon is PaintDrawable) { + val painter = icon + painter.intrinsicWidth = width + painter.intrinsicHeight = height + } else if (icon is BitmapDrawable) { + // Ensure the bitmap has a density. + val bitmapDrawable = icon + val bitmap = bitmapDrawable.bitmap + if (bitmap != null && bitmap.density == Bitmap.DENSITY_NONE) { + bitmapDrawable.setTargetDensity(mContext.resources.displayMetrics) + } + } + val sourceWidth = icon!!.intrinsicWidth + val sourceHeight = icon.intrinsicHeight + if (sourceWidth > 0 && sourceHeight > 0) { + // Scale the icon proportionally to the icon dimensions + val ratio = sourceWidth.toFloat() / sourceHeight + if (sourceWidth > sourceHeight) { + height = (width / ratio).toInt() + } else if (sourceHeight > sourceWidth) { + width = (height * ratio).toInt() + } + } + // no intrinsic size --> use default size + val textureWidth = mIconBitmapSize + val textureHeight = mIconBitmapSize + val bitmap = Bitmap.createBitmap( + textureWidth, textureHeight, + Bitmap.Config.ARGB_8888 + ) + mCanvas.setBitmap(bitmap) + val left = (textureWidth - width) / 2 + val top = (textureHeight - height) / 2 + mOldBounds.set(icon.bounds) + icon.setBounds(left, top, left + width, top + height) + mCanvas.save() + icon.draw(mCanvas) + mCanvas.restore() + icon.bounds = mOldBounds + mCanvas.setBitmap(null) + return bitmap + } + + @JvmOverloads + fun createShortcutIcon( + shortcutInfo: ShortcutInfoCompat, + badged: Boolean = true + ): Bitmap { + val unbadgedDrawable: Drawable? = + pinnedShortcutManager.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi) + val unbadgedBitmap: Bitmap + unbadgedBitmap = createBitmapWithoutShadow(unbadgedDrawable, 0) + if (!badged) { + return unbadgedBitmap + } + val badge = getShortcutInfoBadge(shortcutInfo) + return BitmapRenderer.createHardwareBitmap( + mIconBitmapSize, + mIconBitmapSize, + object : BitmapRenderer.Renderer { + override fun draw(out: Canvas) { + badgeWithDrawable(out, BitmapDrawable(badge.iconBitmap)) + } + }) + } + + private fun getShortcutInfoBadge( + shortcutInfo: ShortcutInfoCompat + ): LauncherItemWithIcon { + val cn = shortcutInfo.getActivity() + val badgePkg = shortcutInfo.getBadgePackage(mContext) + val hasBadgePkgSet = badgePkg != shortcutInfo.getPackage() + return if (cn != null && !hasBadgePkgSet) { + // Get the app info for the source activity. + val appItem = ApplicationItem() + appItem.user = shortcutInfo.getUserHandle() + appItem.componentName = cn + appItem.intent = Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setComponent(cn) + + appItem + } else { + val pkgInfo = PackageItem(badgePkg) + pkgInfo + } + } + + /** + * An extension of [BitmapDrawable] which returns the bitmap pixel size as intrinsic size. + * This allows the badging to be done based on the action bitmap size rather than + * the scaled bitmap size. + */ + private class FixedSizeBitmapDrawable(bitmap: Bitmap) : + BitmapDrawable(null, bitmap) { + override fun getIntrinsicHeight(): Int { + return bitmap.width + } + + override fun getIntrinsicWidth(): Int { + return bitmap.width + } + } + + companion object { + private const val DEFAULT_WRAPPER_BACKGROUND = Color.WHITE + } + + init { + mPm = mContext.packageManager + mFillResIconDpi = idp.fillResIconDpi + mIconBitmapSize = idp.iconBitmapSize + mCanvas = Canvas() + mCanvas.drawFilter = PaintFlagsDrawFilter( + Paint.DITHER_FLAG, + Paint.FILTER_BITMAP_FLAG + ) + } +} \ No newline at end of file diff --git a/data/src/main/res/values/dimens.xml b/data/src/main/res/values/dimens.xml index ade66cc90b..deab6f11f2 100644 --- a/data/src/main/res/values/dimens.xml +++ b/data/src/main/res/values/dimens.xml @@ -26,5 +26,6 @@ 14sp 48dp 24dp + 24dp \ No newline at end of file diff --git a/domain/src/main/java/foundation/e/blisslauncher/domain/entity/PackageItem.kt b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/PackageItem.kt new file mode 100644 index 0000000000..ad79ccca4a --- /dev/null +++ b/domain/src/main/java/foundation/e/blisslauncher/domain/entity/PackageItem.kt @@ -0,0 +1,3 @@ +package foundation.e.blisslauncher.domain.entity + +data class PackageItem(var packageName: String?): LauncherItemWithIcon() \ No newline at end of file -- GitLab From cb1e29986eb40be8be094faf34a954124c2b68db Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Mon, 8 Jun 2020 12:24:58 +0530 Subject: [PATCH 23/23] Improve icon caching --- .../common/AdaptiveIconGenerator.kt | 223 +++++++++++++++ .../common/FixedScaleDrawable.kt | 66 +++++ .../common}/graphics/ColorExtractor.kt | 52 +++- .../data/database/dao/IconDao.kt | 3 + .../e/blisslauncher/data/icon/IconCache.kt | 262 +++++++++++++++++- .../blisslauncher/data/icon/LauncherIcons.kt | 2 +- 6 files changed, 602 insertions(+), 6 deletions(-) create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/AdaptiveIconGenerator.kt create mode 100644 common/src/main/java/foundation/e/blisslauncher/common/FixedScaleDrawable.kt rename {blisslauncherv2/src/main/java/foundation/e/blisslauncher => common/src/main/java/foundation/e/blisslauncher/common}/graphics/ColorExtractor.kt (71%) diff --git a/common/src/main/java/foundation/e/blisslauncher/common/AdaptiveIconGenerator.kt b/common/src/main/java/foundation/e/blisslauncher/common/AdaptiveIconGenerator.kt new file mode 100644 index 0000000000..c200010091 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/AdaptiveIconGenerator.kt @@ -0,0 +1,223 @@ +package foundation.e.blisslauncher.common + +import android.content.Context +import android.graphics.Color +import android.graphics.RectF +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import android.util.SparseIntArray +import androidx.core.graphics.ColorUtils +import foundation.e.blisslauncher.common.compat.AdaptiveIconCompat +import foundation.e.blisslauncher.common.graphics.ColorExtractor + +class AdaptiveIconGenerator(private val context: Context, private val icon: Drawable) { + private var ranLoop = false + private val shouldWrap = false + private var backgroundColor = Color.WHITE + private val useWhiteBackground = true + private var isFullBleed = false + private var noMixinNeeded = false + private var fullBleedChecked = false + private val matchesMaskShape = false + private val isBackgroundWhite = false + private var scale = 0f + private var height = 0 + private var aHeight = 0f + private var width = 0 + private var aWidth = 0f + private var result: Drawable? = null + private fun loop() { + val extractee = icon + if (extractee == null) { + Log.e("AdaptiveIconGenerator", "extractee is null, skipping.") + onExitLoop() + return + } + val bounds = RectF() + scale = 1.0f + if (extractee is ColorDrawable) { + isFullBleed = true + fullBleedChecked = true + } + width = extractee.intrinsicWidth + height = extractee.intrinsicHeight + aWidth = width * (1 - (bounds.left + bounds.right)) + aHeight = height * (1 - (bounds.top + bounds.bottom)) + + // Check if the icon is squarish + val ratio = aHeight / aWidth + val isSquarish = 0.999 < ratio && ratio < 1.0001 + val almostSquarish = isSquarish || 0.97 < ratio && ratio < 1.005 + if (!isSquarish) { + isFullBleed = false + fullBleedChecked = true + } + val bitmap = + Utilities.drawableToBitmap(extractee) + if (bitmap == null) { + onExitLoop() + return + } + if (!bitmap.hasAlpha()) { + isFullBleed = true + fullBleedChecked = true + } + val size = height * width + val rgbScoreHistogram = + SparseIntArray(NUMBER_OF_COLORS_GUESSTIMATE) + val pixels = IntArray(size) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + + /* + * Calculate the number of padding pixels around the actual icon (i) + * +----------------+ + * | top | + * +---+--------+---+ + * | | | | + * | l | i | r | + * | | | | + * +---+--------+---+ + * | bottom | + * +----------------+ + */ + val adjHeight = height - bounds.top - bounds.bottom + val l = bounds.left * width * adjHeight + val top = bounds.top * height * width + val r = bounds.right * width * adjHeight + val bottom = bounds.bottom * height * width + val addPixels = Math.round(l + top + r + bottom) + + // Any icon with less than 10% transparent pixels (padding excluded) is considered "full-bleed-ish" + val maxTransparent = (Math.round(size * .10) + addPixels).toInt() + // Any icon with less than 27% transparent pixels (padding excluded) doesn't need a color mix-in + val noMixinScore = (Math.round(size * .27) + addPixels).toInt() + var highScore = 0 + var bestRGB = 0 + var transparentScore = 0 + for (pixel in pixels) { + val alpha = 0xFF and (pixel shr 24) + if (alpha < MIN_VISIBLE_ALPHA) { + // Drop mostly-transparent pixels. + transparentScore++ + if (transparentScore > maxTransparent) { + isFullBleed = false + fullBleedChecked = true + } + continue + } + // Reduce color complexity. + val rgb: Int = ColorExtractor.posterize(pixel) + if (rgb < 0) { + // Defensively avoid array bounds violations. + continue + } + val currentScore = rgbScoreHistogram[rgb] + 1 + rgbScoreHistogram.append(rgb, currentScore) + if (currentScore > highScore) { + highScore = currentScore + bestRGB = rgb + } + } + // add back the alpha channel + bestRGB = bestRGB or (0xff shl 24) + + // not yet checked = not set to false = has to be full bleed, isBackgroundWhite = true = is adaptive + isFullBleed = isFullBleed or (!fullBleedChecked && !isBackgroundWhite) + + // return early if a mix-in isnt needed + noMixinNeeded = + !isFullBleed && !isBackgroundWhite && almostSquarish && transparentScore <= noMixinScore + + // Currently, it's set to true so a white background is used for all the icons. + if (useWhiteBackground) { + //backgroundColor = Color.WHITE; + backgroundColor = Color.WHITE and -0x7f000001 + onExitLoop() + return + } + if (isFullBleed || noMixinNeeded) { + backgroundColor = bestRGB + onExitLoop() + return + } + + // "single color" + val numColors = rgbScoreHistogram.size() + val singleColor = + numColors <= SINGLE_COLOR_LIMIT + + // Convert to HSL to get the lightness and adjust the color + val hsl = FloatArray(3) + ColorUtils.colorToHSL(bestRGB, hsl) + val lightness = hsl[2] + val light = lightness > .5 + // Apply dark background to mostly white icons + val veryLight = lightness > .75 && singleColor + // Apply light background to mostly dark icons + val veryDark = lightness < .35 && singleColor + + // Adjust color to reach suitable contrast depending on the relationship between the colors + val opaqueSize = size - transparentScore + val pxPerColor = opaqueSize / numColors.toFloat() + val mixRatio = + Math.min(Math.max(pxPerColor / highScore, .15f), .7f) + + // Vary color mix-in based on lightness and amount of colors + val fill = if (light && !veryLight || veryDark) -0x1 else -0xcccccd + backgroundColor = ColorUtils.blendARGB(bestRGB, fill, mixRatio) + onExitLoop() + } + + private fun onExitLoop() { + ranLoop = true + result = genResult() + } + + private fun genResult(): Drawable { + val tmp = AdaptiveIconCompat( + ColorDrawable(), + FixedScaleDrawable() + ) + (tmp.getForeground() as FixedScaleDrawable).setDrawable(icon) + if (isFullBleed || noMixinNeeded) { + val scale: Float + scale = if (noMixinNeeded) { + val upScale = Math.min(width / aWidth, height / aHeight) + NO_MIXIN_ICON_SCALE * upScale + } else { + val upScale = Math.max(width / aWidth, height / aHeight) + FULL_BLEED_ICON_SCALE * upScale + } + (tmp.getForeground() as FixedScaleDrawable).setScale(scale) + } else { + (tmp.getForeground() as FixedScaleDrawable).setScale(scale) + } + (tmp.getBackground() as ColorDrawable).color = backgroundColor + return tmp + } + + fun getResult(): Drawable? { + if (!ranLoop) { + loop() + } + return result + } + + companion object { + // Average number of derived colors (based on averages with ~100 icons and performance testing) + private const val NUMBER_OF_COLORS_GUESSTIMATE = 45 + + // Found after some experimenting, might be improved with some more testing + private const val FULL_BLEED_ICON_SCALE = 1.44f + + // Found after some experimenting, might be improved with some more testing + private const val NO_MIXIN_ICON_SCALE = 1.40f + + // Icons with less than 5 colors are considered as "single color" + private const val SINGLE_COLOR_LIMIT = 5 + + // Minimal alpha to be considered opaque + private const val MIN_VISIBLE_ALPHA = 0xEF + } +} diff --git a/common/src/main/java/foundation/e/blisslauncher/common/FixedScaleDrawable.kt b/common/src/main/java/foundation/e/blisslauncher/common/FixedScaleDrawable.kt new file mode 100644 index 0000000000..764c32c710 --- /dev/null +++ b/common/src/main/java/foundation/e/blisslauncher/common/FixedScaleDrawable.kt @@ -0,0 +1,66 @@ +package foundation.e.blisslauncher.common + +import android.annotation.TargetApi +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.DrawableWrapper +import android.os.Build +import android.util.AttributeSet +import org.xmlpull.v1.XmlPullParser + +/** + * Extension of [DrawableWrapper] which scales the child drawables by a fixed amount. + */ +@TargetApi(Build.VERSION_CODES.N) +class FixedScaleDrawable : + DrawableWrapper(ColorDrawable()) { + private var mScaleX: Float + private var mScaleY: Float + override fun draw(canvas: Canvas) { + val saveCount = canvas.save() + canvas.scale( + mScaleX, mScaleY, + bounds.exactCenterX(), bounds.exactCenterY() + ) + super.draw(canvas) + canvas.restoreToCount(saveCount) + } + + override fun inflate( + r: Resources, + parser: XmlPullParser, + attrs: AttributeSet + ) { + } + + override fun inflate( + r: Resources, + parser: XmlPullParser, + attrs: AttributeSet, + theme: Resources.Theme + ) { + } + + fun setScale(scale: Float) { + val h = intrinsicHeight.toFloat() + val w = intrinsicWidth.toFloat() + mScaleX = scale * LEGACY_ICON_SCALE + mScaleY = scale * LEGACY_ICON_SCALE + if (h > w && w > 0) { + mScaleX *= w / h + } else if (w > h && h > 0) { + mScaleY *= h / w + } + } + + companion object { + // TODO b/33553066 use the constant defined in MaskableIconDrawable + const val LEGACY_ICON_SCALE = .7f * .6667f + } + + init { + mScaleX = LEGACY_ICON_SCALE + mScaleY = LEGACY_ICON_SCALE + } +} diff --git a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/ColorExtractor.kt b/common/src/main/java/foundation/e/blisslauncher/common/graphics/ColorExtractor.kt similarity index 71% rename from blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/ColorExtractor.kt rename to common/src/main/java/foundation/e/blisslauncher/common/graphics/ColorExtractor.kt index 057d77446e..273c385a2a 100644 --- a/blisslauncherv2/src/main/java/foundation/e/blisslauncher/graphics/ColorExtractor.kt +++ b/common/src/main/java/foundation/e/blisslauncher/common/graphics/ColorExtractor.kt @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package foundation.e.blisslauncher.graphics +package foundation.e.blisslauncher.common.graphics import android.graphics.Bitmap import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable import android.util.SparseArray +import foundation.e.blisslauncher.common.Utilities /** * Utility class for extracting colors from a bitmap. @@ -109,4 +112,51 @@ object ColorExtractor { } return bestColor } + + fun isSingleColor(drawable: Drawable?, color: Int): Boolean { + if (drawable == null) return true + val testColor = posterize(color) + if (drawable is ColorDrawable) { + return posterize(drawable.color) == testColor + } + val bitmap: Bitmap = Utilities.drawableToBitmap(drawable) ?: return false + val height = bitmap.height + val width = bitmap.width + val pixels = IntArray(height * width) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + val set: Set = HashSet(pixels.asList()) + val distinctPixels = set.toIntArray() + for (pixel in distinctPixels) { + if (testColor != posterize(pixel)) { + return false + } + } + return true + } + + private const val MAGIC_NUMBER = 25 + + /* + * References: + * https://www.cs.umb.edu/~jreyes/csit114-fall-2007/project4/filters.html#posterize + * https://github.com/gitgraghu/image-processing/blob/master/src/Effects/Posterize.java + */ + fun posterize(rgb: Int): Int { + var red = 0xff and (rgb shr 16) + var green = 0xff and (rgb shr 8) + var blue = 0xff and rgb + red -= red % MAGIC_NUMBER + green -= green % MAGIC_NUMBER + blue -= blue % MAGIC_NUMBER + if (red < 0) { + red = 0 + } + if (green < 0) { + green = 0 + } + if (blue < 0) { + blue = 0 + } + return red shl 16 or (green shl 8) or blue + } } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt index e1c075afa6..7f2135f1bc 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/database/dao/IconDao.kt @@ -20,6 +20,9 @@ interface IconDao { @Query("SELECT * FROM icons WHERE profileId = :userSerial") fun query(userSerial: Long): List + @Query("SELECT * FROM icons WHERE componentName = :userSerial AND profileId = :userSerial") + fun query(componentName: String, userSerial: Long): IconEntity? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrReplace(iconEntity: IconEntity) } \ No newline at end of file diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt index 621e4d6dc6..1b175cc031 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/IconCache.kt @@ -16,6 +16,7 @@ import android.graphics.drawable.Drawable import android.os.Build import android.os.Process import android.os.UserHandle +import android.text.TextUtils import android.util.Log import foundation.e.blisslauncher.common.BitmapRenderer import foundation.e.blisslauncher.common.InvariantDeviceProfile @@ -23,8 +24,12 @@ import foundation.e.blisslauncher.common.Utilities import foundation.e.blisslauncher.common.compat.LauncherAppsCompat import foundation.e.blisslauncher.data.database.dao.IconDao import foundation.e.blisslauncher.data.database.roomentity.IconEntity +import foundation.e.blisslauncher.domain.entity.ApplicationItem +import foundation.e.blisslauncher.domain.entity.LauncherItemWithIcon +import foundation.e.blisslauncher.domain.entity.PackageItem import foundation.e.blisslauncher.domain.keys.ComponentKey import foundation.e.blisslauncher.domain.repository.UserManagerRepository +import timber.log.Timber import java.util.HashSet import java.util.Stack import javax.inject.Inject @@ -42,7 +47,7 @@ class IconCache @Inject constructor( ) { data class CacheEntry( - var bitmap: Bitmap, + var bitmap: Bitmap? = null, var title: CharSequence? = "", var contentDescription: CharSequence = "" ) @@ -117,7 +122,7 @@ class IconCache @Inject constructor( return iconProvider.getIcon(info, iconDpi, flattenDrawable) } - protected fun makeDefaultIcon(user: UserHandle?): Bitmap { + fun makeDefaultIcon(user: UserHandle?): Bitmap { return launcherIcons.createBadgedIconBitmap( getFullResDefaultActivityIcon(), user, Build.VERSION.SDK_INT ) @@ -250,7 +255,7 @@ class IconCache @Inject constructor( } // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} // is called by the icon cache when the job is complete. - updateDBIcons( + updateDbIcons( user, apps, if (Process.myUserHandle() == user) ignorePackagesForMainUser else emptySet() @@ -258,7 +263,7 @@ class IconCache @Inject constructor( } } - private fun updateDBIcons( + private fun updateDbIcons( user: UserHandle, apps: List, ignorePackages: Set @@ -324,6 +329,255 @@ class IconCache @Inject constructor( } } + /** + * Updates {@param application} only if a valid entry is found. + */ + @Synchronized + fun updateTitleAndIcon(application: ApplicationItem) { + val entry: CacheEntry = cacheLocked( + application.componentName, + null, + application.user, false + ) + if (entry.bitmap != null && !isDefaultIcon(entry.bitmap!!, application.user)) { + applyCacheEntry(entry, application) + } + } + + /** + * Fill in {@param info} with the icon and label for {@param activityInfo} + */ + @Synchronized + fun getTitleAndIcon( + info: LauncherItemWithIcon, + activityInfo: LauncherActivityInfo? + ) { + // If we already have activity info, no need to use package icon + getTitleAndIcon(info, activityInfo, false) + } + + /** + * Fill in {@param info} with the icon and label. If the + * corresponding activity is not found, it reverts to the package icon. + */ + @Synchronized + fun getTitleAndIcon(info: LauncherItemWithIcon) { + // null info means not installed, but if we have a component from the intent then + // we should still look in the cache for restored app icons. + if (info.getTargetComponent() == null) { + getDefaultIcon(info.user).let { info.iconBitmap = it } + info.title = "" + info.contentDescription = "" + info.usingLowResIcon = false + } else { + getTitleAndIcon( + info, + launcherApps.resolveActivity(info.getIntent(), info.user), + true + ) + } + } + + /** + * Fill in {@param shortcutInfo} with the icon and label for {@param info} + */ + @Synchronized + private fun getTitleAndIcon( + infoInOut: LauncherItemWithIcon, + activityInfoProvider: LauncherActivityInfo?, + usePkgIcon: Boolean + ) { + val entry: CacheEntry = cacheLocked( + infoInOut.getTargetComponent()!!, activityInfoProvider, + infoInOut.user, usePkgIcon + ) + applyCacheEntry(entry, infoInOut) + } + + /** + * Fill in {@param infoInOut} with the corresponding icon and label. + */ + @Synchronized + fun getTitleAndIconForApp(infoInOut: PackageItem) { + val entry: CacheEntry = getEntryForPackageLocked( + infoInOut.packageName!!, infoInOut.user + ) + applyCacheEntry(entry, infoInOut) + } + + private fun applyCacheEntry(entry: CacheEntry, info: LauncherItemWithIcon) { + info.title = Utilities.trim(entry.title) + info.contentDescription = entry.contentDescription + (if (entry.bitmap == null) getDefaultIcon(info.user) else entry.bitmap).let { + info.iconBitmap = it + } + } + + /** + * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. + * This method is not thread safe, it must be called from a synchronized method. + */ + protected fun cacheLocked( + componentName: ComponentName, + info: LauncherActivityInfo?, + user: UserHandle, usePackageIcon: Boolean + ): CacheEntry { + //Preconditions.assertWorkerThread() + val cacheKey = ComponentKey(componentName, user) + var entry: CacheEntry? = cache[cacheKey] + if (entry == null) { + entry = CacheEntry() + cache[cacheKey] = entry + + if (!getEntryFromDB(cacheKey, entry)) { + if (info != null) { + launcherIcons.createBadgedIconBitmap( + getFullResIcon(info), info.user, + info.applicationInfo.targetSdkVersion + ).let { + entry.bitmap = it + } + } else { + if (usePackageIcon) { + val packageEntry: CacheEntry = + getEntryForPackageLocked( + componentName.packageName, user + ) + entry.bitmap = packageEntry.bitmap + entry.title = packageEntry.title + entry.contentDescription = packageEntry.contentDescription + } + if (entry.bitmap == null) { + getDefaultIcon(user).let { + entry.bitmap = it + } + } + } + } + if (TextUtils.isEmpty(entry.title)) { + if (info != null) { + entry.title = info.label + entry.contentDescription = + userManager.getBadgedLabelForUser(entry.title.toString(), user) + } + } + } + return entry + } + + @Synchronized + fun getDefaultIcon(user: UserHandle): Bitmap? { + if (!mDefaultIcons.containsKey(user)) { + mDefaultIcons[user] = makeDefaultIcon(user) + } + return mDefaultIcons[user] + } + + fun isDefaultIcon( + icon: Bitmap, + user: UserHandle + ): Boolean { + return getDefaultIcon(user) === icon + } + + private fun getEntryFromDB( + cacheKey: ComponentKey, + entry: CacheEntry + ): Boolean { + val iconEntity = iconDao.query( + cacheKey.componentName.flattenToString(), + userManager.getSerialNumberForUser(cacheKey.user) + ) + if (iconEntity != null) { + entry.bitmap = loadIcon(iconEntity.icon, highResOptions) + entry.title = iconEntity.label + if (entry.title == null) { + entry.title = "" + entry.contentDescription = "" + } else { + entry.contentDescription = userManager.getBadgedLabelForUser( + entry.title.toString(), cacheKey.user + ) + } + return true + } else return false + } + + /** + * Gets an entry for the package, which can be used as a fallback entry for various components. + * This method is not thread safe, it must be called from a synchronized method. + */ + private fun getEntryForPackageLocked( + packageName: String, user: UserHandle + ): CacheEntry { + val cacheKey: ComponentKey = getPackageKey(packageName, user) + var entry: CacheEntry? = cache.get(cacheKey) + if (entry == null) { + entry = CacheEntry() + var entryUpdated = true + + // Check the DB first. + if (!getEntryFromDB(cacheKey, entry)) { + try { + val flags = + if (Process.myUserHandle() == user) 0 else PackageManager.MATCH_UNINSTALLED_PACKAGES + val info: PackageInfo = + packageManager.getPackageInfo(packageName, flags) + val appInfo = info.applicationInfo + ?: throw PackageManager.NameNotFoundException("ApplicationInfo is null") + // Load the full res icon for the application, but if useLowResIcon is set, then + // only keep the low resolution icon instead of the larger full-sized icon + val icon: Bitmap = launcherIcons.createBadgedIconBitmap( + appInfo.loadIcon(packageManager), user, appInfo.targetSdkVersion + ) + entry.title = appInfo.loadLabel(packageManager) + entry.contentDescription = + userManager.getBadgedLabelForUser(entry.title.toString(), user) + entry.bitmap = icon + + // Add the icon in the DB here, since these do not get written during + // package updates. + addIconToDB( + entry, + cacheKey.componentName, + packageName, + info, + userManager.getSerialNumberForUser(user) + ) + } catch (e: PackageManager.NameNotFoundException) { + Timber.d("Application not installed $packageName") + entryUpdated = false + } + } + // Only add a filled-out entry to the cache + if (entryUpdated) { + cache[cacheKey] = entry + } + } + return entry + } + + private fun getPackageKey(packageName: String, user: UserHandle): ComponentKey { + val cn = ComponentName(packageName, packageName + EMPTY_CLASS_NAME) + return ComponentKey(cn, user) + } + + private fun loadIcon( + blob: ByteArray, + highResOptions: BitmapFactory.Options? + ): Bitmap? { + return try { + BitmapFactory.decodeByteArray(blob, 0, blob.size, highResOptions) + } catch (e: Exception) { + null + } + } + + @Synchronized + fun clear() { + iconDao.clear() + } + companion object { private const val TAG = "IconCache" diff --git a/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt b/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt index a92e89797c..3975866075 100644 --- a/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt +++ b/data/src/main/java/foundation/e/blisslauncher/data/icon/LauncherIcons.kt @@ -49,7 +49,7 @@ import javax.inject.Inject class LauncherIcons @Inject constructor( context: Context, idp: InvariantDeviceProfile, - val pinnedShortcutManager: PinnedShortcutManager + private val pinnedShortcutManager: PinnedShortcutManager ) : AutoCloseable { override fun close() { -- GitLab