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