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 
17 package android.service.controls;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertTrue;
22 import static org.mockito.ArgumentMatchers.any;
23 import static org.mockito.ArgumentMatchers.eq;
24 import static org.mockito.Mockito.times;
25 import static org.mockito.Mockito.verify;
26 import static org.mockito.Mockito.when;
27 
28 import android.Manifest;
29 import android.app.PendingIntent;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.IIntentSender;
33 import android.content.Intent;
34 import android.content.res.Resources;
35 import android.graphics.Bitmap;
36 import android.graphics.drawable.Icon;
37 import android.os.Binder;
38 import android.os.Bundle;
39 import android.os.IBinder;
40 import android.os.RemoteException;
41 import android.service.controls.actions.CommandAction;
42 import android.service.controls.actions.ControlAction;
43 import android.service.controls.actions.ControlActionWrapper;
44 import android.service.controls.templates.ThumbnailTemplate;
45 
46 import androidx.test.filters.SmallTest;
47 import androidx.test.platform.app.InstrumentationRegistry;
48 import androidx.test.runner.AndroidJUnit4;
49 
50 import com.android.internal.R;
51 
52 import org.junit.Before;
53 import org.junit.Test;
54 import org.junit.runner.RunWith;
55 import org.mockito.ArgumentCaptor;
56 import org.mockito.Captor;
57 import org.mockito.Mock;
58 import org.mockito.MockitoAnnotations;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 import java.util.Objects;
63 import java.util.concurrent.Flow.Publisher;
64 import java.util.concurrent.Flow.Subscriber;
65 import java.util.concurrent.Flow.Subscription;
66 import java.util.function.Consumer;
67 
68 @SmallTest
69 @RunWith(AndroidJUnit4.class)
70 public class ControlProviderServiceTest {
71 
72     private static final String TEST_CONTROLS_PACKAGE = "sysui";
73     private static final ComponentName TEST_COMPONENT =
74             ComponentName.unflattenFromString("test.pkg/.test.cls");
75 
76     private IBinder mToken = new Binder();
77     @Mock
78     private IControlsActionCallback.Stub mActionCallback;
79     @Mock
80     private IControlsSubscriber.Stub mSubscriber;
81     @Mock
82     private IIntentSender mIIntentSender;
83     @Mock
84     private Resources mResources;
85     @Mock
86     private Context mContext;
87     @Captor
88     private ArgumentCaptor<Intent> mIntentArgumentCaptor;
89 
90     private PendingIntent mPendingIntent;
91     private FakeControlsProviderService mControlsProviderService;
92 
93     private IControlsProvider mControlsProvider;
94 
95     @Before
setUp()96     public void setUp() {
97         MockitoAnnotations.initMocks(this);
98 
99         when(mActionCallback.asBinder()).thenCallRealMethod();
100         when(mActionCallback.queryLocalInterface(any())).thenReturn(mActionCallback);
101         when(mSubscriber.asBinder()).thenCallRealMethod();
102         when(mSubscriber.queryLocalInterface(any())).thenReturn(mSubscriber);
103 
104         when(mResources.getString(com.android.internal.R.string.config_controlsPackage))
105                 .thenReturn(TEST_CONTROLS_PACKAGE);
106         when(mContext.getResources()).thenReturn(mResources);
107 
108         Bundle b = new Bundle();
109         b.putBinder(ControlsProviderService.CALLBACK_TOKEN, mToken);
110         Intent intent = new Intent();
111         intent.putExtra(ControlsProviderService.CALLBACK_BUNDLE, b);
112 
113         mPendingIntent = new PendingIntent(mIIntentSender);
114 
115         mControlsProviderService = new FakeControlsProviderService(
116                 InstrumentationRegistry.getInstrumentation().getContext());
117         mControlsProvider = IControlsProvider.Stub.asInterface(
118                 mControlsProviderService.onBind(intent));
119     }
120 
121     @Test
testOnLoad_allStateless()122     public void testOnLoad_allStateless() throws RemoteException {
123         Control control1 = new Control.StatelessBuilder("TEST_ID", mPendingIntent).build();
124         Control control2 = new Control.StatelessBuilder("TEST_ID_2", mPendingIntent)
125                 .setDeviceType(DeviceTypes.TYPE_AIR_FRESHENER).build();
126 
127         ArgumentCaptor<IControlsSubscription.Stub> subscriptionCaptor =
128                 ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
129         ArgumentCaptor<Control> controlCaptor =
130                 ArgumentCaptor.forClass(Control.class);
131 
132         ArrayList<Control> list = new ArrayList<>();
133         list.add(control1);
134         list.add(control2);
135 
136         mControlsProviderService.setControls(list);
137         mControlsProvider.load(mSubscriber);
138         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
139 
140         verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
141         subscriptionCaptor.getValue().request(1000);
142 
143         verify(mSubscriber, times(2))
144                 .onNext(eq(mToken), controlCaptor.capture());
145         List<Control> values = controlCaptor.getAllValues();
146         assertTrue(equals(values.get(0), list.get(0)));
147         assertTrue(equals(values.get(1), list.get(1)));
148 
149         verify(mSubscriber).onComplete(eq(mToken));
150     }
151 
152     @Test
testOnLoad_statefulConvertedToStateless()153     public void testOnLoad_statefulConvertedToStateless() throws RemoteException {
154         Control control = new Control.StatefulBuilder("TEST_ID", mPendingIntent)
155                 .setTitle("TEST_TITLE")
156                 .setStatus(Control.STATUS_OK)
157                 .build();
158         Control statelessControl = new Control.StatelessBuilder(control).build();
159 
160         ArgumentCaptor<IControlsSubscription.Stub> subscriptionCaptor =
161                 ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
162         ArgumentCaptor<Control> controlCaptor =
163                 ArgumentCaptor.forClass(Control.class);
164 
165         ArrayList<Control> list = new ArrayList<>();
166         list.add(control);
167 
168         mControlsProviderService.setControls(list);
169         mControlsProvider.load(mSubscriber);
170         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
171 
172         verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
173         subscriptionCaptor.getValue().request(1000);
174 
175         verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
176         Control c = controlCaptor.getValue();
177         assertFalse(equals(control, c));
178         assertTrue(equals(statelessControl, c));
179         assertEquals(Control.STATUS_UNKNOWN, c.getStatus());
180 
181         verify(mSubscriber).onComplete(eq(mToken));
182     }
183 
184     @Test
testOnLoadSuggested_allStateless()185     public void testOnLoadSuggested_allStateless() throws RemoteException {
186         Control control1 = new Control.StatelessBuilder("TEST_ID", mPendingIntent).build();
187         Control control2 = new Control.StatelessBuilder("TEST_ID_2", mPendingIntent)
188                 .setDeviceType(DeviceTypes.TYPE_AIR_FRESHENER).build();
189 
190         ArgumentCaptor<IControlsSubscription.Stub> subscriptionCaptor =
191                 ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
192         ArgumentCaptor<Control> controlCaptor =
193                 ArgumentCaptor.forClass(Control.class);
194 
195         ArrayList<Control> list = new ArrayList<>();
196         list.add(control1);
197         list.add(control2);
198 
199         mControlsProviderService.setControls(list);
200         mControlsProvider.loadSuggested(mSubscriber);
201         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
202 
203         verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
204         subscriptionCaptor.getValue().request(1);
205 
206         verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
207         Control c = controlCaptor.getValue();
208         assertTrue(equals(c, list.get(0)));
209 
210         verify(mSubscriber).onComplete(eq(mToken));
211     }
212 
213     @Test
testSubscribe()214     public void testSubscribe() throws RemoteException {
215         Control control = new Control.StatefulBuilder("TEST_ID", mPendingIntent)
216                 .setTitle("TEST_TITLE")
217                 .setStatus(Control.STATUS_OK)
218                 .build();
219 
220         Control c = sendControlGetControl(control);
221         assertTrue(equals(c, control));
222     }
223 
224     @Test
testThumbnailRescaled_bigger()225     public void testThumbnailRescaled_bigger() throws RemoteException {
226         Context context = mControlsProviderService.getBaseContext();
227         int maxWidth = context.getResources().getDimensionPixelSize(
228                 R.dimen.controls_thumbnail_image_max_width);
229         int maxHeight = context.getResources().getDimensionPixelSize(
230                 R.dimen.controls_thumbnail_image_max_height);
231 
232         int min = Math.min(maxWidth, maxHeight);
233         int max = Math.max(maxWidth, maxHeight);
234 
235         Bitmap bitmap = Bitmap.createBitmap(max * 2, max * 2, Bitmap.Config.ALPHA_8);
236         Icon icon = Icon.createWithBitmap(bitmap);
237         ThumbnailTemplate template = new ThumbnailTemplate("ID", false, icon, "");
238 
239         Control control = new Control.StatefulBuilder("TEST_ID", mPendingIntent)
240                 .setTitle("TEST_TITLE")
241                 .setStatus(Control.STATUS_OK)
242                 .setControlTemplate(template)
243                 .build();
244 
245         Control c = sendControlGetControl(control);
246 
247         ThumbnailTemplate sentTemplate = (ThumbnailTemplate) c.getControlTemplate();
248         Bitmap sentBitmap = sentTemplate.getThumbnail().getBitmap();
249 
250         // Aspect ratio is kept
251         assertEquals(sentBitmap.getWidth(), sentBitmap.getHeight());
252 
253         assertEquals(min, sentBitmap.getWidth());
254     }
255 
256     @Test
testThumbnailRescaled_smaller()257     public void testThumbnailRescaled_smaller() throws RemoteException {
258         Context context = mControlsProviderService.getBaseContext();
259         int maxWidth = context.getResources().getDimensionPixelSize(
260                 R.dimen.controls_thumbnail_image_max_width);
261         int maxHeight = context.getResources().getDimensionPixelSize(
262                 R.dimen.controls_thumbnail_image_max_height);
263 
264         int min = Math.min(maxWidth, maxHeight);
265 
266         Bitmap bitmap = Bitmap.createBitmap(min / 2, min / 2, Bitmap.Config.ALPHA_8);
267         Icon icon = Icon.createWithBitmap(bitmap);
268         ThumbnailTemplate template = new ThumbnailTemplate("ID", false, icon, "");
269 
270         Control control = new Control.StatefulBuilder("TEST_ID", mPendingIntent)
271                 .setTitle("TEST_TITLE")
272                 .setStatus(Control.STATUS_OK)
273                 .setControlTemplate(template)
274                 .build();
275 
276         Control c = sendControlGetControl(control);
277 
278         ThumbnailTemplate sentTemplate = (ThumbnailTemplate) c.getControlTemplate();
279         Bitmap sentBitmap = sentTemplate.getThumbnail().getBitmap();
280 
281         assertEquals(bitmap.getHeight(), sentBitmap.getHeight());
282         assertEquals(bitmap.getWidth(), sentBitmap.getWidth());
283     }
284 
285     @Test
testOnAction()286     public void testOnAction() throws RemoteException {
287         mControlsProvider.action("TEST_ID", new ControlActionWrapper(
288                 new CommandAction("", null)), mActionCallback);
289         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
290 
291         verify(mActionCallback).accept(mToken, "TEST_ID",
292                 ControlAction.RESPONSE_OK);
293     }
294 
295     @Test
testRequestAdd()296     public void testRequestAdd() {
297         Control control = new Control.StatelessBuilder("TEST_ID", mPendingIntent).build();
298         ControlsProviderService.requestAddControl(mContext, TEST_COMPONENT, control);
299 
300         verify(mContext).sendBroadcast(mIntentArgumentCaptor.capture(),
301                 eq(Manifest.permission.BIND_CONTROLS));
302         Intent intent = mIntentArgumentCaptor.getValue();
303         assertEquals(ControlsProviderService.ACTION_ADD_CONTROL, intent.getAction());
304         assertEquals(TEST_CONTROLS_PACKAGE, intent.getPackage());
305         assertEquals(TEST_COMPONENT, intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME));
306         assertTrue(equals(control,
307                 intent.getParcelableExtra(ControlsProviderService.EXTRA_CONTROL)));
308     }
309 
310     /**
311      * Sends the control through the publisher in {@code mControlsProviderService}, returning
312      * the control obtained by the subscriber
313      */
sendControlGetControl(Control control)314     private Control sendControlGetControl(Control control) throws RemoteException {
315         @SuppressWarnings("unchecked")
316         ArgumentCaptor<Control> controlCaptor =
317                 ArgumentCaptor.forClass(Control.class);
318         ArgumentCaptor<IControlsSubscription.Stub> subscriptionCaptor =
319                 ArgumentCaptor.forClass(IControlsSubscription.Stub.class);
320 
321         ArrayList<Control> list = new ArrayList<>();
322         list.add(control);
323 
324         mControlsProviderService.setControls(list);
325 
326         mControlsProvider.subscribe(new ArrayList<String>(), mSubscriber);
327         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
328 
329         verify(mSubscriber).onSubscribe(eq(mToken), subscriptionCaptor.capture());
330         subscriptionCaptor.getValue().request(1);
331 
332         verify(mSubscriber).onNext(eq(mToken), controlCaptor.capture());
333         return controlCaptor.getValue();
334     }
335 
equals(Control c1, Control c2)336     private static boolean equals(Control c1, Control c2) {
337         if (c1 == c2) return true;
338         if (c1 == null || c2 == null) return false;
339         return Objects.equals(c1.getControlId(), c2.getControlId())
340                 && c1.getDeviceType() == c2.getDeviceType()
341                 && Objects.equals(c1.getTitle(), c2.getTitle())
342                 && Objects.equals(c1.getSubtitle(), c2.getSubtitle())
343                 && Objects.equals(c1.getStructure(), c2.getStructure())
344                 && Objects.equals(c1.getZone(), c2.getZone())
345                 && Objects.equals(c1.getAppIntent(), c2.getAppIntent())
346                 && Objects.equals(c1.getCustomIcon(), c2.getCustomIcon())
347                 && Objects.equals(c1.getCustomColor(), c2.getCustomColor())
348                 && c1.getStatus() == c2.getStatus()
349                 && Objects.equals(c1.getControlTemplate(), c2.getControlTemplate())
350                 && Objects.equals(c1.isAuthRequired(), c2.isAuthRequired())
351                 && Objects.equals(c1.getStatusText(), c2.getStatusText());
352     }
353 
354     static class FakeControlsProviderService extends ControlsProviderService {
355 
FakeControlsProviderService(Context context)356         FakeControlsProviderService(Context context) {
357             super();
358             attachBaseContext(context);
359         }
360 
361         private List<Control> mControls;
362 
setControls(List<Control> controls)363         public void setControls(List<Control> controls) {
364             mControls = controls;
365         }
366 
367         @Override
createPublisherForAllAvailable()368         public Publisher<Control> createPublisherForAllAvailable() {
369             return new Publisher<Control>() {
370                 public void subscribe(final Subscriber s) {
371                     s.onSubscribe(createSubscription(s, mControls));
372                 }
373             };
374         }
375 
376         @Override
createPublisherFor(List<String> ids)377         public Publisher<Control> createPublisherFor(List<String> ids) {
378             return new Publisher<Control>() {
379                 public void subscribe(final Subscriber s) {
380                     s.onSubscribe(createSubscription(s, mControls));
381                 }
382             };
383         }
384 
385         @Override
386         public Publisher<Control> createPublisherForSuggested() {
387             return new Publisher<Control>() {
388                 public void subscribe(final Subscriber s) {
389                     s.onSubscribe(createSubscription(s, mControls));
390                 }
391             };
392         }
393 
394         @Override
395         public void performControlAction(String controlId, ControlAction action,
396                 Consumer<Integer> cb) {
397             cb.accept(ControlAction.RESPONSE_OK);
398         }
399 
400         private Subscription createSubscription(Subscriber s, List<Control> controls) {
401             return new Subscription() {
402                 public void request(long n) {
403                     int i = 0;
404                     for (Control c : mControls) {
405                         if (i++ < n) s.onNext(c);
406                         else break;
407                     }
408                     s.onComplete();
409                 }
410                 public void cancel() {}
411             };
412         }
413     }
414 }
415