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