/**
|
* 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();
|
}
|
}
|
}
|