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

Commit 7921255f authored by Sergey Nikolaienkov's avatar Sergey Nikolaienkov
Browse files

Add Setting kill switch for mic disclosure

Disable if mic disclosure if global settings
"sysui_mic_disclosure_enable" is 0. If the setting is not present -
enable.

Bug: 162308361
Test: adb shell appops start com.google.android.katniss 27 # indicator
comes up
      adb shell settings put global sysui_mic_disclosure_enable 0 #
indicator disappears
      adb shell appops stop com.google.android.katniss 27
      adb shell appops start com.google.android.katniss 27 # indicator
does not come up
      adb shell settings put global sysui_mic_disclosure_enable 1
      adb shell appops start com.google.android.katniss 27 # indicator
comes up

Change-Id: I7317fe58676615578a07f0b37ae5a8cd99db5576
parent 2ea424ea
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -40,5 +40,9 @@ abstract class AudioActivityObserver {
        mListener = listener;
    }

    abstract void start();

    abstract void stop();

    abstract Set<String> getActivePackages();
}
+117 −25
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.annotation.UiThread;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.graphics.PixelFormat;
import android.provider.Settings;
import android.text.TextUtils;
@@ -65,11 +66,13 @@ public class AudioRecordingDisclosureBar implements
    // CtsSystemUiHostTestCases:TvMicrophoneCaptureIndicatorTest
    private static final String LAYOUT_PARAMS_TITLE = "MicrophoneCaptureIndicator";

    private static final String ENABLE_FLAG = "sysui_mic_disclosure_enable";
    private static final String EXEMPT_PACKAGES_LIST = "sysui_mic_disclosure_exempt";
    private static final String FORCED_PACKAGES_LIST = "sysui_mic_disclosure_forced";

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = {"STATE_"}, value = {
            STATE_STOPPED,
            STATE_NOT_SHOWN,
            STATE_APPEARING,
            STATE_SHOWN,
@@ -80,6 +83,7 @@ public class AudioRecordingDisclosureBar implements
    })
    public @interface State {}

    private static final int STATE_STOPPED = -1;
    private static final int STATE_NOT_SHOWN = 0;
    private static final int STATE_APPEARING = 1;
    private static final int STATE_SHOWN = 2;
@@ -94,6 +98,7 @@ public class AudioRecordingDisclosureBar implements
    private static final float PULSE_SCALE = 1.25f;

    private final Context mContext;
    private boolean mIsEnabledInSettings;

    private View mIndicatorView;
    private View mIconTextsContainer;
@@ -104,13 +109,13 @@ public class AudioRecordingDisclosureBar implements
    private TextView mTextView;
    private boolean mIsLtr;

    @State private int mState = STATE_NOT_SHOWN;
    @State private int mState = STATE_STOPPED;

    /**
     * Array of the observers that monitor different aspects of the system, such as AppOps and
     * microphone foreground services
     */
    private final AudioActivityObserver[] mAudioActivityObservers;
    private AudioActivityObserver[] mAudioActivityObservers;
    /**
     * Whether the indicator should expand and show the recording application's label.
     * If disabled ({@code false}) the "minimized" ({@link #STATE_MINIMIZED}) indicator would appear
@@ -144,6 +149,7 @@ public class AudioRecordingDisclosureBar implements
    public AudioRecordingDisclosureBar(Context context) {
        mContext = context;

        // Loading configs
        mRevealRecordingPackages = mContext.getResources().getBoolean(
                R.bool.audio_recording_disclosure_reveal_packages);
        mExemptPackages = new ArraySet<>(
@@ -152,12 +158,54 @@ public class AudioRecordingDisclosureBar implements
        mExemptPackages.addAll(Arrays.asList(getGlobalStringArray(EXEMPT_PACKAGES_LIST)));
        mExemptPackages.removeAll(Arrays.asList(getGlobalStringArray(FORCED_PACKAGES_LIST)));

        // Check setting, and start if enabled
        mIsEnabledInSettings = checkIfEnabledInSettings();
        registerSettingsObserver();
        if (mIsEnabledInSettings) {
            start();
        }
    }

    @UiThread
    private void start() {
        if (mState != STATE_STOPPED) {
            return;
        }
        mState = STATE_NOT_SHOWN;

        if (mAudioActivityObservers == null) {
            mAudioActivityObservers = new AudioActivityObserver[]{
                    new RecordAudioAppOpObserver(mContext, this),
                    new MicrophoneForegroundServicesObserver(mContext, this),
            };
        }

        for (int i = mAudioActivityObservers.length - 1; i >= 0; i--) {
            mAudioActivityObservers[i].start();
        }
    }

    @UiThread
    private void stop() {
        if (mState == STATE_STOPPED) {
            return;
        }
        mState = STATE_STOPPED;

        for (int i = mAudioActivityObservers.length - 1; i >= 0; i--) {
            mAudioActivityObservers[i].stop();
        }

        // Remove the view if shown.
        if (mState != STATE_NOT_SHOWN) {
            removeIndicatorView();
        }

        // Clean up the state.
        mSessionNotifiedPackages.clear();
        mPendingNotificationPackages.clear();
    }

    @UiThread
    @Override
    public void onAudioActivityStateChange(boolean active, String packageName) {
@@ -213,7 +261,6 @@ public class AudioRecordingDisclosureBar implements

    @UiThread
    private void hideIndicatorIfNeeded() {
        if (DEBUG) Log.d(TAG, "hideIndicatorIfNeeded");
        // If not MINIMIZED, will check whether the indicator should be hidden when the indicator
        // comes to the STATE_MINIMIZED eventually.
        if (mState != STATE_MINIMIZED) return;
@@ -222,7 +269,6 @@ public class AudioRecordingDisclosureBar implements
        for (int index = mAudioActivityObservers.length - 1; index >= 0; index--) {
            for (String activePackage : mAudioActivityObservers[index].getActivePackages()) {
                if (mExemptPackages.contains(activePackage)) continue;
                if (DEBUG) Log.d(TAG, "   - there are still ongoing activities");
                return;
            }
        }
@@ -235,7 +281,7 @@ public class AudioRecordingDisclosureBar implements
    @UiThread
    private void show(String packageName) {
        if (DEBUG) {
            Log.d(TAG, "Showing indicator for " + packageName);
            Log.d(TAG, "Showing indicator");
        }

        mIsLtr = mContext.getResources().getConfiguration().getLayoutDirection()
@@ -286,6 +332,10 @@ public class AudioRecordingDisclosureBar implements
                        new ViewTreeObserver.OnGlobalLayoutListener() {
                            @Override
                            public void onGlobalLayout() {
                                if (mState == STATE_STOPPED) {
                                    return;
                                }

                                // Remove the observer
                                mIndicatorView.getViewTreeObserver().removeOnGlobalLayoutListener(
                                        this);
@@ -306,6 +356,10 @@ public class AudioRecordingDisclosureBar implements
                                            @Override
                                            public void onAnimationStart(Animator animation,
                                                    boolean isReverse) {
                                                if (mState == STATE_STOPPED) {
                                                    return;
                                                }

                                                // Indicator is INVISIBLE at the moment, change it.
                                                mIndicatorView.setVisibility(View.VISIBLE);
                                            }
@@ -345,9 +399,6 @@ public class AudioRecordingDisclosureBar implements
        assertRevealingRecordingPackages();

        final String label = getApplicationLabel(packageName);
        if (DEBUG) {
            Log.d(TAG, "Expanding for " + packageName + " (" + label + ")...");
        }
        mTextView.setText(mContext.getString(R.string.app_accessed_mic, label));

        final AnimatorSet set = new AnimatorSet();
@@ -373,7 +424,6 @@ public class AudioRecordingDisclosureBar implements
    private void minimize() {
        assertRevealingRecordingPackages();

        if (DEBUG) Log.d(TAG, "Minimizing...");
        final int targetOffset = (mIsLtr ? 1 : -1) * mTextsContainers.getWidth();
        final AnimatorSet set = new AnimatorSet();
        set.playTogether(
@@ -396,7 +446,9 @@ public class AudioRecordingDisclosureBar implements

    @UiThread
    private void hide() {
        if (DEBUG) Log.d(TAG, "Hiding...");
        if (DEBUG) {
            Log.d(TAG, "Hide indicator");
        }
        final int targetOffset = (mIsLtr ? 1 : -1) * (mIndicatorView.getWidth()
                - (int) mIconTextsContainer.getTranslationX());
        final AnimatorSet set = new AnimatorSet();
@@ -418,9 +470,12 @@ public class AudioRecordingDisclosureBar implements

    @UiThread
    private void onExpanded() {
        if (mState == STATE_STOPPED) {
            return;
        }

        assertRevealingRecordingPackages();

        if (DEBUG) Log.d(TAG, "Expanded");
        mState = STATE_SHOWN;

        mIndicatorView.postDelayed(this::minimize, MAXIMIZED_DURATION);
@@ -428,7 +483,10 @@ public class AudioRecordingDisclosureBar implements

    @UiThread
    private void onMinimized() {
        if (DEBUG) Log.d(TAG, "Minimized");
        if (mState == STATE_STOPPED) {
            return;
        }

        mState = STATE_MINIMIZED;

        if (mRevealRecordingPackages) {
@@ -443,8 +501,21 @@ public class AudioRecordingDisclosureBar implements

    @UiThread
    private void onHidden() {
        if (DEBUG) Log.d(TAG, "Hidden");
        if (mState == STATE_STOPPED) {
            return;
        }

        removeIndicatorView();
        mState = STATE_NOT_SHOWN;

        // Check if anybody started recording while we were in STATE_DISAPPEARING
        if (!mPendingNotificationPackages.isEmpty()) {
            // There is a new application that started recording, tell the user about it.
            show(mPendingNotificationPackages.poll());
        }
    }

    private void removeIndicatorView() {
        final WindowManager windowManager = (WindowManager) mContext.getSystemService(
                Context.WINDOW_SERVICE);
        windowManager.removeView(mIndicatorView);
@@ -456,14 +527,6 @@ public class AudioRecordingDisclosureBar implements
        mTextsContainers = null;
        mTextView = null;
        mBgEnd = null;

        mState = STATE_NOT_SHOWN;

        // Check if anybody started recording while we were in STATE_DISAPPEARING
        if (!mPendingNotificationPackages.isEmpty()) {
            // There is a new application that started recording, tell the user about it.
            show(mPendingNotificationPackages.poll());
        }
    }

    @UiThread
@@ -504,4 +567,33 @@ public class AudioRecordingDisclosureBar implements
                    DEBUG ? new RuntimeException("Should not be called") : null);
        }
    }

    private boolean checkIfEnabledInSettings() {
        // 0 = disabled, everything else = enabled. Enabled by default.
        return Settings.Global.getInt(mContext.getContentResolver(),
                ENABLE_FLAG, 1) != 0;
    }

    private void registerSettingsObserver() {
        final ContentObserver contentObserver = new ContentObserver(
                mContext.getMainThreadHandler()) {
            @Override
            public void onChange(boolean selfChange) {
                if (mIsEnabledInSettings == checkIfEnabledInSettings()) {
                    // Nothing changed as we know it - ignore.
                    return;
                }

                // Things changed: flip the flag.
                mIsEnabledInSettings = !mIsEnabledInSettings;
                if (mIsEnabledInSettings) {
                    start();
                } else {
                    stop();
                }
            }
        };
        mContext.getContentResolver().registerContentObserver(
                Settings.Global.getUriFor(ENABLE_FLAG), false, contentObserver);
    }
}
+20 −9
Original line number Diff line number Diff line
@@ -30,7 +30,6 @@ import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -41,9 +40,8 @@ import java.util.Set;
 */
class MicrophoneForegroundServicesObserver extends AudioActivityObserver {
    private static final String TAG = "MicrophoneForegroundServicesObserver";
    private static final boolean ENABLED = true;

    private final IActivityManager mActivityManager;
    private IActivityManager mActivityManager;
    /**
     * A dictionary that maps PIDs to the package names. We only keep track of the PIDs that are
     * "active" (those that are running FGS with FOREGROUND_SERVICE_TYPE_MICROPHONE flag).
@@ -60,7 +58,10 @@ class MicrophoneForegroundServicesObserver extends AudioActivityObserver {
    MicrophoneForegroundServicesObserver(Context context,
            OnAudioActivityStateChangeListener listener) {
        super(context, listener);
    }

    @Override
    void start() {
        mActivityManager = ActivityManager.getService();
        try {
            mActivityManager.registerProcessObserver(mProcessObserver);
@@ -69,9 +70,20 @@ class MicrophoneForegroundServicesObserver extends AudioActivityObserver {
        }
    }

    @Override
    void stop() {
        try {
            mActivityManager.unregisterProcessObserver(mProcessObserver);
        } catch (RemoteException e) {
            Log.e(TAG, "Couldn't unregister process observer", e);
        }
        mActivityManager = null;
        mPackageToProcessCount.clear();
    }

    @Override
    Set<String> getActivePackages() {
        return ENABLED ? mPackageToProcessCount.keySet() : Collections.emptySet();
        return mPackageToProcessCount.keySet();
    }

    @UiThread
@@ -141,13 +153,12 @@ class MicrophoneForegroundServicesObserver extends AudioActivityObserver {

    @UiThread
    private void notifyPackageStateChanged(String packageName, boolean active) {
        if (active) {
            if (DEBUG) Log.d(TAG, "New microphone fgs detected, package=" + packageName);
        } else {
            if (DEBUG) Log.d(TAG, "Microphone fgs is gone, package=" + packageName);
        if (DEBUG) {
            Log.d(TAG, (active ? "New microphone fgs detected" : "Microphone fgs is gone")
                    + ", package=" + packageName);
        }

        if (ENABLED) mListener.onAudioActivityStateChange(active, packageName);
        mListener.onAudioActivityStateChange(active, packageName);
    }

    @UiThread
+25 −6
Original line number Diff line number Diff line
@@ -42,16 +42,35 @@ class RecordAudioAppOpObserver extends AudioActivityObserver implements

    RecordAudioAppOpObserver(Context context, OnAudioActivityStateChangeListener listener) {
        super(context, listener);
    }

    @Override
    void start() {
        if (DEBUG) {
            Log.d(TAG, "Start");
        }

        // Register AppOpsManager callback
        final AppOpsManager appOpsManager = (AppOpsManager) mContext.getSystemService(
                Context.APP_OPS_SERVICE);
        appOpsManager.startWatchingActive(
        mContext.getSystemService(AppOpsManager.class)
                .startWatchingActive(
                        new String[]{AppOpsManager.OPSTR_RECORD_AUDIO},
                        mContext.getMainExecutor(),
                        this);
    }

    @Override
    void stop() {
        if (DEBUG) {
            Log.d(TAG, "Stop");
        }

        // Unregister AppOpsManager callback
        mContext.getSystemService(AppOpsManager.class).stopWatchingActive(this);

        // Clean up state
        mActiveAudioRecordingPackages.clear();
    }

    @UiThread
    @Override
    Set<String> getActivePackages() {