Merge "Adding new tracing call from SysUI"
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f3db20e..9123959 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,2 +1,2 @@
[Hook Scripts]
-checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT}
diff --git a/SharedLibWrapper/build.gradle b/SharedLibWrapper/build.gradle
new file mode 100644
index 0000000..674e38a
--- /dev/null
+++ b/SharedLibWrapper/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'java'
+
+final String ANDROID_TOP = "${rootDir}/../../.."
+final String FRAMEWORK_PREBUILTS_DIR = "${ANDROID_TOP}/prebuilts/framework_intermediates/"
+
+sourceSets {
+ main {
+ java.srcDirs = ["${ANDROID_TOP}/frameworks/lib/systemui/SharedLibWrapper/src"]
+ }
+}
+
+sourceCompatibility = 1.8
+
+dependencies {
+ implementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
+ compileOnly fileTree(dir: "$ANDROID_TOP/prebuilts/fullsdk-${org.gradle.internal.os.OperatingSystem.current().isMacOsX() ? "darwin" : "linux"}/platforms/${COMPILE_SDK}", include: 'android.jar')
+}
diff --git a/build.gradle b/build.gradle
index e296455..534ca65 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,7 @@
repositories {
mavenCentral()
google()
+ jcenter()
}
dependencies {
classpath GRADLE_CLASS_PATH
@@ -62,12 +63,6 @@
minSdkVersion 28
}
- withQuickstepIconRecents {
- dimension "recents"
-
- minSdkVersion 28
- }
-
withoutQuickstep {
dimension "recents"
}
@@ -78,11 +73,6 @@
if (variant.buildType.name.endsWith('release')) {
variant.setIgnore(true)
}
-
- // Icon recents is Go only
- if (name.contains("WithQuickstepIconRecents") && !name.contains("l3go")) {
- variant.setIgnore(true)
- }
}
sourceSets {
@@ -96,10 +86,6 @@
}
}
- debug {
- manifest.srcFile "AndroidManifest.xml"
- }
-
androidTest {
res.srcDirs = ['tests/res']
java.srcDirs = ['tests/src', 'tests/tapl']
@@ -112,15 +98,30 @@
aosp {
java.srcDirs = ['src_flags', 'src_shortcuts_overrides']
+ }
+
+ aospWithoutQuickstep {
manifest.srcFile "AndroidManifest.xml"
}
+ aospWithQuickstep {
+ manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
+ }
+
l3go {
res.srcDirs = ['go/res']
java.srcDirs = ['go/src']
manifest.srcFile "go/AndroidManifest.xml"
}
+ l3goWithoutQuickstepDebug {
+ manifest.srcFile "AndroidManifest.xml"
+ }
+
+ l3goWithQuickstepDebug {
+ manifest.srcFile "quickstep/AndroidManifest-launcher.xml"
+ }
+
withoutQuickstep {
java.srcDirs = ['src_ui_overrides']
}
@@ -130,20 +131,17 @@
java.srcDirs = ['quickstep/src', 'quickstep/recents_ui_overrides/src']
manifest.srcFile "quickstep/AndroidManifest.xml"
}
-
- withQuickstepIconRecents {
- res.srcDirs = ['quickstep/res', 'go/quickstep/res']
- java.srcDirs = ['quickstep/src', 'go/quickstep/src']
- manifest.srcFile "quickstep/AndroidManifest.xml"
- }
}
}
-repositories {
- maven { url "../../../prebuilts/fullsdk-darwin/extras/android/m2repository" }
- maven { url "../../../prebuilts/fullsdk-linux/extras/android/m2repository" }
- mavenCentral()
- google()
+allprojects {
+ repositories {
+ maven { url "../../../prebuilts/sdk/current/androidx/m2repository" }
+ maven { url "../../../prebuilts/fullsdk-darwin/extras/android/m2repository" }
+ maven { url "../../../prebuilts/fullsdk-linux/extras/android/m2repository" }
+ mavenCentral()
+ google()
+ }
}
dependencies {
@@ -151,14 +149,12 @@
implementation "androidx.recyclerview:recyclerview:${ANDROID_X_VERSION}"
implementation "androidx.preference:preference:${ANDROID_X_VERSION}"
implementation project(':IconLoader')
+ withQuickstepImplementation project(':SharedLibWrapper')
implementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'launcher_protos.jar')
// Recents lib dependency
withQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
- // Recents lib dependency for Go
- withQuickstepIconRecentsImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/quickstep/libs", include: 'sysui_shared.jar')
-
// Required for AOSP to compile. This is already included in the sysui_shared.jar
withoutQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'plugin_core.jar')
@@ -175,7 +171,7 @@
protobuf {
// Configure the protoc executable
protoc {
- artifact = 'com.google.protobuf:protoc:3.0.0-alpha-3'
+ artifact = 'com.google.protobuf:protoc:3.0.0'
generateProtoTasks {
all().each { task ->
diff --git a/go/src/com/android/launcher3/model/LoaderResults.java b/go/src/com/android/launcher3/model/LoaderResults.java
index 26c3313..7130531 100644
--- a/go/src/com/android/launcher3/model/LoaderResults.java
+++ b/go/src/com/android/launcher3/model/LoaderResults.java
@@ -16,10 +16,11 @@
package com.android.launcher3.model;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.model.BgDataModel.Callbacks;
-
-import java.lang.ref.WeakReference;
+import com.android.launcher3.util.LooperExecutor;
/**
* Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -27,8 +28,13 @@
public class LoaderResults extends BaseLoaderResults {
public LoaderResults(LauncherAppState app, BgDataModel dataModel,
- AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
- super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+ AllAppsList allAppsList, Callbacks[] callbacks) {
+ this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+ }
+
+ public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+ AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+ super(app, dataModel, allAppsList, callbacks, executor);
}
@Override
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
index 7b8f4e6..7269b41 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -19,8 +19,10 @@
import android.content.Context;
import android.os.UserHandle;
-import com.android.launcher3.icons.ComponentWithLabel;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.WidgetListRowEntry;
@@ -29,8 +31,6 @@
import java.util.List;
import java.util.Set;
-import androidx.annotation.Nullable;
-
/**
* Widgets data model that is used by the adapters of the widget views and controllers.
*
@@ -39,7 +39,7 @@
public class WidgetsModel {
// True is the widget support is disabled.
- public static final boolean GO_DISABLE_WIDGETS = false;
+ public static final boolean GO_DISABLE_WIDGETS = true;
private static final ArrayList<WidgetListRowEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
diff --git a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
deleted file mode 100644
index 42b1194..0000000
--- a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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 com.android.launcher3.shortcuts;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.UserHandle;
-
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.notification.NotificationKeyData;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
- */
-public class DeepShortcutManager {
-
- private static final DeepShortcutManager sInstance = new DeepShortcutManager();
-
- public static DeepShortcutManager getInstance(Context context) {
- return sInstance;
- }
-
- private final QueryResult mFailure = new QueryResult();
-
- private DeepShortcutManager() { }
-
- /**
- * Queries for the shortcuts with the package name and provided ids.
- *
- * This method is intended to get the full details for shortcuts when they are added or updated,
- * because we only get "key" fields in onShortcutsChanged().
- */
- public QueryResult queryForFullDetails(String packageName,
- List<String> shortcutIds, UserHandle user) {
- return mFailure;
- }
-
- /**
- * Gets all the manifest and dynamic shortcuts associated with the given package and user,
- * to be displayed in the shortcuts container on long press.
- */
- public QueryResult queryForShortcutsContainer(ComponentName activity,
- UserHandle user) {
- return mFailure;
- }
-
- /**
- * Removes the given shortcut from the current list of pinned shortcuts.
- * (Runs on background thread)
- */
- public void unpinShortcut(final ShortcutKey key) {
- }
-
- /**
- * Adds the given shortcut to the current list of pinned shortcuts.
- * (Runs on background thread)
- */
- public void pinShortcut(final ShortcutKey key) {
- }
-
- public void startShortcut(String packageName, String id, Rect sourceBounds,
- Bundle startActivityOptions, UserHandle user) {
- }
-
- public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
- return null;
- }
-
- /**
- * Returns the id's of pinned shortcuts associated with the given package and user.
- *
- * If packageName is null, returns all pinned shortcuts regardless of package.
- */
- public QueryResult queryForPinnedShortcuts(String packageName, UserHandle user) {
- return mFailure;
- }
-
- public QueryResult queryForPinnedShortcuts(String packageName,
- List<String> shortcutIds, UserHandle user) {
- return mFailure;
- }
-
- public QueryResult queryForAllShortcuts(UserHandle user) {
- return mFailure;
- }
-
- public boolean hasHostPermission() {
- return false;
- }
-
-
- public static class QueryResult extends ArrayList<ShortcutInfo> {
-
- public boolean wasSuccess() {
- return true;
- }
- }
-}
diff --git a/gradle.properties b/gradle.properties
index a77f52a..7a51375 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,11 +2,11 @@
android.useAndroidX = true
android.enableJetifier = true
-ANDROID_X_VERSION=1.0.0-beta01
+ANDROID_X_VERSION=1+
-GRADLE_CLASS_PATH=com.android.tools.build:gradle:3.3.0
+GRADLE_CLASS_PATH=com.android.tools.build:gradle:3.5.1
-PROTOBUF_CLASS_PATH=com.google.protobuf:protobuf-gradle-plugin:0.8.6
+PROTOBUF_CLASS_PATH=com.google.protobuf:protobuf-gradle-plugin:0.8.8
PROTOBUF_DEPENDENCY=com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7
BUILD_TOOLS_VERSION=28.0.3
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
index 8a4d2b7..d7a62e1 100644
--- a/iconloaderlib/build.gradle
+++ b/iconloaderlib/build.gradle
@@ -3,7 +3,6 @@
android {
compileSdkVersion COMPILE_SDK
buildToolsVersion BUILD_TOOLS_VERSION
- publishNonDefault true
defaultConfig {
minSdkVersion 25
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 6f63d88..e807791 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -45,6 +45,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
@@ -250,9 +251,9 @@
* @param replaceExisting if true, it will recreate the bitmap even if it already exists in
* the memory. This is useful then the previous bitmap was created using
* old data.
- * package private
*/
- protected synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
+ @VisibleForTesting
+ public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
PackageInfo info, long userSerial, boolean replaceExisting) {
UserHandle user = cachingLogic.getUser(object);
ComponentName componentName = cachingLogic.getComponent(object);
diff --git a/proguard.flags b/proguard.flags
index 272ab7a..01302cf 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -2,12 +2,6 @@
*;
}
-# Proguard will strip new callbacks in LauncherApps.Callback from
-# WrappedCallback if compiled against an older SDK. Don't let this happen.
--keep class com.android.launcher3.compat.** {
- *;
-}
-
-keep class com.android.launcher3.graphics.ShadowDrawable {
public <init>(...);
}
@@ -23,7 +17,10 @@
# support jar.
-keep class androidx.recyclerview.widget.RecyclerView { *; }
-# Preference fragments
+# Fragments
+-keep class ** extends androidx.fragment.app.Fragment {
+ public <init>(...);
+}
-keep class ** extends android.app.Fragment {
public <init>(...);
}
@@ -50,4 +47,4 @@
-dontwarn android.app.**
-dontwarn android.view.**
-dontwarn android.os.**
--dontwarn android.graphics.**
\ No newline at end of file
+-dontwarn android.graphics.**
diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto
index 055ade5..0560d68 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -57,6 +57,7 @@
optional TargetExtension extension = 16;
optional TipType tip_type = 17;
optional int32 search_query_length = 18;
+ optional bool is_work_app = 19;
}
// Used to define what type of item a Target would represent.
@@ -92,7 +93,7 @@
TASKSWITCHER = 12; // Recents UI Container (QuickStep)
APP = 13; // Foreground activity is another app (QuickStep)
TIP = 14; // Onboarding texts (QuickStep)
- SIDELOADED_LAUNCHER = 15;
+ OTHER_LAUNCHER_APP = 15;
}
// Used to define what type of control a Target would represent.
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 826a275..5d871c3 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -91,6 +91,17 @@
android:taskAffinity="${packageName}.locktask"
android:directBootAware="true" />
+ <activity
+ android:name="com.android.quickstep.interaction.BackGestureTutorialActivity"
+ android:autoRemoveFromRecents="true"
+ android:excludeFromRecents="true"
+ android:screenOrientation="portrait">
+ <intent-filter>
+ <action android:name="com.android.quickstep.action.BACK_GESTURE_TUTORIAL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
</application>
</manifest>
diff --git a/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml b/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml
new file mode 100644
index 0000000..f0e70a8
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<vector android:height="24dp" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@color/hotseat_edu_background" android:pathData="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z"/>
+</vector>
diff --git a/quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml b/quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml
new file mode 100644
index 0000000..e3cc549
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<vector android:height="15.53398dp" android:viewportHeight="32"
+ android:viewportWidth="412" android:width="200dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@color/hotseat_edu_background" android:pathData="M412,32v-2.64C349.26,10.51 279.5,0 206,0S62.74,10.51 0,29.36V32H412z"/>
+</vector>
diff --git a/quickstep/recents_ui_overrides/res/drawable/predicted_icon_background.xml b/quickstep/recents_ui_overrides/res/drawable/predicted_icon_background.xml
deleted file mode 100644
index cfc6d48..0000000
--- a/quickstep/recents_ui_overrides/res/drawable/predicted_icon_background.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
- android:inset="@dimen/predicted_icon_background_inset">
- <shape>
- <solid android:color="?attr/folderFillColor" />
- <corners android:radius="@dimen/predicted_icon_background_corner_radius" />
- </shape>
-</inset>
diff --git a/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
new file mode 100644
index 0000000..ee38e3b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<com.android.launcher3.hybridhotseat.HotseatEduDialog xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_gravity="bottom"
+ android:layout_height="wrap_content"
+ android:gravity="bottom"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="32dp"
+ android:background="@drawable/hotseat_prediction_edu_top" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/hotseat_edu_background"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="18dp"
+ android:fontFamily="google-sans"
+ android:paddingLeft="@dimen/hotseat_edu_padding"
+ android:paddingRight="@dimen/hotseat_edu_padding"
+ android:text="@string/hotseat_migrate_title"
+ android:textAlignment="center"
+ android:textColor="@android:color/white"
+ android:textSize="20sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="18dp"
+ android:layout_marginBottom="18dp"
+ android:fontFamily="roboto-medium"
+ android:paddingLeft="@dimen/hotseat_edu_padding"
+ android:paddingRight="@dimen/hotseat_edu_padding"
+ android:text="@string/hotseat_migrate_message"
+ android:textAlignment="center"
+ android:textColor="@android:color/white"
+ android:textSize="16sp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/hotseat_wrapper"
+ android:orientation="vertical">
+
+ <com.android.launcher3.CellLayout
+ android:id="@+id/sample_prediction"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ launcher:containerType="hotseat" />
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/hotseat_edu_padding"
+ android:paddingTop="8dp"
+ android:paddingRight="@dimen/hotseat_edu_padding">
+
+ <Button
+ android:id="@+id/turn_predictions_on"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:background="?android:attr/selectableItemBackground"
+ android:text="@string/hotseat_migrate_accept"
+ android:textAlignment="textEnd"
+ android:textColor="@android:color/white" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/no_thanks"
+ android:text="@string/hotseat_migrate_dismiss"
+ android:layout_gravity="start"
+ android:background="?android:attr/selectableItemBackground"
+ android:textColor="@android:color/white" />
+
+ </FrameLayout>
+ </LinearLayout>
+ </LinearLayout>
+
+</com.android.launcher3.hybridhotseat.HotseatEduDialog>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml
index 7426e30..4fa5684 100644
--- a/quickstep/recents_ui_overrides/res/values/colors.xml
+++ b/quickstep/recents_ui_overrides/res/values/colors.xml
@@ -6,4 +6,6 @@
<color name="all_apps_label_text_dark">#61FFFFFF</color>
<color name="all_apps_prediction_row_separator">#3c000000</color>
<color name="all_apps_prediction_row_separator_dark">#3cffffff</color>
+
+ <color name="hotseat_edu_background">#f01A73E8</color>
</resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
index ee672d4..c458ec7 100644
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -29,8 +29,7 @@
<dimen name="swipe_up_y_overshoot">10dp</dimen>
<dimen name="swipe_up_max_workspace_trans_y">-60dp</dimen>
- <!-- Predicted icon related -->
- <dimen name="predicted_icon_background_corner_radius">15dp</dimen>
- <dimen name="predicted_icon_background_inset">8dp</dimen>
+ <!-- Hybrid hotseat related -->
+ <dimen name="hotseat_edu_padding">24dp</dimen>
</resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
index 38bb180..76972af 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -44,8 +44,8 @@
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.InstantAppResolver;
import java.util.ArrayList;
@@ -167,11 +167,7 @@
@WorkerThread
private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) {
- DeepShortcutManager mgr = DeepShortcutManager.getInstance(mContext);
- List<ShortcutInfo> details = mgr.queryForFullDetails(
- shortcutKey.componentName.getPackageName(),
- Collections.<String>singletonList(shortcutKey.getId()),
- shortcutKey.user);
+ List<ShortcutInfo> details = shortcutKey.buildRequest(mContext).query(ShortcutRequest.ALL);
if (!details.isEmpty()) {
WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index e45eded..06b9f1f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -26,7 +26,6 @@
import androidx.annotation.NonNull;
-import com.android.launcher3.HotseatPredictionController;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
import com.android.launcher3.ItemInfo;
@@ -39,6 +38,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.userevent.nano.LauncherLogProto;
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
new file mode 100644
index 0000000..923e050
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.core.app.NotificationCompat;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.ActivityTracker;
+
+import java.util.List;
+
+/**
+ * Controller class for managing user onboaridng flow for hybrid hotseat
+ */
+public class HotseatEduController {
+ public static final String KEY_HOTSEAT_EDU_SEEN = "hotseat_edu_seen";
+
+ private static final String NOTIFICATION_CHANNEL_ID = "launcher_onboarding";
+ private static final int ONBOARDING_NOTIFICATION_ID = 7641;
+
+ private final Launcher mLauncher;
+ private List<WorkspaceItemInfo> mPredictedApps;
+ private HotseatEduDialog mActiveDialog;
+
+ private final NotificationManager mNotificationManager;
+ private final Notification mNotification;
+
+ HotseatEduController(Launcher launcher) {
+ mLauncher = launcher;
+ mNotificationManager = mLauncher.getSystemService(NotificationManager.class);
+ createNotificationChannel();
+ mNotification = createNotification();
+ }
+
+ void migrate() {
+ ViewGroup hotseatVG = mLauncher.getHotseat().getShortcutsAndWidgets();
+ int workspacePageCount = mLauncher.getWorkspace().getPageCount();
+ for (int i = 0; i < hotseatVG.getChildCount(); i++) {
+ View child = hotseatVG.getChildAt(i);
+ ItemInfo tag = (ItemInfo) child.getTag();
+ mLauncher.getModelWriter().moveItemInDatabase(tag,
+ LauncherSettings.Favorites.CONTAINER_DESKTOP, workspacePageCount, tag.screenId,
+ 0);
+ }
+ }
+
+ void removeNotification() {
+ mNotificationManager.cancel(ONBOARDING_NOTIFICATION_ID);
+ }
+
+ void finishOnboarding() {
+ mLauncher.getModel().rebindCallbacks();
+ mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply();
+ removeNotification();
+ }
+
+ void setPredictedApps(List<WorkspaceItemInfo> predictedApps) {
+ mPredictedApps = predictedApps;
+ if (!mPredictedApps.isEmpty()
+ && mLauncher.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
+ mNotificationManager.notify(ONBOARDING_NOTIFICATION_ID, mNotification);
+ }
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
+ CharSequence name = mLauncher.getString(R.string.hotseat_migrate_title);
+ int importance = NotificationManager.IMPORTANCE_LOW;
+ NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name,
+ importance);
+ mNotificationManager.createNotificationChannel(channel);
+ }
+
+ private Notification createNotification() {
+ Intent intent = new Intent(mLauncher.getApplicationContext(), mLauncher.getClass());
+ intent = new NotificationHandler().addToIntent(intent);
+
+ CharSequence name = mLauncher.getString(R.string.hotseat_migrate_prompt_title);
+ String description = mLauncher.getString(R.string.hotseat_migrate_prompt_content);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(mLauncher,
+ NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(name)
+ .setOngoing(true)
+ .setColor(mLauncher.getColor(R.color.hotseat_edu_background))
+ .setContentIntent(PendingIntent.getActivity(mLauncher, 0, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT))
+ .setSmallIcon(R.drawable.hotseat_edu_notification_icon)
+ .setContentText(description);
+ return builder.build();
+
+ }
+
+ void destroy() {
+ removeNotification();
+ if (mActiveDialog != null) {
+ mActiveDialog.setHotseatEduController(null);
+ }
+ }
+
+ void showDialog() {
+ if (mPredictedApps == null || mPredictedApps.isEmpty()) {
+ return;
+ }
+ if (mActiveDialog != null) {
+ mActiveDialog.handleClose(false);
+ }
+ mActiveDialog = HotseatEduDialog.getDialog(mLauncher);
+ mActiveDialog.setHotseatEduController(this);
+ mActiveDialog.show(mPredictedApps);
+ }
+
+ static class NotificationHandler implements
+ ActivityTracker.SchedulerCallback<QuickstepLauncher> {
+ @Override
+ public boolean init(QuickstepLauncher activity, boolean alreadyOnHome) {
+ activity.getHotseatPredictionController().showEduDialog();
+ return true;
+ }
+ }
+}
+
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
new file mode 100644
index 0000000..4c87945
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.views.AbstractSlideInView;
+
+import java.util.List;
+
+/**
+ * User education dialog for hybrid hotseat. Allows user to migrate hotseat items to a new page in
+ * the workspace and shows predictions on the whole hotseat
+ */
+public class HotseatEduDialog extends AbstractSlideInView implements Insettable {
+
+ private static final int DEFAULT_CLOSE_DURATION = 200;
+
+ public static boolean shown = false;
+
+ private final Rect mInsets = new Rect();
+ private View mHotseatWrapper;
+ private CellLayout mSampleHotseat;
+
+ public void setHotseatEduController(HotseatEduController hotseatEduController) {
+ mHotseatEduController = hotseatEduController;
+ }
+
+ private HotseatEduController mHotseatEduController;
+
+ public HotseatEduDialog(Context context, AttributeSet attr) {
+ this(context, attr, 0);
+ }
+
+ public HotseatEduDialog(Context context, AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mContent = this;
+ }
+
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mHotseatWrapper = findViewById(R.id.hotseat_wrapper);
+ mSampleHotseat = findViewById(R.id.sample_prediction);
+
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+ Rect padding = grid.getHotseatLayoutPadding();
+
+ mSampleHotseat.getLayoutParams().height = grid.cellHeightPx;
+ mSampleHotseat.setGridSize(grid.inv.numHotseatIcons, 1);
+ mSampleHotseat.setPadding(padding.left, 0, padding.right, 0);
+
+ Button turnOnBtn = findViewById(R.id.turn_predictions_on);
+ turnOnBtn.setOnClickListener(this::onMigrate);
+
+ Button learnMoreBtn = findViewById(R.id.no_thanks);
+ learnMoreBtn.setOnClickListener(this::onKeepDefault);
+
+ }
+
+ private void onMigrate(View v) {
+ if (mHotseatEduController == null) return;
+ handleClose(true);
+ mHotseatEduController.migrate();
+ mHotseatEduController.finishOnboarding();
+ Toast.makeText(mLauncher, R.string.hotseat_items_migrated, Toast.LENGTH_LONG).show();
+ }
+
+ private void onKeepDefault(View v) {
+ if (mHotseatEduController == null) return;
+ Toast.makeText(getContext(), R.string.hotseat_no_migration, Toast.LENGTH_LONG).show();
+ mHotseatEduController.finishOnboarding();
+ handleClose(true);
+ }
+
+ @Override
+ public void logActionCommand(int command) {
+ // Since this is on-boarding popup, it is not a user controlled action.
+ }
+
+ @Override
+ public int getLogContainerType() {
+ return LauncherLogProto.ContainerType.TIP;
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_ON_BOARD_POPUP) != 0;
+ }
+
+ @Override
+ public void setInsets(Rect insets) {
+ int leftInset = insets.left - mInsets.left;
+ int rightInset = insets.right - mInsets.right;
+ int bottomInset = insets.bottom - mInsets.bottom;
+ mInsets.set(insets);
+ setPadding(leftInset, getPaddingTop(), rightInset, 0);
+ mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
+ mHotseatWrapper.getPaddingRight(), bottomInset);
+ mHotseatWrapper.getLayoutParams().height =
+ mLauncher.getDeviceProfile().hotseatBarSizePx + insets.bottom;
+ }
+
+
+ private void animateOpen() {
+ if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+ return;
+ }
+ mIsOpen = true;
+ mOpenCloseAnimator.setValues(
+ PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+ mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mOpenCloseAnimator.start();
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ handleClose(true, DEFAULT_CLOSE_DURATION);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ handleClose(false);
+ }
+
+ /**
+ * Opens User education dialog with a list of suggested apps
+ */
+ public void show(List<WorkspaceItemInfo> predictions) {
+ if (getParent() != null
+ || predictions.size() < mLauncher.getDeviceProfile().inv.numHotseatIcons) {
+ return;
+ }
+ mLauncher.getDragLayer().addView(this);
+ animateOpen();
+ for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
+ WorkspaceItemInfo info = predictions.get(i);
+ PredictedAppIcon icon = PredictedAppIcon.createIcon(mSampleHotseat, info);
+ icon.setEnabled(false);
+ icon.verifyHighRes();
+ CellLayout.LayoutParams lp = new CellLayout.LayoutParams(i, 0, 1, 1);
+ mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true);
+ }
+ }
+
+ /**
+ * Factory method for HotseatPredictionUserEdu dialog
+ */
+ public static HotseatEduDialog getDialog(Launcher launcher) {
+ LayoutInflater layoutInflater = LayoutInflater.from(launcher);
+ return (HotseatEduDialog) layoutInflater.inflate(
+ R.layout.predicted_hotseat_edu, launcher.getDragLayer(),
+ false);
+
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
similarity index 71%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
rename to quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index f7e71f3..c2b55ab 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.launcher3;
+package com.android.launcher3.hybridhotseat;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
@@ -35,6 +35,20 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.appprediction.ComponentKeyMapper;
@@ -42,8 +56,10 @@
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.logging.FileLog;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.uioverrides.PredictedAppIcon;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -61,7 +77,7 @@
public class HotseatPredictionController implements DragController.DragListener,
View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher>,
InvariantDeviceProfile.OnIDPChangeListener, AllAppsStore.OnUpdateListener,
- IconCache.ItemInfoUpdateReceiver {
+ IconCache.ItemInfoUpdateReceiver, DragSource {
private static final String TAG = "PredictiveHotseat";
private static final boolean DEBUG = false;
@@ -72,6 +88,9 @@
private static final String APP_LOCATION_HOTSEAT = "hotseat";
private static final String APP_LOCATION_WORKSPACE = "workspace";
+ private static final String BUNDLE_KEY_HOTSEAT = "hotseat_apps";
+ private static final String BUNDLE_KEY_WORKSPACE = "workspace_apps";
+
private static final String PREDICTION_CLIENT = "hotseat";
private DropTarget.DragObject mDragObject;
@@ -79,7 +98,7 @@
private int mPredictedSpotsCount = 0;
private Launcher mLauncher;
- private Hotseat mHotseat;
+ private final Hotseat mHotseat;
private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>();
@@ -87,10 +106,19 @@
private AppPredictor mAppPredictor;
private AllAppsStore mAllAppsStore;
+ private AnimatorSet mIconRemoveAnimators;
+
+ private HotseatEduController mHotseatEduController;
private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
- private static HotseatPredictionController sInstance;
+ private final View.OnLongClickListener mPredictionLongClickListener = v -> {
+ if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
+ if (mLauncher.getWorkspace().isSwitchingState()) return false;
+ // Start the drag
+ mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions());
+ return false;
+ };
public HotseatPredictionController(Launcher launcher) {
mLauncher = launcher;
@@ -101,7 +129,26 @@
mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
launcher.getDeviceProfile().inv.addOnChangeListener(this);
mHotseat.addOnAttachStateChangeListener(this);
- sInstance = this;
+ if (mHotseat.isAttachedToWindow()) {
+ onViewAttachedToWindow(mHotseat);
+ }
+ }
+
+ /**
+ * Returns whether or not the prediction controller is ready to show predictions
+ */
+ public boolean isReady() {
+ return mLauncher.getSharedPrefs().getBoolean(HotseatEduController.KEY_HOTSEAT_EDU_SEEN,
+ false);
+ }
+
+ /**
+ * Transitions to NORMAL workspace mode and shows edu dialog
+ */
+ public void showEduDialog() {
+ if (mHotseatEduController == null) return;
+ mLauncher.getStateManager().goToState(LauncherState.NORMAL, true,
+ () -> mHotseatEduController.showDialog());
}
@Override
@@ -119,12 +166,23 @@
}
private void fillGapsWithPrediction(boolean animate, Runnable callback) {
- if (mDragObject != null) {
+ if (!isReady() || mDragObject != null) {
return;
}
List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
int predictionIndex = 0;
ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
+ // make sure predicted icon removal and filling predictions don't step on each other
+ if (mIconRemoveAnimators != null && mIconRemoveAnimators.isRunning()) {
+ mIconRemoveAnimators.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ fillGapsWithPrediction(animate, callback);
+ mIconRemoveAnimators.removeListener(this);
+ }
+ });
+ return;
+ }
for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
View child = mHotseat.getChildAt(
mHotseat.getCellXFromOrder(rank),
@@ -140,12 +198,11 @@
}
continue;
}
-
WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
if (isPredictedIcon(child) && child.isEnabled()) {
PredictedAppIcon icon = (PredictedAppIcon) child;
icon.applyFromWorkspaceItem(predictedItem);
- icon.finishBinding();
+ icon.finishBinding(mPredictionLongClickListener);
} else {
newItems.add(predictedItem);
}
@@ -160,7 +217,7 @@
for (WorkspaceItemInfo item : itemsToAdd) {
PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
mLauncher.getWorkspace().addInScreenFromBind(icon, item);
- icon.finishBinding();
+ icon.finishBinding(mPredictionLongClickListener);
if (animate) {
animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
}
@@ -210,16 +267,23 @@
mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
this::setPredictedApps);
+ if (!isReady()) {
+ if (mHotseatEduController != null) {
+ mHotseatEduController.destroy();
+ }
+ mHotseatEduController = new HotseatEduController(mLauncher);
+ }
mAppPredictor.requestPredictionUpdate();
}
private Bundle getAppPredictionContextExtra() {
Bundle bundle = new Bundle();
- bundle.putParcelableArrayList(APP_LOCATION_HOTSEAT,
+ bundle.putParcelableArrayList(BUNDLE_KEY_HOTSEAT,
getPinnedAppTargetsInViewGroup((mHotseat.getShortcutsAndWidgets())));
- bundle.putParcelableArrayList(APP_LOCATION_WORKSPACE, getPinnedAppTargetsInViewGroup(
+ bundle.putParcelableArrayList(BUNDLE_KEY_WORKSPACE, getPinnedAppTargetsInViewGroup(
mLauncher.getWorkspace().getScreenWithId(
Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets()));
+
return bundle;
}
@@ -237,6 +301,7 @@
private void setPredictedApps(List<AppTarget> appTargets) {
mComponentKeyMappers.clear();
+ StringBuilder predictionLog = new StringBuilder("predictedApps: [\n");
for (AppTarget appTarget : appTargets) {
ComponentKey key;
if (appTarget.getShortcutInfo() != null) {
@@ -245,10 +310,20 @@
key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
appTarget.getClassName()), appTarget.getUser());
}
+ predictionLog.append(key.toString());
+ predictionLog.append(",rank:");
+ predictionLog.append(appTarget.getRank());
+ predictionLog.append("\n");
mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
}
+ predictionLog.append("]");
+ FileLog.d(TAG, predictionLog.toString());
updateDependencies();
- fillGapsWithPrediction();
+ if (isReady()) {
+ fillGapsWithPrediction();
+ } else if (mHotseatEduController != null) {
+ mHotseatEduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
+ }
}
private void updateDependencies() {
@@ -285,9 +360,12 @@
ItemInfoWithIcon info = mapper.getApp(allAppsStore);
if (info instanceof AppInfo) {
WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((AppInfo) info);
+ predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
predictedApps.add(predictedApp);
} else if (info instanceof WorkspaceItemInfo) {
- predictedApps.add(new WorkspaceItemInfo((WorkspaceItemInfo) info));
+ WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((WorkspaceItemInfo) info);
+ predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+ predictedApps.add(predictedApp);
} else {
if (DEBUG) {
Log.e(TAG, "Predicted app not found: " + mapper);
@@ -313,13 +391,27 @@
return icons;
}
- private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines) {
+ private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines,
+ ItemInfo draggedInfo) {
+ if (mIconRemoveAnimators != null) {
+ mIconRemoveAnimators.end();
+ }
+ mIconRemoveAnimators = new AnimatorSet();
+ removeOutlineDrawings();
for (PredictedAppIcon icon : getPredictedIcons()) {
+ if (!icon.isEnabled()) {
+ continue;
+ }
+ if (icon.getTag().equals(draggedInfo)) {
+ mHotseat.removeView(icon);
+ continue;
+ }
int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
icon.setEnabled(false);
- icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
+ ObjectAnimator animator = ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0);
+ animator.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
if (icon.getParent() != null) {
@@ -327,10 +419,11 @@
}
}
});
+ mIconRemoveAnimators.play(animator);
}
+ mIconRemoveAnimators.start();
}
-
private void notifyItemAction(AppTarget target, String location, int action) {
if (mAppPredictor != null) {
mAppPredictor.notifyAppTargetEvent(new AppTargetEvent.Builder(target,
@@ -340,7 +433,7 @@
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
- removePredictedApps(mOutlineDrawings);
+ removePredictedApps(mOutlineDrawings, dragObject.dragInfo);
mDragObject = dragObject;
if (mOutlineDrawings.isEmpty()) return;
for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
@@ -354,14 +447,25 @@
if (mDragObject == null) {
return;
}
+
ItemInfo dragInfo = mDragObject.dragInfo;
- if (dragInfo instanceof WorkspaceItemInfo && dragInfo.getTargetComponent() != null) {
+ ViewGroup hotseatVG = mHotseat.getShortcutsAndWidgets();
+ ViewGroup firstScreenVG = mLauncher.getWorkspace().getScreenWithId(
+ Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets();
+
+ if (dragInfo instanceof WorkspaceItemInfo
+ && dragInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+ && dragInfo.getTargetComponent() != null) {
AppTarget appTarget = getAppTargetFromItemInfo(dragInfo);
if (!isInHotseat(dragInfo) && isInHotseat(mDragObject.originalDragInfo)) {
- notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
+ if (!getPinnedAppTargetsInViewGroup(hotseatVG).contains(appTarget)) {
+ notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
+ }
}
if (!isInFirstPage(dragInfo) && isInFirstPage(mDragObject.originalDragInfo)) {
- notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
+ if (!getPinnedAppTargetsInViewGroup(firstScreenVG).contains(appTarget)) {
+ notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
+ }
}
if (isInHotseat(dragInfo) && !isInHotseat(mDragObject.originalDragInfo)) {
notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
@@ -371,14 +475,7 @@
}
}
mDragObject = null;
- fillGapsWithPrediction(true, () -> {
- if (mOutlineDrawings.isEmpty()) return;
- for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
- mHotseat.removeDelegatedCellDrawing(outlineDrawing);
- }
- mHotseat.invalidate();
- mOutlineDrawings.clear();
- });
+ fillGapsWithPrediction(true, this::removeOutlineDrawings);
}
@Nullable
@@ -394,11 +491,20 @@
private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
itemInfo.rank = rank;
- itemInfo.cellX = rank;
- itemInfo.cellY = mHotSeatItemsCount - rank - 1;
+ itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
+ itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
itemInfo.screenId = rank;
}
+ private void removeOutlineDrawings() {
+ if (mOutlineDrawings.isEmpty()) return;
+ for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+ mHotseat.removeDelegatedCellDrawing(outlineDrawing);
+ }
+ mHotseat.invalidate();
+ mOutlineDrawings.clear();
+ }
+
@Override
public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
this.mHotSeatItemsCount = profile.numHotseatIcons;
@@ -411,8 +517,17 @@
}
@Override
- public void reapplyItemInfo(ItemInfoWithIcon info) {
+ public void reapplyItemInfo(ItemInfoWithIcon info) {}
+ @Override
+ public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
+ //Does nothing
+ }
+
+ @Override
+ public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+ LauncherLogProto.Target targetParent) {
+ mHotseat.fillInLogContainerData(v, info, target, targetParent);
}
private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
@@ -435,18 +550,22 @@
*/
public static void fillInHybridHotseatRank(
@NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) {
- if (sInstance == null || itemInfo.getTargetComponent() == null
+ QuickstepLauncher launcher = QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+ if (launcher == null || launcher.getHotseatPredictionController() == null
+ || itemInfo.getTargetComponent() == null
|| itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
return;
}
+ HotseatPredictionController controller = launcher.getHotseatPredictionController();
+
final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
- final List<ComponentKeyMapper> predictedApps = sInstance.mComponentKeyMappers;
+ final List<ComponentKeyMapper> predictedApps = controller.mComponentKeyMappers;
IntStream.range(0, predictedApps.size())
.filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
.findFirst()
.ifPresent((rank) -> target.predictedRank =
- Integer.parseInt(sInstance.mPredictedSpotsCount + "0" + rank));
+ Integer.parseInt(controller.mPredictedSpotsCount + "0" + rank));
}
private static boolean isPredictedIcon(View view) {
@@ -461,8 +580,7 @@
}
ItemInfo info = (ItemInfo) view.getTag();
return info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION && (
- info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
- || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT);
+ info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION);
}
private static boolean isInHotseat(ItemInfo itemInfo) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 1dcbffb..bd89626 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -29,7 +29,6 @@
import androidx.core.graphics.ColorUtils;
-import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
@@ -37,7 +36,6 @@
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.graphics.IconPalette;
import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
@@ -47,14 +45,13 @@
*/
public class PredictedAppIcon extends DoubleShadowBubbleTextView {
- private static final float RING_EFFECT_RATIO = 0.12f;
+ private static final float RING_EFFECT_RATIO = 0.11f;
- private DeviceProfile mDeviceProfile;
+ private final DeviceProfile mDeviceProfile;
private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private boolean mIsPinned = false;
private int mNormalizedIconRadius;
-
public PredictedAppIcon(Context context) {
this(context, null, 0);
}
@@ -68,7 +65,7 @@
mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
setOnClickListener(ItemClickHandler.INSTANCE);
- setOnFocusChangeListener(Launcher.getLauncher(context).mFocusHandler);
+ setOnFocusChangeListener(Launcher.getLauncher(context).getFocusHandler());
}
@Override
@@ -105,14 +102,8 @@
/**
* prepares prediction icon for usage after bind
*/
- public void finishBinding() {
- setOnLongClickListener((v) -> {
- PopupContainerWithArrow.showForIcon((BubbleTextView) v);
- if (getParent() != null) {
- getParent().requestDisallowInterceptTouchEvent(true);
- }
- return true;
- });
+ public void finishBinding(OnLongClickListener longClickListener) {
+ setOnLongClickListener(longClickListener);
((CellLayout.LayoutParams) getLayoutParams()).canReorder = false;
setTextVisibility(false);
verifyHighRes();
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 356e443..9b36748 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -27,12 +27,12 @@
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.HotseatPredictionController;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
@@ -179,6 +179,13 @@
}
/**
+ * Returns Prediction controller for hybrid hotseat
+ */
+ public HotseatPredictionController getHotseatPredictionController() {
+ return mHotseatPredictionController;
+ }
+
+ /**
* Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
*/
private void onStateOrResumeChanged() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index bd37e56..73c0c97 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -173,7 +173,7 @@
@Override
public String getDescription(Launcher launcher) {
- return launcher.getString(R.string.accessibility_desc_recent_apps);
+ return launcher.getString(R.string.accessibility_recent_apps);
}
public static float getDefaultSwipeHeight(Launcher launcher) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 613386e..4e08df9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -98,6 +98,7 @@
private final float mYRange;
private final MotionPauseDetector mMotionPauseDetector;
private final float mMotionPauseMinDisplacement;
+ private final LauncherRecentsView mRecentsView;
private boolean mNoIntercept;
private LauncherState mStartState;
@@ -119,6 +120,7 @@
mMotionPauseDetector = new MotionPauseDetector(mLauncher);
mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
R.dimen.motion_pause_detector_min_displacement_from_app);
+ mRecentsView = mLauncher.getOverviewPanel();
}
@Override
@@ -208,6 +210,15 @@
updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder, ANIM_ALL);
mNonOverviewAnim.dispatchOnStart();
+ if (mRecentsView.getTaskViewCount() == 0) {
+ mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> {
+ if (!isEmpty && mSwipeDetector.isDraggingState()) {
+ // We have loaded tasks, update the animators to start at the correct scale etc.
+ setupOverviewAnimators();
+ }
+ });
+ }
+
setupOverviewAnimators();
}
@@ -228,25 +239,25 @@
LauncherState.ScaleAndTranslation toScaleAndTranslation = toState
.getOverviewScaleAndTranslation(mLauncher);
// Update RecentView's translationX to have it start offscreen.
- LauncherRecentsView recentsView = mLauncher.getOverviewPanel();
float startScale = Utilities.mapRange(
SCALE_DOWN_INTERPOLATOR.getInterpolation(Y_ANIM_MIN_PROGRESS),
fromScaleAndTranslation.scale,
toScaleAndTranslation.scale);
- fromScaleAndTranslation.translationX = recentsView.getOffscreenTranslationX(startScale);
+ fromScaleAndTranslation.translationX = mRecentsView.getOffscreenTranslationX(startScale);
// Set RecentView's initial properties.
- recentsView.setScaleX(fromScaleAndTranslation.scale);
- recentsView.setScaleY(fromScaleAndTranslation.scale);
- recentsView.setTranslationX(fromScaleAndTranslation.translationX);
- recentsView.setTranslationY(fromScaleAndTranslation.translationY);
- recentsView.setContentAlpha(1);
+ mRecentsView.setScaleX(fromScaleAndTranslation.scale);
+ mRecentsView.setScaleY(fromScaleAndTranslation.scale);
+ mRecentsView.setTranslationX(fromScaleAndTranslation.translationX);
+ mRecentsView.setTranslationY(fromScaleAndTranslation.translationY);
+ mRecentsView.setContentAlpha(1);
+ mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
// As we drag right, animate the following properties:
// - RecentsView translationX
// - OverviewScrim
AnimatorSet xOverviewAnim = new AnimatorSet();
- xOverviewAnim.play(ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_X,
+ xOverviewAnim.play(ObjectAnimator.ofFloat(mRecentsView, View.TRANSLATION_X,
toScaleAndTranslation.translationX));
xOverviewAnim.play(ObjectAnimator.ofFloat(
mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
@@ -261,11 +272,11 @@
// - RecentsView scale
// - RecentsView fullscreenProgress
AnimatorSet yAnimation = new AnimatorSet();
- Animator translateYAnim = ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_Y,
+ Animator translateYAnim = ObjectAnimator.ofFloat(mRecentsView, View.TRANSLATION_Y,
toScaleAndTranslation.translationY);
- Animator scaleAnim = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
+ Animator scaleAnim = ObjectAnimator.ofFloat(mRecentsView, SCALE_PROPERTY,
toScaleAndTranslation.scale);
- Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS,
+ Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(mRecentsView, FULLSCREEN_PROGRESS,
fromState.getOverviewFullscreenProgress(), toState.getOverviewFullscreenProgress());
scaleAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
fullscreenProgressAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
@@ -466,5 +477,6 @@
mYOverviewAnim = null;
mIsHomeScreenVisible = true;
mSwipeDetector.finishedScrolling();
+ mRecentsView.setOnEmptyMessageUpdatedListener(null);
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index ad02de1..32855d7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -18,7 +18,7 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
@@ -286,7 +286,7 @@
}
});
}
- if (QUICKSTEP_SPRINGS.get()) {
+ if (UNSTABLE_SPRINGS.get()) {
mCurrentAnimation.dispatchOnStartWithVelocity(goingToEnd ? 1f : 0f, velocity);
}
anim.start();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 630dd70..7ff8969 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -17,6 +17,7 @@
import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
@@ -75,9 +76,11 @@
protected static final Rect TEMP_RECT = new Rect();
// Start resisting when swiping past this factor of mTransitionDragLength.
- private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
+ private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = ENABLE_OVERVIEW_ACTIONS.get()
+ ? 2.8f : 1.4f;
// This is how far down we can scale down, where 0f is full screen and 1f is recents.
- private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
+ private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = ENABLE_OVERVIEW_ACTIONS.get()
+ ? 3.6f : 1.8f;
private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
// The distance needed to drag to reach the task size in recents.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index f889bc1..6574d22 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -226,7 +226,7 @@
RecentsActivity activity = getCreatedActivity();
boolean visible = activity != null && activity.isStarted() && activity.hasWindowFocus();
return visible
- ? LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER
+ ? LauncherLogProto.ContainerType.OTHER_LAUNCHER_APP
: LauncherLogProto.ContainerType.APP;
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index 888ea9c..700feef 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -453,8 +453,10 @@
@Override
public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
- super.onRecentsAnimationCanceled(thumbnailData);
mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+
+ // Defer clearing the controller and the targets until after we've updated the state
+ super.onRecentsAnimationCanceled(thumbnailData);
}
/**
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index 8f75c79..8b5283e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -21,7 +21,7 @@
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
@@ -610,10 +610,12 @@
@Override
public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
- super.onRecentsAnimationCanceled(thumbnailData);
+ ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation");
mActivityInitListener.unregister();
mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
- ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation");
+
+ // Defer clearing the controller and the targets until after we've updated the state
+ super.onRecentsAnimationCanceled(thumbnailData);
}
@Override
@@ -971,7 +973,7 @@
}
mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
- if (QUICKSTEP_SPRINGS.get()) {
+ if (UNSTABLE_SPRINGS.get()) {
mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y);
}
mLauncherTransitionController.getAnimationPlayer().start();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewActionsFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewActionsFactory.java
deleted file mode 100644
index 6d17b27..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewActionsFactory.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import android.view.View;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Overview actions are shown in overview underneath the task snapshot. This factory class is
- * overrideable in an overlay. The {@link OverviewActions} class provides the view that should be
- * shown in the Overview.
- */
-public class OverviewActionsFactory implements ResourceBasedOverride {
-
- public static final MainThreadInitializedObject<OverviewActionsFactory> INSTANCE =
- forOverride(OverviewActionsFactory.class, R.string.overview_actions_factory_class);
-
- /** Create a new Overview Actions for interacting between the actions and overview. */
- public OverviewActions createOverviewActions() {
- return new OverviewActions();
- }
-
- /** Overlay overrideable, base class does nothing. */
- public static class OverviewActions {
- /** Get the view to show in the overview. */
- public View getView() {
- return null;
- }
- }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index b5441df..ec7cddf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -19,6 +19,9 @@
import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
import android.graphics.Matrix;
+import android.view.View;
+
+import androidx.annotation.Nullable;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseDraggingActivity;
@@ -75,6 +78,11 @@
*/
public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix) { }
+ @Nullable
+ public View getActionsView() {
+ return null;
+ }
+
/**
* Called when the overlay is no longer used.
*/
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index e7c9195..d212586 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -23,6 +23,7 @@
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.config.FeatureFlags.FAKE_LANDSCAPE_UI;
import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
@@ -500,7 +501,9 @@
base = new AssistantInputConsumer(this, newGestureState, base, mInputMonitorCompat);
}
- if (mOverscrollPlugin != null) {
+ if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()
+ && (mOverscrollPlugin != null)
+ && mOverscrollPlugin.isActive()) {
// Put the overscroll gesture as higher priority than the Assistant or base gestures
base = new OverscrollInputConsumer(this, newGestureState, base, mInputMonitorCompat,
mOverscrollPlugin);
@@ -729,6 +732,7 @@
pw.println("FeatureFlags:");
pw.println(" APPLY_CONFIG_AT_RUNTIME=" + APPLY_CONFIG_AT_RUNTIME.get());
pw.println(" QUICKSTEP_SPRINGS=" + QUICKSTEP_SPRINGS.get());
+ pw.println(" UNSTABLE_SPRINGS=" + UNSTABLE_SPRINGS.get());
pw.println(" ADAPTIVE_ICON_WINDOW_ANIM=" + ADAPTIVE_ICON_WINDOW_ANIM.get());
pw.println(" ENABLE_QUICKSTEP_LIVE_TILE=" + ENABLE_QUICKSTEP_LIVE_TILE.get());
pw.println(" ENABLE_HINTS_IN_OVERVIEW=" + ENABLE_HINTS_IN_OVERVIEW.get());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
new file mode 100644
index 0000000..6f919c1
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackNavBarTouchController.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.fallback;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
+
+/**
+ * In 0-button mode, intercepts swipe up from the nav bar on FallbackRecentsView to go home.
+ */
+public class FallbackNavBarTouchController implements TouchController {
+
+ private final RecentsActivity mActivity;
+ @Nullable
+ private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
+
+ public FallbackNavBarTouchController(RecentsActivity activity) {
+ mActivity = activity;
+ SysUINavigationMode.Mode sysUINavigationMode = SysUINavigationMode.getMode(mActivity);
+ if (sysUINavigationMode == SysUINavigationMode.Mode.NO_BUTTON) {
+ NavBarPosition navBarPosition = new NavBarPosition(sysUINavigationMode,
+ DefaultDisplay.INSTANCE.get(mActivity).getInfo());
+ mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity,
+ true /* disableHorizontalSwipe */, navBarPosition,
+ null /* onInterceptTouch */, this::onSwipeUp);
+ } else {
+ mTriggerSwipeUpTracker = null;
+ }
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ boolean cameFromNavBar = (ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0;
+ if (cameFromNavBar && mTriggerSwipeUpTracker != null) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mTriggerSwipeUpTracker.init();
+ }
+ onControllerTouchEvent(ev);
+ return mTriggerSwipeUpTracker.interceptedTouch();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onControllerTouchEvent(MotionEvent ev) {
+ if (mTriggerSwipeUpTracker != null) {
+ mTriggerSwipeUpTracker.onMotionEvent(ev);
+ return true;
+ }
+ return false;
+ }
+
+ private void onSwipeUp(boolean wasFling) {
+ mActivity.<FallbackRecentsView>getOverviewPanel().startHome();
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
index 1820729..de5fd7c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
@@ -48,7 +48,10 @@
}
public void setup() {
- mControllers = new TouchController[] { new RecentsTaskController(mActivity) };
+ mControllers = new TouchController[] {
+ new RecentsTaskController(mActivity),
+ new FallbackNavBarTouchController(mActivity),
+ };
}
@Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
index e3da98b..0a21413 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -26,14 +26,17 @@
import android.content.Context;
import android.graphics.PointF;
+import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import androidx.annotation.Nullable;
import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
import com.android.quickstep.GestureState;
import com.android.quickstep.InputConsumer;
+import com.android.quickstep.views.LauncherRecentsView;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.plugins.OverscrollPlugin;
import com.android.systemui.shared.system.InputMonitorCompat;
@@ -47,12 +50,12 @@
private static final String TAG = "OverscrollInputConsumer";
- private static final int ANGLE_THRESHOLD = 35; // Degrees
-
private final PointF mDownPos = new PointF();
private final PointF mLastPos = new PointF();
private final PointF mStartDragPos = new PointF();
+ private final int mAngleThreshold;
+ private final float mFlingThresholdPx;
private int mActivePointerId = -1;
private boolean mPassedSlop = false;
@@ -60,19 +63,28 @@
private final Context mContext;
private final GestureState mGestureState;
- @Nullable private final OverscrollPlugin mPlugin;
+ @Nullable
+ private final OverscrollPlugin mPlugin;
+ private final GestureDetector mGestureDetector;
private RecentsView mRecentsView;
public OverscrollInputConsumer(Context context, GestureState gestureState,
InputConsumer delegate, InputMonitorCompat inputMonitor, OverscrollPlugin plugin) {
super(delegate, inputMonitor);
+
+ mAngleThreshold = context.getResources()
+ .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
+ mFlingThresholdPx = context.getResources()
+ .getDimension(R.dimen.gestures_overscroll_fling_threshold);
mContext = context;
mGestureState = gestureState;
mPlugin = plugin;
float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+
mSquaredSlop = slop * slop;
+ mGestureDetector = new GestureDetector(context, new FlingGestureListener());
gestureState.getActivityInterface().createActivityInitListener(this::onActivityInit)
.register();
@@ -139,21 +151,29 @@
mPassedSlop = true;
mStartDragPos.set(mLastPos.x, mLastPos.y);
-
if (isOverscrolled()) {
setActive(ev);
+
+ if (mPlugin != null) {
+ mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
+ }
} else {
mState = STATE_DELEGATE_ACTIVE;
}
}
}
+ if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()
+ && mPlugin != null) {
+ mPlugin.onTouchTraveled(getDistancePx());
+ }
+
break;
}
case ACTION_CANCEL:
case ACTION_UP:
if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
- mPlugin.onOverscroll(getDeviceState());
+ mPlugin.onTouchEnd(getDistancePx());
}
mPassedSlop = false;
@@ -161,6 +181,10 @@
break;
}
+ if (mState != STATE_DELEGATE_ACTIVE) {
+ mGestureDetector.onTouchEvent(ev);
+ }
+
if (mState != STATE_ACTIVE) {
mDelegate.onMotionEvent(ev);
}
@@ -168,12 +192,19 @@
private boolean isOverscrolled() {
// Make sure there isn't an app to quick switch to on our right
- boolean atRightMostApp = (mRecentsView == null || mRecentsView.getRunningTaskIndex() <= 0);
+ int maxIndex = 0;
+ if ((mRecentsView instanceof LauncherRecentsView)
+ && ((LauncherRecentsView) mRecentsView).hasRecentsExtraCard()) {
+ maxIndex = 1;
+ }
+
+ boolean atRightMostApp = (mRecentsView == null
+ || mRecentsView.getRunningTaskIndex() <= maxIndex);
// Check if the gesture is within our angle threshold of horizontal
float deltaY = Math.abs(mLastPos.y - mDownPos.y);
float deltaX = mDownPos.x - mLastPos.x; // Positive if this is a gesture to the left
- boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < ANGLE_THRESHOLD;
+ boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold;
return atRightMostApp && angleInBounds;
}
@@ -193,4 +224,36 @@
return deviceState;
}
+
+ private int getDistancePx() {
+ return (int) Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+ }
+
+ private String getUnderlyingActivity() {
+ return mGestureState.getRunningTask().topActivity.flattenToString();
+ }
+
+ private class FlingGestureListener extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (isValidAngle(velocityX, -velocityY)
+ && getDistancePx() >= mFlingThresholdPx
+ && mState != STATE_DELEGATE_ACTIVE) {
+
+ if (mPlugin != null) {
+ mPlugin.onFling(-velocityX);
+ }
+ }
+ return true;
+ }
+
+ private boolean isValidAngle(float deltaX, float deltaY) {
+ float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+ // normalize so that angle is measured clockwise from horizontal in the bottom right
+ // corner and counterclockwise from horizontal in the bottom left corner
+
+ angle = angle > 90 ? 180 - angle : angle;
+ return (angle < mAngleThreshold);
+ }
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index 875ec29..ca15ca1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -15,23 +15,12 @@
*/
package com.android.quickstep.inputconsumers;
-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_UP;
-
-import static com.android.launcher3.Utilities.squaredHypot;
-
import android.content.Context;
import android.content.Intent;
-import android.graphics.PointF;
import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.Utilities;
import com.android.launcher3.logging.StatsLogUtils;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -39,31 +28,22 @@
import com.android.quickstep.InputConsumer;
import com.android.quickstep.RecentsAnimationDeviceState;
import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
import com.android.systemui.shared.system.InputMonitorCompat;
public class OverviewWithoutFocusInputConsumer implements InputConsumer {
private final Context mContext;
- private final RecentsAnimationDeviceState mDeviceState;
- private final GestureState mGestureState;
private final InputMonitorCompat mInputMonitor;
- private final boolean mDisableHorizontalSwipe;
- private final PointF mDownPos = new PointF();
- private final float mSquaredTouchSlop;
-
- private boolean mInterceptedTouch;
- private VelocityTracker mVelocityTracker;
+ private final TriggerSwipeUpTouchTracker mTriggerSwipeUpTracker;
public OverviewWithoutFocusInputConsumer(Context context,
RecentsAnimationDeviceState deviceState, GestureState gestureState,
InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
mContext = context;
- mDeviceState = deviceState;
- mGestureState = gestureState;
mInputMonitor = inputMonitor;
- mDisableHorizontalSwipe = disableHorizontalSwipe;
- mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
- mVelocityTracker = VelocityTracker.obtain();
+ mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, disableHorizontalSwipe,
+ deviceState.getNavBarPosition(), this::onInterceptTouch, this::onSwipeUp);
}
@Override
@@ -73,97 +53,31 @@
@Override
public boolean allowInterceptByParent() {
- return !mInterceptedTouch;
- }
-
- private void endTouchTracking() {
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
+ return !mTriggerSwipeUpTracker.interceptedTouch();
}
@Override
public void onMotionEvent(MotionEvent ev) {
- if (mVelocityTracker == null) {
- return;
- }
+ mTriggerSwipeUpTracker.onMotionEvent(ev);
+ }
- mVelocityTracker.addMovement(ev);
- switch (ev.getActionMasked()) {
- case ACTION_DOWN: {
- mDownPos.set(ev.getX(), ev.getY());
- break;
- }
- case ACTION_MOVE: {
- if (!mInterceptedTouch) {
- float displacementX = ev.getX() - mDownPos.x;
- float displacementY = ev.getY() - mDownPos.y;
- if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
- if (mDisableHorizontalSwipe
- && Math.abs(displacementX) > Math.abs(displacementY)) {
- // Horizontal gesture is not allowed in this region
- endTouchTracking();
- break;
- }
-
- mInterceptedTouch = true;
-
- if (mInputMonitor != null) {
- mInputMonitor.pilferPointers();
- }
- }
- }
- break;
- }
-
- case ACTION_CANCEL:
- endTouchTracking();
- break;
-
- case ACTION_UP: {
- finishTouchTracking(ev);
- endTouchTracking();
- break;
- }
+ private void onInterceptTouch() {
+ if (mInputMonitor != null) {
+ mInputMonitor.pilferPointers();
}
}
- private void finishTouchTracking(MotionEvent ev) {
- mVelocityTracker.computeCurrentVelocity(100);
- float velocityX = mVelocityTracker.getXVelocity();
- float velocityY = mVelocityTracker.getYVelocity();
- float velocity = mDeviceState.getNavBarPosition().isRightEdge()
- ? -velocityX
- : mDeviceState.getNavBarPosition().isLeftEdge()
- ? velocityX
- : -velocityY;
-
- final boolean triggerQuickstep;
- int touch = Touch.FLING;
- if (Math.abs(velocity) >= ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity()) {
- triggerQuickstep = velocity > 0;
- } else {
- float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
- float displacementY = ev.getY() - mDownPos.y;
- triggerQuickstep = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
- touch = Touch.SWIPE;
- }
-
- if (triggerQuickstep) {
- mContext.startActivity(new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
- ActiveGestureLog.INSTANCE.addLog("startQuickstep");
- BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
- int pageIndex = -1; // This number doesn't reflect workspace page index.
- // It only indicates that launcher client screen was shown.
- int containerType = StatsLogUtils.getContainerTypeFromState(activity.getCurrentState());
- activity.getUserEventDispatcher().logActionOnContainer(
- touch, Direction.UP, containerType, pageIndex);
- activity.getUserEventDispatcher().setPreviousHomeGesture(true);
- } else {
- // ignore
- }
+ private void onSwipeUp(boolean wasFling) {
+ mContext.startActivity(new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_HOME)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ ActiveGestureLog.INSTANCE.addLog("startQuickstep");
+ BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
+ int pageIndex = -1; // This number doesn't reflect workspace page index.
+ // It only indicates that launcher client screen was shown.
+ int containerType = StatsLogUtils.getContainerTypeFromState(activity.getCurrentState());
+ activity.getUserEventDispatcher().logActionOnContainer(
+ wasFling ? Touch.FLING : Touch.SWIPE, Direction.UP, containerType, pageIndex);
+ activity.getUserEventDispatcher().setPreviousHomeGesture(true);
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
new file mode 100644
index 0000000..c71258b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TriggerSwipeUpTouchTracker.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+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_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Tracks motion events to determine whether a gesture on the nav bar is a swipe up.
+ */
+public class TriggerSwipeUpTouchTracker {
+
+ private final PointF mDownPos = new PointF();
+ private final float mSquaredTouchSlop;
+ private final float mMinFlingVelocity;
+ private final boolean mDisableHorizontalSwipe;
+ private final NavBarPosition mNavBarPosition;
+ private final Runnable mOnInterceptTouch;
+ private final OnSwipeUpListener mOnSwipeUp;
+
+ private boolean mInterceptedTouch;
+ private VelocityTracker mVelocityTracker;
+
+ public TriggerSwipeUpTouchTracker(Context context, boolean disableHorizontalSwipe,
+ NavBarPosition navBarPosition, Runnable onInterceptTouch,
+ OnSwipeUpListener onSwipeUp) {
+ mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+ mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+ mNavBarPosition = navBarPosition;
+ mDisableHorizontalSwipe = disableHorizontalSwipe;
+ mOnInterceptTouch = onInterceptTouch;
+ mOnSwipeUp = onSwipeUp;
+
+ init();
+ }
+
+ /**
+ * Reset some initial values to prepare for the next gesture.
+ */
+ public void init() {
+ mInterceptedTouch = false;
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ /**
+ * @return Whether we have passed the touch slop and are still tracking the gesture.
+ */
+ public boolean interceptedTouch() {
+ return mInterceptedTouch;
+ }
+
+ /**
+ * Track motion events to determine whether an atomic swipe up has occurred.
+ */
+ public void onMotionEvent(MotionEvent ev) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+
+ mVelocityTracker.addMovement(ev);
+ switch (ev.getActionMasked()) {
+ case ACTION_DOWN: {
+ mDownPos.set(ev.getX(), ev.getY());
+ break;
+ }
+ case ACTION_MOVE: {
+ if (!mInterceptedTouch) {
+ float displacementX = ev.getX() - mDownPos.x;
+ float displacementY = ev.getY() - mDownPos.y;
+ if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
+ if (mDisableHorizontalSwipe
+ && Math.abs(displacementX) > Math.abs(displacementY)) {
+ // Horizontal gesture is not allowed in this region
+ endTouchTracking();
+ break;
+ }
+
+ mInterceptedTouch = true;
+
+ if (mOnInterceptTouch != null) {
+ mOnInterceptTouch.run();
+ }
+ }
+ }
+ break;
+ }
+
+ case ACTION_CANCEL:
+ endTouchTracking();
+ break;
+
+ case ACTION_UP: {
+ onGestureEnd(ev);
+ endTouchTracking();
+ break;
+ }
+ }
+ }
+
+ private void endTouchTracking() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private void onGestureEnd(MotionEvent ev) {
+ mVelocityTracker.computeCurrentVelocity(1000);
+ float velocityX = mVelocityTracker.getXVelocity();
+ float velocityY = mVelocityTracker.getYVelocity();
+ float velocity = mNavBarPosition.isRightEdge()
+ ? -velocityX
+ : mNavBarPosition.isLeftEdge()
+ ? velocityX
+ : -velocityY;
+
+ final boolean wasFling = Math.abs(velocity) >= mMinFlingVelocity;
+ final boolean isSwipeUp;
+ if (wasFling) {
+ isSwipeUp = velocity > 0;
+ } else {
+ float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
+ float displacementY = ev.getY() - mDownPos.y;
+ isSwipeUp = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
+ }
+
+ if (isSwipeUp && mOnSwipeUp != null) {
+ mOnSwipeUp.onSwipeUp(wasFling);
+ }
+ }
+
+ /**
+ * Callback when the gesture ends and was determined to be a swipe from the nav bar.
+ */
+ public interface OnSwipeUpListener {
+ /**
+ * Called on touch up if a swipe up was detected.
+ * @param wasFling Whether the swipe was a fling, or just passed touch slop at low velocity.
+ */
+ void onSwipeUp(boolean wasFling);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 82fbbc6..1bbb3f5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -378,6 +378,11 @@
}
@Override
+ public boolean hasRecentsExtraCard() {
+ return mRecentsExtraViewContainer != null;
+ }
+
+ @Override
public void setContentAlpha(float alpha) {
super.setContentAlpha(alpha);
if (mRecentsExtraViewContainer != null) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 1478034..04405a1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -30,7 +30,7 @@
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
@@ -308,6 +308,7 @@
private final Point mLastMeasureSize = new Point();
private final int mEmptyMessagePadding;
private boolean mShowEmptyMessage;
+ private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
private Layout mEmptyTextLayout;
private boolean mLiveTileOverlayAttached;
@@ -829,6 +830,11 @@
public abstract void startHome();
+ /** `true` if there is a +1 space available in overview. */
+ public boolean hasRecentsExtraCard() {
+ return false;
+ }
+
public void reset() {
setCurrentTask(-1);
mIgnoreResetTaskId = -1;
@@ -1099,13 +1105,13 @@
private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
- if (QUICKSTEP_SPRINGS.get() && taskView instanceof TaskView)
+ if (UNSTABLE_SPRINGS.get() && taskView instanceof TaskView) {
addAnim(new SpringObjectAnimator<>(taskView, VIEW_TRANSLATE_Y,
MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
SpringForce.STIFFNESS_MEDIUM,
0, -taskView.getHeight()),
duration, LINEAR, anim);
- else {
+ } else {
addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
duration, LINEAR, anim);
}
@@ -1179,7 +1185,7 @@
}
int scrollDiff = newScroll[i] - oldScroll[i] + offset;
if (scrollDiff != 0) {
- if (QUICKSTEP_SPRINGS.get() && child instanceof TaskView) {
+ if (UNSTABLE_SPRINGS.get() && child instanceof TaskView) {
addAnim(new SpringObjectAnimator<>(child, VIEW_TRANSLATE_X,
MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
SpringForce.STIFFNESS_MEDIUM,
@@ -1309,7 +1315,7 @@
}
private void dismissCurrentTask() {
- TaskView taskView = getTaskView(getNextPage());
+ TaskView taskView = getNextPageTaskView();
if (taskView != null) {
dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
}
@@ -1450,6 +1456,10 @@
return null;
}
+ public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
+ mOnEmptyMessageUpdatedListener = listener;
+ }
+
public void updateEmptyMessage() {
boolean isEmpty = getTaskViewCount() == 0;
boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
@@ -1461,6 +1471,10 @@
mShowEmptyMessage = isEmpty;
updateEmptyStateUi(hasSizeChanged);
invalidate();
+
+ if (mOnEmptyMessageUpdatedListener != null) {
+ mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage);
+ }
}
@Override
@@ -1921,4 +1935,15 @@
return !(view instanceof TaskView) && !(view instanceof ClearAllButton)
&& index <= mTaskViewStartIndex;
}
+
+ /**
+ * Used to register callbacks for when our empty message state changes.
+ *
+ * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener)
+ * @see #updateEmptyMessage()
+ */
+ public interface OnEmptyMessageUpdatedListener {
+ /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */
+ void onEmptyMessageUpdated(boolean isEmpty);
+ }
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 4e0fdea..3bc1509 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -197,6 +197,10 @@
updateThumbnailPaintFilter();
}
+ public TaskOverlay getTaskOverlay() {
+ return mOverlay;
+ }
+
public float getDimAlpha() {
return mDimAlpha;
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 94cec72..6e1b24a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -62,7 +62,6 @@
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
import com.android.launcher3.util.PendingAnimation;
import com.android.launcher3.util.ViewPool.Reusable;
-import com.android.quickstep.OverviewActionsFactory;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.TaskIconCache;
import com.android.quickstep.TaskOverlayFactory;
@@ -164,7 +163,6 @@
private final float mWindowCornerRadius;
private final BaseDraggingActivity mActivity;
- private OverviewActionsFactory.OverviewActions mOverviewActions;
@Nullable private View mActionsView;
private ObjectAnimator mIconAndDimAnimator;
@@ -222,7 +220,6 @@
mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius);
mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
- mOverviewActions = OverviewActionsFactory.INSTANCE.get(context).createOverviewActions();
mOutlineProvider = new TaskOutlineProvider(getResources(), mCurrentFullscreenParams);
setOutlineProvider(mOutlineProvider);
}
@@ -239,7 +236,7 @@
if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
- mActionsView = mOverviewActions.getView();
+ mActionsView = mSnapshotView.getTaskOverlay().getActionsView();
if (mActionsView != null) {
TaskView.LayoutParams params = new TaskView.LayoutParams(LayoutParams.MATCH_PARENT,
getResources().getDimensionPixelSize(R.dimen.overview_actions_height),
diff --git a/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml b/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml
new file mode 100644
index 0000000..cd30ef7
--- /dev/null
+++ b/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2020 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="?android:attr/dialogCornerRadius"/>
+ <solid android:color="@color/back_gesture_tutorial_primary_color"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture.xml b/quickstep/res/drawable/back_gesture.xml
new file mode 100644
index 0000000..a5c57b4
--- /dev/null
+++ b/quickstep/res/drawable/back_gesture.xml
@@ -0,0 +1,367 @@
+<!--
+ Copyright (C) 2020 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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="206dp"
+ android:height="435dp"
+ android:viewportWidth="206"
+ android:viewportHeight="435">
+ <group android:name="edgeGroup"
+ android:translateX="197"
+ android:translateY="0">
+ <path
+ android:name="edge"
+ android:fillAlpha="0"
+ android:fillType="nonZero"
+ android:fillColor="#1a73eb"
+ android:pathData=" M0,0 h9 v435 h-9 z " />
+ </group>
+ <group
+ android:name="trailGroup"
+ android:translateX="226"
+ android:translateY="200">
+ <path
+ android:name="trail"
+ android:fillAlpha="1"
+ android:fillType="nonZero"
+ android:pathData=" M0,0 h55 v36 h-55 z ">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startX="0"
+ android:endX="55"
+ android:type="linear">
+ <item
+ android:color="#991a73eb"
+ android:offset="0" />
+ <item
+ android:color="#401a73eb"
+ android:offset="0.5" />
+ <item
+ android:color="#001a73eb"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+ <group android:name="_R_G">
+ <group
+ android:name="_R_G_L_0_G_T_1"
+ android:rotation="11"
+ android:scaleX="0.9"
+ android:scaleY="0.9"
+ android:translateX="309"
+ android:translateY="422.5">
+ <group
+ android:name="_R_G_L_0_G"
+ android:translateX="-145"
+ android:translateY="-208">
+ <path
+ android:name="_R_G_L_0_G_D_0_P_0"
+ android:fillAlpha="1"
+ android:fillColor="#d2e3fc"
+ android:fillType="nonZero"
+ android:pathData=" M12.5 -47 C-7.93,-41.24 -3,-20.5 -1.5,-7 C0,6.5 2.5,22 9,39.5 C13.52,51.67 17.06,63.52 19,113 C21,164 53.5,243.5 53.5,243.5 C53.5,243.5 59,275.5 123.5,326 C188,376.5 283.5,236 290.5,199 C297.5,162 194.5,80 149,73 C103.5,66 90.5,57.5 77,50 C63.5,42.5 57,27 54.5,13.5 C52,0 43.5,-15 40,-25 C36.5,-35 32,-52.5 12.5,-47c " />
+ <path
+ android:name="_R_G_L_0_G_D_1_P_0"
+ android:pathData=" M4.45 -34.66 C4.45,-34.66 10.5,-12.66 10.5,-12.66 C11.24,-9.98 13.98,-8.38 16.67,-9.04 C16.67,-9.04 29.72,-12.27 29.72,-12.27 C32.39,-12.93 34.05,-15.59 33.47,-18.28 C33.47,-18.28 32.11,-24.57 32.11,-24.57 "
+ android:strokeWidth="4"
+ android:strokeAlpha="1"
+ android:strokeColor="#a0c2f9" />
+ <path
+ android:name="_R_G_L_0_G_D_2_P_0"
+ android:pathData=" M18.35 21.81 C21.41,17.24 36.97,10.77 44.63,13.55 "
+ android:strokeWidth="4"
+ android:strokeAlpha="1"
+ android:strokeColor="#a0c2f9" />
+ </group>
+ </group>
+ </group>
+ <group android:name="time_group" />
+ </vector>
+ </aapt:attr>
+ <target android:name="edge">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="333"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="0"
+ android:valueTo="0.2"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="917"
+ android:propertyName="fillAlpha"
+ android:startOffset="333"
+ android:valueFrom="0.2"
+ android:valueTo="0.2"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="583"
+ android:propertyName="fillAlpha"
+ android:startOffset="1250"
+ android:valueFrom="0.2"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="trail">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="2000"
+ android:propertyName="fillAlpha"
+ android:startOffset="0"
+ android:valueFrom="1"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="850"
+ android:propertyName="fillAlpha"
+ android:startOffset="2000"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="trailGroup">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="translateX"
+ android:startOffset="1250"
+ android:valueFrom="226"
+ android:valueTo="226"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1000"
+ android:propertyName="translateX"
+ android:startOffset="1333"
+ android:valueFrom="226"
+ android:valueTo="151"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="517"
+ android:propertyName="translateX"
+ android:startOffset="2333"
+ android:valueFrom="151"
+ android:valueTo="151"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="50"
+ android:propertyName="translateX"
+ android:startOffset="2850"
+ android:valueFrom="226"
+ android:valueTo="226"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_0_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1833"
+ android:propertyName="fillAlpha"
+ android:startOffset="1250"
+ android:valueFrom="1"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="167"
+ android:propertyName="fillAlpha"
+ android:startOffset="3083"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_1_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1833"
+ android:propertyName="strokeAlpha"
+ android:startOffset="1250"
+ android:valueFrom="1"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="strokeAlpha"
+ android:startOffset="3083"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_D_2_P_0">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="1833"
+ android:propertyName="strokeAlpha"
+ android:startOffset="1250"
+ android:valueFrom="1"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="strokeAlpha"
+ android:startOffset="3083"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="translateX"
+ android:startOffset="1250"
+ android:valueFrom="309"
+ android:valueTo="309"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1417"
+ android:propertyName="translateX"
+ android:startOffset="1333"
+ android:valueFrom="309"
+ android:valueTo="251"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="_R_G_L_0_G_T_1">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="83"
+ android:propertyName="rotation"
+ android:startOffset="1250"
+ android:valueFrom="11"
+ android:valueTo="11"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.277,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="1417"
+ android:propertyName="rotation"
+ android:startOffset="1333"
+ android:valueFrom="11"
+ android:valueTo="0"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.277,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="time_group">
+ <aapt:attr name="android:animation">
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="2183"
+ android:propertyName="translateX"
+ android:startOffset="1250"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml b/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml
new file mode 100644
index 0000000..d7b9102
--- /dev/null
+++ b/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2020 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/default_dialog_corner_radius"/>
+ <solid android:color="@color/back_gesture_tutorial_primary_color"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture_tutorial_close_button.xml b/quickstep/res/drawable/back_gesture_tutorial_close_button.xml
new file mode 100644
index 0000000..0702042
--- /dev/null
+++ b/quickstep/res/drawable/back_gesture_tutorial_close_button.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2020 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="13dp"
+ android:viewportHeight="14"
+ android:viewportWidth="14"
+ android:width="13dp">
+ <path
+ android:fillColor="#000000"
+ android:fillType="evenOdd"
+ android:pathData="M14,1.41L12.59,0L7,5.59L1.41,0L0,1.41L5.59,7L0,12.59L1.41,14L7,8.41L12.59,14L14,12.59L8.41,7L14,1.41Z"/>
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/layout/back_gesture_tutorial_activity.xml b/quickstep/res/layout/back_gesture_tutorial_activity.xml
new file mode 100644
index 0000000..e894e89
--- /dev/null
+++ b/quickstep/res/layout/back_gesture_tutorial_activity.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright (C) 2020 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/back_gesture_tutorial_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/back_gesture_tutorial_fragment.xml b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
new file mode 100644
index 0000000..294e46e
--- /dev/null
+++ b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
@@ -0,0 +1,121 @@
+<!--
+ Copyright (C) 2020 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layerType="software"
+ android:background="@color/back_gesture_tutorial_background_color">
+ <!--The layout is rendered on the software layer to avoid b/136158117-->
+
+ <ImageView
+ android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"/>
+
+ <ImageButton
+ android:id="@+id/back_gesture_tutorial_fragment_close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="18dp"
+ android:layout_marginTop="30dp"
+ android:layout_marginStart="4dp"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="@android:color/transparent"
+ android:accessibilityTraversalAfter="@id/back_gesture_tutorial_fragment_titles_container"
+ android:contentDescription="@string/back_gesture_tutorial_close_button_content_description"
+ android:src="@drawable/back_gesture_tutorial_close_button"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="70dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/back_gesture_tutorial_fragment_titles_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:focusable="true">
+
+ <TextView
+ android:id="@+id/back_gesture_tutorial_fragment_title_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginStart="@dimen/back_gesture_tutorial_title_margin_start_end"
+ android:layout_marginEnd="@dimen/back_gesture_tutorial_title_margin_start_end"
+ style="@style/TextAppearance.BackGestureTutorial.Title"/>
+
+ <TextView
+ android:id="@+id/back_gesture_tutorial_fragment_subtitle_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="10dp"
+ android:layout_marginStart="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
+ android:layout_marginEnd="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
+ style="@style/TextAppearance.BackGestureTutorial.Subtitle"/>
+
+ </LinearLayout>
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_weight="1"
+ android:layout_height="0dp"
+ android:layout_marginTop="48dp"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"/>
+
+ <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
+ of elevation and shadow) which is replaced by ripple effect in android:foreground -->
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="46dp"
+ android:layout_marginBottom="48dp"
+ android:layout_gravity="center_horizontal">
+
+ <Button
+ android:id="@+id/back_gesture_tutorial_fragment_action_button"
+ android:layout_width="142dp"
+ android:layout_height="49dp"
+ android:layout_marginEnd="@dimen/back_gesture_tutorial_button_margin_start_end"
+ android:layout_alignParentEnd="true"
+ android:stateListAnimator="@null"
+ android:background="@drawable/back_gesture_tutorial_action_button_background"
+ android:foreground="?android:attr/selectableItemBackgroundBorderless"
+ style="@style/TextAppearance.BackGestureTutorial.ButtonLabel"/>
+
+ <Button
+ android:id="@+id/back_gesture_tutorial_fragment_action_text_button"
+ android:layout_width="142dp"
+ android:layout_height="49dp"
+ android:layout_marginStart="@dimen/back_gesture_tutorial_button_margin_start_end"
+ android:layout_alignParentStart="true"
+ android:stateListAnimator="@null"
+ android:background="@null"
+ android:foreground="?android:attr/selectableItemBackgroundBorderless"
+ style="@style/TextAppearance.BackGestureTutorial.TextButtonLabel"/>
+
+ </RelativeLayout>
+
+ </LinearLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 24ab487..327bb14 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -15,8 +15,6 @@
-->
<resources>
<string name="task_overlay_factory_class" translatable="false"></string>
- <!-- Class name for factory object that creates the overview actions UI when enabled. -->
- <string name="overview_actions_factory_class" translatable="false" />
<!-- Activity which blocks home gesture -->
<string name="gesture_blocking_activity" translatable="false"></string>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 9ff1350..988c78d 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -77,4 +77,12 @@
<!-- Distance to move elements when swiping up to go home from launcher -->
<dimen name="home_pullback_distance">28dp</dimen>
+
+ <!-- Overscroll Gesture -->
+ <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
+
+ <!-- Tips Gesture Tutorial -->
+ <dimen name="back_gesture_tutorial_title_margin_start_end">40dp</dimen>
+ <dimen name="back_gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
+ <dimen name="back_gesture_tutorial_button_margin_start_end">18dp</dimen>
</resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 4319b5d..5f81e1f 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -29,9 +29,6 @@
<!-- Title for an option to enter freeform mode for a given app -->
<string name="recent_task_option_freeform">Freeform</string>
- <!-- Content description for the recent apps panel (not shown on the screen). [CHAR LIMIT=NONE] -->
- <string name="accessibility_desc_recent_apps">Overview</string>
-
<!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
<string name="recents_empty_message">No recent items</string>
@@ -66,5 +63,49 @@
<!-- Text of the tip when user lands in all apps view for the first time, indicating where the tip toast points to is the predicted apps section. [CHAR_LIMIT=50] -->
<string name="all_apps_prediction_tip">Your predicted apps</string>
+ <!-- Content description for a close button. [CHAR LIMIT=NONE] -->
+ <string name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
+ <!-- Hotseat migration notification title -->
+ <string translatable="false" name="hotseat_migrate_prompt_title">Get suggested apps on the home screen</string>
+ <!-- Hotseat migration notification content -->
+ <string translatable="false" name="hotseat_migrate_prompt_content">Tap to set up</string>
+ <!-- Hotseat migration wizard title -->
+ <string translatable="false" name="hotseat_migrate_title">Suggested apps replace the bottom row of apps</string>
+ <!-- Hotseat migration wizard message -->
+ <string translatable="false" name="hotseat_migrate_message">To pin a favorite app, drag it over a suggested app. To hide a suggested app, touch & hold it.</string>
+ <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
+ <string translatable="false" name="hotseat_items_migrated">Bottom row of apps moved to last screen</string>
+ <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
+ <string translatable="false" name="hotseat_no_migration">Bottom row won\'t be replaced. Manually drag apps for predictions.</string>
+ <!-- Button text to opt in for fully predicted hotseat -->
+ <string translatable="false" name="hotseat_migrate_accept">Turn On</string>
+ <!-- Button text to dismiss opt in for fully predicted hotseat -->
+ <string translatable="false" name="hotseat_migrate_dismiss">No thanks</string>
+ <!-- Hotseat onboard notification title -->
+ <string translatable="false" name="hotseat_onboard_notification_title">Your hotseat just got smarter</string>
+ <!-- Hotseat onboard notification detail -->
+ <string translatable="false" name="hotseat_onboard_notification_detail">Tap here to set it up</string>
+
+
+
+ <!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
+ <string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
+ <!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->
+ <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge" translatable="false">Start at the right edge and swipe toward the middle</string>
+
+ <!-- Title shown during interactive part of Back gesture tutorial for left edge. [CHAR LIMIT=30] -->
+ <string name="back_gesture_tutorial_playground_title_swipe_inward_left_edge" translatable="false">Try the other side</string>
+ <!-- Subtitle shown during interactive parts of Back gesture tutorial for left edge. [CHAR LIMIT=60] -->
+ <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge" translatable="false">That\'s it! Now try swiping from the left edge.</string>
+
+ <!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] -->
+ <string name="back_gesture_tutorial_confirm_title" translatable="false">All set</string>
+ <!-- Subtitle shown on the confirmation screen after successful gesture. [CHAR LIMIT=60] -->
+ <string name="back_gesture_tutorial_confirm_subtitle" translatable="false">To change the sensitivity of the back gesture, go to Settings</string>
+
+ <!-- Button text shown on a button on the confirm screen. [CHAR LIMIT=14] -->
+ <string name="back_gesture_tutorial_action_button_label" translatable="false">Done</string>
+ <!-- Button text shown on a text button on the confirm screen. [CHAR LIMIT=14] -->
+ <string name="back_gesture_tutorial_action_text_button_label" translatable="false">Settings</string>
</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index bb364ff..c8d7777 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -25,4 +25,39 @@
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
</style>
+
+ <style name="TextAppearance.BackGestureTutorial"
+ parent="android:TextAppearance.Material.Body1" />
+
+ <style name="TextAppearance.BackGestureTutorial.CallToAction"
+ parent="android:TextAppearance.Material.Body2" />
+
+ <style name="TextAppearance.BackGestureTutorial.Title"
+ parent="TextAppearance.BackGestureTutorial">
+ <item name="android:gravity">center</item>
+ <item name="android:textColor">@color/back_gesture_tutorial_title_color</item>
+ <item name="android:textSize">28sp</item>
+ </style>
+
+ <style name="TextAppearance.BackGestureTutorial.Subtitle"
+ parent="TextAppearance.BackGestureTutorial">
+ <item name="android:gravity">center</item>
+ <item name="android:textColor">@color/back_gesture_tutorial_subtitle_color</item>
+ <item name="android:letterSpacing">0.03</item>
+ <item name="android:textSize">21sp</item>
+ </style>
+
+ <style name="TextAppearance.BackGestureTutorial.ButtonLabel"
+ parent="TextAppearance.BackGestureTutorial.CallToAction">
+ <item name="android:gravity">center</item>
+ <item name="android:textColor">@color/back_gesture_tutorial_action_button_label_color</item>
+ <item name="android:letterSpacing">0.02</item>
+ <item name="android:textSize">16sp</item>
+ <item name="android:textAllCaps">false</item>
+ </style>
+
+ <style name="TextAppearance.BackGestureTutorial.TextButtonLabel"
+ parent="TextAppearance.BackGestureTutorial.ButtonLabel">
+ <item name="android:textColor">@color/back_gesture_tutorial_primary_color</item>
+ </style>
</resources>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index d4db05a..2df490e 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -69,9 +69,9 @@
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.RemoteAnimationTargets;
import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.RemoteAnimationTargets;
import com.android.systemui.shared.system.ActivityCompat;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.QuickStepContract;
@@ -83,6 +83,8 @@
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
import com.android.systemui.shared.system.WindowManagerWrapper;
+import java.lang.ref.WeakReference;
+
/**
* {@link LauncherAppTransitionManager} with Quickstep-specific app transitions for launching from
* home and/or all-apps.
@@ -149,6 +151,9 @@
private DeviceProfile mDeviceProfile;
private RemoteAnimationProvider mRemoteAnimationProvider;
+ // Strong refs to runners which are cleared when the launcher activity is destroyed
+ private WrappedAnimationRunnerImpl mWallpaperOpenRunner;
+ private WrappedAnimationRunnerImpl mAppLaunchRunner;
private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() {
@Override
@@ -176,7 +181,6 @@
mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y);
mLauncher.addOnDeviceProfileChangeListener(this);
- registerRemoteAnimations();
}
@Override
@@ -198,32 +202,9 @@
public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
if (hasControlRemoteAppTransitionPermission()) {
boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
- RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler,
- true /* startAtFrontOfQueue */) {
-
- @Override
- public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
- AnimatorSet anim = new AnimatorSet();
-
- boolean launcherClosing =
- launcherIsATargetWithMode(appTargets, MODE_CLOSING);
-
- if (isLaunchingFromRecents(v, appTargets)) {
- composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets,
- launcherClosing);
- } else {
- composeIconLaunchAnimator(anim, v, appTargets, wallpaperTargets,
- launcherClosing);
- }
-
- if (launcherClosing) {
- anim.addListener(mForceInvisibleListener);
- }
-
- result.setAnimation(anim, mLauncher);
- }
- };
+ mAppLaunchRunner = new AppLaunchAnimationRunner(mHandler, v);
+ RemoteAnimationRunnerCompat runner = new WrappedLauncherAnimationRunner<>(
+ mAppLaunchRunner, true /* startAtFrontOfQueue */);
// Note that this duration is a guess as we do not know if the animation will be a
// recents launch or not for sure until we know the opening app targets.
@@ -598,18 +579,37 @@
/**
* Registers remote animations used when closing apps to home screen.
*/
- private void registerRemoteAnimations() {
- // Unregister this
+ @Override
+ public void registerRemoteAnimations() {
if (hasControlRemoteAppTransitionPermission()) {
+ mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
+
RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat();
definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN,
WindowManagerWrapper.ACTIVITY_TYPE_STANDARD,
- new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(false /* fromUnlock */),
+ new RemoteAnimationAdapterCompat(
+ new WrappedLauncherAnimationRunner<>(mWallpaperOpenRunner,
+ false /* startAtFrontOfQueue */),
CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
new ActivityCompat(mLauncher).registerRemoteAnimations(definition);
}
}
+ /**
+ * Unregisters all remote animations.
+ */
+ @Override
+ public void unregisterRemoteAnimations() {
+ if (hasControlRemoteAppTransitionPermission()) {
+ new ActivityCompat(mLauncher).unregisterRemoteAnimations();
+
+ // Also clear strong references to the runners registered with the remote animation
+ // definition so we don't have to wait for the system gc
+ mWallpaperOpenRunner = null;
+ mAppLaunchRunner = null;
+ }
+ }
+
private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) {
return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode);
}
@@ -618,9 +618,8 @@
* @return Runner that plays when user goes to Launcher
* ie. pressing home, swiping up from nav bar.
*/
- RemoteAnimationRunnerCompat getWallpaperOpenRunner(boolean fromUnlock) {
- return new WallpaperOpenLauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */,
- fromUnlock);
+ WrappedAnimationRunnerImpl createWallpaperOpenRunner(boolean fromUnlock) {
+ return new WallpaperOpenLauncherAnimationRunner(mHandler, fromUnlock);
}
/**
@@ -701,7 +700,8 @@
}
/**
- * Creates an animator that modifies Launcher as a result from {@link #getWallpaperOpenRunner}.
+ * Creates an animator that modifies Launcher as a result from
+ * {@link #createWallpaperOpenRunner}.
*/
private void createLauncherResumeAnimation(AnimatorSet anim) {
if (mLauncher.isInState(LauncherState.ALL_APPS)) {
@@ -761,18 +761,70 @@
}
/**
+ * Used with WrappedLauncherAnimationRunner as an interface for the runner to call back to the
+ * implementation.
+ */
+ protected interface WrappedAnimationRunnerImpl {
+ Handler getHandler();
+ void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ LauncherAnimationRunner.AnimationResult result);
+ }
+
+ /**
+ * This class is needed to wrap any animation runner that is a part of the
+ * RemoteAnimationDefinition:
+ * - Launcher creates a new instance of the LauncherAppTransitionManagerImpl whenever it is
+ * created, which in turn registers a new definition
+ * - When the definition is registered, window manager retains a strong binder reference to the
+ * runner passed in
+ * - If the Launcher activity is recreated, the new definition registered will replace the old
+ * reference in the system's activity record, but until the system server is GC'd, the binder
+ * reference will still exist, which references the runner in the Launcher process, which
+ * references the (old) Launcher activity through this class
+ *
+ * Instead we make the runner provided to the definition static only holding a weak reference to
+ * the runner implementation. When this animation manager is destroyed, we remove the Launcher
+ * reference to the runner, leaving only the weak ref from the runner.
+ */
+ protected static class WrappedLauncherAnimationRunner<R extends WrappedAnimationRunnerImpl>
+ extends LauncherAnimationRunner {
+ private WeakReference<R> mImpl;
+
+ public WrappedLauncherAnimationRunner(R animationRunnerImpl, boolean startAtFrontOfQueue) {
+ super(animationRunnerImpl.getHandler(), startAtFrontOfQueue);
+ mImpl = new WeakReference<>(animationRunnerImpl);
+ }
+
+ @Override
+ public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+ R animationRunnerImpl = mImpl.get();
+ if (animationRunnerImpl != null) {
+ animationRunnerImpl.onCreateAnimation(appTargets, wallpaperTargets, result);
+ }
+ }
+ }
+
+ /**
* Remote animation runner for animation from the app to Launcher, including recents.
*/
- class WallpaperOpenLauncherAnimationRunner extends LauncherAnimationRunner {
+ protected class WallpaperOpenLauncherAnimationRunner implements WrappedAnimationRunnerImpl {
+
+ private final Handler mHandler;
private final boolean mFromUnlock;
- public WallpaperOpenLauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue,
- boolean fromUnlock) {
- super(handler, startAtFrontOfQueue);
+ public WallpaperOpenLauncherAnimationRunner(Handler handler, boolean fromUnlock) {
+ mHandler = handler;
mFromUnlock = fromUnlock;
}
@Override
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ @Override
public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
RemoteAnimationTargetCompat[] wallpaperTargets,
LauncherAnimationRunner.AnimationResult result) {
@@ -842,4 +894,47 @@
result.setAnimation(anim, mLauncher);
}
}
+
+ /**
+ * Remote animation runner for animation to launch an app.
+ */
+ private class AppLaunchAnimationRunner implements WrappedAnimationRunnerImpl {
+
+ private final Handler mHandler;
+ private final View mV;
+
+ AppLaunchAnimationRunner(Handler handler, View v) {
+ mHandler = handler;
+ mV = v;
+ }
+
+ @Override
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ @Override
+ public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+ RemoteAnimationTargetCompat[] wallpaperTargets,
+ LauncherAnimationRunner.AnimationResult result) {
+ AnimatorSet anim = new AnimatorSet();
+
+ boolean launcherClosing =
+ launcherIsATargetWithMode(appTargets, MODE_CLOSING);
+
+ if (isLaunchingFromRecents(mV, appTargets)) {
+ composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
+ launcherClosing);
+ } else {
+ composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
+ launcherClosing);
+ }
+
+ if (launcherClosing) {
+ anim.addListener(mForceInvisibleListener);
+ }
+
+ result.setAnimation(anim, mLauncher);
+ }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
new file mode 100644
index 0000000..3c3f397
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.provider.DeviceConfig;
+
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
+
+@TargetApi(Build.VERSION_CODES.P)
+public class DeviceFlag extends DebugFlag {
+
+ public static final String NAMESPACE_LAUNCHER = "launcher";
+
+ private final boolean mDefaultValueInCode;
+
+ public DeviceFlag(String key, boolean defaultValue, String description) {
+ super(key, getDeviceValue(key, defaultValue), description);
+ mDefaultValueInCode = defaultValue;
+ }
+
+ @Override
+ protected StringBuilder appendProps(StringBuilder src) {
+ return super.appendProps(src).append(", mDefaultValueInCode=").append(mDefaultValueInCode);
+ }
+
+ @Override
+ public void addChangeListener(Context context, Runnable r) {
+ DeviceConfig.addOnPropertiesChangedListener(
+ NAMESPACE_LAUNCHER,
+ context.getMainExecutor(),
+ properties -> {
+ if (!NAMESPACE_LAUNCHER.equals(properties.getNamespace())) {
+ return;
+ }
+ defaultValue = getDeviceValue(key, mDefaultValueInCode);
+ initialize(context);
+ r.run();
+ });
+ }
+
+ protected static boolean getDeviceValue(String key, boolean defaultValue) {
+ return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, defaultValue);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java b/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java
deleted file mode 100644
index 27d81ef..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/TogglableFlag.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import android.content.Context;
-import android.provider.DeviceConfig;
-
-import com.android.launcher3.config.FeatureFlags.BaseTogglableFlag;
-
-public class TogglableFlag extends BaseTogglableFlag {
- public static final String NAMESPACE_LAUNCHER = "launcher";
- public static final String TAG = "TogglableFlag";
-
- public TogglableFlag(String key, boolean defaultValue, String description) {
- super(key, defaultValue, description);
- }
-
- @Override
- public boolean getOverridenDefaultValue(boolean value) {
- return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, getKey(), value);
- }
-
- @Override
- public void addChangeListener(Context context, Runnable r) {
- DeviceConfig.addOnPropertiesChangedListener(
- NAMESPACE_LAUNCHER,
- context.getMainExecutor(),
- (properties) -> {
- if (!NAMESPACE_LAUNCHER.equals(properties.getNamespace())) {
- return;
- }
- initialize(context);
- r.run();
- });
- }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 99b2a81..d5ce734 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -25,7 +25,7 @@
import static com.android.launcher3.anim.Interpolators.ACCEL;
import static com.android.launcher3.anim.Interpolators.DEACCEL;
import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import android.animation.TimeInterpolator;
@@ -277,7 +277,7 @@
private void handleFirstSwipeToOverview(final ValueAnimator animator,
final long expectedDuration, final LauncherState targetState, final float velocity,
final boolean isFling) {
- if (QUICKSTEP_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
+ if (UNSTABLE_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
&& targetState == OVERVIEW) {
mFinishFastOnSecondTouch = true;
} else if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 5539b3e..139721b 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -18,6 +18,8 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -25,6 +27,7 @@
import android.os.RemoteException;
import android.util.Log;
import android.view.MotionEvent;
+
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.quickstep.util.SharedApiCompat;
import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -266,6 +269,12 @@
}
}
+ @Override
+ public void handleImageAsScreenshot(
+ Bitmap screenImage, Rect locationInScreen, Insets visibleInsets, int taskId) {
+
+ }
+
/**
* See SharedApiCompat#setShelfHeight()
*/
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java
new file mode 100644
index 0000000..295ab48
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+import java.util.Optional;
+
+/** Shows the Back gesture interactive tutorial in full screen mode. */
+public class BackGestureTutorialActivity extends FragmentActivity {
+
+ Optional<BackGestureTutorialFragment> mFragment = Optional.empty();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.back_gesture_tutorial_activity);
+
+ mFragment = Optional.of(BackGestureTutorialFragment.newInstance(TutorialStep.ENGAGED,
+ TutorialType.RIGHT_EDGE_BACK_NAVIGATION));
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.back_gesture_tutorial_fragment_container, mFragment.get())
+ .commit();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ hideSystemUI();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mFragment.isPresent()) {
+ mFragment.get().onBackPressed();
+ }
+ }
+
+ private void hideSystemUI() {
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_FULLSCREEN);
+ getWindow().setNavigationBarColor(Color.TRANSPARENT);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
new file mode 100644
index 0000000..486d676
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
+
+import java.util.Optional;
+
+/**
+ * An implementation of {@link BackGestureTutorialController} that defines the behavior of the
+ * {@link TutorialStep#CONFIRM}.
+ */
+final class BackGestureTutorialConfirmController extends BackGestureTutorialController {
+
+ BackGestureTutorialConfirmController(BackGestureTutorialFragment fragment,
+ BackGestureTutorialTypeInfo tutorialTypeInfo) {
+ super(fragment, TutorialStep.CONFIRM, Optional.of(tutorialTypeInfo));
+ }
+
+ @Override
+ Optional<Integer> getTitleStringId() {
+ return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmTitleId());
+ }
+
+ @Override
+ Optional<Integer> getSubtitleStringId() {
+ return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmSubtitleId());
+ }
+
+ @Override
+ Optional<Integer> getActionButtonStringId() {
+ return Optional.of(R.string.back_gesture_tutorial_action_button_label);
+ }
+
+ @Override
+ Optional<Integer> getActionTextButtonStringId() {
+ return Optional.of(R.string.back_gesture_tutorial_action_text_button_label);
+ }
+
+ @Override
+ void onActionButtonClicked(View button) {
+ hideHandCoachingAnimation();
+ if (button == mActionTextButton) {
+ mFragment.startSystemNavigationSetting();
+ }
+ mFragment.closeTutorial();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
new file mode 100644
index 0000000..3fe91a3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+import java.util.Optional;
+
+/**
+ * Defines the behavior of the particular {@link TutorialStep} and implements the transition to it.
+ */
+abstract class BackGestureTutorialController {
+
+ final BackGestureTutorialFragment mFragment;
+ final TutorialStep mTutorialStep;
+ final Optional<BackGestureTutorialTypeInfo> mTutorialTypeInfo;
+ final Button mActionTextButton;
+ final Button mActionButton;
+ final TextView mSubtitleTextView;
+ final ImageButton mCloseButton;
+ final BackGestureTutorialHandAnimation mHandCoachingAnimation;
+ final LinearLayout mTitlesContainer;
+
+ private final TextView mTitleTextView;
+ private final ImageView mHandCoachingView;
+
+ BackGestureTutorialController(
+ BackGestureTutorialFragment fragment,
+ TutorialStep tutorialStep,
+ Optional<BackGestureTutorialTypeInfo> tutorialTypeInfo) {
+ mFragment = fragment;
+ mTutorialStep = tutorialStep;
+ mTutorialTypeInfo = tutorialTypeInfo;
+
+ View rootView = fragment.getRootView();
+ mActionTextButton = rootView.findViewById(
+ R.id.back_gesture_tutorial_fragment_action_text_button);
+ mActionButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_action_button);
+ mSubtitleTextView = rootView.findViewById(
+ R.id.back_gesture_tutorial_fragment_subtitle_view);
+ mTitleTextView = rootView.findViewById(R.id.back_gesture_tutorial_fragment_title_view);
+ mHandCoachingView = rootView.findViewById(
+ R.id.back_gesture_tutorial_fragment_hand_coaching);
+ mHandCoachingAnimation = mFragment.getHandAnimation();
+ mHandCoachingView.bringToFront();
+ mCloseButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button);
+ mTitlesContainer = rootView.findViewById(
+ R.id.back_gesture_tutorial_fragment_titles_container);
+ }
+
+ void transitToController() {
+ updateTitles();
+ updateActionButtons();
+ }
+
+ void hideHandCoachingAnimation() {
+ mHandCoachingAnimation.stop();
+ }
+
+ void onGestureDetected() {
+ hideHandCoachingAnimation();
+
+ if (mTutorialStep == TutorialStep.CONFIRM) {
+ mFragment.closeTutorial();
+ return;
+ }
+
+ if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) {
+ mFragment.changeController(TutorialStep.ENGAGED,
+ TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+ return;
+ }
+
+ mFragment.changeController(TutorialStep.CONFIRM);
+ }
+
+ abstract Optional<Integer> getTitleStringId();
+
+ abstract Optional<Integer> getSubtitleStringId();
+
+ abstract Optional<Integer> getActionButtonStringId();
+
+ abstract Optional<Integer> getActionTextButtonStringId();
+
+ abstract void onActionButtonClicked(View button);
+
+ private void updateActionButtons() {
+ updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
+ updateButton(mActionTextButton, getActionTextButtonStringId(), this::onActionButtonClicked);
+ }
+
+ private static void updateButton(Button button, Optional<Integer> stringId,
+ View.OnClickListener listener) {
+ if (!stringId.isPresent()) {
+ button.setVisibility(View.INVISIBLE);
+ return;
+ }
+
+ button.setVisibility(View.VISIBLE);
+ button.setText(stringId.get());
+ button.setOnClickListener(listener);
+ }
+
+ private void updateTitles() {
+ updateTitleView(mTitleTextView, getTitleStringId(),
+ R.style.TextAppearance_BackGestureTutorial_Title);
+ updateTitleView(mSubtitleTextView, getSubtitleStringId(),
+ R.style.TextAppearance_BackGestureTutorial_Subtitle);
+ }
+
+ private static void updateTitleView(TextView textView, Optional<Integer> stringId,
+ int styleId) {
+ if (!stringId.isPresent()) {
+ textView.setVisibility(View.GONE);
+ return;
+ }
+
+ textView.setVisibility(View.VISIBLE);
+ textView.setText(stringId.get());
+ textView.setTextAppearance(styleId);
+ }
+
+ /**
+ * Constructs {@link BackGestureTutorialController} for providing {@link TutorialType} and
+ * {@link TutorialStep}.
+ */
+ static Optional<BackGestureTutorialController> getTutorialController(
+ BackGestureTutorialFragment fragment, TutorialStep tutorialStep,
+ TutorialType tutorialType) {
+ BackGestureTutorialTypeInfo tutorialTypeInfo =
+ BackGestureTutorialTypeInfoProvider.getTutorialTypeInfo(tutorialType);
+ switch (tutorialStep) {
+ case ENGAGED:
+ return Optional.of(
+ new BackGestureTutorialEngagedController(fragment, tutorialTypeInfo));
+ case CONFIRM:
+ return Optional.of(
+ new BackGestureTutorialConfirmController(fragment, tutorialTypeInfo));
+ default:
+ throw new AssertionError("Unexpected tutorial step: " + tutorialStep);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
new file mode 100644
index 0000000..c9ee1e2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.view.View;
+
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
+
+import java.util.Optional;
+
+/**
+ * An implementation of {@link BackGestureTutorialController} that defines the behavior of the
+ * {@link TutorialStep#ENGAGED}.
+ */
+final class BackGestureTutorialEngagedController extends BackGestureTutorialController {
+
+ BackGestureTutorialEngagedController(
+ BackGestureTutorialFragment fragment, BackGestureTutorialTypeInfo tutorialTypeInfo) {
+ super(fragment, TutorialStep.ENGAGED, Optional.of(tutorialTypeInfo));
+ }
+
+ @Override
+ void transitToController() {
+ super.transitToController();
+ mHandCoachingAnimation.maybeStartLoopedAnimation(mTutorialTypeInfo.get().getTutorialType());
+ }
+
+ @Override
+ Optional<Integer> getTitleStringId() {
+ return Optional.of(mTutorialTypeInfo.get().getTutorialPlaygroundTitleId());
+ }
+
+ @Override
+ Optional<Integer> getSubtitleStringId() {
+ return Optional.of(mTutorialTypeInfo.get().getTutorialEngagedSubtitleId());
+ }
+
+ @Override
+ Optional<Integer> getActionButtonStringId() {
+ return Optional.empty();
+ }
+
+ @Override
+ Optional<Integer> getActionTextButtonStringId() {
+ return Optional.empty();
+ }
+
+ @Override
+ void onActionButtonClicked(View button) {
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
new file mode 100644
index 0000000..54408ce
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.fragment.app.Fragment;
+
+import com.android.launcher3.R;
+
+import java.net.URISyntaxException;
+import java.util.Optional;
+
+/** Shows the Back gesture interactive tutorial. */
+public class BackGestureTutorialFragment extends Fragment {
+
+ private static final String LOG_TAG = "TutorialFragment";
+ private static final String KEY_TUTORIAL_STEP = "tutorialStep";
+ private static final String KEY_TUTORIAL_TYPE = "tutorialType";
+ private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
+ "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S"
+ + ".:settings:fragment_args_key=gesture_system_navigation_input_summary;S"
+ + ".:settings:show_fragment=com.android.settings.gestures"
+ + ".SystemNavigationGestureSettings;end";
+
+ private TutorialStep mTutorialStep;
+ private TutorialType mTutorialType;
+ private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
+ private View mRootView;
+ private BackGestureTutorialHandAnimation mHandCoachingAnimation;
+
+ public static BackGestureTutorialFragment newInstance(
+ TutorialStep tutorialStep, TutorialType tutorialType) {
+ BackGestureTutorialFragment fragment = new BackGestureTutorialFragment();
+ Bundle args = new Bundle();
+ args.putSerializable(KEY_TUTORIAL_STEP, tutorialStep);
+ args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
+ mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
+ mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+
+ mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
+ container, /* attachToRoot= */ false);
+ mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
+ .setOnClickListener(this::onCloseButtonClicked);
+ mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
+
+ return mRootView;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ changeController(mTutorialStep, mTutorialType);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mHandCoachingAnimation.stop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
+ savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ View getRootView() {
+ return mRootView;
+ }
+
+ BackGestureTutorialHandAnimation getHandAnimation() {
+ return mHandCoachingAnimation;
+ }
+
+ void changeController(TutorialStep tutorialStep) {
+ changeController(tutorialStep, mTutorialType);
+ }
+
+ void changeController(TutorialStep tutorialStep, TutorialType tutorialType) {
+ Optional<BackGestureTutorialController> tutorialController =
+ BackGestureTutorialController.getTutorialController(/* fragment= */ this,
+ tutorialStep, tutorialType);
+ if (!tutorialController.isPresent()) {
+ return;
+ }
+
+ mTutorialController = tutorialController;
+ mTutorialController.get().transitToController();
+ this.mTutorialStep = mTutorialController.get().mTutorialStep;
+ this.mTutorialType = tutorialType;
+ }
+
+ void onBackPressed() {
+ if (mTutorialController.isPresent()) {
+ mTutorialController.get().onGestureDetected();
+ }
+ }
+
+ void closeTutorial() {
+ getActivity().finish();
+ }
+
+ void startSystemNavigationSetting() {
+ try {
+ startActivityForResult(
+ Intent.parseUri(SYSTEM_NAVIGATION_SETTING_INTENT, /* flags= */ 0),
+ /* requestCode= */ 0);
+ } catch (URISyntaxException e) {
+ Log.e(LOG_TAG, "The launch Intent Uri is wrong syntax: " + e);
+ } catch (ActivityNotFoundException e) {
+ Log.e(LOG_TAG, "The launch Activity not found: " + e);
+ }
+ }
+
+ private void onCloseButtonClicked(View button) {
+ closeTutorial();
+ }
+
+ /** Denotes the step of the tutorial. */
+ enum TutorialStep {
+ ENGAGED,
+ CONFIRM,
+ }
+
+ /** Denotes the type of the tutorial. */
+ enum TutorialType {
+ RIGHT_EDGE_BACK_NAVIGATION,
+ LEFT_EDGE_BACK_NAVIGATION,
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
new file mode 100644
index 0000000..d03811d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.Context;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+import java.time.Duration;
+
+/** Hand coaching animation. */
+final class BackGestureTutorialHandAnimation {
+
+ // A delay for waiting the Activity fully launches.
+ private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
+
+ private final ImageView mHandCoachingView;
+ private final AnimatedVectorDrawable mGestureAnimation;
+
+ private boolean mIsAnimationPlayed = false;
+
+ BackGestureTutorialHandAnimation(Context context, View rootView) {
+ mHandCoachingView = rootView.findViewById(
+ R.id.back_gesture_tutorial_fragment_hand_coaching);
+ mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context,
+ R.drawable.back_gesture);
+ }
+
+ boolean isRunning() {
+ return mGestureAnimation.isRunning();
+ }
+
+ /**
+ * Starts animation if the playground is launched for the first time.
+ */
+ void maybeStartLoopedAnimation(TutorialType tutorialType) {
+ if (isRunning() || mIsAnimationPlayed) {
+ return;
+ }
+
+ mIsAnimationPlayed = true;
+ clearAnimationCallbacks();
+ mGestureAnimation.registerAnimationCallback(
+ new Animatable2.AnimationCallback() {
+ @Override
+ public void onAnimationEnd(Drawable drawable) {
+ super.onAnimationEnd(drawable);
+ mGestureAnimation.start();
+ }
+ });
+ start(tutorialType);
+ }
+
+ private void start(TutorialType tutorialType) {
+ // Because the gesture animation has only the right side form.
+ // The left side form of the gesture animation is made from flipping the View.
+ float rotationY = tutorialType == TutorialType.LEFT_EDGE_BACK_NAVIGATION ? 180f : 0f;
+ mHandCoachingView.setRotationY(rotationY);
+ mHandCoachingView.setImageDrawable(mGestureAnimation);
+ mHandCoachingView.postDelayed(() -> mGestureAnimation.start(),
+ ANIMATION_START_DELAY.toMillis());
+ }
+
+ private void clearAnimationCallbacks() {
+ mGestureAnimation.clearAnimationCallbacks();
+ }
+
+ void stop() {
+ mIsAnimationPlayed = false;
+ clearAnimationCallbacks();
+ mGestureAnimation.stop();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
new file mode 100644
index 0000000..ac8443d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+/** Defines the UI element identifiers for the particular {@link TutorialType}. */
+final class BackGestureTutorialTypeInfo {
+
+ private final TutorialType mTutorialType;
+ private final int mTutorialPlaygroundTitleId;
+ private final int mTutorialEngagedSubtitleId;
+ private final int mTutorialConfirmTitleId;
+ private final int mTutorialConfirmSubtitleId;
+
+ TutorialType getTutorialType() {
+ return mTutorialType;
+ }
+
+ int getTutorialPlaygroundTitleId() {
+ return mTutorialPlaygroundTitleId;
+ }
+
+ int getTutorialEngagedSubtitleId() {
+ return mTutorialEngagedSubtitleId;
+ }
+
+ int getTutorialConfirmTitleId() {
+ return mTutorialConfirmTitleId;
+ }
+
+ int getTutorialConfirmSubtitleId() {
+ return mTutorialConfirmSubtitleId;
+ }
+
+ static Builder builder() {
+ return new Builder();
+ }
+
+ private BackGestureTutorialTypeInfo(
+ TutorialType tutorialType,
+ int tutorialPlaygroundTitleId,
+ int tutorialEngagedSubtitleId,
+ int tutorialConfirmTitleId,
+ int tutorialConfirmSubtitleId) {
+ mTutorialType = tutorialType;
+ mTutorialPlaygroundTitleId = tutorialPlaygroundTitleId;
+ mTutorialEngagedSubtitleId = tutorialEngagedSubtitleId;
+ mTutorialConfirmTitleId = tutorialConfirmTitleId;
+ mTutorialConfirmSubtitleId = tutorialConfirmSubtitleId;
+ }
+
+ /** Builder for producing {@link BackGestureTutorialTypeInfo} objects. */
+ static class Builder {
+
+ private TutorialType mTutorialType;
+ private Integer mTutorialPlaygroundTitleId;
+ private Integer mTutorialEngagedSubtitleId;
+ private Integer mTutorialConfirmTitleId;
+ private Integer mTutorialConfirmSubtitleId;
+
+ Builder setTutorialType(TutorialType tutorialType) {
+ mTutorialType = tutorialType;
+ return this;
+ }
+
+ Builder setTutorialPlaygroundTitleId(int stringId) {
+ mTutorialPlaygroundTitleId = stringId;
+ return this;
+ }
+
+ Builder setTutorialEngagedSubtitleId(int stringId) {
+ mTutorialEngagedSubtitleId = stringId;
+ return this;
+ }
+
+ Builder setTutorialConfirmTitleId(int stringId) {
+ mTutorialConfirmTitleId = stringId;
+ return this;
+ }
+
+ Builder setTutorialConfirmSubtitleId(int stringId) {
+ mTutorialConfirmSubtitleId = stringId;
+ return this;
+ }
+
+ BackGestureTutorialTypeInfo build() {
+ return new BackGestureTutorialTypeInfo(
+ mTutorialType,
+ mTutorialPlaygroundTitleId,
+ mTutorialEngagedSubtitleId,
+ mTutorialConfirmTitleId,
+ mTutorialConfirmSubtitleId);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
new file mode 100644
index 0000000..9575d83
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+/** Provides instances of {@link BackGestureTutorialTypeInfo} for each {@link TutorialType}. */
+final class BackGestureTutorialTypeInfoProvider {
+
+ private static final BackGestureTutorialTypeInfo RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO =
+ BackGestureTutorialTypeInfo.builder()
+ .setTutorialType(TutorialType.RIGHT_EDGE_BACK_NAVIGATION)
+ .setTutorialPlaygroundTitleId(
+ R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge)
+ .setTutorialEngagedSubtitleId(
+ R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge)
+ .setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
+ .setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
+ .build();
+
+ private static final BackGestureTutorialTypeInfo LEFT_EDGE_BACK_NAV_TUTORIAL_INFO =
+ BackGestureTutorialTypeInfo.builder()
+ .setTutorialType(TutorialType.LEFT_EDGE_BACK_NAVIGATION)
+ .setTutorialPlaygroundTitleId(
+ R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge)
+ .setTutorialEngagedSubtitleId(
+ R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge)
+ .setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
+ .setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
+ .build();
+
+ static BackGestureTutorialTypeInfo getTutorialTypeInfo(TutorialType tutorialType) {
+ switch (tutorialType) {
+ case RIGHT_EDGE_BACK_NAVIGATION:
+ return RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO;
+ case LEFT_EDGE_BACK_NAVIGATION:
+ return LEFT_EDGE_BACK_NAV_TUTORIAL_INFO;
+ default:
+ throw new AssertionError("Unexpected tutorial type: " + tutorialType);
+ }
+ }
+
+ private BackGestureTutorialTypeInfoProvider() {
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 8e5ed1a..53859ad 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -17,30 +17,34 @@
package com.android.quickstep.logging;
import static android.stats.launcher.nano.Launcher.ALLAPPS;
+import static android.stats.launcher.nano.Launcher.BACKGROUND;
+import static android.stats.launcher.nano.Launcher.DISMISS_TASK;
import static android.stats.launcher.nano.Launcher.HOME;
import static android.stats.launcher.nano.Launcher.LAUNCH_APP;
import static android.stats.launcher.nano.Launcher.LAUNCH_TASK;
-import static android.stats.launcher.nano.Launcher.DISMISS_TASK;
-import static android.stats.launcher.nano.Launcher.BACKGROUND;
import static android.stats.launcher.nano.Launcher.OVERVIEW;
import android.content.Context;
import android.content.Intent;
+import android.os.UserHandle;
import android.stats.launcher.nano.Launcher;
import android.stats.launcher.nano.LauncherExtension;
import android.stats.launcher.nano.LauncherTarget;
import android.util.Log;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.ItemInfo;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.logging.StatsLogUtils;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.ComponentKey;
-import com.android.systemui.shared.system.StatsLogCompat;
+import com.android.systemui.shared.system.SysUiStatsLog;
+
import com.google.protobuf.nano.MessageNano;
/**
@@ -60,7 +64,7 @@
public StatsLogCompatManager(Context context) { }
@Override
- public void logAppLaunch(View v, Intent intent) {
+ public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) {
LauncherExtension ext = new LauncherExtension();
ext.srcTarget = new LauncherTarget[SUPPORTED_TARGET_DEPTH];
int srcState = mStateProvider.getCurrentState();
@@ -68,8 +72,8 @@
if (ext.srcTarget[0] != null) {
ext.srcTarget[0].item = LauncherTarget.APP_ICON;
}
- StatsLogCompat.write(LAUNCH_APP, srcState, BACKGROUND /* dstState */,
- MessageNano.toByteArray(ext), true);
+ SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_EVENT, LAUNCH_APP, srcState,
+ BACKGROUND /* dstState */, MessageNano.toByteArray(ext), true);
}
@Override
@@ -78,8 +82,8 @@
ext.srcTarget = new LauncherTarget[SUPPORTED_TARGET_DEPTH];
int srcState = OVERVIEW;
fillInLauncherExtension(v, ext);
- StatsLogCompat.write(LAUNCH_TASK, srcState, BACKGROUND /* dstState */,
- MessageNano.toByteArray(ext), true);
+ SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_EVENT, LAUNCH_TASK, srcState,
+ BACKGROUND /* dstState */, MessageNano.toByteArray(ext), true);
}
@Override
@@ -88,8 +92,8 @@
ext.srcTarget = new LauncherTarget[SUPPORTED_TARGET_DEPTH];
int srcState = OVERVIEW;
fillInLauncherExtension(v, ext);
- StatsLogCompat.write(DISMISS_TASK, srcState, BACKGROUND /* dstState */,
- MessageNano.toByteArray(ext), true);
+ SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_EVENT, DISMISS_TASK, srcState,
+ BACKGROUND /* dstState */, MessageNano.toByteArray(ext), true);
}
@Override
@@ -99,7 +103,7 @@
int srcState = mStateProvider.getCurrentState();
fillInLauncherExtensionWithPageId(ext, pageId);
int launcherAction = isSwipingToLeft ? Launcher.SWIPE_LEFT : Launcher.SWIPE_RIGHT;
- StatsLogCompat.write(launcherAction, srcState, srcState,
+ SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_EVENT, launcherAction, srcState, srcState,
MessageNano.toByteArray(ext), true);
}
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index d49ff89..b249f48 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -15,6 +15,8 @@
*/
package com.android.quickstep.util;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.Context;
@@ -26,7 +28,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
import com.android.quickstep.SysUINavigationMode;
import java.lang.annotation.Retention;
@@ -58,7 +59,7 @@
} else {
Resources res = context.getResources();
- if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+ if (ENABLE_OVERVIEW_ACTIONS.get()) {
//TODO: this needs to account for the swipe gesture height and accessibility
// UI when shown.
extraSpace = 0;
@@ -111,7 +112,7 @@
final int paddingResId;
if (dp.isVerticalBarLayout()) {
paddingResId = R.dimen.landscape_task_card_horz_space;
- } else if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+ } else if (ENABLE_OVERVIEW_ACTIONS.get()) {
paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
} else {
paddingResId = R.dimen.portrait_task_card_horz_space;
@@ -146,6 +147,11 @@
public static int getShelfTrackingDistance(Context context, DeviceProfile dp) {
// Track the bottom of the window.
+ if (ENABLE_OVERVIEW_ACTIONS.get()) {
+ Rect taskSize = new Rect();
+ calculateLauncherTaskSize(context, dp, taskSize);
+ return (dp.heightPx - taskSize.height()) / 2;
+ }
int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
int spaceBetweenShelfAndRecents = (int) context.getResources().getDimension(
R.dimen.task_card_vert_space);
@@ -157,7 +163,7 @@
* @return the margin in pixels.
*/
public static int thumbnailBottomMargin(Resources resources) {
- if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+ if (ENABLE_OVERVIEW_ACTIONS.get()) {
return resources.getDimensionPixelSize(R.dimen.overview_actions_height);
} else {
return 0;
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 5606ac2..b786c8b 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -35,6 +35,7 @@
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.util.Wait;
import com.android.launcher3.util.rule.FailureWatcher;
import com.android.systemui.shared.system.QuickStepContract;
@@ -57,6 +58,8 @@
static final String TAG = "QuickStepOnOffRule";
+ public static final int WAIT_TIME_MS = 10000;
+
public enum Mode {
THREE_BUTTON, TWO_BUTTON, ZERO_BUTTON, ALL
}
@@ -118,8 +121,8 @@
if (mode == THREE_BUTTON || mode == ALL) {
evaluateWithThreeButtons();
}
- } catch (Exception e) {
- Log.e(TAG, "Exception", e);
+ } catch (Throwable e) {
+ Log.e(TAG, "Error", e);
throw e;
} finally {
assertTrue("Couldn't set overlay",
@@ -195,19 +198,14 @@
currentSysUiNavigationMode() == expectedMode);
}
- for (int i = 0; i != 100; ++i) {
- if (mLauncher.getNavigationModel() == expectedMode) break;
- Thread.sleep(100);
- }
- assertTrue("Couldn't switch to " + overlayPackage,
- mLauncher.getNavigationModel() == expectedMode);
+ Wait.atMost("Couldn't switch to " + overlayPackage,
+ () -> mLauncher.getNavigationModel() == expectedMode, WAIT_TIME_MS,
+ mLauncher);
- for (int i = 0; i != 100; ++i) {
- if (mLauncher.getNavigationModeMismatchError() == null) break;
- Thread.sleep(100);
- }
- final String error = mLauncher.getNavigationModeMismatchError();
- assertTrue("Switching nav mode: " + error, error == null);
+ Wait.atMost(() -> "Switching nav mode: "
+ + mLauncher.getNavigationModeMismatchError(),
+ () -> mLauncher.getNavigationModeMismatchError() == null, WAIT_TIME_MS,
+ mLauncher);
return true;
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 428e647..71d77fc 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -43,6 +43,7 @@
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import com.android.quickstep.views.RecentsView;
+import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -51,10 +52,18 @@
@LargeTest
@RunWith(AndroidJUnit4.class)
public class TaplTestsQuickstep extends AbstractQuickStepTest {
+ private int mLauncherPid;
+
@Before
public void setUp() throws Exception {
super.setUp();
TaplTestsLauncher3.initialize(this);
+ mLauncherPid = mLauncher.getPid();
+ }
+
+ @After
+ public void teardown() {
+ assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
}
private void startTestApps() throws Exception {
@@ -100,6 +109,7 @@
@PortraitLandscape
public void testOverview() throws Exception {
startTestApps();
+ // mLauncher.pressHome() also tests an important case of pressing home while in background.
Overview overview = mLauncher.pressHome().switchToOverview();
assertTrue("Launcher internal state didn't switch to Overview",
isInState(LauncherState.OVERVIEW));
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 3c8fe1e..815ae21 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -37,4 +37,10 @@
<color name="all_apps_bg_hand_fill">#E5E5E5</color>
<color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
+
+ <color name="back_gesture_tutorial_background_color">#FFFFFFFF</color>
+ <color name="back_gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
+ <color name="back_gesture_tutorial_title_color">#FF000000</color>
+ <color name="back_gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
+ <color name="back_gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index dec8939..218f6db 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -103,7 +103,7 @@
<!-- Label for install drop target. [CHAR_LIMIT=20] -->
<string name="install_drop_target_label">Install</string>
<!-- Label for install dismiss prediction. -->
- <string translatable="false" name="dismiss_prediction_label">Dismiss prediction</string>
+ <string translatable="false" name="dismiss_prediction_label">Don\'t suggest app</string>
<!-- Label for pinning predicted app. -->
<string name="pin_prediction" translatable="false">Pin Prediction</string>
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
index 310d43c..86a6e8c 100644
--- a/robolectric_tests/Android.mk
+++ b/robolectric_tests/Android.mk
@@ -19,6 +19,8 @@
include $(CLEAR_VARS)
LOCAL_MODULE := LauncherRoboTests
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+
LOCAL_SDK_VERSION := current
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := \
@@ -34,6 +36,9 @@
LOCAL_INSTRUMENTATION_FOR := Launcher3
LOCAL_MODULE_TAGS := optional
+# Generate test_config.properties
+include external/robolectric-shadows/gen_test_config.mk
+
include $(BUILD_STATIC_JAVA_LIBRARY)
############################################
@@ -43,14 +48,11 @@
LOCAL_MODULE := RunLauncherRoboTests
LOCAL_SDK_VERSION := current
-LOCAL_JAVA_LIBRARIES := \
- LauncherRoboTests
+LOCAL_JAVA_LIBRARIES := LauncherRoboTests
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
LOCAL_TEST_PACKAGE := Launcher3
-
-LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src \
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
LOCAL_ROBOTEST_TIMEOUT := 36000
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
index e0d6e53..932b01b 100644
--- a/robolectric_tests/config/robolectric.properties
+++ b/robolectric_tests/config/robolectric.properties
@@ -1,2 +1 @@
-manifest=packages/apps/Launcher3/AndroidManifest.xml
-sdk=26
+sdk=28
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
deleted file mode 100644
index 4bb9a53..0000000
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.android.launcher3.config;
-
-
-import com.android.launcher3.config.FeatureFlags.BaseTogglableFlag;
-import com.android.launcher3.uioverrides.TogglableFlag;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.lang.annotation.Annotation;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Repeatable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-/**
- * Test rule that makes overriding flags in Robolectric tests easier. This rule clears all flags
- * before and after your test, avoiding one test method affecting subsequent methods.
- *
- * <p>Usage:
- * <pre>
- * {@literal @}Rule public final FlagOverrideRule flags = new FlagOverrideRule();
- *
- * {@literal @}FlagOverride(flag = "FOO", value=true)
- * {@literal @}Test public void myTest() {
- * ...
- * }
- * </pre>
- */
-public final class FlagOverrideRule implements TestRule {
-
- /**
- * Container annotation for handling multiple {@link FlagOverride} annotations.
- * <p>
- * <p>Don't use this directly, use repeated {@link FlagOverride} annotations instead.
- */
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.METHOD})
- public @interface FlagOverrides {
- FlagOverride[] value();
- }
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.METHOD})
- @Repeatable(FlagOverrides.class)
- public @interface FlagOverride {
- String key();
-
- boolean value();
- }
-
- @Override
- public Statement apply(Statement base, Description description) {
- return new MyStatement(base, description);
- }
-
- private class MyStatement extends Statement {
-
- private final Statement mBase;
- private final Description mDescription;
-
-
- MyStatement(Statement base, Description description) {
- mBase = base;
- mDescription = description;
- }
-
- @Override
- public void evaluate() throws Throwable {
- Map<String, BaseTogglableFlag> allFlags = FeatureFlags.getTogglableFlags().stream()
- .collect(Collectors.toMap(TogglableFlag::getKey, Function.identity()));
-
- HashMap<BaseTogglableFlag, Boolean> changedValues = new HashMap<>();
- FlagOverride[] overrides = new FlagOverride[0];
- try {
- for (Annotation annotation : mDescription.getAnnotations()) {
- if (annotation.annotationType() == FlagOverride.class) {
- overrides = new FlagOverride[] { (FlagOverride) annotation };
- } else if (annotation.annotationType() == FlagOverrides.class) {
- // Note: this branch is hit if the annotation is repeated
- overrides = ((FlagOverrides) annotation).value();
- }
- }
- for (FlagOverride override : overrides) {
- BaseTogglableFlag flag = allFlags.get(override.key());
- changedValues.put(flag, flag.get());
- flag.setForTests(override.value());
- }
- mBase.evaluate();
- } finally {
- // Clear the values
- changedValues.forEach(BaseTogglableFlag::setForTests);
- }
- }
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
deleted file mode 100644
index 31a037b..0000000
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.android.launcher3.config;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import com.android.launcher3.config.FlagOverrideRule.FlagOverride;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-/**
- * Sample Robolectric test that demonstrates flag-overriding.
- */
-@RunWith(RobolectricTestRunner.class)
-public class FlagOverrideSampleTest {
-
- // Check out https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html for more information
- // on @Rules.
- @Rule
- public final FlagOverrideRule flags = new FlagOverrideRule();
-
- /**
- * Test if flag can be overriden to true via annoation.
- */
- @FlagOverride(key = "FAKE_LANDSCAPE_UI", value = true)
- @Test
- public void withFlagOn() {
- assertTrue(FeatureFlags.FAKE_LANDSCAPE_UI.get());
- }
-
- /**
- * Test if flag can be overriden to false via annoation.
- */
- @FlagOverride(key = "FAKE_LANDSCAPE_UI", value = false)
- @Test
- public void withFlagOff() {
- assertFalse(FeatureFlags.FAKE_LANDSCAPE_UI.get());
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
new file mode 100644
index 0000000..f769055
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.folder;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.shadows.LShadowUserManager;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+@RunWith(LauncherRoboTestRunner.class)
+public final class FolderNameProviderTest {
+ private Context mContext;
+ private WorkspaceItemInfo mItem1;
+ private WorkspaceItemInfo mItem2;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mItem1 = new WorkspaceItemInfo(new AppInfo(
+ new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
+ "title1",
+ LShadowUserManager.newUserHandle(10),
+ new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
+ ));
+ mItem2 = new WorkspaceItemInfo(new AppInfo(
+ new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
+ "title2",
+ LShadowUserManager.newUserHandle(10),
+ new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
+ ));
+ }
+
+ @Test
+ public void getSuggestedFolderName_workAssignedToEnd() {
+ ArrayList<WorkspaceItemInfo> list = new ArrayList<>();
+ list.add(mItem1);
+ list.add(mItem2);
+ String[] suggestedNameOut = new String[FolderNameProvider.SUGGEST_MAX];
+ new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut);
+ assertTrue(suggestedNameOut[0].equals("Work"));
+
+ suggestedNameOut[0] = "candidate1";
+ suggestedNameOut[1] = "candidate2";
+ suggestedNameOut[2] = "candidate3";
+ new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut);
+ assertTrue(suggestedNameOut[3].equals("Work"));
+
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
index 410a077..48b5a45 100644
--- a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
+++ b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
@@ -3,11 +3,12 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.util.Scheduler;
@@ -20,7 +21,7 @@
/**
* Tests for {@link FileLog}
*/
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
public class FileLogTest {
private File mTempDir;
diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
index d7a2278..b7f2243 100644
--- a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -4,54 +4,70 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.util.Pair;
+import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.ContentWriter;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
/**
* Tests for {@link AddWorkspaceItemsTask}
*/
-@RunWith(RobolectricTestRunner.class)
-public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class AddWorkspaceItemsTaskTest {
private final ComponentName mComponent1 = new ComponentName("a", "b");
private final ComponentName mComponent2 = new ComponentName("b", "b");
- private IntArray existingScreens;
- private IntArray newScreens;
- private IntSparseArrayMap<GridOccupancy> screenOccupancy;
+ private Context mTargetContext;
+ private InvariantDeviceProfile mIdp;
+ private LauncherAppState mAppState;
+ private LauncherModelHelper mModelHelper;
+
+ private IntArray mExistingScreens;
+ private IntArray mNewScreens;
+ private IntSparseArrayMap<GridOccupancy> mScreenOccupancy;
@Before
- public void initData() throws Exception {
- existingScreens = new IntArray();
- screenOccupancy = new IntSparseArrayMap<>();
- newScreens = new IntArray();
+ public void setup() {
+ mModelHelper = new LauncherModelHelper();
+ mTargetContext = RuntimeEnvironment.application;
+ mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
+ mIdp.numColumns = mIdp.numRows = 5;
+ mAppState = LauncherAppState.getInstance(mTargetContext);
- idp.numColumns = 5;
- idp.numRows = 5;
+ mExistingScreens = new IntArray();
+ mScreenOccupancy = new IntSparseArrayMap<>();
+ mNewScreens = new IntArray();
}
private AddWorkspaceItemsTask newTask(ItemInfo... items) {
@@ -70,17 +86,17 @@
// Second screen has 2 holes of sizes 3x2 and 2x3
setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
- int[] spaceFound = newTask()
- .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 1, 1);
+ int[] spaceFound = newTask().findSpaceForItem(
+ mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 1, 1);
assertEquals(2, spaceFound[0]);
- assertTrue(screenOccupancy.get(spaceFound[0])
+ assertTrue(mScreenOccupancy.get(spaceFound[0])
.isRegionVacant(spaceFound[1], spaceFound[2], 1, 1));
// Find a larger space
- spaceFound = newTask()
- .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 2, 3);
+ spaceFound = newTask().findSpaceForItem(
+ mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 2, 3);
assertEquals(2, spaceFound[0]);
- assertTrue(screenOccupancy.get(spaceFound[0])
+ assertTrue(mScreenOccupancy.get(spaceFound[0])
.isRegionVacant(spaceFound[1], spaceFound[2], 2, 3));
}
@@ -89,11 +105,11 @@
// First screen has 2 holes of sizes 3x2 and 2x3
setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
- IntArray oldScreens = existingScreens.clone();
- int[] spaceFound = newTask()
- .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 3, 3);
+ IntArray oldScreens = mExistingScreens.clone();
+ int[] spaceFound = newTask().findSpaceForItem(
+ mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 3, 3);
assertFalse(oldScreens.contains(spaceFound[0]));
- assertTrue(newScreens.contains(spaceFound[0]));
+ assertTrue(mNewScreens.contains(spaceFound[0]));
}
@Test
@@ -105,11 +121,14 @@
setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
// Nothing was added
- assertTrue(executeTaskForTest(newTask(info)).isEmpty());
+ assertTrue(mModelHelper.executeTaskForTest(newTask(info)).isEmpty());
}
@Test
public void testAddItem_some_items_added() throws Exception {
+ Callbacks callbacks = mock(Callbacks.class);
+ mModelHelper.getModel().addCallbacks(callbacks);
+
WorkspaceItemInfo info = new WorkspaceItemInfo();
info.intent = new Intent().setComponent(mComponent1);
@@ -119,7 +138,7 @@
// Setup a screen with a hole
setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
- executeTaskForTest(newTask(info, info2)).get(0).run();
+ mModelHelper.executeTaskForTest(newTask(info, info2)).get(0).run();
ArgumentCaptor<ArrayList> notAnimated = ArgumentCaptor.forClass(ArrayList.class);
ArgumentCaptor<ArrayList> animated = ArgumentCaptor.forClass(ArrayList.class);
@@ -134,18 +153,23 @@
}
private int setupWorkspaceWithHoles(int startId, int screenId, Rect... holes) throws Exception {
- GridOccupancy occupancy = new GridOccupancy(idp.numColumns, idp.numRows);
- occupancy.markCells(0, 0, idp.numColumns, idp.numRows, true);
+ return mModelHelper.executeSimpleTask(
+ model -> writeWorkspaceWithHoles(model, startId, screenId, holes));
+ }
+
+ private int writeWorkspaceWithHoles(
+ BgDataModel bgDataModel, int startId, int screenId, Rect... holes) {
+ GridOccupancy occupancy = new GridOccupancy(mIdp.numColumns, mIdp.numRows);
+ occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true);
for (Rect r : holes) {
occupancy.markCells(r, false);
}
- existingScreens.add(screenId);
- screenOccupancy.append(screenId, occupancy);
+ mExistingScreens.add(screenId);
+ mScreenOccupancy.append(screenId, occupancy);
- ExecutorService executor = Executors.newSingleThreadExecutor();
- for (int x = 0; x < idp.numColumns; x++) {
- for (int y = 0; y < idp.numRows; y++) {
+ for (int x = 0; x < mIdp.numColumns; x++) {
+ for (int y = 0; y < mIdp.numRows; y++) {
if (!occupancy.cells[x][y]) {
continue;
}
@@ -157,20 +181,15 @@
info.cellX = x;
info.cellY = y;
info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
- bgDataModel.addItem(targetContext, info, false);
+ bgDataModel.addItem(mTargetContext, info, false);
- executor.execute(() -> {
- ContentWriter writer = new ContentWriter(targetContext);
- info.writeToValues(writer);
- writer.put(Favorites._ID, info.id);
- targetContext.getContentResolver().insert(Favorites.CONTENT_URI,
- writer.getValues(targetContext));
- });
+ ContentWriter writer = new ContentWriter(mTargetContext);
+ info.writeToValues(writer);
+ writer.put(Favorites._ID, info.id);
+ mTargetContext.getContentResolver().insert(Favorites.CONTENT_URI,
+ writer.getValues(mTargetContext));
}
}
-
- executor.submit(() -> null).get();
- executor.shutdown();
return startId;
}
}
diff --git a/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
new file mode 100644
index 0000000..6223760
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/BackupRestoreTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
+import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+
+import static org.junit.Assert.assertTrue;
+
+import android.database.sqlite.SQLiteDatabase;
+
+import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+
+/**
+ * Tests to verify backup and restore flow.
+ */
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(LooperMode.Mode.PAUSED)
+public class BackupRestoreTest {
+
+ private LauncherModelHelper mModelHelper;
+ private SQLiteDatabase mDb;
+
+ @Before
+ public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+ RestoreDbTask.setPending(RuntimeEnvironment.application, true);
+ mDb = mModelHelper.provider.getDb();
+ }
+
+ @Test
+ public void testOnCreateDbIfNotExists_CreatesBackup() {
+ assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java
deleted file mode 100644
index 07834fc..0000000
--- a/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package com.android.launcher3.model;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.database.sqlite.SQLiteDatabase;
-
-import com.android.launcher3.LauncherProvider;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.util.TestLauncherProvider;
-
-import org.junit.Before;
-import org.robolectric.Robolectric;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowContentResolver;
-import org.robolectric.shadows.ShadowLog;
-
-public abstract class BaseGridChangesTestCase {
-
-
- public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
- public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-
- public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
- public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
- public static final int NO__ICON = -1;
-
- public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
-
- public Context mContext;
- public TestLauncherProvider mProvider;
- public SQLiteDatabase mDb;
-
- @Before
- public void setUpBaseCase() {
- ShadowLog.stream = System.out;
-
- mContext = RuntimeEnvironment.application;
- mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class);
- ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider);
- mDb = mProvider.getDb();
- }
-
- /**
- * Adds a dummy item in the DB.
- * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
- * folder (where the type represents the number of items in the folder).
- */
- public int addItem(int type, int screen, int container, int x, int y) {
- int id = LauncherSettings.Settings.call(mContext.getContentResolver(),
- LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
- .getInt(LauncherSettings.Settings.EXTRA_VALUE);
-
- ContentValues values = new ContentValues();
- values.put(LauncherSettings.Favorites._ID, id);
- values.put(LauncherSettings.Favorites.CONTAINER, container);
- values.put(LauncherSettings.Favorites.SCREEN, screen);
- values.put(LauncherSettings.Favorites.CELLX, x);
- values.put(LauncherSettings.Favorites.CELLY, y);
- values.put(LauncherSettings.Favorites.SPANX, 1);
- values.put(LauncherSettings.Favorites.SPANY, 1);
-
- if (type == APP_ICON || type == SHORTCUT) {
- values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
- values.put(LauncherSettings.Favorites.INTENT,
- new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
- } else {
- values.put(LauncherSettings.Favorites.ITEM_TYPE,
- LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
- // Add folder items.
- for (int i = 0; i < type; i++) {
- addItem(APP_ICON, 0, id, 0, 0);
- }
- }
-
- mContext.getContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
- return id;
- }
-
- public int[][][] createGrid(int[][][] typeArray) {
- return createGrid(typeArray, 1);
- }
-
- /**
- * Initializes the DB with dummy elements to represent the provided grid structure.
- * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
- * type definitions. The first dimension represents the screens and the next
- * two represent the workspace grid.
- * @param startScreen First screen id from where the icons will be added.
- * @return the same grid representation where each entry is the corresponding item id.
- */
- public int[][][] createGrid(int[][][] typeArray, int startScreen) {
- LauncherSettings.Settings.call(mContext.getContentResolver(),
- LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
- int[][][] ids = new int[typeArray.length][][];
-
- for (int i = 0; i < typeArray.length; i++) {
- // Add screen to DB
- int screenId = startScreen + i;
-
- // Keep the screen id counter up to date
- LauncherSettings.Settings.call(mContext.getContentResolver(),
- LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
-
- ids[i] = new int[typeArray[i].length][];
- for (int y = 0; y < typeArray[i].length; y++) {
- ids[i][y] = new int[typeArray[i][y].length];
- for (int x = 0; x < typeArray[i][y].length; x++) {
- if (typeArray[i][y][x] < 0) {
- // Empty cell
- ids[i][y][x] = -1;
- } else {
- ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
- }
- }
- }
- }
-
- return ids;
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
deleted file mode 100644
index 012258d..0000000
--- a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
+++ /dev/null
@@ -1,231 +0,0 @@
-package com.android.launcher3.model;
-
-import static com.android.launcher3.shadows.ShadowLooperExecutor.reinitializeStaticExecutors;
-
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.Color;
-import android.os.Process;
-import android.os.UserHandle;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.AppFilter;
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.LauncherProvider;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.TestLauncherProvider;
-
-import org.junit.Before;
-import org.mockito.ArgumentCaptor;
-import org.robolectric.Robolectric;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowContentResolver;
-import org.robolectric.shadows.ShadowLog;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.function.Supplier;
-
-/**
- * Base class for writing tests for Model update tasks.
- */
-public class BaseModelUpdateTaskTestCase {
-
- public final HashMap<Class, HashMap<String, Field>> fieldCache = new HashMap<>();
- public TestLauncherProvider provider;
-
- public Context targetContext;
- public UserHandle myUser;
-
- public InvariantDeviceProfile idp;
- public LauncherAppState appState;
- public LauncherModel model;
- public ModelWriter modelWriter;
- public MyIconCache iconCache;
-
- public BgDataModel bgDataModel;
- public AllAppsList allAppsList;
- public Callbacks callbacks;
-
- @Before
- public void setUp() throws Exception {
- ShadowLog.stream = System.out;
- reinitializeStaticExecutors();
- InstallSessionHelper.INSTANCE.initializeForTesting(null);
-
- provider = Robolectric.setupContentProvider(TestLauncherProvider.class);
- ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider);
-
- callbacks = mock(Callbacks.class);
- appState = mock(LauncherAppState.class);
- model = mock(LauncherModel.class);
- modelWriter = mock(ModelWriter.class);
-
- LauncherAppState.INSTANCE.initializeForTesting(appState);
- when(appState.getModel()).thenReturn(model);
- when(model.getWriter(anyBoolean(), anyBoolean())).thenReturn(modelWriter);
- when(model.getCallback()).thenReturn(callbacks);
-
- myUser = Process.myUserHandle();
-
- bgDataModel = new BgDataModel();
- targetContext = RuntimeEnvironment.application;
-
- idp = new InvariantDeviceProfile();
- iconCache = new MyIconCache(targetContext, idp);
-
- allAppsList = new AllAppsList(iconCache, new AppFilter());
-
- when(appState.getIconCache()).thenReturn(iconCache);
- when(appState.getInvariantDeviceProfile()).thenReturn(idp);
- when(appState.getContext()).thenReturn(targetContext);
- }
-
- /**
- * Synchronously executes the task and returns all the UI callbacks posted.
- */
- public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception {
- when(model.isModelLoaded()).thenReturn(true);
-
- Executor mockExecutor = mock(Executor.class);
-
- task.init(appState, model, bgDataModel, allAppsList, mockExecutor);
- task.run();
- ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
- verify(mockExecutor, atLeast(0)).execute(captor.capture());
-
- return captor.getAllValues();
- }
-
- /**
- * Initializes mock data for the test.
- */
- public void initializeData(String resourceName) throws Exception {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(
- this.getClass().getResourceAsStream(resourceName)))) {
- String line;
- HashMap<String, Class> classMap = new HashMap<>();
- while((line = reader.readLine()) != null) {
- line = line.trim();
- if (line.startsWith("#") || line.isEmpty()) {
- continue;
- }
- String[] commands = line.split(" ");
- switch (commands[0]) {
- case "classMap":
- classMap.put(commands[1], Class.forName(commands[2]));
- break;
- case "bgItem":
- bgDataModel.addItem(targetContext,
- (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), false);
- break;
- case "allApps":
- allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
- break;
- }
- }
- }
- }
-
- private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
- HashMap<String, Field> cache = fieldCache.get(clazz);
- if (cache == null) {
- cache = new HashMap<>();
- Class c = clazz;
- while (c != null) {
- for (Field f : c.getDeclaredFields()) {
- f.setAccessible(true);
- cache.put(f.getName(), f);
- }
- c = c.getSuperclass();
- }
- fieldCache.put(clazz, cache);
- }
-
- Object item = clazz.newInstance();
- for (int i = startIndex; i < fieldDef.length; i++) {
- String[] fieldData = fieldDef[i].split("=", 2);
- Field f = cache.get(fieldData[0]);
- Class type = f.getType();
- if (type == int.class || type == long.class) {
- f.set(item, Integer.parseInt(fieldData[1]));
- } else if (type == CharSequence.class || type == String.class) {
- f.set(item, fieldData[1]);
- } else if (type == Intent.class) {
- if (!fieldData[1].startsWith("#Intent")) {
- fieldData[1] = "#Intent;" + fieldData[1] + ";end";
- }
- f.set(item, Intent.parseUri(fieldData[1], 0));
- } else if (type == ComponentName.class) {
- f.set(item, ComponentName.unflattenFromString(fieldData[1]));
- } else {
- throw new Exception("Added parsing logic for "
- + f.getName() + " of type " + f.getType());
- }
- }
- return item;
- }
-
- public static class MyIconCache extends IconCache {
-
- private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<>();
-
- public MyIconCache(Context context, InvariantDeviceProfile idp) {
- super(context, idp);
- }
-
- @Override
- protected <T> CacheEntry cacheLocked(
- @NonNull ComponentName componentName,
- UserHandle user, @NonNull Supplier<T> infoProvider,
- @NonNull CachingLogic<T> cachingLogic,
- boolean usePackageIcon, boolean useLowResIcon) {
- CacheEntry entry = mCache.get(new ComponentKey(componentName, user));
- if (entry == null) {
- entry = new CacheEntry();
- entry.bitmap = getDefaultIcon(user);
- }
- return entry;
- }
-
- public void addCache(ComponentName key, String title) {
- CacheEntry entry = new CacheEntry();
- entry.bitmap = BitmapInfo.of(newIcon(), Color.RED);
- entry.title = title;
- mCache.put(new ComponentKey(key, Process.myUserHandle()), entry);
- }
-
- public Bitmap newIcon() {
- return Bitmap.createBitmap(1, 1, Config.ARGB_8888);
- }
-
- @Override
- public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
- return BitmapInfo.fromBitmap(newIcon());
- }
- }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 69c5b00..f128e24 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -5,15 +5,34 @@
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Color;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.annotation.NonNull;
+
import com.android.launcher3.AppInfo;
import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
import java.util.Arrays;
import java.util.HashSet;
@@ -21,40 +40,73 @@
/**
* Tests for {@link CacheDataUpdatedTask}
*/
-@RunWith(RobolectricTestRunner.class)
-public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class CacheDataUpdatedTaskTest {
private static final String NEW_LABEL_PREFIX = "new-label-";
+ private LauncherModelHelper mModelHelper;
+
@Before
- public void initData() throws Exception {
- initializeData("/cache_data_updated_task_data.txt");
+ public void setup() throws Exception {
+ mModelHelper = new LauncherModelHelper();
+ mModelHelper.initializeData("/cache_data_updated_task_data.txt");
+
// Add dummy entries in the cache to simulate update
- for (ItemInfo info : bgDataModel.itemsIdMap) {
- iconCache.addCache(info.getTargetComponent(), NEW_LABEL_PREFIX + info.id);
+ Context context = RuntimeEnvironment.application;
+ IconCache iconCache = LauncherAppState.getInstance(context).getIconCache();
+ CachingLogic<ItemInfo> dummyLogic = new CachingLogic<ItemInfo>() {
+ @Override
+ public ComponentName getComponent(ItemInfo info) {
+ return info.getTargetComponent();
+ }
+
+ @Override
+ public UserHandle getUser(ItemInfo info) {
+ return info.user;
+ }
+
+ @Override
+ public CharSequence getLabel(ItemInfo info) {
+ return NEW_LABEL_PREFIX + info.id;
+ }
+
+ @NonNull
+ @Override
+ public BitmapInfo loadIcon(Context context, ItemInfo info) {
+ return BitmapInfo.of(Bitmap.createBitmap(1, 1, Config.ARGB_8888), Color.RED);
+ }
+ };
+
+ UserManager um = context.getSystemService(UserManager.class);
+ for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
+ iconCache.addIconToDBAndMemCache(info, dummyLogic, new PackageInfo(),
+ um.getSerialNumberForUser(info.user), true);
}
}
private CacheDataUpdatedTask newTask(int op, String... pkg) {
- return new CacheDataUpdatedTask(op, myUser, new HashSet<>(Arrays.asList(pkg)));
+ return new CacheDataUpdatedTask(op, Process.myUserHandle(),
+ new HashSet<>(Arrays.asList(pkg)));
}
@Test
public void testCacheUpdate_update_apps() throws Exception {
// Clear all icons from apps list so that its easy to check what was updated
- for (AppInfo info : allAppsList.data) {
+ for (AppInfo info : mModelHelper.getAllAppsList().data) {
info.bitmap = BitmapInfo.LOW_RES_INFO;
}
- executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
+ mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
// Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7)
// is not updated
verifyUpdate(1, 2);
// Verify that only app1 var updated in allAppsList
- assertFalse(allAppsList.data.isEmpty());
- for (AppInfo info : allAppsList.data) {
+ assertFalse(mModelHelper.getAllAppsList().data.isEmpty());
+ for (AppInfo info : mModelHelper.getAllAppsList().data) {
if (info.componentName.getPackageName().equals("app1")) {
assertFalse(info.bitmap.isNullOrLowRes());
} else {
@@ -65,7 +117,7 @@
@Test
public void testSessionUpdate_ignores_normal_apps() throws Exception {
- executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
+ mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
// app1 has no restored shortcuts. Verify that nothing was updated.
verifyUpdate();
@@ -73,7 +125,7 @@
@Test
public void testSessionUpdate_updates_pending_apps() throws Exception {
- executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
+ mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
// app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were
// were updated
@@ -82,7 +134,7 @@
private void verifyUpdate(Integer... idsUpdated) {
HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
- for (ItemInfo info : bgDataModel.itemsIdMap) {
+ for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
if (updates.contains(info.id)) {
assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
assertFalse(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
diff --git a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index b7340cf..1442c55 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -36,11 +36,11 @@
import com.android.launcher3.LauncherProvider.DatabaseHelper;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.io.File;
@@ -48,7 +48,7 @@
/**
* Tests for {@link DbDowngradeHelper}
*/
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
public class DbDowngradeHelperTest {
private static final String SCHEMA_FILE = "test_schema.json";
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index 68713d8..f8ac010 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -16,97 +16,65 @@
package com.android.launcher3.model;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.util.ReflectionHelpers.setField;
import android.content.ComponentName;
+import android.content.Context;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageInstaller.SessionParams;
-import android.net.Uri;
-import android.provider.Settings;
import com.android.launcher3.FolderInfo;
-import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.shadows.LShadowLauncherApps;
-import com.android.launcher3.shadows.LShadowUserManager;
-import com.android.launcher3.shadows.ShadowLooperExecutor;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.LauncherLayoutBuilder;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
-import org.robolectric.shadows.ShadowPackageManager;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStreamWriter;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
/**
* Tests for layout parser for remote layout
*/
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {LShadowUserManager.class, LShadowLauncherApps.class, ShadowLooperExecutor.class})
+@RunWith(LauncherRoboTestRunner.class)
@LooperMode(Mode.PAUSED)
-public class DefaultLayoutProviderTest extends BaseModelUpdateTaskTestCase {
+public class DefaultLayoutProviderTest {
- private static final String SETTINGS_APP = "com.android.settings";
- private static final String TEST_PROVIDER_AUTHORITY =
- DefaultLayoutProviderTest.class.getName().toLowerCase();
-
- private static final int BITMAP_SIZE = 10;
- private static final int GRID_SIZE = 4;
+ private LauncherModelHelper mModelHelper;
+ private Context mTargetContext;
@Before
- public void setUp() throws Exception {
- super.setUp();
- InvariantDeviceProfile.INSTANCE.initializeForTesting(idp);
- CustomWidgetManager.INSTANCE.initializeForTesting(mock(CustomWidgetManager.class));
+ public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+ mTargetContext = RuntimeEnvironment.application;
- idp.numRows = idp.numColumns = idp.numHotseatIcons = GRID_SIZE;
- idp.iconBitmapSize = BITMAP_SIZE;
-
- provider.setAllowLoadDefaultFavorites(true);
- Settings.Secure.putString(targetContext.getContentResolver(),
- "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
-
- ShadowPackageManager spm = shadowOf(targetContext.getPackageManager());
- spm.addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
- TEST_PROVIDER_AUTHORITY;
- spm.addActivityIfNotPresent(new ComponentName(SETTINGS_APP, SETTINGS_APP));
- }
-
- @After
- public void cleanup() {
- InvariantDeviceProfile.INSTANCE.initializeForTesting(null);
- CustomWidgetManager.INSTANCE.initializeForTesting(null);
- InstallSessionHelper.INSTANCE.initializeForTesting(null);
+ shadowOf(mTargetContext.getPackageManager())
+ .addActivityIfNotPresent(new ComponentName(TEST_PACKAGE, TEST_PACKAGE));
}
@Test
public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
- .putApp(SETTINGS_APP, SETTINGS_APP));
+ .putApp(TEST_PACKAGE, TEST_PACKAGE));
// Verify one item in hotseat
- assertEquals(1, bgDataModel.workspaceItems.size());
- ItemInfo info = bgDataModel.workspaceItems.get(0);
+ assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+ ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
assertEquals(LauncherSettings.Favorites.CONTAINER_HOTSEAT, info.container);
assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPLICATION, info.itemType);
}
@@ -114,14 +82,14 @@
@Test
public void testCustomProfileLoaded_with_folder() throws Exception {
writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
- .addApp(SETTINGS_APP, SETTINGS_APP)
- .addApp(SETTINGS_APP, SETTINGS_APP)
- .addApp(SETTINGS_APP, SETTINGS_APP)
+ .addApp(TEST_PACKAGE, TEST_PACKAGE)
+ .addApp(TEST_PACKAGE, TEST_PACKAGE)
+ .addApp(TEST_PACKAGE, TEST_PACKAGE)
.build());
// Verify folder
- assertEquals(1, bgDataModel.workspaceItems.size());
- ItemInfo info = bgDataModel.workspaceItems.get(0);
+ assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+ ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
assertEquals(3, ((FolderInfo) info).contents.size());
}
@@ -134,7 +102,7 @@
SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
params.setAppPackageName(pendingAppPkg);
- PackageInstaller installer = targetContext.getPackageManager().getPackageInstaller();
+ PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
int sessionId = installer.createSession(params);
SessionInfo sessionInfo = installer.getSessionInfo(sessionId);
setField(sessionInfo, "installerPackageName", "com.test");
@@ -144,24 +112,26 @@
.putWidget(pendingAppPkg, "DummyWidget", 2, 2));
// Verify widget
- assertEquals(1, bgDataModel.appWidgets.size());
- ItemInfo info = bgDataModel.appWidgets.get(0);
+ assertEquals(1, mModelHelper.getBgDataModel().appWidgets.size());
+ ItemInfo info = mModelHelper.getBgDataModel().appWidgets.get(0);
assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET, info.itemType);
assertEquals(2, info.spanX);
assertEquals(2, info.spanY);
}
private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- builder.build(new OutputStreamWriter(bos));
+ mModelHelper.setupDefaultLayoutProvider(builder);
- Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, targetContext);
- shadowOf(targetContext.getContentResolver()).registerInputStream(layoutUri,
- new ByteArrayInputStream(bos.toByteArray()));
-
- LoaderResults results = new LoaderResults(appState, bgDataModel, allAppsList, 0,
- new WeakReference<>(callbacks));
- LoaderTask task = new LoaderTask(appState, allAppsList, bgDataModel, results);
+ LoaderResults results = new LoaderResults(
+ LauncherAppState.getInstance(mTargetContext),
+ mModelHelper.getBgDataModel(),
+ mModelHelper.getAllAppsList(),
+ new Callbacks[0]);
+ LoaderTask task = new LoaderTask(
+ LauncherAppState.getInstance(mTargetContext),
+ mModelHelper.getAllAppsList(),
+ mModelHelper.getBgDataModel(),
+ results);
Executors.MODEL_EXECUTOR.submit(() -> task.loadWorkspace(new ArrayList<>())).get();
}
}
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java b/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
index 53287a9..f46b849 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
@@ -6,33 +6,53 @@
import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.DESKTOP;
+import static com.android.launcher3.util.LauncherModelHelper.NO__ICON;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
import android.graphics.Point;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherSettings.Settings;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
/**
* Unit tests for {@link GridBackupTable}
*/
-@RunWith(RobolectricTestRunner.class)
-public class GridBackupTableTest extends BaseGridChangesTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+public class GridBackupTableTest {
private static final int BACKUP_ITEM_COUNT = 12;
+ private LauncherModelHelper mModelHelper;
+ private Context mContext;
+ private SQLiteDatabase mDb;
+
@Before
- public void setupGridData() {
- createGrid(new int[][][]{{
+ public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+ mContext = RuntimeEnvironment.application;
+ mDb = mModelHelper.provider.getDb();
+
+ setupGridData();
+ }
+
+ private void setupGridData() {
+ mModelHelper.createGrid(new int[][][]{{
{ APP_ICON, APP_ICON, SHORTCUT, SHORTCUT},
{ SHORTCUT, SHORTCUT, NO__ICON, NO__ICON},
{ NO__ICON, NO__ICON, SHORTCUT, SHORTCUT},
@@ -81,7 +101,7 @@
assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
- addItem(1, 2, DESKTOP, 1, 1);
+ mModelHelper.addItem(1, 2, DESKTOP, 1, 1);
assertFalse(tableExists(mDb, BACKUP_TABLE_NAME));
}
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 53f6a06..1ed4bca 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -1,25 +1,31 @@
package com.android.launcher3.model;
import static com.android.launcher3.model.GridSizeMigrationTask.getWorkspaceScreenIds;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.HOTSEAT;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import android.content.Context;
import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
import android.graphics.Point;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.config.FlagOverrideRule;
import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask;
import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
import java.util.HashSet;
import java.util.LinkedList;
@@ -27,30 +33,35 @@
/**
* Unit tests for {@link GridSizeMigrationTask}
*/
-@RunWith(RobolectricTestRunner.class)
-public class GridSizeMigrationTaskTest extends BaseGridChangesTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+public class GridSizeMigrationTaskTest {
- @Rule
- public final FlagOverrideRule flags = new FlagOverrideRule();
+ private LauncherModelHelper mModelHelper;
+ private Context mContext;
+ private SQLiteDatabase mDb;
private HashSet<String> mValidPackages;
private InvariantDeviceProfile mIdp;
@Before
public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+ mContext = RuntimeEnvironment.application;
+ mDb = mModelHelper.provider.getDb();
+
mValidPackages = new HashSet<>();
mValidPackages.add(TEST_PACKAGE);
- mIdp = new InvariantDeviceProfile();
+ mIdp = InvariantDeviceProfile.INSTANCE.get(mContext);
}
@Test
public void testHotseatMigration_apps_dropped() throws Exception {
int[] hotseatItems = {
- addItem(APP_ICON, 0, HOTSEAT, 0, 0),
- addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
+ mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0),
+ mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
-1,
- addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
- addItem(APP_ICON, 4, HOTSEAT, 0, 0),
+ mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
+ mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0),
};
mIdp.numHotseatIcons = 3;
@@ -63,11 +74,11 @@
@Test
public void testHotseatMigration_shortcuts_dropped() throws Exception {
int[] hotseatItems = {
- addItem(APP_ICON, 0, HOTSEAT, 0, 0),
- addItem(30, 1, HOTSEAT, 0, 0),
+ mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0),
+ mModelHelper.addItem(30, 1, HOTSEAT, 0, 0),
-1,
- addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
- addItem(10, 4, HOTSEAT, 0, 0),
+ mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
+ mModelHelper.addItem(10, 4, HOTSEAT, 0, 0),
};
mIdp.numHotseatIcons = 3;
@@ -109,7 +120,7 @@
@Test
public void testWorkspace_empty_row_column_removed() throws Exception {
- int[][][] ids = createGrid(new int[][][]{{
+ int[][][] ids = mModelHelper.createGrid(new int[][][]{{
{ 0, 0, -1, 1},
{ 3, 1, -1, 4},
{ -1, -1, -1, -1},
@@ -129,7 +140,7 @@
@Test
public void testWorkspace_new_screen_created() throws Exception {
- int[][][] ids = createGrid(new int[][][]{{
+ int[][][] ids = mModelHelper.createGrid(new int[][][]{{
{ 0, 0, 0, 1},
{ 3, 1, 0, 4},
{ -1, -1, -1, -1},
@@ -151,7 +162,7 @@
@Test
public void testWorkspace_items_merged_in_next_screen() throws Exception {
- int[][][] ids = createGrid(new int[][][]{{
+ int[][][] ids = mModelHelper.createGrid(new int[][][]{{
{ 0, 0, 0, 1},
{ 3, 1, 0, 4},
{ -1, -1, -1, -1},
@@ -179,9 +190,9 @@
@Test
public void testWorkspace_items_not_merged_in_next_screen() throws Exception {
- // First screen has 2 items that need to be moved, but second screen has only one
+ // First screen has 2 mItems that need to be moved, but second screen has only one
// empty space after migration (top-left corner)
- int[][][] ids = createGrid(new int[][][]{{
+ int[][][] ids = mModelHelper.createGrid(new int[][][]{{
{ 0, 0, 0, 1},
{ 3, 1, 0, 4},
{ -1, -1, -1, -1},
@@ -217,7 +228,7 @@
}
// The first screen has one item on the 4th column which needs moving, as the first row
// will be kept empty.
- int[][][] ids = createGrid(new int[][][]{{
+ int[][][] ids = mModelHelper.createGrid(new int[][][]{{
{ -1, -1, -1, -1},
{ 3, 1, 7, 0},
{ 8, 7, 7, -1},
@@ -244,7 +255,7 @@
return;
}
// Items will get moved to the next screen to keep the first screen empty.
- int[][][] ids = createGrid(new int[][][]{{
+ int[][][] ids = mModelHelper.createGrid(new int[][][]{{
{ -1, -1, -1, -1},
{ 0, 1, 0, 0},
{ 8, 7, 7, -1},
@@ -266,7 +277,7 @@
}
/**
- * Verifies that the workspace items are arranged in the provided order.
+ * Verifies that the workspace mItems are arranged in the provided order.
* @param ids A 3d array where the first dimension represents the screen, and the rest two
* represent the workspace grid.
*/
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
similarity index 74%
rename from tests/src/com/android/launcher3/model/LoaderCursorTest.java
rename to robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 0dcfaa8..7fa3ee9 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.launcher3.model;
import static com.android.launcher3.LauncherSettings.Favorites.CELLX;
@@ -17,6 +33,7 @@
import static com.android.launcher3.LauncherSettings.Favorites.SCREEN;
import static com.android.launcher3.LauncherSettings.Favorites.TITLE;
import static com.android.launcher3.LauncherSettings.Favorites._ID;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
@@ -24,69 +41,57 @@
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.LauncherApps;
import android.database.MatrixCursor;
-import android.graphics.Bitmap;
import android.os.Process;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import com.android.launcher3.util.PackageManagerHelper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
/**
* Tests for {@link LoaderCursor}
*/
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
public class LoaderCursorTest {
- private LauncherAppState mMockApp;
- private IconCache mMockIconCache;
+ private LauncherAppState mApp;
private MatrixCursor mCursor;
private InvariantDeviceProfile mIDP;
private Context mContext;
- private LauncherApps mLauncherApps;
private LoaderCursor mLoaderCursor;
@Before
public void setup() {
- mIDP = new InvariantDeviceProfile();
+ mContext = RuntimeEnvironment.application;
+ mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
+ mApp = LauncherAppState.getInstance(mContext);
+
mCursor = new MatrixCursor(new String[] {
ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
_ID, CONTAINER, ITEM_TYPE, PROFILE_ID,
SCREEN, CELLX, CELLY, RESTORED, INTENT
});
- mContext = InstrumentationRegistry.getTargetContext();
- mMockApp = mock(LauncherAppState.class);
- mMockIconCache = mock(IconCache.class);
- when(mMockApp.getIconCache()).thenReturn(mMockIconCache);
- when(mMockApp.getInvariantDeviceProfile()).thenReturn(mIDP);
- when(mMockApp.getContext()).thenReturn(mContext);
- mLauncherApps = mContext.getSystemService(LauncherApps.class);
-
- mLoaderCursor = new LoaderCursor(mCursor, mMockApp);
+ mLoaderCursor = new LoaderCursor(mCursor, mApp);
mLoaderCursor.allUsers.put(0, Process.myUserHandle());
}
@@ -109,26 +114,31 @@
}
@Test
- public void getAppShortcutInfo_dontAllowMissing_validComponent() {
+ public void getAppShortcutInfo_dontAllowMissing_validComponent() throws Exception {
+ ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_PACKAGE);
+ shadowOf(mContext.getPackageManager()).addActivityIfNotPresent(cn);
+
initCursor(ITEM_TYPE_APPLICATION, "");
assertTrue(mLoaderCursor.moveToNext());
- ComponentName cn = mLauncherApps.getActivityList(null, mLoaderCursor.user)
- .get(0).getComponentName();
- WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
- new Intent().setComponent(cn), false /* allowMissingTarget */, true);
+ WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
+ mLoaderCursor.getAppShortcutInfo(
+ new Intent().setComponent(cn), false /* allowMissingTarget */, true))
+ .get();
assertNotNull(info);
assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
}
@Test
- public void getAppShortcutInfo_allowMissing_invalidComponent() {
+ public void getAppShortcutInfo_allowMissing_invalidComponent() throws Exception {
initCursor(ITEM_TYPE_APPLICATION, "");
assertTrue(mLoaderCursor.moveToNext());
ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
- WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
- new Intent().setComponent(cn), true /* allowMissingTarget */, true);
+ WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
+ mLoaderCursor.getAppShortcutInfo(
+ new Intent().setComponent(cn), true /* allowMissingTarget */, true))
+ .get();
assertNotNull(info);
assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
}
@@ -138,11 +148,8 @@
initCursor(ITEM_TYPE_SHORTCUT, "my-shortcut");
assertTrue(mLoaderCursor.moveToNext());
- Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
- when(mMockIconCache.getDefaultIcon(eq(mLoaderCursor.user)))
- .thenReturn(BitmapInfo.fromBitmap(icon));
WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
- assertEquals(icon, info.bitmap.icon);
+ assertTrue(mApp.getIconCache().isDefaultIcon(info.bitmap, info.user));
assertEquals("my-shortcut", info.title);
assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
}
@@ -164,7 +171,7 @@
mIDP.numColumns = 4;
mIDP.numHotseatIcons = 3;
- // Overlapping items are not placed
+ // Overlapping mItems are not placed
assertTrue(mLoaderCursor.checkItemPlacement(
newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
assertFalse(mLoaderCursor.checkItemPlacement(
@@ -190,7 +197,7 @@
mIDP.numColumns = 4;
mIDP.numHotseatIcons = 3;
- // Hotseat items are only placed based on screenId
+ // Hotseat mItems are only placed based on screenId
assertTrue(mLoaderCursor.checkItemPlacement(
newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1)));
assertTrue(mLoaderCursor.checkItemPlacement(
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
new file mode 100644
index 0000000..c7979b2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.util.Executors.createAndStartNewForegroundLooper;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.spy;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Process;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+import com.android.launcher3.util.LooperExecutor;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Tests to verify multiple callbacks in Loader
+ */
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class ModelMultiCallbacksTest {
+
+ private LauncherModelHelper mModelHelper;
+
+ private ShadowPackageManager mSpm;
+ private LooperExecutor mTempMainExecutor;
+
+ @Before
+ public void setUp() throws Exception {
+ mModelHelper = new LauncherModelHelper();
+ mModelHelper.installApp(TEST_PACKAGE);
+
+ mSpm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+
+ // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
+ // so that we can wait appropriately for the loader to complete.
+ mTempMainExecutor = new LooperExecutor(createAndStartNewForegroundLooper("tempMain"));
+ ReflectionHelpers.setField(mModelHelper.getModel(), "mMainExecutor", mTempMainExecutor);
+ }
+
+ @Test
+ public void testTwoCallbacks_loadedTogether() throws Exception {
+ setupWorkspacePages(3);
+
+ MyCallbacks cb1 = spy(MyCallbacks.class);
+ mModelHelper.getModel().addCallbacksAndLoad(cb1);
+
+ waitForLoaderAndTempMainThread();
+ cb1.verifySynchronouslyBound(3);
+
+ // Add a new callback
+ cb1.reset();
+ MyCallbacks cb2 = spy(MyCallbacks.class);
+ cb2.mPageToBindSync = 2;
+ mModelHelper.getModel().addCallbacksAndLoad(cb2);
+
+ waitForLoaderAndTempMainThread();
+ cb1.verifySynchronouslyBound(3);
+ cb2.verifySynchronouslyBound(3);
+
+ // Remove callbacks
+ cb1.reset();
+ cb2.reset();
+
+ // No effect on callbacks when removing an callback
+ mModelHelper.getModel().removeCallbacks(cb2);
+ waitForLoaderAndTempMainThread();
+ assertNull(cb1.mDeferredExecutor);
+ assertNull(cb2.mDeferredExecutor);
+
+ // Reloading only loads registered callbacks
+ mModelHelper.getModel().startLoader();
+ waitForLoaderAndTempMainThread();
+ cb1.verifySynchronouslyBound(3);
+ assertNull(cb2.mDeferredExecutor);
+ }
+
+ @Test
+ public void testTwoCallbacks_receiveUpdates() throws Exception {
+ setupWorkspacePages(1);
+
+ MyCallbacks cb1 = spy(MyCallbacks.class);
+ MyCallbacks cb2 = spy(MyCallbacks.class);
+ mModelHelper.getModel().addCallbacksAndLoad(cb1);
+ mModelHelper.getModel().addCallbacksAndLoad(cb2);
+ waitForLoaderAndTempMainThread();
+
+ cb1.verifyApps(TEST_PACKAGE);
+ cb2.verifyApps(TEST_PACKAGE);
+
+ // Install package 1
+ String pkg1 = "com.test.pkg1";
+ mModelHelper.installApp(pkg1);
+ mModelHelper.getModel().onPackageAdded(pkg1, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ cb1.verifyApps(TEST_PACKAGE, pkg1);
+ cb2.verifyApps(TEST_PACKAGE, pkg1);
+
+ // Install package 2
+ String pkg2 = "com.test.pkg2";
+ mModelHelper.installApp(pkg2);
+ mModelHelper.getModel().onPackageAdded(pkg2, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ cb1.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+ cb2.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+
+ // Uninstall package 2
+ mSpm.removePackage(pkg1);
+ mModelHelper.getModel().onPackageRemoved(pkg1, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ cb1.verifyApps(TEST_PACKAGE, pkg2);
+ cb2.verifyApps(TEST_PACKAGE, pkg2);
+
+ // Unregister a callback and verify updates no longer received
+ mModelHelper.getModel().removeCallbacks(cb2);
+ mSpm.removePackage(pkg2);
+ mModelHelper.getModel().onPackageRemoved(pkg2, Process.myUserHandle());
+ waitForLoaderAndTempMainThread();
+ cb1.verifyApps(TEST_PACKAGE);
+ cb2.verifyApps(TEST_PACKAGE, pkg2);
+ }
+
+ private void waitForLoaderAndTempMainThread() throws Exception {
+ Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+ mTempMainExecutor.submit(() -> { }).get();
+ }
+
+ private void setupWorkspacePages(int pageCount) throws Exception {
+ // Create a layout with 3 pages
+ LauncherLayoutBuilder builder = new LauncherLayoutBuilder();
+ for (int i = 0; i < pageCount; i++) {
+ builder.atWorkspace(1, 1, i).putApp(TEST_PACKAGE, TEST_PACKAGE);
+ }
+ mModelHelper.setupDefaultLayoutProvider(builder);
+ }
+
+ private abstract static class MyCallbacks implements Callbacks {
+
+ final List<ItemInfo> mItems = new ArrayList<>();
+ int mPageToBindSync = 0;
+ int mPageBoundSync = PagedView.INVALID_PAGE;
+ ViewOnDrawExecutor mDeferredExecutor;
+ AppInfo[] mAppInfos;
+
+ MyCallbacks() { }
+
+ @Override
+ public void onPageBoundSynchronously(int page) {
+ mPageBoundSync = page;
+ }
+
+ @Override
+ public void executeOnNextDraw(ViewOnDrawExecutor executor) {
+ mDeferredExecutor = executor;
+ }
+
+ @Override
+ public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
+ mItems.addAll(shortcuts);
+ }
+
+ @Override
+ public void bindAllApplications(AppInfo[] apps) {
+ mAppInfos = apps;
+ }
+
+ @Override
+ public int getPageToBindSynchronously() {
+ return mPageToBindSync;
+ }
+
+ public void reset() {
+ mItems.clear();
+ mPageBoundSync = PagedView.INVALID_PAGE;
+ mDeferredExecutor = null;
+ mAppInfos = null;
+ }
+
+ public void verifySynchronouslyBound(int totalItems) {
+ // Verify that the requested page is bound synchronously
+ assertEquals(mPageBoundSync, mPageToBindSync);
+ assertEquals(mItems.size(), 1);
+ assertEquals(mItems.get(0).screenId, mPageBoundSync);
+ assertNotNull(mDeferredExecutor);
+
+ // Verify that all other pages are bound properly
+ mDeferredExecutor.runAllTasks();
+ assertEquals(mItems.size(), totalItems);
+ }
+
+ public void verifyApps(String... apps) {
+ assertEquals(apps.length, mAppInfos.length);
+ assertEquals(Arrays.stream(mAppInfos)
+ .map(ai -> ai.getTargetComponent().getPackageName())
+ .collect(Collectors.toSet()),
+ new HashSet<>(Arrays.asList(apps)));
+ }
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index a1a4561..bd71f01 100644
--- a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -6,11 +6,14 @@
import com.android.launcher3.LauncherAppWidgetInfo;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
import java.util.Arrays;
import java.util.HashSet;
@@ -18,12 +21,16 @@
/**
* Tests for {@link PackageInstallStateChangedTask}
*/
-@RunWith(RobolectricTestRunner.class)
-public class PackageInstallStateChangedTaskTest extends BaseModelUpdateTaskTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class PackageInstallStateChangedTaskTest {
+
+ private LauncherModelHelper mModelHelper;
@Before
- public void initData() throws Exception {
- initializeData("/package_install_state_change_task_data.txt");
+ public void setup() throws Exception {
+ mModelHelper = new LauncherModelHelper();
+ mModelHelper.initializeData("/package_install_state_change_task_data.txt");
}
private PackageInstallStateChangedTask newTask(String pkg, int progress) {
@@ -35,7 +42,7 @@
@Test
public void testSessionUpdate_ignore_installed() throws Exception {
- executeTaskForTest(newTask("app1", 30));
+ mModelHelper.executeTaskForTest(newTask("app1", 30));
// No shortcuts were updated
verifyProgressUpdate(0);
@@ -43,21 +50,21 @@
@Test
public void testSessionUpdate_shortcuts_updated() throws Exception {
- executeTaskForTest(newTask("app3", 30));
+ mModelHelper.executeTaskForTest(newTask("app3", 30));
verifyProgressUpdate(30, 5, 6, 7);
}
@Test
public void testSessionUpdate_widgets_updated() throws Exception {
- executeTaskForTest(newTask("app4", 30));
+ mModelHelper.executeTaskForTest(newTask("app4", 30));
verifyProgressUpdate(30, 8, 9);
}
private void verifyProgressUpdate(int progress, Integer... idsUpdated) {
HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
- for (ItemInfo info : bgDataModel.itemsIdMap) {
+ for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
if (info instanceof WorkspaceItemInfo) {
assertEquals(updates.contains(info.id) ? progress: 0,
((WorkspaceItemInfo) info).getInstallProgress());
diff --git a/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java b/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
index 83bf7da..7612ae1 100644
--- a/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
@@ -27,9 +27,10 @@
import android.content.pm.ShortcutInfo;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
@@ -39,7 +40,7 @@
/**
* Tests the sorting and filtering of shortcuts in {@link PopupPopulator}.
*/
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
public class PopupPopulatorTest {
@Test
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
similarity index 78%
rename from tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
rename to robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index 27990f4..7ef670c 100644
--- a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.launcher3.provider;
import static org.junit.Assert.assertEquals;
@@ -6,21 +21,18 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
-
import com.android.launcher3.LauncherProvider.DatabaseHelper;
import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
/**
* Tests for {@link RestoreDbTask}
*/
-@MediumTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(LauncherRoboTestRunner.class)
public class RestoreDbTaskTest {
@Test
@@ -83,7 +95,7 @@
private final long mProfileId;
MyDatabaseHelper(long profileId) {
- super(InstrumentationRegistry.getContext(), null);
+ super(RuntimeEnvironment.application, null);
mProfileId = profileId;
}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java
new file mode 100644
index 0000000..696ffd0
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.os.Process;
+import android.os.UserHandle;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowAppWidgetManager;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Extension of {@link ShadowAppWidgetManager} with missing shadow methods
+ */
+@Implements(value = AppWidgetManager.class)
+public class LShadowAppWidgetManager extends ShadowAppWidgetManager {
+
+ @Override
+ protected List<AppWidgetProviderInfo> getInstalledProviders() {
+ return getInstalledProvidersForProfile(null);
+ }
+
+ @Implementation
+ public List<AppWidgetProviderInfo> getInstalledProvidersForProfile(UserHandle profile) {
+ UserHandle user = profile == null ? Process.myUserHandle() : profile;
+ return super.getInstalledProviders().stream().filter(
+ info -> user.equals(info.getProfile())).collect(Collectors.toList());
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java
new file mode 100644
index 0000000..abd90bb
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import android.graphics.Bitmap;
+import android.graphics.Paint;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowBitmap;
+
+/**
+ * Extension of {@link ShadowBitmap} with missing shadow methods
+ */
+@Implements(value = Bitmap.class)
+public class LShadowBitmap extends ShadowBitmap {
+
+ @Implementation
+ protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
+ return extractAlpha();
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
index 204ec9b..166e28b 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -43,6 +43,7 @@
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
/**
* Extension of {@link ShadowLauncherApps} with missing shadow methods
@@ -77,7 +78,7 @@
protected LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) {
ResolveInfo ri = RuntimeEnvironment.application.getPackageManager()
.resolveActivity(intent, 0);
- return getLauncherActivityInfo(ri.activityInfo);
+ return ri == null ? null : getLauncherActivityInfo(ri.activityInfo);
}
public LauncherActivityInfo getLauncherActivityInfo(ActivityInfo activityInfo) {
@@ -93,4 +94,26 @@
return RuntimeEnvironment.application.getPackageManager()
.getApplicationInfo(packageName, flags);
}
+
+ @Implementation
+ public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
+ Intent intent = new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setPackage(packageName);
+ return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
+ .stream()
+ .map(ri -> getLauncherActivityInfo(ri.activityInfo))
+ .collect(Collectors.toList());
+ }
+
+ @Implementation
+ public boolean hasShortcutHostPermission() {
+ return true;
+ }
+
+ @Override
+ protected List<LauncherActivityInfo> getShortcutConfigActivityList(String packageName,
+ UserHandle user) {
+ return Collections.emptyList();
+ }
}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
index edf8edb..576ddbd 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
@@ -16,6 +16,7 @@
package com.android.launcher3.shadows;
+import android.os.Parcel;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.SparseBooleanArray;
@@ -50,4 +51,12 @@
public void setUserLocked(UserHandle userHandle, boolean enabled) {
mLockedUsers.put(userHandle.hashCode(), enabled);
}
+
+ // Create user handle from parcel since UserHandle.of() was only added in later APIs.
+ public static UserHandle newUserHandle(int uid) {
+ Parcel userParcel = Parcel.obtain();
+ userParcel.writeInt(uid);
+ userParcel.setDataPosition(0);
+ return new UserHandle(userParcel);
+ }
}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
new file mode 100644
index 0000000..344f532
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowDeviceFlag.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import android.content.Context;
+
+import com.android.launcher3.uioverrides.DeviceFlag;
+import com.android.launcher3.util.LooperExecutor;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
+ */
+@Implements(value = DeviceFlag.class, isInAndroidSdk = false)
+public class ShadowDeviceFlag {
+
+ /**
+ * Mock change listener as it uses internal system classes not available to robolectric
+ */
+ @Implementation
+ protected void addChangeListener(Context context, Runnable r) { }
+
+ @Implementation
+ protected static boolean getDeviceValue(String key, boolean defaultValue) {
+ return defaultValue;
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
index d56de3c..a3b7dc7 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
@@ -18,25 +18,16 @@
import static com.android.launcher3.util.Executors.createAndStartNewLooper;
-import static org.robolectric.shadow.api.Shadow.invokeConstructor;
-import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
+import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.util.ReflectionHelpers.setField;
import android.os.Handler;
-import android.os.Looper;
-import com.android.launcher3.util.Executors;
import com.android.launcher3.util.LooperExecutor;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Set;
-import java.util.WeakHashMap;
/**
* Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
@@ -44,25 +35,18 @@
@Implements(value = LooperExecutor.class, isInAndroidSdk = false)
public class ShadowLooperExecutor {
- // Keep reference to all created Loopers so they can be torn down after test
- private static Set<LooperExecutor> executors =
- Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
-
- @RealObject private LooperExecutor realExecutor;
+ @RealObject private LooperExecutor mRealExecutor;
@Implementation
- protected void __constructor__(Looper looper) {
- invokeConstructor(LooperExecutor.class, realExecutor, from(Looper.class, looper));
- executors.add(realExecutor);
- }
-
- /**
- * Re-initializes any executor which may have been reset when a test finished
- */
- public static void reinitializeStaticExecutors() {
- for (LooperExecutor executor : new ArrayList<>(executors)) {
- setField(executor, "mHandler",
- new Handler(createAndStartNewLooper(executor.getThread().getName())));
+ protected Handler getHandler() {
+ Handler handler = directlyOn(mRealExecutor, LooperExecutor.class, "getHandler");
+ Thread thread = handler.getLooper().getThread();
+ if (!thread.isAlive()) {
+ // Robolectric destroys all loopers at the end of every test. Since Launcher maintains
+ // some static threads, they need to be reinitialized in case they were destroyed.
+ setField(mRealExecutor, "mHandler",
+ new Handler(createAndStartNewLooper(thread.getName())));
}
+ return directlyOn(mRealExecutor, LooperExecutor.class, "getHandler");
}
}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java
new file mode 100644
index 0000000..6e2ccf8
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shadows;
+
+import static org.robolectric.shadow.api.Shadow.invokeConstructor;
+import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
+
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.MainThreadInitializedObject.ObjectProvider;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Shadow for {@link MainThreadInitializedObject} to provide reset functionality for static sObjects
+ */
+@Implements(value = MainThreadInitializedObject.class, isInAndroidSdk = false)
+public class ShadowMainThreadInitializedObject {
+
+ // Keep reference to all created MainThreadInitializedObject so they can be cleared after test
+ private static Set<MainThreadInitializedObject> sObjects =
+ Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
+
+ @RealObject private MainThreadInitializedObject mRealObject;
+
+ @Implementation
+ protected void __constructor__(ObjectProvider provider) {
+ invokeConstructor(MainThreadInitializedObject.class, mRealObject,
+ from(ObjectProvider.class, provider));
+ sObjects.add(mRealObject);
+ }
+
+ /**
+ * Resets all the initialized sObjects to be null
+ */
+ public static void resetInitializedObjects() {
+ for (MainThreadInitializedObject object : new ArrayList<>(sObjects)) {
+ object.initializeForTesting(null);
+ }
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java b/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
index aa51ad2..e453e31 100644
--- a/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
@@ -2,7 +2,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -11,7 +10,7 @@
/**
* Unit tests for {@link GridOccupancy}
*/
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
public class GridOccupancyTest {
@Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java b/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
index c08e198..5974ea5 100644
--- a/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
@@ -19,12 +19,11 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
/**
* Robolectric unit tests for {@link IntArray}
*/
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
public class IntArrayTest {
@Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
index 8513353..aedf71e 100644
--- a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
@@ -20,8 +20,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -29,7 +27,7 @@
/**
* Robolectric unit tests for {@link IntSet}
*/
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
public class IntSetTest {
@Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
new file mode 100644
index 0000000..e8b7157
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.Settings;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BgDataModel;
+
+import org.mockito.ArgumentCaptor;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+
+/**
+ * Utility class to help manage Launcher Model and related objects for test.
+ */
+public class LauncherModelHelper {
+
+ public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+ public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+
+ public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+ public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+ public static final int NO__ICON = -1;
+ public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
+
+ // Authority for providing a dummy default-workspace-layout data.
+ private static final String TEST_PROVIDER_AUTHORITY =
+ LauncherModelHelper.class.getName().toLowerCase();
+ private static final int DEFAULT_BITMAP_SIZE = 10;
+ private static final int DEFAULT_GRID_SIZE = 4;
+
+
+ private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>();
+ public final TestLauncherProvider provider;
+
+ private BgDataModel mDataModel;
+ private AllAppsList mAllAppsList;
+
+ public LauncherModelHelper() {
+ provider = Robolectric.setupContentProvider(TestLauncherProvider.class);
+ ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider);
+ }
+
+ public LauncherModel getModel() {
+ return LauncherAppState.getInstance(RuntimeEnvironment.application).getModel();
+ }
+
+ public synchronized BgDataModel getBgDataModel() {
+ if (mDataModel == null) {
+ mDataModel = ReflectionHelpers.getField(getModel(), "mBgDataModel");
+ }
+ return mDataModel;
+ }
+
+ public synchronized AllAppsList getAllAppsList() {
+ if (mAllAppsList == null) {
+ mAllAppsList = ReflectionHelpers.getField(getModel(), "mBgAllAppsList");
+ }
+ return mAllAppsList;
+ }
+
+ /**
+ * Synchronously executes the task and returns all the UI callbacks posted.
+ */
+ public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception {
+ LauncherModel model = getModel();
+ if (!model.isModelLoaded()) {
+ ReflectionHelpers.setField(model, "mModelLoaded", true);
+ }
+ Executor mockExecutor = mock(Executor.class);
+ model.enqueueModelUpdateTask(new ModelUpdateTask() {
+ @Override
+ public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
+ AllAppsList allAppsList, Executor uiExecutor) {
+ task.init(app, model, dataModel, allAppsList, mockExecutor);
+ }
+
+ @Override
+ public void run() {
+ task.run();
+ }
+ });
+ MODEL_EXECUTOR.submit(() -> null).get();
+
+ ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mockExecutor, atLeast(0)).execute(captor.capture());
+ return captor.getAllValues();
+ }
+
+ /**
+ * Synchronously executes a task on the model
+ */
+ public <T> T executeSimpleTask(Function<BgDataModel, T> task) throws Exception {
+ BgDataModel dataModel = getBgDataModel();
+ return MODEL_EXECUTOR.submit(() -> task.apply(dataModel)).get();
+ }
+
+ /**
+ * Initializes mock data for the test.
+ */
+ public void initializeData(String resourceName) throws Exception {
+ Context targetContext = RuntimeEnvironment.application;
+ BgDataModel bgDataModel = getBgDataModel();
+ AllAppsList allAppsList = getAllAppsList();
+
+ MODEL_EXECUTOR.submit(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+ this.getClass().getResourceAsStream(resourceName)))) {
+ String line;
+ HashMap<String, Class> classMap = new HashMap<>();
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.startsWith("#") || line.isEmpty()) {
+ continue;
+ }
+ String[] commands = line.split(" ");
+ switch (commands[0]) {
+ case "classMap":
+ classMap.put(commands[1], Class.forName(commands[2]));
+ break;
+ case "bgItem":
+ bgDataModel.addItem(targetContext,
+ (ItemInfo) initItem(classMap.get(commands[1]), commands, 2),
+ false);
+ break;
+ case "allApps":
+ allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
+ break;
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }).get();
+ }
+
+ private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
+ HashMap<String, Field> cache = mFieldCache.get(clazz);
+ if (cache == null) {
+ cache = new HashMap<>();
+ Class c = clazz;
+ while (c != null) {
+ for (Field f : c.getDeclaredFields()) {
+ f.setAccessible(true);
+ cache.put(f.getName(), f);
+ }
+ c = c.getSuperclass();
+ }
+ mFieldCache.put(clazz, cache);
+ }
+
+ Object item = clazz.newInstance();
+ for (int i = startIndex; i < fieldDef.length; i++) {
+ String[] fieldData = fieldDef[i].split("=", 2);
+ Field f = cache.get(fieldData[0]);
+ Class type = f.getType();
+ if (type == int.class || type == long.class) {
+ f.set(item, Integer.parseInt(fieldData[1]));
+ } else if (type == CharSequence.class || type == String.class) {
+ f.set(item, fieldData[1]);
+ } else if (type == Intent.class) {
+ if (!fieldData[1].startsWith("#Intent")) {
+ fieldData[1] = "#Intent;" + fieldData[1] + ";end";
+ }
+ f.set(item, Intent.parseUri(fieldData[1], 0));
+ } else if (type == ComponentName.class) {
+ f.set(item, ComponentName.unflattenFromString(fieldData[1]));
+ } else {
+ throw new Exception("Added parsing logic for "
+ + f.getName() + " of type " + f.getType());
+ }
+ }
+ return item;
+ }
+
+ /**
+ * Adds a dummy item in the DB.
+ * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
+ * folder (where the type represents the number of items in the folder).
+ */
+ public int addItem(int type, int screen, int container, int x, int y) {
+ Context context = RuntimeEnvironment.application;
+ int id = LauncherSettings.Settings.call(context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
+ .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+
+ ContentValues values = new ContentValues();
+ values.put(LauncherSettings.Favorites._ID, id);
+ values.put(LauncherSettings.Favorites.CONTAINER, container);
+ values.put(LauncherSettings.Favorites.SCREEN, screen);
+ values.put(LauncherSettings.Favorites.CELLX, x);
+ values.put(LauncherSettings.Favorites.CELLY, y);
+ values.put(LauncherSettings.Favorites.SPANX, 1);
+ values.put(LauncherSettings.Favorites.SPANY, 1);
+
+ if (type == APP_ICON || type == SHORTCUT) {
+ values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
+ values.put(LauncherSettings.Favorites.INTENT,
+ new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
+ } else {
+ values.put(LauncherSettings.Favorites.ITEM_TYPE,
+ LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
+ // Add folder items.
+ for (int i = 0; i < type; i++) {
+ addItem(APP_ICON, 0, id, 0, 0);
+ }
+ }
+
+ context.getContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
+ return id;
+ }
+
+ public int[][][] createGrid(int[][][] typeArray) {
+ return createGrid(typeArray, 1);
+ }
+
+ /**
+ * Initializes the DB with dummy elements to represent the provided grid structure.
+ * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
+ * type definitions. The first dimension represents the screens and the next
+ * two represent the workspace grid.
+ * @param startScreen First screen id from where the icons will be added.
+ * @return the same grid representation where each entry is the corresponding item id.
+ */
+ public int[][][] createGrid(int[][][] typeArray, int startScreen) {
+ Context context = RuntimeEnvironment.application;
+ LauncherSettings.Settings.call(context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+ LauncherSettings.Settings.call(context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
+ int[][][] ids = new int[typeArray.length][][];
+
+ for (int i = 0; i < typeArray.length; i++) {
+ // Add screen to DB
+ int screenId = startScreen + i;
+
+ // Keep the screen id counter up to date
+ LauncherSettings.Settings.call(context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
+
+ ids[i] = new int[typeArray[i].length][];
+ for (int y = 0; y < typeArray[i].length; y++) {
+ ids[i][y] = new int[typeArray[i][y].length];
+ for (int x = 0; x < typeArray[i][y].length; x++) {
+ if (typeArray[i][y][x] < 0) {
+ // Empty cell
+ ids[i][y][x] = -1;
+ } else {
+ ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
+ }
+ }
+ }
+ }
+
+ return ids;
+ }
+
+ /**
+ * Sets up a dummy provider to load the provided layout by default, next time the layout loads
+ */
+ public void setupDefaultLayoutProvider(LauncherLayoutBuilder builder) throws Exception {
+ Context context = RuntimeEnvironment.application;
+ InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
+ idp.numRows = idp.numColumns = idp.numHotseatIcons = DEFAULT_GRID_SIZE;
+ idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
+
+ Settings.Secure.putString(context.getContentResolver(),
+ "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
+
+ shadowOf(context.getPackageManager())
+ .addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
+ TEST_PROVIDER_AUTHORITY;
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ builder.build(new OutputStreamWriter(bos));
+ Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, context);
+ shadowOf(context.getContentResolver()).registerInputStream(layoutUri,
+ new ByteArrayInputStream(bos.toByteArray()));
+ }
+
+ /**
+ * Simulates an apk install with a default main activity with same class and package name
+ */
+ public void installApp(String component) throws NameNotFoundException {
+ ShadowPackageManager spm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+ ComponentName cn = new ComponentName(component, component);
+ spm.addActivityIfNotPresent(cn);
+
+ IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
+ filter.addCategory(Intent.CATEGORY_LAUNCHER);
+ filter.addCategory(Intent.CATEGORY_DEFAULT);
+ spm.addIntentFilterForActivity(cn, filter);
+ }
+
+ /**
+ * An extension of LauncherProvider backed up by in-memory database.
+ */
+ public static class TestLauncherProvider extends LauncherProvider {
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ public SQLiteDatabase getDb() {
+ createDbIfNotExists();
+ return mOpenHelper.getWritableDatabase();
+ }
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java b/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java
new file mode 100644
index 0000000..b8fff9c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static org.mockito.Mockito.mock;
+
+import com.android.launcher3.shadows.LShadowAppWidgetManager;
+import com.android.launcher3.shadows.LShadowBitmap;
+import com.android.launcher3.shadows.LShadowLauncherApps;
+import com.android.launcher3.shadows.LShadowUserManager;
+import com.android.launcher3.shadows.ShadowLooperExecutor;
+import com.android.launcher3.shadows.ShadowMainThreadInitializedObject;
+import com.android.launcher3.shadows.ShadowDeviceFlag;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.DefaultTestLifecycle;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.TestLifecycle;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+
+import java.lang.reflect.Method;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Test runner with Launcher specific configurations
+ */
+public class LauncherRoboTestRunner extends RobolectricTestRunner {
+
+ private static final Class<?>[] SHADOWS = new Class<?>[] {
+ LShadowAppWidgetManager.class,
+ LShadowUserManager.class,
+ LShadowLauncherApps.class,
+ LShadowBitmap.class,
+
+ ShadowLooperExecutor.class,
+ ShadowMainThreadInitializedObject.class,
+ ShadowDeviceFlag.class,
+ };
+
+ public LauncherRoboTestRunner(Class<?> testClass) throws InitializationError {
+ super(testClass);
+ }
+
+ @Override
+ protected Config buildGlobalConfig() {
+ return new Config.Builder().setShadows(SHADOWS).build();
+ }
+
+ @Nonnull
+ @Override
+ protected Class<? extends TestLifecycle> getTestLifecycleClass() {
+ return LauncherTestLifecycle.class;
+ }
+
+ public static class LauncherTestLifecycle extends DefaultTestLifecycle {
+
+ @Override
+ public void beforeTest(Method method) {
+ super.beforeTest(method);
+ ShadowLog.stream = System.out;
+
+ // Disable plugins
+ PluginManagerWrapper.INSTANCE.initializeForTesting(mock(PluginManagerWrapper.class));
+ }
+
+ @Override
+ public void afterTest(Method method) {
+ super.afterTest(method);
+
+ ShadowLog.stream = null;
+ ShadowMainThreadInitializedObject.resetInitializedObjects();
+ }
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java b/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
deleted file mode 100644
index 7e873e8..0000000
--- a/robolectric_tests/src/com/android/launcher3/util/TestLauncherProvider.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.android.launcher3.util;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-
-import com.android.launcher3.LauncherProvider;
-
-/**
- * An extension of LauncherProvider backed up by in-memory database.
- */
-public class TestLauncherProvider extends LauncherProvider {
-
- private boolean mAllowLoadDefaultFavorites;
-
- @Override
- public boolean onCreate() {
- return true;
- }
-
- @Override
- protected synchronized void createDbIfNotExists() {
- if (mOpenHelper == null) {
- mOpenHelper = new MyDatabaseHelper(getContext(), mAllowLoadDefaultFavorites);
- }
- }
-
- public void setAllowLoadDefaultFavorites(boolean allowLoadDefaultFavorites) {
- mAllowLoadDefaultFavorites = allowLoadDefaultFavorites;
- }
-
- public SQLiteDatabase getDb() {
- createDbIfNotExists();
- return mOpenHelper.getWritableDatabase();
- }
-
- private static class MyDatabaseHelper extends DatabaseHelper {
-
- private final boolean mAllowLoadDefaultFavorites;
-
- MyDatabaseHelper(Context context, boolean allowLoadDefaultFavorites) {
- super(context, null);
- mAllowLoadDefaultFavorites = allowLoadDefaultFavorites;
- initIds();
- }
-
- @Override
- public long getDefaultUserSerial() {
- return 0;
- }
-
- @Override
- protected void onEmptyDbCreated() {
- if (mAllowLoadDefaultFavorites) {
- super.onEmptyDbCreated();
- }
- }
-
- @Override
- protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { }
- }
-}
diff --git a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
similarity index 83%
rename from tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
rename to robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
index 57b0b09..daae818 100644
--- a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
@@ -19,16 +19,15 @@
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import androidx.recyclerview.widget.RecyclerView;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
@@ -37,19 +36,21 @@
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.PackageItemInfo;
import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.util.MultiHashMap;
+import com.android.launcher3.util.LauncherRoboTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
-import java.util.Map;
+import java.util.Collections;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(LauncherRoboTestRunner.class)
public class WidgetsListAdapterTest {
@Mock private LayoutInflater mMockLayoutInflater;
@@ -64,7 +65,7 @@
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mContext = InstrumentationRegistry.getTargetContext();
+ mContext = RuntimeEnvironment.application;
mTestProfile = new InvariantDeviceProfile();
mTestProfile.numRows = 5;
mTestProfile.numColumns = 5;
@@ -121,15 +122,19 @@
/**
* Helper method to generate the sample widget model map that can be used for the tests
* @param num the number of WidgetItem the map should contain
- * @return
*/
private ArrayList<WidgetListRowEntry> generateSampleMap(int num) {
ArrayList<WidgetListRowEntry> result = new ArrayList<>();
if (num <= 0) return result;
+ ShadowPackageManager spm = shadowOf(mContext.getPackageManager());
- MultiHashMap<PackageItemInfo, WidgetItem> newMap = new MultiHashMap();
- WidgetManagerHelper widgetManager = new WidgetManagerHelper(mContext);
- for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(null)) {
+ for (int i = 0; i < num; i++) {
+ ComponentName cn = new ComponentName("com.dummy.apk" + i, "DummyWidet");
+
+ AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+ widgetInfo.provider = cn;
+ ReflectionHelpers.setField(widgetInfo, "providerInfo", spm.addReceiverIfNotPresent(cn));
+
WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
.fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache);
@@ -137,13 +142,8 @@
pInfo.title = pInfo.packageName;
pInfo.user = wi.user;
pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
- newMap.addToList(pInfo, wi);
- if (newMap.size() == num) {
- break;
- }
- }
- for (Map.Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : newMap.entrySet()) {
- result.add(new WidgetListRowEntry(entry.getKey(), entry.getValue()));
+
+ result.add(new WidgetListRowEntry(pInfo, new ArrayList<>(Collections.singleton(wi))));
}
return result;
diff --git a/settings.gradle b/settings.gradle
index b52bd4f..ce13bfb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,5 @@
include ':IconLoader'
project(':IconLoader').projectDir = new File(rootDir, 'iconloaderlib')
+
+include ':SharedLibWrapper'
+project(':SharedLibWrapper').projectDir = new File(rootDir, 'SharedLibWrapper')
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index af219ba..f76ca50 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -26,6 +26,8 @@
import android.os.UserHandle;
import android.os.UserManager;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageManagerHelper;
@@ -89,6 +91,15 @@
runtimeStatusFlags = info.runtimeStatusFlags;
}
+ @VisibleForTesting
+ public AppInfo(ComponentName componentName, CharSequence title,
+ UserHandle user, Intent intent) {
+ this.componentName = componentName;
+ this.title = title;
+ this.user = user;
+ this.intent = intent;
+ }
+
@Override
protected String dumpProperties() {
return super.dumpProperties() + " componentName=" + componentName;
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index f319ae1..3ca4f59 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -16,6 +16,7 @@
package com.android.launcher3;
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -24,7 +25,12 @@
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
+import android.content.pm.LauncherApps;
import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
import android.view.ContextThemeWrapper;
import androidx.annotation.IntDef;
@@ -47,6 +53,8 @@
public abstract class BaseActivity extends Activity
implements UserEventDelegate, LogStateProvider, ActivityContext {
+ private static final String TAG = "BaseActivity";
+
public static final int INVISIBLE_BY_STATE_HANDLER = 1 << 0;
public static final int INVISIBLE_BY_APP_TRANSITIONS = 1 << 1;
public static final int INVISIBLE_BY_PENDING_FLAGS = 1 << 2;
@@ -278,7 +286,7 @@
/**
* Used to set the override visibility state, used only to handle the transition home with the
* recents animation.
- * @see QuickstepAppTransitionManagerImpl#getWallpaperOpenRunner
+ * @see QuickstepAppTransitionManagerImpl#createWallpaperOpenRunner
*/
public void addForceInvisibleFlag(@InvisibilityFlags int flag) {
mForceInvisible |= flag;
@@ -312,6 +320,22 @@
writer.println(prefix + "mForceInvisible: " + mForceInvisible);
}
+ /**
+ * A wrapper around the platform method with Launcher specific checks
+ */
+ public void startShortcut(String packageName, String id, Rect sourceBounds,
+ Bundle startActivityOptions, UserHandle user) {
+ if (GO_DISABLE_WIDGETS) {
+ return;
+ }
+ try {
+ getSystemService(LauncherApps.class).startShortcut(packageName, id, sourceBounds,
+ startActivityOptions, user);
+ } catch (SecurityException | IllegalStateException e) {
+ Log.e(TAG, "Failed to start shortcut", e);
+ }
+ }
+
public static <T extends BaseActivity> T fromContext(Context context) {
if (context instanceof BaseActivity) {
return (T) context;
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 893f64a..df15fc1 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -35,7 +35,6 @@
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.uioverrides.DisplayRotationListener;
import com.android.launcher3.uioverrides.WallpaperColorInfo;
import com.android.launcher3.util.PackageManagerHelper;
@@ -174,8 +173,8 @@
AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(), user,
sourceContainer);
}
- getUserEventDispatcher().logAppLaunch(v, intent);
- getStatsLogManager().logAppLaunch(v, intent);
+ getUserEventDispatcher().logAppLaunch(v, intent, user);
+ getStatsLogManager().logAppLaunch(v, intent, user);
return true;
} catch (NullPointerException|ActivityNotFoundException|SecurityException e) {
Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
@@ -198,8 +197,7 @@
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
String id = ((WorkspaceItemInfo) info).getDeepShortcutId();
String packageName = intent.getPackage();
- DeepShortcutManager.getInstance(this).startShortcut(
- packageName, id, intent.getSourceBounds(), optsBundle, info.user);
+ startShortcut(packageName, id, intent.getSourceBounds(), optsBundle, info.user);
AppLaunchTracker.INSTANCE.get(this).onStartShortcut(packageName, id, info.user,
sourceContainer);
} else {
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index bd48aec..423f2bb 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -126,7 +126,8 @@
onAccessibilityDrop(null, item);
ModelWriter modelWriter = mLauncher.getModelWriter();
Runnable onUndoClicked = () -> {
- modelWriter.abortDelete(itemPage);
+ mLauncher.setPageToBindSynchronously(itemPage);
+ modelWriter.abortDelete();
mLauncher.getUserEventDispatcher().logActionOnControl(TAP, UNDO);
};
Snackbar.show(mLauncher, R.string.item_removed, R.string.undo,
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 52a393f..5b453c3 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -44,7 +44,7 @@
* Implemented by listeners of the back key.
*/
public interface OnBackKeyListener {
- public boolean onBackKey();
+ boolean onBackKey();
}
private OnBackKeyListener mBackKeyListener;
@@ -108,6 +108,7 @@
@Override
public void onCommitCompletion(CompletionInfo text) {
setText(text.getText());
+ setSelection(text.getText().length());
}
/**
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 03ee707..b89e727 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -49,12 +49,17 @@
super(context, attrs, defStyle);
}
- /* Get the orientation specific coordinates given an invariant order in the hotseat. */
- int getCellXFromOrder(int rank) {
+ /**
+ * Returns orientation specific cell X given invariant order in the hotseat
+ */
+ public int getCellXFromOrder(int rank) {
return mHasVerticalHotseat ? 0 : rank;
}
- int getCellYFromOrder(int rank) {
+ /**
+ * Returns orientation specific cell Y given invariant order in the hotseat
+ */
+ public int getCellYFromOrder(int rank) {
return mHasVerticalHotseat ? (getCountY() - (rank + 1)) : 0;
}
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index df03027..3eb02b3 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -49,8 +49,8 @@
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.Thunk;
@@ -538,12 +538,9 @@
return new PendingInstallShortcutInfo(info, context);
}
} else if (decoder.optBoolean(DEEPSHORTCUT_TYPE_KEY)) {
- DeepShortcutManager sm = DeepShortcutManager.getInstance(context);
- List<ShortcutInfo> si = sm.queryForFullDetails(
- decoder.launcherIntent.getPackage(),
- Arrays.asList(decoder.launcherIntent.getStringExtra(
- ShortcutKey.EXTRA_SHORTCUT_ID)),
- decoder.user);
+ List<ShortcutInfo> si = ShortcutKey.fromIntent(decoder.launcherIntent, decoder.user)
+ .buildRequest(context)
+ .query(ShortcutRequest.ALL);
if (si.isEmpty()) {
return null;
} else {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 962e5b0..0c9151a 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -76,6 +76,7 @@
import android.view.KeyboardShortcutInfo;
import android.view.LayoutInflater;
import android.view.Menu;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
@@ -121,6 +122,7 @@
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.qsb.QsbContainerView;
import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.AllAppsSwipeController;
import com.android.launcher3.touch.ItemClickHandler;
@@ -292,6 +294,7 @@
private PopupDataProvider mPopupDataProvider;
private int mSynchronouslyBoundPage = PagedView.INVALID_PAGE;
+ private int mPageToBindSynchronously = PagedView.INVALID_PAGE;
// We only want to get the SharedPreferences once since it does an FS stat each time we get
// it from the context.
@@ -307,7 +310,7 @@
// Request id for any pending activity result
protected int mPendingActivityRequestCode = -1;
- public ViewGroupFocusHelper mFocusHandler;
+ private ViewGroupFocusHelper mFocusHandler;
private RotationHelper mRotationHelper;
@@ -348,7 +351,7 @@
LauncherAppState app = LauncherAppState.getInstance(this);
mOldConfig = new Configuration(getResources().getConfiguration());
- mModel = app.setLauncher(this);
+ mModel = app.getModel();
mRotationHelper = new RotationHelper(this);
InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
initDeviceProfile(idp);
@@ -372,6 +375,7 @@
mPopupDataProvider = new PopupDataProvider(this);
mAppTransitionManager = LauncherAppTransitionManager.newInstance(this);
+ mAppTransitionManager.registerRemoteAnimations();
boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
if (internalStateHandled) {
@@ -386,22 +390,18 @@
// We only load the page synchronously if the user rotates (or triggers a
// configuration change) while launcher is in the foreground
- int currentScreen = PagedView.INVALID_RESTORE_PAGE;
+ int currentScreen = PagedView.INVALID_PAGE;
if (savedInstanceState != null) {
currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
}
+ mPageToBindSynchronously = currentScreen;
- if (!mModel.startLoader(currentScreen)) {
+ if (!mModel.addCallbacksAndLoad(this)) {
if (!internalStateHandled) {
// If we are not binding synchronously, show a fade in animation when
// the first page bind completes.
mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0);
}
- } else {
- // Pages bound synchronously.
- mWorkspace.setCurrentPage(currentScreen);
-
- setWorkspaceLoading(true);
}
// For handling default keys
@@ -522,15 +522,6 @@
}
@Override
- public void rebindModel() {
- int currentPage = mWorkspace.getNextPage();
- if (mModel.startLoader(currentPage)) {
- mWorkspace.setCurrentPage(currentPage);
- setWorkspaceLoading(true);
- }
- }
-
- @Override
public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
onIdpChanged(idp);
}
@@ -548,7 +539,7 @@
// initialized properly.
onSaveInstanceState(new Bundle());
if (oldWallpaperProfile != getWallpaperDeviceProfile()) {
- rebindModel();
+ mModel.rebindCallbacks();
}
}
@@ -617,6 +608,10 @@
return mRotationHelper;
}
+ public ViewGroupFocusHelper getFocusHandler() {
+ return mFocusHandler;
+ }
+
public LauncherStateManager getStateManager() {
return mStateManager;
}
@@ -1451,6 +1446,9 @@
@Override
protected void onNewIntent(Intent intent) {
+ if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+ Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Launcher.onNewIntent: " + intent);
+ }
Object traceToken = TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
super.onNewIntent(intent);
@@ -1558,13 +1556,7 @@
mWorkspace.removeFolderListeners();
PluginManagerWrapper.INSTANCE.get(this).removePluginListener(this);
- // Stop callbacks from LauncherModel
- // It's possible to receive onDestroy after a new Launcher activity has
- // been created. In this case, don't interfere with the new Launcher.
- if (mModel.isCurrentCallbacks(this)) {
- mModel.stopLoader();
- LauncherAppState.getInstance(this).setLauncher(null);
- }
+ mModel.removeCallbacks(this);
mRotationHelper.destroy();
try {
@@ -1578,6 +1570,7 @@
LauncherAppState.getIDP(this).removeOnChangeListener(this);
mOverlayManager.onActivityDestroyed(this);
+ mAppTransitionManager.unregisterRemoteAnimations();
}
public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1759,7 +1752,7 @@
getModelWriter().addItemToDatabase(folderInfo, container, screenId, cellX, cellY);
// Create the view
- FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo);
+ FolderIcon newFolder = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this, layout, folderInfo);
mWorkspace.addInScreen(newFolder, folderInfo);
// Force measure the new folder icon
CellLayout parent = mWorkspace.getParentCellLayoutForView(newFolder);
@@ -1809,10 +1802,21 @@
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
+ if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+ TestLogging.recordEvent("Key event: " + event);
+ }
return (event.getKeyCode() == KeyEvent.KEYCODE_HOME) || super.dispatchKeyEvent(event);
}
@Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (Utilities.IS_RUNNING_IN_TEST_HARNESS && ev.getAction() != MotionEvent.ACTION_MOVE) {
+ TestLogging.recordEvent("Touch event: " + ev);
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
public void onBackPressed() {
if (finishAutoCancelActionMode()) {
return;
@@ -1972,11 +1976,21 @@
}
/**
+ * Sets the next page to bind synchronously on next bind.
+ * @param page
+ */
+ public void setPageToBindSynchronously(int page) {
+ mPageToBindSynchronously = page;
+ }
+
+ /**
* Implementation of the method from LauncherModel.Callbacks.
*/
@Override
- public int getCurrentWorkspaceScreen() {
- if (mWorkspace != null) {
+ public int getPageToBindSynchronously() {
+ if (mPageToBindSynchronously != PagedView.INVALID_PAGE) {
+ return mPageToBindSynchronously;
+ } else if (mWorkspace != null) {
return mWorkspace.getCurrentPage();
} else {
return 0;
@@ -2120,7 +2134,7 @@
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
- view = FolderIcon.fromXml(R.layout.folder_icon, this,
+ view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this,
(ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
(FolderInfo) item);
break;
@@ -2354,6 +2368,8 @@
public void onPageBoundSynchronously(int page) {
mSynchronouslyBoundPage = page;
+ mWorkspace.setCurrentPage(page);
+ mPageToBindSynchronously = PagedView.INVALID_PAGE;
}
@Override
@@ -2418,6 +2434,7 @@
// Since we are just resetting the current page without user interaction,
// override the previous page so we don't log the page switch.
mWorkspace.setCurrentPage(pageBoundFirst, pageBoundFirst /* overridePrevPage */);
+ mPageToBindSynchronously = PagedView.INVALID_PAGE;
// Cache one page worth of icons
getViewCache().setCacheSize(R.layout.folder_application,
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index c6946ca..4cd038d 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -160,11 +160,6 @@
}
}
- LauncherModel setLauncher(Launcher launcher) {
- mModel.initialize(launcher);
- return mModel;
- }
-
public IconCache getIconCache() {
return mIconCache;
}
diff --git a/src/com/android/launcher3/LauncherAppTransitionManager.java b/src/com/android/launcher3/LauncherAppTransitionManager.java
index c55c120..9148c2f 100644
--- a/src/com/android/launcher3/LauncherAppTransitionManager.java
+++ b/src/com/android/launcher3/LauncherAppTransitionManager.java
@@ -67,4 +67,18 @@
public Animator createStateElementAnimation(int index, float... values) {
throw new RuntimeException("Unknown gesture animation " + index);
}
+
+ /**
+ * Registers remote animations for certain system transitions.
+ */
+ public void registerRemoteAnimations() {
+ // Do nothing
+ }
+
+ /**
+ * Unregisters all remote animations.
+ */
+ public void unregisterRemoteAnimations() {
+ // Do nothing
+ }
}
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 67fd7db..cf978b5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,6 +20,7 @@
import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
import android.content.Context;
import android.content.Intent;
@@ -54,19 +55,19 @@
import com.android.launcher3.pm.InstallSessionTracker;
import com.android.launcher3.pm.PackageInstallInfo;
import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.Thunk;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
@@ -81,11 +82,12 @@
static final String TAG = "Launcher.Model";
- @Thunk final LauncherAppState mApp;
- @Thunk final Object mLock = new Object();
- @Thunk
- LoaderTask mLoaderTask;
- @Thunk boolean mIsLoaderTaskRunning;
+ private final LauncherAppState mApp;
+ private final Object mLock = new Object();
+ private final LooperExecutor mMainExecutor = MAIN_EXECUTOR;
+
+ private LoaderTask mLoaderTask;
+ private boolean mIsLoaderTaskRunning;
// Indicates whether the current model data is valid or not.
// We start off with everything not loaded. After that, we assume that
@@ -98,7 +100,7 @@
}
}
- @Thunk WeakReference<Callbacks> mCallbacks;
+ private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
// < only access in worker thread >
private final AllAppsList mBgAllAppsList;
@@ -107,18 +109,15 @@
* All the static data should be accessed on the background thread, A lock should be acquired
* on this object when accessing any data from this model.
*/
- static final BgDataModel sBgDataModel = new BgDataModel();
+ private final BgDataModel mBgDataModel = new BgDataModel();
// Runnable to check if the shortcuts permission has changed.
private final Runnable mShortcutPermissionCheckRunnable = new Runnable() {
@Override
public void run() {
- if (mModelLoaded) {
- boolean hasShortcutHostPermission =
- DeepShortcutManager.getInstance(mApp.getContext()).hasHostPermission();
- if (hasShortcutHostPermission != sBgDataModel.hasShortcutHostPermission) {
- forceReload();
- }
+ if (mModelLoaded && hasShortcutsPermission(mApp.getContext())
+ != mBgDataModel.hasShortcutHostPermission) {
+ forceReload();
}
}
};
@@ -129,31 +128,30 @@
}
/**
+ * Returns AppInfo with corresponding package name.
+ * TODO: move to enqueueModelTask
+ */
+ public Optional<AppInfo> getAppInfoByPackageName(String pkg) {
+ return mBgAllAppsList.data.stream()
+ .filter(info -> info.componentName.getPackageName().equals(pkg))
+ .findAny();
+ }
+
+ /**
* Adds the provided items to the workspace.
*/
public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) {
- Callbacks callbacks = getCallback();
- if (callbacks != null) {
- callbacks.preAddApps();
+ for (Callbacks cb : getCallbacks()) {
+ cb.preAddApps();
}
enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
}
public ModelWriter getWriter(boolean hasVerticalHotseat, boolean verifyChanges) {
- return new ModelWriter(mApp.getContext(), this, sBgDataModel,
+ return new ModelWriter(mApp.getContext(), this, mBgDataModel,
hasVerticalHotseat, verifyChanges);
}
- /**
- * Set this as the current Launcher activity object for the loader.
- */
- public void initialize(Callbacks callbacks) {
- synchronized (mLock) {
- Preconditions.assertUIThread();
- mCallbacks = new WeakReference<>(callbacks);
- }
- }
-
@Override
public void onPackageChanged(String packageName, UserHandle user) {
int op = PackageUpdatedTask.OP_UPDATE;
@@ -220,8 +218,8 @@
Context context = mApp.getContext();
onPackageChanged(packageName, user);
- List<ShortcutInfo> pinnedShortcuts = DeepShortcutManager.getInstance(context)
- .queryForPinnedShortcuts(packageName, user);
+ List<ShortcutInfo> pinnedShortcuts = new ShortcutRequest(context, user)
+ .forPackage(packageName).query(ShortcutRequest.PINNED);
if (!pinnedShortcuts.isEmpty()) {
enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, pinnedShortcuts, user,
false));
@@ -253,21 +251,19 @@
}
}
} else if (IS_DOGFOOD_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
- Launcher l = (Launcher) getCallback();
- l.reload();
+ for (Callbacks cb : getCallbacks()) {
+ if (cb instanceof Launcher) {
+ ((Launcher) cb).recreate();
+ }
+ }
}
}
- public void forceReload() {
- forceReload(-1);
- }
-
/**
* Reloads the workspace items from the DB and re-binds the workspace. This should generally
* not be called as DB updates are automatically followed by UI update
- * @param synchronousBindPage The page to bind first. Can pass -1 to use the current page.
*/
- public void forceReload(int synchronousBindPage) {
+ public void forceReload() {
synchronized (mLock) {
// Stop any existing loaders first, so they don't set mModelLoaded to true later
stopLoader();
@@ -276,37 +272,77 @@
// Start the loader if launcher is already running, otherwise the loader will run,
// the next time launcher starts
- Callbacks callbacks = getCallback();
- if (callbacks != null) {
- if (synchronousBindPage < 0) {
- synchronousBindPage = callbacks.getCurrentWorkspaceScreen();
- }
- startLoader(synchronousBindPage);
+ if (hasCallbacks()) {
+ startLoader();
}
}
- public boolean isCurrentCallbacks(Callbacks callbacks) {
- return (mCallbacks != null && mCallbacks.get() == callbacks);
+ /**
+ * Rebinds all existing callbacks with already loaded model
+ */
+ public void rebindCallbacks() {
+ if (hasCallbacks()) {
+ startLoader();
+ }
+ }
+
+ /**
+ * Removes an existing callback
+ */
+ public void removeCallbacks(Callbacks callbacks) {
+ synchronized (mCallbacksList) {
+ Preconditions.assertUIThread();
+ if (mCallbacksList.remove(callbacks)) {
+ if (stopLoader()) {
+ // Rebind existing callbacks
+ startLoader();
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a callbacks to receive model updates
+ * @return true if workspace load was performed synchronously
+ */
+ public boolean addCallbacksAndLoad(Callbacks callbacks) {
+ synchronized (mLock) {
+ addCallbacks(callbacks);
+ return startLoader();
+
+ }
+ }
+
+ /**
+ * Adds a callbacks to receive model updates
+ */
+ public void addCallbacks(Callbacks callbacks) {
+ Preconditions.assertUIThread();
+ synchronized (mCallbacksList) {
+ mCallbacksList.add(callbacks);
+ }
}
/**
* Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
* @return true if the page could be bound synchronously.
*/
- public boolean startLoader(int synchronousBindPage) {
+ public boolean startLoader() {
// Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
synchronized (mLock) {
// Don't bother to start the thread if we know it's not going to do anything
- if (mCallbacks != null && mCallbacks.get() != null) {
- final Callbacks oldCallbacks = mCallbacks.get();
+ final Callbacks[] callbacksList = getCallbacks();
+ if (callbacksList.length > 0) {
// Clear any pending bind-runnables from the synchronized load process.
- MAIN_EXECUTOR.execute(oldCallbacks::clearPendingBinds);
+ for (Callbacks cb : callbacksList) {
+ mMainExecutor.execute(cb::clearPendingBinds);
+ }
// If there is already one running, tell it to stop.
stopLoader();
- LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel,
- mBgAllAppsList, synchronousBindPage, mCallbacks);
+ LoaderResults loaderResults = new LoaderResults(
+ mApp, mBgDataModel, mBgAllAppsList, callbacksList, mMainExecutor);
if (mModelLoaded && !mIsLoaderTaskRunning) {
// Divide the set of loaded items into those that we are binding synchronously,
// and everything else that is to be bound normally (asynchronously).
@@ -327,21 +363,24 @@
/**
* If there is already a loader task running, tell it to stop.
+ * @return true if an existing loader was stopped.
*/
- public void stopLoader() {
+ public boolean stopLoader() {
synchronized (mLock) {
LoaderTask oldTask = mLoaderTask;
mLoaderTask = null;
if (oldTask != null) {
oldTask.stopLocked();
+ return true;
}
+ return false;
}
}
public void startLoaderForResults(LoaderResults results) {
synchronized (mLock) {
stopLoader();
- mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
+ mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, mBgDataModel, results);
// Always post the loader task, instead of running directly (even on same thread) so
// that we exit any nested synchronized blocks
@@ -489,7 +528,7 @@
}
public void enqueueModelUpdateTask(ModelUpdateTask task) {
- task.init(mApp, this, sBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+ task.init(mApp, this, mBgDataModel, mBgAllAppsList, mMainExecutor);
MODEL_EXECUTOR.execute(task);
}
@@ -560,10 +599,24 @@
+ " componentName=" + info.componentName.getPackageName());
}
}
- sBgDataModel.dump(prefix, fd, writer, args);
+ mBgDataModel.dump(prefix, fd, writer, args);
}
- public Callbacks getCallback() {
- return mCallbacks != null ? mCallbacks.get() : null;
+ /**
+ * Returns true if there are any callbacks attached to the model
+ */
+ public boolean hasCallbacks() {
+ synchronized (mCallbacksList) {
+ return !mCallbacksList.isEmpty();
+ }
+ }
+
+ /**
+ * Returns an array of currently attached callbacks
+ */
+ public Callbacks[] getCallbacks() {
+ synchronized (mCallbacksList) {
+ return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
+ }
}
}
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 900c966..b0ab35c 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.annotation.TargetApi;
import android.app.backup.BackupManager;
@@ -45,6 +46,7 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
@@ -87,6 +89,8 @@
private static final boolean LOGD = false;
private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
+ private static final String TOKEN_RESTORE_BACKUP_TABLE = "restore_backup_table";
+ private static final long RESTORE_BACKUP_TABLE_DELAY = 60000;
/**
* Represents the schema of the database. Changes in scheme need not be backwards compatible.
@@ -387,6 +391,14 @@
tableExists(mOpenHelper.getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
return null;
}
+ case LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE: {
+ final Handler handler = MODEL_EXECUTOR.getHandler();
+ handler.removeCallbacksAndMessages(TOKEN_RESTORE_BACKUP_TABLE);
+ handler.postDelayed(() -> RestoreDbTask.restoreIfPossible(
+ getContext(), mOpenHelper, new BackupManager(getContext())),
+ TOKEN_RESTORE_BACKUP_TABLE, RESTORE_BACKUP_TABLE_DELAY);
+ return null;
+ }
}
return null;
}
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index ec307db..4c5c61c 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -300,6 +300,8 @@
public static final String METHOD_REFRESH_BACKUP_TABLE = "refresh_backup_table";
+ public static final String METHOD_RESTORE_BACKUP_TABLE = "restore_backup_table";
+
public static final String EXTRA_VALUE = "value";
public static Bundle call(ContentResolver cr, String method) {
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index ff2b400..a1888bf 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -64,7 +64,7 @@
private static final String TAG = "PagedView";
private static final boolean DEBUG = false;
- protected static final int INVALID_PAGE = -1;
+ public 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;
@@ -84,8 +84,6 @@
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;
protected int mFlingThresholdVelocity;
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index f0bae02..89f0a3d 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -78,6 +78,7 @@
}
InstallSessionHelper packageInstallerCompat = InstallSessionHelper.INSTANCE.get(context);
+ packageInstallerCompat.restoreDbIfApplicable(info);
if (TextUtils.isEmpty(info.getAppPackageName())
|| info.getInstallReason() != PackageManager.INSTALL_REASON_USER
|| packageInstallerCompat.promiseIconAddedForId(info.getSessionId())) {
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 2bec0ba..af9a1b4 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -65,9 +65,10 @@
import com.android.launcher3.graphics.TintedDrawableSpan;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.ShortcutCachingLogic;
import com.android.launcher3.pm.ShortcutConfigActivityInfo;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.views.Transposable;
@@ -540,15 +541,14 @@
outObj[0] = activityInfo;
return activityInfo.getFullResIcon(appState.getIconCache());
}
- ShortcutKey key = ShortcutKey.fromItemInfo(info);
- DeepShortcutManager sm = DeepShortcutManager.getInstance(launcher);
- List<ShortcutInfo> si = sm.queryForFullDetails(
- key.componentName.getPackageName(), Arrays.asList(key.getId()), key.user);
+ List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info)
+ .buildRequest(launcher)
+ .query(ShortcutRequest.ALL);
if (si.isEmpty()) {
return null;
} else {
outObj[0] = si.get(0);
- return sm.getShortcutIconDrawable(si.get(0),
+ return ShortcutCachingLogic.getIcon(launcher, si.get(0),
appState.getInvariantDeviceProfile().fillResIconDpi);
}
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 7af979c..3278960 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -83,7 +83,6 @@
import com.android.launcher3.pageindicators.WorkspacePageIndicator;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.WorkspaceTouchListener;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -418,9 +417,6 @@
}
// Always enter the spring loaded mode
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Switching to SPRING_LOADED");
- }
mLauncher.getStateManager().goToState(SPRING_LOADED);
}
@@ -500,7 +496,7 @@
// In transposed layout, we add the QSB in the Grid. As workspace does not touch the
// edges, we do not need a full width QSB.
qsb = LayoutInflater.from(getContext())
- .inflate(R.layout.search_container_workspace,firstPage, false);
+ .inflate(R.layout.search_container_workspace, firstPage, false);
}
CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstPage.getCountX(), 1);
@@ -1760,9 +1756,6 @@
public void prepareAccessibilityDrop() { }
public void onDrop(final DragObject d, DragOptions options) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Workspace.onDrop");
- }
mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
CellLayout dropTargetLayout = mDropToLayout;
@@ -2440,9 +2433,6 @@
* to add an item to one of the workspace screens.
*/
private void onDropExternal(final int[] touchXY, final CellLayout cellLayout, DragObject d) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Workspace.onDropExternal");
- }
if (d.dragInfo instanceof PendingAddShortcutInfo) {
WorkspaceItemInfo si = ((PendingAddShortcutInfo) d.dragInfo)
.activityInfo.createWorkspaceItemInfo();
@@ -2556,7 +2546,7 @@
view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
- view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout,
+ view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, mLauncher, cellLayout,
(FolderInfo) info);
break;
default:
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 08ce9c2..0681919 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -11,10 +11,12 @@
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
import android.util.FloatProperty;
import android.view.animation.Interpolator;
@@ -183,8 +185,11 @@
}
public Animator createSpringAnimation(float... progressValues) {
- return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
- SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
+ if (UNSTABLE_SPRINGS.get()) {
+ return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
+ SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
+ }
+ return ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, progressValues);
}
private void setAlphas(LauncherState toState, AnimationConfig config,
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 0c4be62..10e2821 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,13 +15,14 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
import android.content.Context;
import android.content.pm.PackageManager;
import com.android.launcher3.AppInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.LabelComparator;
@@ -398,7 +399,7 @@
private boolean shouldShowWorkFooter() {
return mIsWork && Utilities.ATLEAST_P &&
- (DeepShortcutManager.getInstance(mLauncher).hasHostPermission()
+ (hasShortcutsPermission(mLauncher)
|| mLauncher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
== PackageManager.PERMISSION_GRANTED);
}
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index d47a40e..28579c1 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.os.Bundle;
+import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
@@ -57,6 +58,7 @@
parcel.putInt(TestProtocol.STATE_FIELD, stateOrdinal);
sendEventToTest(accessibilityManager, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
+ Log.d(TestProtocol.PERMANENT_DIAG_TAG, "sendStateEventToTest: " + stateOrdinal);
}
public static void sendScrollFinishedEventToTest(Context context) {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 80c7056..8ccb369 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -16,37 +16,25 @@
package com.android.launcher3.config;
-import static androidx.core.util.Preconditions.checkNotNull;
-
import android.content.Context;
-import android.content.SharedPreferences;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.Keep;
-import androidx.annotation.VisibleForTesting;
import com.android.launcher3.BuildConfig;
import com.android.launcher3.Utilities;
-import com.android.launcher3.uioverrides.TogglableFlag;
+import com.android.launcher3.uioverrides.DeviceFlag;
import java.util.ArrayList;
import java.util.List;
-import java.util.SortedMap;
-import java.util.TreeMap;
/**
* Defines a set of flags used to control various launcher behaviors.
*
* <p>All the flags should be defined here with appropriate default values.
*/
-@Keep
public final class FeatureFlags {
- private static final Object sLock = new Object();
- @GuardedBy("sLock")
- private static final List<TogglableFlag> sFlags = new ArrayList<>();
+ private static final List<DebugFlag> sDebugFlags = new ArrayList<>();
- static final String FLAGS_PREF_NAME = "featureFlags";
+ public static final String FLAGS_PREF_NAME = "featureFlags";
private FeatureFlags() { }
@@ -62,7 +50,6 @@
*/
public static final boolean QSB_ON_FIRST_SCREEN = true;
-
/**
* Feature flag to handle define config changes dynamically instead of killing the process.
*
@@ -73,187 +60,149 @@
* and set a default value for the flag. This will be the default value on Debug builds.
*/
// When enabled the promise icon is visible in all apps while installation an app.
- public static final TogglableFlag PROMISE_APPS_IN_ALL_APPS = new TogglableFlag(
+ public static final BooleanFlag PROMISE_APPS_IN_ALL_APPS = getDebugFlag(
"PROMISE_APPS_IN_ALL_APPS", false, "Add promise icon in all-apps");
// When enabled a promise icon is added to the home screen when install session is active.
- public static final TogglableFlag PROMISE_APPS_NEW_INSTALLS =
- new TogglableFlag("PROMISE_APPS_NEW_INSTALLS", true,
- "Adds a promise icon to the home screen for new install sessions.");
+ public static final BooleanFlag PROMISE_APPS_NEW_INSTALLS = getDebugFlag(
+ "PROMISE_APPS_NEW_INSTALLS", true,
+ "Adds a promise icon to the home screen for new install sessions.");
- public static final TogglableFlag APPLY_CONFIG_AT_RUNTIME = new TogglableFlag(
+ public static final BooleanFlag APPLY_CONFIG_AT_RUNTIME = getDebugFlag(
"APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
- public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
- false, "Enable springs for quickstep animations");
+ public static final BooleanFlag QUICKSTEP_SPRINGS = getDebugFlag(
+ "QUICKSTEP_SPRINGS", true, "Enable springs for quickstep animations");
- public static final TogglableFlag ADAPTIVE_ICON_WINDOW_ANIM = new TogglableFlag(
- "ADAPTIVE_ICON_WINDOW_ANIM", true,
- "Use adaptive icons for window animations.");
+ public static final BooleanFlag UNSTABLE_SPRINGS = getDebugFlag(
+ "UNSTABLE_SPRINGS", false, "Enable unstable springs for quickstep animations");
- public static final TogglableFlag ENABLE_QUICKSTEP_LIVE_TILE = new TogglableFlag(
+ public static final BooleanFlag ADAPTIVE_ICON_WINDOW_ANIM = getDebugFlag(
+ "ADAPTIVE_ICON_WINDOW_ANIM", true, "Use adaptive icons for window animations.");
+
+ public static final BooleanFlag ENABLE_QUICKSTEP_LIVE_TILE = getDebugFlag(
"ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
- public static final TogglableFlag ENABLE_HINTS_IN_OVERVIEW = new TogglableFlag(
- "ENABLE_HINTS_IN_OVERVIEW", true,
- "Show chip hints and gleams on the overview screen");
+ public static final BooleanFlag ENABLE_HINTS_IN_OVERVIEW = getDebugFlag(
+ "ENABLE_HINTS_IN_OVERVIEW", false, "Show chip hints and gleams on the overview screen");
- public static final TogglableFlag FAKE_LANDSCAPE_UI = new TogglableFlag(
- "FAKE_LANDSCAPE_UI", false,
- "Rotate launcher UI instead of using transposed layout");
+ public static final BooleanFlag FAKE_LANDSCAPE_UI = getDebugFlag(
+ "FAKE_LANDSCAPE_UI", false, "Rotate launcher UI instead of using transposed layout");
- public static final TogglableFlag FOLDER_NAME_SUGGEST = new TogglableFlag(
- "FOLDER_NAME_SUGGEST", true,
- "Suggests folder names instead of blank text.");
+ public static final BooleanFlag FOLDER_NAME_SUGGEST = getDebugFlag(
+ "FOLDER_NAME_SUGGEST", false, "Suggests folder names instead of blank text.");
- public static final TogglableFlag APP_SEARCH_IMPROVEMENTS = new TogglableFlag(
+ public static final BooleanFlag APP_SEARCH_IMPROVEMENTS = new DeviceFlag(
"APP_SEARCH_IMPROVEMENTS", false,
"Adds localized title and keyword search and ranking");
- public static final TogglableFlag ENABLE_PREDICTION_DISMISS = new TogglableFlag(
+ public static final BooleanFlag ENABLE_PREDICTION_DISMISS = getDebugFlag(
"ENABLE_PREDICTION_DISMISS", false, "Allow option to dimiss apps from predicted list");
- public static final TogglableFlag ENABLE_QUICK_CAPTURE_GESTURE = new TogglableFlag(
- "ENABLE_QUICK_CAPTURE_GESTURE", false, "Swipe from right to left to quick capture");
+ public static final BooleanFlag ENABLE_QUICK_CAPTURE_GESTURE = getDebugFlag(
+ "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");
- public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
+ public static final BooleanFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = getDebugFlag(
"ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
"Allow Launcher to handle nav bar gestures while Assistant is running over it");
- public static final TogglableFlag ENABLE_HYBRID_HOTSEAT = new TogglableFlag(
+ public static final BooleanFlag ENABLE_HYBRID_HOTSEAT = getDebugFlag(
"ENABLE_HYBRID_HOTSEAT", false, "Fill gaps in hotseat with predicted apps");
- public static final TogglableFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = new TogglableFlag(
+ public static final BooleanFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = getDebugFlag(
"ENABLE_DEEP_SHORTCUT_ICON_CACHE", true, "R/W deep shortcut in IconCache");
- public static final TogglableFlag ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER = new TogglableFlag(
+ public static final BooleanFlag ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER = getDebugFlag(
"ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER", false,
"Show launcher preview in grid picker");
- public static final TogglableFlag ENABLE_OVERVIEW_ACTIONS = new TogglableFlag(
+ public static final BooleanFlag ENABLE_OVERVIEW_ACTIONS = getDebugFlag(
"ENABLE_OVERVIEW_ACTIONS", false, "Show app actions in Overview");
+ public static final BooleanFlag ENABLE_DATABASE_RESTORE = getDebugFlag(
+ "ENABLE_DATABASE_RESTORE", true,
+ "Enable database restore when new restore session is created");
+
+ public static final BooleanFlag ENABLE_UNIVERSAL_SMARTSPACE = getDebugFlag(
+ "ENABLE_UNIVERSAL_SMARTSPACE", false,
+ "Replace Smartspace with a version rendered by System UI.");
+
public static void initialize(Context context) {
- // Avoid the disk read for user builds
- if (Utilities.IS_DEBUG_DEVICE) {
- synchronized (sLock) {
- for (BaseTogglableFlag flag : sFlags) {
- flag.initialize(context);
- }
+ synchronized (sDebugFlags) {
+ for (DebugFlag flag : sDebugFlags) {
+ flag.initialize(context);
}
}
}
- static List<TogglableFlag> getTogglableFlags() {
- // By Java Language Spec 12.4.2
- // https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.2, the
- // TogglableFlag instances on FeatureFlags will be created before those on the FeatureFlags
- // subclass. This code handles flags that are redeclared in FeatureFlags, ensuring the
- // FeatureFlags one takes priority.
- SortedMap<String, TogglableFlag> flagsByKey = new TreeMap<>();
- synchronized (sLock) {
- for (TogglableFlag flag : sFlags) {
- flagsByKey.put(flag.getKey(), flag);
- }
+ static List<DebugFlag> getDebugFlags() {
+ synchronized (sDebugFlags) {
+ return new ArrayList<>(sDebugFlags);
}
- return new ArrayList<>(flagsByKey.values());
}
- public static abstract class BaseTogglableFlag {
- private final String key;
- // should be value that is hardcoded in client side.
- // Comparatively, getDefaultValue() can be overridden.
- private final boolean defaultValue;
- private final String description;
- private boolean currentValue;
+ public static class BooleanFlag {
- public BaseTogglableFlag(
- String key,
- boolean defaultValue,
- String description) {
- this.key = checkNotNull(key);
- this.currentValue = this.defaultValue = defaultValue;
- this.description = checkNotNull(description);
+ public final String key;
+ public boolean defaultValue;
- synchronized (sLock) {
- sFlags.add((TogglableFlag)this);
- }
+ public BooleanFlag(String key, boolean defaultValue) {
+ this.key = key;
+ this.defaultValue = defaultValue;
}
- /** Set the value of this flag. This should only be used in tests. */
- @VisibleForTesting
- void setForTests(boolean value) {
- currentValue = value;
- }
-
- public String getKey() {
- return key;
- }
-
- protected void initialize(Context context) {
- currentValue = getFromStorage(context, getDefaultValue());
- }
-
- protected abstract boolean getOverridenDefaultValue(boolean value);
-
- protected abstract void addChangeListener(Context context, Runnable r);
-
- public void updateStorage(Context context, boolean value) {
- SharedPreferences.Editor editor = context.getSharedPreferences(FLAGS_PREF_NAME,
- Context.MODE_PRIVATE).edit();
- if (value == getDefaultValue()) {
- editor.remove(key).apply();
- } else {
- editor.putBoolean(key, value).apply();
- }
- }
-
- boolean getFromStorage(Context context, boolean defaultValue) {
- return context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
- .getBoolean(key, getDefaultValue());
- }
-
- boolean getDefaultValue() {
- return getOverridenDefaultValue(defaultValue);
- }
-
- /** Returns the value of the flag at process start, including any overrides present. */
public boolean get() {
- return currentValue;
- }
-
- String getDescription() {
- return description;
+ return defaultValue;
}
@Override
public String toString() {
- return "TogglableFlag{"
- + "key=" + key + ", "
- + "defaultValue=" + defaultValue + ", "
- + "overriddenDefaultValue=" + getOverridenDefaultValue(defaultValue) + ", "
- + "currentValue=" + currentValue + ", "
- + "description=" + description
- + "}";
+ return appendProps(new StringBuilder()
+ .append(getClass().getSimpleName()).append('{'))
+ .append('}').toString();
+ }
+
+ protected StringBuilder appendProps(StringBuilder src) {
+ return src.append("key=").append(key).append(", defaultValue=").append(defaultValue);
+ }
+
+ public void addChangeListener(Context context, Runnable r) { }
+ }
+
+ public static class DebugFlag extends BooleanFlag {
+
+ public final String description;
+ private boolean mCurrentValue;
+
+ public DebugFlag(String key, boolean defaultValue, String description) {
+ super(key, defaultValue);
+ this.description = description;
+ mCurrentValue = this.defaultValue;
+ synchronized (sDebugFlags) {
+ sDebugFlags.add(this);
+ }
}
@Override
- public boolean equals(Object o) {
- if (o == this) {
- return true;
- }
- if (o instanceof TogglableFlag) {
- BaseTogglableFlag that = (BaseTogglableFlag) o;
- return (this.key.equals(that.getKey()))
- && (this.getDefaultValue() == that.getDefaultValue())
- && (this.description.equals(that.getDescription()));
- }
- return false;
+ public boolean get() {
+ return mCurrentValue;
+ }
+
+ public void initialize(Context context) {
+ mCurrentValue = context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
+ .getBoolean(key, defaultValue);
}
@Override
- public int hashCode() {
- return key.hashCode();
+ protected StringBuilder appendProps(StringBuilder src) {
+ return super.appendProps(src).append(", mCurrentValue=").append(mCurrentValue)
+ .append(", description=").append(description);
}
}
+
+ private static BooleanFlag getDebugFlag(String key, boolean defaultValue, String description) {
+ return Utilities.IS_DEBUG_DEVICE
+ ? new DebugFlag(key, defaultValue, description)
+ : new BooleanFlag(key, defaultValue);
+ }
}
diff --git a/src/com/android/launcher3/config/FlagTogglerPrefUi.java b/src/com/android/launcher3/config/FlagTogglerPrefUi.java
index a7e3732..6729f74 100644
--- a/src/com/android/launcher3/config/FlagTogglerPrefUi.java
+++ b/src/com/android/launcher3/config/FlagTogglerPrefUi.java
@@ -16,6 +16,8 @@
package com.android.launcher3.config;
+import static com.android.launcher3.config.FeatureFlags.FLAGS_PREF_NAME;
+
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Process;
@@ -31,8 +33,7 @@
import androidx.preference.SwitchPreference;
import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags.BaseTogglableFlag;
-import com.android.launcher3.uioverrides.TogglableFlag;
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
/**
* Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}.
@@ -49,23 +50,26 @@
@Override
public void putBoolean(String key, boolean value) {
- for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
- if (flag.getKey().equals(key)) {
- boolean prevValue = flag.get();
- flag.updateStorage(mContext, value);
- updateMenu();
- if (flag.get() != prevValue) {
- Toast.makeText(mContext, "Flag applied", Toast.LENGTH_SHORT).show();
+ for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
+ if (flag.key.equals(key)) {
+ SharedPreferences.Editor editor = mContext.getSharedPreferences(
+ FLAGS_PREF_NAME, Context.MODE_PRIVATE).edit();
+ if (value == flag.defaultValue) {
+ editor.remove(key).apply();
+ } else {
+ editor.putBoolean(key, value).apply();
}
+ updateMenu();
}
}
}
@Override
public boolean getBoolean(String key, boolean defaultValue) {
- for (BaseTogglableFlag flag : FeatureFlags.getTogglableFlags()) {
- if (flag.getKey().equals(key)) {
- return flag.getFromStorage(mContext, defaultValue);
+ for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
+ if (flag.key.equals(key)) {
+ return mContext.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
+ .getBoolean(key, flag.defaultValue);
}
}
return defaultValue;
@@ -76,7 +80,7 @@
mFragment = fragment;
mContext = fragment.getActivity();
mSharedPreferences = mContext.getSharedPreferences(
- FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE);
+ FLAGS_PREF_NAME, Context.MODE_PRIVATE);
}
public void applyTo(PreferenceGroup parent) {
@@ -84,12 +88,12 @@
// flag with a different value than the default. That way, when we flip flags in
// future, engineers will pick up the new value immediately. To accomplish this, we use a
// custom preference data store.
- for (BaseTogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+ for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
SwitchPreference switchPreference = new SwitchPreference(mContext);
- switchPreference.setKey(flag.getKey());
- switchPreference.setDefaultValue(flag.getDefaultValue());
+ switchPreference.setKey(flag.key);
+ switchPreference.setDefaultValue(flag.defaultValue);
switchPreference.setChecked(getFlagStateFromSharedPrefs(flag));
- switchPreference.setTitle(flag.getKey());
+ switchPreference.setTitle(flag.key);
updateSummary(switchPreference, flag);
switchPreference.setPreferenceDataStore(mDataStore);
parent.addPreference(switchPreference);
@@ -100,11 +104,11 @@
/**
* Updates the summary to show the description and whether the flag overrides the default value.
*/
- private void updateSummary(SwitchPreference switchPreference, BaseTogglableFlag flag) {
- String onWarning = flag.getDefaultValue() ? "" : "<b>OVERRIDDEN</b><br>";
- String offWarning = flag.getDefaultValue() ? "<b>OVERRIDDEN</b><br>" : "";
- switchPreference.setSummaryOn(Html.fromHtml(onWarning + flag.getDescription()));
- switchPreference.setSummaryOff(Html.fromHtml(offWarning + flag.getDescription()));
+ private void updateSummary(SwitchPreference switchPreference, DebugFlag flag) {
+ String onWarning = flag.defaultValue ? "" : "<b>OVERRIDDEN</b><br>";
+ String offWarning = flag.defaultValue ? "<b>OVERRIDDEN</b><br>" : "";
+ switchPreference.setSummaryOn(Html.fromHtml(onWarning + flag.description));
+ switchPreference.setSummaryOff(Html.fromHtml(offWarning + flag.description));
}
private void updateMenu() {
@@ -135,12 +139,12 @@
}
}
- private boolean getFlagStateFromSharedPrefs(BaseTogglableFlag flag) {
- return mDataStore.getBoolean(flag.getKey(), flag.getDefaultValue());
+ private boolean getFlagStateFromSharedPrefs(DebugFlag flag) {
+ return mDataStore.getBoolean(flag.key, flag.defaultValue);
}
private boolean anyChanged() {
- for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
+ for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
if (getFlagStateFromSharedPrefs(flag) != flag.get()) {
return true;
}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index b72fd98..8adec27 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.Utilities.ATLEAST_Q;
import android.animation.ValueAnimator;
import android.content.ComponentName;
@@ -56,6 +57,12 @@
public class DragController implements DragDriver.EventListener, TouchController {
private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
+ /**
+ * When a drag is started from a deep press, you need to drag this much farther than normal to
+ * end a pre-drag. See {@link DragOptions.PreDragCondition#shouldStartDrag(double)}.
+ */
+ private static final int DEEP_PRESS_DISTANCE_FACTOR = 3;
+
@Thunk Launcher mLauncher;
private FlingToDeleteHelper mFlingToDeleteHelper;
@@ -91,9 +98,10 @@
private DropTarget mLastDropTarget;
- @Thunk int mLastTouch[] = new int[2];
- @Thunk long mLastTouchUpTime = -1;
- @Thunk int mDistanceSinceScroll = 0;
+ private final int[] mLastTouch = new int[2];
+ private long mLastTouchUpTime = -1;
+ private int mLastTouchClassification;
+ private int mDistanceSinceScroll = 0;
private int mTmpPoint[] = new int[2];
private Rect mDragLayerRect = new Rect();
@@ -204,7 +212,7 @@
}
mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- dragView.show(mMotionDownX, mMotionDownY);
+ dragView.show(mLastTouch[0], mLastTouch[1]);
mDistanceSinceScroll = 0;
if (!mIsInPreDrag) {
@@ -213,9 +221,7 @@
mOptions.preDragCondition.onPreDragStart(mDragObject);
}
- mLastTouch[0] = mMotionDownX;
- mLastTouch[1] = mMotionDownY;
- handleMoveEvent(mMotionDownX, mMotionDownY);
+ handleMoveEvent(mLastTouch[0], mLastTouch[1]);
mLauncher.getUserEventDispatcher().resetActionDurationMillis();
return dragView;
}
@@ -430,6 +436,11 @@
final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
final int dragLayerX = dragLayerPos[0];
final int dragLayerY = dragLayerPos[1];
+ mLastTouch[0] = dragLayerX;
+ mLastTouch[1] = dragLayerY;
+ if (ATLEAST_Q) {
+ mLastTouchClassification = ev.getClassification();
+ }
switch (action) {
case MotionEvent.ACTION_DOWN:
@@ -488,8 +499,12 @@
mLastTouch[0] = x;
mLastTouch[1] = y;
+ int distanceDragged = mDistanceSinceScroll;
+ if (ATLEAST_Q && mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
+ distanceDragged /= DEEP_PRESS_DISTANCE_FACTOR;
+ }
if (mIsInPreDrag && mOptions.preDragCondition != null
- && mOptions.preDragCondition.shouldStartDrag(mDistanceSinceScroll)) {
+ && mOptions.preDragCondition.shouldStartDrag(distanceDragged)) {
callOnDragStart();
}
}
@@ -579,9 +594,6 @@
}
private void drop(DropTarget dropTarget, Runnable flingAnimation) {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragController.drop");
- }
final int[] coordinates = mCoordinatesTemp;
mDragObject.x = coordinates[0];
mDragObject.y = coordinates[1];
diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java
index 01e0f92..87461d5 100644
--- a/src/com/android/launcher3/dragndrop/DragDriver.java
+++ b/src/com/android/launcher3/dragndrop/DragDriver.java
@@ -54,16 +54,10 @@
mEventListener.onDriverDragMove(ev.getX(), ev.getY());
break;
case MotionEvent.ACTION_UP:
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragDriver.ACTION_UP");
- }
mEventListener.onDriverDragMove(ev.getX(), ev.getY());
mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
break;
case MotionEvent.ACTION_CANCEL:
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragDriver.ACTION_CANCEL");
- }
mEventListener.onDriverDragCancel();
break;
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index f59a192..844189f 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -297,16 +297,22 @@
}
public void startEditingFolderName() {
- post(new Runnable() {
- @Override
- public void run() {
- mFolderName.setHint("");
- mIsEditingName = true;
+ post(() -> {
+ if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+ if (TextUtils.isEmpty(mFolderName.getText())) {
+ final String[] suggestedNames = new String[FolderNameProvider.SUGGEST_MAX];
+ mLauncher.getFolderNameProvider().getSuggestedFolderName(getContext(),
+ mInfo.contents, suggestedNames);
+ mFolderName.setText(suggestedNames[0]);
+ mFolderName.displayCompletions(Arrays.asList(suggestedNames).subList(1,
+ suggestedNames.length));
+ }
}
+ mFolderName.setHint("");
+ mIsEditingName = true;
});
}
-
@Override
public boolean onBackKey() {
// Convert to a string here to ensure that no other state associated with the text field
@@ -316,10 +322,18 @@
mFolderIcon.onTitleChanged(newTitle);
mLauncher.getModelWriter().updateItemInDatabase(mInfo);
- if (TextUtils.isEmpty(mInfo.title)) {
- mFolderName.setHint(R.string.folder_hint_text);
+ if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+ mFolderName.setText(mInfo.title);
+ // TODO: depending on whether the title was manually edited or automatically
+ // suggested, apply different hint.
+ mFolderName.setHint("");
} else {
- mFolderName.setHint(null);
+ if (TextUtils.isEmpty(mInfo.title)) {
+ mFolderName.setHint(R.string.folder_hint_text);
+ mFolderName.setText("");
+ } else {
+ mFolderName.setHint(null);
+ }
}
sendCustomAccessibilityEvent(
@@ -403,7 +417,11 @@
mFolderName.setHint(null);
} else {
mFolderName.setText("");
- mFolderName.setHint(R.string.folder_hint_text);
+ if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+ mFolderName.setHint("");
+ } else {
+ mFolderName.setHint(R.string.folder_hint_text);
+ }
}
// In case any children didn't come across during loading, clean up the folder accordingly
mFolderIcon.post(() -> {
@@ -420,10 +438,10 @@
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()
&& TextUtils.isEmpty(mFolderName.getText().toString())) {
if (suggestName.length > 0 && !TextUtils.isEmpty(suggestName[0])) {
- mFolderName.setHint(suggestName[0]);
+ mFolderName.setHint("");
mFolderName.setText(suggestName[0]);
mInfo.title = suggestName[0];
- animateOpen();
+ animateOpen(mInfo.contents, 0, true);
mFolderName.showKeyboard();
mFolderName.displayCompletions(
Arrays.asList(suggestName).subList(1, suggestName.length));
@@ -519,12 +537,24 @@
* is played.
*/
private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
+ animateOpen(items, pageNo, false);
+ }
+
+ /**
+ * Opens the user folder described by the specified tag. The opening of the folder
+ * is animated relative to the specified View. If the View is null, no animation
+ * is played.
+ */
+ private void animateOpen(List<WorkspaceItemInfo> items, int pageNo, boolean skipUserEventLog) {
Folder openFolder = getOpen(mLauncher);
if (openFolder != null && openFolder != this) {
// Close any open folder before opening a folder.
openFolder.close(true);
}
+ if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+ mLauncher.getFolderNameProvider().load(getContext());
+ }
mContent.bindItems(items);
centerAboutIcon();
mItemsInvalidated = true;
@@ -565,10 +595,13 @@
mState = STATE_OPEN;
announceAccessibilityChanges();
- mLauncher.getUserEventDispatcher().logActionOnItem(
+ if (!skipUserEventLog) {
+ mLauncher.getUserEventDispatcher().logActionOnItem(
Touch.TAP,
Direction.NONE,
ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
+ }
+
mContent.setFocusOnFirstChild();
}
@@ -1338,6 +1371,7 @@
return itemsOnCurrentPage;
}
+ @Override
public void onFocusChange(View v, boolean hasFocus) {
if (v == mFolderName) {
if (hasFocus) {
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 7bbd45d..8c56823 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -67,6 +67,7 @@
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.IconLabelDotView;
import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -79,7 +80,7 @@
*/
public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView {
- @Thunk Launcher mLauncher;
+ @Thunk ActivityContext mActivity;
@Thunk Folder mFolder;
private FolderInfo mInfo;
@@ -153,7 +154,21 @@
mDotParams = new DotRenderer.DrawParams();
}
- public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
+ public static FolderIcon inflateFolderAndIcon(int resId, Launcher launcher, ViewGroup group,
+ FolderInfo folderInfo) {
+ Folder folder = Folder.fromXml(launcher);
+ folder.setDragController(launcher.getDragController());
+
+ FolderIcon icon = inflateIcon(resId, launcher, group, folderInfo);
+ folder.setFolderIcon(icon);
+ folder.bind(folderInfo);
+ icon.setFolder(folder);
+
+ icon.setOnFocusChangeListener(launcher.getFocusHandler());
+ return icon;
+ }
+
+ public static FolderIcon inflateIcon(int resId, ActivityContext activity, ViewGroup group,
FolderInfo folderInfo) {
@SuppressWarnings("all") // suppress dead code warning
final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
@@ -163,7 +178,7 @@
"is dependent on this");
}
- DeviceProfile grid = launcher.getWallpaperDeviceProfile();
+ DeviceProfile grid = activity.getWallpaperDeviceProfile();
FolderIcon icon = (FolderIcon) LayoutInflater.from(group.getContext())
.inflate(resId, group, false);
@@ -177,19 +192,27 @@
icon.setTag(folderInfo);
icon.setOnClickListener(ItemClickHandler.INSTANCE);
icon.mInfo = folderInfo;
- icon.mLauncher = launcher;
+ icon.mActivity = activity;
icon.mDotRenderer = grid.mDotRendererWorkSpace;
- icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
- Folder folder = Folder.fromXml(launcher);
- folder.setDragController(launcher.getDragController());
- folder.setFolderIcon(icon);
- folder.bind(folderInfo);
- icon.setFolder(folder);
- icon.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
+
+ icon.setContentDescription(
+ group.getContext().getString(R.string.folder_name_format, folderInfo.title));
+
+ // Keep the notification dot up to date with the sum of all the content's dots.
+ FolderDotInfo folderDotInfo = new FolderDotInfo();
+ for (WorkspaceItemInfo si : folderInfo.contents) {
+ folderDotInfo.addDotInfo(activity.getDotInfoForItem(si));
+ }
+ icon.setDotInfo(folderDotInfo);
+
+ icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
+
+ icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
+ icon.mPreviewVerifier.setFolderInfo(folderInfo);
+ icon.updatePreviewItems(false);
folderInfo.addListener(icon);
- icon.setOnFocusChangeListener(launcher.mFocusHandler);
return icon;
}
@@ -217,9 +240,6 @@
private void setFolder(Folder folder) {
mFolder = folder;
- mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
- mPreviewVerifier.setFolderInfo(mFolder.getInfo());
- updatePreviewItems(false);
}
private boolean willAcceptItem(ItemInfo item) {
@@ -301,14 +321,15 @@
// Typically, the animateView corresponds to the DragView; however, if this is being done
// after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
// will not have a view to animate
- if (animateView != null) {
- DragLayer dragLayer = mLauncher.getDragLayer();
+ if (animateView != null && mActivity instanceof Launcher) {
+ final Launcher launcher = (Launcher) mActivity;
+ DragLayer dragLayer = launcher.getDragLayer();
Rect from = new Rect();
dragLayer.getViewRectRelativeToSelf(animateView, from);
Rect to = finalRect;
if (to == null) {
to = new Rect();
- Workspace workspace = mLauncher.getWorkspace();
+ Workspace workspace = launcher.getWorkspace();
// Set cellLayout and this to it's final state to compute final animation locations
workspace.setFinalTransitionTransform();
float scaleX = getScaleX();
@@ -374,7 +395,7 @@
String[] suggestedNameOut = new String[FolderNameProvider.SUGGEST_MAX];
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
Executors.UI_HELPER_EXECUTOR.post(() -> {
- mLauncher.getFolderNameProvider().getSuggestedFolderName(
+ launcher.getFolderNameProvider().getSuggestedFolderName(
getContext(), mInfo.contents, suggestedNameOut);
showFinalView(finalIndex, item, suggestedNameOut);
});
@@ -539,7 +560,7 @@
if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
Rect iconBounds = mDotParams.iconBounds;
BubbleTextView.getIconBounds(this, iconBounds,
- mLauncher.getWallpaperDeviceProfile().iconSizePx);
+ mActivity.getWallpaperDeviceProfile().iconSizePx);
float iconScale = (float) mBackground.previewSize / iconBounds.width();
Utilities.scaleRectAboutCenter(iconBounds, iconScale);
@@ -597,7 +618,7 @@
@Override
public void onAdd(WorkspaceItemInfo item, int rank) {
boolean wasDotted = mDotInfo.hasDot();
- mDotInfo.addDotInfo(mLauncher.getDotInfoForItem(item));
+ mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
boolean isDotted = mDotInfo.hasDot();
updateDotScale(wasDotted, isDotted);
invalidate();
@@ -607,7 +628,7 @@
@Override
public void onRemove(WorkspaceItemInfo item) {
boolean wasDotted = mDotInfo.hasDot();
- mDotInfo.subtractDotInfo(mLauncher.getDotInfoForItem(item));
+ mDotInfo.subtractDotInfo(mActivity.getDotInfoForItem(item));
boolean isDotted = mDotInfo.hasDot();
updateDotScale(wasDotted, isDotted);
invalidate();
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index 37aa815..386de23 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -15,53 +15,117 @@
*/
package com.android.launcher3.folder;
-import android.content.ComponentName;
import android.content.Context;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
-import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.config.FeatureFlags;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
/**
* Locates provider for the folder name.
*/
public class FolderNameProvider {
+ private static final String TAG = "FolderNameProvider";
+ private static final boolean DEBUG = FeatureFlags.FOLDER_NAME_SUGGEST.get();
+
/**
- * IME usually has up to 3 suggest slots. Adding one as in Launcher, there are folder
- * name edit box that we can also provide suggestion.
+ * IME usually has up to 3 suggest slots. In total, there are 4 suggest slots as the folder
+ * name edit box can also be used to provide suggestion.
*/
public static final int SUGGEST_MAX = 4;
/**
- * Returns suggested folder name.
+ * When inheriting class requires precaching, override this method.
*/
- public CharSequence getSuggestedFolderName(Context context,
- ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] suggestName) {
- // Currently only run the algorithm on initial folder creation.
- // For more than 2 items in the folder, the ranking algorithm for finding
- // candidate folder name should be rewritten.
- if (workspaceItemInfos.size() == 2) {
- ComponentName cmp1 = workspaceItemInfos.get(0).getTargetComponent();
- ComponentName cmp2 = workspaceItemInfos.get(1).getTargetComponent();
+ public void load(Context context) {}
- String pkgName0 = cmp1 == null ? "" : cmp1.getPackageName();
- String pkgName1 = cmp2 == null ? "" : cmp2.getPackageName();
- // If the two icons are from the same package,
- // then assign the main icon's name
- if (pkgName0.equals(pkgName1)) {
- WorkspaceItemInfo wInfo0 = workspaceItemInfos.get(0);
- WorkspaceItemInfo wInfo1 = workspaceItemInfos.get(1);
- if (workspaceItemInfos.get(0).itemType == Favorites.ITEM_TYPE_APPLICATION) {
- suggestName[0] = wInfo0.title;
- } else if (wInfo1.itemType == Favorites.ITEM_TYPE_APPLICATION) {
- suggestName[0] = wInfo1.title;
- }
- return suggestName[0];
- // two icons are all shortcuts. Don't assign title
+ public CharSequence getSuggestedFolderName(Context context,
+ ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] candidates) {
+
+ if (DEBUG) {
+ Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates));
+ }
+ // If all the icons are from work profile,
+ // Then, suggest "Work" as the folder name
+ List<WorkspaceItemInfo> distinctItemInfos = workspaceItemInfos.stream()
+ .filter(distinctByKey(p-> p.user))
+ .collect(Collectors.toList());
+
+ if (distinctItemInfos.size() == 1
+ && !distinctItemInfos.get(0).user.equals(Process.myUserHandle())) {
+ // Place it as last viable suggestion
+ setAsLastSuggestion(candidates,
+ context.getResources().getString(R.string.work_folder_name));
+ }
+
+ // If all the icons are from same package (e.g., main icon, shortcut, shortcut)
+ // Then, suggest the package's title as the folder name
+ distinctItemInfos = workspaceItemInfos.stream()
+ .filter(distinctByKey(p-> p.getTargetComponent() != null
+ ? p.getTargetComponent().getPackageName() : ""))
+ .collect(Collectors.toList());
+
+ if (distinctItemInfos.size() == 1) {
+ Optional<AppInfo> info = LauncherAppState.getInstance(context).getModel()
+ .getAppInfoByPackageName(distinctItemInfos.get(0).getTargetComponent()
+ .getPackageName());
+ // Place it as first viable suggestion and shift everything else
+ info.ifPresent(i -> setAsFirstSuggestion(candidates, i.title.toString()));
+ }
+ if (DEBUG) {
+ Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates));
+ }
+ return candidates[0];
+ }
+
+ private void setAsFirstSuggestion(CharSequence[] candidatesOut, CharSequence candidate) {
+ if (contains(candidatesOut, candidate)) {
+ return;
+ }
+ for (int i = candidatesOut.length - 1; i > 0; i--) {
+ if (!TextUtils.isEmpty(candidatesOut[i - 1])) {
+ candidatesOut[i] = candidatesOut[i - 1];
}
}
- return suggestName[0];
+ candidatesOut[0] = candidate;
+ }
+
+ private void setAsLastSuggestion(CharSequence[] candidatesOut, CharSequence candidate) {
+ if (contains(candidatesOut, candidate)) {
+ return;
+ }
+ for (int i = 0; i < candidate.length(); i++) {
+ if (TextUtils.isEmpty(candidatesOut[i])) {
+ candidatesOut[i] = candidate;
+ }
+ }
+ }
+
+ private boolean contains(CharSequence[] list, CharSequence key) {
+ return Arrays.asList(list).stream()
+ .filter(s -> s != null)
+ .anyMatch(s -> s.toString().equalsIgnoreCase(key.toString()));
+ }
+
+ // This method can be moved to some Utility class location.
+ private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
+ Map<Object, Boolean> map = new ConcurrentHashMap<>();
+ return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
}
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 5b3a05e..27aa43e 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -37,10 +37,10 @@
import androidx.annotation.NonNull;
-import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
import java.util.List;
@@ -94,7 +94,8 @@
public PreviewItemManager(FolderIcon icon) {
mContext = icon.getContext();
mIcon = icon;
- mIconSize = Launcher.getLauncher(mContext).getDeviceProfile().folderChildIconSizePx;
+ mIconSize = ActivityContext.lookupContext(
+ mContext).getDeviceProfile().folderChildIconSizePx;
}
/**
@@ -132,7 +133,7 @@
mTotalWidth = totalSize;
mPrevTopPadding = mIcon.getPaddingTop();
- mIcon.mBackground.setup(mIcon.mLauncher, mIcon.mLauncher, mIcon, mTotalWidth,
+ mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth,
mIcon.getPaddingTop());
mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
Utilities.isRtl(mIcon.getResources()));
@@ -152,7 +153,7 @@
}
private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
- float iconSize = mIcon.mLauncher.getDeviceProfile().iconSizePx;
+ float iconSize = mIcon.mActivity.getDeviceProfile().iconSizePx;
final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
final float trans = (mIcon.mBackground.previewSize - iconSize) / 2;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 0c5535f..def76e8 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -49,6 +49,7 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.FolderInfo;
import com.android.launcher3.Hotseat;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.InvariantDeviceProfile;
@@ -63,11 +64,13 @@
import com.android.launcher3.WorkspaceLayoutManager;
import com.android.launcher3.allapps.SearchUiManager;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.BitmapRenderer;
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.LoaderResults;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
@@ -239,6 +242,12 @@
addInScreenFromBind(icon, info);
}
+ private void inflateAndAddFolder(FolderInfo info) {
+ FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
+ info);
+ addInScreenFromBind(folderIcon, info);
+ }
+
private void dispatchVisibilityAggregated(View view, boolean isVisible) {
// Similar to View.dispatchVisibilityAggregated implementation.
final boolean thisVisible = view.getVisibility() == VISIBLE;
@@ -288,7 +297,7 @@
inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
- // TODO: for folder implementation here.
+ inflateAndAddFolder((FolderInfo) itemInfo);
break;
default:
break;
@@ -369,7 +378,7 @@
if (!mModel.isModelLoaded()) {
Log.d(TAG, "Workspace not loaded, loading now");
mModel.startLoaderForResults(
- new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+ new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
return new ArrayList<>();
}
return mBgDataModel.workspaceItems;
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 4d3599e..e67d244 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -35,7 +35,6 @@
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.model.PackageItemInfo;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.util.Themes;
import java.util.function.Supplier;
@@ -133,8 +132,8 @@
public BitmapInfo createShortcutIconLegacy(ShortcutInfo shortcutInfo, boolean badged,
@Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
- Drawable unbadgedDrawable = DeepShortcutManager.getInstance(mContext)
- .getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
+ Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon(
+ mContext, shortcutInfo, mFillResIconDpi);
IconCache cache = LauncherAppState.getInstance(mContext).getIconCache();
final Bitmap unbadgedBitmap;
if (unbadgedDrawable != null) {
diff --git a/src/com/android/launcher3/icons/ShortcutCachingLogic.java b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
index 5c21470..b856dd1 100644
--- a/src/com/android/launcher3/icons/ShortcutCachingLogic.java
+++ b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
@@ -16,19 +16,22 @@
package com.android.launcher3.icons;
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+
import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.LauncherApps;
import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
+import android.util.Log;
import androidx.annotation.NonNull;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.Themes;
@@ -37,6 +40,8 @@
*/
public class ShortcutCachingLogic implements CachingLogic<ShortcutInfo> {
+ private static final String TAG = "ShortcutCachingLogic";
+
@Override
public ComponentName getComponent(ShortcutInfo info) {
return ShortcutKey.fromInfo(info).componentName;
@@ -56,8 +61,8 @@
@Override
public BitmapInfo loadIcon(Context context, ShortcutInfo info) {
try (LauncherIcons li = LauncherIcons.obtain(context)) {
- Drawable unbadgedDrawable = DeepShortcutManager.getInstance(context)
- .getShortcutIconDrawable(info, LauncherAppState.getIDP(context).fillResIconDpi);
+ Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon(
+ context, info, LauncherAppState.getIDP(context).fillResIconDpi);
if (unbadgedDrawable == null) return BitmapInfo.LOW_RES_INFO;
return new BitmapInfo(li.createScaledBitmapWithoutShadow(
unbadgedDrawable, 0), Themes.getColorAccent(context));
@@ -76,4 +81,21 @@
public boolean addToMemCache() {
return false;
}
+
+ /**
+ * Similar to {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)} with additional
+ * Launcher specific checks
+ */
+ public static Drawable getIcon(Context context, ShortcutInfo shortcutInfo, int density) {
+ if (GO_DISABLE_WIDGETS) {
+ return null;
+ }
+ try {
+ return context.getSystemService(LauncherApps.class)
+ .getShortcutIconDrawable(shortcutInfo, density);
+ } catch (SecurityException | IllegalStateException e) {
+ Log.e(TAG, "Failed to get shortcut icon", e);
+ return null;
+ }
+ }
}
diff --git a/src/com/android/launcher3/logging/DumpTargetWrapper.java b/src/com/android/launcher3/logging/DumpTargetWrapper.java
index 365e8f2..067bdfd 100644
--- a/src/com/android/launcher3/logging/DumpTargetWrapper.java
+++ b/src/com/android/launcher3/logging/DumpTargetWrapper.java
@@ -15,17 +15,22 @@
*/
package com.android.launcher3.logging;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+
+import android.content.ComponentName;
import android.os.Process;
import android.text.TextUtils;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppWidgetInfo;
import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.model.nano.LauncherDumpProto;
import com.android.launcher3.model.nano.LauncherDumpProto.ContainerType;
import com.android.launcher3.model.nano.LauncherDumpProto.DumpTarget;
import com.android.launcher3.model.nano.LauncherDumpProto.ItemType;
import com.android.launcher3.model.nano.LauncherDumpProto.UserType;
+import com.android.launcher3.util.ShortcutUtil;
import java.util.ArrayList;
import java.util.List;
@@ -73,20 +78,23 @@
public DumpTarget newItemTarget(ItemInfo info) {
DumpTarget dt = new DumpTarget();
dt.type = DumpTarget.Type.ITEM;
-
+ if (info == null) {
+ return dt;
+ }
switch (info.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
dt.itemType = ItemType.APP_ICON;
break;
- case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
- dt.itemType = ItemType.UNKNOWN_ITEMTYPE;
- break;
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
dt.itemType = ItemType.WIDGET;
break;
- case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+ case ITEM_TYPE_DEEP_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
dt.itemType = ItemType.SHORTCUT;
break;
+ default:
+ dt.itemType = ItemType.UNKNOWN_ITEMTYPE;
+ break;
}
return dt;
}
@@ -120,6 +128,9 @@
}
private static String getItemStr(DumpTarget t) {
+ if (t == null) {
+ return "";
+ }
String typeStr = LoggerUtils.getFieldName(t.itemType, ItemType.class);
if (!TextUtils.isEmpty(t.packageName)) {
typeStr += ", package=" + t.packageName;
@@ -132,8 +143,15 @@
}
public DumpTarget writeToDumpTarget(ItemInfo info) {
- node.component = info.getTargetComponent() == null? "":
- info.getTargetComponent().flattenToString();
+ if (info == null) {
+ return node;
+ }
+ if (ShortcutUtil.isDeepShortcut(info)) {
+ node.component = ((WorkspaceItemInfo) info).getDeepShortcutId();
+ } else {
+ ComponentName cmp = info.getTargetComponent();
+ node.component = cmp == null ? "" : cmp.flattenToString();
+ }
node.packageName = info.getTargetComponent() == null? "":
info.getTargetComponent().getPackageName();
if (info instanceof LauncherAppWidgetInfo) {
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index 04cf20a..2c972a0 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -42,6 +42,8 @@
private static Handler sHandler = null;
private static File sLogsDirectory = null;
+ private static final int LOG_DAYS = 2;
+
public static void setDir(File logsDir) {
if (ENABLED) {
synchronized (DATE_FORMAT) {
@@ -147,7 +149,7 @@
case MSG_WRITE: {
Calendar cal = Calendar.getInstance();
// suffix with 0 or 1 based on the day of the year.
- String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) & 1);
+ String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % LOG_DAYS);
if (!fileName.equals(mCurrentFileName)) {
closeWriter();
@@ -195,8 +197,9 @@
(Pair<PrintWriter, CountDownLatch>) msg.obj;
if (p.first != null) {
- dumpFile(p.first, FILE_NAME_PREFIX + 0);
- dumpFile(p.first, FILE_NAME_PREFIX + 1);
+ for (int i = 0; i < LOG_DAYS; i++) {
+ dumpFile(p.first, FILE_NAME_PREFIX + i);
+ }
}
p.second.countDown();
return true;
@@ -226,4 +229,15 @@
}
}
}
+
+ /**
+ * Gets files used for FileLog
+ */
+ public static File[] getLogFiles() {
+ File[] files = new File[LOG_DAYS];
+ for (int i = 0; i < LOG_DAYS; i++) {
+ files[i] = new File(sLogsDirectory, FILE_NAME_PREFIX + i);
+ }
+ return files;
+ }
}
diff --git a/src/com/android/launcher3/logging/LoggerUtils.java b/src/com/android/launcher3/logging/LoggerUtils.java
index 598792a..f352b46 100644
--- a/src/com/android/launcher3/logging/LoggerUtils.java
+++ b/src/com/android/launcher3/logging/LoggerUtils.java
@@ -142,8 +142,10 @@
typeStr += ", grid(" + t.gridX + "," + t.gridY + ")";
} else if ((t.packageNameHash != 0 || t.componentHash != 0 || t.intentHash != 0)
&& t.itemType != ItemType.TASK) {
- typeStr += ", predictiveRank=" + t.predictedRank + ", grid(" + t.gridX + "," + t.gridY
- + "), span(" + t.spanX + "," + t.spanY + "), pageIdx=" + t.pageIndex;
+ typeStr +=
+ ", isWorkApp=" + t.isWorkApp + ", predictiveRank=" + t.predictedRank + ", grid("
+ + t.gridX + "," + t.gridY + "), span(" + t.spanX + "," + t.spanY
+ + "), pageIdx=" + t.pageIndex;
}
if (t.searchQueryLength != 0) {
typeStr += ", searchQueryLength=" + t.searchQueryLength;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index cad95b0..9dfd7ab 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -17,12 +17,15 @@
import android.content.Context;
import android.content.Intent;
+import android.os.UserHandle;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.R;
+import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
/**
* Handles the user event logging in Q.
@@ -38,7 +41,10 @@
return mgr;
}
- public void logAppLaunch(View v, Intent intent) { }
+ /**
+ * Logs app launches
+ */
+ public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) { }
public void logTaskLaunch(View v, ComponentKey key) { }
public void logTaskDismiss(View v, ComponentKey key) { }
public void logSwipeOnContainer(boolean isSwipingToLeft, int pageId) { }
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index 99906fe..8289da9 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -33,7 +33,9 @@
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.os.Process;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.util.Log;
import android.view.View;
@@ -135,7 +137,7 @@
// --------------------------------------------------------------
@Deprecated
- public void logAppLaunch(View v, Intent intent) {
+ public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) {
LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP),
newItemTarget(v, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
@@ -143,7 +145,13 @@
if (mDelegate != null) {
mDelegate.modifyUserEvent(event);
}
- fillIntentInfo(event.srcTarget[0], intent);
+ fillIntentInfo(event.srcTarget[0], intent, userHandle);
+ }
+ ItemInfo info = (ItemInfo) v.getTag();
+ if (Utilities.IS_DEBUG_DEVICE && FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
+ FileLog.d(TAG, "appLaunch: packageName:" + info.getTargetComponent().getPackageName()
+ + ",isWorkApp:" + (info.user != null && !Process.myUserHandle().equals(
+ userHandle)) + ",launchLocation:" + info.container);
}
dispatchUserEvent(event, intent);
mAppOrTaskLaunch = true;
@@ -171,8 +179,9 @@
mAppOrTaskLaunch = true;
}
- protected void fillIntentInfo(Target target, Intent intent) {
+ protected void fillIntentInfo(Target target, Intent intent, @Nullable UserHandle userHandle) {
target.intentHash = intent.hashCode();
+ target.isWorkApp = userHandle != null && !userHandle.equals(Process.myUserHandle());
fillComponentInfo(target, intent.getComponent());
}
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 76c2951..0d12183 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -18,9 +18,7 @@
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import android.os.Looper;
import android.util.Log;
import com.android.launcher3.AppInfo;
@@ -32,12 +30,13 @@
import com.android.launcher3.PagedView;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.LooperIdleLock;
import com.android.launcher3.util.ViewOnDrawExecutor;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -49,40 +48,29 @@
protected static final int INVALID_SCREEN_ID = -1;
private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
- protected final Executor mUiExecutor;
+ protected final LooperExecutor mUiExecutor;
protected final LauncherAppState mApp;
protected final BgDataModel mBgDataModel;
private final AllAppsList mBgAllAppsList;
- protected final int mPageToBindFirst;
- protected final WeakReference<Callbacks> mCallbacks;
+ private final Callbacks[] mCallbacksList;
private int mMyBindingId;
public BaseLoaderResults(LauncherAppState app, BgDataModel dataModel,
- AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
- mUiExecutor = MAIN_EXECUTOR;
+ AllAppsList allAppsList, Callbacks[] callbacksList, LooperExecutor uiExecutor) {
+ mUiExecutor = uiExecutor;
mApp = app;
mBgDataModel = dataModel;
mBgAllAppsList = allAppsList;
- mPageToBindFirst = pageToBindFirst;
- mCallbacks = callbacks == null ? new WeakReference<>(null) : callbacks;
+ mCallbacksList = callbacksList;
}
/**
* Binds all loaded data to actual views on the main thread.
*/
public void bindWorkspace() {
- Callbacks callbacks = mCallbacks.get();
- // Don't use these two variables in any of the callback runnables.
- // Otherwise we hold a reference to them.
- if (callbacks == null) {
- // This launcher has exited and nobody bothered to tell us. Just bail.
- Log.w(TAG, "LoaderTask running with no launcher");
- return;
- }
-
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
@@ -96,97 +84,9 @@
mMyBindingId = mBgDataModel.lastBindId;
}
- final int currentScreen;
- {
- int currScreen = mPageToBindFirst != PagedView.INVALID_RESTORE_PAGE
- ? mPageToBindFirst : callbacks.getCurrentWorkspaceScreen();
- if (currScreen >= orderedScreenIds.size()) {
- // There may be no workspace screens (just hotseat items and an empty page).
- currScreen = PagedView.INVALID_RESTORE_PAGE;
- }
- currentScreen = currScreen;
- }
- final boolean validFirstPage = currentScreen >= 0;
- final int currentScreenId =
- validFirstPage ? orderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
-
- // Separate the items that are on the current screen, and all the other remaining items
- ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
- ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
- ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
- ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
-
- filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
- otherWorkspaceItems);
- filterCurrentWorkspaceItems(currentScreenId, appWidgets, currentAppWidgets,
- otherAppWidgets);
- final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
- sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
- sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
-
- // Tell the workspace that we're about to start binding items
- executeCallbacksTask(c -> {
- c.clearPendingBinds();
- c.startBinding();
- }, mUiExecutor);
-
- // Bind workspace screens
- executeCallbacksTask(c -> c.bindScreens(orderedScreenIds), mUiExecutor);
-
- Executor mainExecutor = mUiExecutor;
- // Load items on the current page.
- bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
- bindAppWidgets(currentAppWidgets, mainExecutor);
- // In case of validFirstPage, only bind the first screen, and defer binding the
- // remaining screens after first onDraw (and an optional the fade animation whichever
- // happens later).
- // This ensures that the first screen is immediately visible (eg. during rotation)
- // In case of !validFirstPage, bind all pages one after other.
- final Executor deferredExecutor =
- validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
-
- executeCallbacksTask(c -> c.finishFirstPageBind(
- validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
-
- bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
- bindAppWidgets(otherAppWidgets, deferredExecutor);
- // Tell the workspace that we're done binding items
- executeCallbacksTask(c -> c.finishBindingItems(mPageToBindFirst), deferredExecutor);
-
- if (validFirstPage) {
- executeCallbacksTask(c -> {
- // We are loading synchronously, which means, some of the pages will be
- // bound after first draw. Inform the callbacks that page binding is
- // not complete, and schedule the remaining pages.
- if (currentScreen != PagedView.INVALID_RESTORE_PAGE) {
- c.onPageBoundSynchronously(currentScreen);
- }
- c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
-
- }, mUiExecutor);
- }
- }
-
- protected void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems,
- final Executor executor) {
- // Bind the workspace items
- int N = workspaceItems.size();
- for (int i = 0; i < N; i += ITEMS_CHUNK) {
- final int start = i;
- final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
- executeCallbacksTask(
- c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
- executor);
- }
- }
-
- private void bindAppWidgets(ArrayList<LauncherAppWidgetInfo> appWidgets, Executor executor) {
- int N;// Bind the widgets, one at a time
- N = appWidgets.size();
- for (int i = 0; i < N; i++) {
- final ItemInfo widget = appWidgets.get(i);
- executeCallbacksTask(
- c -> c.bindItems(Collections.singletonList(widget), false), executor);
+ for (Callbacks cb : mCallbacksList) {
+ new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
+ workspaceItems, appWidgets, orderedScreenIds).bind();
}
}
@@ -206,19 +106,155 @@
Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
return;
}
- Callbacks callbacks = mCallbacks.get();
- if (callbacks != null) {
- task.execute(callbacks);
+ for (Callbacks cb : mCallbacksList) {
+ task.execute(cb);
}
});
}
public LooperIdleLock newIdleLock(Object lock) {
- LooperIdleLock idleLock = new LooperIdleLock(lock, Looper.getMainLooper());
+ LooperIdleLock idleLock = new LooperIdleLock(lock, mUiExecutor.getLooper());
// If we are not binding or if the main looper is already idle, there is no reason to wait
- if (mCallbacks.get() == null || Looper.getMainLooper().getQueue().isIdle()) {
+ if (mUiExecutor.getLooper().getQueue().isIdle()) {
idleLock.queueIdle();
}
return idleLock;
}
+
+ private static class WorkspaceBinder {
+
+ private final Executor mUiExecutor;
+ private final Callbacks mCallbacks;
+
+ private final LauncherAppState mApp;
+ private final BgDataModel mBgDataModel;
+
+ private final int mMyBindingId;
+ private final ArrayList<ItemInfo> mWorkspaceItems;
+ private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
+ private final IntArray mOrderedScreenIds;
+
+
+ WorkspaceBinder(Callbacks callbacks,
+ Executor uiExecutor,
+ LauncherAppState app,
+ BgDataModel bgDataModel,
+ int myBindingId,
+ ArrayList<ItemInfo> workspaceItems,
+ ArrayList<LauncherAppWidgetInfo> appWidgets,
+ IntArray orderedScreenIds) {
+ mCallbacks = callbacks;
+ mUiExecutor = uiExecutor;
+ mApp = app;
+ mBgDataModel = bgDataModel;
+ mMyBindingId = myBindingId;
+ mWorkspaceItems = workspaceItems;
+ mAppWidgets = appWidgets;
+ mOrderedScreenIds = orderedScreenIds;
+ }
+
+ private void bind() {
+ final int currentScreen;
+ {
+ // Create an anonymous scope to calculate currentScreen as it has to be a
+ // final variable.
+ int currScreen = mCallbacks.getPageToBindSynchronously();
+ if (currScreen >= mOrderedScreenIds.size()) {
+ // There may be no workspace screens (just hotseat items and an empty page).
+ currScreen = PagedView.INVALID_PAGE;
+ }
+ currentScreen = currScreen;
+ }
+ final boolean validFirstPage = currentScreen >= 0;
+ final int currentScreenId =
+ validFirstPage ? mOrderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
+
+ // Separate the items that are on the current screen, and all the other remaining items
+ ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
+ ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
+ ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
+ ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+
+ filterCurrentWorkspaceItems(currentScreenId, mWorkspaceItems, currentWorkspaceItems,
+ otherWorkspaceItems);
+ filterCurrentWorkspaceItems(currentScreenId, mAppWidgets, currentAppWidgets,
+ otherAppWidgets);
+ final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
+ sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
+ sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
+
+ // Tell the workspace that we're about to start binding items
+ executeCallbacksTask(c -> {
+ c.clearPendingBinds();
+ c.startBinding();
+ }, mUiExecutor);
+
+ // Bind workspace screens
+ executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
+
+ Executor mainExecutor = mUiExecutor;
+ // Load items on the current page.
+ bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
+ bindAppWidgets(currentAppWidgets, mainExecutor);
+ // In case of validFirstPage, only bind the first screen, and defer binding the
+ // remaining screens after first onDraw (and an optional the fade animation whichever
+ // happens later).
+ // This ensures that the first screen is immediately visible (eg. during rotation)
+ // In case of !validFirstPage, bind all pages one after other.
+ final Executor deferredExecutor =
+ validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
+
+ executeCallbacksTask(c -> c.finishFirstPageBind(
+ validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
+
+ bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
+ bindAppWidgets(otherAppWidgets, deferredExecutor);
+ // Tell the workspace that we're done binding items
+ executeCallbacksTask(c -> c.finishBindingItems(currentScreen), deferredExecutor);
+
+ if (validFirstPage) {
+ executeCallbacksTask(c -> {
+ // We are loading synchronously, which means, some of the pages will be
+ // bound after first draw. Inform the mCallbacks that page binding is
+ // not complete, and schedule the remaining pages.
+ c.onPageBoundSynchronously(currentScreen);
+ c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
+
+ }, mUiExecutor);
+ }
+ }
+
+ private void bindWorkspaceItems(
+ final ArrayList<ItemInfo> workspaceItems, final Executor executor) {
+ // Bind the workspace items
+ int count = workspaceItems.size();
+ for (int i = 0; i < count; i += ITEMS_CHUNK) {
+ final int start = i;
+ final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
+ executeCallbacksTask(
+ c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
+ executor);
+ }
+ }
+
+ private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor) {
+ // Bind the widgets, one at a time
+ int count = appWidgets.size();
+ for (int i = 0; i < count; i++) {
+ final ItemInfo widget = appWidgets.get(i);
+ executeCallbacksTask(
+ c -> c.bindItems(Collections.singletonList(widget), false), executor);
+ }
+ }
+
+ protected void executeCallbacksTask(CallbackTask task, Executor executor) {
+ executor.execute(() -> {
+ if (mMyBindingId != mBgDataModel.lastBindId) {
+ Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
+ return;
+ }
+ task.execute(mCallbacks);
+ });
+ }
+ }
}
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index e12633b..5a7b4d3 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -20,17 +20,16 @@
import com.android.launcher3.AppInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.widget.WidgetListRowEntry;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -78,13 +77,9 @@
* Schedules a {@param task} to be executed on the current callbacks.
*/
public final void scheduleCallbackTask(final CallbackTask task) {
- final Callbacks callbacks = mModel.getCallback();
- mUiExecutor.execute(() -> {
- Callbacks cb = mModel.getCallback();
- if (callbacks == cb && cb != null) {
- task.execute(callbacks);
- }
- });
+ for (final Callbacks cb : mModel.getCallbacks()) {
+ mUiExecutor.execute(() -> task.execute(cb));
+ }
}
public ModelWriter getModelWriter() {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 0e20270..c24b939 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -15,7 +15,11 @@
*/
package com.android.launcher3.model;
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
+
import android.content.Context;
+import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.os.UserHandle;
import android.text.TextUtils;
@@ -29,15 +33,15 @@
import com.android.launcher3.LauncherAppWidgetInfo;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.PromiseAppInfo;
-import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.Workspace;
+import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.DumpTargetWrapper;
import com.android.launcher3.model.nano.LauncherDumpProto;
import com.android.launcher3.model.nano.LauncherDumpProto.ContainerType;
import com.android.launcher3.model.nano.LauncherDumpProto.DumpTarget;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
@@ -59,6 +63,8 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
/**
* All the data stored in-memory and managed by the LauncherModel
@@ -287,7 +293,7 @@
if ((count == null || --count.value == 0)
&& !InstallShortcutReceiver.getPendingShortcuts(context)
.contains(pinnedShortcut)) {
- DeepShortcutManager.getInstance(context).unpinShortcut(pinnedShortcut);
+ unpinShortcut(context, pinnedShortcut);
}
// Fall through.
}
@@ -324,7 +330,7 @@
// Since this is a new item, pin the shortcut in the system server.
if (newItem && count.value == 1) {
- DeepShortcutManager.getInstance(context).pinShortcut(pinnedShortcut);
+ updatePinnedShortcuts(context, pinnedShortcut, List::add);
}
// Fall through
}
@@ -355,6 +361,36 @@
}
/**
+ * Removes the given shortcut from the current list of pinned shortcuts.
+ * (Runs on background thread)
+ */
+ public void unpinShortcut(Context context, ShortcutKey key) {
+ updatePinnedShortcuts(context, key, List::remove);
+ }
+
+ private void updatePinnedShortcuts(Context context, ShortcutKey key,
+ BiConsumer<List<String>, String> idOp) {
+ if (GO_DISABLE_WIDGETS) {
+ return;
+ }
+ String packageName = key.componentName.getPackageName();
+ String id = key.getId();
+ UserHandle user = key.user;
+ List<String> pinnedIds = new ShortcutRequest(context, user)
+ .forPackage(packageName)
+ .query(PINNED)
+ .stream()
+ .map(ShortcutInfo::getId)
+ .collect(Collectors.toCollection(ArrayList::new));
+ idOp.accept(pinnedIds, id);
+ try {
+ context.getSystemService(LauncherApps.class).pinShortcuts(packageName, pinnedIds, user);
+ } catch (SecurityException | IllegalStateException e) {
+ Log.w(TAG, "Failed to pin shortcut", e);
+ }
+ }
+
+ /**
* Return an existing FolderInfo object if we have encountered this ID previously,
* or make a new one.
*/
@@ -400,9 +436,10 @@
}
public interface Callbacks {
- void rebindModel();
-
- int getCurrentWorkspaceScreen();
+ /**
+ * Returns the page number to bind first, synchronously if possible or -1
+ */
+ int getPageToBindSynchronously();
void clearPendingBinds();
void startBinding();
void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
diff --git a/src/com/android/launcher3/model/GridBackupTable.java b/src/com/android/launcher3/model/GridBackupTable.java
index 11d4edd..fc9948e 100644
--- a/src/com/android/launcher3/model/GridBackupTable.java
+++ b/src/com/android/launcher3/model/GridBackupTable.java
@@ -27,6 +27,8 @@
import android.os.Process;
import android.util.Log;
+import androidx.annotation.IntDef;
+
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherSettings.Settings;
import com.android.launcher3.pm.UserCache;
@@ -45,6 +47,19 @@
private static final String KEY_GRID_Y_SIZE = Favorites.SPANY;
private static final String KEY_DB_VERSION = Favorites.RANK;
+ public static final int OPTION_REQUIRES_SANITIZATION = 1;
+
+ /** STATE_NOT_FOUND indicates backup doesn't exist in the db. */
+ private static final int STATE_NOT_FOUND = 0;
+ /**
+ * STATE_RAW indicates the backup has not yet been sanitized. This implies it might still
+ * posses app info that doesn't exist in the workspace and needed to be sanitized before
+ * put into use.
+ */
+ private static final int STATE_RAW = 1;
+ /** STATE_SANITIZED indicates the backup has already been sanitized, thus can be used as-is. */
+ private static final int STATE_SANITIZED = 2;
+
private final Context mContext;
private final SQLiteDatabase mDb;
@@ -56,6 +71,9 @@
private int mRestoredGridX;
private int mRestoredGridY;
+ @IntDef({STATE_NOT_FOUND, STATE_RAW, STATE_SANITIZED})
+ private @interface BackupState { }
+
public GridBackupTable(Context context, SQLiteDatabase db,
int hotseatSize, int gridX, int gridY) {
mContext = context;
@@ -66,6 +84,10 @@
mOldGridY = gridY;
}
+ /**
+ * Create a backup from current workspace layout if one isn't created already (Note backup
+ * created this way is always sanitized). Otherwise restore from the backup instead.
+ */
public boolean backupOrRestoreAsNeeded() {
// Check if backup table exists
if (!tableExists(mDb, BACKUP_TABLE_NAME)) {
@@ -74,16 +96,16 @@
// No need to copy if empty DB was created.
return false;
}
-
- copyTable(Favorites.TABLE_NAME, BACKUP_TABLE_NAME);
- encodeDBProperties();
+ doBackup(UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+ Process.myUserHandle()), 0);
return false;
}
-
- if (!loadDbProperties()) {
+ if (loadDBProperties() != STATE_SANITIZED) {
return false;
}
- copyTable(BACKUP_TABLE_NAME, Favorites.TABLE_NAME);
+ long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+ Process.myUserHandle());
+ copyTable(BACKUP_TABLE_NAME, Favorites.TABLE_NAME, userSerial);
Log.d(TAG, "Backup table found");
return true;
}
@@ -93,43 +115,84 @@
return mRestoredHotseatSize;
}
- private void copyTable(String from, String to) {
- long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
- Process.myUserHandle());
+ /**
+ * Copy valid grid entries from one table to another.
+ */
+ private void copyTable(String from, String to, long userSerial) {
dropTable(mDb, to);
Favorites.addTableToDb(mDb, userSerial, false, to);
mDb.execSQL("INSERT INTO " + to + " SELECT * FROM " + from + " where _id > " + ID_PROPERTY);
}
- private void encodeDBProperties() {
+ private void encodeDBProperties(int options) {
ContentValues values = new ContentValues();
values.put(Favorites._ID, ID_PROPERTY);
values.put(KEY_DB_VERSION, mDb.getVersion());
values.put(KEY_GRID_X_SIZE, mOldGridX);
values.put(KEY_GRID_Y_SIZE, mOldGridY);
values.put(KEY_HOTSEAT_SIZE, mOldHotseatSize);
+ values.put(Favorites.OPTIONS, options);
mDb.insert(BACKUP_TABLE_NAME, null, values);
}
- private boolean loadDbProperties() {
+ /**
+ * Load DB properties from grid backup table.
+ */
+ public @BackupState int loadDBProperties() {
try (Cursor c = mDb.query(BACKUP_TABLE_NAME, new String[] {
- KEY_DB_VERSION, // 0
- KEY_GRID_X_SIZE, // 1
- KEY_GRID_Y_SIZE, // 2
- KEY_HOTSEAT_SIZE}, // 3
+ KEY_DB_VERSION, // 0
+ KEY_GRID_X_SIZE, // 1
+ KEY_GRID_Y_SIZE, // 2
+ KEY_HOTSEAT_SIZE, // 3
+ Favorites.OPTIONS}, // 4
"_id=" + ID_PROPERTY, null, null, null, null)) {
if (!c.moveToNext()) {
Log.e(TAG, "Meta data not found in backup table");
- return false;
+ return STATE_NOT_FOUND;
}
- if (mDb.getVersion() != c.getInt(0)) {
- return false;
+ if (!validateDBVersion(mDb.getVersion(), c.getInt(0))) {
+ return STATE_NOT_FOUND;
}
mRestoredGridX = c.getInt(1);
mRestoredGridY = c.getInt(2);
mRestoredHotseatSize = c.getInt(3);
- return true;
+ boolean isSanitized = (c.getInt(4) & OPTION_REQUIRES_SANITIZATION) == 0;
+ return isSanitized ? STATE_SANITIZED : STATE_RAW;
}
}
+
+ /**
+ * Restore workspace from raw backup if available.
+ */
+ public boolean restoreFromRawBackupIfAvailable(long oldProfileId) {
+ if (!tableExists(mDb, Favorites.BACKUP_TABLE_NAME)
+ || loadDBProperties() != STATE_RAW
+ || mOldHotseatSize != mRestoredHotseatSize
+ || mOldGridX != mRestoredGridX
+ || mOldGridY != mRestoredGridY) {
+ // skip restore if dimensions in backup table differs from current setup.
+ return false;
+ }
+ copyTable(Favorites.BACKUP_TABLE_NAME, Favorites.TABLE_NAME, oldProfileId);
+ Log.d(TAG, "Backup restored");
+ return true;
+ }
+
+ /**
+ * Performs a backup on the workspace layout.
+ */
+ public void doBackup(long profileId, int options) {
+ copyTable(Favorites.TABLE_NAME, Favorites.BACKUP_TABLE_NAME, profileId);
+ encodeDBProperties(options);
+ }
+
+ private static boolean validateDBVersion(int expected, int actual) {
+ if (expected != actual) {
+ Log.e(TAG, String.format("Launcher.db version mismatch, expecting %d but %d was found",
+ expected, actual));
+ return false;
+ }
+ return true;
+ }
}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 605bb75..571d41a 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -21,6 +21,7 @@
import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
import android.appwidget.AppWidgetProviderInfo;
@@ -72,8 +73,9 @@
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.provider.ImportDataTask;
import com.android.launcher3.qsb.QsbContainerView;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IOUtils;
import com.android.launcher3.util.LooperIdleLock;
@@ -114,7 +116,6 @@
private final UserManager mUserManager;
private final UserCache mUserCache;
- private final DeepShortcutManager mShortcutManager;
private final InstallSessionHelper mSessionHelper;
private final IconCache mIconCache;
@@ -130,7 +131,6 @@
mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
mUserManager = mApp.getContext().getSystemService(UserManager.class);
mUserCache = UserCache.INSTANCE.get(mApp.getContext());
- mShortcutManager = DeepShortcutManager.getInstance(mApp.getContext());
mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext());
mIconCache = mApp.getIconCache();
}
@@ -349,8 +349,8 @@
// We can only query for shortcuts when the user is unlocked.
if (userUnlocked) {
- DeepShortcutManager.QueryResult pinnedShortcuts =
- mShortcutManager.queryForPinnedShortcuts(null, user);
+ QueryResult pinnedShortcuts = new ShortcutRequest(context, user)
+ .query(ShortcutRequest.PINNED);
if (pinnedShortcuts.wasSuccess()) {
for (ShortcutInfo shortcut : pinnedShortcuts) {
shortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
@@ -786,7 +786,7 @@
if ((numTimesPinned == null || numTimesPinned.value == 0)
&& !pendingShortcuts.contains(key)) {
// Shortcut is pinned but doesn't exist on the workspace; unpin it.
- mShortcutManager.unpinShortcut(key);
+ mBgDataModel.unpinShortcut(context, key);
}
}
@@ -884,12 +884,12 @@
private List<ShortcutInfo> loadDeepShortcuts() {
List<ShortcutInfo> allShortcuts = new ArrayList<>();
mBgDataModel.deepShortcutMap.clear();
- mBgDataModel.hasShortcutHostPermission = mShortcutManager.hasHostPermission();
+ mBgDataModel.hasShortcutHostPermission = hasShortcutsPermission(mApp.getContext());
if (mBgDataModel.hasShortcutHostPermission) {
for (UserHandle user : mUserCache.getUserProfiles()) {
if (mUserManager.isUserUnlocked(user)) {
- List<ShortcutInfo> shortcuts =
- mShortcutManager.queryForAllShortcuts(user);
+ List<ShortcutInfo> shortcuts = new ShortcutRequest(mApp.getContext(), user)
+ .query(ShortcutRequest.ALL);
allShortcuts.addAll(shortcuts);
mBgDataModel.updateDeepShortcutCounts(null, user, shortcuts);
}
diff --git a/src/com/android/launcher3/model/ModelPreload.java b/src/com/android/launcher3/model/ModelPreload.java
index 2bd6cd4..713492b 100644
--- a/src/com/android/launcher3/model/ModelPreload.java
+++ b/src/com/android/launcher3/model/ModelPreload.java
@@ -18,14 +18,15 @@
import android.content.Context;
import android.util.Log;
+import androidx.annotation.WorkerThread;
+
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import java.util.concurrent.Executor;
-import androidx.annotation.WorkerThread;
-
/**
* Utility class to preload LauncherModel
*/
@@ -50,7 +51,7 @@
@Override
public final void run() {
mModel.startLoaderForResultsIfNotLoaded(
- new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+ new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
Log.d(TAG, "Preload completed : " + mModel.isModelLoaded());
onComplete(mModel.isModelLoaded());
}
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index bdf3a69..ccd1554 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -41,7 +41,6 @@
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.ContentWriter;
import com.android.launcher3.util.ItemInfoMatcher;
@@ -350,12 +349,15 @@
mDeleteRunnables.clear();
}
- public void abortDelete(int pageToBindFirst) {
+ /**
+ * Aborts a previous delete operation pending commit
+ */
+ public void abortDelete() {
mPreparingToUndo = false;
mDeleteRunnables.clear();
// We do a full reload here instead of just a rebind because Folders change their internal
// state when dragging an item out, which clobbers the rebind unless we load from the DB.
- mModel.forceReload(pageToBindFirst);
+ mModel.forceReload();
}
private class UpdateItemRunnable extends UpdateItemBaseRunnable {
@@ -472,7 +474,7 @@
}
void verifyModel() {
- if (!mVerifyChanges || mModel.getCallback() == null) {
+ if (!mVerifyChanges || !mModel.hasCallbacks()) {
return;
}
@@ -488,11 +490,9 @@
// Bound model has not changed during the job
return;
}
+
// Bound model was changed between submitting the job and executing the job
- Callbacks callbacks = mModel.getCallback();
- if (callbacks != null) {
- callbacks.rebindModel();
- }
+ mModel.rebindCallbacks();
});
}
}
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 3361ff0..48c56e9 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -41,7 +41,7 @@
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.IntSparseArrayMap;
@@ -208,10 +208,11 @@
if (si.isPromise() && isNewApkAvailable) {
boolean isTargetValid = true;
if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
- List<ShortcutInfo> shortcut = DeepShortcutManager
- .getInstance(context).queryForPinnedShortcuts(
- cn.getPackageName(),
- Arrays.asList(si.getDeepShortcutId()), mUser);
+ List<ShortcutInfo> shortcut =
+ new ShortcutRequest(context, mUser)
+ .forPackage(cn.getPackageName(),
+ si.getDeepShortcutId())
+ .query(ShortcutRequest.PINNED);
if (shortcut.isEmpty()) {
isTargetValid = false;
} else {
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 05225d4..b0e7a69 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -24,8 +24,8 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.MultiHashMap;
@@ -54,8 +54,6 @@
@Override
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
final Context context = app.getContext();
- DeepShortcutManager deepShortcutManager = DeepShortcutManager.getInstance(context);
-
// Find WorkspaceItemInfo's that have changed on the workspace.
HashSet<ShortcutKey> removedKeys = new HashSet<>();
MultiHashMap<ShortcutKey, WorkspaceItemInfo> keyToShortcutInfo = new MultiHashMap<>();
@@ -74,8 +72,9 @@
final ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
if (!keyToShortcutInfo.isEmpty()) {
// Update the workspace to reflect the changes to updated shortcuts residing on it.
- List<ShortcutInfo> shortcuts = deepShortcutManager.queryForFullDetails(
- mPackageName, new ArrayList<>(allIds), mUser);
+ List<ShortcutInfo> shortcuts = new ShortcutRequest(context, mUser)
+ .forPackage(mPackageName, new ArrayList<>(allIds))
+ .query(ShortcutRequest.ALL);
for (ShortcutInfo fullDetails : shortcuts) {
ShortcutKey key = ShortcutKey.fromInfo(fullDetails);
List<WorkspaceItemInfo> workspaceItemInfos = keyToShortcutInfo.remove(key);
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index 694ae1a..d527423 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -27,8 +27,9 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.ItemInfoMatcher;
@@ -52,12 +53,11 @@
public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
Context context = app.getContext();
boolean isUserUnlocked = context.getSystemService(UserManager.class).isUserUnlocked(mUser);
- DeepShortcutManager deepShortcutManager = DeepShortcutManager.getInstance(context);
HashMap<ShortcutKey, ShortcutInfo> pinnedShortcuts = new HashMap<>();
if (isUserUnlocked) {
- DeepShortcutManager.QueryResult shortcuts =
- deepShortcutManager.queryForPinnedShortcuts(null, mUser);
+ QueryResult shortcuts = new ShortcutRequest(context, mUser)
+ .query(ShortcutRequest.PINNED);
if (shortcuts.wasSuccess()) {
for (ShortcutInfo shortcut : shortcuts) {
pinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), shortcut);
@@ -115,7 +115,8 @@
if (isUserUnlocked) {
dataModel.updateDeepShortcutCounts(
- null, mUser, deepShortcutManager.queryForAllShortcuts(mUser));
+ null, mUser,
+ new ShortcutRequest(context, mUser).query(ShortcutRequest.ALL));
}
bindDeepShortcuts(dataModel);
}
diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java
index c7de5b0..fd3d41a 100644
--- a/src/com/android/launcher3/notification/NotificationFooterLayout.java
+++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java
@@ -80,17 +80,28 @@
int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
mIconLayoutParams = new LayoutParams(iconSize, iconSize);
mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL;
- // Compute margin start for each icon such that the icons between the first one
- // and the ellipsis are evenly spaced out.
+ setWidth((int) res.getDimension(R.dimen.bg_popup_item_width));
+ mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
+ }
+
+
+ /**
+ * Compute margin start for each icon such that the icons between the first one and the ellipsis
+ * are evenly spaced out.
+ */
+ public void setWidth(int width) {
+ if (getLayoutParams() != null) {
+ getLayoutParams().width = width;
+ }
+ Resources res = getResources();
+ int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
+
int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding);
int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset)
+ res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size);
- int footerWidth = res.getDimensionPixelSize(R.dimen.bg_popup_item_width);
- int availableIconRowSpace = footerWidth - paddingEnd - ellipsisSpace
+ int availableIconRowSpace = width - paddingEnd - ellipsisSpace
- iconSize * MAX_FOOTER_NOTIFICATIONS;
mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
-
- mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
}
@Override
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 021fb30..0320aa3 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -86,6 +86,13 @@
}
}
+ /**
+ * Sets width for notification footer and spaces out items evenly
+ */
+ public void setFooterWidth(int footerWidth) {
+ mFooter.setWidth(footerWidth);
+ }
+
public void removeFooter() {
if (mContainer.indexOfChild(mFooter) >= 0) {
mContainer.removeView(mFooter);
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 186293f..976d7ba 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -29,6 +29,10 @@
import android.os.UserHandle;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.android.launcher3.LauncherSettings;
import com.android.launcher3.SessionCommitReceiver;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
@@ -52,6 +56,8 @@
// Set<String> of session ids of promise icons that have been added to the home screen
// as FLAG_PROMISE_NEW_INSTALLS.
protected static final String PROMISE_ICON_IDS = "promise_icon_ids";
+ public static final String KEY_INSTALL_SESSION_CREATED_TIMESTAMP =
+ "key_install_session_created_timestamp";
private static final boolean DEBUG = false;
@@ -159,6 +165,34 @@
return list;
}
+ /**
+ * Attempt to restore workspace layout if the session is triggered due to device restore and it
+ * has a newer timestamp.
+ */
+ public boolean restoreDbIfApplicable(@NonNull final SessionInfo info) {
+ if (!Utilities.ATLEAST_OREO || !FeatureFlags.ENABLE_DATABASE_RESTORE.get()) {
+ return false;
+ }
+ if (isRestore(info) && hasNewerTimestamp(mAppContext, info)) {
+ LauncherSettings.Settings.call(mAppContext.getContentResolver(),
+ LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE);
+ return true;
+ }
+ return false;
+ }
+
+ @RequiresApi(26)
+ private static boolean isRestore(@NonNull final SessionInfo info) {
+ return info.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_RESTORE;
+ }
+
+ private static boolean hasNewerTimestamp(
+ @NonNull final Context context, @NonNull final SessionInfo info) {
+ return PackageManagerHelper.getSessionCreatedTimeInMillis(info)
+ > Utilities.getDevicePrefs(context).getLong(
+ KEY_INSTALL_SESSION_CREATED_TIMESTAMP, 0);
+ }
+
public boolean promiseIconAddedForId(int sessionId) {
return mPromiseIconIds.contains(sessionId);
}
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index e70673a..b764a07 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -37,7 +37,6 @@
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
-import android.util.Log;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;
@@ -66,7 +65,6 @@
import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.util.PackageUserKey;
@@ -257,6 +255,16 @@
mNumNotifications = notificationKeys.size();
mOriginalIcon = originalIcon;
+ boolean hasDeepShortcuts = shortcutCount > 0;
+ int containerWidth = (int) getResources().getDimension(R.dimen.bg_popup_item_width);
+
+ // if there are deep shortcuts, we might want to increase the width of shortcuts to fit
+ // horizontally laid out system shortcuts.
+ if (hasDeepShortcuts) {
+ containerWidth = (int) Math.max(containerWidth,
+ systemShortcuts.size() * getResources().getDimension(
+ R.dimen.system_shortcut_header_icon_touch_size));
+ }
// Add views
if (mNumNotifications > 0) {
// Add notification entries
@@ -265,18 +273,22 @@
if (mNumNotifications == 1) {
mNotificationItemView.removeFooter();
}
+ else {
+ mNotificationItemView.setFooterWidth(containerWidth);
+ }
updateNotificationHeader();
}
int viewsToFlip = getChildCount();
mSystemShortcutContainer = this;
-
- if (shortcutCount > 0) {
+ if (hasDeepShortcuts) {
if (mNotificationItemView != null) {
mNotificationItemView.addGutter();
}
for (int i = shortcutCount; i > 0; i--) {
- mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut, this));
+ DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, this);
+ v.getLayoutParams().width = containerWidth;
+ mShortcuts.add(v);
}
updateHiddenShortcuts();
@@ -339,7 +351,9 @@
}
public void applyNotificationInfos(List<NotificationInfo> notificationInfos) {
- mNotificationItemView.applyNotificationInfos(notificationInfos);
+ if (mNotificationItemView != null) {
+ mNotificationItemView.applyNotificationInfos(notificationInfos);
+ }
}
private void updateHiddenShortcuts() {
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index 80c6683..947f49d 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -31,8 +31,8 @@
import com.android.launcher3.notification.NotificationInfo;
import com.android.launcher3.notification.NotificationKeyData;
import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.PackageUserKey;
import java.util.ArrayList;
@@ -144,8 +144,9 @@
uiHandler.post(() -> container.applyNotificationInfos(infos));
}
- List<ShortcutInfo> shortcuts = DeepShortcutManager.getInstance(launcher)
- .queryForShortcutsContainer(activity, user);
+ List<ShortcutInfo> shortcuts = new ShortcutRequest(launcher, user)
+ .withContainer(activity)
+ .query(ShortcutRequest.PUBLISHED);
String shortcutIdToDeDupe = notificationKeys.isEmpty() ? null
: notificationKeys.get(0).shortcutId;
shortcuts = PopupPopulator.sortAndFilterShortcuts(shortcuts, shortcutIdToDeDupe);
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index b580bd6..48f1c49 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -178,7 +178,10 @@
public static final Factory<Launcher> DISMISS_PREDICTION = (launcher, itemInfo) -> {
if (!FeatureFlags.ENABLE_PREDICTION_DISMISS.get()) return null;
- if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) return null;
+ if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION
+ && itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+ return null;
+ }
return new DismissPrediction(launcher, itemInfo);
};
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index fb33551..407ff31 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -16,6 +16,7 @@
package com.android.launcher3.provider;
+import static com.android.launcher3.pm.InstallSessionHelper.KEY_INSTALL_SESSION_CREATED_TIMESTAMP;
import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
import android.app.backup.BackupManager;
@@ -25,23 +26,28 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.LongSparseArray;
import android.util.SparseLongArray;
import androidx.annotation.NonNull;
import com.android.launcher3.AppWidgetsRestoredReceiver;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetInfo;
import com.android.launcher3.LauncherProvider.DatabaseHelper;
import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.Utilities;
+import com.android.launcher3.WorkspaceItemInfo;
import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.GridBackupTable;
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.LogConfig;
import java.io.InvalidObjectException;
+import java.util.Arrays;
/**
* Utility class to update DB schema after it has been restored.
@@ -65,6 +71,7 @@
SQLiteDatabase db = helper.getWritableDatabase();
try (SQLiteTransaction t = new SQLiteTransaction(db)) {
RestoreDbTask task = new RestoreDbTask();
+ task.backupWorkspace(context, db);
task.sanitizeDB(helper, db, backupManager);
task.restoreAppWidgetIdsIfExists(context);
t.commit();
@@ -76,6 +83,47 @@
}
/**
+ * Restore the workspace if backup is available.
+ */
+ public static boolean restoreIfPossible(@NonNull Context context,
+ @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager) {
+ Utilities.getDevicePrefs(context).edit().putLong(
+ KEY_INSTALL_SESSION_CREATED_TIMESTAMP, System.currentTimeMillis()).apply();
+ final SQLiteDatabase db = helper.getWritableDatabase();
+ try (SQLiteTransaction t = new SQLiteTransaction(db)) {
+ RestoreDbTask task = new RestoreDbTask();
+ task.restoreWorkspace(context, db, helper, backupManager);
+ task.restoreAppWidgetIdsIfExists(context);
+ t.commit();
+ return true;
+ } catch (Exception e) {
+ FileLog.e(TAG, "Failed to restore db", e);
+ return false;
+ }
+ }
+
+ /**
+ * Backup the workspace so that if things go south in restore, we can recover these entries.
+ */
+ private void backupWorkspace(Context context, SQLiteDatabase db) throws Exception {
+ InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+ new GridBackupTable(context, db, idp.numHotseatIcons, idp.numColumns, idp.numRows)
+ .doBackup(getDefaultProfileId(db), GridBackupTable.OPTION_REQUIRES_SANITIZATION);
+ }
+
+ private void restoreWorkspace(@NonNull Context context, @NonNull SQLiteDatabase db,
+ @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)
+ throws Exception {
+ final InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+ GridBackupTable backupTable = new GridBackupTable(context, db,
+ idp.numHotseatIcons, idp.numColumns, idp.numRows);
+ if (backupTable.restoreFromRawBackupIfAvailable(getDefaultProfileId(db))) {
+ sanitizeDB(helper, db, backupManager);
+ LauncherAppState.getInstance(context).getModel().forceReload();
+ }
+ }
+
+ /**
* Makes the following changes in the provider DB.
* 1. Removes all entries belonging to any profiles that were not restored.
* 2. Marks all entries as restored. The flags are updated during first load or as
@@ -107,15 +155,14 @@
int numProfiles = profileMapping.size();
String[] profileIds = new String[numProfiles];
profileIds[0] = Long.toString(oldProfileId);
- StringBuilder whereClause = new StringBuilder("profileId != ?");
- for (int i = profileMapping.size() - 1; i >= 1; --i) {
- whereClause.append(" AND profileId != ?");
+ for (int i = numProfiles - 1; i >= 1; --i) {
profileIds[i] = Long.toString(profileMapping.keyAt(i));
}
- int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
- if (itemsDeleted > 0) {
- FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
- }
+ final String[] args = new String[profileIds.length];
+ Arrays.fill(args, "?");
+ final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
+ int itemsDeleted = db.delete(Favorites.TABLE_NAME, where, profileIds);
+ FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
// Mark all items as restored.
boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS);
@@ -125,15 +172,16 @@
db.update(Favorites.TABLE_NAME, values, null, null);
// Mark widgets with appropriate restore flag.
- values.put(Favorites.RESTORED, LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
- LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
- LauncherAppWidgetInfo.FLAG_UI_NOT_READY |
- (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0));
+ values.put(Favorites.RESTORED, LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
+ | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
+ | LauncherAppWidgetInfo.FLAG_UI_NOT_READY
+ | (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0));
db.update(Favorites.TABLE_NAME, values, "itemType = ?",
new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)});
- // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp location.
- // Using Long.MIN_VALUE since profile ids can not be negative, so there will be no overlap.
+ // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp
+ // location. Using Long.MIN_VALUE since profile ids can not be negative, so there will
+ // be no overlap.
final long tempLocationOffset = Long.MIN_VALUE;
SparseLongArray tempMigratedIds = new SparseLongArray(profileMapping.size());
int numTempMigrations = 0;
@@ -191,10 +239,10 @@
private LongSparseArray<Long> getManagedProfileIds(SQLiteDatabase db, long defaultProfileId) {
LongSparseArray<Long> ids = new LongSparseArray<>();
try (Cursor c = db.rawQuery("SELECT profileId from favorites WHERE profileId != ? "
- + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})){
- while (c.moveToNext()) {
- ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null);
- }
+ + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})) {
+ while (c.moveToNext()) {
+ ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null);
+ }
}
return ids;
}
@@ -215,7 +263,7 @@
* Returns the profile id used in the favorites table of the provided db.
*/
protected long getDefaultProfileId(SQLiteDatabase db) throws Exception {
- try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)){
+ try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)) {
int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME);
while (c.moveToNext()) {
if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) {
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
index 70665ca..fa1a85f 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutKey.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java
@@ -1,6 +1,7 @@
package com.android.launcher3.shortcuts;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.os.UserHandle;
@@ -29,6 +30,14 @@
return componentName.getClassName();
}
+ /**
+ * Creates a {@link ShortcutRequest} for this key
+ */
+ public ShortcutRequest buildRequest(Context context) {
+ return new ShortcutRequest(context, user)
+ .forPackage(componentName.getPackageName(), getId());
+ }
+
public static ShortcutKey fromInfo(ShortcutInfo shortcutInfo) {
return new ShortcutKey(shortcutInfo.getPackage(), shortcutInfo.getUserHandle(),
shortcutInfo.getId());
diff --git a/src/com/android/launcher3/shortcuts/ShortcutRequest.java b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
new file mode 100644
index 0000000..5291ce4
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shortcuts;
+
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+
+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.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility class to streamline Shortcut query
+ */
+public class ShortcutRequest {
+
+ private static final String TAG = "ShortcutRequest";
+
+ public static final int ALL = ShortcutQuery.FLAG_MATCH_DYNAMIC
+ | ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_PINNED;
+ public static final int PUBLISHED = ShortcutQuery.FLAG_MATCH_DYNAMIC
+ | ShortcutQuery.FLAG_MATCH_MANIFEST;
+ public static final int PINNED = ShortcutQuery.FLAG_MATCH_PINNED;
+
+ private final ShortcutQuery mQuery = GO_DISABLE_WIDGETS ? null : new ShortcutQuery();
+
+ private final Context mContext;
+ private final UserHandle mUserHandle;
+
+ boolean mFailed = false;
+
+ public ShortcutRequest(Context context, UserHandle userHandle) {
+ mContext = context;
+ mUserHandle = userHandle;
+ }
+
+ /** @see #forPackage(String, List) */
+ public ShortcutRequest forPackage(String packageName) {
+ return forPackage(packageName, (List<String>) null);
+ }
+
+ /** @see #forPackage(String, List) */
+ public ShortcutRequest forPackage(String packageName, String... shortcutIds) {
+ return forPackage(packageName, Arrays.asList(shortcutIds));
+ }
+
+ /**
+ * @param shortcutIds If null, match all shortcuts, otherwise only match the given id's.
+ * @return A list of ShortcutInfo's associated with the given package.
+ */
+ public ShortcutRequest forPackage(String packageName, @Nullable List<String> shortcutIds) {
+ if (!GO_DISABLE_WIDGETS && packageName != null) {
+ mQuery.setPackage(packageName);
+ mQuery.setShortcutIds(shortcutIds);
+ }
+ return this;
+ }
+
+ public ShortcutRequest withContainer(@Nullable ComponentName activity) {
+ if (!GO_DISABLE_WIDGETS) {
+ if (activity == null) {
+ mFailed = true;
+ } else {
+ mQuery.setActivity(activity);
+ }
+ }
+ return this;
+ }
+
+ public QueryResult query(int flags) {
+ if (GO_DISABLE_WIDGETS || mFailed) {
+ return QueryResult.DEFAULT;
+ }
+ mQuery.setQueryFlags(flags);
+
+ try {
+ return new QueryResult(mContext.getSystemService(LauncherApps.class)
+ .getShortcuts(mQuery, mUserHandle));
+ } catch (SecurityException | IllegalStateException e) {
+ Log.e(TAG, "Failed to query for shortcuts", e);
+ return QueryResult.DEFAULT;
+ }
+ }
+
+ public static class QueryResult extends ArrayList<ShortcutInfo> {
+
+ static final QueryResult DEFAULT = new QueryResult(GO_DISABLE_WIDGETS);
+
+ private final boolean mWasSuccess;
+
+ QueryResult(List<ShortcutInfo> result) {
+ super(result == null ? Collections.emptyList() : result);
+ mWasSuccess = true;
+ }
+
+ QueryResult(boolean wasSuccess) {
+ mWasSuccess = wasSuccess;
+ }
+
+
+ public boolean wasSuccess() {
+ return mWasSuccess;
+ }
+ }
+}
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 40e267b..506830d 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -24,6 +24,7 @@
import android.graphics.Color;
import android.os.Bundle;
import android.os.Debug;
+import android.system.Os;
import android.view.View;
import androidx.annotation.Keep;
@@ -125,17 +126,22 @@
case TestProtocol.REQUEST_APPS_LIST_SCROLL_Y: {
try {
- final int deferUpdatesFlags = MAIN_EXECUTOR.submit(() ->
+ final int scroll = MAIN_EXECUTOR.submit(() ->
mLauncher.getAppsView().getActiveRecyclerView().getCurrentScrollY())
.get();
response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
- deferUpdatesFlags);
+ scroll);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
break;
}
+ case TestProtocol.REQUEST_PID: {
+ response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, Os.getpid());
+ break;
+ }
+
case TestProtocol.REQUEST_TOTAL_PSS_KB: {
runGcAndFinalizersSync();
Debug.MemoryInfo mem = new Debug.MemoryInfo();
diff --git a/src/com/android/launcher3/testing/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
new file mode 100644
index 0000000..fd066c1
--- /dev/null
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.testing;
+
+import android.util.Log;
+
+import com.android.launcher3.Utilities;
+
+public final class TestLogging {
+ public static void recordEvent(String event) {
+ if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+ Log.d(TestProtocol.TAPL_EVENTS_TAG, event);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 5aae841..01c207f 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -31,6 +31,7 @@
public static final int QUICK_SWITCH_STATE_ORDINAL = 4;
public static final int ALL_APPS_STATE_ORDINAL = 5;
public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
+ public static final String TAPL_EVENTS_TAG = "TaplEvents";
public static String stateOrdinalToString(int ordinal) {
switch (ordinal) {
@@ -73,6 +74,7 @@
public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y";
public static final String REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN = "overview-left-margin";
public static final String REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN = "overview-right-margin";
+ public static final String REQUEST_PID = "pid";
public static final String REQUEST_TOTAL_PSS_KB = "total_pss";
public static final String REQUEST_JAVA_LEAK = "java-leak";
public static final String REQUEST_NATIVE_LEAK = "native-leak";
@@ -86,6 +88,5 @@
public static final String PERMANENT_DIAG_TAG = "TaplTarget";
public static final String NO_BACKGROUND_TO_OVERVIEW_TAG = "b/138251824";
- public static final String NO_DRAG_TO_WORKSPACE = "b/138729456";
public static final String APP_NOT_DISABLED = "b/139891609";
}
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index f40f976..d193bef 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -23,7 +23,7 @@
import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_SCALE_COMPONENT;
import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
import android.animation.Animator;
@@ -434,7 +434,7 @@
updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
targetState, velocity, fling);
mCurrentAnimation.dispatchOnStartWithVelocity(endProgress, velocity);
- if (fling && targetState == LauncherState.ALL_APPS && !QUICKSTEP_SPRINGS.get()) {
+ if (fling && targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
}
anim.start();
@@ -508,7 +508,7 @@
mAtomicComponentsController.getAnimationPlayer().end();
mAtomicComponentsController = null;
}
- cancelAnimationControllers();
+ clearState();
boolean shouldGoToTargetState = true;
if (mPendingAnimation != null) {
boolean reachedTarget = mToState == targetState;
@@ -546,13 +546,13 @@
mAtomicAnim = null;
}
mScheduleResumeAtomicComponent = false;
+ mDetector.finishedScrolling();
+ mDetector.setDetectableScrollConditions(0, false);
}
private void cancelAnimationControllers() {
mCurrentAnimation = null;
cancelAtomicComponentsController();
- mDetector.finishedScrolling();
- mDetector.setDetectableScrollConditions(0, false);
}
private void cancelAtomicComponentsController() {
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 12ca5ee..30283da 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -24,6 +24,10 @@
import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.LinkedList;
+import java.util.Queue;
/**
* Scroll/drag/swipe gesture detector.
@@ -49,13 +53,15 @@
protected final boolean mIsRtl;
protected final float mTouchSlop;
protected final float mMaxVelocity;
+ private final Queue<Runnable> mSetStateQueue = new LinkedList<>();
private int mActivePointerId = INVALID_POINTER_ID;
private VelocityTracker mVelocityTracker;
private PointF mLastDisplacement = new PointF();
private PointF mDisplacement = new PointF();
protected PointF mSubtractDisplacement = new PointF();
- private ScrollState mState = ScrollState.IDLE;
+ @VisibleForTesting ScrollState mState = ScrollState.IDLE;
+ private boolean mIsSettingState;
protected boolean mIgnoreSlopWhenSettling;
@@ -195,6 +201,12 @@
// SETTLING -> (View settled) -> IDLE
private void setState(ScrollState newState) {
+ if (mIsSettingState) {
+ mSetStateQueue.add(() -> setState(newState));
+ return;
+ }
+ mIsSettingState = true;
+
if (DBG) {
Log.d(TAG, "setState:" + mState + "->" + newState);
}
@@ -212,6 +224,10 @@
}
mState = newState;
+ mIsSettingState = false;
+ if (!mSetStateQueue.isEmpty()) {
+ mSetStateQueue.remove().run();
+ }
}
private void initializeDragging() {
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 66fdc94..310d598 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -25,7 +25,6 @@
import android.graphics.PointF;
import android.graphics.Rect;
-import android.util.Log;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
@@ -37,10 +36,8 @@
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
-import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -178,9 +175,6 @@
mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
Action.Direction.NONE, ContainerType.WORKSPACE,
mWorkspace.getCurrentPage());
- if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
- Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Opening options popup on long press");
- }
OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
} else {
cancelLongPress();
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
index 4d5ee49..0a32734 100644
--- a/src/com/android/launcher3/util/Executors.java
+++ b/src/com/android/launcher3/util/Executors.java
@@ -19,7 +19,6 @@
import android.os.Looper;
import android.os.Process;
-import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -36,9 +35,9 @@
private static final int KEEP_ALIVE = 1;
/**
- * An {@link Executor} to be used with async task with no limit on the queue size.
+ * An {@link ThreadPoolExecutor} to be used with async task with no limit on the queue size.
*/
- public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
+ public static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, new LinkedBlockingQueue<>());
diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index 4acdb5c..b54074e 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -28,4 +28,9 @@
* When turned on, icon cache is only fetched from memory and not disk.
*/
public static final String MEMORY_ONLY_ICON_CACHE = "MemoryOnlyIconCache";
+
+ /**
+ * When turned on, we enable doodle related logging.
+ */
+ public static final String DOODLE_LOGGING = "DoodleLogging";
}
diff --git a/src/com/android/launcher3/util/LooperExecutor.java b/src/com/android/launcher3/util/LooperExecutor.java
index 8ac600f..3a8a13c 100644
--- a/src/com/android/launcher3/util/LooperExecutor.java
+++ b/src/com/android/launcher3/util/LooperExecutor.java
@@ -41,10 +41,10 @@
@Override
public void execute(Runnable runnable) {
- if (mHandler.getLooper() == Looper.myLooper()) {
+ if (getHandler().getLooper() == Looper.myLooper()) {
runnable.run();
} else {
- mHandler.post(runnable);
+ getHandler().post(runnable);
}
}
@@ -52,7 +52,7 @@
* Same as execute, but never runs the action inline.
*/
public void post(Runnable runnable) {
- mHandler.post(runnable);
+ getHandler().post(runnable);
}
/**
@@ -96,14 +96,14 @@
* Returns the thread for this executor
*/
public Thread getThread() {
- return mHandler.getLooper().getThread();
+ return getHandler().getLooper().getThread();
}
/**
* Returns the looper for this executor
*/
public Looper getLooper() {
- return mHandler.getLooper();
+ return getHandler().getLooper();
}
/**
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 2d56ce7..6c18747 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,6 +16,7 @@
package com.android.launcher3.util;
+import static android.content.pm.PackageInstaller.SessionInfo;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
import android.app.AppOpsManager;
@@ -44,6 +45,8 @@
import android.util.Pair;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.AppInfo;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppWidgetInfo;
@@ -332,4 +335,28 @@
}
return false;
}
+
+ /**
+ * Returns true if Launcher has the permission to access shortcuts.
+ * @see LauncherApps#hasShortcutHostPermission()
+ */
+ public static boolean hasShortcutsPermission(Context context) {
+ try {
+ return context.getSystemService(LauncherApps.class).hasShortcutHostPermission();
+ } catch (SecurityException | IllegalStateException e) {
+ Log.e(TAG, "Failed to make shortcut manager call", e);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the created time in millis of given session info. Returns 0 if not available.
+ */
+ public static long getSessionCreatedTimeInMillis(@NonNull final SessionInfo info) {
+ try {
+ return (long) SessionInfo.class.getDeclaredMethod("getCreatedMillis").invoke(info);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
}
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index 5a131c8..451ae28 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -23,6 +23,8 @@
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewTreeObserver.OnDrawListener;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.Launcher;
import java.util.ArrayList;
@@ -118,7 +120,11 @@
return mCompleted;
}
- protected void runAllTasks() {
+ /**
+ * Executes all tasks immediately
+ */
+ @VisibleForTesting
+ public void runAllTasks() {
for (final Runnable r : mTasks) {
r.run();
}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index e43fc8a..cae2c3a 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -261,10 +261,6 @@
}
case ACTION_CANCEL:
case ACTION_UP:
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE,
- "BaseDragLayer.ACTION_UP/CANCEL " + ev);
- }
mTouchDispatchState &= ~TOUCH_DISPATCHING_GESTURE;
mTouchDispatchState &= ~TOUCH_DISPATCHING_VIEW;
break;
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 88d34da..5ba931d 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -47,7 +47,6 @@
import java.util.ArrayList;
import java.util.List;
-
/**
* Popup shown on long pressing an empty space in launcher
*/
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
index 60eb304..28a9193 100644
--- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -24,11 +24,11 @@
* the user to a more recent app).
*/
@ProvidesInterface(action = com.android.systemui.plugins.OverscrollPlugin.ACTION,
- version = com.android.systemui.plugins.OverlayPlugin.VERSION)
+ version = com.android.systemui.plugins.OverscrollPlugin.VERSION)
public interface OverscrollPlugin extends Plugin {
String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
- int VERSION = 1;
+ int VERSION = 3;
String DEVICE_STATE_LOCKED = "Locked";
String DEVICE_STATE_LAUNCHER = "Launcher";
@@ -36,9 +36,38 @@
String DEVICE_STATE_UNKNOWN = "Unknown";
/**
- * Called when the user completed a right to left swipe in the gesture area.
- *
- * @param deviceState One of the DEVICE_STATE_* constants.
+ * @return true if the plugin is active and will accept overscroll gestures
*/
- void onOverscroll(String deviceState);
+ boolean isActive();
+
+ /**
+ * Called when a touch is down and has been recognized as an overscroll gesture.
+ * A call of this method will always result in `onTouchUp` being called, and possibly
+ * `onFling` as well.
+ *
+ * @param deviceState String representing the current device state
+ * @param underlyingActivity String representing the currently active Activity
+ */
+ void onTouchStart(String deviceState, String underlyingActivity);
+
+ /**
+ * Called when a touch that was previously recognized has moved.
+ *
+ * @param px distance between the position of touch on this update and the position of the
+ * touch when it was initially recognized.
+ */
+ void onTouchTraveled(int px);
+
+ /**
+ * Called when a touch that was previously recognized has ended.
+ *
+ * @param px distance between the position of touch on this update and the position of the
+ * touch when it was initially recognized.
+ */
+ void onTouchEnd(int px);
+
+ /**
+ * Called when the user starts Compose with a fling. `onTouchUp` will also be called.
+ */
+ void onFling(float velocity);
}
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index 789bfd8..dcb4636 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -16,12 +16,14 @@
package com.android.launcher3.model;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.widget.WidgetListRowEntry;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
@@ -31,8 +33,13 @@
public class LoaderResults extends BaseLoaderResults {
public LoaderResults(LauncherAppState app, BgDataModel dataModel,
- AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
- super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+ AllAppsList allAppsList, Callbacks[] callbacks) {
+ this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+ }
+
+ public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+ AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+ super(app, dataModel, allAppsList, callbacks, executor);
}
@Override
diff --git a/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
deleted file mode 100644
index 57f4164..0000000
--- a/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * 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 com.android.launcher3.shortcuts;
-
-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.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
- */
-public class DeepShortcutManager {
- private static final String TAG = "DeepShortcutManager";
-
- private static final int FLAG_GET_ALL = ShortcutQuery.FLAG_MATCH_DYNAMIC
- | ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_PINNED;
-
- private static DeepShortcutManager sInstance;
-
- public static DeepShortcutManager getInstance(Context context) {
- if (sInstance == null) {
- sInstance = new DeepShortcutManager(context.getApplicationContext());
- }
- return sInstance;
- }
-
- private final LauncherApps mLauncherApps;
-
- private DeepShortcutManager(Context context) {
- mLauncherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
- }
-
- /**
- * Queries for the shortcuts with the package name and provided ids.
- *
- * This method is intended to get the full details for shortcuts when they are added or updated,
- * because we only get "key" fields in onShortcutsChanged().
- */
- public QueryResult queryForFullDetails(String packageName,
- List<String> shortcutIds, UserHandle user) {
- return query(FLAG_GET_ALL, packageName, null, shortcutIds, user);
- }
-
- /**
- * Gets all the manifest and dynamic shortcuts associated with the given package and user,
- * to be displayed in the shortcuts container on long press.
- */
- public QueryResult queryForShortcutsContainer(@Nullable ComponentName activity,
- UserHandle user) {
- if (activity == null) return QueryResult.FAILURE;
- return query(ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_DYNAMIC,
- activity.getPackageName(), activity, null, user);
- }
-
- /**
- * Removes the given shortcut from the current list of pinned shortcuts.
- * (Runs on background thread)
- */
- public void unpinShortcut(final ShortcutKey key) {
- String packageName = key.componentName.getPackageName();
- String id = key.getId();
- UserHandle user = key.user;
- List<String> pinnedIds = extractIds(queryForPinnedShortcuts(packageName, user));
- pinnedIds.remove(id);
- try {
- mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
- } catch (SecurityException|IllegalStateException e) {
- Log.w(TAG, "Failed to unpin shortcut", e);
- }
- }
-
- /**
- * Adds the given shortcut to the current list of pinned shortcuts.
- * (Runs on background thread)
- */
- public void pinShortcut(final ShortcutKey key) {
- String packageName = key.componentName.getPackageName();
- String id = key.getId();
- UserHandle user = key.user;
- List<String> pinnedIds = extractIds(queryForPinnedShortcuts(packageName, user));
- pinnedIds.add(id);
- try {
- mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
- } catch (SecurityException|IllegalStateException e) {
- Log.w(TAG, "Failed to pin shortcut", e);
- }
- }
-
- public void startShortcut(String packageName, String id, Rect sourceBounds,
- Bundle startActivityOptions, UserHandle user) {
- try {
- mLauncherApps.startShortcut(packageName, id, sourceBounds,
- startActivityOptions, user);
- } catch (SecurityException|IllegalStateException e) {
- Log.e(TAG, "Failed to start shortcut", e);
- }
- }
-
- public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
- try {
- return mLauncherApps.getShortcutIconDrawable(shortcutInfo, density);
- } catch (SecurityException|IllegalStateException e) {
- Log.e(TAG, "Failed to get shortcut icon", e);
- return null;
- }
- }
-
- /**
- * Returns the id's of pinned shortcuts associated with the given package and user.
- *
- * If packageName is null, returns all pinned shortcuts regardless of package.
- */
- public QueryResult queryForPinnedShortcuts(String packageName, UserHandle user) {
- return queryForPinnedShortcuts(packageName, null, user);
- }
-
- public QueryResult queryForPinnedShortcuts(String packageName, List<String> shortcutIds,
- UserHandle user) {
- return query(ShortcutQuery.FLAG_MATCH_PINNED, packageName, null, shortcutIds, user);
- }
-
- public QueryResult queryForAllShortcuts(UserHandle user) {
- return query(FLAG_GET_ALL, null, null, null, user);
- }
-
- private static List<String> extractIds(List<ShortcutInfo> shortcuts) {
- List<String> shortcutIds = new ArrayList<>(shortcuts.size());
- for (ShortcutInfo shortcut : shortcuts) {
- shortcutIds.add(shortcut.getId());
- }
- return shortcutIds;
- }
-
- /**
- * Query the system server for all the shortcuts matching the given parameters.
- * If packageName == null, we query for all shortcuts with the passed flags, regardless of app.
- *
- * TODO: Use the cache to optimize this so we don't make an RPC every time.
- */
- private QueryResult query(int flags, String packageName, ComponentName activity,
- List<String> shortcutIds, UserHandle user) {
- ShortcutQuery q = new ShortcutQuery();
- q.setQueryFlags(flags);
- if (packageName != null) {
- q.setPackage(packageName);
- q.setActivity(activity);
- q.setShortcutIds(shortcutIds);
- }
- try {
- return new QueryResult(mLauncherApps.getShortcuts(q, user));
- } catch (SecurityException|IllegalStateException e) {
- Log.e(TAG, "Failed to query for shortcuts", e);
- return QueryResult.FAILURE;
- }
- }
-
- public boolean hasHostPermission() {
- try {
- return mLauncherApps.hasShortcutHostPermission();
- } catch (SecurityException|IllegalStateException e) {
- Log.e(TAG, "Failed to make shortcut manager call", e);
- }
- return false;
- }
-
- public static class QueryResult extends ArrayList<ShortcutInfo> {
-
- static QueryResult FAILURE = new QueryResult();
-
- private final boolean mWasSuccess;
-
- QueryResult(List<ShortcutInfo> result) {
- super(result == null ? Collections.emptyList() : result);
- mWasSuccess = true;
- }
-
- QueryResult() {
- mWasSuccess = false;
- }
-
-
- public boolean wasSuccess() {
- return mWasSuccess;
- }
- }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java b/src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
similarity index 63%
rename from src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java
rename to src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
index d7bb293..5c1ac28 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/TogglableFlag.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -16,21 +16,11 @@
package com.android.launcher3.uioverrides;
-import android.content.Context;
+import com.android.launcher3.config.FeatureFlags.DebugFlag;
-import com.android.launcher3.config.FeatureFlags.BaseTogglableFlag;
+public class DeviceFlag extends DebugFlag {
-public class TogglableFlag extends BaseTogglableFlag {
-
- public TogglableFlag(String key, boolean defaultValue, String description) {
+ public DeviceFlag(String key, boolean defaultValue, String description) {
super(key, defaultValue, description);
}
-
- @Override
- public boolean getOverridenDefaultValue(boolean value) {
- return value;
- }
-
- @Override
- public void addChangeListener(Context context, Runnable r) { }
}
diff --git a/tests/Android.mk b/tests/Android.mk
index 83fdddc..d1a6c06 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -57,7 +57,7 @@
LOCAL_PRIVATE_PLATFORM_APIS := true
LOCAL_STATIC_JAVA_LIBRARIES += launcher-aosp-tapl
else
- LOCAL_SDK_VERSION := 28
+ LOCAL_SDK_VERSION := system_28
LOCAL_MIN_SDK_VERSION := 21
LOCAL_STATIC_JAVA_LIBRARIES += ub-launcher-aosp-tapl
endif
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 56eca6d..1c8f095 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<uses-permission android:name="android.permission.READ_LOGS"/>
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner"/>
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 5174e4d..6d463b5 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -21,9 +21,11 @@
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
+import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyFloat;
import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -168,4 +170,21 @@
// TODO: actually calculate the following parameters and do exact value checks.
verify(mMockListener).onDragEnd(anyFloat());
}
+
+ @Test
+ public void testInterleavedSetState() {
+ doAnswer(invocationOnMock -> {
+ // Sets state to IDLE. (Normally onDragEnd() will have state SETTLING.)
+ mDetector.finishedScrolling();
+ return null;
+ }).when(mMockListener).onDragEnd(anyFloat());
+
+ mGenerator.put(0, 100, 100);
+ mGenerator.move(0, 100, 100 + mTouchSlop);
+ mGenerator.move(0, 100, 100 + mTouchSlop * 2);
+ mGenerator.lift(0);
+ verify(mMockListener).onDragEnd(anyFloat());
+ assertTrue("SwipeDetector should be IDLE but was " + mDetector.mState,
+ mDetector.isIdleState());
+ }
}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 4243ed0..60dad12 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -92,7 +92,7 @@
public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
- public static final long DEFAULT_UI_TIMEOUT = 60000; // b/136278866
+ public static final long DEFAULT_UI_TIMEOUT = 10000;
private static final String TAG = "AbstractLauncherUiTest";
protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
@@ -259,7 +259,7 @@
protected <T> T getOnUiThread(final Callable<T> callback) {
try {
return mMainThreadExecutor.submit(callback).get();
- } catch (Exception e) {
+ } catch (Throwable e) {
throw new RuntimeException(e);
}
}
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 80bb3ed..1a68122 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -38,8 +38,8 @@
evaluateInPortrait();
evaluateInLandscape();
- } catch (Exception e) {
- Log.e(TAG, "Exception", e);
+ } catch (Throwable e) {
+ Log.e(TAG, "Error", e);
throw e;
} finally {
mTest.mDevice.setOrientationNatural();
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index d7096b0..61f5150 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,6 +18,9 @@
import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_POSTSUBMIT;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -36,10 +39,12 @@
import com.android.launcher3.tapl.AppIconMenuItem;
import com.android.launcher3.tapl.Widgets;
import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.widget.WidgetsFullSheet;
import com.android.launcher3.widget.WidgetsRecyclerView;
+import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -50,10 +55,18 @@
public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
private static final String APP_NAME = "LauncherTestApp";
+ private int mLauncherPid;
+
@Before
public void setUp() throws Exception {
super.setUp();
initialize(this);
+ mLauncherPid = mLauncher.getPid();
+ }
+
+ @After
+ public void teardown() {
+ assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
}
public static void initialize(AbstractLauncherUiTest test) throws Exception {
@@ -100,6 +113,16 @@
mLauncher.pressHome();
}
+ // b/146432215: remove @Stability after 2/1/2020 if this test doesn't flake
+ @Test
+ @Stability(flavors = LOCAL | UNBUNDLED_POSTSUBMIT)
+ public void testOpenHomeSettingsFromWorkspace() {
+ mDevice.pressMenu();
+ mDevice.waitForIdle();
+ mLauncher.getOptionsPopupMenu().getMenuItem("Home settings")
+ .launch(mDevice.getLauncherPackageName());
+ }
+
@Test
@Ignore
public void testPressHomeOnAllAppsContextMenu() throws Exception {
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 0472ce1..62e2a53 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -103,11 +103,11 @@
setResult(acceptConfig);
if (acceptConfig) {
- Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+ Wait.atMost("", new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
} else {
// Verify that the widget id is deleted.
- Wait.atMost(null, () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
+ Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
}
}
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index d909ad7..59b861c 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -170,7 +170,7 @@
// Go back to home
mLauncher.pressHome();
- Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
+ Wait.atMost("", new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
mLauncher);
}
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
index 2663d02..2ab1e00 100644
--- a/tests/src/com/android/launcher3/util/Wait.java
+++ b/tests/src/com/android/launcher3/util/Wait.java
@@ -7,6 +7,8 @@
import org.junit.Assert;
+import java.util.function.Supplier;
+
/**
* A utility class for waiting for a condition to be true.
*/
@@ -16,10 +18,16 @@
public static void atMost(String message, Condition condition, long timeout,
LauncherInstrumentation launcher) {
+ atMost(() -> message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
+ }
+
+ public static void atMost(Supplier<String> message, Condition condition, long timeout,
+ LauncherInstrumentation launcher) {
atMost(message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
}
- public static void atMost(String message, Condition condition, long timeout, long sleepMillis,
+ public static void atMost(Supplier<String> message, Condition condition, long timeout,
+ long sleepMillis,
LauncherInstrumentation launcher) {
final long startTime = SystemClock.uptimeMillis();
long endTime = startTime + timeout;
@@ -45,6 +53,6 @@
}
Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis());
launcher.checkForAnomaly();
- Assert.fail(message);
+ Assert.fail(message.get());
}
}
diff --git a/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java b/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
new file mode 100644
index 0000000..56c885d
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util.rule;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import androidx.test.uiautomator.UiDevice;
+
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+class FailureInvestigator {
+ private static boolean matches(String regex, CharSequence string) {
+ return Pattern.compile(regex).matcher(string).find();
+ }
+
+ static int getBugForFailure(CharSequence exception, String testsStartTime) {
+ final String logSinceTestsStart;
+ try {
+ logSinceTestsStart =
+ UiDevice.getInstance(getInstrumentation())
+ .executeShellCommand("logcat -d -t " + testsStartTime.replace(" ", ""));
+ } catch (IOException e) {
+ return 0;
+ }
+
+ if (matches(
+ "java.lang.AssertionError: http://go/tapl : Tests are broken by a non-Launcher "
+ + "system error: Phone is locked",
+ exception)) {
+ if (matches(
+ "BroadcastQueue: Can't deliver broadcast to com.android.systemui.*Crashing it",
+ logSinceTestsStart)) {
+ return 147845913;
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
index cdda0f0..feb89b9 100644
--- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
+++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
@@ -8,10 +8,13 @@
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
public class FailureWatcher extends TestWatcher {
private static final String TAG = "FailureWatcher";
@@ -35,6 +38,30 @@
}
}
+ private static final String testsStartTime =
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ base.evaluate();
+ } catch (Throwable e) {
+ final int bug =
+ FailureInvestigator.getBugForFailure(e.toString(), testsStartTime);
+ if (bug == 0) throw e;
+
+ Log.e(TAG, "Known bug found for the original failure "
+ + android.util.Log.getStackTraceString(e));
+ throw new AssertionError(
+ "Detected a failure that matches a known bug b/" + bug);
+ }
+ }
+ };
+ }
+
@Override
protected void failed(Throwable e, Description description) {
onError(mDevice, description, e);
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index f98957e..b394bcb 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -102,14 +102,15 @@
final String launcherVersion;
try {
+ final String launcherPackageName = UiDevice.getInstance(getInstrumentation())
+ .getLauncherPackageName();
+ Log.d(TAG, "Launcher package: " + launcherPackageName);
+
launcherVersion = getInstrumentation().
getContext().
getPackageManager().
- getPackageInfo(
- UiDevice.getInstance(getInstrumentation()).
- getLauncherPackageName(),
- 0).
- versionName;
+ getPackageInfo(launcherPackageName, 0)
+ .versionName;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
@@ -148,7 +149,8 @@
Log.d(TAG, "PLATFORM PRESUBMIT");
runFlavor = PLATFORM_PRESUBMIT;
} else if (launcherBuildMatcher.group("platform") != null
- && platformBuildMatcher.group("postsubmit") != null) {
+ && (platformBuildMatcher.group("postsubmit") != null
+ || platformBuildMatcher.group("commandLine") != null)) {
Log.d(TAG, "PLATFORM POSTSUBMIT");
runFlavor = PLATFORM_POSTSUBMIT;
} else {
diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
index 03d1600..afb50e0 100644
--- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
+++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
@@ -37,8 +37,10 @@
}
public void addAutomatically() {
- mLauncher.waitForObjectInContainer(
- mWidgetCell.getParent().getParent().getParent().getParent(),
- By.text(ADD_AUTOMATICALLY)).click();
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ mLauncher.waitForObjectInContainer(
+ mWidgetCell.getParent().getParent().getParent().getParent(),
+ By.text(ADD_AUTOMATICALLY)).click();
+ }
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 3bdeb14..4a2d699 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -99,8 +99,9 @@
*/
@NonNull
public AppIcon getAppIcon(String appName) {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "getting app icon " + appName + " on all apps")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "getting app icon " + appName + " on all apps")) {
final UiObject2 allAppsContainer = verifyActiveContainer();
final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer,
"apps_list_view");
@@ -130,6 +131,9 @@
searchBox.getVisibleBounds().bottom
- allAppsContainer.getVisibleBounds().top);
final int newScroll = getAllAppsScroll();
+ mLauncher.assertTrue(
+ "Scrolled in a wrong direction in AllApps: from " + scroll + " to "
+ + newScroll, newScroll >= scroll);
if (newScroll == scroll) break;
mLauncher.assertTrue(
@@ -197,7 +201,8 @@
* Flings forward (down) and waits the fling's end.
*/
public void flingForward() {
- try (LauncherInstrumentation.Closable c =
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c =
mLauncher.addContextLayer("want to fling forward in all apps")) {
final UiObject2 allAppsContainer = verifyActiveContainer();
// Start the gesture in the center to avoid starting at elements near the top.
@@ -211,7 +216,8 @@
* Flings backward (up) and waits the fling's end.
*/
public void flingBackward() {
- try (LauncherInstrumentation.Closable c =
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c =
mLauncher.addContextLayer("want to fling backward in all apps")) {
final UiObject2 allAppsContainer = verifyActiveContainer();
// Start the gesture in the center, for symmetry with forward.
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
index f48d4dd..835790d 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
@@ -42,8 +42,9 @@
*/
@NonNull
public Overview switchBackToOverview() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "want to switch back from all apps to overview")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to switch back from all apps to overview")) {
final UiObject2 allAppsContainer = verifyActiveContainer();
// Swipe from the search box to the bottom.
final UiObject2 qsb = mLauncher.waitForObjectInContainer(
@@ -55,7 +56,8 @@
final int endY = start.y + swipeHeight;
LauncherInstrumentation.log("AllAppsFromOverview.switchBackToOverview before swipe");
- mLauncher.swipeToState(start.x, start.y, start.x, endY, 60, OVERVIEW_STATE_ORDINAL);
+ mLauncher.swipeToState(start.x, start.y, start.x, endY, 60, OVERVIEW_STATE_ORDINAL,
+ LauncherInstrumentation.GestureScope.INSIDE);
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("swiped down")) {
return new Overview(mLauncher);
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 44fc3f7..0a6ed7f 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,9 +16,6 @@
package com.android.launcher3.tapl;
-import android.graphics.Point;
-import android.os.SystemClock;
-import android.view.MotionEvent;
import android.widget.TextView;
import androidx.test.uiautomator.By;
@@ -29,6 +26,7 @@
* App icon, whether in all apps or in workspace/
*/
public final class AppIcon extends Launchable {
+
AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
super(launcher, icon);
}
@@ -41,14 +39,10 @@
* Long-clicks the icon to open its menu.
*/
public AppIconMenu openMenu() {
- final Point iconCenter = mObject.getVisibleCenter();
- final long downTime = SystemClock.uptimeMillis();
- mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, iconCenter);
- final UiObject2 deepShortcutsContainer = mLauncher.waitForLauncherObject(
- "deep_shortcuts_container");
- mLauncher.sendPointer(
- downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, iconCenter);
- return new AppIconMenu(mLauncher, deepShortcutsContainer);
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
+ mObject, "deep_shortcuts_container"));
+ }
}
@Override
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index d9ae778..c37e451 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -52,8 +52,9 @@
*/
@NonNull
public BaseOverview switchToOverview() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "want to switch from background to overview")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to switch from background to overview")) {
verifyActiveContainer();
goToOverviewUnchecked();
return mLauncher.isFallbackOverview() ?
@@ -61,6 +62,10 @@
}
}
+ protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
+ return false;
+ }
+
protected void goToOverviewUnchecked() {
switch (mLauncher.getNavigationModel()) {
case ZERO_BUTTON: {
@@ -73,19 +78,26 @@
new Point(centerX, startY - swipeHeight - mLauncher.getTouchSlop());
final long downTime = SystemClock.uptimeMillis();
- mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
+ final LauncherInstrumentation.GestureScope gestureScope =
+ zeroButtonToOverviewGestureStartsInLauncher()
+ ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+ : LauncherInstrumentation.GestureScope.OUTSIDE;
+ mLauncher.sendPointer(
+ downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
mLauncher.executeAndWaitForEvent(
() -> mLauncher.movePointer(
downTime,
downTime,
ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION,
start,
- end),
+ end,
+ gestureScope),
event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(event.getClassName()),
() -> "Pause wasn't detected");
mLauncher.runToState(
() -> mLauncher.sendPointer(
- downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end),
+ downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end,
+ gestureScope),
OVERVIEW_STATE_ORDINAL);
break;
}
@@ -108,7 +120,8 @@
startY = endY = mLauncher.getDevice().getDisplayHeight() / 2;
}
- mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL);
+ mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL,
+ LauncherInstrumentation.GestureScope.OUTSIDE);
break;
}
@@ -124,8 +137,9 @@
* Swipes right or double presses the square button to switch to the previous app.
*/
public Background quickSwitchToPreviousApp() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "want to quick switch to the previous app")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to quick switch to the previous app")) {
verifyActiveContainer();
quickSwitchToPreviousApp(getExpectedStateForQuickSwitch());
return new Background(mLauncher);
@@ -160,7 +174,12 @@
endX = startX;
endY = 0;
}
- mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState);
+ mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState,
+ mLauncher.getNavigationModel()
+ == LauncherInstrumentation.NavigationModel.ZERO_BUTTON
+ ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+ : LauncherInstrumentation.GestureScope.OUTSIDE
+ );
break;
}
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 339e14f..e5c83e2 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -48,6 +48,12 @@
* Flings forward (left) and waits the fling's end.
*/
public void flingForward() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ flingForwardImpl();
+ }
+ }
+
+ private void flingForwardImpl() {
try (LauncherInstrumentation.Closable c =
mLauncher.addContextLayer("want to fling forward in overview")) {
LauncherInstrumentation.log("Overview.flingForward before fling");
@@ -65,17 +71,19 @@
* Dismissed all tasks by scrolling to Clear-all button and pressing it.
*/
public void dismissAllTasks() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "dismissing all tasks")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "dismissing all tasks")) {
final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
for (int i = 0;
i < FLINGS_FOR_DISMISS_LIMIT
&& !verifyActiveContainer().hasObject(clearAllSelector);
++i) {
- flingForward();
+ flingForwardImpl();
}
- mLauncher.waitForObjectInContainer(verifyActiveContainer(), clearAllSelector).click();
+ mLauncher.clickLauncherObject(
+ mLauncher.waitForObjectInContainer(verifyActiveContainer(), clearAllSelector));
}
}
@@ -83,7 +91,8 @@
* Flings backward (right) and waits the fling's end.
*/
public void flingBackward() {
- try (LauncherInstrumentation.Closable c =
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c =
mLauncher.addContextLayer("want to fling backward in overview")) {
LauncherInstrumentation.log("Overview.flingBackward before fling");
final UiObject2 overview = verifyActiveContainer();
diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java
deleted file mode 100644
index 6e6734d..0000000
--- a/tests/tapl/com/android/launcher3/tapl/Folder.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.tapl;
-
-import android.widget.FrameLayout;
-
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.UiObject2;
-
-/**
- * App folder in workspace/
- */
-public final class Folder {
- Folder(LauncherInstrumentation launcher, UiObject2 icon) {
- }
-
- static BySelector getSelector(String folderName, LauncherInstrumentation launcher) {
- return By.clazz(FrameLayout.class).desc(folderName).pkg(launcher.getLauncherPackageName());
- }
-}
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index 1e4d937..c06e254 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -48,8 +48,9 @@
@NonNull
@Override
public Overview switchToOverview() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "want to switch from home to overview")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to switch from home to overview")) {
verifyActiveContainer();
goToOverviewUnchecked();
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
@@ -60,6 +61,11 @@
}
@Override
+ protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
+ return true;
+ }
+
+ @Override
protected int getExpectedStateForQuickSwitch() {
return QUICK_SWITCH_STATE_ORDINAL;
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 6881197..1722d5b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -46,7 +46,9 @@
* Clicks the object to launch its app.
*/
public Background launch(String expectedPackageName) {
- return launch(By.pkg(expectedPackageName));
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ return launch(By.pkg(expectedPackageName));
+ }
}
private Background launch(BySelector selector) {
@@ -54,7 +56,7 @@
mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
mLauncher.executeAndWaitForEvent(
- () -> mObject.click(),
+ () -> mLauncher.clickLauncherObject(mObject),
event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
() -> "Launching an app didn't open a new window: " + mObject.getText());
@@ -69,17 +71,20 @@
* Drags an object to the center of homescreen.
*/
public void dragToWorkspace() {
- final Point launchableCenter = getObject().getVisibleCenter();
- final Point displaySize = mLauncher.getRealDisplaySize();
- final int width = displaySize.x / 2;
- Workspace.dragIconToWorkspace(
- mLauncher,
- this,
- new Point(
- launchableCenter.x >= width ?
- launchableCenter.x - width / 2 : launchableCenter.x + width / 2,
- displaySize.y / 2),
- getLongPressIndicator());
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ final Point launchableCenter = getObject().getVisibleCenter();
+ final Point displaySize = mLauncher.getRealDisplaySize();
+ final int width = displaySize.x / 2;
+ Workspace.dragIconToWorkspace(
+ mLauncher,
+ this,
+ new Point(
+ launchableCenter.x >= width
+ ? launchableCenter.x - width / 2
+ : launchableCenter.x + width / 2,
+ displaySize.y / 2),
+ getLongPressIndicator());
+ }
}
protected abstract String getLongPressIndicator();
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index e37d898..9b14cd3 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -49,12 +49,12 @@
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.Configurator;
import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.StaleObjectException;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
@@ -68,9 +68,11 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Date;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
@@ -78,6 +80,8 @@
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -91,6 +95,14 @@
private static final int GESTURE_STEP_MS = 16;
private static long START_TIME = System.currentTimeMillis();
+ static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
+ "[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9]"
+ + ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<event>.*)");
+
+ private static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
+ private static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
+ private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
+
// Types for launcher containers that the user is interacting with. "Background" is a
// pseudo-container corresponding to inactive launcher covered by another app.
public enum ContainerType {
@@ -99,6 +111,13 @@
public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
+ // Where the gesture happens: outside of Launcher, inside or from inside to outside.
+ enum GestureScope {
+ OUTSIDE, INSIDE, INSIDE_TO_OUTSIDE
+ }
+
+ ;
+
// Base class for launcher containers.
static abstract class VisibleContainer {
protected final LauncherInstrumentation mLauncher;
@@ -145,6 +164,20 @@
private Consumer<ContainerType> mOnSettledStateAction;
+ // Not null when we are collecting expected events to compare with actual ones.
+ private List<Pattern> mExpectedEvents;
+
+ private String mTimeBeforeFirstLogEvent;
+
+ private static Pattern getTouchEventPattern(String action) {
+ // The pattern includes sanity checks that we don't get a multi-touch events or other
+ // surprises.
+ return Pattern.compile(
+ "Touch event: MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
+ +
+ ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount=1");
+ }
+
/**
* Constructs the root of TAPL hierarchy. You get all other objects from it.
*/
@@ -183,13 +216,8 @@
.authority(testProviderAuthority)
.build();
- try {
- mDevice.executeShellCommand("pm grant " + testPackage +
- " android.permission.WRITE_SECURE_SETTINGS");
- } catch (IOException e) {
- fail(e.toString());
- }
-
+ mInstrumentation.getUiAutomation().grantRuntimePermission(
+ testPackage, "android.permission.WRITE_SECURE_SETTINGS");
PackageManager pm = getContext().getPackageManager();
ProviderInfo pi = pm.resolveContentProvider(
@@ -260,9 +288,9 @@
Closable addContextLayer(String piece) {
mDiagnosticContext.addLast(piece);
- log("Added context: " + getContextDescription());
+ log("Entering context: " + piece);
return () -> {
- log("Removing context: " + getContextDescription());
+ log("Leaving context: " + piece);
mDiagnosticContext.removeLast();
};
}
@@ -303,8 +331,29 @@
public void checkForAnomaly() {
final String anomalyMessage = getAnomalyMessage();
if (anomalyMessage != null) {
- failWithSystemHealth(
- "Tests are broken by a non-Launcher system error: " + anomalyMessage);
+ String message = "Tests are broken by a non-Launcher system error: " + anomalyMessage;
+ log("Hierarchy dump for: " + message);
+ dumpViewHierarchy();
+
+ Assert.fail(formatSystemHealthMessage(message));
+ }
+ }
+
+ private String getVisiblePackages() {
+ return mDevice.findObjects(By.textStartsWith(""))
+ .stream()
+ .map(LauncherInstrumentation::getApplicationPackageSafe)
+ .distinct()
+ .filter(pkg -> pkg != null && !"com.android.systemui".equals(pkg))
+ .collect(Collectors.joining(", "));
+ }
+
+ private static String getApplicationPackageSafe(UiObject2 object) {
+ try {
+ return object.getApplicationPackage();
+ } catch (StaleObjectException e) {
+ // We are looking at all object in the system; external ones can suddenly go away.
+ return null;
}
}
@@ -314,7 +363,7 @@
if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
- return "Background";
+ return "Background (" + getVisiblePackages() + ")";
}
public void setSystemHealthSupplier(Function<Long, String> supplier) {
@@ -325,41 +374,42 @@
mOnSettledStateAction = onSettledStateAction;
}
- private String getSystemHealthMessage() {
+ private String formatSystemHealthMessage(String message) {
final String testPackage = getContext().getPackageName();
- try {
- mDevice.executeShellCommand("pm grant " + testPackage +
- " android.permission.READ_LOGS");
- mDevice.executeShellCommand("pm grant " + testPackage +
- " android.permission.PACKAGE_USAGE_STATS");
- } catch (IOException e) {
- e.printStackTrace();
- }
- return mSystemHealthSupplier != null
+ mInstrumentation.getUiAutomation().grantRuntimePermission(
+ testPackage, "android.permission.READ_LOGS");
+ mInstrumentation.getUiAutomation().grantRuntimePermission(
+ testPackage, "android.permission.PACKAGE_USAGE_STATS");
+
+ final String systemHealth = mSystemHealthSupplier != null
? mSystemHealthSupplier.apply(START_TIME)
: TestHelpers.getSystemHealthMessage(getContext(), START_TIME);
+
+ if (systemHealth != null) {
+ return message
+ + ",\nperhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
+ + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
+ }
+
+ return message;
}
private void fail(String message) {
checkForAnomaly();
- failWithSystemHealth("http://go/tapl : " + getContextDescription() + message +
- " (visible state: " + getVisibleStateMessage() + ")");
- }
-
- private void failWithSystemHealth(String message) {
- final String systemHealth = getSystemHealthMessage();
- if (systemHealth != null) {
- message = message
- + ", perhaps because of system health problems:\n<<<<<<<<<<<<<<<<<<\n"
- + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
- }
-
+ message = "http://go/tapl : " + getContextDescription() + message
+ + " (visible state: " + getVisibleStateMessage() + ")";
log("Hierarchy dump for: " + message);
dumpViewHierarchy();
- Assert.fail(message);
+ final String eventMismatch = getEventMismatchMessage(false);
+
+ if (eventMismatch != null) {
+ message = message + ",\nhaving produced wrong events:\n " + eventMismatch;
+ }
+
+ Assert.fail(formatSystemHealthMessage(message));
}
private String getContextDescription() {
@@ -429,12 +479,6 @@
assertEquals("Unexpected display rotation",
mExpectedRotation, mDevice.getDisplayRotation());
- // b/136278866
- for (int i = 0; i != 100; ++i) {
- if (getNavigationModeMismatchError() == null) break;
- sleep(100);
- }
-
final String error = getNavigationModeMismatchError();
assertTrue(error, error == null);
log("verifyContainerType: " + containerType);
@@ -534,56 +578,65 @@
* @return the Workspace object.
*/
public Workspace pressHome() {
- // Click home, then wait for any accessibility event, then wait until accessibility events
- // stop.
- // We need waiting for any accessibility event generated after pressing Home because
- // otherwise waitForIdle may return immediately in case when there was a big enough pause in
- // accessibility events prior to pressing Home.
- final String action;
- if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
- checkForAnomaly();
+ try (LauncherInstrumentation.Closable e = eventsCheck()) {
+ // Click home, then wait for any accessibility event, then wait until accessibility
+ // events stop.
+ // We need waiting for any accessibility event generated after pressing Home because
+ // otherwise waitForIdle may return immediately in case when there was a big enough
+ // pause in accessibility events prior to pressing Home.
+ final String action;
+ if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
+ checkForAnomaly();
- final Point displaySize = getRealDisplaySize();
+ final Point displaySize = getRealDisplaySize();
- if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
- linearGesture(
- displaySize.x / 2, displaySize.y - 1,
- displaySize.x / 2, 0,
- ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
- false);
- try (LauncherInstrumentation.Closable c = addContextLayer(
- "Swiped up from context menu to home")) {
- waitUntilGone(CONTEXT_MENU_RES_ID);
- }
- }
- if (hasLauncherObject(WORKSPACE_RES_ID)) {
- log(action = "already at home");
- } else {
- log("Hierarchy before swiping up to home");
- dumpViewHierarchy();
- log(action = "swiping up to home from " + getVisibleStateMessage());
-
- try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
- swipeToState(
+ if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
+ linearGesture(
displaySize.x / 2, displaySize.y - 1,
displaySize.x / 2, 0,
- ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL);
+ ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
+ false, GestureScope.INSIDE_TO_OUTSIDE);
+ try (LauncherInstrumentation.Closable c = addContextLayer(
+ "Swiped up from context menu to home")) {
+ waitUntilGone(CONTEXT_MENU_RES_ID);
+ }
+ }
+ if (hasLauncherObject(WORKSPACE_RES_ID)) {
+ log(action = "already at home");
+ } else {
+ log("Hierarchy before swiping up to home:");
+ dumpViewHierarchy();
+ log(action = "swiping up to home from " + getVisibleStateMessage());
+
+ try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
+ swipeToState(
+ displaySize.x / 2, displaySize.y - 1,
+ displaySize.x / 2, 0,
+ ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
+ hasLauncherObject(By.textStartsWith(""))
+ ? GestureScope.INSIDE_TO_OUTSIDE
+ : GestureScope.OUTSIDE);
+ }
+ }
+ } else {
+ log("Hierarchy before clicking home:");
+ dumpViewHierarchy();
+ log(action = "clicking home button from " + getVisibleStateMessage());
+ try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
+ mDevice.waitForIdle();
+ runToState(
+ waitForSystemUiObject("home")::click,
+ NORMAL_STATE_ORDINAL,
+ !hasLauncherObject(WORKSPACE_RES_ID)
+ && (hasLauncherObject(APPS_RES_ID)
+ || hasLauncherObject(OVERVIEW_RES_ID)));
+ mDevice.waitForIdle();
}
}
- } else {
- log(action = "clicking home button");
- executeAndWaitForEvent(
- () -> {
- log("LauncherInstrumentation.pressHome before clicking");
- waitForSystemUiObject("home").click();
- },
- event -> true,
- () -> "Pressing Home didn't produce any events");
- mDevice.waitForIdle();
- }
- try (LauncherInstrumentation.Closable c = addContextLayer(
- "performed action to switch to Home - " + action)) {
- return getWorkspace();
+ try (LauncherInstrumentation.Closable c = addContextLayer(
+ "performed action to switch to Home - " + action)) {
+ return getWorkspace();
+ }
}
}
@@ -674,6 +727,20 @@
}
}
+ /**
+ * Gets the Options Popup Menu object if the current state is showing the popup menu. Fails if
+ * the launcher is not in that state.
+ *
+ * @return Options Popup Menu object.
+ */
+ @NonNull
+ public OptionsPopupMenu getOptionsPopupMenu() {
+ try (LauncherInstrumentation.Closable c = addContextLayer(
+ "want to get context menu object")) {
+ return new OptionsPopupMenu(this);
+ }
+ }
+
void waitUntilGone(String resId) {
assertTrue("Unexpected launcher object visible: " + resId,
mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
@@ -717,11 +784,18 @@
return object;
}
- @Nullable
private boolean hasLauncherObject(String resId) {
return mDevice.hasObject(getLauncherObjectSelector(resId));
}
+ private boolean hasLauncherObject(BySelector selector) {
+ return mDevice.hasObject(makeLauncherSelector(selector));
+ }
+
+ private BySelector makeLauncherSelector(BySelector selector) {
+ return By.copy(selector).pkg(getLauncherPackageName());
+ }
+
@NonNull
UiObject2 waitForLauncherObject(String resName) {
return waitForObjectBySelector(getLauncherObjectSelector(resName));
@@ -729,12 +803,12 @@
@NonNull
UiObject2 waitForLauncherObject(BySelector selector) {
- return waitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()));
+ return waitForObjectBySelector(makeLauncherSelector(selector));
}
@NonNull
UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
- return tryWaitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()), timeout);
+ return tryWaitForObjectBySelector(makeLauncherSelector(selector), timeout);
}
@NonNull
@@ -783,12 +857,20 @@
+ "]";
}
+ void runToState(Runnable command, int expectedState, boolean requireEvent) {
+ if (requireEvent) {
+ runToState(command, expectedState);
+ } else {
+ command.run();
+ }
+ }
+
void runToState(Runnable command, int expectedState) {
final List<Integer> actualEvents = new ArrayList<>();
executeAndWaitForEvent(
command,
event -> isSwitchToStateEvent(event, expectedState, actualEvents),
- () -> "Failed to receive an event for the swipe end: expected "
+ () -> "Failed to receive an event for the state change: expected "
+ TestProtocol.stateOrdinalToString(expectedState)
+ ", actual: " + eventListToString(actualEvents));
}
@@ -803,8 +885,11 @@
return actualState == expectedState;
}
- void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState) {
- runToState(() -> linearGesture(startX, startY, endX, endY, steps, false), expectedState);
+ void swipeToState(int startX, int startY, int endX, int endY, int steps, int expectedState,
+ GestureScope gestureScope) {
+ runToState(
+ () -> linearGesture(startX, startY, endX, endY, steps, false, gestureScope),
+ expectedState);
}
int getBottomGestureSize() {
@@ -817,6 +902,12 @@
return container.getVisibleBounds().bottom - bottomGestureStartOnScreen;
}
+ void clickLauncherObject(UiObject2 object) {
+ expectEvent(LauncherInstrumentation.EVENT_TOUCH_DOWN);
+ expectEvent(LauncherInstrumentation.EVENT_TOUCH_UP);
+ object.click();
+ }
+
void scrollToLastVisibleRow(
UiObject2 container,
Collection<UiObject2> items,
@@ -888,7 +979,8 @@
}
executeAndWaitForEvent(
- () -> linearGesture(startX, startY, endX, endY, steps, slowDown),
+ () -> linearGesture(
+ startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
() -> "Didn't receive a scroll end message: " + startX + ", " + startY
+ ", " + endX + ", " + endY);
@@ -896,21 +988,24 @@
// Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
// fixed interval each time.
- void linearGesture(int startX, int startY, int endX, int endY, int steps, boolean slowDown) {
+ void linearGesture(int startX, int startY, int endX, int endY, int steps, boolean slowDown,
+ GestureScope gestureScope) {
log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
final long downTime = SystemClock.uptimeMillis();
final Point start = new Point(startX, startY);
final Point end = new Point(endX, endY);
- sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start);
- final long endTime = movePointer(start, end, steps, downTime, slowDown);
- sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end);
+ sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
+ final long endTime = movePointer(start, end, steps, downTime, slowDown, gestureScope);
+ sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
}
- long movePointer(Point start, Point end, int steps, long downTime, boolean slowDown) {
- long endTime = movePointer(downTime, downTime, steps * GESTURE_STEP_MS, start, end);
+ long movePointer(Point start, Point end, int steps, long downTime, boolean slowDown,
+ GestureScope gestureScope) {
+ long endTime = movePointer(
+ downTime, downTime, steps * GESTURE_STEP_MS, start, end, gestureScope);
if (slowDown) {
endTime = movePointer(downTime, endTime + GESTURE_STEP_MS, 5 * GESTURE_STEP_MS, end,
- end);
+ end, gestureScope);
}
return endTime;
}
@@ -945,13 +1040,27 @@
0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
}
- void sendPointer(long downTime, long currentTime, int action, Point point) {
+ void sendPointer(long downTime, long currentTime, int action, Point point,
+ GestureScope gestureScope) {
+ if (gestureScope != GestureScope.OUTSIDE) {
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ expectEvent(EVENT_TOUCH_DOWN);
+ break;
+ case MotionEvent.ACTION_UP:
+ expectEvent(gestureScope == GestureScope.INSIDE
+ ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
+ break;
+ }
+ }
+
final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);
mInstrumentation.getUiAutomation().injectInputEvent(event, true);
event.recycle();
}
- long movePointer(long downTime, long startTime, long duration, Point from, Point to) {
+ long movePointer(long downTime, long startTime, long duration, Point from, Point to,
+ GestureScope gestureScope) {
log("movePointer: " + from + " to " + to);
final Point point = new Point();
long steps = duration / GESTURE_STEP_MS;
@@ -965,7 +1074,7 @@
point.x = from.x + (int) (progress * (to.x - from.x));
point.y = from.y + (int) (progress * (to.y - from.y));
- sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point);
+ sendPointer(downTime, currentTime, MotionEvent.ACTION_MOVE, point, gestureScope);
}
return currentTime;
}
@@ -974,6 +1083,17 @@
return getSystemIntegerRes(context, "config_navBarInteractionMode");
}
+ @NonNull
+ UiObject2 clickAndGet(@NonNull final UiObject2 target, @NonNull String resName) {
+ final Point targetCenter = target.getVisibleCenter();
+ final long downTime = SystemClock.uptimeMillis();
+ sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter, GestureScope.INSIDE);
+ final UiObject2 result = waitForLauncherObject(resName);
+ sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
+ GestureScope.INSIDE);
+ return result;
+ }
+
private static int getSystemIntegerRes(Context context, String resName) {
Resources res = context.getResources();
int resId = res.getIdentifier(resName, "integer", "android");
@@ -1033,6 +1153,10 @@
getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
+ public int getPid() {
+ return getTestInfo(TestProtocol.REQUEST_PID).getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ }
+
public void produceJavaLeak() {
getTestInfo(TestProtocol.REQUEST_JAVA_LEAK);
}
@@ -1054,4 +1178,98 @@
}
return tasks;
}
+
+ private List<String> getEvents() {
+ final ArrayList<String> events = new ArrayList<>();
+ try {
+ final String logcatTimeParameter =
+ mTimeBeforeFirstLogEvent != null ? " -t " + mTimeBeforeFirstLogEvent : "";
+ final String logcatEvents = mDevice.executeShellCommand(
+ "logcat -d --pid=" + getPid() + logcatTimeParameter
+ + " -s " + TestProtocol.TAPL_EVENTS_TAG);
+ final Matcher matcher = EVENT_LOG_ENTRY.matcher(logcatEvents);
+ while (matcher.find()) {
+ events.add(matcher.group("event"));
+ }
+ return events;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void startRecordingEvents() {
+ Assert.assertTrue("Already recording events", mExpectedEvents == null);
+ mExpectedEvents = new ArrayList<>();
+ mTimeBeforeFirstLogEvent = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
+ .format(new Date())
+ .replaceAll(" ", "");
+ }
+
+ private void stopRecordingEvents() {
+ mExpectedEvents = null;
+ }
+
+ Closable eventsCheck() {
+ // Entering events check block.
+ startRecordingEvents();
+
+ return () -> {
+ // Leaving events check block.
+ if (mExpectedEvents == null) {
+ return; // There was a failure. Noo need to report another one.
+ }
+
+ final String message = getEventMismatchMessage(true);
+ if (message != null) {
+ Assert.fail(formatSystemHealthMessage(
+ "http://go/tapl : unexpected event sequence: " + message));
+ }
+ };
+ }
+
+ void expectEvent(Pattern expected) {
+ if (mExpectedEvents != null) mExpectedEvents.add(expected);
+ }
+
+ private String getEventMismatchMessage(boolean waitForExpectedCount) {
+ if (mExpectedEvents == null) return null;
+
+ try {
+ List<String> actual = getEvents();
+
+ if (waitForExpectedCount) {
+ // Wait until Launcher generates the expected number of events.
+ final long endTime = SystemClock.uptimeMillis() + WAIT_TIME_MS;
+ while (SystemClock.uptimeMillis() < endTime
+ && actual.size() < mExpectedEvents.size()) {
+ SystemClock.sleep(100);
+ actual = getEvents();
+ }
+ }
+
+ for (int i = 0; i < mExpectedEvents.size(); ++i) {
+ if (i >= actual.size()) {
+ return formatEventMismatchMessage("too few actual events", actual, i);
+ }
+ if (!mExpectedEvents.get(i).matcher(actual.get(i)).find()) {
+ return formatEventMismatchMessage("mismatched event", actual, i);
+ }
+ }
+
+ if (actual.size() > mExpectedEvents.size()) {
+ return formatEventMismatchMessage(
+ "too many actual events", actual, mExpectedEvents.size());
+ }
+ } finally {
+ stopRecordingEvents();
+ }
+
+ return null;
+ }
+
+ private String formatEventMismatchMessage(String message, List<String> actual, int position) {
+ return message + ", pos=" + position
+ + ", expected=" + mExpectedEvents
+ + ", actual=" + actual;
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
new file mode 100644
index 0000000..282fca9
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+
+public class OptionsPopupMenu {
+
+ private final LauncherInstrumentation mLauncher;
+ private final UiObject2 mDeepShortcutsContainer;
+
+ OptionsPopupMenu(LauncherInstrumentation launcher) {
+ mLauncher = launcher;
+ mDeepShortcutsContainer = launcher.waitForLauncherObject("deep_shortcuts_container");
+ }
+
+ /**
+ * Returns a menu item with a given label. Fails if it doesn't exist.
+ */
+ @NonNull
+ public OptionsPopupMenuItem getMenuItem(@NonNull final String label) {
+ final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
+ By.text(label));
+ return new OptionsPopupMenuItem(mLauncher, menuItem);
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
new file mode 100644
index 0000000..c2f701b
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+public class OptionsPopupMenuItem {
+
+ private final LauncherInstrumentation mLauncher;
+ private final UiObject2 mObject;
+
+ OptionsPopupMenuItem(@NonNull LauncherInstrumentation launcher, @NonNull UiObject2 shortcut) {
+ mLauncher = launcher;
+ mObject = shortcut;
+ }
+
+ /**
+ * Clicks the option.
+ */
+ @NonNull
+ public void launch(@NonNull String expectedPackageName) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ LauncherInstrumentation.log("OptionsPopupMenuItem before click "
+ + mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
+ mLauncher.clickLauncherObject(mObject);
+ mLauncher.assertTrue(
+ "App didn't start: " + By.pkg(expectedPackageName),
+ mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)),
+ LauncherInstrumentation.WAIT_TIME_MS));
+ }
+ }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 16a64a7..4d673a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -45,8 +45,9 @@
*/
@NonNull
public AllAppsFromOverview switchToAllApps() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "want to switch from overview to all apps")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to switch from overview to all apps")) {
verifyActiveContainer();
// Swipe from an app icon to the top.
@@ -59,7 +60,8 @@
mLauncher.getDevice().getDisplayWidth() / 2,
0,
12,
- ALL_APPS_STATE_ORDINAL);
+ ALL_APPS_STATE_ORDINAL,
+ LauncherInstrumentation.GestureScope.INSIDE);
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
"swiped all way up from overview")) {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 46f8ba5..b21b242 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -45,14 +45,16 @@
* Swipes the task up.
*/
public void dismiss() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "want to dismiss a task")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to dismiss a task")) {
verifyActiveContainer();
// Dismiss the task via flinging it up.
final Rect taskBounds = mTask.getVisibleBounds();
final int centerX = taskBounds.centerX();
final int centerY = taskBounds.centerY();
- mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false);
+ mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false,
+ LauncherInstrumentation.GestureScope.INSIDE);
mLauncher.waitForIdle();
}
}
@@ -61,15 +63,17 @@
* Clicks at the task.
*/
public Background open() {
- verifyActiveContainer();
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "clicking an overview task")) {
- mLauncher.executeAndWaitForEvent(
- () -> mTask.click(),
- event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
- () -> "Launching task didn't open a new window: "
- + mTask.getParent().getContentDescription());
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ verifyActiveContainer();
+ try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "clicking an overview task")) {
+ mLauncher.executeAndWaitForEvent(
+ () -> mLauncher.clickLauncherObject(mTask),
+ event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+ () -> "Launching task didn't open a new window: "
+ + mTask.getParent().getContentDescription());
+ }
+ return new Background(mLauncher);
}
- return new Background(mLauncher);
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
index e882171..b8791e8 100644
--- a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
+++ b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
@@ -151,8 +151,7 @@
? "Current time: " + new Date(System.currentTimeMillis()) + "\n" + errors
: null;
} catch (Exception e) {
- return "Failed to get system health diags, maybe build your test via .bp instead of "
- + ".mk? " + android.util.Log.getStackTraceString(e);
+ return null;
}
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java
index 1b6d8c4..dfd74ed 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widget.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widget.java
@@ -22,6 +22,7 @@
* Widget in workspace or a widget list.
*/
public final class Widget extends Launchable {
+
Widget(LauncherInstrumentation launcher, UiObject2 icon) {
super(launcher, icon);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index d208c66..ede5bd9 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -41,8 +41,9 @@
* Flings forward (down) and waits the fling's end.
*/
public void flingForward() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "want to fling forward in widgets")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to fling forward in widgets")) {
LauncherInstrumentation.log("Widgets.flingForward enter");
final UiObject2 widgetsContainer = verifyActiveContainer();
mLauncher.scroll(
@@ -62,8 +63,9 @@
* Flings backward (up) and waits the fling's end.
*/
public void flingBackward() {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "want to fling backwards in widgets")) {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to fling backwards in widgets")) {
LauncherInstrumentation.log("Widgets.flingBackward enter");
final UiObject2 widgetsContainer = verifyActiveContainer();
mLauncher.scroll(
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 3299d5d..a0d5443 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -38,11 +38,21 @@
import com.android.launcher3.ResourceUtils;
import com.android.launcher3.testing.TestProtocol;
+import java.util.regex.Pattern;
+
/**
* Operations on the workspace screen.
*/
public final class Workspace extends Home {
private static final int FLING_STEPS = 10;
+
+ static final Pattern EVENT_CTRL_W_DOWN = Pattern.compile(
+ "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_W"
+ + ".*?metaState=META_CTRL_ON");
+ static final Pattern EVENT_CTRL_W_UP = Pattern.compile(
+ "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W"
+ + ".*?metaState=META_CTRL_ON");
+
private final UiObject2 mHotseat;
Workspace(LauncherInstrumentation launcher) {
@@ -85,7 +95,8 @@
*/
@NonNull
public AllApps switchToAllApps() {
- try (LauncherInstrumentation.Closable c =
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c =
mLauncher.addContextLayer("want to switch from workspace to all apps")) {
verifyActiveContainer();
final int deviceHeight = mLauncher.getDevice().getDisplayHeight();
@@ -107,7 +118,7 @@
0,
startY - swipeHeight - mLauncher.getTouchSlop(),
12,
- ALL_APPS_STATE_ORDINAL);
+ ALL_APPS_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE);
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
"swiped to all apps")) {
@@ -156,21 +167,23 @@
* second screen.
*/
public void ensureWorkspaceIsScrollable() {
- final UiObject2 workspace = verifyActiveContainer();
- if (!isWorkspaceScrollable(workspace)) {
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
- "dragging icon to a second page of workspace to make it scrollable")) {
- dragIconToWorkspace(
- mLauncher,
- getHotseatAppIcon("Chrome"),
- new Point(mLauncher.getDevice().getDisplayWidth(),
- workspace.getVisibleBounds().centerY()),
- "deep_shortcuts_container");
- verifyActiveContainer();
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ final UiObject2 workspace = verifyActiveContainer();
+ if (!isWorkspaceScrollable(workspace)) {
+ try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "dragging icon to a second page of workspace to make it scrollable")) {
+ dragIconToWorkspace(
+ mLauncher,
+ getHotseatAppIcon("Chrome"),
+ new Point(mLauncher.getDevice().getDisplayWidth(),
+ workspace.getVisibleBounds().centerY()),
+ "deep_shortcuts_container");
+ verifyActiveContainer();
+ }
}
+ assertTrue("Home screen workspace didn't become scrollable",
+ isWorkspaceScrollable(workspace));
}
- assertTrue("Home screen workspace didn't become scrollable",
- isWorkspaceScrollable(workspace));
}
private boolean isWorkspaceScrollable(UiObject2 workspace) {
@@ -183,12 +196,6 @@
mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
}
- @NonNull
- public Folder getHotseatFolder(String appName) {
- return new Folder(mLauncher, mLauncher.waitForObjectInContainer(
- mHotseat, Folder.getSelector(appName, mLauncher)));
- }
-
static void dragIconToWorkspace(
LauncherInstrumentation launcher, Launchable launchable, Point dest,
String longPressIndicator) {
@@ -198,17 +205,19 @@
launcher.runToState(
() -> {
launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
- launchableCenter);
+ launchableCenter, LauncherInstrumentation.GestureScope.INSIDE);
LauncherInstrumentation.log("dragIconToWorkspace: sent down");
launcher.waitForLauncherObject(longPressIndicator);
LauncherInstrumentation.log("dragIconToWorkspace: indicator");
- launcher.movePointer(launchableCenter, dest, 10, downTime, true);
+ launcher.movePointer(launchableCenter, dest, 10, downTime, true,
+ LauncherInstrumentation.GestureScope.INSIDE);
},
SPRING_LOADED_STATE_ORDINAL);
LauncherInstrumentation.log("dragIconToWorkspace: moved pointer");
launcher.runToState(
() -> launcher.sendPointer(
- downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest),
+ downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
+ LauncherInstrumentation.GestureScope.INSIDE),
NORMAL_STATE_ORDINAL);
LauncherInstrumentation.log("dragIconToWorkspace: end");
launcher.waitUntilGone("drop_target_bar");
@@ -219,11 +228,13 @@
* recoil to complete.
*/
public void flingForward() {
- final UiObject2 workspace = verifyActiveContainer();
- mLauncher.scroll(workspace, Direction.RIGHT,
- new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0),
- FLING_STEPS, false);
- verifyActiveContainer();
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ final UiObject2 workspace = verifyActiveContainer();
+ mLauncher.scroll(workspace, Direction.RIGHT,
+ new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0),
+ FLING_STEPS, false);
+ verifyActiveContainer();
+ }
}
/**
@@ -231,11 +242,13 @@
* recoil to complete.
*/
public void flingBackward() {
- final UiObject2 workspace = verifyActiveContainer();
- mLauncher.scroll(workspace, Direction.LEFT,
- new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0),
- FLING_STEPS, false);
- verifyActiveContainer();
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ final UiObject2 workspace = verifyActiveContainer();
+ mLauncher.scroll(workspace, Direction.LEFT,
+ new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0),
+ FLING_STEPS, false);
+ verifyActiveContainer();
+ }
}
/**
@@ -245,10 +258,14 @@
*/
@NonNull
public Widgets openAllWidgets() {
- verifyActiveContainer();
- mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
- try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
- return new Widgets(mLauncher);
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ verifyActiveContainer();
+ mLauncher.expectEvent(EVENT_CTRL_W_DOWN);
+ mLauncher.expectEvent(EVENT_CTRL_W_UP);
+ mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
+ try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
+ return new Widgets(mLauncher);
+ }
}
}
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
new file mode 100644
index 0000000..0f4163d
--- /dev/null
+++ b/tools/checkstyle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd" [
+ <!ENTITY defaultCopyrightCheck SYSTEM "../../../../prebuilts/checkstyle/default-copyright-check.xml">
+ <!ENTITY defaultJavadocChecks SYSTEM "../../../../prebuilts/checkstyle/default-javadoc-checks.xml">
+ <!ENTITY defaultTreewalkerChecks SYSTEM "../../../../prebuilts/checkstyle/default-treewalker-checks.xml">
+ <!ENTITY defaultModuleChecks SYSTEM "../../../../prebuilts/checkstyle/default-module-checks.xml">
+]>
+
+<module name="Checker">
+ &defaultModuleChecks;
+ &defaultCopyrightCheck;
+ <module name="TreeWalker">
+ &defaultJavadocChecks;
+ &defaultTreewalkerChecks;
+ </module>
+
+ <module name="SuppressionFilter">
+ <property name="file" value="tools/checkstyle_suppression.xml" />
+ </module>
+</module>
diff --git a/tools/checkstyle_suppression.xml b/tools/checkstyle_suppression.xml
new file mode 100644
index 0000000..799e750
--- /dev/null
+++ b/tools/checkstyle_suppression.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
+<suppressions>
+
+ <!-- Robolectric uses magic method names like `__constructor__` -->
+ <suppress files="/robolectric_tests" checks="MethodName|JavadocType|JavadocMethod" />
+
+</suppressions>
diff --git a/print_db.py b/tools/print_db.py
similarity index 100%
rename from print_db.py
rename to tools/print_db.py