Merge "Reuse phone privacy indicator impl. for tv" into sc-dev
diff --git a/packages/SystemUI/res/layout/tv_audio_recording_indicator.xml b/packages/SystemUI/res/layout/tv_audio_recording_indicator.xml
deleted file mode 100644
index b62018d..0000000
--- a/packages/SystemUI/res/layout/tv_audio_recording_indicator.xml
+++ /dev/null
@@ -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>
diff --git a/packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml
new file mode 100644
index 0000000..dff148b
--- /dev/null
+++ b/packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml
@@ -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>
+
diff --git a/packages/SystemUI/res/values-television/config.xml b/packages/SystemUI/res/values-television/config.xml
index 722f148..b02d8b8 100644
--- a/packages/SystemUI/res/values-television/config.xml
+++ b/packages/SystemUI/res/values-television/config.xml
@@ -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>
diff --git a/packages/SystemUI/res/values-television/dimens.xml b/packages/SystemUI/res/values-television/dimens.xml
index 6da0c69..7626db9 100644
--- a/packages/SystemUI/res/values-television/dimens.xml
+++ b/packages/SystemUI/res/values-television/dimens.xml
@@ -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>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config_tv.xml b/packages/SystemUI/res/values/config_tv.xml
deleted file mode 100644
index 2e77674..0000000
--- a/packages/SystemUI/res/values/config_tv.xml
+++ /dev/null
@@ -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>
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
index ec3188a..5d226d5 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
@@ -29,6 +29,7 @@
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 @@
@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
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java
new file mode 100644
index 0000000..0fa7b59
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java
@@ -0,0 +1,334 @@
+/*
+ * 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.
+ * 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.privacy.television;
+
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+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.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.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.List;
+
+import javax.inject.Inject;
+
+/**
+ * A SystemUI component responsible for notifying the user whenever an application is
+ * recording audio, accessing the camera or accessing the location.
+ */
+@SysUISingleton
+public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemController.Callback {
+ private static final String TAG = "TvOngoingPrivacyChip";
+ static final boolean DEBUG = false;
+
+ // This title is used in CameraMicIndicatorsPermissionTest and
+ // RecognitionServiceMicIndicatorTest.
+ private static final String LAYOUT_PARAMS_TITLE = "MicrophoneCaptureIndicator";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"STATE_"}, value = {
+ STATE_NOT_SHOWN,
+ STATE_APPEARING,
+ STATE_SHOWN,
+ STATE_DISAPPEARING
+ })
+ public @interface State {
+ }
+
+ private static final int STATE_NOT_SHOWN = 0;
+ private static final int STATE_APPEARING = 1;
+ private static final int STATE_SHOWN = 2;
+ private static final int STATE_DISAPPEARING = 3;
+
+ private static final int ANIMATION_DURATION_MS = 200;
+
+ private final Context mContext;
+ private final PrivacyItemController mPrivacyItemController;
+
+ private View mIndicatorView;
+ private boolean mViewAndWindowAdded;
+ private ObjectAnimator mAnimator;
+
+ private boolean mAllIndicatorsFlagEnabled;
+ private boolean mMicCameraIndicatorFlagEnabled;
+ private boolean mLocationIndicatorEnabled;
+ private List<PrivacyItem> mPrivacyItems;
+
+ private LinearLayout mIconsContainer;
+ private final int mIconSize;
+ private final int mIconMarginStart;
+
+ @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;
+
+ Resources res = mContext.getResources();
+ mIconMarginStart = Math.round(res.getDimension(R.dimen.privacy_chip_icon_margin));
+ mIconSize = res.getDimensionPixelSize(R.dimen.privacy_chip_icon_size);
+
+ mAllIndicatorsFlagEnabled = privacyItemController.getAllIndicatorsAvailable();
+ mMicCameraIndicatorFlagEnabled = privacyItemController.getMicCameraAvailable();
+ mLocationIndicatorEnabled = privacyItemController.getLocationAvailable();
+
+ if (DEBUG) {
+ Log.d(TAG, "allIndicators: " + mAllIndicatorsFlagEnabled);
+ Log.d(TAG, "micCameraIndicators: " + mMicCameraIndicatorFlagEnabled);
+ Log.d(TAG, "locationIndicators: " + mLocationIndicatorEnabled);
+ }
+ }
+
+ @Override
+ public void start() {
+ mPrivacyItemController.addCallback(this);
+ }
+
+ @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();
+ }
+ }
+
+ @UiThread
+ private void hideIndicatorIfNeeded() {
+ if (mState == STATE_NOT_SHOWN || mState == STATE_DISAPPEARING) return;
+
+ if (mViewAndWindowAdded) {
+ mState = STATE_DISAPPEARING;
+ animateDisappearance();
+ } else {
+ // Appearing animation has not started yet, as we were still waiting for the View to be
+ // laid out.
+ mState = STATE_NOT_SHOWN;
+ removeIndicatorView();
+ }
+ }
+
+ @UiThread
+ private void showIndicator() {
+ mState = STATE_APPEARING;
+
+ // Inflate the indicator view
+ mIndicatorView = LayoutInflater.from(mContext).inflate(
+ 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.
+ // 3. Start a "fade in" (alpha) animation.
+ mIndicatorView.setAlpha(0f);
+ mIndicatorView
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // State could have changed to NOT_SHOWN (if all the recorders are
+ // already gone)
+ if (mState != STATE_APPEARING) return;
+
+ mViewAndWindowAdded = true;
+ // Remove the observer
+ mIndicatorView.getViewTreeObserver().removeOnGlobalLayoutListener(
+ this);
+
+ animateAppearance();
+ }
+ });
+
+ 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 | Gravity.END;
+ layoutParams.setTitle(LAYOUT_PARAMS_TITLE);
+ layoutParams.packageName = mContext.getPackageName();
+ 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);
+ }
+
+ private void animateDisappearance() {
+ animateAlphaTo(0f);
+ }
+
+ private void animateAlphaTo(final float endValue) {
+ if (mAnimator == null) {
+ if (DEBUG) Log.d(TAG, "set up animator");
+
+ mAnimator = new ObjectAnimator();
+ mAnimator.setTarget(mIndicatorView);
+ mAnimator.setProperty(View.ALPHA);
+ mAnimator.addListener(new AnimatorListenerAdapter() {
+ boolean mCancelled;
+
+ @Override
+ public void onAnimationStart(Animator animation, boolean isReverse) {
+ if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationStart");
+ mCancelled = false;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationCancel");
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationEnd");
+ // When ValueAnimator#cancel() is called it always calls onAnimationCancel(...)
+ // and then onAnimationEnd(...). We, however, only want to proceed here if the
+ // animation ended "naturally".
+ if (!mCancelled) {
+ onAnimationFinished();
+ }
+ }
+ });
+ } else if (mAnimator.isRunning()) {
+ if (DEBUG) Log.d(TAG, "cancel running animation");
+ mAnimator.cancel();
+ }
+
+ final float currentValue = mIndicatorView.getAlpha();
+ if (DEBUG) Log.d(TAG, "animate alpha to " + endValue + " from " + currentValue);
+
+ mAnimator.setDuration((int) (Math.abs(currentValue - endValue) * ANIMATION_DURATION_MS));
+ mAnimator.setFloatValues(endValue);
+ mAnimator.start();
+ }
+
+ private void onAnimationFinished() {
+ if (DEBUG) Log.d(TAG, "onAnimationFinished");
+
+ if (mState == STATE_APPEARING) {
+ mState = STATE_SHOWN;
+ } else if (mState == STATE_DISAPPEARING) {
+ removeIndicatorView();
+ mState = STATE_NOT_SHOWN;
+ }
+ }
+
+ private void removeIndicatorView() {
+ if (DEBUG) Log.d(TAG, "removeIndicatorView");
+
+ final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
+ if (windowManager != null) {
+ windowManager.removeView(mIndicatorView);
+ }
+
+ mIndicatorView = null;
+ mAnimator = null;
+
+ mViewAndWindowAdded = false;
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
index bdf2b0c..37a763b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -22,12 +22,10 @@
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 @@
/**
* 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 @@
} 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
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioActivityObserver.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioActivityObserver.java
deleted file mode 100644
index bbab625..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioActivityObserver.java
+++ /dev/null
@@ -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();
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java
deleted file mode 100644
index c9d1b71..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java
+++ /dev/null
@@ -1,381 +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.provider.DeviceConfig.NAMESPACE_PRIVACY;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.annotation.IntDef;
-import android.annotation.UiThread;
-import android.content.Context;
-import android.graphics.PixelFormat;
-import android.provider.DeviceConfig;
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.view.WindowManager;
-
-import com.android.systemui.R;
-import com.android.systemui.statusbar.tv.TvStatusBar;
-
-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;
-
-/**
- * A component of {@link TvStatusBar} responsible for notifying the user whenever an application is
- * recording audio.
- *
- * @see TvStatusBar
- */
-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
- 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 {}
-
- 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;
- private static final int STATE_DISAPPEARING = 3;
-
- private static final int ANIMATION_DURATION_MS = 200;
-
- private final Context mContext;
- private boolean mIsEnabled;
-
- private View mIndicatorView;
- private boolean mViewAndWindowAdded;
- private ObjectAnimator mAnimator;
-
- @State private int mState = STATE_STOPPED;
-
- /**
- * Array of the observers that monitor different aspects of the system, such as AppOps and
- * microphone foreground services
- */
- private 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<>();
-
- public AudioRecordingDisclosureBar(Context context) {
- mContext = context;
-
- // Load configs
- reloadExemptPackages();
-
- mIsEnabled = DeviceConfig.getBoolean(NAMESPACE_PRIVACY, ENABLED_FLAG, true);
- // Start if enabled
- if (mIsEnabled) {
- start();
- }
-
- // 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);
- }
-
- if (mExemptPackages.contains(packageName)) {
- if (DEBUG) Log.d(TAG, " - exempt package: ignoring");
- return;
- }
-
- if (active) {
- showIfNeeded();
- } else {
- hideIndicatorIfNeeded();
- }
- }
-
- @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 (mViewAndWindowAdded) {
- mState = STATE_DISAPPEARING;
- animateDisappearance();
- } else {
- // Appearing animation has not started yet, as we were still waiting for the View to be
- // laid out.
- mState = STATE_NOT_SHOWN;
- removeIndicatorView();
- }
- }
-
- @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;
- 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);
-
- // 1. Set alpha to 0.
- // 2. Wait until the window is shown and the view is laid out.
- // 3. Start a "fade in" (alpha) animation.
- mIndicatorView.setAlpha(0f);
- mIndicatorView
- .getViewTreeObserver()
- .addOnGlobalLayoutListener(
- new ViewTreeObserver.OnGlobalLayoutListener() {
- @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)
- if (mState != STATE_APPEARING) return;
-
- mViewAndWindowAdded = true;
- // Remove the observer
- mIndicatorView.getViewTreeObserver().removeOnGlobalLayoutListener(
- this);
-
- animateAppearance();
- }
- });
-
- final boolean isLtr = mContext.getResources().getConfiguration().getLayoutDirection()
- == View.LAYOUT_DIRECTION_LTR;
- 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.setTitle(LAYOUT_PARAMS_TITLE);
- layoutParams.packageName = mContext.getPackageName();
- final WindowManager windowManager = (WindowManager) mContext.getSystemService(
- Context.WINDOW_SERVICE);
- windowManager.addView(mIndicatorView, layoutParams);
- }
-
-
- private void animateAppearance() {
- animateAlphaTo(1f);
- }
-
- private void animateDisappearance() {
- animateAlphaTo(0f);
- }
-
- private void animateAlphaTo(final float endValue) {
- if (mAnimator == null) {
- if (DEBUG) Log.d(TAG, "set up animator");
-
- mAnimator = new ObjectAnimator();
- mAnimator.setTarget(mIndicatorView);
- mAnimator.setProperty(View.ALPHA);
- mAnimator.addListener(new AnimatorListenerAdapter() {
- boolean mCancelled;
-
- @Override
- public void onAnimationStart(Animator animation, boolean isReverse) {
- if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationStart");
- mCancelled = false;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationCancel");
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationEnd");
- // When ValueAnimator#cancel() is called it always calls onAnimationCancel(...)
- // and then onAnimationEnd(...). We, however, only want to proceed here if the
- // animation ended "naturally".
- if (!mCancelled) {
- onAnimationFinished();
- }
- }
- });
- } else if (mAnimator.isRunning()) {
- if (DEBUG) Log.d(TAG, "cancel running animation");
- mAnimator.cancel();
- }
-
- final float currentValue = mIndicatorView.getAlpha();
- if (DEBUG) Log.d(TAG, "animate alpha to " + endValue + " from " + currentValue);
-
- mAnimator.setDuration((int) (Math.abs(currentValue - endValue) * ANIMATION_DURATION_MS));
- mAnimator.setFloatValues(endValue);
- mAnimator.start();
- }
-
- private void onAnimationFinished() {
- if (DEBUG) Log.d(TAG, "onAnimationFinished");
-
- if (mState == STATE_APPEARING) {
- mState = STATE_SHOWN;
- } else if (mState == STATE_DISAPPEARING) {
- removeIndicatorView();
- mState = STATE_NOT_SHOWN;
- }
- }
-
- 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);
-
- mIndicatorView = null;
- mAnimator = null;
-
- 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();
- }
- }
- }
- };
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/MicrophoneForegroundServicesObserver.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/MicrophoneForegroundServicesObserver.java
deleted file mode 100644
index 8caf95f..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/MicrophoneForegroundServicesObserver.java
+++ /dev/null
@@ -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));
- }
- };
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/RecordAudioAppOpObserver.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/RecordAudioAppOpObserver.java
deleted file mode 100644
index 9a2b4a9..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/RecordAudioAppOpObserver.java
+++ /dev/null
@@ -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);
- }
- }
- }
-}