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