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

Commit 17b1888d authored by Hani Kazmi's avatar Hani Kazmi Committed by Android (Google) Code Review
Browse files

Merge "[AAPM] Add abstract class for feature hooks" into main

parents 52ba00e4 4cd87c23
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -42,7 +42,7 @@ import java.util.concurrent.Executor;
@FlaggedApi(Flags.FLAG_AAPM_API)
@SystemService(Context.ADVANCED_PROTECTION_SERVICE)
public class AdvancedProtectionManager {
    private static final String TAG = "AdvancedProtectionM";
    private static final String TAG = "AdvancedProtectionMgr";

    private final ConcurrentHashMap<Callback, IAdvancedProtectionCallback>
            mCallbackMap = new ConcurrentHashMap<>();
@@ -73,7 +73,7 @@ public class AdvancedProtectionManager {
     * Registers a {@link Callback} to be notified of changes to the Advanced Protection state.
     *
     * <p>The provided callback will be called on the specified executor with the updated
     * {@link AdvancedProtectionState}. Methods are called when the state changes, as well as once
     * state. Methods are called when the state changes, as well as once
     * on initial registration.
     *
     * @param executor The executor of where the callback will execute.
+40 −3
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.provider.Settings.Secure.ADVANCED_PROTECTION_MODE;
import android.Manifest;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Binder;
import android.os.Handler;
@@ -35,37 +36,55 @@ import android.provider.Settings;
import android.security.advancedprotection.IAdvancedProtectionCallback;
import android.security.advancedprotection.IAdvancedProtectionService;
import android.util.ArrayMap;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.pm.UserManagerInternal;
import com.android.server.security.advancedprotection.features.AdvancedProtectionHook;

import java.io.FileDescriptor;
import java.util.ArrayList;

/** @hide */
public class AdvancedProtectionService extends IAdvancedProtectionService.Stub  {
    private static final String TAG = "AdvancedProtectionService";
    private static final int MODE_CHANGED = 0;
    private static final int CALLBACK_ADDED = 1;

    private final Context mContext;
    private final Handler mHandler;
    private final AdvancedProtectionStore mStore;

    // Features owned by the service - their code will be executed when state changes
    private final ArrayList<AdvancedProtectionHook> mHooks = new ArrayList<>();
    // External features - they will be called on state change
    private final ArrayMap<IBinder, IAdvancedProtectionCallback> mCallbacks = new ArrayMap<>();

    private AdvancedProtectionService(@NonNull Context context) {
        super(PermissionEnforcer.fromContext(context));
        mContext = context;
        mHandler = new AdvancedProtectionHandler(FgThread.get().getLooper());
        mStore = new AdvancedProtectionStore(context);
    }

    private void initFeatures(boolean enabled) {
    }

    // Only for tests
    @VisibleForTesting
    AdvancedProtectionService(@NonNull Context context, @NonNull AdvancedProtectionStore store,
            @NonNull Looper looper, @NonNull PermissionEnforcer permissionEnforcer) {
            @NonNull Looper looper, @NonNull PermissionEnforcer permissionEnforcer,
            @Nullable AdvancedProtectionHook hook) {
        super(permissionEnforcer);
        mContext = context;
        mStore = store;
        mHandler = new AdvancedProtectionHandler(looper);
        if (hook != null) {
            mHooks.add(hook);
        }
    }

    @Override
@@ -100,8 +119,8 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub

    @Override
    @EnforcePermission(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE)
    public void unregisterAdvancedProtectionCallback(@NonNull IAdvancedProtectionCallback callback)
            throws RemoteException {
    public void unregisterAdvancedProtectionCallback(
            @NonNull IAdvancedProtectionCallback callback) {
        unregisterAdvancedProtectionCallback_enforcePermission();
        synchronized (mCallbacks) {
            mCallbacks.remove(callback.asBinder());
@@ -118,6 +137,7 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub
                if (enabled != isAdvancedProtectionEnabledInternal()) {
                    mStore.store(enabled);
                    sendModeChanged(enabled);
                    Slog.i(TAG, "Advanced protection is " + (enabled ? "enabled" : "disabled"));
                }
            }
        } finally {
@@ -156,6 +176,17 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub
        public void onStart() {
            publishBinderService(Context.ADVANCED_PROTECTION_SERVICE, mService);
        }

        @Override
        public void onBootPhase(@BootPhase int phase) {
            if (phase == PHASE_SYSTEM_SERVICES_READY) {
                boolean enabled = mService.isAdvancedProtectionEnabledInternal();
                if (enabled) {
                    Slog.i(TAG, "Advanced protection is enabled");
                }
                mService.initFeatures(enabled);
            }
        }
    }

    @VisibleForTesting
@@ -205,6 +236,12 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub
        private void handleAllCallbacks(boolean enabled) {
            ArrayList<IAdvancedProtectionCallback> deadObjects = new ArrayList<>();

            for (int i = 0; i < mHooks.size(); i++) {
                AdvancedProtectionHook feature = mHooks.get(i);
                if (feature.isAvailable()) {
                    feature.onAdvancedProtectionChanged(enabled);
                }
            }
            synchronized (mCallbacks) {
                for (int i = 0; i < mCallbacks.size(); i++) {
                    IAdvancedProtectionCallback callback = mCallbacks.valueAt(i);
+31 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.security.advancedprotection.features;

import android.annotation.NonNull;
import android.content.Context;

/** @hide */
public abstract class AdvancedProtectionHook {
    /** Called on boot phase PHASE_SYSTEM_SERVICES_READY */
    public AdvancedProtectionHook(@NonNull Context context, boolean enabled) {}
    /** Whether this feature is relevant on this device. If false, onAdvancedProtectionChanged will
     * not be called, and the feature will not be displayed in the onboarding UX. */
    public abstract boolean isAvailable();
    /** Called whenever advanced protection state changes */
    public void onAdvancedProtectionChanged(boolean enabled) {}
}
+158 −14
Original line number Diff line number Diff line
@@ -28,28 +28,34 @@ import android.os.RemoteException;
import android.os.test.FakePermissionEnforcer;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.security.advancedprotection.IAdvancedProtectionCallback;

import com.android.server.security.advancedprotection.features.AdvancedProtectionHook;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.concurrent.atomic.AtomicBoolean;

@SuppressLint("VisibleForTests")
@RunWith(JUnit4.class)
public class AdvancedProtectionServiceTest {
    private AdvancedProtectionService mService;
    private FakePermissionEnforcer mPermissionEnforcer;
    private Context mContext;
    private AdvancedProtectionService.AdvancedProtectionStore mStore;
    private TestLooper mLooper;

    @Before
    @SuppressLint("VisibleForTests")
    public void setup() throws Settings.SettingNotFoundException {
        mContext = mock(Context.class);
        mPermissionEnforcer = new FakePermissionEnforcer();
        mPermissionEnforcer.grant(Manifest.permission.SET_ADVANCED_PROTECTION_MODE);
        mPermissionEnforcer.grant(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE);

        AdvancedProtectionService.AdvancedProtectionStore store =
                new AdvancedProtectionService.AdvancedProtectionStore(mContext) {
        mStore = new AdvancedProtectionService.AdvancedProtectionStore(mContext) {
            private boolean mEnabled = false;

            @Override
@@ -63,22 +69,146 @@ public class AdvancedProtectionServiceTest {
            }
        };

        mService = new AdvancedProtectionService(mContext, store, new TestLooper().getLooper(),
                mPermissionEnforcer);
        mLooper = new TestLooper();
        mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
                mPermissionEnforcer, null);
    }

    @Test
    public void testEnableProtection() throws RemoteException {
    public void testToggleProtection() {
        mService.setAdvancedProtectionEnabled(true);
        assertTrue(mService.isAdvancedProtectionEnabled());

        mService.setAdvancedProtectionEnabled(false);
        assertFalse(mService.isAdvancedProtectionEnabled());
    }

    @Test
    public void testDisableProtection() throws RemoteException {
        mService.setAdvancedProtectionEnabled(false);
    public void testDisableProtection_byDefault() {
        assertFalse(mService.isAdvancedProtectionEnabled());
    }

    @Test
    public void testEnableProtection_withHook() {
        AtomicBoolean callbackCaptor = new AtomicBoolean(false);
        AdvancedProtectionHook hook =
                new AdvancedProtectionHook(mContext, true) {
                    @Override
                    public boolean isAvailable() {
                        return true;
                    }

                    @Override
                    public void onAdvancedProtectionChanged(boolean enabled) {
                        callbackCaptor.set(enabled);
                    }
                };

        mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
                mPermissionEnforcer, hook);
        mService.setAdvancedProtectionEnabled(true);
        mLooper.dispatchNext();

        assertTrue(callbackCaptor.get());
    }

    @Test
    public void testEnableProtection_withFeature_notAvailable() {
        AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false);
        AdvancedProtectionHook hook =
                new AdvancedProtectionHook(mContext, true) {
                    @Override
                    public boolean isAvailable() {
                        return false;
                    }

                    @Override
                    public void onAdvancedProtectionChanged(boolean enabled) {
                        callbackCalledCaptor.set(true);
                    }
                };

        mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
                mPermissionEnforcer, hook);
        mService.setAdvancedProtectionEnabled(true);
        mLooper.dispatchNext();
        assertFalse(callbackCalledCaptor.get());
    }

    @Test
    public void testEnableProtection_withFeature_notCalledIfModeNotChanged() {
        AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false);
        AdvancedProtectionHook hook =
                new AdvancedProtectionHook(mContext, true) {
                    @Override
                    public boolean isAvailable() {
                        return true;
                    }

                    @Override
                    public void onAdvancedProtectionChanged(boolean enabled) {
                        callbackCalledCaptor.set(true);
                    }
                };

        mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
                mPermissionEnforcer, hook);
        mService.setAdvancedProtectionEnabled(true);
        mLooper.dispatchNext();
        assertTrue(callbackCalledCaptor.get());

        callbackCalledCaptor.set(false);
        mService.setAdvancedProtectionEnabled(true);
        mLooper.dispatchAll();
        assertFalse(callbackCalledCaptor.get());
    }

    @Test
    public void testRegisterCallback() throws RemoteException {
        AtomicBoolean callbackCaptor = new AtomicBoolean(false);
        IAdvancedProtectionCallback callback = new IAdvancedProtectionCallback.Stub() {
            @Override
            public void onAdvancedProtectionChanged(boolean enabled) {
                callbackCaptor.set(enabled);
            }
        };

        mService.setAdvancedProtectionEnabled(true);
        mLooper.dispatchAll();

        mService.registerAdvancedProtectionCallback(callback);
        mLooper.dispatchNext();
        assertTrue(callbackCaptor.get());

        mService.setAdvancedProtectionEnabled(false);
        mLooper.dispatchNext();

        assertFalse(callbackCaptor.get());
    }

    @Test
    public void testUnregisterCallback() throws RemoteException {
        AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false);
        IAdvancedProtectionCallback callback = new IAdvancedProtectionCallback.Stub() {
            @Override
            public void onAdvancedProtectionChanged(boolean enabled) {
                callbackCalledCaptor.set(true);
            }
        };

        mService.setAdvancedProtectionEnabled(true);
        mService.registerAdvancedProtectionCallback(callback);
        mLooper.dispatchAll();
        callbackCalledCaptor.set(false);

        mService.unregisterAdvancedProtectionCallback(callback);
        mService.setAdvancedProtectionEnabled(false);

        mLooper.dispatchNext();
        assertFalse(callbackCalledCaptor.get());
    }


    @Test
    public void testSetProtection_withoutPermission() {
        mPermissionEnforcer.revoke(Manifest.permission.SET_ADVANCED_PROTECTION_MODE);
@@ -90,4 +220,18 @@ public class AdvancedProtectionServiceTest {
        mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE);
        assertThrows(SecurityException.class, () -> mService.isAdvancedProtectionEnabled());
    }

    @Test
    public void testRegisterCallback_withoutPermission() {
        mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE);
        assertThrows(SecurityException.class, () -> mService.registerAdvancedProtectionCallback(
                new IAdvancedProtectionCallback.Default()));
    }

    @Test
    public void testUnregisterCallback_withoutPermission() {
        mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE);
        assertThrows(SecurityException.class, () -> mService.unregisterAdvancedProtectionCallback(
                new IAdvancedProtectionCallback.Default()));
    }
}