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