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.LauncherModelHelper.TEST_PACKAGE;
19 
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertNull;
24 import static org.junit.Assert.assertTrue;
25 import static org.mockito.Mockito.spy;
26 
27 import android.os.Process;
28 
29 import androidx.test.ext.junit.runners.AndroidJUnit4;
30 import androidx.test.filters.SmallTest;
31 
32 import com.android.launcher3.model.BgDataModel.Callbacks;
33 import com.android.launcher3.model.data.AppInfo;
34 import com.android.launcher3.model.data.ItemInfo;
35 import com.android.launcher3.util.Executors;
36 import com.android.launcher3.util.IntArray;
37 import com.android.launcher3.util.IntSet;
38 import com.android.launcher3.util.LauncherLayoutBuilder;
39 import com.android.launcher3.util.LauncherModelHelper;
40 import com.android.launcher3.util.RunnableList;
41 import com.android.launcher3.util.TestUtil;
42 
43 import org.junit.After;
44 import org.junit.Before;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.List;
51 import java.util.Set;
52 import java.util.stream.Collectors;
53 
54 /**
55  * Tests to verify multiple callbacks in Loader
56  */
57 @SmallTest
58 @RunWith(AndroidJUnit4.class)
59 public class ModelMultiCallbacksTest {
60 
61     private LauncherModelHelper mModelHelper;
62 
63     @Before
setUp()64     public void setUp() {
65         mModelHelper = new LauncherModelHelper();
66     }
67 
68     @After
tearDown()69     public void tearDown() throws Exception {
70         mModelHelper.destroy();
71         TestUtil.uninstallDummyApp();
72     }
73 
74     @Test
testTwoCallbacks_loadedTogether()75     public void testTwoCallbacks_loadedTogether() throws Exception {
76         setupWorkspacePages(3);
77 
78         MyCallbacks cb1 = spy(MyCallbacks.class);
79         Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb1));
80 
81         waitForLoaderAndTempMainThread();
82         cb1.verifySynchronouslyBound(3);
83 
84         // Add a new callback
85         cb1.reset();
86         MyCallbacks cb2 = spy(MyCallbacks.class);
87         cb2.mPageToBindSync = IntSet.wrap(2);
88         Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb2));
89 
90         waitForLoaderAndTempMainThread();
91         assertFalse(cb1.bindStarted);
92         cb2.verifySynchronouslyBound(3);
93 
94         // Remove callbacks
95         cb1.reset();
96         cb2.reset();
97 
98         // No effect on callbacks when removing an callback
99         Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().removeCallbacks(cb2));
100         waitForLoaderAndTempMainThread();
101         assertNull(cb1.mPendingTasks);
102         assertNull(cb2.mPendingTasks);
103 
104         // Reloading only loads registered callbacks
105         mModelHelper.getModel().startLoader();
106         waitForLoaderAndTempMainThread();
107         cb1.verifySynchronouslyBound(3);
108         assertNull(cb2.mPendingTasks);
109     }
110 
111     @Test
testTwoCallbacks_receiveUpdates()112     public void testTwoCallbacks_receiveUpdates() throws Exception {
113         TestUtil.uninstallDummyApp();
114 
115         setupWorkspacePages(1);
116 
117         MyCallbacks cb1 = spy(MyCallbacks.class);
118         MyCallbacks cb2 = spy(MyCallbacks.class);
119         Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb1));
120         Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().addCallbacksAndLoad(cb2));
121         waitForLoaderAndTempMainThread();
122 
123         assertTrue(cb1.allApps().contains(TEST_PACKAGE));
124         assertTrue(cb2.allApps().contains(TEST_PACKAGE));
125 
126         // Install package 1
127         TestUtil.installDummyApp();
128         mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
129         waitForLoaderAndTempMainThread();
130         assertTrue(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
131         assertTrue(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
132 
133         // Uninstall package 2
134         TestUtil.uninstallDummyApp();
135         mModelHelper.getModel().onPackageRemoved(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
136         waitForLoaderAndTempMainThread();
137         assertFalse(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
138         assertFalse(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
139 
140         // Unregister a callback and verify updates no longer received
141         Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().removeCallbacks(cb2));
142         TestUtil.installDummyApp();
143         mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
144         waitForLoaderAndTempMainThread();
145 
146         // cb2 didn't get the update
147         assertTrue(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
148         assertFalse(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
149     }
150 
waitForLoaderAndTempMainThread()151     private void waitForLoaderAndTempMainThread() throws Exception {
152         Executors.MAIN_EXECUTOR.submit(() -> { }).get();
153         Executors.MODEL_EXECUTOR.submit(() -> { }).get();
154         Executors.MAIN_EXECUTOR.submit(() -> { }).get();
155     }
156 
setupWorkspacePages(int pageCount)157     private void setupWorkspacePages(int pageCount) throws Exception {
158         // Create a layout with 3 pages
159         LauncherLayoutBuilder builder = new LauncherLayoutBuilder();
160         for (int i = 0; i < pageCount; i++) {
161             builder.atWorkspace(1, 1, i).putApp(TEST_PACKAGE, TEST_PACKAGE);
162         }
163         mModelHelper.setupDefaultLayoutProvider(builder);
164     }
165 
166     private abstract static class MyCallbacks implements Callbacks {
167 
168         final List<ItemInfo> mItems = new ArrayList<>();
169         IntSet mPageToBindSync = IntSet.wrap(0);
170         IntSet mPageBoundSync = new IntSet();
171         RunnableList mPendingTasks;
172         AppInfo[] mAppInfos;
173         boolean bindStarted;
174 
MyCallbacks()175         MyCallbacks() { }
176 
177         @Override
startBinding()178         public void startBinding() {
179             bindStarted = true;
180         }
181 
182         @Override
onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks)183         public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
184             mPageBoundSync = boundPages;
185             mPendingTasks = pendingTasks;
186         }
187 
188         @Override
bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons)189         public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
190             mItems.addAll(shortcuts);
191         }
192 
193         @Override
bindAllApplications(AppInfo[] apps, int flags)194         public void bindAllApplications(AppInfo[] apps, int flags) {
195             mAppInfos = apps;
196         }
197 
198         @Override
getPagesToBindSynchronously(IntArray orderedScreenIds)199         public IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) {
200             return mPageToBindSync;
201         }
202 
reset()203         public void reset() {
204             mItems.clear();
205             mPageBoundSync = new IntSet();
206             mPendingTasks = null;
207             mAppInfos = null;
208             bindStarted = false;
209         }
210 
verifySynchronouslyBound(int totalItems)211         public void verifySynchronouslyBound(int totalItems) {
212             // Verify that the requested page is bound synchronously
213             assertTrue(bindStarted);
214             assertEquals(mPageToBindSync, mPageBoundSync);
215             assertEquals(mItems.size(), 1);
216             assertEquals(IntSet.wrap(mItems.get(0).screenId), mPageBoundSync);
217             assertNotNull(mPendingTasks);
218 
219             // Verify that all other pages are bound properly
220             mPendingTasks.executeAllAndDestroy();
221             assertEquals(mItems.size(), totalItems);
222         }
223 
allApps()224         public Set<String> allApps() {
225             return Arrays.stream(mAppInfos)
226                     .map(ai -> ai.getTargetComponent().getPackageName())
227                     .collect(Collectors.toSet());
228         }
229 
verifyApps(String... apps)230         public void verifyApps(String... apps) {
231             assertTrue(allApps().containsAll(Arrays.asList(apps)));
232         }
233     }
234 }
235