Home | History | Annotate | Download | only in internal
      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    */
     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
     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 
     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
    122   protected CountDownLatch createCountDownLatch() {
    123     return new CountDownLatch(1);
    124   }
    125 
    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
    168   State getCurrentState() {
    169     return serviceContext.state;
    170   }
    171 
    172   private ServiceContext getCurrentServiceState() {
    173     return serviceContext;
    174   }
    175 
    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 
    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    */
    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
    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 
    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 
    305     private ServiceContext(State state) {
    306       this(state, /* compatService= */ null);
    307     }
    308   }
    309 
    310   @VisibleForTesting
    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
    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