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

Commit d07dc4d4 authored by Sergey Nikolaienkov's avatar Sergey Nikolaienkov
Browse files

Account for mic fgs in AudioRecordingDisclosureBar

Take into account foreground services of type 'microphone' in
AudioRecordingDisclosureBar - watch processes that run such fg services
and treat them as so they are already recording.

Bug: 152364373
Test: make, run audio recording app
Change-Id: I7961ad332d5741a12f029dcc95e909572f30ad05
parent d0e41326
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -255,6 +255,9 @@
    <!-- Query all packages on device on R+ -->
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />

    <!-- Permission to register process observer -->
    <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/>

    <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
    <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
    <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
+3 −1
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.os.UserHandle;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.SystemUI;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar;

import javax.inject.Inject;
import javax.inject.Singleton;
@@ -66,7 +67,8 @@ public class TvStatusBar extends SystemUI implements CommandQueue.Callbacks {
            // If the system process isn't there we're doomed anyway.
        }

        new AudioRecordingDisclosureBar(mContext).start();
        // Creating AudioRecordingDisclosureBar and just letting it run
        new AudioRecordingDisclosureBar(mContext);
    }

    @Override
+44 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.systemui.statusbar.tv.micdisclosure;

import android.content.Context;

import java.util.Set;

/**
 * A base class for implementing observers for different kinds of activities related to audio
 * recording. These observers are to be initialized by {@link AudioRecordingDisclosureBar} and to
 * report back to it.
 */
abstract class AudioActivityObserver {

    interface OnAudioActivityStateChangeListener {
        void onAudioActivityStateChange(boolean active, String packageName);
    }

    final Context mContext;

    final OnAudioActivityStateChangeListener mListener;

    AudioActivityObserver(Context context, OnAudioActivityStateChangeListener listener) {
        mContext = context;
        mListener = listener;
    }

    abstract Set<String> getActivePackages();
}
+79 −68
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 * Copyright (C) 2020 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.
@@ -14,7 +14,7 @@
 * limitations under the License.
 */

package com.android.systemui.statusbar.tv;
package com.android.systemui.statusbar.tv.micdisclosure;

import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

@@ -25,7 +25,6 @@ import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.IntDef;
import android.annotation.UiThread;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -40,6 +39,7 @@ import android.view.WindowManager;
import android.widget.TextView;

import com.android.systemui.R;
import com.android.systemui.statusbar.tv.TvStatusBar;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -54,9 +54,10 @@ import java.util.Set;
 *
 * @see TvStatusBar
 */
class AudioRecordingDisclosureBar {
    private static final String TAG = "AudioRecordingDisclosureBar";
    private static final boolean DEBUG = false;
public class AudioRecordingDisclosureBar implements
        AudioActivityObserver.OnAudioActivityStateChangeListener {
    private static final String TAG = "AudioRecordingDisclosure";
    static final boolean DEBUG = false;

    // This title is used to test the microphone disclosure indicator in
    // CtsSystemUiHostTestCases:TvMicrophoneCaptureIndicatorTest
@@ -98,10 +99,12 @@ class AudioRecordingDisclosureBar {
    private TextView mTextView;

    @State private int mState = STATE_NOT_SHOWN;

    /**
     * Set of the applications that currently are conducting audio recording.
     * Array of the observers that monitor different aspects of the system, such as AppOps and
     * microphone foreground services
     */
    private final Set<String> mActiveAudioRecordingPackages = new ArraySet<>();
    private final AudioActivityObserver[] mAudioActivityObservers;
    /**
     * Set of applications that we've notified the user about since the indicator came up. Meaning
     * that if an application is in this list then at some point since the indicator came up, it
@@ -119,29 +122,52 @@ class AudioRecordingDisclosureBar {
     * one of the two aforementioned states.
     */
    private final Queue<String> mPendingNotificationPackages = new LinkedList<>();
    /**
     * Set of applications for which we make an exception and do not show the indicator. This gets
     * populated once - in {@link #AudioRecordingDisclosureBar(Context)}.
     */
    private final Set<String> mExemptPackages;

    AudioRecordingDisclosureBar(Context context) {
    public AudioRecordingDisclosureBar(Context context) {
        mContext = context;
    }

    void start() {
        // Register AppOpsManager callback
        final AppOpsManager appOpsManager = (AppOpsManager) mContext.getSystemService(
                Context.APP_OPS_SERVICE);
        appOpsManager.startWatchingActive(
                new String[]{AppOpsManager.OPSTR_RECORD_AUDIO},
                mContext.getMainExecutor(),
                new OnActiveRecordingListener());
        mExemptPackages = new ArraySet<>(
                Arrays.asList(mContext.getResources().getStringArray(
                        R.array.audio_recording_disclosure_exempt_apps)));

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

    @UiThread
    private void onStartedRecording(String packageName) {
        if (!mActiveAudioRecordingPackages.add(packageName)) {
            // This app is already known to perform recording
    @Override
    public void onAudioActivityStateChange(boolean active, String packageName) {
        if (DEBUG) {
            Log.d(TAG,
                    "onAudioActivityStateChange, packageName=" + packageName + ", active="
                            + active);
        }

        if (mExemptPackages.contains(packageName)) {
            if (DEBUG) Log.d(TAG, "   - exempt package: ignoring");
            return;
        }

        if (active) {
            showIndicatorForPackageIfNeeded(packageName);
        } else {
            hideIndicatorIfNeeded();
        }
    }

    @UiThread
    private void showIndicatorForPackageIfNeeded(String packageName) {
        if (DEBUG) Log.d(TAG, "showIndicatorForPackageIfNeeded, packageName=" + packageName);
        if (!mSessionNotifiedPackages.add(packageName)) {
            // We've already notified user about this app, no need to do it again.
            if (DEBUG) Log.d(TAG, "   - already notified");
            return;
        }

@@ -167,23 +193,33 @@ class AudioRecordingDisclosureBar {
    }

    @UiThread
    private void onDoneRecording(String packageName) {
        if (!mActiveAudioRecordingPackages.remove(packageName)) {
            // Was not marked as an active recorder, do nothing
    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;

        // If is in the STATE_MINIMIZED, but there are other active recorders - simply ignore.
        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;
            }
        }

        // If not MINIMIZED, will check whether the indicator should be hidden when the indicator
        // comes to the STATE_MINIMIZED eventually. If is in the STATE_MINIMIZED, but there are
        // other active recorders - simply ignore.
        if (mState == STATE_MINIMIZED && mActiveAudioRecordingPackages.isEmpty()) {
        // Clear the state and hide the indicator.
        mSessionNotifiedPackages.clear();
        hide();
    }
    }

    @UiThread
    private void show(String packageName) {
        final String label = getApplicationLabel(packageName);
        if (DEBUG) {
            Log.d(TAG, "Showing indicator for " + packageName + " (" + label + ")...");
        }

        // Inflate the indicator view
        mIndicatorView = LayoutInflater.from(mContext).inflate(
                R.layout.tv_audio_recording_indicator,
@@ -196,7 +232,6 @@ class AudioRecordingDisclosureBar {
        mBgRight = mIndicatorView.findViewById(R.id.bg_right);

        // Set up the notification text
        final String label = getApplicationLabel(packageName);
        mTextView.setText(mContext.getString(R.string.app_accessed_mic, label));

        // Initially change the visibility to INVISIBLE, wait until and receives the size and
@@ -260,6 +295,9 @@ class AudioRecordingDisclosureBar {
    @UiThread
    private void expand(String packageName) {
        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();
@@ -283,6 +321,7 @@ class AudioRecordingDisclosureBar {

    @UiThread
    private void minimize() {
        if (DEBUG) Log.d(TAG, "Minimizing...");
        final int targetOffset = mTextsContainers.getWidth();
        final AnimatorSet set = new AnimatorSet();
        set.playTogether(
@@ -305,6 +344,7 @@ class AudioRecordingDisclosureBar {

    @UiThread
    private void hide() {
        if (DEBUG) Log.d(TAG, "Hiding...");
        final int targetOffset =
                mIndicatorView.getWidth() - (int) mIconTextsContainer.getTranslationX();
        final AnimatorSet set = new AnimatorSet();
@@ -326,6 +366,7 @@ class AudioRecordingDisclosureBar {

    @UiThread
    private void onExpanded() {
        if (DEBUG) Log.d(TAG, "Expanded");
        mState = STATE_SHOWN;

        mIndicatorView.postDelayed(this::minimize, MAXIMIZED_DURATION);
@@ -333,20 +374,21 @@ class AudioRecordingDisclosureBar {

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

        if (!mPendingNotificationPackages.isEmpty()) {
            // There is a new application that started recording, tell the user about it.
            expand(mPendingNotificationPackages.poll());
        } else if (mActiveAudioRecordingPackages.isEmpty()) {
            // Nobody is recording anymore, clear state and remove the indicator.
            mSessionNotifiedPackages.clear();
            hide();
        } else {
            hideIndicatorIfNeeded();
        }
    }

    @UiThread
    private void onHidden() {
        if (DEBUG) Log.d(TAG, "Hidden");

        final WindowManager windowManager = (WindowManager) mContext.getSystemService(
                Context.WINDOW_SERVICE);
        windowManager.removeView(mIndicatorView);
@@ -392,35 +434,4 @@ class AudioRecordingDisclosureBar {
        }
        return pm.getApplicationLabel(appInfo).toString();
    }

    private class OnActiveRecordingListener implements AppOpsManager.OnOpActiveChangedListener {
        private final Set<String> mExemptApps;

        private OnActiveRecordingListener() {
            mExemptApps = new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(
                    R.array.audio_recording_disclosure_exempt_apps)));
        }

        @Override
        public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
            if (DEBUG) {
                Log.d(TAG,
                        "OP_RECORD_AUDIO active change, active=" + active + ", app="
                                + packageName);
            }

            if (mExemptApps.contains(packageName)) {
                if (DEBUG) {
                    Log.d(TAG, "\t- exempt app");
                }
                return;
            }

            if (active) {
                onStartedRecording(packageName);
            } else {
                onDoneRecording(packageName);
            }
        }
    }
}
+189 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.systemui.statusbar.tv.micdisclosure;

import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;

import static com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar.DEBUG;

import android.annotation.UiThread;
import android.app.ActivityManager;
import android.app.IActivityManager;
import android.app.IProcessObserver;
import android.content.Context;
import android.os.RemoteException;
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;

/**
 * The purpose of these class is to detect packages that are running foreground services of type
 * 'microphone' and to report back to {@link AudioRecordingDisclosureBar}.
 */
class MicrophoneForegroundServicesObserver extends AudioActivityObserver {
    private static final String TAG = "MicrophoneForegroundServicesObserver";
    private static final boolean ENABLED = true;

    private final 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).
     */
    private final SparseArray<String[]> mPidToPackages = new SparseArray<>();
    /**
     * A dictionary that maps "active" packages to the number of the "active" processes associated
     * with those packages. We really only need this in case when one application is running in
     * multiple processes, so that we don't lose track of the package when one of its "active"
     * processes ceases, while others remain "active".
     */
    private final Map<String, Integer> mPackageToProcessCount = new ArrayMap<>();

    MicrophoneForegroundServicesObserver(Context context,
            OnAudioActivityStateChangeListener listener) {
        super(context, listener);

        mActivityManager = ActivityManager.getService();
        try {
            mActivityManager.registerProcessObserver(mProcessObserver);
        } catch (RemoteException e) {
            Log.e(TAG, "Couldn't register process observer", e);
        }
    }

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

    @UiThread
    private void onProcessForegroundServicesChanged(int pid, boolean hasMicFgs) {
        final String[] changedPackages;
        if (hasMicFgs) {
            if (mPidToPackages.contains(pid)) {
                // We are already tracking this pid - ignore.
                changedPackages = null;
            } else {
                changedPackages = getPackageNames(pid);
                mPidToPackages.append(pid, changedPackages);
            }
        } else {
            changedPackages = mPidToPackages.removeReturnOld(pid);
        }

        if (changedPackages == null) {
            return;
        }

        for (int index = changedPackages.length - 1; index >= 0; index--) {
            final String packageName = changedPackages[index];
            int processCount = mPackageToProcessCount.getOrDefault(packageName, 0);
            final boolean shouldNotify;
            if (hasMicFgs) {
                processCount++;
                shouldNotify = processCount == 1;
            } else {
                processCount--;
                shouldNotify = processCount == 0;
            }
            if (processCount > 0) {
                mPackageToProcessCount.put(packageName, processCount);
            } else {
                mPackageToProcessCount.remove(packageName);
            }
            if (shouldNotify) notifyPackageStateChanged(packageName, hasMicFgs);
        }
    }

    @UiThread
    private void onProcessDied(int pid) {
        final String[] packages = mPidToPackages.removeReturnOld(pid);
        if (packages == null) {
            // This PID was not active - ignore.
            return;
        }

        for (int index = packages.length - 1; index >= 0; index--) {
            final String packageName = packages[index];
            int processCount = mPackageToProcessCount.getOrDefault(packageName, 0);
            if (processCount <= 0) {
                Log.e(TAG, "Bookkeeping error, process count for " + packageName + " is "
                        + processCount);
                continue;
            }
            processCount--;
            if (processCount > 0) {
                mPackageToProcessCount.put(packageName, processCount);
            } else {
                mPackageToProcessCount.remove(packageName);
                notifyPackageStateChanged(packageName, false);
            }
        }
    }

    @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 (ENABLED) mListener.onAudioActivityStateChange(active, packageName);
    }

    @UiThread
    private String[] getPackageNames(int pid) {
        final List<ActivityManager.RunningAppProcessInfo> runningApps;
        try {
            runningApps = mActivityManager.getRunningAppProcesses();
        } catch (RemoteException e) {
            Log.d(TAG, "Couldn't get package name for pid=" + pid);
            return null;
        }
        if (runningApps == null) {
            Log.wtf(TAG, "No running apps reported");
        }
        for (ActivityManager.RunningAppProcessInfo app : runningApps) {
            if (app.pid == pid) {
                return app.pkgList;
            }
        }
        return null;
    }

    private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
        @Override
        public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {}

        @Override
        public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {
            mContext.getMainExecutor().execute(() -> onProcessForegroundServicesChanged(pid,
                    (serviceTypes & FOREGROUND_SERVICE_TYPE_MICROPHONE) != 0));
        }

        @Override
        public void onProcessDied(int pid, int uid) {
            mContext.getMainExecutor().execute(
                    () -> MicrophoneForegroundServicesObserver.this.onProcessDied(pid));
        }
    };
}
Loading