Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f658e370 authored by Sarp Misoglu's avatar Sarp Misoglu
Browse files

Move BackupAgent connection to a helper class

This is to isolate some scope of UserBackupManagerService. It's too big
and unreadable.

I've also added some tests. But since these are blocking and async
operations, I couldn't see any way other than adding some thread sleeps
to avoid flakiness.

These tests could still end up being too flaky. If that's the case we
might have to leave this code untested :(

This is a no-op refactor.

Bug: 376661510
Flag: EXEMPT refactor
Test: atest BackupAgentConnectionManagerTest & atest CtsBackupHostTestCases
Change-Id: I9893336fb224148ce5b2f3c6fa2fc26828d2a1e9
parent 0d027067
Loading
Loading
Loading
Loading
+318 −0
Original line number Diff line number Diff line
/*
 * 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.server.backup.BackupManagerService.MORE_DEBUG;
import static com.android.server.backup.BackupManagerService.TAG;

import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ApplicationThreadConstants;
import android.app.IActivityManager;
import android.app.IBackupAgent;
import android.app.backup.BackupAnnotations;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.backup.internal.LifecycleOperationStorage;

import java.util.Set;

/**
 * Handles the lifecycle of {@link IBackupAgent}s that the {@link UserBackupManagerService}
 * communicates with.
 *
 * <p>There can only be one agent that's connected to at a time.
 *
 * <p>There should be only one instance of this class per {@link UserBackupManagerService}.
 */
public class BackupAgentConnectionManager {

    /**
     * Enables the OS making a decision on whether backup restricted mode should be used for apps
     * that haven't explicitly opted in or out. See
     * {@link android.content.pm.PackageManager#PROPERTY_USE_RESTRICTED_BACKUP_MODE} for details.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
    public static final long OS_DECIDES_BACKUP_RESTRICTED_MODE = 376661510;

    // The thread performing the sequence of queued backups binds to each app's agent
    // in succession.  Bind notifications are asynchronously delivered through the
    // Activity Manager; use this lock object to signal when a requested binding has
    // completed.
    private final Object mAgentConnectLock = new Object();
    private IBackupAgent mConnectedAgent;
    private volatile boolean mConnecting;
    private final ArraySet<String> mRestoreNoRestrictedModePackages = new ArraySet<>();
    private final ArraySet<String> mBackupNoRestrictedModePackages = new ArraySet<>();

    private final IActivityManager mActivityManager;
    private final ActivityManagerInternal mActivityManagerInternal;
    private final LifecycleOperationStorage mOperationStorage;
    private final PackageManager mPackageManager;
    private final UserBackupManagerService mUserBackupManagerService;
    private final int mUserId;
    private final String mUserIdMsg;

    BackupAgentConnectionManager(LifecycleOperationStorage operationStorage,
            PackageManager packageManager, UserBackupManagerService userBackupManagerService,
            int userId) {
        mActivityManager = ActivityManager.getService();
        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
        mOperationStorage = operationStorage;
        mPackageManager = packageManager;
        mUserBackupManagerService = userBackupManagerService;
        mUserId = userId;
        mUserIdMsg = "[UserID:" + userId + "] ";
    }

    /**
     * Fires off a backup agent, blocking until it attaches (and ActivityManager will call
     * {@link #agentConnected(String, IBinder)}) or until this operation times out.
     *
     * @param mode a {@code BACKUP_MODE} from {@link android.app.ApplicationThreadConstants}.
     */
    @Nullable
    public IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode,
            @BackupAnnotations.BackupDestination int backupDestination) {
        IBackupAgent agent = null;
        synchronized (mAgentConnectLock) {
            mConnecting = true;
            mConnectedAgent = null;
            boolean useRestrictedMode = shouldUseRestrictedBackupModeForPackage(mode,
                    app.packageName);
            try {
                if (mActivityManager.bindBackupAgent(app.packageName, mode, mUserId,
                        backupDestination, useRestrictedMode)) {
                    Slog.d(TAG, mUserIdMsg + "awaiting agent for " + app);

                    // success; wait for the agent to arrive
                    // only wait 10 seconds for the bind to happen
                    long timeoutMark = System.currentTimeMillis() + 10 * 1000;
                    while (mConnecting && mConnectedAgent == null && (System.currentTimeMillis()
                            < timeoutMark)) {
                        try {
                            mAgentConnectLock.wait(5000);
                        } catch (InterruptedException e) {
                            // just bail
                            Slog.w(TAG, mUserIdMsg + "Interrupted: " + e);
                            mConnecting = false;
                            mConnectedAgent = null;
                        }
                    }

                    // if we timed out with no connect, abort and move on
                    if (mConnecting) {
                        Slog.w(TAG, mUserIdMsg + "Timeout waiting for agent " + app);
                        mConnectedAgent = null;
                    }
                    Slog.i(TAG, mUserIdMsg + "got agent " + mConnectedAgent);
                    agent = mConnectedAgent;
                }
            } catch (RemoteException e) {
                // can't happen - ActivityManager is local
            }
        }
        if (agent == null) {
            mActivityManagerInternal.clearPendingBackup(mUserId);
        }
        return agent;
    }

    /**
     * Tell the ActivityManager that we are done with the {@link IBackupAgent} of this {@code app}.
     * It will tell the app to destroy the agent.
     */
    public void unbindAgent(ApplicationInfo app) {
        try {
            mActivityManager.unbindBackupAgent(app);
        } catch (RemoteException e) {
            // Can't happen - activity manager is local
        }
    }

    /**
     * Callback: a requested backup agent has been instantiated. This should only be called from
     * the
     * {@link ActivityManager} when it's telling us that an agent is ready after a call to
     * {@link #bindToAgentSynchronous(ApplicationInfo, int, int)}.
     */
    public void agentConnected(String packageName, IBinder agentBinder) {
        synchronized (mAgentConnectLock) {
            if (getCallingUid() == android.os.Process.SYSTEM_UID) {
                Slog.d(TAG,
                        mUserIdMsg + "agentConnected pkg=" + packageName + " agent=" + agentBinder);
                mConnectedAgent = IBackupAgent.Stub.asInterface(agentBinder);
                mConnecting = false;
            } else {
                Slog.w(TAG, mUserIdMsg + "Non-system process uid=" + getCallingUid()
                        + " claiming agent connected");
            }
            mAgentConnectLock.notifyAll();
        }
    }

    /**
     * Callback: a backup agent has failed to come up, or has unexpectedly quit. If the agent failed
     * to come up in the first place, the agentBinder argument will be {@code null}. This should
     * only be called from the {@link ActivityManager}.
     */
    public void agentDisconnected(String packageName) {
        synchronized (mAgentConnectLock) {
            if (getCallingUid() == Process.SYSTEM_UID) {
                mConnectedAgent = null;
                mConnecting = false;
            } else {
                Slog.w(TAG, mUserIdMsg + "Non-system process uid=" + getCallingUid()
                        + " claiming agent disconnected");
            }
            Slog.w(TAG, mUserIdMsg + "agentDisconnected: the backup agent for " + packageName
                    + " died: cancel current operations");

            // Offload operation cancellation off the main thread as the cancellation callbacks
            // might call out to BackupTransport. Other operations started on the same package
            // before the cancellation callback has executed will also be cancelled by the callback.
            Runnable cancellationRunnable = () -> {
                // handleCancel() causes the PerformFullTransportBackupTask to go on to
                // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so
                // that the package being backed up doesn't get stuck in restricted mode until the
                // backup time-out elapses.
                for (int token : mOperationStorage.operationTokensForPackage(packageName)) {
                    if (MORE_DEBUG) {
                        Slog.d(TAG,
                                mUserIdMsg + "agentDisconnected: will handleCancel(all) for token:"
                                        + Integer.toHexString(token));
                    }
                    mUserBackupManagerService.handleCancel(token, true /* cancelAll */);
                }
            };
            getThreadForCancellation(cancellationRunnable).start();

            mAgentConnectLock.notifyAll();
        }
    }

    /**
     * Marks the given set of packages as packages that should not be put into restricted mode if
     * they are started for the given {@link BackupAnnotations.OperationType}.
     */
    public void setNoRestrictedModePackages(Set<String> packageNames,
            @BackupAnnotations.OperationType int opType) {
        if (opType == BackupAnnotations.OperationType.BACKUP) {
            mBackupNoRestrictedModePackages.clear();
            mBackupNoRestrictedModePackages.addAll(packageNames);
        } else if (opType == BackupAnnotations.OperationType.RESTORE) {
            mRestoreNoRestrictedModePackages.clear();
            mRestoreNoRestrictedModePackages.addAll(packageNames);
        } else {
            throw new IllegalArgumentException("opType must be BACKUP or RESTORE");
        }
    }

    /**
     * Clears the list of packages that should not be put into restricted mode for either backup or
     * restore.
     */
    public void clearNoRestrictedModePackages() {
        mBackupNoRestrictedModePackages.clear();
        mRestoreNoRestrictedModePackages.clear();
    }

    /**
     * If the app has specified {@link PackageManager#PROPERTY_USE_RESTRICTED_BACKUP_MODE}, then
     * its value is returned. If it hasn't and it targets an SDK below
     * {@link Build.VERSION_CODES#BAKLAVA} then returns true. If it targets a newer SDK, then
     * returns the decision made by the {@link android.app.backup.BackupTransport}.
     *
     * <p>When this method is called, we should have already asked the transport and cached its
     * response in {@link #mBackupNoRestrictedModePackages} or
     * {@link #mRestoreNoRestrictedModePackages} so this method will immediately return without
     * any IPC to the transport.
     */
    private boolean shouldUseRestrictedBackupModeForPackage(
            @BackupAnnotations.OperationType int mode, String packageName) {
        if (!Flags.enableRestrictedModeChanges()) {
            return true;
        }

        // Key/Value apps are never put in restricted mode.
        if (mode == ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL
                || mode == ApplicationThreadConstants.BACKUP_MODE_RESTORE) {
            return false;
        }

        try {
            PackageManager.Property property = mPackageManager.getPropertyAsUser(
                    PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE,
                    packageName, /* className= */ null, mUserId);
            if (property.isBoolean()) {
                // If the package has explicitly specified, we won't ask the transport.
                return property.getBoolean();
            } else {
                Slog.w(TAG,
                        PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE + "must be a boolean.");
            }
        } catch (NameNotFoundException e) {
            // This is expected when the package has not defined the property in its manifest.
        }

        // The package has not specified the property. The behavior depends on the package's
        // targetSdk.
        // <36 gets the old behavior of always using restricted mode.
        if (!CompatChanges.isChangeEnabled(OS_DECIDES_BACKUP_RESTRICTED_MODE, packageName,
                UserHandle.of(mUserId))) {
            return true;
        }

        // Apps targeting >=36 get the behavior decided by the transport.
        // By this point, we should have asked the transport and cached its decision.
        if ((mode == ApplicationThreadConstants.BACKUP_MODE_FULL
                && mBackupNoRestrictedModePackages.contains(packageName)) || (
                mode == ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL
                        && mRestoreNoRestrictedModePackages.contains(packageName))) {
            Slog.d(TAG, "Transport requested no restricted mode for: " + packageName);
            return false;
        }
        return true;
    }

    @VisibleForTesting
    Thread getThreadForCancellation(Runnable operation) {
        return new Thread(operation, /* operationName */ "agent-disconnected");
    }

    @VisibleForTesting
    int getCallingUid() {
        return Binder.getCallingUid();
    }
}
+4 −2
Original line number Diff line number Diff line
@@ -658,7 +658,8 @@ public class BackupManagerService extends IBackupManager.Stub {
                getServiceForUserIfCallerHasPermission(userId, "agentConnected()");

        if (userBackupManagerService != null) {
            userBackupManagerService.agentConnected(packageName, agentBinder);
            userBackupManagerService.getBackupAgentConnectionManager().agentConnected(packageName,
                    agentBinder);
        }
    }

@@ -683,7 +684,8 @@ public class BackupManagerService extends IBackupManager.Stub {
                getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()");

        if (userBackupManagerService != null) {
            userBackupManagerService.agentDisconnected(packageName);
            userBackupManagerService.getBackupAgentConnectionManager().agentDisconnected(
                    packageName);
        }
    }

+2 −1
Original line number Diff line number Diff line
@@ -146,7 +146,8 @@ public class KeyValueAdbBackupEngine {

    private IBackupAgent bindToAgent(ApplicationInfo targetApp) {
        try {
            return mBackupManagerService.bindToAgentSynchronous(targetApp,
            return mBackupManagerService.getBackupAgentConnectionManager().bindToAgentSynchronous(
                    targetApp,
                    ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL,
                    BackupAnnotations.BackupDestination.CLOUD);
        } catch (SecurityException e) {
+9 −257

File changed.

Preview size limit exceeded, changes collapsed.

+1 −1
Original line number Diff line number Diff line
@@ -314,7 +314,7 @@ public class FullBackupEngine {
                Slog.d(TAG, "Binding to full backup agent : " + mPkg.packageName);
            }
            mAgent =
                    backupManagerService.bindToAgentSynchronous(
                    backupManagerService.getBackupAgentConnectionManager().bindToAgentSynchronous(
                            mPkg.applicationInfo, ApplicationThreadConstants.BACKUP_MODE_FULL,
                            mBackupEligibilityRules.getBackupDestination());
        }
Loading