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