Merge "Reuse phone privacy indicator impl. for tv" into sc-dev

This commit is contained in:
Jacqueline Bronger
2021-02-23 12:46:46 +00:00
committed by Android (Google) Code Review
11 changed files with 171 additions and 589 deletions

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<FrameLayout
android:layout_width="34dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:background="@drawable/tv_rect_shadow_rounded">
<ImageView
android:layout_width="13dp"
android:layout_height="13dp"
android:layout_gravity="center"
android:src="@drawable/tv_ic_mic_white"/>
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:layout_gravity="center">
<LinearLayout
android:id="@+id/icons_container"
android:background="@drawable/tv_rect_shadow_rounded"
android:padding="@dimen/privacy_chip_icon_padding"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"/>
</FrameLayout>

View File

@@ -28,6 +28,7 @@
<string-array name="config_systemUIServiceComponents" translatable="false">
<item>com.android.systemui.util.NotificationChannels</item>
<item>com.android.systemui.volume.VolumeUI</item>
<item>com.android.systemui.privacy.television.TvOngoingPrivacyChip</item>
<item>com.android.systemui.statusbar.tv.TvStatusBar</item>
<item>com.android.systemui.statusbar.tv.notifications.TvNotificationPanel</item>
<item>com.android.systemui.statusbar.tv.notifications.TvNotificationHandler</item>

View File

@@ -17,4 +17,8 @@
<resources>
<!-- Opacity at which the background for the shutdown UI will be drawn. -->
<item name="shutdown_scrim_behind_alpha" format="float" type="dimen">1.0</item>
<dimen name="privacy_chip_icon_margin">3dp</dimen>
<dimen name="privacy_chip_icon_padding">8dp</dimen>
<dimen name="privacy_chip_icon_size">13dp</dimen>
</resources>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 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.
-->
<resources>
<!-- Whether to enable microphone disclosure indicator
(com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar). -->
<bool name="audio_recording_disclosure_enabled">true</bool>
</resources>

View File

@@ -29,6 +29,7 @@ import com.android.systemui.keyguard.dagger.KeyguardModule;
import com.android.systemui.media.systemsounds.HomeSoundEffectController;
import com.android.systemui.people.widget.PeopleSpaceWidgetEnabler;
import com.android.systemui.power.PowerUI;
import com.android.systemui.privacy.television.TvOngoingPrivacyChip;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsModule;
import com.android.systemui.shortcut.ShortcutKeyDispatcher;
@@ -155,6 +156,12 @@ public abstract class SystemUIBinder {
@ClassKey(TvNotificationPanel.class)
public abstract SystemUI bindsTvNotificationPanel(TvNotificationPanel sysui);
/** Inject into TvOngoingPrivacyChip. */
@Binds
@IntoMap
@ClassKey(TvOngoingPrivacyChip.class)
public abstract SystemUI bindsTvOngoingPrivacyChip(TvOngoingPrivacyChip sysui);
/** Inject into VolumeUI. */
@Binds
@IntoMap

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 The Android Open Source Project
* Copyright (C) 2021 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,9 +14,8 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.tv.micdisclosure;
package com.android.systemui.privacy.television;
import static android.provider.DeviceConfig.NAMESPACE_PRIVACY;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import android.animation.Animator;
@@ -25,57 +24,56 @@ import android.animation.ObjectAnimator;
import android.annotation.IntDef;
import android.annotation.UiThread;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.ArraySet;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.android.systemui.R;
import com.android.systemui.statusbar.tv.TvStatusBar;
import com.android.systemui.SystemUI;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.privacy.PrivacyChipBuilder;
import com.android.systemui.privacy.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
/**
* A component of {@link TvStatusBar} responsible for notifying the user whenever an application is
* recording audio.
*
* @see TvStatusBar
* A SystemUI component responsible for notifying the user whenever an application is
* recording audio, accessing the camera or accessing the location.
*/
public class AudioRecordingDisclosureBar implements
AudioActivityObserver.OnAudioActivityStateChangeListener {
private static final String TAG = "AudioRecordingDisclosure";
@SysUISingleton
public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemController.Callback {
private static final String TAG = "TvOngoingPrivacyChip";
static final boolean DEBUG = false;
// This title is used to test the microphone disclosure indicator in
// CtsSystemUiHostTestCases:TvMicrophoneCaptureIndicatorTest
// This title is used in CameraMicIndicatorsPermissionTest and
// RecognitionServiceMicIndicatorTest.
private static final String LAYOUT_PARAMS_TITLE = "MicrophoneCaptureIndicator";
private static final String ENABLED_FLAG = "mic_disclosure_enabled";
private static final String EXEMPT_PACKAGES_LIST = "mic_disclosure_exempt_packages";
private static final String FORCED_PACKAGES_LIST = "mic_disclosure_forced_packages";
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"STATE_"}, value = {
STATE_STOPPED,
STATE_NOT_SHOWN,
STATE_APPEARING,
STATE_SHOWN,
STATE_DISAPPEARING
})
public @interface State {}
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;
@@ -84,106 +82,89 @@ public class AudioRecordingDisclosureBar implements
private static final int ANIMATION_DURATION_MS = 200;
private final Context mContext;
private boolean mIsEnabled;
private final PrivacyItemController mPrivacyItemController;
private View mIndicatorView;
private boolean mViewAndWindowAdded;
private ObjectAnimator mAnimator;
@State private int mState = STATE_STOPPED;
private boolean mAllIndicatorsFlagEnabled;
private boolean mMicCameraIndicatorFlagEnabled;
private boolean mLocationIndicatorEnabled;
private List<PrivacyItem> mPrivacyItems;
/**
* Array of the observers that monitor different aspects of the system, such as AppOps and
* microphone foreground services
*/
private AudioActivityObserver[] mAudioActivityObservers;
/**
* 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 = new ArraySet<>();
private LinearLayout mIconsContainer;
private final int mIconSize;
private final int mIconMarginStart;
public AudioRecordingDisclosureBar(Context context) {
@State
private int mState = STATE_NOT_SHOWN;
@Inject
public TvOngoingPrivacyChip(Context context, PrivacyItemController privacyItemController) {
super(context);
Log.d(TAG, "Privacy chip running without id");
mContext = context;
mPrivacyItemController = privacyItemController;
// Load configs
reloadExemptPackages();
Resources res = mContext.getResources();
mIconMarginStart = Math.round(res.getDimension(R.dimen.privacy_chip_icon_margin));
mIconSize = res.getDimensionPixelSize(R.dimen.privacy_chip_icon_size);
mIsEnabled = DeviceConfig.getBoolean(NAMESPACE_PRIVACY, ENABLED_FLAG, true);
// Start if enabled
if (mIsEnabled) {
start();
}
mAllIndicatorsFlagEnabled = privacyItemController.getAllIndicatorsAvailable();
mMicCameraIndicatorFlagEnabled = privacyItemController.getMicCameraAvailable();
mLocationIndicatorEnabled = privacyItemController.getLocationAvailable();
// Set up a config change listener
DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_PRIVACY, mContext.getMainExecutor(),
mConfigChangeListener);
}
private void reloadExemptPackages() {
mExemptPackages.clear();
mExemptPackages.addAll(Arrays.asList(mContext.getResources().getStringArray(
R.array.audio_recording_disclosure_exempt_apps)));
mExemptPackages.addAll(
splitByComma(
DeviceConfig.getString(NAMESPACE_PRIVACY, EXEMPT_PACKAGES_LIST, null)));
mExemptPackages.removeAll(
splitByComma(
DeviceConfig.getString(NAMESPACE_PRIVACY, FORCED_PACKAGES_LIST, null)));
}
@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();
}
}
@UiThread
@Override
public void onAudioActivityStateChange(boolean active, String packageName) {
if (DEBUG) {
Log.d(TAG,
"onAudioActivityStateChange, packageName=" + packageName + ", active="
+ active);
Log.d(TAG, "allIndicators: " + mAllIndicatorsFlagEnabled);
Log.d(TAG, "micCameraIndicators: " + mMicCameraIndicatorFlagEnabled);
Log.d(TAG, "locationIndicators: " + mLocationIndicatorEnabled);
}
}
if (mExemptPackages.contains(packageName)) {
if (DEBUG) Log.d(TAG, " - exempt package: ignoring");
return;
}
@Override
public void start() {
mPrivacyItemController.addCallback(this);
}
if (active) {
showIfNeeded();
@Override
public void onPrivacyItemsChanged(List<PrivacyItem> privacyItems) {
if (DEBUG) Log.d(TAG, "PrivacyItemsChanged");
mPrivacyItems = privacyItems;
updateUI();
}
@Override
public void onFlagAllChanged(boolean flag) {
if (DEBUG) Log.d(TAG, "all indicators enabled: " + flag);
mAllIndicatorsFlagEnabled = flag;
}
@Override
public void onFlagMicCameraChanged(boolean flag) {
if (DEBUG) Log.d(TAG, "mic/camera indicators enabled: " + flag);
mMicCameraIndicatorFlagEnabled = flag;
}
@Override
public void onFlagLocationChanged(boolean flag) {
if (DEBUG) Log.d(TAG, "location indicators enabled: " + flag);
mLocationIndicatorEnabled = flag;
}
private void updateUI() {
if (DEBUG) Log.d(TAG, mPrivacyItems.size() + " privacy items");
if ((mMicCameraIndicatorFlagEnabled || mAllIndicatorsFlagEnabled
|| mLocationIndicatorEnabled) && !mPrivacyItems.isEmpty()) {
if (mState == STATE_NOT_SHOWN || mState == STATE_DISAPPEARING) {
showIndicator();
} else {
if (DEBUG) Log.d(TAG, "only updating icons");
PrivacyChipBuilder builder = new PrivacyChipBuilder(mContext, mPrivacyItems);
setIcons(builder.generateIcons(), mIconsContainer);
mIconsContainer.requestLayout();
}
} else {
hideIndicatorIfNeeded();
}
@@ -191,12 +172,7 @@ public class AudioRecordingDisclosureBar implements
@UiThread
private void hideIndicatorIfNeeded() {
// If STOPPED, NOT_SHOWN or DISAPPEARING - nothing else for us to do here.
if (mState != STATE_SHOWN && mState != STATE_APPEARING) return;
if (hasActiveRecorders()) {
return;
}
if (mState == STATE_NOT_SHOWN || mState == STATE_DISAPPEARING) return;
if (mViewAndWindowAdded) {
mState = STATE_DISAPPEARING;
@@ -210,23 +186,12 @@ public class AudioRecordingDisclosureBar implements
}
@UiThread
private void showIfNeeded() {
// If STOPPED, SHOWN or APPEARING - nothing else for us to do here.
if (mState != STATE_NOT_SHOWN && mState != STATE_DISAPPEARING) return;
if (DEBUG) Log.d(TAG, "Showing indicator");
final int prevState = mState;
private void showIndicator() {
mState = STATE_APPEARING;
if (prevState == STATE_DISAPPEARING) {
animateAppearance();
return;
}
// Inflate the indicator view
mIndicatorView = LayoutInflater.from(mContext).inflate(
R.layout.tv_audio_recording_indicator, null);
R.layout.tv_ongoing_privacy_chip, null);
// 1. Set alpha to 0.
// 2. Wait until the window is shown and the view is laid out.
@@ -239,7 +204,7 @@ public class AudioRecordingDisclosureBar implements
@Override
public void onGlobalLayout() {
// State could have changed to NOT_SHOWN (if all the recorders are
// already gone) to STOPPED (if the indicator was disabled)
// already gone)
if (mState != STATE_APPEARING) return;
mViewAndWindowAdded = true;
@@ -251,22 +216,41 @@ public class AudioRecordingDisclosureBar implements
}
});
final boolean isLtr = mContext.getResources().getConfiguration().getLayoutDirection()
== View.LAYOUT_DIRECTION_LTR;
mIconsContainer = mIndicatorView.findViewById(R.id.icons_container);
PrivacyChipBuilder builder = new PrivacyChipBuilder(mContext, mPrivacyItems);
setIcons(builder.generateIcons(), mIconsContainer);
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
WRAP_CONTENT,
WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
layoutParams.gravity = Gravity.TOP | (isLtr ? Gravity.RIGHT : Gravity.LEFT);
layoutParams.gravity = Gravity.TOP | Gravity.END;
layoutParams.setTitle(LAYOUT_PARAMS_TITLE);
layoutParams.packageName = mContext.getPackageName();
final WindowManager windowManager = (WindowManager) mContext.getSystemService(
Context.WINDOW_SERVICE);
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
windowManager.addView(mIndicatorView, layoutParams);
}
private void setIcons(List<Drawable> icons, ViewGroup iconsContainer) {
iconsContainer.removeAllViews();
for (int i = 0; i < icons.size(); i++) {
Drawable icon = icons.get(i);
icon.mutate().setTint(Color.WHITE);
ImageView imageView = new ImageView(mContext);
imageView.setImageDrawable(icon);
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
mIconsContainer.addView(imageView, mIconSize, mIconSize);
if (i != 0) {
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) imageView.getLayoutParams();
layoutParams.setMarginStart(mIconMarginStart);
imageView.setLayoutParams(layoutParams);
}
}
}
private void animateAppearance() {
animateAlphaTo(1f);
@@ -333,22 +317,13 @@ public class AudioRecordingDisclosureBar implements
}
}
private boolean hasActiveRecorders() {
for (int index = mAudioActivityObservers.length - 1; index >= 0; index--) {
for (String activePackage : mAudioActivityObservers[index].getActivePackages()) {
if (mExemptPackages.contains(activePackage)) continue;
return true;
}
}
return false;
}
private void removeIndicatorView() {
if (DEBUG) Log.d(TAG, "removeIndicatorView");
final WindowManager windowManager = (WindowManager) mContext.getSystemService(
Context.WINDOW_SERVICE);
windowManager.removeView(mIndicatorView);
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
if (windowManager != null) {
windowManager.removeView(mIndicatorView);
}
mIndicatorView = null;
mAnimator = null;
@@ -356,26 +331,4 @@ public class AudioRecordingDisclosureBar implements
mViewAndWindowAdded = false;
}
private static List<String> splitByComma(String string) {
return TextUtils.isEmpty(string) ? Collections.emptyList() : Arrays.asList(
string.split(","));
}
private final DeviceConfig.OnPropertiesChangedListener mConfigChangeListener =
new DeviceConfig.OnPropertiesChangedListener() {
@Override
public void onPropertiesChanged(DeviceConfig.Properties properties) {
reloadExemptPackages();
// Check if was enabled/disabled
if (mIsEnabled != properties.getBoolean(ENABLED_FLAG, true)) {
mIsEnabled = !mIsEnabled;
if (mIsEnabled) {
start();
} else {
stop();
}
}
}
};
}

View File

@@ -22,12 +22,10 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar;
import javax.inject.Inject;
@@ -36,11 +34,6 @@ import dagger.Lazy;
/**
* Status bar implementation for "large screen" products that mostly present no on-screen nav.
* Serves as a collection of UI components, rather than showing its own UI.
* The following is the list of elements that constitute the TV-specific status bar:
* <ul>
* <li> {@link AudioRecordingDisclosureBar} - shown whenever applications are conducting audio
* recording, discloses the responsible applications </li>
* </ul>
*/
@SysUISingleton
public class TvStatusBar extends SystemUI implements CommandQueue.Callbacks {
@@ -66,11 +59,6 @@ public class TvStatusBar extends SystemUI implements CommandQueue.Callbacks {
} catch (RemoteException ex) {
// If the system process isn't there we're doomed anyway.
}
if (mContext.getResources().getBoolean(R.bool.audio_recording_disclosure_enabled)) {
// Creating AudioRecordingDisclosureBar and just letting it run
new AudioRecordingDisclosureBar(mContext);
}
}
@Override

View File

@@ -1,48 +0,0 @@
/*
* 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 void start();
abstract void stop();
abstract Set<String> getActivePackages();
}

View File

@@ -1,200 +0,0 @@
/*
* 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.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 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);
}
@Override
void start() {
mActivityManager = ActivityManager.getService();
try {
mActivityManager.registerProcessObserver(mProcessObserver);
} catch (RemoteException e) {
Log.e(TAG, "Couldn't register process observer", e);
}
}
@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 mPackageToProcessCount.keySet();
}
@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 (DEBUG) {
Log.d(TAG, (active ? "New microphone fgs detected" : "Microphone fgs is gone")
+ ", package=" + packageName);
}
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

@@ -1,99 +0,0 @@
/*
* 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);
}
@Override
void start() {
if (DEBUG) {
Log.d(TAG, "Start");
}
// Register AppOpsManager callback
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() {
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);
}
}
}
}