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