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

Commit 22345890 authored by Gil Cukierman's avatar Gil Cukierman Committed by Android (Google) Code Review
Browse files

Merge "Implement the identifier disclosure window" into main

parents 51f404dd 0a942a36
Loading
Loading
Loading
Loading
+165 −20
Original line number Diff line number Diff line
@@ -18,47 +18,101 @@ package com.android.internal.telephony.security;

import android.telephony.CellularIdentifierDisclosure;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.telephony.Rlog;

import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Encapsulates logic to emit notifications to the user that their cellular identifiers were
 * disclosed in the clear.
 * disclosed in the clear. Callers add CellularIdentifierDisclosure instances by calling
 * addDisclosure.
 *
 * <p>This class will either emit notifications through SafetyCenterManager if SafetyCenter exists
 * on a device, or it will emit system notifications otherwise.
 * <p>This class is thread safe and is designed to do costly work on worker threads. The intention
 * is to allow callers to add disclosures from a Looper thread without worrying about blocking for
 * IPC.
 *
 * @hide
 */
public class CellularIdentifierDisclosureNotifier {

    private static final String TAG = "CellularIdentifierDisclosureNotifier";
    private static final long DEFAULT_WINDOW_CLOSE_DURATION_IN_MINUTES = 15;
    private static CellularIdentifierDisclosureNotifier sInstance = null;
    private final long mWindowCloseDuration;
    private final TimeUnit mWindowCloseUnit;
    private final Object mEnabledLock = new Object();

    @GuardedBy("mEnabledLock")
    private boolean mEnabled = false;
    // This is a single threaded executor. This is important because we want to ensure certain
    // events are strictly serialized.
    private ScheduledExecutorService mSerializedWorkQueue;

    private AtomicInteger mDisclosureCount;

    // One should only interact with this future from within the work queue's thread.
    private ScheduledFuture<?> mWhenWindowCloses;

    public CellularIdentifierDisclosureNotifier() {
        this(Executors.newSingleThreadScheduledExecutor(), DEFAULT_WINDOW_CLOSE_DURATION_IN_MINUTES,
                TimeUnit.MINUTES);
    }

    /**
     * Construct a CellularIdentifierDisclosureNotifier by injection. This should only be used for
     * testing.
     *
     * @param notificationQueue a ScheduledExecutorService that should only execute on a single
     *     thread.
     */
    @VisibleForTesting
    public CellularIdentifierDisclosureNotifier() {}
    public CellularIdentifierDisclosureNotifier(
            ScheduledExecutorService notificationQueue,
            long windowCloseDuration,
            TimeUnit windowCloseUnit) {
        mSerializedWorkQueue = notificationQueue;
        mWindowCloseDuration = windowCloseDuration;
        mWindowCloseUnit = windowCloseUnit;
        mDisclosureCount = new AtomicInteger(0);
    }

    /**
     * Add a CellularIdentifierDisclosure to be tracked by this instance.
     * If appropriate, this will trigger a user notification.
     */
    public void addDisclosure(CellularIdentifierDisclosure disclosure) {
        // TODO (b/308985417) this is a stub method for now. Logic
        // for tracking disclosures and emitting notifications will flow
        // from here.
        Rlog.d(TAG, "Identifier disclosure reported: " + disclosure);

        synchronized (mEnabledLock) {
            if (!mEnabled) {
                Rlog.d(TAG, "Skipping disclosure because notifier was disabled.");
                return;
            }

    /**
     * Get a singleton CellularIdentifierDisclosureNotifier.
     */
    public static synchronized CellularIdentifierDisclosureNotifier getInstance() {
        if (sInstance == null) {
            sInstance = new CellularIdentifierDisclosureNotifier();
            // Don't notify if this disclosure happened in service of an emergency. That's a user
            // initiated action that we don't want to interfere with.
            if (disclosure.isEmergency()) {
                Rlog.i(TAG, "Ignoring identifier disclosure associated with an emergency.");
                return;
            }

        return sInstance;
            // Schedule incrementAndNotify from within the lock because we're sure at this point
            // that we're enabled. This allows incrementAndNotify to avoid re-checking mEnabled
            // because we know that any actions taken on disabled will be scheduled after this
            // incrementAndNotify call.
            try {
                mSerializedWorkQueue.execute(incrementAndNotify());
            } catch (RejectedExecutionException e) {
                Rlog.e(TAG, "Failed to schedule incrementAndNotify: " + e.getMessage());
            }
        } // end mEnabledLock
    }

    /**
@@ -66,8 +120,15 @@ public class CellularIdentifierDisclosureNotifier {
     * disclosures again and potentially emitting notifications.
     */
    public void enable() {
        synchronized (mEnabledLock) {
            Rlog.d(TAG, "enabled");
            mEnabled = true;
            try {
                mSerializedWorkQueue.execute(onEnableNotifier());
            } catch (RejectedExecutionException e) {
                Rlog.e(TAG, "Failed to schedule onEnableNotifier: " + e.getMessage());
            }
        }
    }

    /**
@@ -77,10 +138,94 @@ public class CellularIdentifierDisclosureNotifier {
     */
    public void disable() {
        Rlog.d(TAG, "disabled");
        synchronized (mEnabledLock) {
            mEnabled = false;
            try {
                mSerializedWorkQueue.execute(onDisableNotifier());
            } catch (RejectedExecutionException e) {
                Rlog.e(TAG, "Failed to schedule onDisableNotifier: " + e.getMessage());
            }
        }
    }

    public boolean isEnabled() {
        synchronized (mEnabledLock) {
            return mEnabled;
        }
    }

    @VisibleForTesting
    public int getCurrentDisclosureCount() {
        return mDisclosureCount.get();
    }

    /** Get a singleton CellularIdentifierDisclosureNotifier. */
    public static synchronized CellularIdentifierDisclosureNotifier getInstance() {
        if (sInstance == null) {
            sInstance = new CellularIdentifierDisclosureNotifier();
        }

        return sInstance;
    }

    private Runnable closeWindow() {
        return () -> {
            Rlog.i(TAG,
                    "Disclosure window closing. Disclosure count was " + mDisclosureCount.get());
            mDisclosureCount.set(0);
        };
    }

    private Runnable incrementAndNotify() {
        return () -> {
            int newCount = mDisclosureCount.incrementAndGet();
            Rlog.d(TAG, "Emitting notification. New disclosure count " + newCount);

            // To reset the timer for our window, we first cancel an existing timer.
            boolean cancelled = cancelWindowCloseFuture();
            Rlog.d(TAG, "Result of attempting to cancel window closing future: " + cancelled);

            try {
                mWhenWindowCloses =
                        mSerializedWorkQueue.schedule(
                                closeWindow(), mWindowCloseDuration, mWindowCloseUnit);
            } catch (RejectedExecutionException e) {
                Rlog.e(TAG, "Failed to schedule closeWindow: " + e.getMessage());
            }
        };
    }

    private Runnable onDisableNotifier() {
        return () -> {
            mDisclosureCount.set(0);
            cancelWindowCloseFuture();
            Rlog.d(TAG, "On disable notifier");
        };
    }

    private Runnable onEnableNotifier() {
        return () -> {
            Rlog.i(TAG, "On enable notifier");
        };
    }

    /**
     * A helper to cancel the Future that is in charge of closing the disclosure window. This must
     * only be called from within the single-threaded executor. Calling this method leaves a
     * completed or cancelled future in mWhenWindowCloses.
     *
     * @return boolean indicating whether or not the Future was actually cancelled. If false, this
     * likely indicates that the disclosure window has already closed.
     */
    private boolean cancelWindowCloseFuture() {
        if (mWhenWindowCloses == null) {
            return false;
        }

        // While we choose not to interrupt a running Future (we pass `false` to the `cancel`
        // call), we shouldn't ever actually need this functionality because all the work on the
        // queue is serialized on a single thread. Nothing about the `closeWindow` call is ready
        // to handle interrupts, though, so this seems like a safer choice.
        return mWhenWindowCloses.cancel(false);
    }
}
+140 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.internal.telephony.security;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;

import android.telephony.CellularIdentifierDisclosure;

import com.android.internal.telephony.TestExecutorService;

import org.junit.Before;
import org.junit.Test;

import java.util.concurrent.TimeUnit;

public class CellularIdentifierDisclosureNotifierTest {

    // 15 minutes and 100 milliseconds. Can be used to advance time in a test executor far enough
    // to (hopefully, if the code is behaving) close a disclosure window.
    private static final long WINDOW_CLOSE_ADVANCE_MILLIS = (15 * 60 * 1000) + 100;
    private CellularIdentifierDisclosure mDislosure;

    @Before
    public void setUp() {
        mDislosure =
                new CellularIdentifierDisclosure(
                        CellularIdentifierDisclosure.NAS_PROTOCOL_MESSAGE_ATTACH_REQUEST,
                        CellularIdentifierDisclosure.CELLULAR_IDENTIFIER_IMSI,
                        "001001",
                        false);
    }

    @Test
    public void testInitializeDisabled() {
        TestExecutorService executor = new TestExecutorService();
        CellularIdentifierDisclosureNotifier notifier =
                new CellularIdentifierDisclosureNotifier(executor, 15, TimeUnit.MINUTES);

        assertFalse(notifier.isEnabled());
    }

    @Test
    public void testDisableAddDisclosureNop() {
        TestExecutorService executor = new TestExecutorService();
        CellularIdentifierDisclosureNotifier notifier =
                new CellularIdentifierDisclosureNotifier(executor, 15, TimeUnit.MINUTES);

        assertFalse(notifier.isEnabled());
        notifier.addDisclosure(mDislosure);
        assertEquals(0, notifier.getCurrentDisclosureCount());
    }

    @Test
    public void testAddDisclosureEmergencyNop() {
        TestExecutorService executor = new TestExecutorService();
        CellularIdentifierDisclosureNotifier notifier =
                new CellularIdentifierDisclosureNotifier(executor, 15, TimeUnit.MINUTES);
        CellularIdentifierDisclosure emergencyDisclosure =
                new CellularIdentifierDisclosure(
                        CellularIdentifierDisclosure.NAS_PROTOCOL_MESSAGE_ATTACH_REQUEST,
                        CellularIdentifierDisclosure.CELLULAR_IDENTIFIER_IMSI,
                        "001001",
                        true);

        notifier.enable();
        notifier.addDisclosure(emergencyDisclosure);

        assertEquals(0, notifier.getCurrentDisclosureCount());
    }

    @Test
    public void testAddDisclosureCountIncrements() {
        TestExecutorService executor = new TestExecutorService();
        CellularIdentifierDisclosureNotifier notifier =
                new CellularIdentifierDisclosureNotifier(executor, 15, TimeUnit.MINUTES);

        notifier.enable();
        notifier.addDisclosure(mDislosure);
        notifier.addDisclosure(mDislosure);
        notifier.addDisclosure(mDislosure);

        assertEquals(3, notifier.getCurrentDisclosureCount());
    }

    @Test
    public void testAddDisclosureThenWindowClose() {
        TestExecutorService executor = new TestExecutorService();
        CellularIdentifierDisclosureNotifier notifier =
                new CellularIdentifierDisclosureNotifier(executor, 15, TimeUnit.MINUTES);

        // One round of disclosures
        notifier.enable();
        notifier.addDisclosure(mDislosure);
        notifier.addDisclosure(mDislosure);
        assertEquals(2, notifier.getCurrentDisclosureCount());

        // Window close should reset the counter
        executor.advanceTime(WINDOW_CLOSE_ADVANCE_MILLIS);
        assertEquals(0, notifier.getCurrentDisclosureCount());

        // A new disclosure should increment as normal
        notifier.addDisclosure(mDislosure);
        assertEquals(1, notifier.getCurrentDisclosureCount());
    }

    @Test
    public void testDisableClosesWindow() {
        TestExecutorService executor = new TestExecutorService();
        CellularIdentifierDisclosureNotifier notifier =
                new CellularIdentifierDisclosureNotifier(executor, 15, TimeUnit.MINUTES);

        // One round of disclosures
        notifier.enable();
        notifier.addDisclosure(mDislosure);
        notifier.addDisclosure(mDislosure);
        assertEquals(2, notifier.getCurrentDisclosureCount());

        notifier.disable();
        assertFalse(notifier.isEnabled());

        // We're disabled now so no disclosures should open the disclosure window
        notifier.addDisclosure(mDislosure);
        assertEquals(0, notifier.getCurrentDisclosureCount());
    }
}