Move TV-specific classes out of SystemUI
Bug: 289184493 Test: m TvSystemUI Change-Id: Ieb087a59a1837141c29b42717653fbf463269873
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user