1 /* 2 * Copyright (C) 2018 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.google.android.setupcompat.internal; 18 19 import android.annotation.SuppressLint; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.ServiceConnection; 24 import android.os.IBinder; 25 import android.os.Looper; 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 import androidx.annotation.VisibleForTesting; 29 import com.google.android.setupcompat.ISetupCompatService; 30 import com.google.android.setupcompat.util.Logger; 31 import java.util.concurrent.CountDownLatch; 32 import java.util.concurrent.TimeUnit; 33 import java.util.concurrent.TimeoutException; 34 import java.util.concurrent.atomic.AtomicReference; 35 import java.util.function.UnaryOperator; 36 37 /** 38 * This class provides an instance of {@link ISetupCompatService}. It keeps track of the connection 39 * state and reconnects if necessary. 40 */ 41 public class SetupCompatServiceProvider { 42 43 private static final Logger LOG = new Logger("SetupCompatServiceProvider"); 44 45 /** 46 * Returns an instance of {@link ISetupCompatService} if one already exists. If not, attempts to 47 * rebind if the current state allows such an operation and waits until {@code waitTime} for 48 * receiving the stub reference via {@link ServiceConnection#onServiceConnected(ComponentName, 49 * IBinder)}. 50 * 51 * @throws IllegalStateException if called from the main thread since this is a blocking 52 * operation. 53 * @throws TimeoutException if timed out waiting for {@code waitTime}. 54 */ get(Context context, long waitTime, @NonNull TimeUnit timeUnit)55 public static ISetupCompatService get(Context context, long waitTime, @NonNull TimeUnit timeUnit) 56 throws TimeoutException, InterruptedException { 57 return getInstance(context).getService(waitTime, timeUnit); 58 } 59 60 @VisibleForTesting getService(long timeout, TimeUnit timeUnit)61 public ISetupCompatService getService(long timeout, TimeUnit timeUnit) 62 throws TimeoutException, InterruptedException { 63 Preconditions.checkState( 64 disableLooperCheckForTesting || Looper.getMainLooper() != Looper.myLooper(), 65 "getService blocks and should not be called from the main thread."); 66 ServiceContext serviceContext = getCurrentServiceState(); 67 switch (serviceContext.state) { 68 case CONNECTED: 69 return serviceContext.compatService; 70 71 case SERVICE_NOT_USABLE: 72 case BIND_FAILED: 73 // End states, no valid connection can be obtained ever. 74 return null; 75 76 case DISCONNECTED: 77 case BINDING: 78 return waitForConnection(timeout, timeUnit); 79 80 case REBIND_REQUIRED: 81 requestServiceBind(); 82 return waitForConnection(timeout, timeUnit); 83 84 case NOT_STARTED: 85 LOG.w("NOT_STARTED state only possible before instance is created."); 86 return null; 87 } 88 throw new IllegalStateException("Unknown state = " + serviceContext.state); 89 } 90 waitForConnection(long timeout, TimeUnit timeUnit)91 private ISetupCompatService waitForConnection(long timeout, TimeUnit timeUnit) 92 throws TimeoutException, InterruptedException { 93 ServiceContext currentServiceState = getCurrentServiceState(); 94 if (currentServiceState.state == State.CONNECTED) { 95 return currentServiceState.compatService; 96 } 97 98 CountDownLatch connectedStateLatch = getConnectedCondition(); 99 LOG.atInfo("Waiting for service to get connected"); 100 boolean stateChanged = connectedStateLatch.await(timeout, timeUnit); 101 if (!stateChanged) { 102 // Even though documentation states that disconnected service should connect again, 103 // requesting rebind reduces the wait time to acquire a new connection. 104 requestServiceBind(); 105 throw new TimeoutException( 106 String.format("Failed to acquire connection after [%s %s]", timeout, timeUnit)); 107 } 108 currentServiceState = getCurrentServiceState(); 109 LOG.atInfo( 110 String.format( 111 "Finished waiting for service to get connected. Current state = %s", 112 currentServiceState.state)); 113 return currentServiceState.compatService; 114 } 115 116 /** 117 * This method is being overwritten by {@link SetupCompatServiceProviderTest} for injecting an 118 * instance of {@link CountDownLatch}. 119 */ 120 @VisibleForTesting createCountDownLatch()121 protected CountDownLatch createCountDownLatch() { 122 return new CountDownLatch(1); 123 } 124 requestServiceBind()125 private synchronized void requestServiceBind() { 126 ServiceContext currentServiceState = getCurrentServiceState(); 127 if (currentServiceState.state == State.CONNECTED) { 128 LOG.atInfo("Refusing to rebind since current state is already connected"); 129 return; 130 } 131 if (currentServiceState.state != State.NOT_STARTED) { 132 LOG.atInfo("Unbinding existing service connection."); 133 context.unbindService(serviceConnection); 134 } 135 136 boolean bindAllowed; 137 try { 138 bindAllowed = 139 context.bindService(COMPAT_SERVICE_INTENT, serviceConnection, Context.BIND_AUTO_CREATE); 140 } catch (SecurityException e) { 141 LOG.e("Unable to bind to compat service. " + e); 142 bindAllowed = false; 143 } 144 145 if (bindAllowed) { 146 // Robolectric calls ServiceConnection#onServiceConnected inline during Context#bindService. 147 // This check prevents us from overriding connected state which usually arrives much later 148 // in the normal world 149 if (getCurrentState() != State.CONNECTED) { 150 swapServiceContextAndNotify(new ServiceContext(State.BINDING)); 151 LOG.atInfo("Context#bindService went through, now waiting for service connection"); 152 } 153 } else { 154 // SetupWizard is not installed/calling app does not have permissions to bind. 155 swapServiceContextAndNotify(new ServiceContext(State.BIND_FAILED)); 156 LOG.e("Context#bindService did not succeed."); 157 } 158 } 159 160 @VisibleForTesting 161 static final Intent COMPAT_SERVICE_INTENT = 162 new Intent() 163 .setPackage("com.google.android.setupwizard") 164 .setAction("com.google.android.setupcompat.SetupCompatService.BIND"); 165 166 @VisibleForTesting getCurrentState()167 State getCurrentState() { 168 return serviceContext.state; 169 } 170 getCurrentServiceState()171 private synchronized ServiceContext getCurrentServiceState() { 172 return serviceContext; 173 } 174 175 @VisibleForTesting swapServiceContextAndNotify(ServiceContext latestServiceContext)176 void swapServiceContextAndNotify(ServiceContext latestServiceContext) { 177 LOG.atInfo( 178 String.format("State changed: %s -> %s", serviceContext.state, latestServiceContext.state)); 179 180 serviceContext = latestServiceContext; 181 CountDownLatch countDownLatch = getAndClearConnectedCondition(); 182 if (countDownLatch != null) { 183 countDownLatch.countDown(); 184 } 185 } 186 getAndClearConnectedCondition()187 private CountDownLatch getAndClearConnectedCondition() { 188 return connectedConditionRef.getAndSet(/* newValue= */ null); 189 } 190 191 /** 192 * Cannot use {@link AtomicReference#updateAndGet(UnaryOperator)} to fix null reference since the 193 * library needs to be compatible with legacy android devices. 194 */ getConnectedCondition()195 private CountDownLatch getConnectedCondition() { 196 CountDownLatch countDownLatch; 197 // Loop until either count down latch is found or successfully able to update atomic reference. 198 do { 199 countDownLatch = connectedConditionRef.get(); 200 if (countDownLatch != null) { 201 return countDownLatch; 202 } 203 countDownLatch = createCountDownLatch(); 204 } while (!connectedConditionRef.compareAndSet(/* expectedValue= */ null, countDownLatch)); 205 return countDownLatch; 206 } 207 208 @VisibleForTesting SetupCompatServiceProvider(Context context)209 SetupCompatServiceProvider(Context context) { 210 this.context = context.getApplicationContext(); 211 } 212 213 @VisibleForTesting 214 final ServiceConnection serviceConnection = 215 new ServiceConnection() { 216 @Override 217 public void onServiceConnected(ComponentName componentName, IBinder binder) { 218 State state = State.CONNECTED; 219 if (binder == null) { 220 state = State.DISCONNECTED; 221 LOG.w("Binder is null when onServiceConnected was called!"); 222 } 223 swapServiceContextAndNotify( 224 new ServiceContext(state, ISetupCompatService.Stub.asInterface(binder))); 225 } 226 227 @Override 228 public void onServiceDisconnected(ComponentName componentName) { 229 swapServiceContextAndNotify(new ServiceContext(State.DISCONNECTED)); 230 } 231 232 @Override 233 public void onBindingDied(ComponentName name) { 234 swapServiceContextAndNotify(new ServiceContext(State.REBIND_REQUIRED)); 235 } 236 237 @Override 238 public void onNullBinding(ComponentName name) { 239 swapServiceContextAndNotify(new ServiceContext(State.SERVICE_NOT_USABLE)); 240 } 241 }; 242 243 private volatile ServiceContext serviceContext = new ServiceContext(State.NOT_STARTED); 244 private final Context context; 245 private final AtomicReference<CountDownLatch> connectedConditionRef = new AtomicReference<>(); 246 247 @VisibleForTesting 248 enum State { 249 /** Initial state of the service instance is completely created. */ 250 NOT_STARTED, 251 252 /** 253 * Attempt to call {@link Context#bindService(Intent, ServiceConnection, int)} failed because, 254 * either Setupwizard is not installed or the app does not have permission to bind. This is an 255 * unrecoverable situation. 256 */ 257 BIND_FAILED, 258 259 /** 260 * Call to bind with the service went through, now waiting for {@link 261 * ServiceConnection#onServiceConnected(ComponentName, IBinder)}. 262 */ 263 BINDING, 264 265 /** Provider is connected to the service and can call the API(s). */ 266 CONNECTED, 267 268 /** 269 * Not connected since provider received the call {@link 270 * ServiceConnection#onServiceDisconnected(ComponentName)}, and waiting for {@link 271 * ServiceConnection#onServiceConnected(ComponentName, IBinder)}. 272 */ 273 DISCONNECTED, 274 275 /** 276 * Similar to {@link #BIND_FAILED}, the bind call went through but we received a "null" binding 277 * via {@link ServiceConnection#onNullBinding(ComponentName)}. This is an unrecoverable 278 * situation. 279 */ 280 SERVICE_NOT_USABLE, 281 282 /** 283 * The provider has requested rebind via {@link Context#bindService(Intent, ServiceConnection, 284 * int)} and is waiting for a service connection. 285 */ 286 REBIND_REQUIRED 287 } 288 289 @VisibleForTesting 290 static final class ServiceContext { 291 final State state; 292 @Nullable final ISetupCompatService compatService; 293 ServiceContext(State state, @Nullable ISetupCompatService compatService)294 private ServiceContext(State state, @Nullable ISetupCompatService compatService) { 295 this.state = state; 296 this.compatService = compatService; 297 if (state == State.CONNECTED) { 298 Preconditions.checkNotNull( 299 compatService, "CompatService cannot be null when state is connected"); 300 } 301 } 302 303 @VisibleForTesting ServiceContext(State state)304 ServiceContext(State state) { 305 this(state, /* compatService= */ null); 306 } 307 } 308 309 @VisibleForTesting getInstance(@onNull Context context)310 static SetupCompatServiceProvider getInstance(@NonNull Context context) { 311 Preconditions.checkNotNull(context, "Context object cannot be null."); 312 SetupCompatServiceProvider result = instance; 313 if (result == null) { 314 synchronized (SetupCompatServiceProvider.class) { 315 result = instance; 316 if (result == null) { 317 instance = result = new SetupCompatServiceProvider(context.getApplicationContext()); 318 instance.requestServiceBind(); 319 } 320 } 321 } 322 return result; 323 } 324 325 @VisibleForTesting setInstanceForTesting(SetupCompatServiceProvider testInstance)326 public static void setInstanceForTesting(SetupCompatServiceProvider testInstance) { 327 instance = testInstance; 328 } 329 330 @VisibleForTesting static boolean disableLooperCheckForTesting = false; 331 332 // The instance is coming from Application context which alive during the application activate and 333 // it's not depend on the activities life cycle, so we can avoid memory leak. However linter 334 // cannot distinguish Application context or activity context, so we add @SuppressLint to avoid 335 // lint error. 336 @SuppressLint("StaticFieldLeak") 337 private static volatile SetupCompatServiceProvider instance; 338 } 339