1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package com.android.launcher3.ui.widget;
17
18import android.appwidget.AppWidgetHost;
19import android.appwidget.AppWidgetManager;
20import android.content.ComponentName;
21import android.content.ContentResolver;
22import android.content.ContentValues;
23import android.content.pm.PackageInstaller;
24import android.content.pm.PackageInstaller.SessionParams;
25import android.content.pm.PackageManager;
26import android.database.Cursor;
27import android.os.Bundle;
28import android.support.test.filters.LargeTest;
29import android.support.test.runner.AndroidJUnit4;
30import android.support.test.uiautomator.UiSelector;
31
32import com.android.launcher3.LauncherAppWidgetHost;
33import com.android.launcher3.widget.LauncherAppWidgetHostView;
34import com.android.launcher3.LauncherAppWidgetInfo;
35import com.android.launcher3.LauncherAppWidgetProviderInfo;
36import com.android.launcher3.LauncherModel;
37import com.android.launcher3.LauncherSettings;
38import com.android.launcher3.widget.PendingAppWidgetHostView;
39import com.android.launcher3.Workspace;
40import com.android.launcher3.compat.AppWidgetManagerCompat;
41import com.android.launcher3.compat.PackageInstallerCompat;
42import com.android.launcher3.ui.AbstractLauncherUiTest;
43import com.android.launcher3.util.ContentWriter;
44import com.android.launcher3.util.LooperExecutor;
45import com.android.launcher3.util.rule.LauncherActivityRule;
46import com.android.launcher3.util.rule.ShellCommandRule;
47import com.android.launcher3.widget.PendingAddWidgetInfo;
48import com.android.launcher3.widget.WidgetHostViewLoader;
49
50import org.junit.After;
51import org.junit.Before;
52import org.junit.Rule;
53import org.junit.Test;
54import org.junit.runner.RunWith;
55
56import java.util.Set;
57import java.util.concurrent.Callable;
58import java.util.concurrent.TimeUnit;
59
60import static org.junit.Assert.assertEquals;
61import static org.junit.Assert.assertFalse;
62import static org.junit.Assert.assertNotNull;
63import static org.junit.Assert.assertTrue;
64
65/**
66 * Tests for bind widget flow.
67 *
68 * Note running these tests will clear the workspace on the device.
69 */
70@LargeTest
71@RunWith(AndroidJUnit4.class)
72public class BindWidgetTest extends AbstractLauncherUiTest {
73
74    @Rule public LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
75    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind();
76
77    private ContentResolver mResolver;
78    private AppWidgetManagerCompat mWidgetManager;
79
80    // Objects created during test, which should be cleaned up in the end.
81    private Cursor mCursor;
82    // App install session id.
83    private int mSessionId = -1;
84
85    @Override
86    @Before
87    public void setUp() throws Exception {
88        super.setUp();
89
90        mResolver = mTargetContext.getContentResolver();
91        mWidgetManager = AppWidgetManagerCompat.getInstance(mTargetContext);
92
93        // Clear all existing data
94        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
95        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
96    }
97
98    @After
99    public void tearDown() throws Exception {
100        if (mCursor != null) {
101            mCursor.close();
102        }
103
104        if (mSessionId > -1) {
105            mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
106        }
107    }
108
109    @Test
110    public void testBindNormalWidget_withConfig() {
111        LauncherAppWidgetProviderInfo info = findWidgetProvider(true);
112        LauncherAppWidgetInfo item = createWidgetInfo(info, true);
113
114        setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
115    }
116
117    @Test
118    public void testBindNormalWidget_withoutConfig() {
119        LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
120        LauncherAppWidgetInfo item = createWidgetInfo(info, true);
121
122        setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
123    }
124
125    @Test
126    public void testUnboundWidget_removed() throws Exception {
127        LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
128        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
129        item.appWidgetId = -33;
130
131        // Since there is no widget to verify, just wait until the workspace is ready.
132        setupAndVerifyContents(item, Workspace.class, null);
133
134        waitUntilLoaderIdle();
135        // Item deleted from db
136        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
137                null, null, null, null, null);
138        assertEquals(0, mCursor.getCount());
139
140        // The view does not exist
141        assertFalse(mDevice.findObject(new UiSelector().description(info.label)).exists());
142    }
143
144    @Test
145    public void testPendingWidget_autoRestored() {
146        // A non-restored widget with no config screen gets restored automatically.
147        LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
148
149        // Do not bind the widget
150        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
151        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
152
153        setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
154    }
155
156    @Test
157    public void testPendingWidget_withConfigScreen() throws Exception {
158        // A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
159        LauncherAppWidgetProviderInfo info = findWidgetProvider(true);
160
161        // Do not bind the widget
162        LauncherAppWidgetInfo item = createWidgetInfo(info, false);
163        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
164
165        setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
166        waitUntilLoaderIdle();
167        // Item deleted from db
168        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
169                null, null, null, null, null);
170        mCursor.moveToNext();
171
172        // Widget has a valid Id now.
173        assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
174                & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
175        assertNotNull(AppWidgetManager.getInstance(mTargetContext)
176                .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
177                        LauncherSettings.Favorites.APPWIDGET_ID))));
178    }
179
180    @Test
181    public void testPendingWidget_notRestored_removed() throws Exception {
182        LauncherAppWidgetInfo item = getInvalidWidgetInfo();
183        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
184                | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
185
186        setupAndVerifyContents(item, Workspace.class, null);
187        // The view does not exist
188        assertFalse(mDevice.findObject(
189                new UiSelector().className(PendingAppWidgetHostView.class)).exists());
190        waitUntilLoaderIdle();
191        // Item deleted from db
192        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
193                null, null, null, null, null);
194        assertEquals(0, mCursor.getCount());
195    }
196
197    @Test
198    public void testPendingWidget_notRestored_brokenInstall() throws Exception {
199        // A widget which is was being installed once, even if its not being
200        // installed at the moment is not removed.
201        LauncherAppWidgetInfo item = getInvalidWidgetInfo();
202        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
203                | LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
204                | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
205
206        setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
207        // Verify item still exists in db
208        waitUntilLoaderIdle();
209        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
210                null, null, null, null, null);
211        assertEquals(1, mCursor.getCount());
212
213        // Widget still has an invalid id.
214        mCursor.moveToNext();
215        assertEquals(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID,
216                mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
217                        & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
218    }
219
220    @Test
221    public void testPendingWidget_notRestored_activeInstall() throws Exception {
222        // A widget which is being installed is not removed
223        LauncherAppWidgetInfo item = getInvalidWidgetInfo();
224        item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
225                | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
226
227        // Create an active installer session
228        SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
229        params.setAppPackageName(item.providerName.getPackageName());
230        PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
231        mSessionId = installer.createSession(params);
232
233        setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
234        // Verify item still exists in db
235        waitUntilLoaderIdle();
236        mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
237                null, null, null, null, null);
238        assertEquals(1, mCursor.getCount());
239
240        // Widget still has an invalid id.
241        mCursor.moveToNext();
242        assertEquals(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID,
243                mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
244                        & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
245    }
246
247    /**
248     * Adds {@param item} on the homescreen on the 0th screen at 0,0, and verifies that the
249     * widget class is displayed on the homescreen.
250     * @param widgetClass the View class which is displayed on the homescreen
251     * @param desc the content description of the view or null.
252     */
253    private void setupAndVerifyContents(
254            LauncherAppWidgetInfo item, Class<?> widgetClass, String desc) {
255        long screenId = Workspace.FIRST_SCREEN_ID;
256        // Update the screen id counter for the provider.
257        LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
258
259        if (screenId > Workspace.FIRST_SCREEN_ID) {
260            screenId = Workspace.FIRST_SCREEN_ID;
261        }
262        ContentValues v = new ContentValues();
263        v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
264        v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, 0);
265        mResolver.insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v);
266
267        // Insert the item
268        ContentWriter writer = new ContentWriter(mTargetContext);
269        item.id = LauncherSettings.Settings.call(
270                mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
271                .getLong(LauncherSettings.Settings.EXTRA_VALUE);
272        item.screenId = screenId;
273        item.onAddToDatabase(writer);
274        writer.put(LauncherSettings.Favorites._ID, item.id);
275        mResolver.insert(LauncherSettings.Favorites.CONTENT_URI, writer.getValues(mTargetContext));
276        resetLoaderState();
277
278        // Launch the home activity
279        mActivityMonitor.startLauncher();
280        // Verify UI
281        UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName())
282                .className(widgetClass);
283        if (desc != null) {
284            selector = selector.description(desc);
285        }
286        assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT));
287    }
288
289    /**
290     * Creates a LauncherAppWidgetInfo corresponding to {@param info}
291     * @param bindWidget if true the info is bound and a valid widgetId is assigned to
292     *                   the LauncherAppWidgetInfo
293     */
294    private LauncherAppWidgetInfo createWidgetInfo(
295            LauncherAppWidgetProviderInfo info, boolean bindWidget) {
296        LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(
297                LauncherAppWidgetInfo.NO_ID, info.provider);
298        item.spanX = info.minSpanX;
299        item.spanY = info.minSpanY;
300        item.minSpanX = info.minSpanX;
301        item.minSpanY = info.minSpanY;
302        item.user = info.getProfile();
303        item.cellX = 0;
304        item.cellY = 1;
305        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
306
307        if (bindWidget) {
308            PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info);
309            pendingInfo.spanX = item.spanX;
310            pendingInfo.spanY = item.spanY;
311            pendingInfo.minSpanX = item.minSpanX;
312            pendingInfo.minSpanY = item.minSpanY;
313            Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(mTargetContext, pendingInfo);
314
315            AppWidgetHost host = new LauncherAppWidgetHost(mTargetContext);
316            int widgetId = host.allocateAppWidgetId();
317            if (!mWidgetManager.bindAppWidgetIdIfAllowed(widgetId, info, options)) {
318                host.deleteAppWidgetId(widgetId);
319                throw new IllegalArgumentException("Unable to bind widget id");
320            }
321            item.appWidgetId = widgetId;
322        }
323        return item;
324    }
325
326    /**
327     * Returns a LauncherAppWidgetInfo with package name which is not present on the device
328     */
329    private LauncherAppWidgetInfo getInvalidWidgetInfo() {
330        String invalidPackage = "com.invalidpackage";
331        int count = 0;
332        String pkg = invalidPackage;
333
334        Set<String> activePackage = getOnUiThread(new Callable<Set<String>>() {
335            @Override
336            public Set<String> call() throws Exception {
337                return PackageInstallerCompat.getInstance(mTargetContext)
338                        .updateAndGetActiveSessionCache().keySet();
339            }
340        });
341        while(true) {
342            try {
343                mTargetContext.getPackageManager().getPackageInfo(
344                        pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
345            } catch (Exception e) {
346                if (!activePackage.contains(pkg)) {
347                    break;
348                }
349            }
350            pkg = invalidPackage + count;
351            count ++;
352        }
353        LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(10,
354                new ComponentName(pkg, "com.test.widgetprovider"));
355        item.spanX = 2;
356        item.spanY = 2;
357        item.minSpanX = 2;
358        item.minSpanY = 2;
359        item.cellX = 0;
360        item.cellY = 1;
361        item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
362        return item;
363    }
364
365    /**
366     * Blocks the current thread until all the jobs in the main worker thread are complete.
367     */
368    private void waitUntilLoaderIdle() throws Exception {
369        new LooperExecutor(LauncherModel.getWorkerLooper())
370                .submit(new Runnable() {
371                    @Override
372                    public void run() { }
373                }).get(DEFAULT_WORKER_TIMEOUT_SECS, TimeUnit.SECONDS);
374    }
375}
376