• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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