blob: 6a1bf178ad6034ccd79edabaa534b6ae6565daca [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.annotation.WorkerThread;
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.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
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.internal.util.Preconditions;
import com.android.server.backup.transport.OnTransportRegisteredListener;
import com.android.server.backup.transport.TransportClient;
import com.android.server.backup.transport.TransportClientManager;
import com.android.server.backup.transport.TransportConnectionListener;
import com.android.server.backup.transport.TransportNotAvailableException;
import com.android.server.backup.transport.TransportNotRegisteredException;
import com.android.server.backup.transport.TransportStats;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
/** 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 final Intent mTransportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST);
private final Context mContext;
private final PackageManager mPackageManager;
private final Set<ComponentName> mTransportWhitelist;
private final TransportClientManager mTransportClientManager;
private final TransportStats mTransportStats;
private OnTransportRegisteredListener mOnTransportRegisteredListener = (c, n) -> {};
/**
* Lock for registered transports and currently selected transport.
*
* <p><b>Warning:</b> No calls to {@link IBackupTransport} or calls that result in transport
* code being executed such as {@link TransportClient#connect(String)}} and its variants should
* be made with this lock held, risk of deadlock.
*/
private final Object mTransportLock = new Object();
/** @see #getRegisteredTransportNames() */
@GuardedBy("mTransportLock")
private final Map<ComponentName, TransportDescription> mRegisteredTransportsDescriptionMap =
new ArrayMap<>();
@GuardedBy("mTransportLock")
@Nullable
private volatile String mCurrentTransportName;
TransportManager(Context context, Set<ComponentName> whitelist, String selectedTransport) {
mContext = context;
mPackageManager = context.getPackageManager();
mTransportWhitelist = Preconditions.checkNotNull(whitelist);
mCurrentTransportName = selectedTransport;
mTransportStats = new TransportStats();
mTransportClientManager = new TransportClientManager(context, mTransportStats);
}
@VisibleForTesting
TransportManager(
Context context,
Set<ComponentName> whitelist,
String selectedTransport,
TransportClientManager transportClientManager) {
mContext = context;
mPackageManager = context.getPackageManager();
mTransportWhitelist = Preconditions.checkNotNull(whitelist);
mCurrentTransportName = selectedTransport;
mTransportStats = new TransportStats();
mTransportClientManager = transportClientManager;
}
/* Sets a listener to be called whenever a transport is registered. */
public void setOnTransportRegisteredListener(OnTransportRegisteredListener listener) {
mOnTransportRegisteredListener = listener;
}
@WorkerThread
void onPackageAdded(String packageName) {
registerTransportsFromPackage(packageName, transportComponent -> true);
}
void onPackageRemoved(String packageName) {
synchronized (mTransportLock) {
mRegisteredTransportsDescriptionMap.keySet().removeIf(fromPackageFilter(packageName));
}
}
@WorkerThread
void onPackageChanged(String packageName, String... components) {
// Unfortunately this can't be atomic because we risk a deadlock if
// registerTransportsFromPackage() is put inside the synchronized block
Set<ComponentName> transportComponents = new ArraySet<>(components.length);
for (String componentName : components) {
transportComponents.add(new ComponentName(packageName, componentName));
}
synchronized (mTransportLock) {
mRegisteredTransportsDescriptionMap.keySet().removeIf(transportComponents::contains);
}
registerTransportsFromPackage(packageName, transportComponents::contains);
}
/**
* Returns the {@link ComponentName}s of the registered transports.
*
* <p>A *registered* transport is a transport that satisfies intent with action
* android.backup.TRANSPORT_HOST, returns true for {@link #isTransportTrusted(ComponentName)}
* and that we have successfully connected to once.
*/
ComponentName[] getRegisteredTransportComponents() {
synchronized (mTransportLock) {
return mRegisteredTransportsDescriptionMap
.keySet()
.toArray(new ComponentName[mRegisteredTransportsDescriptionMap.size()]);
}
}
/**
* Returns the names of the registered transports.
*
* @see #getRegisteredTransportComponents()
*/
String[] getRegisteredTransportNames() {
synchronized (mTransportLock) {
String[] transportNames = new String[mRegisteredTransportsDescriptionMap.size()];
int i = 0;
for (TransportDescription description : mRegisteredTransportsDescriptionMap.values()) {
transportNames[i] = description.name;
i++;
}
return transportNames;
}
}
/** Returns a set with the whitelisted transports. */
Set<ComponentName> getTransportWhitelist() {
return mTransportWhitelist;
}
@Nullable
String getCurrentTransportName() {
return mCurrentTransportName;
}
/**
* Returns the transport name associated with {@code transportComponent}.
*
* @throws TransportNotRegisteredException if the transport is not registered.
*/
public String getTransportName(ComponentName transportComponent)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportComponent).name;
}
}
/**
* Retrieves the transport dir name of {@code transportComponent}.
*
* @throws TransportNotRegisteredException if the transport is not registered.
*/
public String getTransportDirName(ComponentName transportComponent)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportComponent)
.transportDirName;
}
}
/**
* Retrieves the transport dir name of {@code transportName}.
*
* @throws TransportNotRegisteredException if the transport is not registered.
*/
public String getTransportDirName(String transportName) throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportName).transportDirName;
}
}
/**
* Retrieves the configuration intent of {@code transportName}.
*
* @throws TransportNotRegisteredException if the transport is not registered.
*/
@Nullable
public Intent getTransportConfigurationIntent(String transportName)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportName)
.configurationIntent;
}
}
/**
* Retrieves the current destination string of {@code transportName}.
*
* @throws TransportNotRegisteredException if the transport is not registered.
*/
public String getTransportCurrentDestinationString(String transportName)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportName)
.currentDestinationString;
}
}
/**
* Retrieves the data management intent of {@code transportName}.
*
* @throws TransportNotRegisteredException if the transport is not registered.
*/
@Nullable
public Intent getTransportDataManagementIntent(String transportName)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportName)
.dataManagementIntent;
}
}
/**
* Retrieves the data management label of {@code transportName}.
*
* @throws TransportNotRegisteredException if the transport is not registered.
*/
@Nullable
public String getTransportDataManagementLabel(String transportName)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportName)
.dataManagementLabel;
}
}
/* Returns true if the transport identified by {@code transportName} is registered. */
public boolean isTransportRegistered(String transportName) {
synchronized (mTransportLock) {
return getRegisteredTransportEntryLocked(transportName) != null;
}
}
/**
* Execute {@code transportConsumer} for each registered transport passing the transport name.
* This is called with an internal lock held, ensuring that the transport will remain registered
* while {@code transportConsumer} is being executed. Don't do heavy operations in {@code
* transportConsumer}.
*
* <p><b>Warning:</b> Do NOT make any calls to {@link IBackupTransport} or call any variants of
* {@link TransportClient#connect(String)} here, otherwise you risk deadlock.
*/
public void forEachRegisteredTransport(Consumer<String> transportConsumer) {
synchronized (mTransportLock) {
for (TransportDescription transportDescription :
mRegisteredTransportsDescriptionMap.values()) {
transportConsumer.accept(transportDescription.name);
}
}
}
/**
* Updates given values for the transport already registered and identified with {@param
* transportComponent}. If the transport is not registered it will log and return.
*/
public void updateTransportAttributes(
ComponentName transportComponent,
String name,
@Nullable Intent configurationIntent,
String currentDestinationString,
@Nullable Intent dataManagementIntent,
@Nullable String dataManagementLabel) {
synchronized (mTransportLock) {
TransportDescription description =
mRegisteredTransportsDescriptionMap.get(transportComponent);
if (description == null) {
Slog.e(TAG, "Transport " + name + " not registered tried to change description");
return;
}
description.name = name;
description.configurationIntent = configurationIntent;
description.currentDestinationString = currentDestinationString;
description.dataManagementIntent = dataManagementIntent;
description.dataManagementLabel = dataManagementLabel;
Slog.d(TAG, "Transport " + name + " updated its attributes");
}
}
@GuardedBy("mTransportLock")
private TransportDescription getRegisteredTransportDescriptionOrThrowLocked(
ComponentName transportComponent) throws TransportNotRegisteredException {
TransportDescription description =
mRegisteredTransportsDescriptionMap.get(transportComponent);
if (description == null) {
throw new TransportNotRegisteredException(transportComponent);
}
return description;
}
@GuardedBy("mTransportLock")
private TransportDescription getRegisteredTransportDescriptionOrThrowLocked(
String transportName) throws TransportNotRegisteredException {
TransportDescription description = getRegisteredTransportDescriptionLocked(transportName);
if (description == null) {
throw new TransportNotRegisteredException(transportName);
}
return description;
}
@GuardedBy("mTransportLock")
@Nullable
private ComponentName getRegisteredTransportComponentLocked(String transportName) {
Map.Entry<ComponentName, TransportDescription> entry =
getRegisteredTransportEntryLocked(transportName);
return (entry == null) ? null : entry.getKey();
}
@GuardedBy("mTransportLock")
@Nullable
private TransportDescription getRegisteredTransportDescriptionLocked(String transportName) {
Map.Entry<ComponentName, TransportDescription> entry =
getRegisteredTransportEntryLocked(transportName);
return (entry == null) ? null : entry.getValue();
}
@GuardedBy("mTransportLock")
@Nullable
private Map.Entry<ComponentName, TransportDescription> getRegisteredTransportEntryLocked(
String transportName) {
for (Map.Entry<ComponentName, TransportDescription> entry :
mRegisteredTransportsDescriptionMap.entrySet()) {
TransportDescription description = entry.getValue();
if (transportName.equals(description.name)) {
return entry;
}
}
return null;
}
/**
* Returns a {@link TransportClient} for {@code transportName} or {@code null} if not
* registered.
*
* @param transportName The name of the transport.
* @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 registered.
*/
@Nullable
public TransportClient getTransportClient(String transportName, String caller) {
try {
return getTransportClientOrThrow(transportName, caller);
} catch (TransportNotRegisteredException e) {
Slog.w(TAG, "Transport " + transportName + " not registered");
return null;
}
}
/**
* Returns a {@link TransportClient} for {@code transportName} or throws if not registered.
*
* @param transportName The name of the transport.
* @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}.
* @throws TransportNotRegisteredException if the transport is not registered.
*/
public TransportClient getTransportClientOrThrow(String transportName, String caller)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
ComponentName component = getRegisteredTransportComponentLocked(transportName);
if (component == null) {
throw new TransportNotRegisteredException(transportName);
}
return mTransportClientManager.getTransportClient(component, caller);
}
}
/**
* Returns a {@link TransportClient} for the current transport or {@code null} if not
* registered.
*
* @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 registered.
*/
@Nullable
public TransportClient getCurrentTransportClient(String caller) {
synchronized (mTransportLock) {
return getTransportClient(mCurrentTransportName, caller);
}
}
/**
* Returns a {@link TransportClient} for the current transport or throws if not registered.
*
* @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}.
* @throws TransportNotRegisteredException if the transport is not registered.
*/
public TransportClient getCurrentTransportClientOrThrow(String caller)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getTransportClientOrThrow(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);
}
/**
* Sets {@code transportName} as selected transport and returns previously selected transport
* name. If there was no previous transport it returns null.
*
* <p>You should NOT call this method in new code. This won't make any checks against {@code
* transportName}, putting any operation at risk of a {@link TransportNotRegisteredException} or
* another error at the time it's being executed.
*
* <p>{@link Deprecated} as public, this method can be used as private.
*/
@Deprecated
@Nullable
String selectTransport(String transportName) {
synchronized (mTransportLock) {
String prevTransport = mCurrentTransportName;
mCurrentTransportName = transportName;
return prevTransport;
}
}
/**
* Tries to register the transport if not registered. If successful also selects the transport.
*
* @param transportComponent Host of the transport.
* @return One of {@link BackupManager#SUCCESS}, {@link BackupManager#ERROR_TRANSPORT_INVALID}
* or {@link BackupManager#ERROR_TRANSPORT_UNAVAILABLE}.
*/
@WorkerThread
public int registerAndSelectTransport(ComponentName transportComponent) {
// If it's already registered we select and return
synchronized (mTransportLock) {
try {
selectTransport(getTransportName(transportComponent));
return BackupManager.SUCCESS;
} catch (TransportNotRegisteredException e) {
// Fall through and release lock
}
}
// We can't call registerTransport() with the transport lock held
int result = registerTransport(transportComponent);
if (result != BackupManager.SUCCESS) {
return result;
}
synchronized (mTransportLock) {
try {
selectTransport(getTransportName(transportComponent));
return BackupManager.SUCCESS;
} catch (TransportNotRegisteredException e) {
Slog.wtf(TAG, "Transport got unregistered");
return BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
}
}
}
@WorkerThread
public void registerTransports() {
registerTransportsForIntent(mTransportServiceIntent, transportComponent -> true);
}
@WorkerThread
private void registerTransportsFromPackage(
String packageName, Predicate<ComponentName> transportComponentFilter) {
try {
mPackageManager.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
Slog.e(TAG, "Trying to register transports from package not found " + packageName);
return;
}
registerTransportsForIntent(
new Intent(mTransportServiceIntent).setPackage(packageName),
transportComponentFilter.and(fromPackageFilter(packageName)));
}
@WorkerThread
private void registerTransportsForIntent(
Intent intent, Predicate<ComponentName> transportComponentFilter) {
List<ResolveInfo> hosts =
mPackageManager.queryIntentServicesAsUser(intent, 0, UserHandle.USER_SYSTEM);
if (hosts == null) {
return;
}
for (ResolveInfo host : hosts) {
ComponentName transportComponent = host.serviceInfo.getComponentName();
if (transportComponentFilter.test(transportComponent)
&& isTransportTrusted(transportComponent)) {
registerTransport(transportComponent);
}
}
}
/** 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;
}
/**
* Tries to register transport represented by {@code transportComponent}.
*
* <p><b>Warning:</b> Don't call this with the transport lock held.
*
* @param transportComponent Host of the transport that we want to register.
* @return One of {@link BackupManager#SUCCESS}, {@link BackupManager#ERROR_TRANSPORT_INVALID}
* or {@link BackupManager#ERROR_TRANSPORT_UNAVAILABLE}.
*/
@WorkerThread
private int registerTransport(ComponentName transportComponent) {
checkCanUseTransport();
if (!isTransportTrusted(transportComponent)) {
return BackupManager.ERROR_TRANSPORT_INVALID;
}
String transportString = transportComponent.flattenToShortString();
String callerLogString = "TransportManager.registerTransport()";
Bundle extras = new Bundle();
extras.putBoolean(BackupTransport.EXTRA_TRANSPORT_REGISTRATION, true);
TransportClient transportClient = mTransportClientManager.getTransportClient(
transportComponent, extras, callerLogString);
final IBackupTransport transport;
try {
transport = transportClient.connectOrThrow(callerLogString);
} catch (TransportNotAvailableException e) {
Slog.e(TAG, "Couldn't connect to transport " + transportString + " for registration");
mTransportClientManager.disposeOfTransportClient(transportClient, callerLogString);
return BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
}
int result;
try {
String transportName = transport.name();
String transportDirName = transport.transportDirName();
registerTransport(transportComponent, transport);
// If registerTransport() hasn't thrown...
Slog.d(TAG, "Transport " + transportString + " registered");
mOnTransportRegisteredListener.onTransportRegistered(transportName, transportDirName);
result = BackupManager.SUCCESS;
} catch (RemoteException e) {
Slog.e(TAG, "Transport " + transportString + " died while registering");
result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
}
mTransportClientManager.disposeOfTransportClient(transportClient, callerLogString);
return result;
}
/** If {@link RemoteException} is thrown the transport is guaranteed to not be registered. */
private void registerTransport(ComponentName transportComponent, IBackupTransport transport)
throws RemoteException {
checkCanUseTransport();
TransportDescription description =
new TransportDescription(
transport.name(),
transport.transportDirName(),
transport.configurationIntent(),
transport.currentDestinationString(),
transport.dataManagementIntent(),
transport.dataManagementLabel());
synchronized (mTransportLock) {
mRegisteredTransportsDescriptionMap.put(transportComponent, description);
}
}
private void checkCanUseTransport() {
Preconditions.checkState(
!Thread.holdsLock(mTransportLock), "Can't call transport with transport lock held");
}
public void dumpTransportClients(PrintWriter pw) {
mTransportClientManager.dump(pw);
}
public void dumpTransportStats(PrintWriter pw) {
mTransportStats.dump(pw);
}
private static Predicate<ComponentName> fromPackageFilter(String packageName) {
return transportComponent -> packageName.equals(transportComponent.getPackageName());
}
private static class TransportDescription {
private String name;
private final String transportDirName;
@Nullable private Intent configurationIntent;
private String currentDestinationString;
@Nullable private Intent dataManagementIntent;
@Nullable private String dataManagementLabel;
private TransportDescription(
String name,
String transportDirName,
@Nullable Intent configurationIntent,
String currentDestinationString,
@Nullable Intent dataManagementIntent,
@Nullable String dataManagementLabel) {
this.name = name;
this.transportDirName = transportDirName;
this.configurationIntent = configurationIntent;
this.currentDestinationString = currentDestinationString;
this.dataManagementIntent = dataManagementIntent;
this.dataManagementLabel = dataManagementLabel;
}
}
}