| /* |
| * 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 static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; |
| |
| import android.annotation.Nullable; |
| import android.app.backup.BackupManager; |
| 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 com.android.server.backup.transport.TransportNotRegisteredException; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| 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 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 TransportBoundListener mTransportBoundListener; |
| |
| 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<>(); |
| |
| /** @see #getEligibleTransportComponents() */ |
| @GuardedBy("mTransportLock") |
| private final Set<ComponentName> mEligibleTransports = new ArraySet<>(); |
| |
| /** @see #getRegisteredTransportNames() */ |
| @GuardedBy("mTransportLock") |
| private final Map<ComponentName, TransportDescription> mRegisteredTransportsDescriptionMap = |
| new ArrayMap<>(); |
| |
| @GuardedBy("mTransportLock") |
| private volatile String mCurrentTransportName; |
| |
| |
| /** |
| * 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) { |
| this(context, whitelist, defaultTransport, looper); |
| mTransportBoundListener = listener; |
| } |
| |
| TransportManager( |
| Context context, |
| Set<ComponentName> whitelist, |
| String defaultTransport, |
| Looper looper) { |
| mContext = context; |
| mPackageManager = context.getPackageManager(); |
| if (whitelist != null) { |
| mTransportWhitelist = whitelist; |
| } else { |
| mTransportWhitelist = new ArraySet<>(); |
| } |
| mCurrentTransportName = defaultTransport; |
| mHandler = new RebindOnTimeoutHandler(looper); |
| mTransportClientManager = new TransportClientManager(context); |
| } |
| |
| public void setTransportBoundListener(TransportBoundListener transportBoundListener) { |
| mTransportBoundListener = transportBoundListener; |
| } |
| |
| 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()); |
| } |
| } |
| } |
| removeTransportsIfLocked( |
| componentName -> packageName.equals(componentName.getPackageName())); |
| } |
| } |
| |
| 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. |
| Set<ComponentName> transportsToBeRemoved = new ArraySet<>(); |
| for (String component : components) { |
| ComponentName componentName = new ComponentName(packageName, component); |
| transportsToBeRemoved.add(componentName); |
| TransportConnection removed = mValidTransports.remove(componentName); |
| if (removed != null) { |
| mContext.unbindService(removed); |
| log_verbose("Package changed. Removing transport: " + |
| componentName.flattenToShortString()); |
| } |
| } |
| removeTransportsIfLocked(transportsToBeRemoved::contains); |
| bindToAllInternal(packageName, components); |
| } |
| } |
| |
| @GuardedBy("mTransportLock") |
| private void removeTransportsIfLocked(Predicate<ComponentName> filter) { |
| mEligibleTransports.removeIf(filter); |
| mRegisteredTransportsDescriptionMap.keySet().removeIf(filter); |
| } |
| |
| 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); |
| } |
| |
| /** |
| * Retrieve 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; |
| } |
| } |
| |
| /** |
| * Retrieve 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; |
| } |
| } |
| |
| /** |
| * Retrieve 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; |
| } |
| } |
| |
| /** |
| * Retrieve 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; |
| } |
| } |
| |
| /** |
| * 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}. |
| */ |
| public void forEachRegisteredTransport(Consumer<String> transportConsumer) { |
| synchronized (mTransportLock) { |
| for (TransportDescription transportDescription |
| : mRegisteredTransportsDescriptionMap.values()) { |
| transportConsumer.accept(transportDescription.name); |
| } |
| } |
| } |
| |
| 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 transportComponent} or {@code null} if not |
| * found. |
| */ |
| @Nullable |
| public String getTransportName(ComponentName transportComponent) { |
| synchronized (mTransportLock) { |
| TransportDescription description = |
| mRegisteredTransportsDescriptionMap.get(transportComponent); |
| if (description == null) { |
| Slog.e(TAG, "Trying to find name of unregistered transport " + transportComponent); |
| return null; |
| } |
| return description.name; |
| } |
| } |
| |
| @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") |
| private TransportDescription getRegisteredTransportDescriptionOrThrowLocked( |
| String transportName) throws TransportNotRegisteredException { |
| TransportDescription description = getRegisteredTransportDescriptionLocked(transportName); |
| if (description == null) { |
| throw new TransportNotRegisteredException(transportName); |
| } |
| return description; |
| } |
| |
| |
| @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; |
| } |
| |
| @Nullable |
| public TransportClient getTransportClient(String transportName, String caller) { |
| synchronized (mTransportLock) { |
| ComponentName component = getRegisteredTransportComponentLocked(transportName); |
| if (component == null) { |
| Slog.w(TAG, "Transport " + transportName + " not registered"); |
| return null; |
| } |
| TransportDescription description = mRegisteredTransportsDescriptionMap.get(component); |
| return mTransportClientManager.getTransportClient( |
| component, description.transportDirName, caller); |
| } |
| } |
| |
| public boolean isTransportRegistered(String transportName) { |
| synchronized (mTransportLock) { |
| return getRegisteredTransportEntryLocked(transportName) != null; |
| } |
| } |
| |
| /** |
| * 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()]); |
| } |
| } |
| |
| /** |
| * An *eligible* transport is a service component that satisfies intent with action |
| * android.backup.TRANSPORT_HOST and returns true for |
| * {@link #isTransportTrusted(ComponentName)}. It may be registered or not registered. |
| * This method returns the {@link ComponentName}s of those transports. |
| */ |
| ComponentName[] getEligibleTransportComponents() { |
| synchronized (mTransportLock) { |
| return mEligibleTransports.toArray(new ComponentName[mEligibleTransports.size()]); |
| } |
| } |
| |
| Set<ComponentName> getTransportWhitelist() { |
| return mTransportWhitelist; |
| } |
| |
| /** |
| * A *registered* transport is an eligible transport that has been successfully connected and |
| * that returned true for method |
| * {@link TransportBoundListener#onTransportBound(IBackupTransport)} of TransportBoundListener |
| * provided in the constructor. This method returns the names of the registered transports. |
| */ |
| String[] getRegisteredTransportNames() { |
| synchronized (mTransportLock) { |
| return mRegisteredTransportsDescriptionMap.values().stream() |
| .map(transportDescription -> transportDescription.name) |
| .toArray(String[]::new); |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| |
| @Nullable |
| String getCurrentTransportName() { |
| return mCurrentTransportName; |
| } |
| |
| 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); |
| } |
| } |
| |
| |
| // This is for mocking, Mockito can't mock if package-protected and in the same package but |
| // different class loaders. Checked with the debugger and class loaders are different |
| // See https://github.com/mockito/mockito/issues/796 |
| @VisibleForTesting(visibility = PACKAGE) |
| public 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); |
| synchronized (mTransportLock) { |
| mEligibleTransports.add(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()); |
| } |
| |
| /** If {@link RemoteException} is thrown the transport is guaranteed to not be registered. */ |
| private void registerTransport(ComponentName transportComponent, IBackupTransport transport) |
| throws RemoteException { |
| synchronized (mTransportLock) { |
| String name = transport.name(); |
| TransportDescription description = new TransportDescription( |
| name, |
| transport.transportDirName(), |
| transport.configurationIntent(), |
| transport.currentDestinationString(), |
| transport.dataManagementIntent(), |
| transport.dataManagementLabel()); |
| mRegisteredTransportsDescriptionMap.put(transportComponent, description); |
| } |
| } |
| |
| private class TransportConnection implements ServiceConnection { |
| |
| // Hold mTransportLock 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. |
| final boolean valid; |
| if (mTransportBoundListener != null) { |
| valid = mTransportBoundListener.onTransportBound(mBinder); |
| } else { |
| Slog.w(TAG, "setTransportBoundListener() not called, assuming transport " |
| + component + " valid"); |
| valid = true; |
| } |
| if (valid) { |
| // We're now using the always-bound connection to do the registration but |
| // when we remove the always-bound code this will be in the first binding |
| // TODO: Move registration to first binding |
| registerTransport(component, mBinder); |
| // If registerTransport() hasn't thrown... |
| success = true; |
| } |
| } 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 (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); |
| mEligibleTransports.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); |
| } |
| |
| 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; |
| } |
| } |
| } |