blob: a2b5cb883b8a0fbfd748908c682d9c5b28136c47 [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.backup;
import android.annotation.Nullable;
import android.app.backup.BackupManager;
import android.app.backup.BackupTransport;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
import com.android.server.EventLogTags;
import com.android.server.backup.transport.TransportClient;
import com.android.server.backup.transport.TransportClientManager;
import com.android.server.backup.transport.TransportConnectionListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Handles in-memory bookkeeping of all BackupTransport objects.
*/
public class TransportManager {
private static final String TAG = "BackupTransportManager";
@VisibleForTesting
public static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST";
private static final long REBINDING_TIMEOUT_UNPROVISIONED_MS = 30 * 1000; // 30 sec
private static final long REBINDING_TIMEOUT_PROVISIONED_MS = 5 * 60 * 1000; // 5 mins
private static final int REBINDING_TIMEOUT_MSG = 1;
private final Intent mTransportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST);
private final Context mContext;
private final PackageManager mPackageManager;
private final Set<ComponentName> mTransportWhitelist;
private final Handler mHandler;
private final TransportClientManager mTransportClientManager;
/**
* This listener is called after we bind to any transport. If it returns true, this is a valid
* transport.
*/
private final TransportBoundListener mTransportBoundListener;
private String mCurrentTransportName;
/** Lock on this before accessing mValidTransports and mBoundTransports. */
private final Object mTransportLock = new Object();
/**
* We have detected these transports on the device. Unless in exceptional cases, we are also
* bound to all of these.
*/
@GuardedBy("mTransportLock")
private final Map<ComponentName, TransportConnection> mValidTransports = new ArrayMap<>();
/** We are currently bound to these transports. */
@GuardedBy("mTransportLock")
private final Map<String, ComponentName> mBoundTransports = new ArrayMap<>();
/** Names of transports we've bound to at least once */
@GuardedBy("mTransportLock")
private final Map<String, ComponentName> mTransportsByName = new ArrayMap<>();
/**
* Callback interface for {@link #ensureTransportReady(ComponentName, TransportReadyCallback)}.
*/
public interface TransportReadyCallback {
/**
* Will be called when the transport is ready.
*/
void onSuccess(String transportName);
/**
* Will be called when it's not possible to make transport ready.
*/
void onFailure(int reason);
}
TransportManager(Context context, Set<ComponentName> whitelist, String defaultTransport,
TransportBoundListener listener, Looper looper) {
mContext = context;
mPackageManager = context.getPackageManager();
if (whitelist != null) {
mTransportWhitelist = whitelist;
} else {
mTransportWhitelist = new ArraySet<>();
}
mCurrentTransportName = defaultTransport;
mTransportBoundListener = listener;
mHandler = new RebindOnTimeoutHandler(looper);
mTransportClientManager = new TransportClientManager(context);
}
void onPackageAdded(String packageName) {
// New package added. Bind to all transports it contains.
synchronized (mTransportLock) {
log_verbose("Package added. Binding to all transports. " + packageName);
bindToAllInternal(packageName, null /* all components */);
}
}
void onPackageRemoved(String packageName) {
// Package removed. Remove all its transports from our list. These transports have already
// been removed from mBoundTransports because onServiceDisconnected would already been
// called on TransportConnection objects.
synchronized (mTransportLock) {
Iterator<Map.Entry<ComponentName, TransportConnection>> iter =
mValidTransports.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<ComponentName, TransportConnection> validTransport = iter.next();
ComponentName componentName = validTransport.getKey();
if (componentName.getPackageName().equals(packageName)) {
TransportConnection transportConnection = validTransport.getValue();
iter.remove();
if (transportConnection != null) {
mContext.unbindService(transportConnection);
log_verbose("Package removed, removing transport: "
+ componentName.flattenToShortString());
}
}
}
}
}
void onPackageChanged(String packageName, String[] components) {
synchronized (mTransportLock) {
// Remove all changed components from mValidTransports. We'll bind to them again
// and re-add them if still valid.
for (String component : components) {
ComponentName componentName = new ComponentName(packageName, component);
TransportConnection removed = mValidTransports.remove(componentName);
if (removed != null) {
mContext.unbindService(removed);
log_verbose("Package changed. Removing transport: " +
componentName.flattenToShortString());
}
}
bindToAllInternal(packageName, components);
}
}
public IBackupTransport getTransportBinder(String transportName) {
synchronized (mTransportLock) {
ComponentName component = mBoundTransports.get(transportName);
if (component == null) {
Slog.w(TAG, "Transport " + transportName + " not bound.");
return null;
}
TransportConnection conn = mValidTransports.get(component);
if (conn == null) {
Slog.w(TAG, "Transport " + transportName + " not valid.");
return null;
}
return conn.getBinder();
}
}
public IBackupTransport getCurrentTransportBinder() {
return getTransportBinder(mCurrentTransportName);
}
public String getTransportName(IBackupTransport binder) {
synchronized (mTransportLock) {
for (TransportConnection conn : mValidTransports.values()) {
if (conn.getBinder() == binder) {
return conn.getName();
}
}
}
return null;
}
/**
* Returns the transport name associated with {@param transportClient} or {@code null} if not
* found.
*/
@Nullable
public String getTransportName(TransportClient transportClient) {
ComponentName transportComponent = transportClient.getTransportComponent();
synchronized (mTransportLock) {
for (Map.Entry<String, ComponentName> transportEntry : mTransportsByName.entrySet()) {
if (transportEntry.getValue().equals(transportComponent)) {
return transportEntry.getKey();
}
}
return null;
}
}
/**
* Returns a {@link TransportClient} for {@param transportName} or {@code null} if not found.
*
* @param transportName The name of the transport as returned by {@link BackupTransport#name()}.
* @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
* {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
* details.
* @return A {@link TransportClient} or null if not found.
*/
@Nullable
public TransportClient getTransportClient(String transportName, String caller) {
ComponentName transportComponent = mTransportsByName.get(transportName);
if (transportComponent == null) {
Slog.w(TAG, "Transport " + transportName + " not registered");
return null;
}
return mTransportClientManager.getTransportClient(transportComponent, caller);
}
/**
* Returns a {@link TransportClient} for the current transport or null if not found.
*
* @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
* {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
* details.
* @return A {@link TransportClient} or null if not found.
*/
@Nullable
public TransportClient getCurrentTransportClient(String caller) {
return getTransportClient(mCurrentTransportName, caller);
}
/**
* Disposes of the {@link TransportClient}.
*
* @param transportClient The {@link TransportClient} to be disposed of.
* @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
* {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
* details.
*/
public void disposeOfTransportClient(TransportClient transportClient, String caller) {
mTransportClientManager.disposeOfTransportClient(transportClient, caller);
}
String[] getBoundTransportNames() {
synchronized (mTransportLock) {
return mBoundTransports.keySet().toArray(new String[mBoundTransports.size()]);
}
}
ComponentName[] getAllTransportComponents() {
synchronized (mTransportLock) {
return mValidTransports.keySet().toArray(new ComponentName[mValidTransports.size()]);
}
}
String getCurrentTransportName() {
return mCurrentTransportName;
}
Set<ComponentName> getTransportWhitelist() {
return mTransportWhitelist;
}
String selectTransport(String transport) {
synchronized (mTransportLock) {
String prevTransport = mCurrentTransportName;
mCurrentTransportName = transport;
return prevTransport;
}
}
void ensureTransportReady(ComponentName transportComponent,
TransportReadyCallback listener) {
synchronized (mTransportLock) {
TransportConnection conn = mValidTransports.get(transportComponent);
if (conn == null) {
listener.onFailure(BackupManager.ERROR_TRANSPORT_UNAVAILABLE);
return;
}
// Transport can be unbound if the process hosting it crashed.
conn.bindIfUnbound();
conn.addListener(listener);
}
}
void registerAllTransports() {
bindToAllInternal(null /* all packages */, null /* all components */);
}
/**
* Bind to all transports belonging to the given package and the given component list.
* null acts a wildcard.
*
* If packageName is null, bind to all transports in all packages.
* If components is null, bind to all transports in the given package.
*/
private void bindToAllInternal(String packageName, String[] components) {
PackageInfo pkgInfo = null;
if (packageName != null) {
try {
pkgInfo = mPackageManager.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Package not found: " + packageName);
return;
}
}
Intent intent = new Intent(mTransportServiceIntent);
if (packageName != null) {
intent.setPackage(packageName);
}
List<ResolveInfo> hosts = mPackageManager.queryIntentServicesAsUser(
intent, 0, UserHandle.USER_SYSTEM);
if (hosts != null) {
for (ResolveInfo host : hosts) {
final ComponentName infoComponentName = getComponentName(host.serviceInfo);
boolean shouldBind = false;
if (components != null && packageName != null) {
for (String component : components) {
ComponentName cn = new ComponentName(pkgInfo.packageName, component);
if (infoComponentName.equals(cn)) {
shouldBind = true;
break;
}
}
} else {
shouldBind = true;
}
if (shouldBind && isTransportTrusted(infoComponentName)) {
tryBindTransport(infoComponentName);
}
}
}
}
/** Transport has to be whitelisted and privileged. */
private boolean isTransportTrusted(ComponentName transport) {
if (!mTransportWhitelist.contains(transport)) {
Slog.w(TAG, "BackupTransport " + transport.flattenToShortString() +
" not whitelisted.");
return false;
}
try {
PackageInfo packInfo = mPackageManager.getPackageInfo(transport.getPackageName(), 0);
if ((packInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED)
== 0) {
Slog.w(TAG, "Transport package " + transport.getPackageName() + " not privileged");
return false;
}
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Package not found.", e);
return false;
}
return true;
}
private void tryBindTransport(ComponentName transportComponentName) {
Slog.d(TAG, "Binding to transport: " + transportComponentName.flattenToShortString());
// TODO: b/22388012 (Multi user backup and restore)
TransportConnection connection = new TransportConnection(transportComponentName);
if (bindToTransport(transportComponentName, connection)) {
synchronized (mTransportLock) {
mValidTransports.put(transportComponentName, connection);
}
} else {
Slog.w(TAG, "Couldn't bind to transport " + transportComponentName);
}
}
private boolean bindToTransport(ComponentName componentName, ServiceConnection connection) {
Intent intent = new Intent(mTransportServiceIntent)
.setComponent(componentName);
return mContext.bindServiceAsUser(intent, connection, Context.BIND_AUTO_CREATE,
createSystemUserHandle());
}
private class TransportConnection implements ServiceConnection {
// Hold mTransportsLock to access these fields so as to provide a consistent view of them.
private volatile IBackupTransport mBinder;
private final List<TransportReadyCallback> mListeners = new ArrayList<>();
private volatile String mTransportName;
private final ComponentName mTransportComponent;
private TransportConnection(ComponentName transportComponent) {
mTransportComponent = transportComponent;
}
@Override
public void onServiceConnected(ComponentName component, IBinder binder) {
synchronized (mTransportLock) {
mBinder = IBackupTransport.Stub.asInterface(binder);
boolean success = false;
EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE,
component.flattenToShortString(), 1);
try {
mTransportName = mBinder.name();
// BackupManager requests some fields from the transport. If they are
// invalid, throw away this transport.
success = mTransportBoundListener.onTransportBound(mBinder);
} catch (RemoteException e) {
success = false;
Slog.e(TAG, "Couldn't get transport name.", e);
} finally {
// we need to intern() the String of the component, so that we can use it with
// Handler's removeMessages(), which uses == operator to compare the tokens
String componentShortString = component.flattenToShortString().intern();
if (success) {
Slog.d(TAG, "Bound to transport: " + componentShortString);
mTransportsByName.put(mTransportName, component);
mBoundTransports.put(mTransportName, component);
for (TransportReadyCallback listener : mListeners) {
listener.onSuccess(mTransportName);
}
// cancel rebinding on timeout for this component as we've already connected
mHandler.removeMessages(REBINDING_TIMEOUT_MSG, componentShortString);
} else {
Slog.w(TAG, "Bound to transport " + componentShortString +
" but it is invalid");
EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE,
componentShortString, 0);
mContext.unbindService(this);
mValidTransports.remove(component);
mBinder = null;
for (TransportReadyCallback listener : mListeners) {
listener.onFailure(BackupManager.ERROR_TRANSPORT_INVALID);
}
}
mListeners.clear();
}
}
}
@Override
public void onServiceDisconnected(ComponentName component) {
synchronized (mTransportLock) {
mBinder = null;
mBoundTransports.remove(mTransportName);
}
String componentShortString = component.flattenToShortString();
EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, componentShortString, 0);
Slog.w(TAG, "Disconnected from transport " + componentShortString);
scheduleRebindTimeout(component);
}
/**
* We'll attempt to explicitly rebind to a transport if it hasn't happened automatically
* for a few minutes after the binding went away.
*/
private void scheduleRebindTimeout(ComponentName component) {
// we need to intern() the String of the component, so that we can use it with Handler's
// removeMessages(), which uses == operator to compare the tokens
final String componentShortString = component.flattenToShortString().intern();
final long rebindTimeout = getRebindTimeout();
mHandler.removeMessages(REBINDING_TIMEOUT_MSG, componentShortString);
Message msg = mHandler.obtainMessage(REBINDING_TIMEOUT_MSG);
msg.obj = componentShortString;
mHandler.sendMessageDelayed(msg, rebindTimeout);
Slog.d(TAG, "Scheduled explicit rebinding for " + componentShortString + " in "
+ rebindTimeout + "ms");
}
// Intentionally not synchronized -- the variable is volatile and changes to its value
// are inside synchronized blocks, providing a memory sync barrier; and this method
// does not touch any other state protected by that lock.
private IBackupTransport getBinder() {
return mBinder;
}
// Intentionally not synchronized; same as getBinder()
private String getName() {
return mTransportName;
}
// Intentionally not synchronized; same as getBinder()
private void bindIfUnbound() {
if (mBinder == null) {
Slog.d(TAG,
"Rebinding to transport " + mTransportComponent.flattenToShortString());
bindToTransport(mTransportComponent, this);
}
}
private void addListener(TransportReadyCallback listener) {
synchronized (mTransportLock) {
if (mBinder == null) {
// We are waiting for bind to complete. If mBinder is set to null after the bind
// is complete due to transport being invalid, we won't find 'this' connection
// object in mValidTransports list and this function can't be called.
mListeners.add(listener);
} else {
listener.onSuccess(mTransportName);
}
}
}
private long getRebindTimeout() {
final boolean isDeviceProvisioned = Settings.Global.getInt(
mContext.getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0) != 0;
return isDeviceProvisioned
? REBINDING_TIMEOUT_PROVISIONED_MS
: REBINDING_TIMEOUT_UNPROVISIONED_MS;
}
}
public interface TransportBoundListener {
/** Should return true if this is a valid transport. */
boolean onTransportBound(IBackupTransport binder);
}
private class RebindOnTimeoutHandler extends Handler {
RebindOnTimeoutHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == REBINDING_TIMEOUT_MSG) {
String componentShortString = (String) msg.obj;
ComponentName transportComponent =
ComponentName.unflattenFromString(componentShortString);
synchronized (mTransportLock) {
if (mBoundTransports.containsValue(transportComponent)) {
Slog.d(TAG, "Explicit rebinding timeout passed, but already bound to "
+ componentShortString + " so not attempting to rebind");
return;
}
Slog.d(TAG, "Explicit rebinding timeout passed, attempting rebinding to: "
+ componentShortString);
// unbind the existing (broken) connection
TransportConnection conn = mValidTransports.get(transportComponent);
if (conn != null) {
mContext.unbindService(conn);
Slog.d(TAG, "Unbinding the existing (broken) connection to transport: "
+ componentShortString);
}
}
// rebind to transport
tryBindTransport(transportComponent);
} else {
Slog.e(TAG, "Unknown message sent to RebindOnTimeoutHandler, msg.what: "
+ msg.what);
}
}
}
private static void log_verbose(String message) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Slog.v(TAG, message);
}
}
// These only exists to make it testable with Robolectric, which is not updated to API level 24
// yet.
// TODO: Get rid of this once Robolectric is updated.
private static ComponentName getComponentName(ServiceInfo serviceInfo) {
return new ComponentName(serviceInfo.packageName, serviceInfo.name);
}
// These only exists to make it testable with Robolectric, which is not updated to API level 24
// yet.
// TODO: Get rid of this once Robolectric is updated.
public static UserHandle createSystemUserHandle() {
return new UserHandle(UserHandle.USER_SYSTEM);
}
}