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.app.cts; 18 19 import static android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES; 20 import static android.view.Display.DEFAULT_DISPLAY; 21 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 22 23 import static com.google.common.truth.Truth.assertThat; 24 25 import static junit.framework.Assert.assertTrue; 26 27 import static org.junit.Assume.assumeTrue; 28 import static org.testng.Assert.assertThrows; 29 30 import android.Manifest; 31 import android.app.ActivityManager; 32 import android.app.Instrumentation; 33 import android.app.UiAutomation; 34 import android.app.stubs.shared.FakeView; 35 import android.app.stubs.shared.FutureServiceConnection; 36 import android.app.stubs.shared.ICloseSystemDialogsTestsService; 37 import android.app.stubs.shared.NotificationHelper; 38 import android.app.stubs.shared.TestNotificationListener; 39 import android.content.BroadcastReceiver; 40 import android.content.ComponentName; 41 import android.content.ContentResolver; 42 import android.content.Context; 43 import android.content.Intent; 44 import android.content.IntentFilter; 45 import android.database.ContentObserver; 46 import android.hardware.display.DisplayManager; 47 import android.os.Bundle; 48 import android.os.ConditionVariable; 49 import android.os.Handler; 50 import android.os.Looper; 51 import android.os.Process; 52 import android.os.ResultReceiver; 53 import android.permission.PermissionManager; 54 import android.permission.cts.PermissionUtils; 55 import android.provider.Settings; 56 import android.server.wm.WindowManagerStateHelper; 57 import android.view.Display; 58 import android.view.WindowManager; 59 import android.view.WindowManager.LayoutParams; 60 61 import androidx.test.platform.app.InstrumentationRegistry; 62 import androidx.test.runner.AndroidJUnit4; 63 64 import com.android.compatibility.common.util.SystemUtil; 65 66 import org.junit.After; 67 import org.junit.Before; 68 import org.junit.Ignore; 69 import org.junit.Test; 70 import org.junit.runner.RunWith; 71 72 import java.util.concurrent.CompletableFuture; 73 import java.util.concurrent.CountDownLatch; 74 import java.util.concurrent.TimeUnit; 75 76 @RunWith(AndroidJUnit4.class) 77 public class CloseSystemDialogsTest { 78 private static final String TEST_SERVICE = 79 "android.app.stubs.shared.CloseSystemDialogsTestService"; 80 private static final String APP_COMPAT_ENABLE = "enable"; 81 private static final String APP_COMPAT_DISABLE = "disable"; 82 private static final String APP_COMPAT_RESET = "reset"; 83 private static final String ACTION_SENTINEL = "sentinel"; 84 private static final String REASON = "test"; 85 private static final long TIMEOUT_MS = 3000; 86 private static final String ACCESSIBILITY_SERVICE = 87 "android.app.stubs.shared.AppAccessibilityService"; 88 89 /** 90 * This test is not self-instrumenting, so we need to bind to the service in the instrumentation 91 * target package (instead of our package). 92 */ 93 private static final String APP_SELF = "android.app.stubs"; 94 95 /** 96 * Use com.android.app1 instead of android.app.stubs because the latter is the target of 97 * instrumentation, hence it also has shell powers for {@link 98 * Intent#ACTION_CLOSE_SYSTEM_DIALOGS} and we don't want those powers under simulation. 99 */ 100 private static final String APP_HELPER = "com.android.app4"; 101 102 private Instrumentation mInstrumentation; 103 private FutureServiceConnection mConnection; 104 private Context mContext; 105 private ContentResolver mResolver; 106 private ICloseSystemDialogsTestsService mService; 107 private volatile WindowManager mSawWindowManager; 108 private volatile Context mSawContext; 109 private volatile CompletableFuture<Void> mCloseSystemDialogsReceived; 110 private volatile ConditionVariable mSentinelReceived; 111 private volatile FakeView mFakeView; 112 private WindowManagerStateHelper mWindowState; 113 private IntentReceiver mIntentReceiver; 114 private Handler mMainHandler; 115 private TestNotificationListener mNotificationListener; 116 private NotificationHelper mNotificationHelper; 117 private String mPreviousHiddenApiPolicy; 118 private String mPreviousAccessibilityServices; 119 private String mPreviousAccessibilityEnabled; 120 private boolean mResetAccessibility; 121 122 123 @Before setUp()124 public void setUp() throws Exception { 125 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 126 mContext = mInstrumentation.getTargetContext(); 127 PermissionUtils.grantPermission(APP_SELF, Manifest.permission.POST_NOTIFICATIONS); 128 mResolver = mContext.getContentResolver(); 129 mMainHandler = new Handler(Looper.getMainLooper()); 130 mNotificationHelper = new NotificationHelper(mContext); 131 mNotificationHelper.enableListener(APP_SELF); 132 mNotificationListener = TestNotificationListener.getInstance(); 133 mWindowState = new WindowManagerStateHelper(); 134 enableUserFinal(); 135 136 final CountDownLatch latch = new CountDownLatch(1); 137 mContext.getContentResolver().registerContentObserver( 138 Settings.Global.getUriFor(Settings.Global.HIDDEN_API_POLICY), false, 139 new ContentObserver(null) { 140 @Override 141 public void onChange(boolean selfChange) { 142 super.onChange(selfChange); 143 latch.countDown(); 144 } 145 }); 146 147 // We need to test that a few hidden APIs are properly protected in the helper app. The 148 // helper app we're using doesn't have the checks disabled because it's not the target of 149 // instrumentation, see comment on APP_HELPER for details. 150 mPreviousHiddenApiPolicy = setHiddenApiPolicy("1"); 151 latch.await(3, TimeUnit.SECONDS); 152 153 // Add a receiver that will verify if the intent was sent or not 154 mIntentReceiver = new IntentReceiver(); 155 mCloseSystemDialogsReceived = new CompletableFuture<>(); 156 IntentFilter filter = new IntentFilter(); 157 filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 158 filter.addAction(ACTION_SENTINEL); 159 mContext.registerReceiver(mIntentReceiver, filter, Context.RECEIVER_EXPORTED_UNAUDITED); 160 161 // Add a view to verify if the view got the callback or not 162 mSawContext = getContextForSaw(mContext); 163 mSawWindowManager = mSawContext.getSystemService(WindowManager.class); 164 mMainHandler.post(() -> { 165 mFakeView = new FakeView(mSawContext); 166 mSawWindowManager.addView(mFakeView, new LayoutParams(TYPE_APPLICATION_OVERLAY)); 167 }); 168 } 169 170 @After tearDown()171 public void tearDown() throws Exception { 172 if (mConnection != null) { 173 mContext.unbindService(mConnection); 174 } 175 if (mResetAccessibility) { 176 setAccessibilityState(mPreviousAccessibilityEnabled, mPreviousAccessibilityServices); 177 } 178 mMainHandler.post(() -> mSawWindowManager.removeViewImmediate(mFakeView)); 179 mContext.unregisterReceiver(mIntentReceiver); 180 resetUserFinal(); 181 setHiddenApiPolicy(mPreviousHiddenApiPolicy); 182 compat(APP_COMPAT_RESET, ActivityManager.LOCK_DOWN_CLOSE_SYSTEM_DIALOGS, APP_HELPER); 183 compat(APP_COMPAT_RESET, "NOTIFICATION_TRAMPOLINE_BLOCK", APP_HELPER); 184 mNotificationHelper.disableListener(APP_SELF); 185 mNotificationListener.resetData(); 186 // Use test API to prevent PermissionManager from killing the test process when revoking 187 // permission. 188 SystemUtil.runWithShellPermissionIdentity( 189 () -> mContext.getSystemService(PermissionManager.class) 190 .revokePostNotificationPermissionWithoutKillForTest( 191 mContext.getPackageName(), 192 Process.myUserHandle().getIdentifier()), 193 Manifest.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL, 194 Manifest.permission.REVOKE_RUNTIME_PERMISSIONS); 195 } 196 197 /** Intent.ACTION_CLOSE_SYSTEM_DIALOGS */ 198 199 @Test testCloseSystemDialogs_whenTargetSdkCurrent_isBlockedAndThrows()200 public void testCloseSystemDialogs_whenTargetSdkCurrent_isBlockedAndThrows() throws Exception { 201 setTargetCurrent(); 202 mService = getService(APP_HELPER); 203 204 assertThrows(SecurityException.class, () -> mService.sendCloseSystemDialogsBroadcast()); 205 206 assertCloseSystemDialogsNotReceived(); 207 } 208 209 @Test testCloseSystemDialogs_whenTargetSdk30_isBlockedButDoesNotThrow()210 public void testCloseSystemDialogs_whenTargetSdk30_isBlockedButDoesNotThrow() throws Exception { 211 mService = getService(APP_HELPER); 212 213 mService.sendCloseSystemDialogsBroadcast(); 214 215 assertCloseSystemDialogsNotReceived(); 216 } 217 218 @Test testCloseSystemDialogs_whenTestInstrumentedViaShell_isSent()219 public void testCloseSystemDialogs_whenTestInstrumentedViaShell_isSent() throws Exception { 220 mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 221 222 assertCloseSystemDialogsReceived(); 223 } 224 225 @Test testCloseSystemDialogs_whenRunningAsShell_isSent()226 public void testCloseSystemDialogs_whenRunningAsShell_isSent() throws Exception { 227 SystemUtil.runWithShellPermissionIdentity( 228 () -> mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))); 229 230 assertCloseSystemDialogsReceived(); 231 } 232 233 @Test testCloseSystemDialogs_inTrampolineWhenTargetSdkCurrent_isBlockedAndThrows()234 public void testCloseSystemDialogs_inTrampolineWhenTargetSdkCurrent_isBlockedAndThrows() 235 throws Exception { 236 setTargetCurrent(); 237 int notificationId = 42; 238 CompletableFuture<Integer> result = new CompletableFuture<>(); 239 mService = getService(APP_HELPER); 240 241 mService.postNotification(notificationId, new FutureReceiver(result), 242 /* usePendingIntent */ false); 243 244 mNotificationHelper.clickNotification(notificationId, /* searchAll */ true); 245 assertThat(result.get()).isEqualTo( 246 ICloseSystemDialogsTestsService.RESULT_SECURITY_EXCEPTION); 247 assertCloseSystemDialogsNotReceived(); 248 } 249 250 @Test 251 @Ignore testCloseSystemDialogs_inTrampolineWhenTargetSdk30_isSent()252 public void testCloseSystemDialogs_inTrampolineWhenTargetSdk30_isSent() throws Exception { 253 int notificationId = 43; 254 CompletableFuture<Integer> result = new CompletableFuture<>(); 255 mService = getService(APP_HELPER); 256 257 mService.postNotification(notificationId, new FutureReceiver(result), 258 /* usePendingIntent */ false); 259 260 mNotificationHelper.clickNotification(notificationId, /* searchAll */ true); 261 assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK); 262 assertCloseSystemDialogsReceived(); 263 } 264 265 /** System doesn't throw on the PI's sender call stack. */ 266 @Test testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdkCurrent_isBlocked()267 public void testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdkCurrent_isBlocked() 268 throws Exception { 269 setTargetCurrent(); 270 int notificationId = 44; 271 CompletableFuture<Integer> result = new CompletableFuture<>(); 272 mService = getService(APP_HELPER); 273 274 mService.postNotification(notificationId, new FutureReceiver(result), 275 /* usePendingIntent */ true); 276 277 mNotificationHelper.clickNotification(notificationId, /* searchAll */ true); 278 assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK); 279 assertCloseSystemDialogsNotReceived(); 280 } 281 282 @Test testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdk30_isSent()283 public void testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdk30_isSent() 284 throws Exception { 285 int notificationId = 45; 286 CompletableFuture<Integer> result = new CompletableFuture<>(); 287 mService = getService(APP_HELPER); 288 289 mService.postNotification(notificationId, new FutureReceiver(result), 290 /* usePendingIntent */ true); 291 292 mNotificationHelper.clickNotification(notificationId, /* searchAll */ true); 293 assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK); 294 assertCloseSystemDialogsReceived(); 295 } 296 297 @Test testCloseSystemDialogs_withWindowAboveShadeAndTargetSdk30_isSent()298 public void testCloseSystemDialogs_withWindowAboveShadeAndTargetSdk30_isSent() 299 throws Exception { 300 // Test is only applicable to devices that have a notification shade. 301 assumeTrue(mWindowState.hasNotificationShade()); 302 mService = getService(APP_HELPER); 303 setAccessibilityService(APP_HELPER, ACCESSIBILITY_SERVICE); 304 assertTrue(mService.waitForAccessibilityServiceWindow(TIMEOUT_MS)); 305 306 mService.sendCloseSystemDialogsBroadcast(); 307 308 assertCloseSystemDialogsReceived(); 309 } 310 311 /** IWindowManager.closeSystemDialogs() */ 312 313 @Test testCloseSystemDialogsViaWindowManager_whenTestInstrumentedViaShell_isSent()314 public void testCloseSystemDialogsViaWindowManager_whenTestInstrumentedViaShell_isSent() 315 throws Exception { 316 mService = getService(APP_SELF); 317 318 mService.closeSystemDialogsViaWindowManager(REASON); 319 320 assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON); 321 } 322 323 @Test testCloseSystemDialogsViaWindowManager_whenRunningAsShell_isSent()324 public void testCloseSystemDialogsViaWindowManager_whenRunningAsShell_isSent() 325 throws Exception { 326 mService = getService(APP_SELF); 327 328 SystemUtil.runWithShellPermissionIdentity( 329 () -> mService.closeSystemDialogsViaWindowManager(REASON)); 330 331 assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON); 332 } 333 334 @Test testCloseSystemDialogsViaWindowManager_whenTargetSdkCurrent_isBlockedAndThrows()335 public void testCloseSystemDialogsViaWindowManager_whenTargetSdkCurrent_isBlockedAndThrows() 336 throws Exception { 337 setTargetCurrent(); 338 mService = getService(APP_HELPER); 339 340 assertThrows(SecurityException.class, 341 () -> mService.closeSystemDialogsViaWindowManager(REASON)); 342 343 assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null); 344 } 345 346 347 @Test testCloseSystemDialogsViaWindowManager_whenTargetSdk30_isBlockedButDoesNotThrow()348 public void testCloseSystemDialogsViaWindowManager_whenTargetSdk30_isBlockedButDoesNotThrow() 349 throws Exception { 350 mService = getService(APP_HELPER); 351 352 mService.closeSystemDialogsViaWindowManager(REASON); 353 354 assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null); 355 } 356 357 /** IActivityManager.closeSystemDialogs() */ 358 359 @Test testCloseSystemDialogsViaActivityManager_whenTestInstrumentedViaShell_isSent()360 public void testCloseSystemDialogsViaActivityManager_whenTestInstrumentedViaShell_isSent() 361 throws Exception { 362 mService = getService(APP_SELF); 363 364 mService.closeSystemDialogsViaActivityManager(REASON); 365 366 assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON); 367 assertCloseSystemDialogsReceived(); 368 } 369 370 @Test testCloseSystemDialogsViaActivityManager_whenRunningAsShell_isSent()371 public void testCloseSystemDialogsViaActivityManager_whenRunningAsShell_isSent() 372 throws Exception { 373 mService = getService(APP_SELF); 374 375 SystemUtil.runWithShellPermissionIdentity( 376 () -> mService.closeSystemDialogsViaActivityManager(REASON)); 377 378 assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON); 379 assertCloseSystemDialogsReceived(); 380 } 381 382 @Test testCloseSystemDialogsViaActivityManager_whenTargetSdkCurrent_isBlockedAndThrows()383 public void testCloseSystemDialogsViaActivityManager_whenTargetSdkCurrent_isBlockedAndThrows() 384 throws Exception { 385 setTargetCurrent(); 386 mService = getService(APP_HELPER); 387 388 assertThrows(SecurityException.class, 389 () -> mService.closeSystemDialogsViaActivityManager(REASON)); 390 391 assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null); 392 assertCloseSystemDialogsNotReceived(); 393 } 394 395 @Test testCloseSystemDialogsViaActivityManager_whenTargetSdk30_isBlockedButDoesNotThrow()396 public void testCloseSystemDialogsViaActivityManager_whenTargetSdk30_isBlockedButDoesNotThrow() 397 throws Exception { 398 mService = getService(APP_HELPER); 399 400 mService.closeSystemDialogsViaActivityManager(REASON); 401 402 assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null); 403 assertCloseSystemDialogsNotReceived(); 404 } 405 setTargetCurrent()406 private void setTargetCurrent() { 407 // The helper app has targetSdk=30, opting-in to changes emulates targeting latest sdk. 408 compat(APP_COMPAT_ENABLE, ActivityManager.LOCK_DOWN_CLOSE_SYSTEM_DIALOGS, APP_HELPER); 409 compat(APP_COMPAT_ENABLE, "NOTIFICATION_TRAMPOLINE_BLOCK", APP_HELPER); 410 } 411 assertCloseSystemDialogsNotReceived()412 private void assertCloseSystemDialogsNotReceived() { 413 // If both broadcasts are sent, they will be received in order here since they are both 414 // registered receivers in the "bg" queue in system_server and belong to the same app. 415 // This is guaranteed by a series of handlers that are the same in both cases and due to the 416 // fact that the binder that system_server uses to call into the app is the same (since the 417 // app is the same) and one-way calls on the same binder object are ordered. 418 mSentinelReceived = new ConditionVariable(false); 419 Intent intent = new Intent(ACTION_SENTINEL); 420 intent.setPackage(mContext.getPackageName()); 421 mContext.sendBroadcast(intent); 422 mSentinelReceived.block(); 423 assertThat(mCloseSystemDialogsReceived.isDone()).isFalse(); 424 } 425 assertCloseSystemDialogsReceived()426 private void assertCloseSystemDialogsReceived() throws Exception { 427 mCloseSystemDialogsReceived.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); 428 // No TimeoutException thrown 429 } 430 getService(String packageName)431 private ICloseSystemDialogsTestsService getService(String packageName) throws Exception { 432 ICloseSystemDialogsTestsService service = 433 ICloseSystemDialogsTestsService.Stub.asInterface( 434 connect(packageName).get(TIMEOUT_MS)); 435 assertTrue("Can't call @hide methods", service.waitUntilReady(TIMEOUT_MS)); 436 return service; 437 } 438 connect(String packageName)439 private FutureServiceConnection connect(String packageName) { 440 if (mConnection != null) { 441 return mConnection; 442 } 443 mConnection = new FutureServiceConnection(); 444 Intent intent = new Intent(); 445 intent.setComponent(ComponentName.createRelative(packageName, TEST_SERVICE)); 446 assertTrue(mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE)); 447 return mConnection; 448 } 449 setHiddenApiPolicy(String policy)450 private String setHiddenApiPolicy(String policy) throws Exception { 451 return SystemUtil.callWithShellPermissionIdentity(() -> { 452 String previous = Settings.Global.getString(mResolver, 453 Settings.Global.HIDDEN_API_POLICY); 454 Settings.Global.putString(mResolver, Settings.Global.HIDDEN_API_POLICY, policy); 455 return previous; 456 }); 457 } 458 setAccessibilityService(String packageName, String service)459 private void setAccessibilityService(String packageName, String service) throws Exception { 460 setAccessibilityState("1", packageName + "/" + service); 461 } 462 setAccessibilityState(String enabled, String services)463 private void setAccessibilityState(String enabled, String services) { 464 mResetAccessibility = true; 465 UiAutomation uiAutomation = mInstrumentation.getUiAutomation( 466 FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); 467 SystemUtil.runWithShellPermissionIdentity(uiAutomation, () -> { 468 mPreviousAccessibilityServices = Settings.Secure.getString(mResolver, 469 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 470 mPreviousAccessibilityEnabled = Settings.Secure.getString(mResolver, 471 Settings.Secure.ACCESSIBILITY_ENABLED); 472 Settings.Secure.putString(mResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 473 services); 474 Settings.Secure.putString(mResolver, Settings.Secure.ACCESSIBILITY_ENABLED, enabled); 475 }); 476 } 477 enableUserFinal()478 private static void enableUserFinal() { 479 SystemUtil.runShellCommand( 480 "settings put global force_non_debuggable_final_build_for_compat 1"); 481 } 482 resetUserFinal()483 private static void resetUserFinal() { 484 SystemUtil.runShellCommand( 485 "settings put global force_non_debuggable_final_build_for_compat 0"); 486 } 487 compat(String command, String changeId, String packageName)488 private static void compat(String command, String changeId, String packageName) { 489 SystemUtil.runShellCommand( 490 String.format("am compat %s %s %s", command, changeId, packageName)); 491 } 492 compat(String command, long changeId, String packageName)493 private static void compat(String command, long changeId, String packageName) { 494 compat(command, Long.toString(changeId), packageName); 495 } 496 getContextForSaw(Context context)497 private static Context getContextForSaw(Context context) { 498 DisplayManager displayManager = context.getSystemService(DisplayManager.class); 499 Display display = displayManager.getDisplay(DEFAULT_DISPLAY); 500 Context displayContext = context.createDisplayContext(display); 501 return displayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null); 502 } 503 504 private class IntentReceiver extends BroadcastReceiver { 505 @Override onReceive(Context context, Intent intent)506 public void onReceive(Context context, Intent intent) { 507 switch (intent.getAction()) { 508 case Intent.ACTION_CLOSE_SYSTEM_DIALOGS: 509 mCloseSystemDialogsReceived.complete(null); 510 break; 511 case ACTION_SENTINEL: 512 mSentinelReceived.open(); 513 break; 514 } 515 } 516 } 517 518 private class FutureReceiver extends ResultReceiver { 519 private final CompletableFuture<Integer> mFuture; 520 FutureReceiver(CompletableFuture<Integer> future)521 FutureReceiver(CompletableFuture<Integer> future) { 522 super(mMainHandler); 523 mFuture = future; 524 } 525 526 @Override onReceiveResult(int resultCode, Bundle resultData)527 protected void onReceiveResult(int resultCode, Bundle resultData) { 528 mFuture.complete(resultCode); 529 } 530 } 531 } 532