blob: da1f32c9d20355ac42ac56b78a124d79576f0dbd [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.app.backup.BackupManager;
import android.app.backup.SelectBackupTransportCallback;
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.backup.IBackupTransport;
import com.android.server.EventLogTags;
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.
*/
class TransportManager {
private static final String TAG = "BackupTransportManager";
private 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;
/**
* 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<>();
TransportManager(Context context, Set<ComponentName> whitelist, String defaultTransport,
TransportBoundListener listener, Looper looper) {
mContext = context;
mPackageManager = context.getPackageManager();
mTransportWhitelist = (whitelist != null) ? whitelist : new ArraySet<>();
mCurrentTransportName = defaultTransport;
mTransportBoundListener = listener;
mHandler = new RebindOnTimeoutHandler(looper);
}
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);
}
}
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();
}
}
IBackupTransport getCurrentTransportBinder() {
return getTransportBinder(mCurrentTransportName);
}
String getTransportName(IBackupTransport binder) {
synchronized (mTransportLock) {
for (TransportConnection conn : mValidTransports.values()) {
if (conn.getBinder() == binder) {
return conn.getName();
}
}
}
return null;
}
String[] getBoundTransportNames() {
synchronized (mTransportLock) {
return mBoundTransports.keySet().toArray(new String[mBoundTransports.size()]);
}
}
ComponentName[] getAllTransportCompenents() {
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, SelectBackupTransportCallback 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 = host.serviceInfo.getComponentName();
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,
UserHandle.SYSTEM);
}
private class TransportConnection implements ServiceConnection {
// Hold mTransportsLock to access these fields so as to provide a consistent view of them.
private IBackupTransport mBinder;
private final List<SelectBackupTransportCallback> mListeners = new ArrayList<>();
private 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);
mBoundTransports.put(mTransportName, component);
for (SelectBackupTransportCallback 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 (SelectBackupTransportCallback 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");
}
private IBackupTransport getBinder() {
synchronized (mTransportLock) {
return mBinder;
}
}
private String getName() {
synchronized (mTransportLock) {
return mTransportName;
}
}
private void bindIfUnbound() {
synchronized (mTransportLock) {
if (mBinder == null) {
Slog.d(TAG,
"Rebinding to transport " + mTransportComponent.flattenToShortString());
bindToTransport(mTransportComponent, this);
}
}
}
private void addListener(SelectBackupTransportCallback 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;
}
}
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);
}
}
}