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.content.pm.ApplicationInfo.CATEGORY_PRODUCTIVITY; 19 import static android.content.pm.ApplicationInfo.FLAG_INSTALLED; 20 import static android.os.Process.myUserHandle; 21 22 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; 23 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 25 import static com.android.launcher3.util.TestUtil.runOnExecutorSync; 26 import static com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo; 27 28 import static com.google.common.truth.Truth.assertThat; 29 30 import static org.mockito.ArgumentMatchers.any; 31 import static org.mockito.ArgumentMatchers.anyInt; 32 import static org.mockito.ArgumentMatchers.anyString; 33 import static org.mockito.ArgumentMatchers.eq; 34 import static org.mockito.Mockito.doAnswer; 35 import static org.mockito.Mockito.doReturn; 36 37 import android.app.prediction.AppTarget; 38 import android.app.prediction.AppTargetId; 39 import android.appwidget.AppWidgetManager; 40 import android.appwidget.AppWidgetProviderInfo; 41 import android.content.ComponentName; 42 import android.content.pm.ApplicationInfo; 43 import android.content.pm.LauncherApps; 44 import android.os.UserHandle; 45 import android.platform.test.flag.junit.SetFlagsRule; 46 import android.text.TextUtils; 47 48 import androidx.test.core.content.pm.ApplicationInfoBuilder; 49 import androidx.test.ext.junit.runners.AndroidJUnit4; 50 import androidx.test.filters.SmallTest; 51 52 import com.android.launcher3.Flags; 53 import com.android.launcher3.model.BgDataModel.FixedContainerItems; 54 import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; 55 import com.android.launcher3.util.LauncherLayoutBuilder; 56 import com.android.launcher3.util.LauncherModelHelper; 57 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 58 import com.android.launcher3.widget.PendingAddWidgetInfo; 59 60 import org.junit.After; 61 import org.junit.Before; 62 import org.junit.Rule; 63 import org.junit.Test; 64 import org.junit.runner.RunWith; 65 66 import java.util.Arrays; 67 import java.util.List; 68 import java.util.stream.Collectors; 69 70 @SmallTest 71 @RunWith(AndroidJUnit4.class) 72 public final class WidgetsPredicationUpdateTaskTest { 73 74 @Rule 75 public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); 76 77 private AppWidgetProviderInfo mApp1Provider1; 78 private AppWidgetProviderInfo mApp1Provider2; 79 private AppWidgetProviderInfo mApp2Provider1; 80 private AppWidgetProviderInfo mApp4Provider1; 81 private AppWidgetProviderInfo mApp4Provider2; 82 private AppWidgetProviderInfo mApp5Provider1; 83 private List<AppWidgetProviderInfo> allWidgets; 84 85 private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback(); 86 private LauncherModelHelper mModelHelper; 87 private UserHandle mUserHandle; 88 private LauncherApps mLauncherApps; 89 90 91 @Before setup()92 public void setup() throws Exception { 93 mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS); 94 mModelHelper = new LauncherModelHelper(); 95 96 mUserHandle = myUserHandle(); 97 mApp1Provider1 = createAppWidgetProviderInfo( 98 ComponentName.createRelative("app1", "provider1")); 99 mApp1Provider2 = createAppWidgetProviderInfo( 100 ComponentName.createRelative("app1", "provider2")); 101 mApp2Provider1 = createAppWidgetProviderInfo( 102 ComponentName.createRelative("app2", "provider1")); 103 mApp4Provider1 = createAppWidgetProviderInfo( 104 ComponentName.createRelative("app4", "provider1")); 105 mApp4Provider2 = createAppWidgetProviderInfo( 106 ComponentName.createRelative("app4", ".provider2")); 107 mApp5Provider1 = createAppWidgetProviderInfo( 108 ComponentName.createRelative("app5", "provider1")); 109 allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1, 110 mApp4Provider1, mApp4Provider2, mApp5Provider1); 111 112 mLauncherApps = mModelHelper.sandboxContext.spyService(LauncherApps.class); 113 doAnswer(i -> { 114 String pkg = i.getArgument(0); 115 ApplicationInfo applicationInfo = ApplicationInfoBuilder.newBuilder() 116 .setPackageName(pkg) 117 .setName("App " + pkg) 118 .build(); 119 applicationInfo.category = CATEGORY_PRODUCTIVITY; 120 applicationInfo.flags = FLAG_INSTALLED; 121 return applicationInfo; 122 }).when(mLauncherApps).getApplicationInfo(anyString(), anyInt(), any()); 123 124 AppWidgetManager manager = mModelHelper.sandboxContext.spyService(AppWidgetManager.class); 125 doReturn(allWidgets).when(manager).getInstalledProviders(); 126 doReturn(allWidgets).when(manager).getInstalledProvidersForProfile(eq(myUserHandle())); 127 doAnswer(i -> { 128 String pkg = i.getArgument(0); 129 return TextUtils.isEmpty(pkg) ? allWidgets : allWidgets.stream() 130 .filter(a -> pkg.equals(a.provider.getPackageName())) 131 .collect(Collectors.toList()); 132 }).when(manager).getInstalledProvidersForPackage(any(), eq(myUserHandle())); 133 134 LauncherLayoutBuilder builder = new LauncherLayoutBuilder() 135 .atWorkspace(0, 1, 2).putWidget("app4", "provider1", 1, 1) 136 .atWorkspace(0, 1, 3).putWidget("app5", "provider1", 1, 1); 137 mModelHelper.setupDefaultLayoutProvider(builder); 138 MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(mCallback)).get(); 139 mModelHelper.loadModelSync(); 140 } 141 142 @After tearDown()143 public void tearDown() { 144 mModelHelper.destroy(); 145 } 146 147 @Test widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()148 public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder() { 149 // Run on model executor so that no other task runs in the middle. 150 runOnExecutorSync(MODEL_EXECUTOR, () -> { 151 // WHEN newPredicationTask is executed with app predication of 5 apps. 152 AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1", 153 mUserHandle); 154 AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "provider1", 155 mUserHandle); 156 AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className", 157 mUserHandle); 158 AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1", 159 mUserHandle); 160 AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1", 161 mUserHandle); 162 mCallback.mRecommendedWidgets = null; 163 mModelHelper.getModel().enqueueModelUpdateTask( 164 newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1))); 165 runOnExecutorSync(MAIN_EXECUTOR, () -> { }); 166 167 // THEN only 2 widgets are returned because 168 // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are 169 // excluded from the result. 170 // 2. app3 doesn't have a widget. 171 // 3. only 1 widget is picked from app1 because we only want to promote one widget 172 // per app. 173 List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items 174 .stream() 175 .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) 176 .collect(Collectors.toList()); 177 assertThat(recommendedWidgets).hasSize(2); 178 recommendedWidgets.forEach(pendingAddWidgetInfo -> 179 assertThat(pendingAddWidgetInfo.recommendationCategory).isNotNull() 180 ); 181 assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1); 182 assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1); 183 }); 184 } 185 186 @Test widgetsRecommendationRan_shouldReturnEmptyWidgetsWhenEmpty()187 public void widgetsRecommendationRan_shouldReturnEmptyWidgetsWhenEmpty() { 188 runOnExecutorSync(MODEL_EXECUTOR, () -> { 189 190 // Not installed widget 191 AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider3", 192 mUserHandle); 193 // Not installed app 194 AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1", 195 mUserHandle); 196 // Workspace added widgets 197 AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1", 198 mUserHandle); 199 AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1", 200 mUserHandle); 201 202 mCallback.mRecommendedWidgets = null; 203 mModelHelper.getModel().enqueueModelUpdateTask( 204 newWidgetsPredicationTask(List.of(widget5, widget3, widget4, widget1))); 205 runOnExecutorSync(MAIN_EXECUTOR, () -> { }); 206 207 // Only widgets suggested by prediction system are returned. 208 List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items 209 .stream() 210 .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) 211 .collect(Collectors.toList()); 212 assertThat(recommendedWidgets).hasSize(0); 213 }); 214 } 215 assertWidgetInfo( LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected)216 private void assertWidgetInfo( 217 LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) { 218 assertThat(actual.provider).isEqualTo(expected.provider); 219 assertThat(actual.getUser()).isEqualTo(expected.getProfile()); 220 } 221 newWidgetsPredicationTask(List<AppTarget> appTargets)222 private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List<AppTarget> appTargets) { 223 return new WidgetsPredictionUpdateTask( 224 new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"), 225 appTargets); 226 } 227 228 private final class FakeBgDataModelCallback implements BgDataModel.Callbacks { 229 230 private FixedContainerItems mRecommendedWidgets = null; 231 232 @Override bindExtraContainerItems(FixedContainerItems item)233 public void bindExtraContainerItems(FixedContainerItems item) { 234 mRecommendedWidgets = item; 235 } 236 } 237 } 238