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 com.android.eventlib;
18 
19 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
20 import static android.content.Context.BIND_AUTO_CREATE;
21 
22 import static com.android.eventlib.QueryService.EARLIEST_LOG_TIME_KEY;
23 import static com.android.eventlib.QueryService.EVENT_KEY;
24 import static com.android.eventlib.QueryService.QUERIER_KEY;
25 import static com.android.eventlib.QueryService.TIMEOUT_KEY;
26 
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.ServiceConnection;
31 import android.os.Bundle;
32 import android.os.IBinder;
33 import android.os.RemoteException;
34 import android.util.Log;
35 
36 import com.android.bedstead.nene.TestApis;
37 import com.android.bedstead.nene.packages.Package;
38 import com.android.bedstead.nene.permissions.PermissionContext;
39 import com.android.bedstead.nene.users.UserReference;
40 
41 import java.time.Duration;
42 import java.time.Instant;
43 import java.util.concurrent.CountDownLatch;
44 import java.util.concurrent.TimeUnit;
45 import java.util.concurrent.atomic.AtomicBoolean;
46 import java.util.concurrent.atomic.AtomicReference;
47 
48 /**
49  * Implementation of {@link EventQuerier} used to query a single other process.
50  */
51 public class
52     RemoteEventQuerier<E extends Event, F extends EventLogsQuery> implements EventQuerier<E> {
53 
54     private static final int CONNECTION_TIMEOUT_SECONDS = 30;
55     private static final String LOG_TAG = "RemoteEventQuerier";
56     private static final Context sContext = TestApis.context().instrumentedContext();
57 
58     private final String mPackageName;
59     private final EventLogsQuery<E, F> mEventLogsQuery;
60     private int mPollSkip = 0;
61 
RemoteEventQuerier(String packageName, EventLogsQuery<E, F> eventLogsQuery)62     public RemoteEventQuerier(String packageName, EventLogsQuery<E, F> eventLogsQuery) {
63         mPackageName = packageName;
64         mEventLogsQuery = eventLogsQuery;
65     }
66 
67     private final ServiceConnection connection =
68             new ServiceConnection() {
69                 @Override
70                 public void onBindingDied(ComponentName name) {
71                     mQuery.set(null);
72                     Log.e(LOG_TAG, "Binding died for " + name);
73                 }
74 
75                 @Override
76                 public void onNullBinding(ComponentName name) {
77                     throw new RuntimeException("onNullBinding for " + name);
78                 }
79 
80                 @Override
81                 public void onServiceConnected(ComponentName className, IBinder service) {
82                     Log.i(LOG_TAG, "onServiceConnected for " + className);
83                     mQuery.set(IQueryService.Stub.asInterface(service));
84                     mConnectionCountdown.countDown();
85                 }
86 
87                 @Override
88                 public void onServiceDisconnected(ComponentName className) {
89                     mQuery.set(null);
90                     Log.i(LOG_TAG, "Service disconnected from " + className);
91                 }
92             };
93 
94     @Override
poll(Instant earliestLogTime, Duration timeout)95     public E poll(Instant earliestLogTime, Duration timeout) {
96         try {
97             ensureInitialised();
98             Instant endTime = Instant.now().plus(timeout);
99             Bundle data = createRequestBundle();
100             Duration remainingTimeout = Duration.between(Instant.now(), endTime);
101             data.putSerializable(TIMEOUT_KEY, remainingTimeout);
102             try {
103                 Bundle resultMessage = mQuery.get().poll(data, mPollSkip++);
104                 E e = (E) resultMessage.getSerializable(EVENT_KEY);
105                 while (e != null && !mEventLogsQuery.filterAll(e)) {
106                     remainingTimeout = Duration.between(Instant.now(), endTime);
107                     data.putSerializable(TIMEOUT_KEY, remainingTimeout);
108                     resultMessage = mQuery.get().poll(data, mPollSkip++);
109                     e = (E) resultMessage.getSerializable(EVENT_KEY);
110                 }
111                 return e;
112             } catch (RemoteException e) {
113                 throw new IllegalStateException("Error making cross-process call", e);
114             }
115         } finally {
116             ensureClosed();
117         }
118     }
119 
createRequestBundle()120     private Bundle createRequestBundle() {
121         Bundle data = new Bundle();
122         data.putSerializable(EARLIEST_LOG_TIME_KEY, EventLogs.sEarliestLogTime);
123         data.putSerializable(QUERIER_KEY, mEventLogsQuery);
124         return data;
125     }
126 
127     private AtomicReference<IQueryService> mQuery = new AtomicReference<>();
128     private CountDownLatch mConnectionCountdown;
129 
130     private static final int MAX_INITIALISATION_ATTEMPTS = 20;
131     private static final long INITIALISATION_ATTEMPT_DELAY_MS = 50;
132 
ensureClosed()133     private void ensureClosed() {
134         mQuery.set(null);
135         sContext.unbindService(connection);
136     }
137 
ensureInitialised()138     private void ensureInitialised() {
139         // We have retries for binding because there are a number of reasons binding could fail in
140         // unpredictable ways
141         int attempts = 0;
142         while (attempts++ < MAX_INITIALISATION_ATTEMPTS) {
143             try {
144                 ensureInitialisedOrThrow();
145                 return;
146             } catch (Exception | Error e) {
147                 // Ignore, we will retry
148                 Log.i(LOG_TAG, "Error connecting", e);
149             }
150             try {
151                 Thread.sleep(INITIALISATION_ATTEMPT_DELAY_MS);
152             } catch (InterruptedException e) {
153                 throw new IllegalStateException("Interrupted while initialising", e);
154             }
155         }
156         ensureInitialisedOrThrow();
157     }
158 
ensureInitialisedOrThrow()159     private void ensureInitialisedOrThrow() {
160         if (mQuery.get() != null) {
161             return;
162         }
163 
164         blockingConnectOrFail();
165     }
166 
blockingConnectOrFail()167     private void blockingConnectOrFail() {
168         mConnectionCountdown = new CountDownLatch(1);
169         Intent intent = new Intent();
170         intent.setPackage(mPackageName);
171         intent.setClassName(mPackageName, "com.android.eventlib.QueryService");
172 
173         AtomicBoolean didBind = new AtomicBoolean(false);
174         if (mEventLogsQuery.getUserHandle() != null
175                 && mEventLogsQuery.getUserHandle().getIdentifier()
176                 != TestApis.users().instrumented().id()) {
177             try (PermissionContext p =
178                          TestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
179                 didBind.set(sContext.bindServiceAsUser(
180                         intent, connection, /* flags= */ BIND_AUTO_CREATE,
181                         mEventLogsQuery.getUserHandle()));
182             }
183         } else {
184             didBind.set(sContext.bindService(intent, connection, /* flags= */ BIND_AUTO_CREATE));
185         }
186 
187         if (didBind.get()) {
188             try {
189                 mConnectionCountdown.await(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
190             } catch (InterruptedException e) {
191                 throw new IllegalStateException("Interrupted while binding to service", e);
192             }
193         } else {
194             UserReference user = (mEventLogsQuery.getUserHandle() == null)
195                     ? TestApis.users().instrumented()
196                     : TestApis.users().find(mEventLogsQuery.getUserHandle());
197             if (!user.exists()) {
198                 throw new AssertionError("Tried to bind to user " + mEventLogsQuery.getUserHandle() + " but does not exist");
199             }
200             if (!user.isUnlocked()) {
201                 throw new AssertionError("Tried to bind to user " + user
202                         + " but they are not unlocked");
203             }
204             Package pkg = TestApis.packages().find(mPackageName);
205             if (!pkg.installedOnUser(user)) {
206                 throw new AssertionError("Tried to bind to package " + mPackageName + " but it is not installed on target user " + user);
207             }
208 
209             throw new IllegalStateException("Tried to bind but call returned false (intent is "
210                     + intent + ", user is  " + mEventLogsQuery.getUserHandle() + ")");
211         }
212 
213         if (mQuery.get() == null) {
214             throw new IllegalStateException("Tried to bind but failed. Expected onServiceConnected"
215                     + " to have been called but it was not.");
216         }
217     }
218 }
219