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