Reuse phone privacy indicator impl. for tv
The UI is TV specific Bug: 180500214 Test: atest CameraMicIndicatorsPermissionTest Test: atest RecognitionServiceMicIndicatorTest Change-Id: I9fe87f838e303dcbf252a9b83a7ceddcfe3b3513
This commit is contained in:
@@ -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>
|
||||
35
packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml
Normal file
35
packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -28,6 +28,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator;
|
||||
import com.android.systemui.keyguard.dagger.KeyguardModule;
|
||||
import com.android.systemui.media.systemsounds.HomeSoundEffectController;
|
||||
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;
|
||||
@@ -154,6 +155,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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user