blob: 3490b1d1895393aa008360101d4b9d676093eb83 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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 com.android.server.am;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
/**
* Connects to a given service component on a given user.
*
* - Call {@link #bind()} to create a connection.
* - Call {@link #unbind()} to disconnect. Make sure to disconnect when the user stops.
*
* Add onConnected/onDisconnected callbacks as needed.
*
* When the target process gets killed (by OOM-killer, etc), then the activity manager will
* re-connect the connection automatically, in which case onServiceDisconnected() gets called
* and then onServiceConnected().
*
* However sometimes the activity manager just "kills" the connection -- like when the target
* package gets updated or the target process crashes multiple times in a row, in which case
* onBindingDied() gets called. This class handles this case by re-connecting in the time
* {@link #mRebindBackoffMs}. If this happens again, this class increases the back-off time
* by {@link #mRebindBackoffIncrease} and retry. The back-off time is capped at
* {@link #mRebindMaxBackoffMs}.
*
* The back-off time will never be reset until {@link #unbind()} and {@link #bind()} are called
* explicitly.
*
* NOTE: This class does *not* handle package-updates -- i.e. even if the binding dies due to
* the target package being updated, this class won't reconnect. This is because this class doesn't
* know what to do when the service component has gone missing, for example. If the user of this
* class wants to restore the connection, then it should call {@link #unbind()} and {@link #bind}
* explicitly.
*
* atest ${ANDROID_BUILD_TOP}/frameworks/base/services/tests/mockingservicestests/src/com/android/server/am/PersistentConnectionTest.java
*/
public abstract class PersistentConnection<T> {
private final Object mLock = new Object();
private final static boolean DEBUG = false;
private final String mTag;
private final Context mContext;
private final Handler mHandler;
private final int mUserId;
private final ComponentName mComponentName;
private long mNextBackoffMs;
private final long mRebindBackoffMs;
private final double mRebindBackoffIncrease;
private final long mRebindMaxBackoffMs;
private final long mResetBackoffDelay;
private long mReconnectTime;
// TODO too many booleans... Should clean up.
@GuardedBy("mLock")
private boolean mBound;
/**
* Whether {@link #bind()} has been called and {@link #unbind()} hasn't been yet; meaning this
* is the expected bind state from the caller's point of view.
*/
@GuardedBy("mLock")
private boolean mShouldBeBound;
@GuardedBy("mLock")
private boolean mRebindScheduled;
@GuardedBy("mLock")
private boolean mIsConnected;
@GuardedBy("mLock")
private T mService;
@GuardedBy("mLock")
private int mNumConnected;
@GuardedBy("mLock")
private int mNumDisconnected;
@GuardedBy("mLock")
private int mNumBindingDied;
@GuardedBy("mLock")
private long mLastConnectedTime;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mLock) {
if (!mBound) {
// Callback came in after PersistentConnection.unbind() was called.
// We just ignore this.
// (We've already called unbindService() already in unbind)
Log.w(mTag, "Connected: " + mComponentName.flattenToShortString()
+ " u" + mUserId + " but not bound, ignore.");
return;
}
Log.i(mTag, "Connected: " + mComponentName.flattenToShortString()
+ " u" + mUserId);
mNumConnected++;
mIsConnected = true;
mLastConnectedTime = injectUptimeMillis();
mService = asInterface(service);
scheduleStableCheckLocked();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
Log.i(mTag, "Disconnected: " + mComponentName.flattenToShortString()
+ " u" + mUserId);
mNumDisconnected++;
cleanUpConnectionLocked();
// Note we won't increase the rebind timeout here, because we don't explicitly
// rebind in this case.
}
}
@Override
public void onBindingDied(ComponentName name) {
// Activity manager gave up; we'll schedule a re-connect by ourselves.
synchronized (mLock) {
if (!mBound) {
// Callback came in late?
Log.w(mTag, "Binding died: " + mComponentName.flattenToShortString()
+ " u" + mUserId + " but not bound, ignore.");
return;
}
Log.w(mTag, "Binding died: " + mComponentName.flattenToShortString()
+ " u" + mUserId);
mNumBindingDied++;
scheduleRebindLocked();
}
}
};
private final Runnable mBindForBackoffRunnable = () -> bindForBackoff();
public PersistentConnection(@NonNull String tag, @NonNull Context context,
@NonNull Handler handler, int userId, @NonNull ComponentName componentName,
long rebindBackoffSeconds, double rebindBackoffIncrease, long rebindMaxBackoffSeconds,
long resetBackoffDelay) {
mTag = tag;
mContext = context;
mHandler = handler;
mUserId = userId;
mComponentName = componentName;
mRebindBackoffMs = rebindBackoffSeconds * 1000;
mRebindBackoffIncrease = rebindBackoffIncrease;
mRebindMaxBackoffMs = rebindMaxBackoffSeconds * 1000;
mResetBackoffDelay = resetBackoffDelay * 1000;
mNextBackoffMs = mRebindBackoffMs;
}
public final ComponentName getComponentName() {
return mComponentName;
}
public final int getUserId() {
return mUserId;
}
protected abstract int getBindFlags();
/**
* @return whether {@link #bind()} has been called and {@link #unbind()} hasn't.
*
* Note when the AM gives up on connection, this class detects it and un-bind automatically,
* and schedule rebind, and {@link #isBound} returns false when it's waiting for a retry.
*/
public final boolean isBound() {
synchronized (mLock) {
return mBound;
}
}
/**
* @return whether re-bind is scheduled after the AM gives up on a connection.
*/
public final boolean isRebindScheduled() {
synchronized (mLock) {
return mRebindScheduled;
}
}
/**
* @return whether connected.
*/
public final boolean isConnected() {
synchronized (mLock) {
return mIsConnected;
}
}
/**
* @return the service binder interface.
*/
public final T getServiceBinder() {
synchronized (mLock) {
return mService;
}
}
/**
* Connects to the service.
*/
public final void bind() {
synchronized (mLock) {
mShouldBeBound = true;
bindInnerLocked(/* resetBackoff= */ true);
}
}
/** Return the next back-off time */
public long getNextBackoffMs() {
synchronized (mLock) {
return mNextBackoffMs;
}
}
/** Return the number of times the connected callback called. */
public int getNumConnected() {
synchronized (mLock) {
return mNumConnected;
}
}
/** Return the number of times the disconnected callback called. */
public int getNumDisconnected() {
synchronized (mLock) {
return mNumDisconnected;
}
}
/** Return the number of times the binding died callback called. */
public int getNumBindingDied() {
synchronized (mLock) {
return mNumBindingDied;
}
}
@GuardedBy("mLock")
private void resetBackoffLocked() {
if (mNextBackoffMs != mRebindBackoffMs) {
mNextBackoffMs = mRebindBackoffMs;
Log.i(mTag, "Backoff reset to " + mNextBackoffMs);
}
}
@GuardedBy("mLock")
public final void bindInnerLocked(boolean resetBackoff) {
unscheduleRebindLocked();
if (mBound) {
return;
}
mBound = true;
unscheduleStableCheckLocked();
if (resetBackoff) {
resetBackoffLocked();
}
final Intent service = new Intent().setComponent(mComponentName);
if (DEBUG) {
Log.d(mTag, "Attempting to connect to " + mComponentName);
}
final boolean success = mContext.bindServiceAsUser(service, mServiceConnection,
Context.BIND_AUTO_CREATE | getBindFlags(),
mHandler, UserHandle.of(mUserId));
if (!success) {
Log.e(mTag, "Binding: " + service.getComponent() + " u" + mUserId
+ " failed.");
}
}
final void bindForBackoff() {
synchronized (mLock) {
if (!mShouldBeBound) {
// Race condition -- by the time we got here, unbind() has already been called.
return;
}
bindInnerLocked(/* resetBackoff= */ false);
}
}
@GuardedBy("mLock")
private void cleanUpConnectionLocked() {
mIsConnected = false;
mService = null;
unscheduleStableCheckLocked();
}
/**
* Disconnect from the service.
*/
public final void unbind() {
synchronized (mLock) {
mShouldBeBound = false;
unbindLocked();
unscheduleStableCheckLocked();
}
}
@GuardedBy("mLock")
private final void unbindLocked() {
unscheduleRebindLocked();
if (!mBound) {
return;
}
Log.i(mTag, "Stopping: " + mComponentName.flattenToShortString() + " u" + mUserId);
mBound = false;
mContext.unbindService(mServiceConnection);
cleanUpConnectionLocked();
}
@GuardedBy("mLock")
void unscheduleRebindLocked() {
injectRemoveCallbacks(mBindForBackoffRunnable);
mRebindScheduled = false;
}
@GuardedBy("mLock")
void scheduleRebindLocked() {
unbindLocked();
if (!mRebindScheduled) {
Log.i(mTag, "Scheduling to reconnect in " + mNextBackoffMs + " ms (uptime)");
mReconnectTime = injectUptimeMillis() + mNextBackoffMs;
injectPostAtTime(mBindForBackoffRunnable, mReconnectTime);
mNextBackoffMs = Math.min(mRebindMaxBackoffMs,
(long) (mNextBackoffMs * mRebindBackoffIncrease));
mRebindScheduled = true;
}
}
private final Runnable mStableCheck = this::stableConnectionCheck;
private void stableConnectionCheck() {
synchronized (mLock) {
final long now = injectUptimeMillis();
final long timeRemaining = (mLastConnectedTime + mResetBackoffDelay) - now;
if (DEBUG) {
Log.d(mTag, "stableConnectionCheck: bound=" + mBound + " connected=" + mIsConnected
+ " remaining=" + timeRemaining);
}
if (mBound && mIsConnected && timeRemaining <= 0) {
resetBackoffLocked();
}
}
}
@GuardedBy("mLock")
private void unscheduleStableCheckLocked() {
injectRemoveCallbacks(mStableCheck);
}
@GuardedBy("mLock")
private void scheduleStableCheckLocked() {
unscheduleStableCheckLocked();
injectPostAtTime(mStableCheck, injectUptimeMillis() + mResetBackoffDelay);
}
/** Must be implemented by a subclass to convert an {@link IBinder} to a stub. */
protected abstract T asInterface(IBinder binder);
public void dump(String prefix, PrintWriter pw) {
synchronized (mLock) {
pw.print(prefix);
pw.print(mComponentName.flattenToShortString());
pw.print(" u");
pw.print(mUserId);
pw.print(mBound ? " [bound]" : " [not bound]");
pw.print(mIsConnected ? " [connected]" : " [not connected]");
if (mRebindScheduled) {
pw.print(" reconnect in ");
TimeUtils.formatDuration((mReconnectTime - injectUptimeMillis()), pw);
}
pw.println();
pw.print(prefix);
pw.print(" Next backoff(sec): ");
pw.print(mNextBackoffMs / 1000);
pw.println();
pw.print(prefix);
pw.print(" Connected: ");
pw.print(mNumConnected);
pw.print(" Disconnected: ");
pw.print(mNumDisconnected);
pw.print(" Died: ");
pw.print(mNumBindingDied);
if (mIsConnected) {
pw.print(" Duration: ");
TimeUtils.formatDuration((injectUptimeMillis() - mLastConnectedTime), pw);
}
pw.println();
}
}
@VisibleForTesting
void injectRemoveCallbacks(Runnable r) {
mHandler.removeCallbacks(r);
}
@VisibleForTesting
void injectPostAtTime(Runnable r, long uptimeMillis) {
mHandler.postAtTime(r, uptimeMillis);
}
@VisibleForTesting
long injectUptimeMillis() {
return SystemClock.uptimeMillis();
}
@VisibleForTesting
long getNextBackoffMsForTest() {
return mNextBackoffMs;
}
@VisibleForTesting
long getReconnectTimeForTest() {
return mReconnectTime;
}
@VisibleForTesting
ServiceConnection getServiceConnectionForTest() {
return mServiceConnection;
}
@VisibleForTesting
Runnable getBindForBackoffRunnableForTest() {
return mBindForBackoffRunnable;
}
@VisibleForTesting
Runnable getStableCheckRunnableForTest() {
return mStableCheck;
}
@VisibleForTesting
boolean shouldBeBoundForTest() {
return mShouldBeBound;
}
}