• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.ui.widget;
17 
18 import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
19 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
20 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
21 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
22 import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
23 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
24 import static com.android.launcher3.util.TestUtil.getOnUiThread;
25 import static com.android.launcher3.util.Wait.atMost;
26 import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
27 
28 import static org.junit.Assert.assertEquals;
29 import static org.junit.Assert.assertNotNull;
30 
31 import android.appwidget.AppWidgetManager;
32 import android.content.ComponentName;
33 import android.content.pm.PackageInstaller;
34 import android.content.pm.PackageInstaller.SessionParams;
35 import android.content.pm.PackageManager;
36 import android.database.Cursor;
37 import android.os.Bundle;
38 import android.text.TextUtils;
39 import android.widget.RemoteViews;
40 
41 import androidx.test.ext.junit.runners.AndroidJUnit4;
42 import androidx.test.filters.LargeTest;
43 
44 import com.android.launcher3.Launcher;
45 import com.android.launcher3.LauncherAppState;
46 import com.android.launcher3.LauncherModel;
47 import com.android.launcher3.LauncherSettings;
48 import com.android.launcher3.R;
49 import com.android.launcher3.celllayout.FavoriteItemsTransaction;
50 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
51 import com.android.launcher3.pm.InstallSessionHelper;
52 import com.android.launcher3.ui.TestViewHelpers;
53 import com.android.launcher3.util.BaseLauncherActivityTest;
54 import com.android.launcher3.util.rule.ShellCommandRule;
55 import com.android.launcher3.widget.LauncherAppWidgetHostView;
56 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
57 import com.android.launcher3.widget.PendingAppWidgetHostView;
58 import com.android.launcher3.widget.WidgetManagerHelper;
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.HashSet;
67 import java.util.Set;
68 import java.util.function.Consumer;
69 import java.util.function.Function;
70 
71 /**
72  * Tests for bind widget flow.
73  *
74  * Note running these tests will clear the workspace on the device.
75  */
76 @LargeTest
77 @RunWith(AndroidJUnit4.class)
78 public class BindWidgetTest extends BaseLauncherActivityTest<Launcher> {
79 
80     @Rule
81     public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
82 
83     // Objects created during test, which should be cleaned up in the end.
84     private Cursor mCursor;
85     // App install session id.
86     private int mSessionId = -1;
87 
88     private LauncherModel mModel;
89 
90     @Before
setUp()91     public void setUp() throws Exception {
92         mModel = LauncherAppState.getInstance(targetContext()).getModel();
93     }
94 
95     @After
tearDown()96     public void tearDown() {
97         if (mCursor != null) {
98             mCursor.close();
99         }
100 
101         if (mSessionId > -1) {
102             targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId);
103         }
104     }
105 
106     @Test
testBindNormalWidget_withConfig()107     public void testBindNormalWidget_withConfig() {
108         LauncherAppWidgetProviderInfo info = addWidgetToScreen(true, true, i -> { });
109         verifyWidgetPresent(info);
110     }
111 
112     @Test
testBindNormalWidget_withoutConfig()113     public void testBindNormalWidget_withoutConfig() {
114         LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, true, i -> { });
115         verifyWidgetPresent(info);
116     }
117 
118     @Test
testUnboundWidget_removed()119     public void testUnboundWidget_removed() {
120         LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false,
121                 item -> item.appWidgetId = -33);
122 
123         // Item deleted from db
124         mCursor = queryItem();
125         assertEquals(0, mCursor.getCount());
126 
127         // The view does not exist
128         verifyItemEventuallyNull("Widget exists", widgetProvider(info));
129     }
130 
131     @Test
testPendingWidget_autoRestored()132     public void testPendingWidget_autoRestored() {
133         // A non-restored widget with no config screen gets restored automatically.
134         // Do not bind the widget
135         LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false,
136                 item -> item.restoreStatus = FLAG_ID_NOT_VALID);
137         verifyWidgetPresent(info);
138     }
139 
140     @Test
testPendingWidget_withConfigScreen()141     public void testPendingWidget_withConfigScreen() {
142         // A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
143         // Do not bind the widget
144         LauncherAppWidgetProviderInfo info = addWidgetToScreen(true, false,
145                 item -> item.restoreStatus = FLAG_ID_NOT_VALID);
146         verifyPendingWidgetPresent();
147 
148         mCursor = queryItem();
149         mCursor.moveToNext();
150 
151         // Widget has a valid Id now.
152         assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
153                 & FLAG_ID_NOT_VALID);
154         assertNotNull(AppWidgetManager.getInstance(targetContext())
155                 .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
156                         LauncherSettings.Favorites.APPWIDGET_ID))));
157 
158         // send OPTION_APPWIDGET_RESTORE_COMPLETED
159         int appWidgetId = mCursor.getInt(
160                 mCursor.getColumnIndex(LauncherSettings.Favorites.APPWIDGET_ID));
161         AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(targetContext());
162 
163         Bundle b = new Bundle();
164         b.putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true);
165         RemoteViews remoteViews = new RemoteViews(
166                 targetContext().getPackageName(), R.layout.appwidget_not_ready);
167         appWidgetManager.updateAppWidgetOptions(appWidgetId, b);
168         appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
169 
170         // verify changes are reflected
171         waitForLauncherCondition("App widget options did not update",
172                 l -> appWidgetManager.getAppWidgetOptions(appWidgetId).getBoolean(
173                         WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED));
174         executeOnLauncher(l -> l.getAppWidgetHolder().startListening());
175         verifyWidgetPresent(info);
176         verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider());
177     }
178 
179     @Test
testPendingWidget_notRestored_removed()180     public void testPendingWidget_notRestored_removed() {
181         addPendingItemToScreen(getInvalidWidgetInfo(), FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
182 
183         verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider());
184         // Item deleted from db
185         mCursor = queryItem();
186         assertEquals(0, mCursor.getCount());
187     }
188 
189     @Test
testPendingWidget_notRestored_brokenInstall()190     public void testPendingWidget_notRestored_brokenInstall() {
191         // A widget which is was being installed once, even if its not being
192         // installed at the moment is not removed.
193         addPendingItemToScreen(getInvalidWidgetInfo(),
194                 FLAG_ID_NOT_VALID | FLAG_RESTORE_STARTED | FLAG_PROVIDER_NOT_READY);
195         verifyPendingWidgetPresent();
196 
197         // Verify item still exists in db
198         mCursor = queryItem();
199         assertEquals(1, mCursor.getCount());
200 
201         // Widget still has an invalid id.
202         mCursor.moveToNext();
203         assertEquals(FLAG_ID_NOT_VALID,
204                 mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
205                         & FLAG_ID_NOT_VALID);
206     }
207 
208     @Test
testPendingWidget_notRestored_activeInstall()209     public void testPendingWidget_notRestored_activeInstall() throws Exception {
210         // A widget which is being installed is not removed
211         LauncherAppWidgetInfo item = getInvalidWidgetInfo();
212 
213         // Create an active installer session
214         SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
215         params.setAppPackageName(item.providerName.getPackageName());
216         PackageInstaller installer = targetContext().getPackageManager().getPackageInstaller();
217         mSessionId = installer.createSession(params);
218 
219         addPendingItemToScreen(item, FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
220         verifyPendingWidgetPresent();
221 
222         // Verify item still exists in db
223         mCursor = queryItem();
224         assertEquals(1, mCursor.getCount());
225 
226         // Widget still has an invalid id.
227         mCursor.moveToNext();
228         assertEquals(FLAG_ID_NOT_VALID,
229                 mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
230                         & FLAG_ID_NOT_VALID);
231     }
232 
verifyWidgetPresent(LauncherAppWidgetProviderInfo info)233     private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
234         getOnceNotNull("Widget is not present", widgetProvider(info));
235     }
236 
verifyPendingWidgetPresent()237     private void verifyPendingWidgetPresent() {
238         getOnceNotNull("Widget is not present", pendingWidgetProvider());
239     }
240 
pendingWidgetProvider()241     private Function<Launcher, Object> pendingWidgetProvider() {
242         return l -> l.getWorkspace().getFirstMatch(
243                 (item, view) -> view instanceof PendingAppWidgetHostView);
244     }
245 
widgetProvider(LauncherAppWidgetProviderInfo info)246     private Function<Launcher, Object> widgetProvider(LauncherAppWidgetProviderInfo info) {
247         return l -> l.getWorkspace().getFirstMatch((item, view) ->
248                 view instanceof LauncherAppWidgetHostView
249                         && TextUtils.equals(info.label, view.getContentDescription()));
250     }
251 
verifyItemEventuallyNull(String message, Function<Launcher, Object> provider)252     private void verifyItemEventuallyNull(String message, Function<Launcher, Object> provider) {
253         atMost(message, () -> getFromLauncher(provider) == null);
254     }
255 
addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus)256     private void addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus) {
257         item.restoreStatus = restoreStatus;
258         item.screenId = FIRST_SCREEN_ID;
259         new FavoriteItemsTransaction(targetContext()).addItem(() -> item).commit();
260         loadLauncherSync();
261     }
262 
addWidgetToScreen(boolean hasConfigureScreen, boolean bindWidget, Consumer<LauncherAppWidgetInfo> itemOverride)263     private LauncherAppWidgetProviderInfo addWidgetToScreen(boolean hasConfigureScreen,
264             boolean bindWidget, Consumer<LauncherAppWidgetInfo> itemOverride) {
265         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(hasConfigureScreen);
266         new FavoriteItemsTransaction(targetContext())
267                 .addItem(() -> {
268                     LauncherAppWidgetInfo item =
269                             createWidgetInfo(info, targetContext(), bindWidget);
270                     item.screenId = FIRST_SCREEN_ID;
271                     itemOverride.accept(item);
272                     return item;
273                 }).commit();
274         loadLauncherSync();
275         return info;
276     }
277 
278     /**
279      * Returns a LauncherAppWidgetInfo with package name which is not present on the device
280      */
getInvalidWidgetInfo()281     private LauncherAppWidgetInfo getInvalidWidgetInfo() {
282         String invalidPackage = "com.invalidpackage";
283         int count = 0;
284         String pkg = invalidPackage;
285 
286         Set<String> activePackage = getOnUiThread(() -> {
287             Set<String> packages = new HashSet<>();
288             InstallSessionHelper.INSTANCE.get(targetContext()).getActiveSessions()
289                     .keySet().forEach(packageUserKey -> packages.add(packageUserKey.mPackageName));
290             return packages;
291         });
292         while (true) {
293             try {
294                 targetContext().getPackageManager().getPackageInfo(
295                         pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
296             } catch (Exception e) {
297                 if (!activePackage.contains(pkg)) {
298                     break;
299                 }
300             }
301             pkg = invalidPackage + count;
302             count++;
303         }
304         LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(10,
305                 new ComponentName(pkg, "com.test.widgetprovider"));
306         item.spanX = 2;
307         item.spanY = 2;
308         item.minSpanX = 2;
309         item.minSpanY = 2;
310         item.cellX = 0;
311         item.cellY = 1;
312         item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
313         return item;
314     }
315 
queryItem()316     private Cursor queryItem() {
317         try {
318             return MODEL_EXECUTOR.submit(() ->
319                 mModel.getModelDbController().query(
320                         null, itemIdMatch(0), null, null)).get();
321         } catch (Exception e) {
322             throw new RuntimeException(e);
323         }
324     }
325 }
326