Merge "Revert "include integration test for open settings menu from workspace"" into ub-launcher3-master
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/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/protos/launcher_log.proto b/protos/launcher_log.proto
index 8f413dc..0560d68 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -93,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..0fd4aac
--- /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.rebindModel();
+ 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 72%
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..8f6081b 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;
@@ -44,6 +58,7 @@
import com.android.launcher3.icons.IconCache;
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 +76,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 +87,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 +97,7 @@
private int mPredictedSpotsCount = 0;
private Launcher mLauncher;
- private Hotseat mHotseat;
+ private final Hotseat mHotseat;
private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>();
@@ -87,10 +105,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 +128,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 +165,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 +197,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 +216,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 +266,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;
}
@@ -248,7 +311,11 @@
mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
}
updateDependencies();
- fillGapsWithPrediction();
+ if (isReady()) {
+ fillGapsWithPrediction();
+ } else if (mHotseatEduController != null) {
+ mHotseatEduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
+ }
}
private void updateDependencies() {
@@ -285,9 +352,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 +383,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 +411,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 +425,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 +439,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 +467,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 +483,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 +509,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 +542,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 +572,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..27ac284 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 Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private boolean mIsPinned = false;
private int mNormalizedIconRadius;
-
public PredictedAppIcon(Context context) {
this(context, null, 0);
}
@@ -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 37a3929..4b5ba95 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;
@@ -178,6 +178,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/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/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index bafb2ef..29df5cc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -484,7 +484,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);
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/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 bcaa126..47bc31a 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
@@ -830,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;
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/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..ce87527 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -66,5 +66,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">Your Hotseat just got smarter</string>
+ <!-- Hotseat migration notification content -->
+ <string translatable="false" name="hotseat_migrate_prompt_content">Tap here to setup and learn more</string>
+ <!-- Hotseat migration wizard title -->
+ <string translatable="false" name="hotseat_migrate_title">Pixel Suggests apps you\'ll need next</string>
+ <!-- Hotseat migration wizard message -->
+ <string translatable="false" name="hotseat_migrate_message">Suggested apps will replace the bottom row of apps. To pin an app, drag it over a suggested app. Touch & hold an app to hide it.</string>
+ <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
+ <string translatable="false" name="hotseat_items_migrated">Your hotseat items have been moved to the last page.</string>
+ <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
+ <string translatable="false" name="hotseat_no_migration">You can remove items from the hotseat manually to see suggested apps in their spot.</string>
+ <!-- Button text to opt in for fully predicted hotseat -->
+ <string translatable="false" name="hotseat_migrate_accept">Migrate</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/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/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
index 4bb9a53..d33fecd 100644
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
+++ b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
@@ -14,6 +14,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@@ -35,6 +36,8 @@
*/
public final class FlagOverrideRule implements TestRule {
+ private final HashMap<String, Boolean> mDefaultOverrides = new HashMap<>();
+
/**
* Container annotation for handling multiple {@link FlagOverride} annotations.
* <p>
@@ -60,6 +63,14 @@
return new MyStatement(base, description);
}
+ /**
+ * Sets a default override to apply on all tests
+ */
+ public FlagOverrideRule setOverride(BaseTogglableFlag flag, boolean value) {
+ mDefaultOverrides.put(flag.getKey(), value);
+ return this;
+ }
+
private class MyStatement extends Statement {
private final Statement mBase;
@@ -87,11 +98,15 @@
overrides = ((FlagOverrides) annotation).value();
}
}
- for (FlagOverride override : overrides) {
- BaseTogglableFlag flag = allFlags.get(override.key());
+
+ HashMap<String, Boolean> allOverrides = new HashMap<>(mDefaultOverrides);
+ Arrays.stream(overrides).forEach(o -> allOverrides.put(o.key(), o.value()));
+
+ allOverrides.forEach((key, val) -> {
+ BaseTogglableFlag flag = allFlags.get(key);
changedValues.put(flag, flag.get());
- flag.setForTests(override.value());
- }
+ flag.setForTests(val);
+ });
mBase.evaluate();
} finally {
// Clear the values
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
index 31a037b..2a359df 100644
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
+++ b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
@@ -4,16 +4,16 @@
import static org.junit.Assert.assertTrue;
import com.android.launcher3.config.FlagOverrideRule.FlagOverride;
+import com.android.launcher3.util.LauncherRoboTestRunner;
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)
+@RunWith(LauncherRoboTestRunner.class)
public class FlagOverrideSampleTest {
// Check out https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html for more information
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..ea7c137 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().initialize(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/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..e0ddcb1 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -22,6 +22,7 @@
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;
@@ -31,23 +32,20 @@
import com.android.launcher3.FolderInfo;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherProvider;
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;
@@ -61,10 +59,9 @@
/**
* 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 =
@@ -73,40 +70,37 @@
private static final int BITMAP_SIZE = 10;
private static final int GRID_SIZE = 4;
+ private LauncherModelHelper mModelHelper;
+ private Context mTargetContext;
+ private InvariantDeviceProfile mIdp;
+
@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;
+ mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
+ mIdp.numRows = mIdp.numColumns = mIdp.numHotseatIcons = GRID_SIZE;
+ mIdp.iconBitmapSize = BITMAP_SIZE;
- provider.setAllowLoadDefaultFavorites(true);
- Settings.Secure.putString(targetContext.getContentResolver(),
+ mModelHelper.provider.setAllowLoadDefaultFavorites(true);
+ Settings.Secure.putString(mTargetContext.getContentResolver(),
"launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
- ShadowPackageManager spm = shadowOf(targetContext.getPackageManager());
+ ShadowPackageManager spm = shadowOf(mTargetContext.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);
- }
-
@Test
public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
.putApp(SETTINGS_APP, SETTINGS_APP));
// 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);
}
@@ -120,8 +114,8 @@
.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 +128,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,8 +138,8 @@
.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);
@@ -155,13 +149,21 @@
ByteArrayOutputStream bos = new ByteArrayOutputStream();
builder.build(new OutputStreamWriter(bos));
- Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, targetContext);
- shadowOf(targetContext.getContentResolver()).registerInputStream(layoutUri,
+ Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, mTargetContext);
+ shadowOf(mTargetContext.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(),
+ 0,
+ new WeakReference<>(mock(Callbacks.class)));
+ 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..8dd7588 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},
@@ -181,7 +192,7 @@
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
// 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},
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
similarity index 76%
rename from tests/src/com/android/launcher3/model/LoaderCursorTest.java
rename to robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 0dcfaa8..4854314 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,43 +41,38 @@
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;
@@ -71,22 +83,18 @@
@Before
public void setup() {
- mIDP = new InvariantDeviceProfile();
+ mContext = RuntimeEnvironment.application;
+ mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
+ mApp = LauncherAppState.getInstance(mContext);
+ mLauncherApps = mContext.getSystemService(LauncherApps.class);
+
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 +117,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 +151,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);
}
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..ccbc18a 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -77,7 +77,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) {
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/shadows/ShadowTogglableFlag.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowTogglableFlag.java
new file mode 100644
index 0000000..3603dd8
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowTogglableFlag.java
@@ -0,0 +1,38 @@
+/*
+ * 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.TogglableFlag;
+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 = TogglableFlag.class, isInAndroidSdk = false)
+public class ShadowTogglableFlag {
+
+ /**
+ * Mock change listener as it uses internal system classes not available to robolectric
+ */
+ @Implementation
+ protected void addChangeListener(Context context, Runnable r) { }
+}
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..1a03f9f
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -0,0 +1,288 @@
+/*
+ * 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 android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.launcher3.AppInfo;
+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.util.ReflectionHelpers;
+
+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.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";
+
+ 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);
+ 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;
+ }
+}
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..5c6b486
--- /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.ShadowTogglableFlag;
+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,
+ ShadowTogglableFlag.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/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/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index f319ae1..ea63fa7 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;
@@ -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 21c819a..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;
@@ -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/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 52a393f..8b6d209 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -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/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 67fd7db..e005320 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,7 +55,7 @@
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.PackageUserKey;
@@ -107,18 +108,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();
}
}
};
@@ -140,7 +138,7 @@
}
public ModelWriter getWriter(boolean hasVerticalHotseat, boolean verifyChanges) {
- return new ModelWriter(mApp.getContext(), this, sBgDataModel,
+ return new ModelWriter(mApp.getContext(), this, mBgDataModel,
hasVerticalHotseat, verifyChanges);
}
@@ -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));
@@ -305,7 +303,7 @@
// If there is already one running, tell it to stop.
stopLoader();
- LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel,
+ LoaderResults loaderResults = new LoaderResults(mApp, mBgDataModel,
mBgAllAppsList, synchronousBindPage, mCallbacks);
if (mModelLoaded && !mIsLoaderTaskRunning) {
// Divide the set of loaded items into those that we are binding synchronously,
@@ -341,7 +339,7 @@
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 +487,7 @@
}
public void enqueueModelUpdateTask(ModelUpdateTask task) {
- task.init(mApp, this, sBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+ task.init(mApp, this, mBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
MODEL_EXECUTOR.execute(task);
}
@@ -560,7 +558,7 @@
+ " componentName=" + info.componentName.getPackageName());
}
}
- sBgDataModel.dump(prefix, fd, writer, args);
+ mBgDataModel.dump(prefix, fd, writer, args);
}
public Callbacks getCallback() {
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..f96e735 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -418,9 +418,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);
}
@@ -1760,9 +1757,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 +2434,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();
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/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 7334964..003ca82 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -114,7 +114,7 @@
"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");
+ "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");
public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
"ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
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/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 7bbd45d..f322061 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -180,6 +180,14 @@
icon.mLauncher = launcher;
icon.mDotRenderer = grid.mDotRendererWorkSpace;
icon.setContentDescription(launcher.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(launcher.getDotInfoForItem(si));
+ }
+ icon.setDotInfo(folderDotInfo);
+
Folder folder = Folder.fromXml(launcher);
folder.setDragController(launcher.getDragController());
folder.setFolderIcon(icon);
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/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/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index 499cd2a..8289da9 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -147,6 +147,12 @@
}
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;
}
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 0e20270..88f2a09 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.
*/
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/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/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index e70673a..72c95c4 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();
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..9987994 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -34,8 +34,8 @@
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.provider.LauncherDbUtils.SQLiteTransaction;
import com.android.launcher3.util.IntArray;
@@ -112,9 +112,16 @@
whereClause.append(" AND profileId != ?");
profileIds[i] = Long.toString(profileMapping.keyAt(i));
}
- int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
- if (itemsDeleted > 0) {
+ try {
+ int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
+ } catch (IllegalArgumentException exception) {
+ // b/147114476
+ FileLog.e(TAG, new StringBuilder("Failed to execute delete, where clause: '")
+ .append(whereClause).append("', profile Id size:").append(profileIds.length)
+ .append("profileIds: ").append(String.join(", ", profileIds)).toString()
+ );
+ throw exception;
}
// Mark all items as restored.
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..e6203b4
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+ }
+
+ public ShortcutRequest forPackage(String packageName, String... shortcutIds) {
+ return forPackage(packageName, Arrays.asList(shortcutIds));
+ }
+
+ 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/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 5aae841..dd8df88 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -86,6 +86,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/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..8b2ee36 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -332,4 +332,17 @@
}
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;
+ }
}
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_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/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/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/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index b715de0..28c9e7a 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -55,6 +55,7 @@
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;
@@ -311,12 +312,21 @@
private String getVisiblePackages() {
return mDevice.findObjects(By.textStartsWith(""))
.stream()
- .map(object -> object.getApplicationPackage())
+ .map(LauncherInstrumentation::getApplicationPackageSafe)
.distinct()
- .filter(pkg -> !"com.android.systemui".equals(pkg))
+ .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;
+ }
+ }
+
private String getVisibleStateMessage() {
if (hasLauncherObject(CONTEXT_MENU_RES_ID)) return "Context Menu";
if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
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;
}
}
}