1 /*
2  * Copyright (C) 2020 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 com.android.launcher3.util.Executors.createAndStartNewForegroundLooper;
19 import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertNull;
24 import static org.mockito.Mockito.spy;
25 import static org.robolectric.Shadows.shadowOf;
26 
27 import android.os.Process;
28 
29 import com.android.launcher3.PagedView;
30 import com.android.launcher3.model.BgDataModel.Callbacks;
31 import com.android.launcher3.model.data.AppInfo;
32 import com.android.launcher3.model.data.ItemInfo;
33 import com.android.launcher3.util.Executors;
34 import com.android.launcher3.util.LauncherLayoutBuilder;
35 import com.android.launcher3.util.LauncherModelHelper;
36 import com.android.launcher3.util.LooperExecutor;
37 import com.android.launcher3.util.ViewOnDrawExecutor;
38 
39 import org.junit.Before;
40 import org.junit.Test;
41 import org.junit.runner.RunWith;
42 import org.robolectric.RobolectricTestRunner;
43 import org.robolectric.RuntimeEnvironment;
44 import org.robolectric.annotation.LooperMode;
45 import org.robolectric.annotation.LooperMode.Mode;
46 import org.robolectric.shadows.ShadowPackageManager;
47 import org.robolectric.util.ReflectionHelpers;
48 
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.HashSet;
52 import java.util.List;
53 import java.util.stream.Collectors;
54 
55 /**
56  * Tests to verify multiple callbacks in Loader
57  */
58 @RunWith(RobolectricTestRunner.class)
59 @LooperMode(Mode.PAUSED)
60 public class ModelMultiCallbacksTest {
61 
62     private LauncherModelHelper mModelHelper;
63 
64     private ShadowPackageManager mSpm;
65     private LooperExecutor mTempMainExecutor;
66 
67     @Before
setUp()68     public void setUp() throws Exception {
69         mModelHelper = new LauncherModelHelper();
70         mModelHelper.installApp(TEST_PACKAGE);
71 
72         mSpm = shadowOf(RuntimeEnvironment.application.getPackageManager());
73 
74         // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
75         // so that we can wait appropriately for the loader to complete.
76         mTempMainExecutor = new LooperExecutor(createAndStartNewForegroundLooper("tempMain"));
77         ReflectionHelpers.setField(mModelHelper.getModel(), "mMainExecutor", mTempMainExecutor);
78     }
79 
80     @Test
testTwoCallbacks_loadedTogether()81     public void testTwoCallbacks_loadedTogether() throws Exception {
82         setupWorkspacePages(3);
83 
84         MyCallbacks cb1 = spy(MyCallbacks.class);
85         mModelHelper.getModel().addCallbacksAndLoad(cb1);
86 
87         waitForLoaderAndTempMainThread();
88         cb1.verifySynchronouslyBound(3);
89 
90         // Add a new callback
91         cb1.reset();
92         MyCallbacks cb2 = spy(MyCallbacks.class);
93         cb2.mPageToBindSync = 2;
94         mModelHelper.getModel().addCallbacksAndLoad(cb2);
95 
96         waitForLoaderAndTempMainThread();
97         cb1.verifySynchronouslyBound(3);
98         cb2.verifySynchronouslyBound(3);
99 
100         // Remove callbacks
101         cb1.reset();
102         cb2.reset();
103 
104         // No effect on callbacks when removing an callback
105         mModelHelper.getModel().removeCallbacks(cb2);
106         waitForLoaderAndTempMainThread();
107         assertNull(cb1.mDeferredExecutor);
108         assertNull(cb2.mDeferredExecutor);
109 
110         // Reloading only loads registered callbacks
111         mModelHelper.getModel().startLoader();
112         waitForLoaderAndTempMainThread();
113         cb1.verifySynchronouslyBound(3);
114         assertNull(cb2.mDeferredExecutor);
115     }
116 
117     @Test
testTwoCallbacks_receiveUpdates()118     public void testTwoCallbacks_receiveUpdates() throws Exception {
119         setupWorkspacePages(1);
120 
121         MyCallbacks cb1 = spy(MyCallbacks.class);
122         MyCallbacks cb2 = spy(MyCallbacks.class);
123         mModelHelper.getModel().addCallbacksAndLoad(cb1);
124         mModelHelper.getModel().addCallbacksAndLoad(cb2);
125         waitForLoaderAndTempMainThread();
126 
127         cb1.verifyApps(TEST_PACKAGE);
128         cb2.verifyApps(TEST_PACKAGE);
129 
130         // Install package 1
131         String pkg1 = "com.test.pkg1";
132         mModelHelper.installApp(pkg1);
133         mModelHelper.getModel().onPackageAdded(pkg1, Process.myUserHandle());
134         waitForLoaderAndTempMainThread();
135         cb1.verifyApps(TEST_PACKAGE, pkg1);
136         cb2.verifyApps(TEST_PACKAGE, pkg1);
137 
138         // Install package 2
139         String pkg2 = "com.test.pkg2";
140         mModelHelper.installApp(pkg2);
141         mModelHelper.getModel().onPackageAdded(pkg2, Process.myUserHandle());
142         waitForLoaderAndTempMainThread();
143         cb1.verifyApps(TEST_PACKAGE, pkg1, pkg2);
144         cb2.verifyApps(TEST_PACKAGE, pkg1, pkg2);
145 
146         // Uninstall package 2
147         mSpm.removePackage(pkg1);
148         mModelHelper.getModel().onPackageRemoved(pkg1, Process.myUserHandle());
149         waitForLoaderAndTempMainThread();
150         cb1.verifyApps(TEST_PACKAGE, pkg2);
151         cb2.verifyApps(TEST_PACKAGE, pkg2);
152 
153         // Unregister a callback and verify updates no longer received
154         mModelHelper.getModel().removeCallbacks(cb2);
155         mSpm.removePackage(pkg2);
156         mModelHelper.getModel().onPackageRemoved(pkg2, Process.myUserHandle());
157         waitForLoaderAndTempMainThread();
158         cb1.verifyApps(TEST_PACKAGE);
159         cb2.verifyApps(TEST_PACKAGE, pkg2);
160     }
161 
waitForLoaderAndTempMainThread()162     private void waitForLoaderAndTempMainThread() throws Exception {
163         Executors.MODEL_EXECUTOR.submit(() -> { }).get();
164         mTempMainExecutor.submit(() -> { }).get();
165     }
166 
setupWorkspacePages(int pageCount)167     private void setupWorkspacePages(int pageCount) throws Exception {
168         // Create a layout with 3 pages
169         LauncherLayoutBuilder builder = new LauncherLayoutBuilder();
170         for (int i = 0; i < pageCount; i++) {
171             builder.atWorkspace(1, 1, i).putApp(TEST_PACKAGE, TEST_PACKAGE);
172         }
173         mModelHelper.setupDefaultLayoutProvider(builder);
174     }
175 
176     private abstract static class MyCallbacks implements Callbacks {
177 
178         final List<ItemInfo> mItems = new ArrayList<>();
179         int mPageToBindSync = 0;
180         int mPageBoundSync = PagedView.INVALID_PAGE;
181         ViewOnDrawExecutor mDeferredExecutor;
182         AppInfo[] mAppInfos;
183 
MyCallbacks()184         MyCallbacks() { }
185 
186         @Override
onPageBoundSynchronously(int page)187         public void onPageBoundSynchronously(int page) {
188             mPageBoundSync = page;
189         }
190 
191         @Override
executeOnNextDraw(ViewOnDrawExecutor executor)192         public void executeOnNextDraw(ViewOnDrawExecutor executor) {
193             mDeferredExecutor = executor;
194         }
195 
196         @Override
bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons)197         public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
198             mItems.addAll(shortcuts);
199         }
200 
201         @Override
bindAllApplications(AppInfo[] apps, int flags)202         public void bindAllApplications(AppInfo[] apps, int flags) {
203             mAppInfos = apps;
204         }
205 
206         @Override
getPageToBindSynchronously()207         public int getPageToBindSynchronously() {
208             return mPageToBindSync;
209         }
210 
reset()211         public void reset() {
212             mItems.clear();
213             mPageBoundSync = PagedView.INVALID_PAGE;
214             mDeferredExecutor = null;
215             mAppInfos = null;
216         }
217 
verifySynchronouslyBound(int totalItems)218         public void verifySynchronouslyBound(int totalItems) {
219             // Verify that the requested page is bound synchronously
220             assertEquals(mPageBoundSync, mPageToBindSync);
221             assertEquals(mItems.size(), 1);
222             assertEquals(mItems.get(0).screenId, mPageBoundSync);
223             assertNotNull(mDeferredExecutor);
224 
225             // Verify that all other pages are bound properly
226             mDeferredExecutor.runAllTasks();
227             assertEquals(mItems.size(), totalItems);
228         }
229 
verifyApps(String... apps)230         public void verifyApps(String... apps) {
231             assertEquals(apps.length, mAppInfos.length);
232             assertEquals(Arrays.stream(mAppInfos)
233                     .map(ai -> ai.getTargetComponent().getPackageName())
234                     .collect(Collectors.toSet()),
235                     new HashSet<>(Arrays.asList(apps)));
236         }
237     }
238 }
239