Move TV-specific classes out of SystemUI

Bug: 289184493
Test: m TvSystemUI
Change-Id: Ieb087a59a1837141c29b42717653fbf463269873
This commit is contained in:
Nikolas Havrikov
2023-05-25 23:44:38 +02:00
parent 2ebd19f7a7
commit 98ca007dd5
30 changed files with 1 additions and 3634 deletions

View File

@@ -548,26 +548,6 @@
android:excludeFromRecents="true">
</activity>
<!-- started from UsbDeviceSettingsManager -->
<activity android:name=".usb.tv.TvUsbConfirmActivity"
android:exported="true"
android:launchMode="singleTop"
android:permission="android.permission.MANAGE_USB"
android:theme="@style/BottomSheet"
android:finishOnCloseSystemDialogs="true"
android:excludeFromRecents="true">
</activity>
<!-- started from UsbDeviceSettingsManager -->
<activity android:name=".usb.tv.TvUsbPermissionActivity"
android:exported="true"
android:launchMode="singleTop"
android:permission="android.permission.MANAGE_USB"
android:theme="@style/BottomSheet"
android:finishOnCloseSystemDialogs="true"
android:excludeFromRecents="true">
</activity>
<!-- started from UsbDeviceSettingsManager -->
<activity android:name=".usb.UsbResolverActivity"
android:exported="true"
@@ -577,16 +557,6 @@
android:excludeFromRecents="true">
</activity>
<!-- started from HdmiCecLocalDevicePlayback -->
<activity android:name=".hdmi.HdmiCecSetMenuLanguageActivity"
android:exported="true"
android:launchMode="singleTop"
android:permission="android.permission.CHANGE_CONFIGURATION"
android:theme="@style/BottomSheet"
android:finishOnCloseSystemDialogs="true"
android:excludeFromRecents="true">
</activity>
<!-- started from SensoryPrivacyService -->
<activity android:name=".sensorprivacy.SensorUseStartedActivity"
android:exported="true"
@@ -597,27 +567,6 @@
android:showForAllUsers="true">
</activity>
<!-- started from SensoryPrivacyService -->
<activity android:name=".sensorprivacy.television.TvUnblockSensorActivity"
android:exported="true"
android:launchMode="singleTop"
android:permission="android.permission.MANAGE_SENSOR_PRIVACY"
android:theme="@style/BottomSheet"
android:finishOnCloseSystemDialogs="true"
android:showForAllUsers="true">
</activity>
<!-- started from SensoryPrivacyService -->
<activity android:name=".sensorprivacy.television.TvSensorPrivacyChangedActivity"
android:exported="true"
android:launchMode="singleTop"
android:permission="android.permission.MANAGE_SENSOR_PRIVACY"
android:theme="@style/BottomSheet"
android:finishOnCloseSystemDialogs="true"
android:showForAllUsers="true">
</activity>
<!-- started from UsbDeviceSettingsManager -->
<activity android:name=".usb.UsbAccessoryUriActivity"
android:exported="true"
@@ -703,14 +652,6 @@
android:exported="false"
android:permission="android.permission.MANAGE_MEDIA_PROJECTION"/>
<!-- started from TvNotificationPanel -->
<activity
android:name=".statusbar.tv.notifications.TvNotificationPanelActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:noHistory="true"
android:theme="@style/TvSidePanelTheme" />
<!-- started from SliceProvider -->
<activity android:name=".SlicePermissionActivity"
android:theme="@style/Theme.SystemUI.Dialog.Alert"

View File

@@ -49,7 +49,6 @@ import com.android.systemui.dock.DockManager;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.hdmi.HdmiCecSetMenuLanguageHelper;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
@@ -242,7 +241,6 @@ public class Dependency {
@Inject Lazy<LocationController> mLocationController;
@Inject Lazy<RotationLockController> mRotationLockController;
@Inject Lazy<ZenModeController> mZenModeController;
@Inject Lazy<HdmiCecSetMenuLanguageHelper> mHdmiCecSetMenuLanguageHelper;
@Inject Lazy<HotspotController> mHotspotController;
@Inject Lazy<CastController> mCastController;
@Inject Lazy<FlashlightController> mFlashlightController;

View File

@@ -20,7 +20,6 @@ import android.app.Activity;
import com.android.systemui.ForegroundServicesDialog;
import com.android.systemui.contrast.ContrastDialogActivity;
import com.android.systemui.hdmi.HdmiCecSetMenuLanguageActivity;
import com.android.systemui.keyguard.WorkLockActivity;
import com.android.systemui.people.PeopleSpaceActivity;
import com.android.systemui.people.widget.LaunchConversationActivity;
@@ -28,10 +27,7 @@ import com.android.systemui.screenshot.LongScreenshotActivity;
import com.android.systemui.screenshot.appclips.AppClipsActivity;
import com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity;
import com.android.systemui.sensorprivacy.SensorUseStartedActivity;
import com.android.systemui.sensorprivacy.television.TvSensorPrivacyChangedActivity;
import com.android.systemui.sensorprivacy.television.TvUnblockSensorActivity;
import com.android.systemui.settings.brightness.BrightnessDialog;
import com.android.systemui.statusbar.tv.notifications.TvNotificationPanelActivity;
import com.android.systemui.telephony.ui.activity.SwitchToManagedProfileForCallActivity;
import com.android.systemui.tuner.TunerActivity;
import com.android.systemui.usb.UsbAccessoryUriActivity;
@@ -118,12 +114,6 @@ public abstract class DefaultActivityBinder {
@ClassKey(CreateUserActivity.class)
public abstract Activity bindCreateUserActivity(CreateUserActivity activity);
/** Inject into TvNotificationPanelActivity. */
@Binds
@IntoMap
@ClassKey(TvNotificationPanelActivity.class)
public abstract Activity bindTvNotificationPanelActivity(TvNotificationPanelActivity activity);
/** Inject into PeopleSpaceActivity. */
@Binds
@IntoMap
@@ -160,25 +150,7 @@ public abstract class DefaultActivityBinder {
@ClassKey(SensorUseStartedActivity.class)
public abstract Activity bindSensorUseStartedActivity(SensorUseStartedActivity activity);
/** Inject into TvUnblockSensorActivity. */
@Binds
@IntoMap
@ClassKey(TvUnblockSensorActivity.class)
public abstract Activity bindTvUnblockSensorActivity(TvUnblockSensorActivity activity);
/** Inject into HdmiCecSetMenuLanguageActivity. */
@Binds
@IntoMap
@ClassKey(HdmiCecSetMenuLanguageActivity.class)
public abstract Activity bindHdmiCecSetMenuLanguageActivity(
HdmiCecSetMenuLanguageActivity activity);
/** Inject into TvSensorPrivacyChangedActivity. */
@Binds
@IntoMap
@ClassKey(TvSensorPrivacyChangedActivity.class)
public abstract Activity bindTvSensorPrivacyChangedActivity(
TvSensorPrivacyChangedActivity activity);
/** Inject into SwitchToManagedProfileForCallActivity. */
@Binds

View File

@@ -21,11 +21,9 @@ import android.os.HandlerThread;
import androidx.annotation.Nullable;
import com.android.systemui.SystemUIInitializer;
import com.android.systemui.tv.TvWMComponent;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.dagger.TvWMShellModule;
import com.android.wm.shell.dagger.WMShellModule;
import com.android.wm.shell.dagger.WMSingleton;
import com.android.wm.shell.desktopmode.DesktopMode;
@@ -52,7 +50,7 @@ import java.util.Optional;
* provided by its particular device/form-factor SystemUI implementation.
*
* ie. {@link WMComponent} includes {@link WMShellModule}
* and {@link TvWMComponent} includes {@link TvWMShellModule}
* and {@code TvWMComponent} includes {@link com.android.wm.shell.dagger.TvWMShellModule}
*/
@WMSingleton
@Subcomponent(modules = {WMShellModule.class})

View File

@@ -1,98 +0,0 @@
/*
* Copyright (C) 2022 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.hdmi;
import android.hardware.hdmi.HdmiControlManager;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.tv.TvBottomSheetActivity;
import javax.inject.Inject;
/**
* Confirmation dialog shown when Set Menu Language CEC message was received.
*/
public class HdmiCecSetMenuLanguageActivity extends TvBottomSheetActivity
implements View.OnClickListener {
private static final String TAG = HdmiCecSetMenuLanguageActivity.class.getSimpleName();
private final HdmiCecSetMenuLanguageHelper mHdmiCecSetMenuLanguageHelper;
@Inject
public HdmiCecSetMenuLanguageActivity(
HdmiCecSetMenuLanguageHelper hdmiCecSetMenuLanguageHelper) {
mHdmiCecSetMenuLanguageHelper = hdmiCecSetMenuLanguageHelper;
}
@Override
public final void onCreate(Bundle b) {
super.onCreate(b);
getWindow().addPrivateFlags(
WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
String languageTag = getIntent().getStringExtra(HdmiControlManager.EXTRA_LOCALE);
mHdmiCecSetMenuLanguageHelper.setLocale(languageTag);
if (mHdmiCecSetMenuLanguageHelper.isLocaleDenylisted()) {
finish();
}
}
@Override
public void onResume() {
super.onResume();
CharSequence title = getString(R.string.hdmi_cec_set_menu_language_title,
mHdmiCecSetMenuLanguageHelper.getLocale().getDisplayLanguage());
CharSequence text = getString(R.string.hdmi_cec_set_menu_language_description);
initUI(title, text);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.bottom_sheet_positive_button) {
mHdmiCecSetMenuLanguageHelper.acceptLocale();
} else {
mHdmiCecSetMenuLanguageHelper.declineLocale();
}
finish();
}
void initUI(CharSequence title, CharSequence text) {
TextView titleTextView = findViewById(R.id.bottom_sheet_title);
TextView contentTextView = findViewById(R.id.bottom_sheet_body);
ImageView icon = findViewById(R.id.bottom_sheet_icon);
ImageView secondIcon = findViewById(R.id.bottom_sheet_second_icon);
Button okButton = findViewById(R.id.bottom_sheet_positive_button);
Button cancelButton = findViewById(R.id.bottom_sheet_negative_button);
titleTextView.setText(title);
contentTextView.setText(text);
icon.setImageResource(com.android.internal.R.drawable.ic_settings_language);
secondIcon.setVisibility(View.GONE);
okButton.setText(R.string.hdmi_cec_set_menu_language_accept);
okButton.setOnClickListener(this);
cancelButton.setText(R.string.hdmi_cec_set_menu_language_decline);
cancelButton.setOnClickListener(this);
cancelButton.requestFocus();
}
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright (C) 2022 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.hdmi;
import android.os.UserHandle;
import android.provider.Settings;
import com.android.internal.app.LocalePicker;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.util.settings.SecureSettings;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
* Helper class to separate model and view for system language change initiated by HDMI CEC.
*/
@SysUISingleton
public class HdmiCecSetMenuLanguageHelper {
private static final String TAG = HdmiCecSetMenuLanguageHelper.class.getSimpleName();
private static final String SEPARATOR = ",";
private final Executor mBackgroundExecutor;
private final SecureSettings mSecureSettings;
private Locale mLocale;
private HashSet<String> mDenylist;
@Inject
public HdmiCecSetMenuLanguageHelper(@Background Executor executor,
SecureSettings secureSettings) {
mBackgroundExecutor = executor;
mSecureSettings = secureSettings;
String denylist = mSecureSettings.getStringForUser(
Settings.Secure.HDMI_CEC_SET_MENU_LANGUAGE_DENYLIST, UserHandle.USER_CURRENT);
mDenylist = new HashSet<>(denylist == null
? Collections.EMPTY_SET
: Arrays.asList(denylist.split(SEPARATOR)));
}
/**
* Set internal locale based on given language tag.
*/
public void setLocale(String languageTag) {
mLocale = Locale.forLanguageTag(languageTag);
}
/**
* Returns the locale from {@code <Set Menu Language>} CEC message.
*/
public Locale getLocale() {
return mLocale;
}
/**
* Returns whether the locale from {@code <Set Menu Language>} CEC message was already
* denylisted.
*/
public boolean isLocaleDenylisted() {
return mDenylist.contains(mLocale.toLanguageTag());
}
/**
* Accepts the new locale and updates system language.
*/
public void acceptLocale() {
mBackgroundExecutor.execute(() -> LocalePicker.updateLocale(mLocale));
}
/**
* Declines the locale and puts it on the denylist.
*/
public void declineLocale() {
mDenylist.add(mLocale.toLanguageTag());
mSecureSettings.putStringForUser(Settings.Secure.HDMI_CEC_SET_MENU_LANGUAGE_DENYLIST,
String.join(SEPARATOR, mDenylist), UserHandle.USER_CURRENT);
}
}

View File

@@ -1,236 +0,0 @@
/*
* 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 android.animation.Animator;
import android.animation.AnimatorInflater;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.MathUtils;
import android.view.Gravity;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.systemui.R;
/**
* Drawable that can go from being the background of the privacy icons to a small dot.
* The icons are not included.
*/
public class PrivacyChipDrawable extends Drawable {
private static final String TAG = PrivacyChipDrawable.class.getSimpleName();
private static final boolean DEBUG = false;
private final Paint mChipPaint;
private final Paint mBgPaint;
private final Rect mTmpRect = new Rect();
private final Rect mBgRect = new Rect();
private final RectF mTmpRectF = new RectF();
private final Path mPath = new Path();
private final Animator mCollapse;
private final Animator mExpand;
private final int mLayoutDirection;
private final int mBgWidth;
private final int mBgHeight;
private final int mBgRadius;
private final int mDotSize;
private final float mExpandedChipRadius;
private final float mCollapsedChipRadius;
private final boolean mCollapseToDot;
private boolean mIsExpanded = true;
private float mCollapseProgress = 0f;
public PrivacyChipDrawable(Context context, int chipColorRes, boolean collapseToDot) {
mCollapseToDot = collapseToDot;
mChipPaint = new Paint();
mChipPaint.setStyle(Paint.Style.FILL);
mChipPaint.setColor(context.getColor(chipColorRes));
mChipPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mBgPaint = new Paint();
mBgPaint.setStyle(Paint.Style.FILL);
mBgPaint.setColor(context.getColor(R.color.privacy_chip_dot_bg_tint));
mBgPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
Resources res = context.getResources();
mLayoutDirection = res.getConfiguration().getLayoutDirection();
mBgWidth = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_width);
mBgHeight = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_height);
mBgRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_radius);
mDotSize = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_size);
mExpandedChipRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_radius);
mCollapsedChipRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_radius);
mExpand = AnimatorInflater.loadAnimator(context, R.anim.tv_privacy_chip_expand);
mExpand.setTarget(this);
mCollapse = AnimatorInflater.loadAnimator(context, R.anim.tv_privacy_chip_collapse);
mCollapse.setTarget(this);
}
/**
* @return how far the chip is currently collapsed.
* @see #setCollapseProgress(float)
*/
@Keep
public float getCollapseProgress() {
return mCollapseProgress;
}
/**
* Sets the collapsing progress of the chip to its collapsed state.
* @param pct How far the chip is collapsed, in the range 0-1.
* 0=fully expanded, 1=fully collapsed.
*/
@Keep
public void setCollapseProgress(float pct) {
mCollapseProgress = pct;
invalidateSelf();
}
@Override
public void draw(@NonNull Canvas canvas) {
if (mCollapseProgress > 0f) {
// draw background
getBackgroundBounds(mBgRect);
mTmpRectF.set(mBgRect);
canvas.drawRoundRect(mTmpRectF, mBgRadius, mBgRadius, mBgPaint);
}
getForegroundBounds(mTmpRectF);
float radius = MathUtils.lerp(
mExpandedChipRadius,
mCollapseToDot ? mCollapsedChipRadius : mBgRadius,
mCollapseProgress);
canvas.drawRoundRect(mTmpRectF, radius, radius, mChipPaint);
}
private void getBackgroundBounds(Rect out) {
Rect bounds = getBounds();
Gravity.apply(Gravity.END, mBgWidth, mBgHeight, bounds, out, mLayoutDirection);
}
private void getCollapsedForegroundBounds(Rect out) {
Rect bounds = getBounds();
getBackgroundBounds(mBgRect);
if (mCollapseToDot) {
Gravity.apply(Gravity.CENTER, mDotSize, mDotSize, mBgRect, out);
} else {
out.set(bounds.left, mBgRect.top, bounds.right, mBgRect.bottom);
}
}
private void getForegroundBounds(RectF out) {
Rect bounds = getBounds();
getCollapsedForegroundBounds(mTmpRect);
lerpRect(bounds, mTmpRect, mCollapseProgress, out);
}
private void lerpRect(Rect start, Rect stop, float amount, RectF out) {
float left = MathUtils.lerp(start.left, stop.left, amount);
float top = MathUtils.lerp(start.top, stop.top, amount);
float right = MathUtils.lerp(start.right, stop.right, amount);
float bottom = MathUtils.lerp(start.bottom, stop.bottom, amount);
out.set(left, top, right, bottom);
}
/**
* Clips the given canvas to this chip's foreground shape.
* @param canvas Canvas to clip.
*/
public void clipToForeground(Canvas canvas) {
getForegroundBounds(mTmpRectF);
float radius = MathUtils.lerp(
mExpandedChipRadius,
mCollapseToDot ? mCollapsedChipRadius : mBgRadius,
mCollapseProgress);
mPath.reset();
mPath.addRoundRect(mTmpRectF, radius, radius, Path.Direction.CW);
canvas.clipPath(mPath);
}
@Override
protected void onBoundsChange(@NonNull Rect bounds) {
super.onBoundsChange(bounds);
invalidateSelf();
}
@Override
public void setAlpha(int alpha) {
mChipPaint.setAlpha(alpha);
mBgPaint.setAlpha(alpha);
}
/**
* Transitions to a full chip.
*
* @param animate Whether to animate the change to a full chip, or expand instantly.
*/
public void expand(boolean animate) {
if (DEBUG) Log.d(TAG, "expanding");
if (mIsExpanded) {
return;
}
mIsExpanded = true;
if (animate) {
mCollapse.cancel();
mExpand.start();
} else {
mCollapseProgress = 0f;
invalidateSelf();
}
}
/**
* Starts the animation to a dot.
*/
public void collapse() {
if (DEBUG) Log.d(TAG, "collapsing");
if (!mIsExpanded) {
return;
}
mIsExpanded = false;
mExpand.cancel();
mCollapse.start();
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
// no-op
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}

View File

@@ -1,247 +0,0 @@
/*
* Copyright (C) 2022 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 android.annotation.ColorRes;
import android.annotation.IntDef;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import com.android.systemui.R;
import com.android.systemui.privacy.PrivacyType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* View that shows indicator icons for privacy items.
*/
public class PrivacyItemsChip extends FrameLayout {
private static final String TAG = "PrivacyItemsChip";
private static final boolean DEBUG = false;
/**
* Configuration for a PrivacyItemsChip's appearance.
*/
public static class ChipConfig {
public final List<PrivacyType> privacyTypes;
@ColorRes
public final int colorRes;
public final boolean collapseToDot;
/**
* @param privacyTypes Privacy types to show icons for, in order.
* @param colorRes Color resource for the chip's foreground color.
* @param collapseToDot Whether to collapse the chip in to a dot,
* or just collapse it into a smaller chip with icons still visible.
*/
public ChipConfig(@NonNull List<PrivacyType> privacyTypes, int colorRes,
boolean collapseToDot) {
this.privacyTypes = privacyTypes;
this.colorRes = colorRes;
this.collapseToDot = collapseToDot;
}
}
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"STATE_"}, value = {
STATE_NOT_SHOWN,
STATE_EXPANDED,
STATE_COLLAPSED,
})
public @interface State {
}
private static final int STATE_NOT_SHOWN = 0;
private static final int STATE_EXPANDED = 1;
private static final int STATE_COLLAPSED = 2;
private final ChipConfig mConfig;
private final int mIconSize;
private final int mCollapsedIconSize;
private final int mIconMarginHorizontal;
private final PrivacyChipDrawable mChipBackgroundDrawable;
private final List<ImageView> mIcons = new ArrayList<>();
@State
private int mState = STATE_NOT_SHOWN;
public PrivacyItemsChip(@NonNull Context context, @NonNull ChipConfig config) {
super(context);
mConfig = config;
setVisibility(View.GONE);
Resources res = context.getResources();
mIconSize = res.getDimensionPixelSize(R.dimen.privacy_chip_icon_size);
mCollapsedIconSize = res.getDimensionPixelSize(R.dimen.privacy_chip_collapsed_icon_size);
mIconMarginHorizontal =
res.getDimensionPixelSize(R.dimen.privacy_chip_icon_margin_in_between);
LayoutInflater.from(context).inflate(R.layout.tv_ongoing_privacy_chip, this);
LinearLayout iconsContainer = findViewById(R.id.icons_container);
mChipBackgroundDrawable = new PrivacyChipDrawable(
context, config.colorRes, config.collapseToDot);
mChipBackgroundDrawable.setCallback(new Drawable.Callback() {
@Override
public void invalidateDrawable(@NonNull Drawable who) {
invalidate();
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
}
});
setBackground(mChipBackgroundDrawable);
for (PrivacyType type : config.privacyTypes) {
ImageView typeIconView = new ImageView(context);
Drawable icon = type.getIcon(context);
icon.mutate().setTint(context.getColor(R.color.privacy_icon_tint));
typeIconView.setImageDrawable(icon);
typeIconView.setScaleType(ImageView.ScaleType.FIT_CENTER);
mIcons.add(typeIconView);
iconsContainer.addView(typeIconView, mIconSize, mIconSize);
LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) typeIconView.getLayoutParams();
lp.leftMargin = mIconMarginHorizontal;
lp.rightMargin = mIconMarginHorizontal;
typeIconView.setVisibility(View.GONE);
}
}
/**
* Sets the active privacy types, and expands the chip if there are active items and the chip is
* currently collapsed, or hides the chip if there are no active items.
*
* @param types The set of active privacy types. Only types configured in {@link ChipConfig}
* are shown.
*/
public void expandForTypes(Set<PrivacyType> types) {
if (DEBUG) Log.d(TAG, "expandForTypes, state=" + stateToString(mState));
boolean hasActiveTypes = false;
for (int i = 0; i < mConfig.privacyTypes.size(); i++) {
PrivacyType type = mConfig.privacyTypes.get(i);
ImageView icon = mIcons.get(i);
boolean isTypeActive = types.contains(type);
hasActiveTypes = hasActiveTypes || isTypeActive;
icon.setVisibility(isTypeActive ? View.VISIBLE : View.GONE);
// Set icon size to expanded size
ViewGroup.LayoutParams lp = icon.getLayoutParams();
lp.width = mIconSize;
lp.height = mIconSize;
icon.requestLayout();
}
if (hasActiveTypes) {
if (DEBUG) Log.d(TAG, "Chip has active types, expanding");
if (mState == STATE_NOT_SHOWN) {
mChipBackgroundDrawable.expand(/* animate= */ false);
} else if (mState == STATE_COLLAPSED) {
mChipBackgroundDrawable.expand(/* animate= */ true);
}
setVisibility(View.VISIBLE);
setState(STATE_EXPANDED);
} else {
if (DEBUG) Log.d(TAG, "Chip has no active types, hiding");
setVisibility(View.GONE);
setState(STATE_NOT_SHOWN);
}
}
/**
* Collapses this chip if currently expanded.
*/
public void collapse() {
if (DEBUG) Log.d(TAG, "collapse");
if (mState != STATE_EXPANDED) {
return;
}
setState(STATE_COLLAPSED);
for (ImageView icon : mIcons) {
if (mConfig.collapseToDot) {
icon.setVisibility(View.GONE);
} else {
ViewGroup.LayoutParams lp = icon.getLayoutParams();
lp.width = mCollapsedIconSize;
lp.height = mCollapsedIconSize;
icon.requestLayout();
}
}
mChipBackgroundDrawable.collapse();
}
public boolean isExpanded() {
return mState == STATE_EXPANDED;
}
private void setState(@State int state) {
if (mState != state) {
if (DEBUG) Log.d(TAG, "State changed: " + stateToString(state));
mState = state;
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
mChipBackgroundDrawable.clipToForeground(canvas);
super.dispatchDraw(canvas);
}
/**
* Used in debug logs.
*/
private static String stateToString(@State int state) {
switch (state) {
case STATE_NOT_SHOWN:
return "NOT_SHOWN";
case STATE_EXPANDED:
return "EXPANDED";
case STATE_COLLAPSED:
return "COLLAPSED";
default:
return "INVALID";
}
}
}

View File

@@ -1,518 +0,0 @@
/*
* 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.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.transition.AutoTransition;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.ArraySet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.IWindowManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.android.systemui.CoreStartable;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.privacy.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.privacy.PrivacyType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
/**
* A SystemUI component responsible for notifying the user whenever an application is
* recording audio, camera, the screen, or accessing the location.
*/
@SysUISingleton
public class TvPrivacyChipsController
implements CoreStartable, PrivacyItemController.Callback {
private static final String TAG = "TvPrivacyChipsController";
private static final boolean DEBUG = false;
// This title is used in CameraMicIndicatorsPermissionTest and
// RecognitionServiceMicIndicatorTest.
private static final String LAYOUT_PARAMS_TITLE = "MicrophoneCaptureIndicator";
// Chips configuration. We're not showing a location indicator on TV.
static final List<PrivacyItemsChip.ChipConfig> CHIPS = Arrays.asList(
new PrivacyItemsChip.ChipConfig(
Collections.singletonList(PrivacyType.TYPE_MEDIA_PROJECTION),
R.color.privacy_media_projection_chip,
/* collapseToDot= */ false),
new PrivacyItemsChip.ChipConfig(
Arrays.asList(PrivacyType.TYPE_CAMERA, PrivacyType.TYPE_MICROPHONE),
R.color.privacy_mic_cam_chip,
/* collapseToDot= */ true)
);
// Avoid multiple messages after rapid changes such as starting/stopping both camera and mic.
private static final int ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS = 500;
/**
* Time to collect privacy item updates before applying them.
* Since MediaProjection and AppOps come from different data sources,
* PrivacyItem updates when screen & audio recording ends do not come at the same time.
* Without this, if eg. MediaProjection ends first, you'd see the microphone chip expand and
* almost immediately fade out as it is expanding. With this, the two chips disappear together.
*/
private static final int PRIVACY_ITEM_DEBOUNCE_TIMEOUT_MS = 200;
// How long chips stay expanded after an update.
private static final int EXPANDED_DURATION_MS = 4000;
private final Context mContext;
private final Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
private final Runnable mCollapseRunnable = this::collapseChips;
private final Runnable mUpdatePrivacyItemsRunnable = this::updateChipsAndAnnounce;
private final Runnable mAccessibilityRunnable = this::makeAccessibilityAnnouncement;
private final PrivacyItemController mPrivacyItemController;
private final IWindowManager mIWindowManager;
private final Rect[] mBounds = new Rect[4];
private final TransitionSet mTransition;
private final TransitionSet mCollapseTransition;
private boolean mIsRtl;
@Nullable
private ViewGroup mChipsContainer;
@Nullable
private List<PrivacyItemsChip> mChips;
@NonNull
private List<PrivacyItem> mPrivacyItems = Collections.emptyList();
@NonNull
private final List<PrivacyItem> mItemsBeforeLastAnnouncement = new ArrayList<>();
@Inject
public TvPrivacyChipsController(Context context, PrivacyItemController privacyItemController,
IWindowManager iWindowManager) {
mContext = context;
if (DEBUG) Log.d(TAG, "TvPrivacyChipsController running");
mPrivacyItemController = privacyItemController;
mIWindowManager = iWindowManager;
Resources res = mContext.getResources();
mIsRtl = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
updateStaticPrivacyIndicatorBounds();
Interpolator collapseInterpolator = AnimationUtils.loadInterpolator(context,
R.interpolator.tv_privacy_chip_collapse_interpolator);
Interpolator expandInterpolator = AnimationUtils.loadInterpolator(context,
R.interpolator.tv_privacy_chip_expand_interpolator);
TransitionSet chipFadeTransition = new TransitionSet()
.addTransition(new Fade(Fade.IN))
.addTransition(new Fade(Fade.OUT));
chipFadeTransition.setOrdering(TransitionSet.ORDERING_TOGETHER);
chipFadeTransition.excludeTarget(ImageView.class, true);
Transition chipBoundsExpandTransition = new ChangeBounds();
chipBoundsExpandTransition.excludeTarget(ImageView.class, true);
chipBoundsExpandTransition.setInterpolator(expandInterpolator);
Transition chipBoundsCollapseTransition = new ChangeBounds();
chipBoundsCollapseTransition.excludeTarget(ImageView.class, true);
chipBoundsCollapseTransition.setInterpolator(collapseInterpolator);
TransitionSet iconCollapseTransition = new AutoTransition();
iconCollapseTransition.setOrdering(TransitionSet.ORDERING_TOGETHER);
iconCollapseTransition.addTarget(ImageView.class);
iconCollapseTransition.setInterpolator(collapseInterpolator);
TransitionSet iconExpandTransition = new AutoTransition();
iconExpandTransition.setOrdering(TransitionSet.ORDERING_TOGETHER);
iconExpandTransition.addTarget(ImageView.class);
iconExpandTransition.setInterpolator(expandInterpolator);
mTransition = new TransitionSet()
.addTransition(chipFadeTransition)
.addTransition(chipBoundsExpandTransition)
.addTransition(iconExpandTransition)
.setOrdering(TransitionSet.ORDERING_TOGETHER)
.setDuration(res.getInteger(R.integer.privacy_chip_animation_millis));
mCollapseTransition = new TransitionSet()
.addTransition(chipFadeTransition)
.addTransition(chipBoundsCollapseTransition)
.addTransition(iconCollapseTransition)
.setOrdering(TransitionSet.ORDERING_TOGETHER)
.setDuration(res.getInteger(R.integer.privacy_chip_animation_millis));
Transition.TransitionListener transitionListener = new Transition.TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
if (DEBUG) Log.v(TAG, "onTransitionStart");
}
@Override
public void onTransitionEnd(Transition transition) {
if (DEBUG) Log.v(TAG, "onTransitionEnd");
if (mChips != null) {
boolean hasVisibleChip = false;
boolean hasExpandedChip = false;
for (PrivacyItemsChip chip : mChips) {
hasVisibleChip = hasVisibleChip || chip.getVisibility() == View.VISIBLE;
hasExpandedChip = hasExpandedChip || chip.isExpanded();
}
if (!hasVisibleChip) {
if (DEBUG) Log.d(TAG, "No chips visible anymore");
removeIndicatorView();
} else if (hasExpandedChip) {
if (DEBUG) Log.d(TAG, "Has expanded chips");
collapseLater();
}
}
}
@Override
public void onTransitionCancel(Transition transition) {
}
@Override
public void onTransitionPause(Transition transition) {
}
@Override
public void onTransitionResume(Transition transition) {
}
};
mTransition.addListener(transitionListener);
mCollapseTransition.addListener(transitionListener);
}
@Override
public void onConfigurationChanged(Configuration config) {
boolean updatedRtl = config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
if (mIsRtl == updatedRtl) {
return;
}
mIsRtl = updatedRtl;
// Update privacy chip location.
if (mChipsContainer != null) {
removeIndicatorView();
createAndShowIndicator();
}
updateStaticPrivacyIndicatorBounds();
}
@Override
public void start() {
mPrivacyItemController.addCallback(this);
}
@UiThread
@Override
public void onPrivacyItemsChanged(List<PrivacyItem> privacyItems) {
if (DEBUG) Log.d(TAG, "onPrivacyItemsChanged");
List<PrivacyItem> filteredPrivacyItems = new ArrayList<>(privacyItems);
if (filteredPrivacyItems.removeIf(
privacyItem -> !isPrivacyTypeShown(privacyItem.getPrivacyType()))) {
if (DEBUG) Log.v(TAG, "Removed privacy items we don't show");
}
// Do they have the same elements? (order doesn't matter)
if (privacyItems.size() == mPrivacyItems.size() && mPrivacyItems.containsAll(
privacyItems)) {
if (DEBUG) Log.d(TAG, "No change to relevant privacy items");
return;
}
mPrivacyItems = privacyItems;
if (!mUiThreadHandler.hasCallbacks(mUpdatePrivacyItemsRunnable)) {
mUiThreadHandler.postDelayed(mUpdatePrivacyItemsRunnable,
PRIVACY_ITEM_DEBOUNCE_TIMEOUT_MS);
}
}
private boolean isPrivacyTypeShown(@NonNull PrivacyType type) {
for (PrivacyItemsChip.ChipConfig chip : CHIPS) {
if (chip.privacyTypes.contains(type)) {
return true;
}
}
return false;
}
@UiThread
private void updateChipsAndAnnounce() {
updateChips();
postAccessibilityAnnouncement();
}
private void updateStaticPrivacyIndicatorBounds() {
Resources res = mContext.getResources();
int mMaxExpandedWidth = res.getDimensionPixelSize(R.dimen.privacy_chips_max_width);
int mMaxExpandedHeight = res.getDimensionPixelSize(R.dimen.privacy_chip_height);
int mChipMarginTotal = 2 * res.getDimensionPixelSize(R.dimen.privacy_chip_margin);
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
Rect screenBounds = windowManager.getCurrentWindowMetrics().getBounds();
mBounds[0] = new Rect(
mIsRtl ? screenBounds.left
: screenBounds.right - mMaxExpandedWidth,
screenBounds.top,
mIsRtl ? screenBounds.left + mMaxExpandedWidth
: screenBounds.right,
screenBounds.top + mChipMarginTotal + mMaxExpandedHeight
);
if (DEBUG) Log.v(TAG, "privacy indicator bounds: " + mBounds[0].toShortString());
try {
mIWindowManager.updateStaticPrivacyIndicatorBounds(mContext.getDisplayId(), mBounds);
} catch (RemoteException e) {
Log.w(TAG, "could not update privacy indicator bounds");
}
}
@UiThread
private void updateChips() {
if (DEBUG) Log.d(TAG, "updateChips: " + mPrivacyItems.size() + " privacy items");
if (mChipsContainer == null) {
if (!mPrivacyItems.isEmpty()) {
createAndShowIndicator();
}
return;
}
Set<PrivacyType> activePrivacyTypes = new ArraySet<>();
mPrivacyItems.forEach(item -> activePrivacyTypes.add(item.getPrivacyType()));
TransitionManager.beginDelayedTransition(mChipsContainer, mTransition);
mChips.forEach(chip -> chip.expandForTypes(activePrivacyTypes));
}
/**
* Collapse the chip {@link #EXPANDED_DURATION_MS} from now.
*/
private void collapseLater() {
mUiThreadHandler.removeCallbacks(mCollapseRunnable);
if (DEBUG) Log.d(TAG, "Chips will collapse in " + EXPANDED_DURATION_MS + "ms");
mUiThreadHandler.postDelayed(mCollapseRunnable, EXPANDED_DURATION_MS);
}
private void collapseChips() {
if (DEBUG) Log.d(TAG, "collapseChips");
if (mChipsContainer == null) {
return;
}
boolean hasExpandedChip = false;
for (PrivacyItemsChip chip : mChips) {
hasExpandedChip |= chip.isExpanded();
}
if (mChipsContainer != null && hasExpandedChip) {
TransitionManager.beginDelayedTransition(mChipsContainer, mCollapseTransition);
for (PrivacyItemsChip chip : mChips) {
chip.collapse();
}
}
}
@UiThread
private void createAndShowIndicator() {
if (DEBUG) Log.i(TAG, "Creating privacy indicators");
Context privacyChipContext = new ContextThemeWrapper(mContext, R.style.PrivacyChip);
mChips = new ArrayList<>();
mChipsContainer = (ViewGroup) LayoutInflater.from(privacyChipContext)
.inflate(R.layout.tv_privacy_chip_container, null);
int chipMargins = privacyChipContext.getResources()
.getDimensionPixelSize(R.dimen.privacy_chip_margin);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
lp.setMarginStart(chipMargins);
lp.setMarginEnd(chipMargins);
for (PrivacyItemsChip.ChipConfig chipConfig : CHIPS) {
PrivacyItemsChip chip = new PrivacyItemsChip(privacyChipContext, chipConfig);
mChipsContainer.addView(chip, lp);
mChips.add(chip);
}
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
windowManager.addView(mChipsContainer, getWindowLayoutParams());
final ViewGroup container = mChipsContainer;
mChipsContainer.getViewTreeObserver()
.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (DEBUG) Log.v(TAG, "Chips container laid out");
container.getViewTreeObserver().removeOnGlobalLayoutListener(this);
updateChips();
}
});
}
private WindowManager.LayoutParams getWindowLayoutParams() {
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 | (mIsRtl ? Gravity.LEFT : Gravity.RIGHT);
layoutParams.setTitle(LAYOUT_PARAMS_TITLE);
layoutParams.packageName = mContext.getPackageName();
return layoutParams;
}
@UiThread
private void removeIndicatorView() {
if (DEBUG) Log.d(TAG, "removeIndicatorView");
mUiThreadHandler.removeCallbacks(mCollapseRunnable);
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
if (windowManager != null && mChipsContainer != null) {
windowManager.removeView(mChipsContainer);
}
mChipsContainer = null;
mChips = null;
}
/**
* Schedules the accessibility announcement to be made after {@link
* #ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS} (if possible). This is so that only one announcement is
* made instead of two separate ones if both the camera and the mic are started/stopped.
*/
@UiThread
private void postAccessibilityAnnouncement() {
mUiThreadHandler.removeCallbacks(mAccessibilityRunnable);
if (mPrivacyItems.size() == 0) {
// Announce immediately since announcement cannot be made once the chip is gone.
makeAccessibilityAnnouncement();
} else {
mUiThreadHandler.postDelayed(mAccessibilityRunnable,
ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS);
}
}
private void makeAccessibilityAnnouncement() {
if (mChipsContainer == null) {
return;
}
boolean cameraWasRecording = listContainsPrivacyType(mItemsBeforeLastAnnouncement,
PrivacyType.TYPE_CAMERA);
boolean cameraIsRecording = listContainsPrivacyType(mPrivacyItems,
PrivacyType.TYPE_CAMERA);
boolean micWasRecording = listContainsPrivacyType(mItemsBeforeLastAnnouncement,
PrivacyType.TYPE_MICROPHONE);
boolean micIsRecording = listContainsPrivacyType(mPrivacyItems,
PrivacyType.TYPE_MICROPHONE);
boolean screenWasRecording = listContainsPrivacyType(mItemsBeforeLastAnnouncement,
PrivacyType.TYPE_MEDIA_PROJECTION);
boolean screenIsRecording = listContainsPrivacyType(mPrivacyItems,
PrivacyType.TYPE_MEDIA_PROJECTION);
int announcement = 0;
if (!cameraWasRecording && cameraIsRecording && !micWasRecording && micIsRecording) {
// Both started
announcement = R.string.mic_and_camera_recording_announcement;
} else if (cameraWasRecording && !cameraIsRecording && micWasRecording && !micIsRecording) {
// Both stopped
announcement = R.string.mic_camera_stopped_recording_announcement;
} else {
// Did the camera start or stop?
if (cameraWasRecording && !cameraIsRecording) {
announcement = R.string.camera_stopped_recording_announcement;
} else if (!cameraWasRecording && cameraIsRecording) {
announcement = R.string.camera_recording_announcement;
}
// Announce camera changes now since we might need a second announcement about the mic.
if (announcement != 0) {
mChipsContainer.announceForAccessibility(mContext.getString(announcement));
announcement = 0;
}
// Did the mic start or stop?
if (micWasRecording && !micIsRecording) {
announcement = R.string.mic_stopped_recording_announcement;
} else if (!micWasRecording && micIsRecording) {
announcement = R.string.mic_recording_announcement;
}
}
if (announcement != 0) {
mChipsContainer.announceForAccessibility(mContext.getString(announcement));
}
if (!screenWasRecording && screenIsRecording) {
mChipsContainer.announceForAccessibility(
mContext.getString(R.string.screen_recording_announcement));
} else if (screenWasRecording && !screenIsRecording) {
mChipsContainer.announceForAccessibility(
mContext.getString(R.string.screen_stopped_recording_announcement));
}
mItemsBeforeLastAnnouncement.clear();
mItemsBeforeLastAnnouncement.addAll(mPrivacyItems);
}
private boolean listContainsPrivacyType(List<PrivacyItem> list, PrivacyType privacyType) {
for (PrivacyItem item : list) {
if (item.getPrivacyType() == privacyType) {
return true;
}
}
return false;
}
}

View File

@@ -1,233 +0,0 @@
/*
* Copyright (C) 2022 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.sensorprivacy.television;
import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
import android.annotation.DimenRes;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.hardware.SensorPrivacyManager;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
import com.android.systemui.tv.TvBottomSheetActivity;
import com.android.systemui.util.settings.GlobalSettings;
import javax.inject.Inject;
/**
* Bottom sheet that is shown when the camera/mic sensors privacy state changed
* by the global software toggle or physical privacy switch.
*/
public class TvSensorPrivacyChangedActivity extends TvBottomSheetActivity {
private static final String TAG = TvSensorPrivacyChangedActivity.class.getSimpleName();
private static final int ALL_SENSORS = Integer.MAX_VALUE;
private int mSensor = -1;
private int mToggleType = -1;
private final GlobalSettings mGlobalSettings;
private final IndividualSensorPrivacyController mSensorPrivacyController;
private IndividualSensorPrivacyController.Callback mSensorPrivacyCallback;
private TextView mTitle;
private TextView mContent;
private ImageView mIcon;
private ImageView mSecondIcon;
private Button mPositiveButton;
private Button mCancelButton;
@Inject
public TvSensorPrivacyChangedActivity(
IndividualSensorPrivacyController individualSensorPrivacyController,
GlobalSettings globalSettings) {
mSensorPrivacyController = individualSensorPrivacyController;
mGlobalSettings = globalSettings;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addSystemFlags(
WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
boolean allSensors = getIntent().getBooleanExtra(SensorPrivacyManager.EXTRA_ALL_SENSORS,
false);
if (allSensors) {
mSensor = ALL_SENSORS;
} else {
mSensor = getIntent().getIntExtra(SensorPrivacyManager.EXTRA_SENSOR, -1);
}
mToggleType = getIntent().getIntExtra(SensorPrivacyManager.EXTRA_TOGGLE_TYPE, -1);
if (mSensor == -1 || mToggleType == -1) {
Log.v(TAG, "Invalid extras");
finish();
return;
}
// Do not show for software toggles
if (mToggleType == SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE) {
finish();
return;
}
mSensorPrivacyCallback = (sensor, blocked) -> {
updateUI();
};
initUI();
}
private void initUI() {
mTitle = findViewById(R.id.bottom_sheet_title);
mContent = findViewById(R.id.bottom_sheet_body);
mIcon = findViewById(R.id.bottom_sheet_icon);
// mic icon if both icons are shown
mSecondIcon = findViewById(R.id.bottom_sheet_second_icon);
mPositiveButton = findViewById(R.id.bottom_sheet_positive_button);
mCancelButton = findViewById(R.id.bottom_sheet_negative_button);
mCancelButton.setText(android.R.string.cancel);
mCancelButton.setOnClickListener(v -> finish());
updateUI();
}
private void updateUI() {
final Resources resources = getResources();
setIconTint(resources.getBoolean(R.bool.config_unblockHwSensorIconEnableTint));
setIconSize(R.dimen.unblock_hw_sensor_icon_width, R.dimen.unblock_hw_sensor_icon_height);
switch (mSensor) {
case CAMERA:
updateUiForCameraUpdate(
mSensorPrivacyController.isSensorBlockedByHardwareToggle(CAMERA));
break;
case MICROPHONE:
default:
updateUiForMicUpdate(
mSensorPrivacyController.isSensorBlockedByHardwareToggle(MICROPHONE));
break;
}
// Start animation if drawable is animated
Drawable iconDrawable = mIcon.getDrawable();
if (iconDrawable instanceof Animatable) {
((Animatable) iconDrawable).start();
}
mPositiveButton.setVisibility(View.GONE);
mCancelButton.setText(android.R.string.ok);
}
private void updateUiForMicUpdate(boolean blocked) {
if (blocked) {
mTitle.setText(R.string.sensor_privacy_mic_turned_off_dialog_title);
if (isExplicitUserInteractionAudioBypassAllowed()) {
mContent.setText(R.string.sensor_privacy_mic_blocked_with_exception_dialog_content);
} else {
mContent.setText(R.string.sensor_privacy_mic_blocked_no_exception_dialog_content);
}
mIcon.setImageResource(R.drawable.unblock_hw_sensor_microphone);
mSecondIcon.setVisibility(View.GONE);
} else {
mTitle.setText(R.string.sensor_privacy_mic_turned_on_dialog_title);
mContent.setText(R.string.sensor_privacy_mic_unblocked_dialog_content);
mIcon.setImageResource(com.android.internal.R.drawable.ic_mic_allowed);
mSecondIcon.setVisibility(View.GONE);
}
}
private void updateUiForCameraUpdate(boolean blocked) {
if (blocked) {
mTitle.setText(R.string.sensor_privacy_camera_turned_off_dialog_title);
mContent.setText(R.string.sensor_privacy_camera_blocked_dialog_content);
mIcon.setImageResource(R.drawable.unblock_hw_sensor_camera);
mSecondIcon.setVisibility(View.GONE);
} else {
mTitle.setText(R.string.sensor_privacy_camera_turned_on_dialog_title);
mContent.setText(R.string.sensor_privacy_camera_unblocked_dialog_content);
mIcon.setImageResource(com.android.internal.R.drawable.ic_camera_allowed);
mSecondIcon.setVisibility(View.GONE);
}
}
private void setIconTint(boolean enableTint) {
final Resources resources = getResources();
if (enableTint) {
final ColorStateList iconTint = resources.getColorStateList(
R.color.bottom_sheet_icon_color, getTheme());
mIcon.setImageTintList(iconTint);
mSecondIcon.setImageTintList(iconTint);
} else {
mIcon.setImageTintList(null);
mSecondIcon.setImageTintList(null);
}
mIcon.invalidate();
mSecondIcon.invalidate();
}
private void setIconSize(@DimenRes int widthRes, @DimenRes int heightRes) {
final Resources resources = getResources();
final int iconWidth = resources.getDimensionPixelSize(widthRes);
final int iconHeight = resources.getDimensionPixelSize(heightRes);
mIcon.getLayoutParams().width = iconWidth;
mIcon.getLayoutParams().height = iconHeight;
mIcon.invalidate();
mSecondIcon.getLayoutParams().width = iconWidth;
mSecondIcon.getLayoutParams().height = iconHeight;
mSecondIcon.invalidate();
}
private boolean isExplicitUserInteractionAudioBypassAllowed() {
return mGlobalSettings.getInt(
Settings.Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED, 1) == 1;
}
@Override
public void onResume() {
super.onResume();
updateUI();
mSensorPrivacyController.addCallback(mSensorPrivacyCallback);
}
@Override
public void onPause() {
mSensorPrivacyController.removeCallback(mSensorPrivacyCallback);
super.onPause();
}
}

View File

@@ -1,342 +0,0 @@
/*
* 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.sensorprivacy.television;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
import static android.hardware.SensorPrivacyManager.Sources.OTHER;
import android.annotation.DimenRes;
import android.app.AppOpsManager;
import android.app.role.RoleManager;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.hardware.SensorPrivacyManager;
import android.os.Bundle;
import android.os.UserHandle;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.systemui.R;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
import com.android.systemui.tv.TvBottomSheetActivity;
import java.util.List;
import javax.inject.Inject;
/**
* Bottom sheet that is shown when the camera/mic sensors are blocked by the global software toggle
* or physical privacy switch.
*/
public class TvUnblockSensorActivity extends TvBottomSheetActivity {
private static final String TAG = TvUnblockSensorActivity.class.getSimpleName();
private static final String ACTION_MANAGE_CAMERA_PRIVACY =
"android.settings.MANAGE_CAMERA_PRIVACY";
private static final String ACTION_MANAGE_MICROPHONE_PRIVACY =
"android.settings.MANAGE_MICROPHONE_PRIVACY";
private static final int ALL_SENSORS = Integer.MAX_VALUE;
private int mSensor = -1;
private final AppOpsManager mAppOpsManager;
private final RoleManager mRoleManager;
private final IndividualSensorPrivacyController mSensorPrivacyController;
private IndividualSensorPrivacyController.Callback mSensorPrivacyCallback;
private TextView mTitle;
private TextView mContent;
private ImageView mIcon;
private ImageView mSecondIcon;
private Button mPositiveButton;
private Button mCancelButton;
@Inject
public TvUnblockSensorActivity(
IndividualSensorPrivacyController individualSensorPrivacyController,
AppOpsManager appOpsManager, RoleManager roleManager) {
mSensorPrivacyController = individualSensorPrivacyController;
mAppOpsManager = appOpsManager;
mRoleManager = roleManager;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addSystemFlags(
WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
boolean allSensors = getIntent().getBooleanExtra(SensorPrivacyManager.EXTRA_ALL_SENSORS,
false);
if (allSensors) {
mSensor = ALL_SENSORS;
} else {
mSensor = getIntent().getIntExtra(SensorPrivacyManager.EXTRA_SENSOR, -1);
}
if (mSensor == -1) {
Log.v(TAG, "Invalid extras");
finish();
return;
}
mSensorPrivacyCallback = (sensor, blocked) -> {
if (mSensor == ALL_SENSORS && !mSensorPrivacyController.isSensorBlocked(CAMERA)
&& !mSensorPrivacyController.isSensorBlocked(MICROPHONE)) {
showToastAndFinish();
} else if (this.mSensor == sensor && !blocked) {
showToastAndFinish();
} else {
updateUI();
}
};
initUI();
}
private void showToastAndFinish() {
final int toastMsgResId;
switch(mSensor) {
case MICROPHONE:
toastMsgResId = R.string.sensor_privacy_mic_unblocked_toast_content;
break;
case CAMERA:
toastMsgResId = R.string.sensor_privacy_camera_unblocked_toast_content;
break;
case ALL_SENSORS:
default:
toastMsgResId = R.string.sensor_privacy_mic_camera_unblocked_toast_content;
break;
}
showToastAndFinish(toastMsgResId);
}
private void showToastAndFinish(int toastMsgResId) {
Toast.makeText(this, toastMsgResId, Toast.LENGTH_SHORT).show();
finish();
}
private boolean isBlockedByHardwareToggle() {
if (mSensor == ALL_SENSORS) {
return mSensorPrivacyController.isSensorBlockedByHardwareToggle(CAMERA)
|| mSensorPrivacyController.isSensorBlockedByHardwareToggle(MICROPHONE);
} else {
return mSensorPrivacyController.isSensorBlockedByHardwareToggle(mSensor);
}
}
private void initUI() {
mTitle = findViewById(R.id.bottom_sheet_title);
mContent = findViewById(R.id.bottom_sheet_body);
mIcon = findViewById(R.id.bottom_sheet_icon);
// mic icon if both icons are shown
mSecondIcon = findViewById(R.id.bottom_sheet_second_icon);
mPositiveButton = findViewById(R.id.bottom_sheet_positive_button);
mCancelButton = findViewById(R.id.bottom_sheet_negative_button);
mCancelButton.setText(android.R.string.cancel);
mCancelButton.setOnClickListener(v -> finish());
updateUI();
}
private void updateUI() {
if (isHTTAccessDisabled()) {
updateUiForHTT();
} else if (isBlockedByHardwareToggle()) {
updateUiForHardwareToggle();
} else {
updateUiForSoftwareToggle();
}
}
private void updateUiForHardwareToggle() {
final Resources resources = getResources();
boolean micBlocked = (mSensor == MICROPHONE || mSensor == ALL_SENSORS)
&& mSensorPrivacyController.isSensorBlockedByHardwareToggle(MICROPHONE);
boolean cameraBlocked = (mSensor == CAMERA || mSensor == ALL_SENSORS)
&& mSensorPrivacyController.isSensorBlockedByHardwareToggle(CAMERA);
setIconTint(resources.getBoolean(R.bool.config_unblockHwSensorIconEnableTint));
setIconSize(R.dimen.unblock_hw_sensor_icon_width, R.dimen.unblock_hw_sensor_icon_height);
if (micBlocked && cameraBlocked) {
mTitle.setText(R.string.sensor_privacy_start_use_mic_camera_blocked_dialog_title);
mContent.setText(
R.string.sensor_privacy_start_use_mic_camera_blocked_dialog_content);
mIcon.setImageResource(R.drawable.unblock_hw_sensor_all);
Drawable secondIconDrawable = resources.getDrawable(
R.drawable.unblock_hw_sensor_all_second, getTheme());
if (secondIconDrawable == null) {
mSecondIcon.setVisibility(View.GONE);
} else {
mSecondIcon.setImageDrawable(secondIconDrawable);
}
} else if (cameraBlocked) {
mTitle.setText(R.string.sensor_privacy_start_use_camera_blocked_dialog_title);
mContent.setText(R.string.sensor_privacy_start_use_camera_blocked_dialog_content);
mIcon.setImageResource(R.drawable.unblock_hw_sensor_camera);
mSecondIcon.setVisibility(View.GONE);
} else if (micBlocked) {
mTitle.setText(R.string.sensor_privacy_start_use_mic_blocked_dialog_title);
mContent.setText(R.string.sensor_privacy_start_use_mic_blocked_dialog_content);
mIcon.setImageResource(R.drawable.unblock_hw_sensor_microphone);
mSecondIcon.setVisibility(View.GONE);
}
// Start animation if drawable is animated
Drawable iconDrawable = mIcon.getDrawable();
if (iconDrawable instanceof Animatable) {
((Animatable) iconDrawable).start();
}
mPositiveButton.setVisibility(View.GONE);
mCancelButton.setText(android.R.string.ok);
}
private void updateUiForSoftwareToggle() {
setIconTint(true);
setIconSize(R.dimen.bottom_sheet_icon_size, R.dimen.bottom_sheet_icon_size);
switch (mSensor) {
case MICROPHONE:
mTitle.setText(R.string.sensor_privacy_start_use_mic_blocked_dialog_title);
mContent.setText(R.string.sensor_privacy_start_use_mic_dialog_content);
mIcon.setImageResource(com.android.internal.R.drawable.perm_group_microphone);
mSecondIcon.setVisibility(View.GONE);
break;
case CAMERA:
mTitle.setText(R.string.sensor_privacy_start_use_camera_blocked_dialog_title);
mContent.setText(R.string.sensor_privacy_start_use_camera_dialog_content);
mIcon.setImageResource(com.android.internal.R.drawable.perm_group_camera);
mSecondIcon.setVisibility(View.GONE);
break;
case ALL_SENSORS:
default:
mTitle.setText(R.string.sensor_privacy_start_use_mic_camera_blocked_dialog_title);
mContent.setText(R.string.sensor_privacy_start_use_mic_camera_dialog_content);
mIcon.setImageResource(com.android.internal.R.drawable.perm_group_camera);
mSecondIcon.setImageResource(
com.android.internal.R.drawable.perm_group_microphone);
break;
}
mPositiveButton.setText(
com.android.internal.R.string.sensor_privacy_start_use_dialog_turn_on_button);
mPositiveButton.setOnClickListener(v -> {
if (mSensor == ALL_SENSORS) {
mSensorPrivacyController.setSensorBlocked(OTHER, CAMERA, false);
mSensorPrivacyController.setSensorBlocked(OTHER, MICROPHONE, false);
} else {
mSensorPrivacyController.setSensorBlocked(OTHER, mSensor, false);
}
});
}
private void updateUiForHTT() {
setIconTint(true);
setIconSize(R.dimen.bottom_sheet_icon_size, R.dimen.bottom_sheet_icon_size);
mTitle.setText(R.string.sensor_privacy_start_use_mic_blocked_dialog_title);
mContent.setText(R.string.sensor_privacy_htt_blocked_dialog_content);
mIcon.setImageResource(com.android.internal.R.drawable.perm_group_microphone);
mSecondIcon.setVisibility(View.GONE);
mPositiveButton.setText(R.string.sensor_privacy_dialog_open_settings);
mPositiveButton.setOnClickListener(v -> {
Intent openPrivacySettings = new Intent(ACTION_MANAGE_MICROPHONE_PRIVACY);
ActivityInfo activityInfo = openPrivacySettings.resolveActivityInfo(getPackageManager(),
MATCH_SYSTEM_ONLY);
if (activityInfo == null) {
showToastAndFinish(com.android.internal.R.string.noApplications);
} else {
startActivity(openPrivacySettings);
finish();
}
});
}
private void setIconTint(boolean enableTint) {
final Resources resources = getResources();
if (enableTint) {
final ColorStateList iconTint = resources.getColorStateList(
R.color.bottom_sheet_icon_color, getTheme());
mIcon.setImageTintList(iconTint);
mSecondIcon.setImageTintList(iconTint);
} else {
mIcon.setImageTintList(null);
mSecondIcon.setImageTintList(null);
}
mIcon.invalidate();
mSecondIcon.invalidate();
}
private void setIconSize(@DimenRes int widthRes, @DimenRes int heightRes) {
final Resources resources = getResources();
final int iconWidth = resources.getDimensionPixelSize(widthRes);
final int iconHeight = resources.getDimensionPixelSize(heightRes);
mIcon.getLayoutParams().width = iconWidth;
mIcon.getLayoutParams().height = iconHeight;
mIcon.invalidate();
mSecondIcon.getLayoutParams().width = iconWidth;
mSecondIcon.getLayoutParams().height = iconHeight;
mSecondIcon.invalidate();
}
private boolean isHTTAccessDisabled() {
String pkg = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
List<String> assistantPkgs = mRoleManager.getRoleHolders(RoleManager.ROLE_ASSISTANT);
if (!assistantPkgs.contains(pkg)) {
return false;
}
return (mAppOpsManager.checkOpNoThrow(
AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, UserHandle.myUserId(),
pkg) != AppOpsManager.MODE_ALLOWED);
}
@Override
public void onResume() {
super.onResume();
updateUI();
mSensorPrivacyController.addCallback(mSensorPrivacyCallback);
}
@Override
public void onPause() {
mSensorPrivacyController.removeCallback(mSensorPrivacyCallback);
super.onPause();
}
}

View File

@@ -1,8 +0,0 @@
# Android TV Core Framework
rgl@google.com
valiiftime@google.com
galinap@google.com
patrikf@google.com
robhor@google.com
sergeynv@google.com

View File

@@ -1,87 +0,0 @@
/*
* Copyright (C) 2012 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;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.CoreStartable;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.KeyboardShortcuts;
import dagger.Lazy;
import javax.inject.Inject;
/**
* 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.
*/
@SysUISingleton
public class TvStatusBar implements CoreStartable, CommandQueue.Callbacks {
private static final String ACTION_SHOW_PIP_MENU =
"com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU";
private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
private final Context mContext;
private final CommandQueue mCommandQueue;
private final Lazy<AssistManager> mAssistManagerLazy;
@Inject
public TvStatusBar(Context context, CommandQueue commandQueue,
Lazy<AssistManager> assistManagerLazy) {
mContext = context;
mCommandQueue = commandQueue;
mAssistManagerLazy = assistManagerLazy;
}
@Override
public void start() {
final IStatusBarService barService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
mCommandQueue.addCallback(this);
try {
barService.registerStatusBar(mCommandQueue);
} catch (RemoteException ex) {
// If the system process isn't there we're doomed anyway.
}
}
@Override
public void startAssist(Bundle args) {
mAssistManagerLazy.get().startAssist(args);
}
@Override
public void showPictureInPictureMenu() {
mContext.sendBroadcast(
new Intent(ACTION_SHOW_PIP_MENU).setPackage(mContext.getPackageName()),
SYSTEMUI_PERMISSION);
}
@Override
public void toggleKeyboardShortcutsMenu(int deviceId) {
KeyboardShortcuts.show(mContext, deviceId);
}
}

View File

@@ -1,140 +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.
*/
@file:JvmName("VpnStatusObserver")
package com.android.systemui.statusbar.tv
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import com.android.internal.messages.nano.SystemMessageProto
import com.android.internal.net.VpnConfig
import com.android.systemui.CoreStartable
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.policy.SecurityController
import javax.inject.Inject
/**
* Observes if a vpn connection is active and displays a notification to the user
*/
@SysUISingleton
class VpnStatusObserver @Inject constructor(
private val context: Context,
private val securityController: SecurityController
) : CoreStartable,
SecurityController.SecurityControllerCallback {
private var vpnConnected = false
private val notificationManager = NotificationManager.from(context)
private val notificationChannel = createNotificationChannel()
private val vpnConnectedNotificationBuilder = createVpnConnectedNotificationBuilder()
private val vpnDisconnectedNotification = createVpnDisconnectedNotification()
private val vpnIconId: Int
get() = if (securityController.isVpnBranded) {
R.drawable.stat_sys_branded_vpn
} else {
R.drawable.stat_sys_vpn_ic
}
private val vpnName: String?
get() = securityController.primaryVpnName ?: securityController.workProfileVpnName
override fun start() {
// register callback to vpn state changes
securityController.addCallback(this)
}
override fun onStateChanged() {
securityController.isVpnEnabled.let { newVpnConnected ->
if (vpnConnected != newVpnConnected) {
if (newVpnConnected) {
notifyVpnConnected()
} else {
notifyVpnDisconnected()
}
vpnConnected = newVpnConnected
}
}
}
private fun notifyVpnConnected() = notificationManager.notify(
NOTIFICATION_TAG,
SystemMessageProto.SystemMessage.NOTE_VPN_STATUS,
createVpnConnectedNotification()
)
private fun notifyVpnDisconnected() = notificationManager.run {
// remove existing connected notification
cancel(NOTIFICATION_TAG, SystemMessageProto.SystemMessage.NOTE_VPN_STATUS)
// show the disconnected notification only for a short while
notify(NOTIFICATION_TAG, SystemMessageProto.SystemMessage.NOTE_VPN_DISCONNECTED,
vpnDisconnectedNotification)
}
private fun createNotificationChannel() =
NotificationChannel(
NOTIFICATION_CHANNEL_TV_VPN,
NOTIFICATION_CHANNEL_TV_VPN,
NotificationManager.IMPORTANCE_HIGH
).also {
notificationManager.createNotificationChannel(it)
}
private fun createVpnConnectedNotification() =
vpnConnectedNotificationBuilder
.apply {
vpnName?.let {
setContentText(
context.getString(
R.string.notification_disclosure_vpn_text, it
)
)
}
}
.build()
private fun createVpnConnectedNotificationBuilder() =
Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN)
.setSmallIcon(vpnIconId)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_SYSTEM)
.extend(Notification.TvExtender())
.setOngoing(true)
.setContentTitle(context.getString(R.string.notification_vpn_connected))
.setContentIntent(VpnConfig.getIntentForStatusPanel(context))
private fun createVpnDisconnectedNotification() =
Notification.Builder(context, NOTIFICATION_CHANNEL_TV_VPN)
.setSmallIcon(vpnIconId)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_SYSTEM)
.extend(Notification.TvExtender())
.setTimeoutAfter(VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS)
.setContentTitle(context.getString(R.string.notification_vpn_disconnected))
.build()
companion object {
const val NOTIFICATION_CHANNEL_TV_VPN = "VPN Status"
val NOTIFICATION_TAG: String = VpnStatusObserver::class.java.simpleName
private const val TAG = "TvVpnNotification"
private const val VPN_DISCONNECTED_NOTIFICATION_TIMEOUT_MS = 5_000L
}
}

View File

@@ -1,116 +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.notifications;
import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.PendingIntent;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.android.systemui.R;
/**
* Adapter for the VerticalGridView of the TvNotificationsPanelView.
*/
public class TvNotificationAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = "TvNotificationAdapter";
private SparseArray<StatusBarNotification> mNotifications;
public TvNotificationAdapter() {
setHasStableIds(true);
}
@NonNull
@Override
public TvNotificationViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.tv_notification_item,
parent, false);
return new TvNotificationViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (mNotifications == null) {
Log.e(TAG, "Could not bind view holder because the notification is missing");
return;
}
TvNotificationViewHolder holder = (TvNotificationViewHolder) viewHolder;
Notification notification = mNotifications.valueAt(position).getNotification();
holder.mTitle.setText(notification.extras.getString(Notification.EXTRA_TITLE));
holder.mDetails.setText(notification.extras.getString(Notification.EXTRA_TEXT));
holder.mPendingIntent = notification.contentIntent;
}
@Override
public int getItemCount() {
return mNotifications == null ? 0 : mNotifications.size();
}
@Override
public long getItemId(int position) {
// the item id is the notification id
return mNotifications.keyAt(position);
}
/**
* Updates the notifications and calls notifyDataSetChanged().
*/
public void setNotifications(SparseArray<StatusBarNotification> notifications) {
this.mNotifications = notifications;
notifyDataSetChanged();
}
private static class TvNotificationViewHolder extends RecyclerView.ViewHolder implements
View.OnClickListener {
final TextView mTitle;
final TextView mDetails;
PendingIntent mPendingIntent;
protected TvNotificationViewHolder(View itemView) {
super(itemView);
mTitle = itemView.findViewById(R.id.tv_notification_title);
mDetails = itemView.findViewById(R.id.tv_notification_details);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
try {
if (mPendingIntent != null) {
BroadcastOptions options = BroadcastOptions.makeBasic();
options.setInteractive(true);
options.setPendingIntentBackgroundActivityStartMode(
BroadcastOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
mPendingIntent.send(options.toBundle());
}
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "Pending intent canceled for : " + mPendingIntent);
}
}
}
}

View File

@@ -1,115 +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.notifications;
import android.annotation.Nullable;
import android.app.Notification;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.SparseArray;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.NotificationListener;
import javax.inject.Inject;
/**
* Keeps track of the notifications on TV.
*/
@SysUISingleton
public class TvNotificationHandler implements CoreStartable,
NotificationListener.NotificationHandler {
private static final String TAG = "TvNotificationHandler";
private final NotificationListener mNotificationListener;
private final SparseArray<StatusBarNotification> mNotifications = new SparseArray<>();
@Nullable
private Listener mUpdateListener;
@Inject
public TvNotificationHandler(NotificationListener notificationListener) {
mNotificationListener = notificationListener;
}
public SparseArray<StatusBarNotification> getCurrentNotifications() {
return mNotifications;
}
public void setTvNotificationListener(Listener listener) {
mUpdateListener = listener;
}
@Override
public void start() {
mNotificationListener.addNotificationHandler(this);
mNotificationListener.registerAsSystemService();
}
@Override
public void onNotificationPosted(StatusBarNotification sbn,
NotificationListenerService.RankingMap rankingMap) {
if (!new Notification.TvExtender(sbn.getNotification()).isAvailableOnTv()) {
Log.v(TAG, "Notification not added because it isn't relevant for tv");
return;
}
mNotifications.put(sbn.getId(), sbn);
if (mUpdateListener != null) {
mUpdateListener.notificationsUpdated(mNotifications);
}
Log.d(TAG, "Notification added");
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn,
NotificationListenerService.RankingMap rankingMap) {
if (mNotifications.contains(sbn.getId())) {
mNotifications.remove(sbn.getId());
Log.d(TAG, "Notification removed");
if (mUpdateListener != null) {
mUpdateListener.notificationsUpdated(mNotifications);
}
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn,
NotificationListenerService.RankingMap rankingMap, int reason) {
onNotificationRemoved(sbn, rankingMap);
}
@Override
public void onNotificationRankingUpdate(NotificationListenerService.RankingMap rankingMap) {
// noop
}
@Override
public void onNotificationsInitialized() {
// noop
}
/**
* Get notified when the notifications are updated.
*/
interface Listener {
void notificationsUpdated(SparseArray<StatusBarNotification> sbns);
}
}

View File

@@ -1,133 +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.notifications;
import android.Manifest;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
import android.util.Log;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.CommandQueue;
import javax.inject.Inject;
/**
* Offers control methods for the notification panel handler on TV devices.
*/
@SysUISingleton
public class TvNotificationPanel implements CoreStartable, CommandQueue.Callbacks {
private static final String TAG = "TvNotificationPanel";
private final Context mContext;
private final CommandQueue mCommandQueue;
private final String mNotificationHandlerPackage;
@Inject
public TvNotificationPanel(Context context, CommandQueue commandQueue) {
mContext = context;
mCommandQueue = commandQueue;
mNotificationHandlerPackage = mContext.getResources().getString(
com.android.internal.R.string.config_notificationHandlerPackage);
}
@Override
public void start() {
mCommandQueue.addCallback(this);
}
@Override
public void togglePanel() {
if (!mNotificationHandlerPackage.isEmpty()) {
startNotificationHandlerActivity(
new Intent(NotificationManager.ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL));
} else {
openInternalNotificationPanel(
NotificationManager.ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL);
}
}
@Override
public void animateExpandNotificationsPanel() {
if (!mNotificationHandlerPackage.isEmpty()) {
startNotificationHandlerActivity(
new Intent(NotificationManager.ACTION_OPEN_NOTIFICATION_HANDLER_PANEL));
} else {
openInternalNotificationPanel(
NotificationManager.ACTION_OPEN_NOTIFICATION_HANDLER_PANEL);
}
}
@Override
public void animateCollapsePanels(int flags, boolean force) {
if (!mNotificationHandlerPackage.isEmpty()
&& (flags & CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL) == 0) {
Intent closeNotificationIntent = new Intent(
NotificationManager.ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL);
closeNotificationIntent.setPackage(mNotificationHandlerPackage);
mContext.sendBroadcastAsUser(closeNotificationIntent, UserHandle.CURRENT);
} else {
openInternalNotificationPanel(
NotificationManager.ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL);
}
}
private void openInternalNotificationPanel(String action) {
Intent intent = new Intent(mContext, TvNotificationPanelActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.setAction(action);
mContext.startActivityAsUser(intent, UserHandle.SYSTEM);
}
/**
* Starts the activity intent if all of the following are true
* <ul>
* <li> the notification handler package is a system component </li>
* <li> the provided intent is handled by the notification handler package </li>
* <li> the notification handler requests the
* {@link android.Manifest.permission#STATUS_BAR_SERVICE} permission for the given intent</li>
* </ul>
*
* @param intent The intent for starting the desired activity
*/
private void startNotificationHandlerActivity(Intent intent) {
intent.setPackage(mNotificationHandlerPackage);
PackageManager pm = mContext.getPackageManager();
ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_SYSTEM_ONLY);
if (ri != null && ri.activityInfo != null) {
if (ri.activityInfo.permission != null && ri.activityInfo.permission.equals(
Manifest.permission.STATUS_BAR_SERVICE)) {
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
mContext.startActivityAsUser(intent, UserHandle.CURRENT);
} else {
Log.e(TAG,
"Not launching notification handler activity: Notification handler does "
+ "not require the STATUS_BAR_SERVICE permission for intent "
+ intent.getAction());
}
} else {
Log.e(TAG,
"Not launching notification handler activity: Could not resolve activityInfo "
+ "for intent "
+ intent.getAction());
}
}
}

View File

@@ -1,142 +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.notifications;
import android.annotation.NonNull;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.View;
import androidx.leanback.widget.VerticalGridView;
import com.android.systemui.R;
import java.util.function.Consumer;
import javax.inject.Inject;
/**
* This Activity shows a notification panel for tv. It is used if no other app (e.g. a launcher) can
* be found to show the notifications.
*/
public class TvNotificationPanelActivity extends Activity implements
TvNotificationHandler.Listener {
private final TvNotificationHandler mTvNotificationHandler;
private TvNotificationAdapter mTvNotificationAdapter;
private VerticalGridView mNotificationListView;
private View mNotificationPlaceholder;
private boolean mPanelAlreadyOpen = false;
private final Consumer<Boolean> mBlurConsumer = this::enableBlur;
@Inject
public TvNotificationPanelActivity(TvNotificationHandler tvNotificationHandler) {
super();
mTvNotificationHandler = tvNotificationHandler;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (maybeClosePanel(getIntent())) {
return;
}
mPanelAlreadyOpen = true;
setContentView(R.layout.tv_notification_panel);
mNotificationPlaceholder = findViewById(R.id.no_tv_notifications);
mTvNotificationAdapter = new TvNotificationAdapter();
mNotificationListView = findViewById(R.id.notifications_list);
mNotificationListView.setAdapter(mTvNotificationAdapter);
mNotificationListView.setColumnWidth(R.dimen.tv_notification_panel_width);
mTvNotificationHandler.setTvNotificationListener(this);
notificationsUpdated(mTvNotificationHandler.getCurrentNotifications());
}
@Override
public void notificationsUpdated(@NonNull SparseArray<StatusBarNotification> notificationList) {
mTvNotificationAdapter.setNotifications(notificationList);
boolean noNotifications = notificationList.size() == 0;
mNotificationListView.setVisibility(noNotifications ? View.GONE : View.VISIBLE);
mNotificationPlaceholder.setVisibility(noNotifications ? View.VISIBLE : View.GONE);
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
maybeClosePanel(intent);
}
/**
* Handles intents from onCreate and onNewIntent.
*
* @return true if the panel is being closed, false if it is being opened
*/
private boolean maybeClosePanel(Intent intent) {
if (NotificationManager.ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL.equals(intent.getAction())
|| (mPanelAlreadyOpen
&& NotificationManager.ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL.equals(
intent.getAction()))) {
finish();
return true;
}
return false;
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
getWindow().setGravity(Gravity.END);
getWindowManager().addCrossWindowBlurEnabledListener(mBlurConsumer);
}
private void enableBlur(boolean enabled) {
if (enabled) {
int blurRadius = getResources().getDimensionPixelSize(
R.dimen.tv_notification_blur_radius);
getWindow().setBackgroundDrawable(
new ColorDrawable(getColor(R.color.tv_notification_blur_background_color)));
getWindow().setBackgroundBlurRadius(blurRadius);
} else {
getWindow().setBackgroundDrawable(
new ColorDrawable(getColor(R.color.tv_notification_default_background_color)));
getWindow().setBackgroundBlurRadius(0);
}
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
getWindowManager().removeCrossWindowBlurEnabledListener(mBlurConsumer);
}
@Override
public void onDestroy() {
super.onDestroy();
mTvNotificationHandler.setTvNotificationListener(null);
}
}

View File

@@ -1,171 +0,0 @@
/*
* 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.tv
import com.android.systemui.CoreStartable
import com.android.systemui.SliceBroadcastRelayHandler
import com.android.systemui.accessibility.WindowMagnification
import com.android.systemui.dagger.qualifiers.PerUser
import com.android.systemui.globalactions.GlobalActionsComponent
import com.android.systemui.keyboard.KeyboardUI
import com.android.systemui.media.RingtonePlayer
import com.android.systemui.media.systemsounds.HomeSoundEffectController
import com.android.systemui.power.PowerUI
import com.android.systemui.privacy.television.TvPrivacyChipsController
import com.android.systemui.shortcut.ShortcutKeyDispatcher
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.tv.TvStatusBar
import com.android.systemui.statusbar.tv.VpnStatusObserver
import com.android.systemui.statusbar.tv.notifications.TvNotificationHandler
import com.android.systemui.statusbar.tv.notifications.TvNotificationPanel
import com.android.systemui.theme.ThemeOverlayController
import com.android.systemui.toast.ToastUI
import com.android.systemui.usb.StorageNotification
import com.android.systemui.util.NotificationChannels
import com.android.systemui.volume.VolumeUI
import com.android.systemui.wmshell.WMShell
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
/**
* Collection of {@link CoreStartable}s that should be run on TV.
*/
@Module
abstract class TVSystemUICoreStartableModule {
/** Inject into GlobalActionsComponent. */
@Binds
@IntoMap
@ClassKey(GlobalActionsComponent::class)
abstract fun bindGlobalActionsComponent(sysui: GlobalActionsComponent): CoreStartable
/** Inject into HomeSoundEffectController. */
@Binds
@IntoMap
@ClassKey(HomeSoundEffectController::class)
abstract fun bindHomeSoundEffectController(sysui: HomeSoundEffectController): CoreStartable
/** Inject into InstantAppNotifier. */
@Binds
@IntoMap
@ClassKey(InstantAppNotifier::class)
abstract fun bindInstantAppNotifier(sysui: InstantAppNotifier): CoreStartable
/** Inject into KeyboardUI. */
@Binds
@IntoMap
@ClassKey(KeyboardUI::class)
abstract fun bindKeyboardUI(sysui: KeyboardUI): CoreStartable
/** Inject into NotificationChannels. */
@Binds
@IntoMap
@ClassKey(NotificationChannels::class)
@PerUser
abstract fun bindNotificationChannels(sysui: NotificationChannels): CoreStartable
/** Inject into PowerUI. */
@Binds
@IntoMap
@ClassKey(PowerUI::class)
abstract fun bindPowerUI(sysui: PowerUI): CoreStartable
/** Inject into RingtonePlayer. */
@Binds
@IntoMap
@ClassKey(RingtonePlayer::class)
abstract fun bind(sysui: RingtonePlayer): CoreStartable
/** Inject into ShortcutKeyDispatcher. */
@Binds
@IntoMap
@ClassKey(ShortcutKeyDispatcher::class)
abstract fun bindShortcutKeyDispatcher(sysui: ShortcutKeyDispatcher): CoreStartable
/** Inject into SliceBroadcastRelayHandler. */
@Binds
@IntoMap
@ClassKey(SliceBroadcastRelayHandler::class)
abstract fun bindSliceBroadcastRelayHandler(sysui: SliceBroadcastRelayHandler): CoreStartable
/** Inject into StorageNotification. */
@Binds
@IntoMap
@ClassKey(StorageNotification::class)
abstract fun bindStorageNotification(sysui: StorageNotification): CoreStartable
/** Inject into ThemeOverlayController. */
@Binds
@IntoMap
@ClassKey(ThemeOverlayController::class)
abstract fun bindThemeOverlayController(sysui: ThemeOverlayController): CoreStartable
/** Inject into ToastUI. */
@Binds
@IntoMap
@ClassKey(ToastUI::class)
abstract fun bindToastUI(service: ToastUI): CoreStartable
/** Inject into TvNotificationHandler. */
@Binds
@IntoMap
@ClassKey(TvNotificationHandler::class)
abstract fun bindTvNotificationHandler(sysui: TvNotificationHandler): CoreStartable
/** Inject into TvNotificationPanel. */
@Binds
@IntoMap
@ClassKey(TvNotificationPanel::class)
abstract fun bindTvNotificationPanel(sysui: TvNotificationPanel): CoreStartable
/** Inject into TvPrivacyChipsController. */
@Binds
@IntoMap
@ClassKey(TvPrivacyChipsController::class)
abstract fun bindTvPrivacyChipsController(sysui: TvPrivacyChipsController): CoreStartable
/** Inject into TvStatusBar. */
@Binds
@IntoMap
@ClassKey(TvStatusBar::class)
abstract fun bindTvStatusBar(sysui: TvStatusBar): CoreStartable
/** Inject into VolumeUI. */
@Binds
@IntoMap
@ClassKey(VolumeUI::class)
abstract fun bindVolumeUI(sysui: VolumeUI): CoreStartable
/** Inject into VpnStatusObserver. */
@Binds
@IntoMap
@ClassKey(VpnStatusObserver::class)
abstract fun bindVpnStatusObserver(sysui: VpnStatusObserver): CoreStartable
/** Inject into WindowMagnification. */
@Binds
@IntoMap
@ClassKey(WindowMagnification::class)
abstract fun bindWindowMagnification(sysui: WindowMagnification): CoreStartable
/** Inject into WMShell. */
@Binds
@IntoMap
@ClassKey(WMShell::class)
abstract fun bindWMShell(sysui: WMShell): CoreStartable
}

View File

@@ -1,107 +0,0 @@
/*
* 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.tv;
import android.app.Activity;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import com.android.systemui.R;
import java.util.Collections;
import java.util.function.Consumer;
/**
* Generic bottom sheet with up to two icons in the beginning and two buttons.
*/
public abstract class TvBottomSheetActivity extends Activity {
private static final String TAG = TvBottomSheetActivity.class.getSimpleName();
private Drawable mBackgroundWithBlur;
private Drawable mBackgroundWithoutBlur;
private final Consumer<Boolean> mBlurConsumer = this::onBlurChanged;
private void onBlurChanged(boolean enabled) {
Log.v(TAG, "blur enabled: " + enabled);
getWindow().setBackgroundDrawable(enabled ? mBackgroundWithBlur : mBackgroundWithoutBlur);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tv_bottom_sheet);
overridePendingTransition(R.anim.tv_bottom_sheet_enter, 0);
mBackgroundWithBlur = getResources()
.getDrawable(R.drawable.bottom_sheet_background_with_blur);
mBackgroundWithoutBlur = getResources().getDrawable(R.drawable.bottom_sheet_background);
DisplayMetrics metrics = getResources().getDisplayMetrics();
int screenWidth = metrics.widthPixels;
int screenHeight = metrics.heightPixels;
int marginPx = getResources().getDimensionPixelSize(R.dimen.bottom_sheet_margin);
WindowManager.LayoutParams windowParams = getWindow().getAttributes();
windowParams.width = screenWidth - marginPx * 2;
windowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
windowParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
windowParams.horizontalMargin = 0f;
windowParams.verticalMargin = (float) marginPx / screenHeight;
windowParams.format = PixelFormat.TRANSPARENT;
windowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
windowParams.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
windowParams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
getWindow().setAttributes(windowParams);
getWindow().setElevation(getWindow().getElevation() + 5);
getWindow().setBackgroundBlurRadius(getResources().getDimensionPixelSize(
R.dimen.bottom_sheet_background_blur_radius));
final View rootView = findViewById(R.id.bottom_sheet);
rootView.addOnLayoutChangeListener((view, l, t, r, b, oldL, oldT, oldR, oldB) -> {
rootView.setUnrestrictedPreferKeepClearRects(
Collections.singletonList(new Rect(0, 0, r - l, b - t)));
});
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
getWindowManager().addCrossWindowBlurEnabledListener(mBlurConsumer);
}
@Override
public void onDetachedFromWindow() {
getWindowManager().removeCrossWindowBlurEnabledListener(mBlurConsumer);
super.onDetachedFromWindow();
}
@Override
public void finish() {
super.finish();
overridePendingTransition(0, R.anim.tv_bottom_sheet_exit);
}
}

View File

@@ -1,45 +0,0 @@
/*
* 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.
*/
package com.android.systemui.tv;
import com.android.systemui.dagger.GlobalModule;
import com.android.systemui.dagger.GlobalRootComponent;
import javax.inject.Singleton;
import dagger.Component;
/**
* Root component for Dagger injection.
*/
@Singleton
@Component(modules = {GlobalModule.class})
public interface TvGlobalRootComponent extends GlobalRootComponent {
/**
* Component Builder interface. This allows to bind Context instance in the component
*/
@Component.Builder
interface Builder extends GlobalRootComponent.Builder {
TvGlobalRootComponent build();
}
@Override
TvWMComponent.Builder getWMComponentBuilder();
@Override
TvSysUIComponent.Builder getSysUIComponent();
}

View File

@@ -1,61 +0,0 @@
/*
* 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.
*/
package com.android.systemui.tv;
import com.android.systemui.dagger.DefaultComponentBinder;
import com.android.systemui.dagger.DependencyProvider;
import com.android.systemui.dagger.SysUIComponent;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.SystemUIModule;
import com.android.systemui.globalactions.ShutdownUiModule;
import com.android.systemui.keyguard.dagger.KeyguardModule;
import com.android.systemui.recents.RecentsModule;
import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule;
import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
import com.android.systemui.statusbar.notification.row.NotificationRowModule;
import com.android.systemui.wallpapers.dagger.NoopWallpaperModule;
import dagger.Subcomponent;
/**
* Dagger Subcomponent for Core SysUI.
*/
@SysUISingleton
@Subcomponent(modules = {
CentralSurfacesDependenciesModule.class,
DefaultComponentBinder.class,
DependencyProvider.class,
KeyguardModule.class,
NoopWallpaperModule.class,
NotificationRowModule.class,
NotificationsModule.class,
RecentsModule.class,
ShutdownUiModule.class,
SystemUIModule.class,
TvSystemUIBinder.class,
TVSystemUICoreStartableModule.class,
TvSystemUIModule.class})
public interface TvSysUIComponent extends SysUIComponent {
/**
* Builder for a SysUIComponent.
*/
@Subcomponent.Builder
interface Builder extends SysUIComponent.Builder {
TvSysUIComponent build();
}
}

View File

@@ -1,28 +0,0 @@
/*
* 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.
*/
package com.android.systemui.tv;
import com.android.systemui.dagger.GlobalRootComponent;
import dagger.Binds;
import dagger.Module;
@Module
interface TvSystemUIBinder {
@Binds
GlobalRootComponent bindGlobalRootComponent(TvGlobalRootComponent globalRootComponent);
}

View File

@@ -1,37 +0,0 @@
/*
* 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.
*/
package com.android.systemui.tv;
import android.content.Context;
import com.android.systemui.SystemUIInitializer;
import com.android.systemui.dagger.GlobalRootComponent;
/**
* TV variant {@link SystemUIInitializer}, that substitutes default {@link GlobalRootComponent} for
* {@link TvGlobalRootComponent}
*/
public class TvSystemUIInitializer extends SystemUIInitializer {
public TvSystemUIInitializer(Context context) {
super(context);
}
@Override
protected GlobalRootComponent.Builder getGlobalRootComponentBuilder() {
return DaggerTvGlobalRootComponent.builder();
}
}

View File

@@ -1,221 +0,0 @@
/*
* 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.
*/
package com.android.systemui.tv;
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
import static com.android.systemui.Dependency.LEAK_REPORT_EMAIL_NAME;
import android.content.Context;
import android.hardware.SensorPrivacyManager;
import android.os.Handler;
import com.android.internal.logging.UiEventLogger;
import com.android.keyguard.KeyguardViewController;
import com.android.systemui.dagger.ReferenceSystemUIModule;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dock.DockManager;
import com.android.systemui.dock.DockManagerImpl;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.navigationbar.gestural.GestureModule;
import com.android.systemui.plugins.qs.QSFactory;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.power.dagger.PowerModule;
import com.android.systemui.privacy.MediaProjectionPrivacyItemMonitor;
import com.android.systemui.privacy.PrivacyItemMonitor;
import com.android.systemui.qs.dagger.QSModule;
import com.android.systemui.qs.tileimpl.QSFactoryImpl;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsImplementation;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
import com.android.systemui.shade.ShadeEmptyImplModule;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.KeyboardShortcutsModule;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.events.StatusBarEventsModule;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.phone.DozeServiceHost;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
import com.android.systemui.statusbar.policy.AospPolicyModule;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyControllerImpl;
import com.android.systemui.statusbar.policy.SensorPrivacyController;
import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl;
import com.android.systemui.statusbar.tv.notifications.TvNotificationHandler;
import com.android.systemui.volume.dagger.VolumeModule;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import javax.inject.Named;
/**
* A TV specific version of {@link ReferenceSystemUIModule}.
*
* Code here should be specific to the TV variant of SystemUI and will not be included in other
* variants of SystemUI.
*/
@Module(
includes = {
AospPolicyModule.class,
GestureModule.class,
MultiUserUtilsModule.class,
PowerModule.class,
QSModule.class,
ReferenceScreenshotModule.class,
ShadeEmptyImplModule.class,
StatusBarEventsModule.class,
VolumeModule.class,
KeyboardShortcutsModule.class
}
)
public abstract class TvSystemUIModule {
@SysUISingleton
@Provides
@Named(LEAK_REPORT_EMAIL_NAME)
static String provideLeakReportEmail() {
return "";
}
@Binds
abstract NotificationLockscreenUserManager bindNotificationLockscreenUserManager(
NotificationLockscreenUserManagerImpl notificationLockscreenUserManager);
@Provides
@SysUISingleton
static SensorPrivacyController provideSensorPrivacyController(
SensorPrivacyManager sensorPrivacyManager) {
SensorPrivacyController spC = new SensorPrivacyControllerImpl(sensorPrivacyManager);
spC.init();
return spC;
}
@Provides
@SysUISingleton
static IndividualSensorPrivacyController provideIndividualSensorPrivacyController(
SensorPrivacyManager sensorPrivacyManager) {
IndividualSensorPrivacyController spC = new IndividualSensorPrivacyControllerImpl(
sensorPrivacyManager);
spC.init();
return spC;
}
@Binds
@SysUISingleton
abstract QSFactory bindQSFactory(QSFactoryImpl qsFactoryImpl);
@Binds
abstract DockManager bindDockManager(DockManagerImpl dockManager);
@SysUISingleton
@Provides
@Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME)
static boolean provideAllowNotificationLongPress() {
return true;
}
@SysUISingleton
@Provides
static HeadsUpManagerPhone provideHeadsUpManagerPhone(
Context context,
HeadsUpManagerLogger headsUpManagerLogger,
StatusBarStateController statusBarStateController,
KeyguardBypassController bypassController,
GroupMembershipManager groupManager,
VisualStabilityProvider visualStabilityProvider,
ConfigurationController configurationController,
@Main Handler handler,
AccessibilityManagerWrapper accessibilityManagerWrapper,
UiEventLogger uiEventLogger,
ShadeExpansionStateManager shadeExpansionStateManager) {
return new HeadsUpManagerPhone(
context,
headsUpManagerLogger,
statusBarStateController,
bypassController,
groupManager,
visualStabilityProvider,
configurationController,
handler,
accessibilityManagerWrapper,
uiEventLogger,
shadeExpansionStateManager
);
}
@Binds
abstract HeadsUpManager bindHeadsUpManagerPhone(HeadsUpManagerPhone headsUpManagerPhone);
@Provides
@SysUISingleton
static Recents provideRecents(Context context, RecentsImplementation recentsImplementation,
CommandQueue commandQueue) {
return new Recents(context, recentsImplementation, commandQueue);
}
@SysUISingleton
@Provides
static DeviceProvisionedController providesDeviceProvisionedController(
DeviceProvisionedControllerImpl deviceProvisionedController) {
deviceProvisionedController.init();
return deviceProvisionedController;
}
@Binds
abstract KeyguardViewController bindKeyguardViewController(
StatusBarKeyguardViewManager statusBarKeyguardViewManager);
@Binds
abstract NotificationShadeWindowController bindNotificationShadeController(
NotificationShadeWindowControllerImpl notificationShadeWindowController);
@Binds
abstract DozeHost provideDozeHost(DozeServiceHost dozeServiceHost);
@Provides
@SysUISingleton
static TvNotificationHandler provideTvNotificationHandler(
NotificationListener notificationListener) {
return new TvNotificationHandler(notificationListener);
}
/**
* Binds {@link MediaProjectionPrivacyItemMonitor} into the set of {@link PrivacyItemMonitor}.
*/
@Binds
@IntoSet
abstract PrivacyItemMonitor bindMediaProjectionPrivacyItemMonitor(
MediaProjectionPrivacyItemMonitor mediaProjectionPrivacyItemMonitor);
}

View File

@@ -1,40 +0,0 @@
/*
* 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.
*/
package com.android.systemui.tv;
import com.android.systemui.dagger.WMComponent;
import com.android.wm.shell.dagger.WMSingleton;
import com.android.wm.shell.dagger.TvWMShellModule;
import dagger.Subcomponent;
/**
* Dagger Subcomponent for WindowManager.
*/
@WMSingleton
@Subcomponent(modules = {TvWMShellModule.class})
public interface TvWMComponent extends WMComponent {
/**
* Builder for a SysUIComponent.
*/
@Subcomponent.Builder
interface Builder extends WMComponent.Builder {
TvWMComponent build();
}
}

View File

@@ -1,53 +0,0 @@
/*
* 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.usb.tv;
import com.android.systemui.R;
/**
* Dialog shown to confirm the package to start when a USB device or accessory is attached and there
* is only one package that claims to handle this USB device or accessory.
*/
public class TvUsbConfirmActivity extends TvUsbDialogActivity {
private static final String TAG = TvUsbConfirmActivity.class.getSimpleName();
@Override
protected void onResume() {
super.onResume();
final int strId;
if (mDialogHelper.isUsbDevice()) {
boolean useRecordWarning = mDialogHelper.deviceHasAudioCapture()
&& !mDialogHelper.packageHasAudioRecordingPermission();
strId = useRecordWarning
? R.string.usb_device_confirm_prompt_warn
: R.string.usb_device_confirm_prompt;
} else {
// UsbAccessory case
strId = R.string.usb_accessory_confirm_prompt;
}
CharSequence text = getString(strId, mDialogHelper.getAppName(),
mDialogHelper.getDeviceDescription());
initUI(mDialogHelper.getAppName(), text);
}
@Override
void onConfirm() {
mDialogHelper.grantUidAccessPermission();
mDialogHelper.confirmDialogStartActivity();
finish();
}
}

View File

@@ -1,95 +0,0 @@
/*
* 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.usb.tv;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.tv.TvBottomSheetActivity;
import com.android.systemui.usb.UsbDialogHelper;
abstract class TvUsbDialogActivity extends TvBottomSheetActivity implements View.OnClickListener {
private static final String TAG = TvUsbDialogActivity.class.getSimpleName();
UsbDialogHelper mDialogHelper;
@Override
public final void onCreate(Bundle b) {
super.onCreate(b);
getWindow().addPrivateFlags(
WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
try {
mDialogHelper = new UsbDialogHelper(getApplicationContext(), getIntent());
} catch (IllegalStateException e) {
Log.e(TAG, "unable to initialize", e);
finish();
}
}
@Override
protected void onResume() {
super.onResume();
mDialogHelper.registerUsbDisconnectedReceiver(this);
}
@Override
protected void onPause() {
if (mDialogHelper != null) {
mDialogHelper.unregisterUsbDisconnectedReceiver(this);
}
super.onPause();
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.bottom_sheet_positive_button) {
onConfirm();
} else {
finish();
}
}
/**
* Called when the ok button is clicked.
*/
abstract void onConfirm();
void initUI(CharSequence title, CharSequence text) {
TextView titleTextView = findViewById(R.id.bottom_sheet_title);
TextView contentTextView = findViewById(R.id.bottom_sheet_body);
ImageView icon = findViewById(R.id.bottom_sheet_icon);
ImageView secondIcon = findViewById(R.id.bottom_sheet_second_icon);
Button okButton = findViewById(R.id.bottom_sheet_positive_button);
Button cancelButton = findViewById(R.id.bottom_sheet_negative_button);
titleTextView.setText(title);
contentTextView.setText(text);
icon.setImageResource(com.android.internal.R.drawable.ic_usb_48dp);
secondIcon.setVisibility(View.GONE);
okButton.setText(android.R.string.ok);
okButton.setOnClickListener(this);
cancelButton.setText(android.R.string.cancel);
cancelButton.setOnClickListener(this);
cancelButton.requestFocus();
}
}

View File

@@ -1,62 +0,0 @@
/*
* 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.usb.tv;
import com.android.systemui.R;
/**
* Dialog shown when a package requests access to a USB device or accessory on TVs.
*/
public class TvUsbPermissionActivity extends TvUsbDialogActivity {
private static final String TAG = TvUsbPermissionActivity.class.getSimpleName();
private boolean mPermissionGranted = false;
@Override
public void onResume() {
super.onResume();
final int strId;
if (mDialogHelper.isUsbDevice()) {
boolean useRecordWarning = mDialogHelper.deviceHasAudioCapture()
&& !mDialogHelper.packageHasAudioRecordingPermission();
strId = useRecordWarning
? R.string.usb_device_permission_prompt_warn
: R.string.usb_device_permission_prompt;
} else {
// UsbAccessory case
strId = R.string.usb_accessory_permission_prompt;
}
CharSequence text = getString(strId, mDialogHelper.getAppName(),
mDialogHelper.getDeviceDescription());
initUI(mDialogHelper.getAppName(), text);
}
@Override
protected void onPause() {
if (isFinishing()) {
mDialogHelper.sendPermissionDialogResponse(mPermissionGranted);
}
super.onPause();
}
@Override
void onConfirm() {
mDialogHelper.grantUidAccessPermission();
mPermissionGranted = true;
finish();
}
}

View File

@@ -1,109 +0,0 @@
/*
* Copyright (C) 2022 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.hdmi;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.os.UserHandle;
import android.provider.Settings;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.settings.SecureSettings;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Locale;
import java.util.concurrent.Executor;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class HdmiCecSetMenuLanguageHelperTest extends SysuiTestCase {
private HdmiCecSetMenuLanguageHelper mHdmiCecSetMenuLanguageHelper;
@Mock
private Executor mExecutor;
@Mock
private SecureSettings mSecureSettings;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mSecureSettings.getStringForUser(
Settings.Secure.HDMI_CEC_SET_MENU_LANGUAGE_DENYLIST,
UserHandle.USER_CURRENT)).thenReturn(null);
mHdmiCecSetMenuLanguageHelper =
new HdmiCecSetMenuLanguageHelper(mExecutor, mSecureSettings);
}
@Test
public void testSetGetLocale() {
mHdmiCecSetMenuLanguageHelper.setLocale("en");
assertThat(mHdmiCecSetMenuLanguageHelper.getLocale()).isEqualTo(Locale.ENGLISH);
}
@Test
public void testIsLocaleDenylisted_EmptyByDefault() {
mHdmiCecSetMenuLanguageHelper.setLocale("en");
assertThat(mHdmiCecSetMenuLanguageHelper.isLocaleDenylisted()).isEqualTo(false);
}
@Test
public void testIsLocaleDenylisted_AcceptLanguage() {
mHdmiCecSetMenuLanguageHelper.setLocale("de");
mHdmiCecSetMenuLanguageHelper.acceptLocale();
assertThat(mHdmiCecSetMenuLanguageHelper.isLocaleDenylisted()).isEqualTo(false);
verify(mExecutor).execute(any());
}
@Test
public void testIsLocaleDenylisted_DeclineLanguage() {
mHdmiCecSetMenuLanguageHelper.setLocale("de");
mHdmiCecSetMenuLanguageHelper.declineLocale();
assertThat(mHdmiCecSetMenuLanguageHelper.isLocaleDenylisted()).isEqualTo(true);
verify(mSecureSettings).putStringForUser(
Settings.Secure.HDMI_CEC_SET_MENU_LANGUAGE_DENYLIST, "de",
UserHandle.USER_CURRENT);
}
@Test
public void testIsLocaleDenylisted_DeclineTwoLanguages() {
mHdmiCecSetMenuLanguageHelper.setLocale("de");
mHdmiCecSetMenuLanguageHelper.declineLocale();
assertThat(mHdmiCecSetMenuLanguageHelper.isLocaleDenylisted()).isEqualTo(true);
verify(mSecureSettings).putStringForUser(
Settings.Secure.HDMI_CEC_SET_MENU_LANGUAGE_DENYLIST, "de",
UserHandle.USER_CURRENT);
mHdmiCecSetMenuLanguageHelper.setLocale("pl");
mHdmiCecSetMenuLanguageHelper.declineLocale();
assertThat(mHdmiCecSetMenuLanguageHelper.isLocaleDenylisted()).isEqualTo(true);
verify(mSecureSettings).putStringForUser(
Settings.Secure.HDMI_CEC_SET_MENU_LANGUAGE_DENYLIST, "de,pl",
UserHandle.USER_CURRENT);
}
}