1 /*
2  * Copyright (C) 2017 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.view.inputmethod.cts.util;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.os.Bundle;
24 import android.os.IBinder;
25 import android.os.RemoteException;
26 import android.os.ResultReceiver;
27 
28 import androidx.annotation.NonNull;
29 
30 import java.util.concurrent.ArrayBlockingQueue;
31 import java.util.concurrent.BlockingQueue;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.TimeoutException;
34 import java.util.concurrent.atomic.AtomicBoolean;
35 
36 /**
37  * Helper class to trigger the situation where window in a different process gains focus.
38  */
39 public class WindowFocusStealer implements AutoCloseable {
40 
41     private static final class MyResultReceiver extends ResultReceiver {
42         final BlockingQueue<Integer> mQueue = new ArrayBlockingQueue<>(1);
43 
MyResultReceiver()44         MyResultReceiver() {
45             super(null);
46         }
47 
48         @Override
onReceiveResult(int resultCode, Bundle resultData)49         protected void onReceiveResult(int resultCode, Bundle resultData) {
50             mQueue.add(resultCode);
51         }
52 
waitResult(long timeout)53         public void waitResult(long timeout) throws TimeoutException {
54             final Object result;
55             try {
56                 result = mQueue.poll(timeout, TimeUnit.MILLISECONDS);
57             } catch (InterruptedException e) {
58                 throw new IllegalStateException(e);
59             }
60             if (result == null) {
61                 throw new TimeoutException();
62             }
63         }
64     }
65 
66     @NonNull
67     private final IWindowFocusStealer mService;
68     @NonNull
69     private final Runnable mCloser;
70 
71     /**
72      * Let a window in a different process gain the window focus.
73      *
74      * @param parentAppWindowToken Token returned from
75      *                             {@link android.view.View#getApplicationWindowToken()}
76      * @param timeout              timeout in millisecond
77      * @throws TimeoutException when failed to have the window focused within {@code timeout}
78      */
stealWindowFocus(IBinder parentAppWindowToken, long timeout)79     public void stealWindowFocus(IBinder parentAppWindowToken, long timeout)
80             throws TimeoutException {
81         final MyResultReceiver resultReceiver = new MyResultReceiver();
82         try {
83             mService.stealWindowFocus(parentAppWindowToken, resultReceiver);
84         } catch (RemoteException e) {
85             throw new IllegalStateException(e);
86         }
87         resultReceiver.waitResult(timeout);
88     }
89 
WindowFocusStealer(@onNull IWindowFocusStealer service, @NonNull Runnable closer)90     private WindowFocusStealer(@NonNull IWindowFocusStealer service, @NonNull Runnable closer) {
91         mService = service;
92         mCloser = closer;
93     }
94 
95     /**
96      * Establishes a connection to the service.
97      *
98      * @param context {@link Context} to which {@link WindowFocusStealerService} belongs
99      * @param timeout timeout in millisecond
100      * @throws TimeoutException when failed to establish the connection within {@code timeout}
101      */
connect(Context context, long timeout)102     public static WindowFocusStealer connect(Context context, long timeout)
103             throws TimeoutException {
104         final BlockingQueue<IWindowFocusStealer> queue = new ArrayBlockingQueue<>(1);
105 
106         final ServiceConnection connection = new ServiceConnection() {
107             public void onServiceConnected(ComponentName className, IBinder service) {
108                 queue.add(IWindowFocusStealer.Stub.asInterface(service));
109             }
110 
111             public void onServiceDisconnected(ComponentName className) {
112             }
113         };
114 
115         final Intent intent = new Intent();
116         intent.setClass(context, WindowFocusStealerService.class);
117         context.bindService(intent, connection, Context.BIND_AUTO_CREATE);
118 
119         final IWindowFocusStealer focusStealer;
120         try {
121             focusStealer = queue.poll(timeout, TimeUnit.MILLISECONDS);
122         } catch (InterruptedException e) {
123             throw new IllegalStateException(e);
124         }
125 
126         if (focusStealer == null) {
127             throw new TimeoutException();
128         }
129 
130         final AtomicBoolean closed = new AtomicBoolean(false);
131         return new WindowFocusStealer(focusStealer, () -> {
132             if (closed.compareAndSet(false, true)) {
133                 context.unbindService(connection);
134             }
135         });
136     }
137 
138     /**
139      * Removes the temporary window and clean up everything.
140      */
141     @Override
142     public void close() throws Exception {
143         mCloser.run();
144     }
145 }
146