• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020 The gRPC Authors
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 io.grpc.binder.internal;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.os.IBinder;
24 import androidx.annotation.AnyThread;
25 import androidx.annotation.MainThread;
26 import com.google.common.annotations.VisibleForTesting;
27 import io.grpc.Status;
28 import java.util.concurrent.Executor;
29 import java.util.logging.Level;
30 import java.util.logging.Logger;
31 import javax.annotation.Nullable;
32 import javax.annotation.concurrent.GuardedBy;
33 import javax.annotation.concurrent.ThreadSafe;
34 
35 /**
36  * Manages an Android binding that's restricted to at most one connection to the remote Service.
37  *
38  * <p>A note on synchronization & locking in this class. Clients of this class are likely to manage
39  * their own internal state via synchronization. In order to avoid deadlocks, we must not hold any
40  * locks while calling observer callbacks.
41  *
42  * <p>For this reason, while internal consistency is handled with synchronization (the state field),
43  * consistency on our observer callbacks is ensured by doing everything on the application's main
44  * thread.
45  */
46 @ThreadSafe
47 final class ServiceBinding implements Bindable, ServiceConnection {
48 
49   private static final Logger logger = Logger.getLogger(ServiceBinding.class.getName());
50 
51   // States can only ever transition in one direction.
52   private enum State {
53     NOT_BINDING,
54     BINDING,
55     BOUND,
56     UNBOUND,
57   }
58 
59   private final Intent bindIntent;
60   private final int bindFlags;
61   private final Observer observer;
62   private final Executor mainThreadExecutor;
63 
64   @GuardedBy("this")
65   private State state;
66 
67   // The following fields are intentionally not guarded, since (aside from the constructor),
68   // they're only modified in the main thread. The constructor contains a synchronized block
69   // to ensure there's a write barrier when these fields are first written.
70 
71   @Nullable private Context sourceContext; // Only null in the unbound state.
72 
73   private State reportedState; // Only used on the main thread.
74 
75   @AnyThread
ServiceBinding( Executor mainThreadExecutor, Context sourceContext, Intent bindIntent, int bindFlags, Observer observer)76   ServiceBinding(
77       Executor mainThreadExecutor,
78       Context sourceContext,
79       Intent bindIntent,
80       int bindFlags,
81       Observer observer) {
82     // We need to synchronize here ensure other threads see all
83     // non-final fields initialized after the constructor.
84     synchronized (this) {
85       this.bindIntent = bindIntent;
86       this.bindFlags = bindFlags;
87       this.observer = observer;
88       this.sourceContext = sourceContext;
89       this.mainThreadExecutor = mainThreadExecutor;
90       state = State.NOT_BINDING;
91       reportedState = State.NOT_BINDING;
92     }
93   }
94 
95   @MainThread
notifyBound(IBinder binder)96   private void notifyBound(IBinder binder) {
97     if (reportedState == State.NOT_BINDING) {
98       reportedState = State.BOUND;
99       logger.log(Level.FINEST, "notify bound - notifying");
100       observer.onBound(binder);
101     }
102   }
103 
104   @MainThread
notifyUnbound(Status reason)105   private void notifyUnbound(Status reason) {
106     logger.log(Level.FINEST, "notify unbound ", reason);
107     clearReferences();
108     if (reportedState != State.UNBOUND) {
109       reportedState = State.UNBOUND;
110       logger.log(Level.FINEST, "notify unbound - notifying");
111       observer.onUnbound(reason);
112     }
113   }
114 
115   @AnyThread
116   @Override
bind()117   public synchronized void bind() {
118     if (state == State.NOT_BINDING) {
119       state = State.BINDING;
120       Status bindResult = bindInternal(sourceContext, bindIntent, this, bindFlags);
121       if (!bindResult.isOk()) {
122         handleBindServiceFailure(sourceContext, this);
123         state = State.UNBOUND;
124         mainThreadExecutor.execute(() -> notifyUnbound(bindResult));
125       }
126     }
127   }
128 
bindInternal( Context context, Intent bindIntent, ServiceConnection conn, int flags)129   private static Status bindInternal(
130       Context context, Intent bindIntent, ServiceConnection conn, int flags) {
131     try {
132       if (!context.bindService(bindIntent, conn, flags)) {
133         return Status.UNIMPLEMENTED.withDescription(
134             "bindService(" + bindIntent + ") returned false");
135       }
136       return Status.OK;
137     } catch (SecurityException e) {
138       return Status.PERMISSION_DENIED.withCause(e).withDescription(
139           "SecurityException from bindService");
140     } catch (RuntimeException e) {
141       return Status.INTERNAL.withCause(e).withDescription(
142           "RuntimeException from bindService");
143     }
144   }
145 
146   // Over the years, the API contract for Context#bindService() has been inconsistent on the subject
147   // of error handling. But inspecting recent AOSP implementations shows that, internally,
148   // bindService() retains a reference to the ServiceConnection when it throws certain Exceptions
149   // and even when it returns false. To avoid leaks, we *always* call unbindService() in case of
150   // error and simply ignore any "Service not registered" IAE and other RuntimeExceptions.
handleBindServiceFailure(Context context, ServiceConnection conn)151   private static void handleBindServiceFailure(Context context, ServiceConnection conn) {
152     try {
153       context.unbindService(conn);
154     } catch (RuntimeException e) {
155       logger.log(Level.FINE, "Could not clean up after bindService() failure.", e);
156     }
157   }
158 
159   @Override
160   @AnyThread
unbind()161   public void unbind() {
162     unbindInternal(Status.CANCELLED);
163   }
164 
165   @AnyThread
unbindInternal(Status reason)166   void unbindInternal(Status reason) {
167     Context unbindFrom = null;
168     synchronized (this) {
169       if (state == State.BINDING || state == State.BOUND) {
170         unbindFrom = sourceContext;
171       }
172       state = State.UNBOUND;
173     }
174     mainThreadExecutor.execute(() -> notifyUnbound(reason));
175     if (unbindFrom != null) {
176       unbindFrom.unbindService(this);
177     }
178   }
179 
180   @MainThread
clearReferences()181   private void clearReferences() {
182     sourceContext = null;
183   }
184 
185   @Override
186   @MainThread
onServiceConnected(ComponentName className, IBinder binder)187   public void onServiceConnected(ComponentName className, IBinder binder) {
188     boolean bound = false;
189     synchronized (this) {
190       if (state == State.BINDING) {
191         state = State.BOUND;
192         bound = true;
193       }
194     }
195     if (bound) {
196       // We call notify directly because we know we're on the main thread already.
197       // (every millisecond counts in this path).
198       notifyBound(binder);
199     }
200   }
201 
202   @Override
203   @MainThread
onServiceDisconnected(ComponentName name)204   public void onServiceDisconnected(ComponentName name) {
205     unbindInternal(Status.UNAVAILABLE.withDescription("onServiceDisconnected: " + name));
206   }
207 
208   @Override
209   @MainThread
onNullBinding(ComponentName name)210   public void onNullBinding(ComponentName name) {
211     unbindInternal(Status.UNIMPLEMENTED.withDescription("onNullBinding: " + name));
212   }
213 
214   @Override
215   @MainThread
onBindingDied(ComponentName name)216   public void onBindingDied(ComponentName name) {
217     unbindInternal(Status.UNAVAILABLE.withDescription("onBindingDied: " + name));
218   }
219 
220   @VisibleForTesting
isSourceContextCleared()221   synchronized boolean isSourceContextCleared() {
222     return sourceContext == null;
223   }
224 }
225