blob: c5553881cf6cd2d4d5491f019416311b60c99d01 [file] [log] [blame]
/**
* Copyright (c) 2016, 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.utils;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
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.IBinder.DeathRecipient;
import android.os.IInterface;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Slog;
import java.text.SimpleDateFormat;
import java.util.Objects;
import java.util.Date;
/**
* Manages the lifecycle of an application-provided service bound from system server.
*
* @hide
*/
public class ManagedApplicationService {
private final String TAG = getClass().getSimpleName();
/**
* Attempt to reconnect service forever if an onBindingDied or onServiceDisconnected event
* is received.
*/
public static final int RETRY_FOREVER = 1;
/**
* Never attempt to reconnect the service - a single onBindingDied or onServiceDisconnected
* event will cause this to fully unbind the service and never attempt to reconnect.
*/
public static final int RETRY_NEVER = 2;
/**
* Attempt to reconnect the service until the maximum number of retries is reached, then stop.
*
* The first retry will occur MIN_RETRY_DURATION_MS after the disconnection, and each
* subsequent retry will occur after 2x the duration used for the previous retry up to the
* MAX_RETRY_DURATION_MS duration.
*
* In this case, retries mean a full unbindService/bindService pair to handle cases when the
* usual service re-connection logic in ActiveServices has very high backoff times or when the
* serviceconnection has fully died due to a package update or similar.
*/
public static final int RETRY_BEST_EFFORT = 3;
// Maximum number of retries before giving up (for RETRY_BEST_EFFORT).
private static final int MAX_RETRY_COUNT = 4;
// Max time between retry attempts.
private static final long MAX_RETRY_DURATION_MS = 16000;
// Min time between retry attempts.
private static final long MIN_RETRY_DURATION_MS = 2000;
// Time since the last retry attempt after which to clear the retry attempt counter.
private static final long RETRY_RESET_TIME_MS = MAX_RETRY_DURATION_MS * 4;
private final Context mContext;
private final int mUserId;
private final ComponentName mComponent;
private final int mClientLabel;
private final String mSettingsAction;
private final BinderChecker mChecker;
private final boolean mIsImportant;
private final int mRetryType;
private final Handler mHandler;
private final Runnable mRetryRunnable = this::doRetry;
private final EventCallback mEventCb;
private final Object mLock = new Object();
// State protected by mLock
private ServiceConnection mConnection;
private IInterface mBoundInterface;
private PendingEvent mPendingEvent;
private int mRetryCount;
private long mLastRetryTimeMs;
private long mNextRetryDurationMs = MIN_RETRY_DURATION_MS;
private boolean mRetrying;
public static interface LogFormattable {
String toLogString(SimpleDateFormat dateFormat);
}
/**
* Lifecycle event of this managed service.
*/
public static class LogEvent implements LogFormattable {
public static final int EVENT_CONNECTED = 1;
public static final int EVENT_DISCONNECTED = 2;
public static final int EVENT_BINDING_DIED = 3;
public static final int EVENT_STOPPED_PERMANENTLY = 4;
// Time of the events in "current time ms" timebase.
public final long timestamp;
// Name of the component for this system service.
public final ComponentName component;
// ID of the event that occurred.
public final int event;
public LogEvent(long timestamp, ComponentName component, int event) {
this.timestamp = timestamp;
this.component = component;
this.event = event;
}
@Override
public String toLogString(SimpleDateFormat dateFormat) {
return dateFormat.format(new Date(timestamp)) + " " + eventToString(event)
+ " Managed Service: "
+ ((component == null) ? "None" : component.flattenToString());
}
public static String eventToString(int event) {
switch (event) {
case EVENT_CONNECTED:
return "Connected";
case EVENT_DISCONNECTED:
return "Disconnected";
case EVENT_BINDING_DIED:
return "Binding Died For";
case EVENT_STOPPED_PERMANENTLY:
return "Permanently Stopped";
default:
return "Unknown Event Occurred";
}
}
}
private ManagedApplicationService(final Context context, final ComponentName component,
final int userId, int clientLabel, String settingsAction,
BinderChecker binderChecker, boolean isImportant, int retryType, Handler handler,
EventCallback eventCallback) {
mContext = context;
mComponent = component;
mUserId = userId;
mClientLabel = clientLabel;
mSettingsAction = settingsAction;
mChecker = binderChecker;
mIsImportant = isImportant;
mRetryType = retryType;
mHandler = handler;
mEventCb = eventCallback;
}
/**
* Implement to validate returned IBinder instance.
*/
public interface BinderChecker {
IInterface asInterface(IBinder binder);
boolean checkType(IInterface service);
}
/**
* Implement to call IInterface methods after service is connected.
*/
public interface PendingEvent {
void runEvent(IInterface service) throws RemoteException;
}
/**
* Implement to be notified about any problems with remote service.
*/
public interface EventCallback {
/**
* Called when an sevice lifecycle event occurs.
*/
void onServiceEvent(LogEvent event);
}
/**
* Create a new ManagedApplicationService object but do not yet bind to the user service.
*
* @param context a Context to use for binding the application service.
* @param component the {@link ComponentName} of the application service to bind.
* @param userId the user ID of user to bind the application service as.
* @param clientLabel the resource ID of a label displayed to the user indicating the
* binding service, or 0 if none is desired.
* @param settingsAction an action that can be used to open the Settings UI to enable/disable
* binding to these services, or null if none is desired.
* @param binderChecker an interface used to validate the returned binder object, or null if
* this interface is unchecked.
* @param isImportant bind the user service with BIND_IMPORTANT.
* @param retryType reconnect behavior to have when bound service is disconnected.
* @param handler the Handler to use for retries and delivering EventCallbacks.
* @param eventCallback a callback used to deliver disconnection events, or null if you
* don't care.
* @return a ManagedApplicationService instance.
*/
public static ManagedApplicationService build(@NonNull final Context context,
@NonNull final ComponentName component, final int userId, int clientLabel,
@Nullable String settingsAction, @Nullable BinderChecker binderChecker,
boolean isImportant, int retryType, @NonNull Handler handler,
@Nullable EventCallback eventCallback) {
return new ManagedApplicationService(context, component, userId, clientLabel,
settingsAction, binderChecker, isImportant, retryType, handler, eventCallback);
}
/**
* @return the user ID of the user that owns the bound service.
*/
public int getUserId() {
return mUserId;
}
/**
* @return the component of the bound service.
*/
public ComponentName getComponent() {
return mComponent;
}
/**
* Asynchronously unbind from the application service if the bound service component and user
* does not match the given signature.
*
* @param componentName the component that must match.
* @param userId the user ID that must match.
* @return {@code true} if not matching.
*/
public boolean disconnectIfNotMatching(final ComponentName componentName, final int userId) {
if (matches(componentName, userId)) {
return false;
}
disconnect();
return true;
}
/**
* Send an event to run as soon as the binder interface is available.
*
* @param event a {@link PendingEvent} to send.
*/
public void sendEvent(@NonNull PendingEvent event) {
IInterface iface;
synchronized (mLock) {
iface = mBoundInterface;
if (iface == null) {
mPendingEvent = event;
}
}
if (iface != null) {
try {
event.runEvent(iface);
} catch (RuntimeException | RemoteException ex) {
Slog.e(TAG, "Received exception from user service: ", ex);
}
}
}
/**
* Asynchronously unbind from the application service if bound.
*/
public void disconnect() {
synchronized (mLock) {
// Unbind existing connection, if it exists
if (mConnection == null) {
return;
}
mContext.unbindService(mConnection);
mConnection = null;
mBoundInterface = null;
}
}
/**
* Asynchronously bind to the application service if not bound.
*/
public void connect() {
synchronized (mLock) {
if (mConnection != null) {
// We're already connected or are trying to connect
return;
}
Intent intent = new Intent().setComponent(mComponent);
if (mClientLabel != 0) {
intent.putExtra(Intent.EXTRA_CLIENT_LABEL, mClientLabel);
}
if (mSettingsAction != null) {
intent.putExtra(Intent.EXTRA_CLIENT_INTENT,
PendingIntent.getActivity(mContext, 0, new Intent(mSettingsAction), 0));
}
mConnection = new ServiceConnection() {
@Override
public void onBindingDied(ComponentName componentName) {
final long timestamp = System.currentTimeMillis();
Slog.w(TAG, "Service binding died: " + componentName);
synchronized (mLock) {
if (mConnection != this) {
return;
}
mHandler.post(() -> {
mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
LogEvent.EVENT_BINDING_DIED));
});
mBoundInterface = null;
startRetriesLocked();
}
}
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
final long timestamp = System.currentTimeMillis();
Slog.i(TAG, "Service connected: " + componentName);
IInterface iface = null;
PendingEvent pendingEvent = null;
synchronized (mLock) {
if (mConnection != this) {
// Must've been unbound.
return;
}
mHandler.post(() -> {
mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
LogEvent.EVENT_CONNECTED));
});
stopRetriesLocked();
mBoundInterface = null;
if (mChecker != null) {
mBoundInterface = mChecker.asInterface(iBinder);
if (!mChecker.checkType(mBoundInterface)) {
// Received an invalid binder, disconnect.
mBoundInterface = null;
Slog.w(TAG, "Invalid binder from " + componentName);
startRetriesLocked();
return;
}
iface = mBoundInterface;
pendingEvent = mPendingEvent;
mPendingEvent = null;
}
}
if (iface != null && pendingEvent != null) {
try {
pendingEvent.runEvent(iface);
} catch (RuntimeException | RemoteException ex) {
Slog.e(TAG, "Received exception from user service: ", ex);
startRetriesLocked();
}
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
final long timestamp = System.currentTimeMillis();
Slog.w(TAG, "Service disconnected: " + componentName);
synchronized (mLock) {
if (mConnection != this) {
return;
}
mHandler.post(() -> {
mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
LogEvent.EVENT_DISCONNECTED));
});
mBoundInterface = null;
startRetriesLocked();
}
}
};
int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
if (mIsImportant) {
flags |= Context.BIND_IMPORTANT;
}
try {
if (!mContext.bindServiceAsUser(intent, mConnection, flags,
new UserHandle(mUserId))) {
Slog.w(TAG, "Unable to bind service: " + intent);
startRetriesLocked();
}
} catch (SecurityException e) {
Slog.w(TAG, "Unable to bind service: " + intent, e);
startRetriesLocked();
}
}
}
private boolean matches(final ComponentName component, final int userId) {
return Objects.equals(mComponent, component) && mUserId == userId;
}
private void startRetriesLocked() {
if (checkAndDeliverServiceDiedCbLocked()) {
// If we delivered the service callback, disconnect and stop retrying.
disconnect();
return;
}
if (mRetrying) {
// Retry already queued, don't queue a new one.
return;
}
mRetrying = true;
queueRetryLocked();
}
private void stopRetriesLocked() {
mRetrying = false;
mHandler.removeCallbacks(mRetryRunnable);
}
private void queueRetryLocked() {
long now = SystemClock.uptimeMillis();
if ((now - mLastRetryTimeMs) > RETRY_RESET_TIME_MS) {
// It's been longer than the reset time since we last had to retry. Re-initialize.
mNextRetryDurationMs = MIN_RETRY_DURATION_MS;
mRetryCount = 0;
}
mLastRetryTimeMs = now;
mHandler.postDelayed(mRetryRunnable, mNextRetryDurationMs);
mNextRetryDurationMs = Math.min(2 * mNextRetryDurationMs, MAX_RETRY_DURATION_MS);
mRetryCount++;
}
private boolean checkAndDeliverServiceDiedCbLocked() {
if (mRetryType == RETRY_NEVER || (mRetryType == RETRY_BEST_EFFORT
&& mRetryCount >= MAX_RETRY_COUNT)) {
// If we never retry, or we've exhausted our retries, post the onServiceDied callback.
Slog.e(TAG, "Service " + mComponent + " has died too much, not retrying.");
if (mEventCb != null) {
final long timestamp = System.currentTimeMillis();
mHandler.post(() -> {
mEventCb.onServiceEvent(new LogEvent(timestamp, mComponent,
LogEvent.EVENT_STOPPED_PERMANENTLY));
});
}
return true;
}
return false;
}
private void doRetry() {
synchronized (mLock) {
if (mConnection == null) {
// We disconnected for good. Don't attempt to retry.
return;
}
if (!mRetrying) {
// We successfully connected. Don't attempt to retry.
return;
}
Slog.i(TAG, "Attempting to reconnect " + mComponent + "...");
// While frameworks may restart the remote Service if we stay bound, we have little
// control of the backoff timing for reconnecting the service. In the event of a
// process crash, the backoff time can be very large (1-30 min), which is not
// acceptable for the types of services this is used for. Instead force an unbind/bind
// sequence to cause a more immediate retry.
disconnect();
if (checkAndDeliverServiceDiedCbLocked()) {
// No more retries.
return;
}
queueRetryLocked();
connect();
}
}
}