blob: 7c97b93b038cd92f31d7ecd781041d516ed10c71 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES 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.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.robolectric.Shadows.shadowOf;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetId;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.os.Process;
import android.os.UserHandle;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.shadows.ShadowDeviceFlag;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.LauncherModelHelper;
import com.android.launcher3.util.ViewOnDrawExecutor;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowAppWidgetManager;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
@RunWith(RobolectricTestRunner.class)
public final class WidgetsPredicationUpdateTaskTest {
private AppWidgetProviderInfo mApp1Provider1 = new AppWidgetProviderInfo();
private AppWidgetProviderInfo mApp1Provider2 = new AppWidgetProviderInfo();
private AppWidgetProviderInfo mApp2Provider1 = new AppWidgetProviderInfo();
private AppWidgetProviderInfo mApp4Provider1 = new AppWidgetProviderInfo();
private AppWidgetProviderInfo mApp4Provider2 = new AppWidgetProviderInfo();
private AppWidgetProviderInfo mApp5Provider1 = new AppWidgetProviderInfo();
private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback();
private Context mContext;
private LauncherModelHelper mModelHelper;
private UserHandle mUserHandle;
private InvariantDeviceProfile mTestProfile;
@Mock
private IconCache mIconCache;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
doAnswer(invocation -> {
ComponentWithLabel componentWithLabel = invocation.getArgument(0);
return componentWithLabel.getComponent().getShortClassName();
}).when(mIconCache).getTitleNoCache(any());
mContext = RuntimeEnvironment.application;
mModelHelper = new LauncherModelHelper();
mUserHandle = Process.myUserHandle();
mTestProfile = new InvariantDeviceProfile();
// 2 widgets, app4/provider1 & app5/provider1, have already been added to the workspace.
mModelHelper.initializeData("/widgets_predication_update_task_data.txt");
ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
mApp1Provider1.provider = ComponentName.createRelative("app1", "provider1");
ReflectionHelpers.setField(mApp1Provider1, "providerInfo",
packageManager.addReceiverIfNotPresent(mApp1Provider1.provider));
mApp1Provider2.provider = ComponentName.createRelative("app1", "provider2");
ReflectionHelpers.setField(mApp1Provider2, "providerInfo",
packageManager.addReceiverIfNotPresent(mApp1Provider2.provider));
mApp2Provider1.provider = ComponentName.createRelative("app2", "provider1");
ReflectionHelpers.setField(mApp2Provider1, "providerInfo",
packageManager.addReceiverIfNotPresent(mApp2Provider1.provider));
mApp4Provider1.provider = ComponentName.createRelative("app4", "provider1");
ReflectionHelpers.setField(mApp4Provider1, "providerInfo",
packageManager.addReceiverIfNotPresent(mApp4Provider1.provider));
mApp4Provider2.provider = ComponentName.createRelative("app4", ".provider2");
ReflectionHelpers.setField(mApp4Provider2, "providerInfo",
packageManager.addReceiverIfNotPresent(mApp4Provider2.provider));
mApp5Provider1.provider = ComponentName.createRelative("app5", "provider1");
ReflectionHelpers.setField(mApp5Provider1, "providerInfo",
packageManager.addReceiverIfNotPresent(mApp5Provider1.provider));
ShadowAppWidgetManager shadowAppWidgetManager =
shadowOf(mContext.getSystemService(AppWidgetManager.class));
shadowAppWidgetManager.addInstalledProvider(mApp1Provider1);
shadowAppWidgetManager.addInstalledProvider(mApp1Provider2);
shadowAppWidgetManager.addInstalledProvider(mApp2Provider1);
shadowAppWidgetManager.addInstalledProvider(mApp4Provider1);
shadowAppWidgetManager.addInstalledProvider(mApp4Provider2);
shadowAppWidgetManager.addInstalledProvider(mApp5Provider1);
mModelHelper.getModel().addCallbacks(mCallback);
MODEL_EXECUTOR.post(() -> mModelHelper.getBgDataModel().widgetsModel.update(
LauncherAppState.getInstance(mContext), /* packageUser= */ null));
waitUntilIdle();
}
@Test
public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()
throws Exception {
// WHEN newPredicationTask is executed with app predication of 5 apps.
AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "className",
mUserHandle);
AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "className",
mUserHandle);
AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className",
mUserHandle);
AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "className",
mUserHandle);
AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "className",
mUserHandle);
mModelHelper.executeTaskForTest(
newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1)))
.forEach(Runnable::run);
// THEN only 3 widgets are returned because
// 1. app5/provider1 & app4/provider1 have already been added to workspace. They are
// excluded from the result.
// 2. app3 doesn't have a widget.
// 3. only 1 widget is picked from app1 because we only want to promote one widget per app.
List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
.stream()
.map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
.collect(Collectors.toList());
assertThat(recommendedWidgets).hasSize(3);
assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
assertWidgetInfo(recommendedWidgets.get(1).info, mApp4Provider2);
assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
}
@Test
public void widgetsRecommendationRan_localFilterDisabled_shouldReturnWidgetsInPredicationOrder()
throws Exception {
ShadowDeviceFlag shadowDeviceFlag = Shadow.extract(
FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER);
shadowDeviceFlag.setValue(false);
// WHEN newPredicationTask is executed with 5 predicated widgets.
AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
mUserHandle);
AppTarget widget2 = new AppTarget(new AppTargetId("app1"), "app1", "provider2",
mUserHandle);
// Not installed app
AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1",
mUserHandle);
// Not installed widget
AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider3",
mUserHandle);
AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
mUserHandle);
mModelHelper.executeTaskForTest(
newWidgetsPredicationTask(List.of(widget5, widget3, widget2, widget4, widget1)))
.forEach(Runnable::run);
// THEN only 3 widgets are returned because the launcher only filters out non-exist widgets.
List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
.stream()
.map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
.collect(Collectors.toList());
assertThat(recommendedWidgets).hasSize(3);
assertWidgetInfo(recommendedWidgets.get(0).info, mApp5Provider1);
assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider2);
assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
}
private void assertWidgetInfo(
LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) {
assertThat(actual.provider).isEqualTo(expected.provider);
assertThat(actual.getUser()).isEqualTo(expected.getProfile());
}
private void waitUntilIdle() {
shadowOf(MODEL_EXECUTOR.getLooper()).idle();
shadowOf(MAIN_EXECUTOR.getLooper()).idle();
}
private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List<AppTarget> appTargets) {
return new WidgetsPredictionUpdateTask(
new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"),
appTargets);
}
private final class FakeBgDataModelCallback implements BgDataModel.Callbacks {
private FixedContainerItems mRecommendedWidgets = null;
@Override
public void bindExtraContainerItems(FixedContainerItems item) {
mRecommendedWidgets = item;
}
@Override
public int getPageToBindSynchronously() {
return 0;
}
@Override
public void clearPendingBinds() { }
@Override
public void startBinding() { }
@Override
public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
@Override
public void bindScreens(IntArray orderedScreenIds) { }
@Override
public void finishFirstPageBind(ViewOnDrawExecutor executor) { }
@Override
public void finishBindingItems(int pageBoundFirst) { }
@Override
public void preAddApps() { }
@Override
public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
ArrayList<ItemInfo> addAnimated) { }
@Override
public void bindIncrementalDownloadProgressUpdated(AppInfo app) { }
@Override
public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
@Override
public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
@Override
public void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
@Override
public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
@Override
public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
@Override
public void onPageBoundSynchronously(int page) { }
@Override
public void executeOnNextDraw(ViewOnDrawExecutor executor) { }
@Override
public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }
@Override
public void bindAllApplications(AppInfo[] apps, int flags) { }
}
}