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
This commit is contained in:
Sergey Nikolaienkov
2020-03-26 17:57:40 +01:00
parent d0e4132654
commit d07dc4d4cc
6 changed files with 398 additions and 69 deletions

View File

@@ -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" />

View File

@@ -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

View File

@@ -0,0 +1,44 @@
/*
* 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();
}

View File

@@ -1,5 +1,5 @@
/*
* 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
return;
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()) {
mSessionNotifiedPackages.clear();
hide();
}
// 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);
}
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* 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));
}
};
}

View File

@@ -0,0 +1,80 @@
/*
* 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 com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar.DEBUG;
import android.annotation.UiThread;
import android.app.AppOpsManager;
import android.content.Context;
import android.util.ArraySet;
import android.util.Log;
import java.util.Set;
/**
* The purpose of these class is to detect packages that are conducting audio recording (according
* to {@link AppOpsManager}) and report this to {@link AudioRecordingDisclosureBar}.
*/
class RecordAudioAppOpObserver extends AudioActivityObserver implements
AppOpsManager.OnOpActiveChangedListener {
private static final String TAG = "RecordAudioAppOpObserver";
/**
* Set of the applications that currently are conducting audio recording according to {@link
* AppOpsManager}.
*/
private final Set<String> mActiveAudioRecordingPackages = new ArraySet<>();
RecordAudioAppOpObserver(Context context, OnAudioActivityStateChangeListener listener) {
super(context, listener);
// Register AppOpsManager callback
final AppOpsManager appOpsManager = (AppOpsManager) mContext.getSystemService(
Context.APP_OPS_SERVICE);
appOpsManager.startWatchingActive(
new String[]{AppOpsManager.OPSTR_RECORD_AUDIO},
mContext.getMainExecutor(),
this);
}
@UiThread
@Override
Set<String> getActivePackages() {
return mActiveAudioRecordingPackages;
}
@UiThread
@Override
public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
if (DEBUG) {
Log.d(TAG,
"OP_RECORD_AUDIO active change, active=" + active + ", package="
+ packageName);
}
if (active) {
if (mActiveAudioRecordingPackages.add(packageName)) {
mListener.onAudioActivityStateChange(true, packageName);
}
} else {
if (mActiveAudioRecordingPackages.remove(packageName)) {
mListener.onAudioActivityStateChange(false, packageName);
}
}
}
}