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