1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.model; 17 18 import static android.os.Process.myUserHandle; 19 20 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; 21 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 22 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 23 import static com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo; 24 25 import static com.google.common.truth.Truth.assertThat; 26 27 import static org.mockito.ArgumentMatchers.any; 28 import static org.mockito.ArgumentMatchers.eq; 29 import static org.mockito.Mockito.doAnswer; 30 import static org.mockito.Mockito.doReturn; 31 32 import android.app.prediction.AppTarget; 33 import android.app.prediction.AppTargetId; 34 import android.appwidget.AppWidgetManager; 35 import android.appwidget.AppWidgetProviderInfo; 36 import android.content.ComponentName; 37 import android.os.UserHandle; 38 import android.text.TextUtils; 39 import android.util.Log; 40 41 import androidx.test.ext.junit.runners.AndroidJUnit4; 42 import androidx.test.filters.SmallTest; 43 44 import com.android.launcher3.LauncherAppState; 45 import com.android.launcher3.config.FeatureFlags; 46 import com.android.launcher3.icons.ComponentWithLabel; 47 import com.android.launcher3.icons.IconCache; 48 import com.android.launcher3.model.BgDataModel.FixedContainerItems; 49 import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; 50 import com.android.launcher3.util.LauncherModelHelper; 51 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 52 import com.android.launcher3.widget.PendingAddWidgetInfo; 53 54 import org.junit.After; 55 import org.junit.Before; 56 import org.junit.Test; 57 import org.junit.runner.RunWith; 58 import org.mockito.Mock; 59 import org.mockito.MockitoAnnotations; 60 61 import java.util.Arrays; 62 import java.util.List; 63 import java.util.stream.Collectors; 64 65 @SmallTest 66 @RunWith(AndroidJUnit4.class) 67 public final class WidgetsPredicationUpdateTaskTest { 68 69 private AppWidgetProviderInfo mApp1Provider1; 70 private AppWidgetProviderInfo mApp1Provider2; 71 private AppWidgetProviderInfo mApp2Provider1; 72 private AppWidgetProviderInfo mApp4Provider1; 73 private AppWidgetProviderInfo mApp4Provider2; 74 private AppWidgetProviderInfo mApp5Provider1; 75 private List<AppWidgetProviderInfo> allWidgets; 76 77 private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback(); 78 private LauncherModelHelper mModelHelper; 79 private UserHandle mUserHandle; 80 81 @Mock 82 private IconCache mIconCache; 83 84 @Before setup()85 public void setup() throws Exception { 86 mModelHelper = new LauncherModelHelper(); 87 MockitoAnnotations.initMocks(this); 88 doAnswer(invocation -> { 89 ComponentWithLabel componentWithLabel = invocation.getArgument(0); 90 return componentWithLabel.getComponent().getShortClassName(); 91 }).when(mIconCache).getTitleNoCache(any()); 92 93 mUserHandle = myUserHandle(); 94 mApp1Provider1 = createAppWidgetProviderInfo( 95 ComponentName.createRelative("app1", "provider1")); 96 mApp1Provider2 = createAppWidgetProviderInfo( 97 ComponentName.createRelative("app1", "provider2")); 98 mApp2Provider1 = createAppWidgetProviderInfo( 99 ComponentName.createRelative("app2", "provider1")); 100 mApp4Provider1 = createAppWidgetProviderInfo( 101 ComponentName.createRelative("app4", "provider1")); 102 mApp4Provider2 = createAppWidgetProviderInfo( 103 ComponentName.createRelative("app4", ".provider2")); 104 mApp5Provider1 = createAppWidgetProviderInfo( 105 ComponentName.createRelative("app5", "provider1")); 106 allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1, 107 mApp4Provider1, mApp4Provider2, mApp5Provider1); 108 109 AppWidgetManager manager = mModelHelper.sandboxContext.spyService(AppWidgetManager.class); 110 doReturn(allWidgets).when(manager).getInstalledProviders(); 111 doReturn(allWidgets).when(manager).getInstalledProvidersForProfile(eq(myUserHandle())); 112 doAnswer(i -> { 113 String pkg = i.getArgument(0); 114 Log.e("Hello", "Getting v " + pkg); 115 return TextUtils.isEmpty(pkg) ? allWidgets : allWidgets.stream() 116 .filter(a -> pkg.equals(a.provider.getPackageName())) 117 .collect(Collectors.toList()); 118 }).when(manager).getInstalledProvidersForPackage(any(), eq(myUserHandle())); 119 120 // 2 widgets, app4/provider1 & app5/provider1, have already been added to the workspace. 121 mModelHelper.initializeData("widgets_predication_update_task_data"); 122 123 MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(mCallback)).get(); 124 MODEL_EXECUTOR.post(() -> mModelHelper.getBgDataModel().widgetsModel.update( 125 LauncherAppState.getInstance(mModelHelper.sandboxContext), 126 /* packageUser= */ null)); 127 128 MODEL_EXECUTOR.submit(() -> { }).get(); 129 MAIN_EXECUTOR.submit(() -> { }).get(); 130 } 131 132 @After tearDown()133 public void tearDown() { 134 mModelHelper.destroy(); 135 } 136 137 @Test widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()138 public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder() 139 throws Exception { 140 // WHEN newPredicationTask is executed with app predication of 5 apps. 141 AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "className", 142 mUserHandle); 143 AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "className", 144 mUserHandle); 145 AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className", 146 mUserHandle); 147 AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "className", 148 mUserHandle); 149 AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "className", 150 mUserHandle); 151 mModelHelper.executeTaskForTest( 152 newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1))) 153 .forEach(Runnable::run); 154 155 // THEN only 3 widgets are returned because 156 // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are 157 // excluded from the result. 158 // 2. app3 doesn't have a widget. 159 // 3. only 1 widget is picked from app1 because we only want to promote one widget per app. 160 List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items 161 .stream() 162 .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) 163 .collect(Collectors.toList()); 164 assertThat(recommendedWidgets).hasSize(3); 165 assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1); 166 assertWidgetInfo(recommendedWidgets.get(1).info, mApp4Provider2); 167 assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1); 168 } 169 170 @Test widgetsRecommendationRan_localFilterDisabled_shouldReturnWidgetsInPredicationOrder()171 public void widgetsRecommendationRan_localFilterDisabled_shouldReturnWidgetsInPredicationOrder() 172 throws Exception { 173 if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) { 174 return; 175 } 176 177 // WHEN newPredicationTask is executed with 5 predicated widgets. 178 AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1", 179 mUserHandle); 180 AppTarget widget2 = new AppTarget(new AppTargetId("app1"), "app1", "provider2", 181 mUserHandle); 182 // Not installed app 183 AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1", 184 mUserHandle); 185 // Not installed widget 186 AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider3", 187 mUserHandle); 188 AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1", 189 mUserHandle); 190 mModelHelper.executeTaskForTest( 191 newWidgetsPredicationTask(List.of(widget5, widget3, widget2, widget4, widget1))) 192 .forEach(Runnable::run); 193 194 // THEN only 3 widgets are returned because the launcher only filters out non-exist widgets. 195 List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items 196 .stream() 197 .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) 198 .collect(Collectors.toList()); 199 assertThat(recommendedWidgets).hasSize(3); 200 assertWidgetInfo(recommendedWidgets.get(0).info, mApp5Provider1); 201 assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider2); 202 assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1); 203 } 204 assertWidgetInfo( LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected)205 private void assertWidgetInfo( 206 LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) { 207 assertThat(actual.provider).isEqualTo(expected.provider); 208 assertThat(actual.getUser()).isEqualTo(expected.getProfile()); 209 } 210 newWidgetsPredicationTask(List<AppTarget> appTargets)211 private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List<AppTarget> appTargets) { 212 return new WidgetsPredictionUpdateTask( 213 new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"), 214 appTargets); 215 } 216 217 private final class FakeBgDataModelCallback implements BgDataModel.Callbacks { 218 219 private FixedContainerItems mRecommendedWidgets = null; 220 221 @Override bindExtraContainerItems(FixedContainerItems item)222 public void bindExtraContainerItems(FixedContainerItems item) { 223 mRecommendedWidgets = item; 224 } 225 } 226 } 227