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