/* * 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 mTransportWhitelist; private final TransportClientManager mTransportClientManager; private final TransportStats mTransportStats; private OnTransportRegisteredListener mOnTransportRegisteredListener = (c, n) -> {}; /** * Lock for registered transports and currently selected transport. * *

Warning: 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 mRegisteredTransportsDescriptionMap = new ArrayMap<>(); @GuardedBy("mTransportLock") @Nullable private volatile String mCurrentTransportName; TransportManager(Context context, Set 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 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 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. * *

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 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}. * *

Warning: 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 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 entry = getRegisteredTransportEntryLocked(transportName); return (entry == null) ? null : entry.getKey(); } @GuardedBy("mTransportLock") @Nullable private TransportDescription getRegisteredTransportDescriptionLocked(String transportName) { Map.Entry entry = getRegisteredTransportEntryLocked(transportName); return (entry == null) ? null : entry.getValue(); } @GuardedBy("mTransportLock") @Nullable private Map.Entry getRegisteredTransportEntryLocked( String transportName) { for (Map.Entry 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. * *

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. * *

{@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 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 transportComponentFilter) { List 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}. * *

Warning: 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 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; } } }