aboutsummaryrefslogtreecommitdiff
path: root/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java
blob: 650ead9bcdbfd6e7f50caadc51acc78a33786ce4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/*
 * Copyright 2020 The gRPC Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.grpc.binder.internal;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import com.google.common.annotations.VisibleForTesting;
import io.grpc.Status;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Manages an Android binding that's restricted to at most one connection to the remote Service.
 *
 * <p>A note on synchronization & locking in this class. Clients of this class are likely to manage
 * their own internal state via synchronization. In order to avoid deadlocks, we must not hold any
 * locks while calling observer callbacks.
 *
 * <p>For this reason, while internal consistency is handled with synchronization (the state field),
 * consistency on our observer callbacks is ensured by doing everything on the application's main
 * thread.
 */
@ThreadSafe
final class ServiceBinding implements Bindable, ServiceConnection {

  private static final Logger logger = Logger.getLogger(ServiceBinding.class.getName());

  // States can only ever transition in one direction.
  private enum State {
    NOT_BINDING,
    BINDING,
    BOUND,
    UNBOUND,
  }

  private final Intent bindIntent;
  private final int bindFlags;
  private final Observer observer;
  private final Executor mainThreadExecutor;

  @GuardedBy("this")
  private State state;

  // The following fields are intentionally not guarded, since (aside from the constructor),
  // they're only modified in the main thread. The constructor contains a synchronized block
  // to ensure there's a write barrier when these fields are first written.

  @Nullable private Context sourceContext; // Only null in the unbound state.

  private State reportedState; // Only used on the main thread.

  @AnyThread
  ServiceBinding(
      Executor mainThreadExecutor,
      Context sourceContext,
      Intent bindIntent,
      int bindFlags,
      Observer observer) {
    // We need to synchronize here ensure other threads see all
    // non-final fields initialized after the constructor.
    synchronized (this) {
      this.bindIntent = bindIntent;
      this.bindFlags = bindFlags;
      this.observer = observer;
      this.sourceContext = sourceContext;
      this.mainThreadExecutor = mainThreadExecutor;
      state = State.NOT_BINDING;
      reportedState = State.NOT_BINDING;
    }
  }

  @MainThread
  private void notifyBound(IBinder binder) {
    if (reportedState == State.NOT_BINDING) {
      reportedState = State.BOUND;
      logger.log(Level.FINEST, "notify bound - notifying");
      observer.onBound(binder);
    }
  }

  @MainThread
  private void notifyUnbound(Status reason) {
    logger.log(Level.FINEST, "notify unbound ", reason);
    clearReferences();
    if (reportedState != State.UNBOUND) {
      reportedState = State.UNBOUND;
      logger.log(Level.FINEST, "notify unbound - notifying");
      observer.onUnbound(reason);
    }
  }

  @AnyThread
  @Override
  public synchronized void bind() {
    if (state == State.NOT_BINDING) {
      state = State.BINDING;
      Status bindResult = bindInternal(sourceContext, bindIntent, this, bindFlags);
      if (!bindResult.isOk()) {
        handleBindServiceFailure(sourceContext, this);
        state = State.UNBOUND;
        mainThreadExecutor.execute(() -> notifyUnbound(bindResult));
      }
    }
  }

  private static Status bindInternal(
      Context context, Intent bindIntent, ServiceConnection conn, int flags) {
    try {
      if (!context.bindService(bindIntent, conn, flags)) {
        return Status.UNIMPLEMENTED.withDescription(
            "bindService(" + bindIntent + ") returned false");
      }
      return Status.OK;
    } catch (SecurityException e) {
      return Status.PERMISSION_DENIED.withCause(e).withDescription(
          "SecurityException from bindService");
    } catch (RuntimeException e) {
      return Status.INTERNAL.withCause(e).withDescription(
          "RuntimeException from bindService");
    }
  }

  // Over the years, the API contract for Context#bindService() has been inconsistent on the subject
  // of error handling. But inspecting recent AOSP implementations shows that, internally,
  // bindService() retains a reference to the ServiceConnection when it throws certain Exceptions
  // and even when it returns false. To avoid leaks, we *always* call unbindService() in case of
  // error and simply ignore any "Service not registered" IAE and other RuntimeExceptions.
  private static void handleBindServiceFailure(Context context, ServiceConnection conn) {
    try {
      context.unbindService(conn);
    } catch (RuntimeException e) {
      logger.log(Level.FINE, "Could not clean up after bindService() failure.", e);
    }
  }

  @Override
  @AnyThread
  public void unbind() {
    unbindInternal(Status.CANCELLED);
  }

  @AnyThread
  void unbindInternal(Status reason) {
    Context unbindFrom = null;
    synchronized (this) {
      if (state == State.BINDING || state == State.BOUND) {
        unbindFrom = sourceContext;
      }
      state = State.UNBOUND;
    }
    mainThreadExecutor.execute(() -> notifyUnbound(reason));
    if (unbindFrom != null) {
      unbindFrom.unbindService(this);
    }
  }

  @MainThread
  private void clearReferences() {
    sourceContext = null;
  }

  @Override
  @MainThread
  public void onServiceConnected(ComponentName className, IBinder binder) {
    boolean bound = false;
    synchronized (this) {
      if (state == State.BINDING) {
        state = State.BOUND;
        bound = true;
      }
    }
    if (bound) {
      // We call notify directly because we know we're on the main thread already.
      // (every millisecond counts in this path).
      notifyBound(binder);
    }
  }

  @Override
  @MainThread
  public void onServiceDisconnected(ComponentName name) {
    unbindInternal(Status.UNAVAILABLE.withDescription("onServiceDisconnected: " + name));
  }

  @Override
  @MainThread
  public void onNullBinding(ComponentName name) {
    unbindInternal(Status.UNIMPLEMENTED.withDescription("onNullBinding: " + name));
  }

  @Override
  @MainThread
  public void onBindingDied(ComponentName name) {
    unbindInternal(Status.UNAVAILABLE.withDescription("onBindingDied: " + name));
  }

  @VisibleForTesting
  synchronized boolean isSourceContextCleared() {
    return sourceContext == null;
  }
}