blob: 95e9c20235b0ba80763fde7c56ed31bebb18c1c4 [file] [log] [blame]
/*
* Copyright (C) 2017 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.keyguard;
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.ColorInt;
import android.annotation.StyleRes;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.text.LineBreaker;
import android.net.Uri;
import android.os.Trace;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.Display;
import android.view.View;
import android.view.animation.Animation;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.SliceViewManager;
import androidx.slice.core.SliceQuery;
import androidx.slice.widget.ListContent;
import androidx.slice.widget.RowContent;
import androidx.slice.widget.SliceContent;
import androidx.slice.widget.SliceLiveData;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.keyguard.KeyguardSliceProvider;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.wakelock.KeepAwakeAnimationListener;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
/**
* View visible under the clock on the lock screen and AoD.
*/
public class KeyguardSliceView extends LinearLayout implements View.OnClickListener,
Observer<Slice>, TunerService.Tunable, ConfigurationController.ConfigurationListener {
private static final String TAG = "KeyguardSliceView";
public static final int DEFAULT_ANIM_DURATION = 550;
private final HashMap<View, PendingIntent> mClickActions;
private final ActivityStarter mActivityStarter;
private final ConfigurationController mConfigurationController;
private final LayoutTransition mLayoutTransition;
private final TunerService mTunerService;
private Uri mKeyguardSliceUri;
@VisibleForTesting
TextView mTitle;
private Row mRow;
private int mTextColor;
private float mDarkAmount = 0;
private LiveData<Slice> mLiveData;
private int mDisplayId = INVALID_DISPLAY;
private int mIconSize;
private int mIconSizeWithHeader;
/**
* Runnable called whenever the view contents change.
*/
private Runnable mContentChangeListener;
private Slice mSlice;
private boolean mHasHeader;
private final int mRowWithHeaderPadding;
private final int mRowPadding;
private float mRowTextSize;
private float mRowWithHeaderTextSize;
@Inject
public KeyguardSliceView(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
ActivityStarter activityStarter, ConfigurationController configurationController,
TunerService tunerService, @Main Resources resources) {
super(context, attrs);
mTunerService = tunerService;
mClickActions = new HashMap<>();
mRowPadding = resources.getDimensionPixelSize(R.dimen.subtitle_clock_padding);
mRowWithHeaderPadding = resources.getDimensionPixelSize(R.dimen.header_subtitle_padding);
mActivityStarter = activityStarter;
mConfigurationController = configurationController;
mLayoutTransition = new LayoutTransition();
mLayoutTransition.setStagger(LayoutTransition.CHANGE_APPEARING, DEFAULT_ANIM_DURATION / 2);
mLayoutTransition.setDuration(LayoutTransition.APPEARING, DEFAULT_ANIM_DURATION);
mLayoutTransition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 2);
mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
mLayoutTransition.setInterpolator(LayoutTransition.APPEARING,
Interpolators.FAST_OUT_SLOW_IN);
mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
mLayoutTransition.setAnimateParentHierarchy(false);
}
// Temporary workaround to allow KeyguardStatusView to inflate a copy for Universal Smartspace.
// Eventually the existing copy will be reparented instead, and we won't need this.
public KeyguardSliceView(Context context, AttributeSet attributeSet) {
this(context, attributeSet, Dependency.get(ActivityStarter.class),
Dependency.get(ConfigurationController.class), Dependency.get(TunerService.class),
context.getResources());
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTitle = findViewById(R.id.title);
mRow = findViewById(R.id.row);
mTextColor = Utils.getColorAttrDefaultColor(mContext, R.attr.wallpaperTextColor);
mIconSize = (int) mContext.getResources().getDimension(R.dimen.widget_icon_size);
mIconSizeWithHeader = (int) mContext.getResources().getDimension(R.dimen.header_icon_size);
mRowTextSize = mContext.getResources().getDimensionPixelSize(
R.dimen.widget_label_font_size);
mRowWithHeaderTextSize = mContext.getResources().getDimensionPixelSize(
R.dimen.header_row_font_size);
mTitle.setOnClickListener(this);
mTitle.setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Display display = getDisplay();
if (display != null) {
mDisplayId = display.getDisplayId();
}
mTunerService.addTunable(this, Settings.Secure.KEYGUARD_SLICE_URI);
// Make sure we always have the most current slice
if (mDisplayId == DEFAULT_DISPLAY) {
mLiveData.observeForever(this);
}
mConfigurationController.addCallback(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// TODO(b/117344873) Remove below work around after this issue be fixed.
if (mDisplayId == DEFAULT_DISPLAY) {
mLiveData.removeObserver(this);
}
mTunerService.removeTunable(this);
mConfigurationController.removeCallback(this);
}
@Override
public void onVisibilityAggregated(boolean isVisible) {
super.onVisibilityAggregated(isVisible);
setLayoutTransition(isVisible ? mLayoutTransition : null);
}
/**
* Returns whether the current visible slice has a title/header.
*/
public boolean hasHeader() {
return mHasHeader;
}
private void showSlice() {
Trace.beginSection("KeyguardSliceView#showSlice");
if (mSlice == null) {
mTitle.setVisibility(GONE);
mRow.setVisibility(GONE);
mHasHeader = false;
if (mContentChangeListener != null) {
mContentChangeListener.run();
}
Trace.endSection();
return;
}
mClickActions.clear();
ListContent lc = new ListContent(getContext(), mSlice);
SliceContent headerContent = lc.getHeader();
mHasHeader = headerContent != null && !headerContent.getSliceItem().hasHint(HINT_LIST_ITEM);
List<SliceContent> subItems = new ArrayList<>();
for (int i = 0; i < lc.getRowItems().size(); i++) {
SliceContent subItem = lc.getRowItems().get(i);
String itemUri = subItem.getSliceItem().getSlice().getUri().toString();
// Filter out the action row
if (!KeyguardSliceProvider.KEYGUARD_ACTION_URI.equals(itemUri)) {
subItems.add(subItem);
}
}
if (!mHasHeader) {
mTitle.setVisibility(GONE);
} else {
mTitle.setVisibility(VISIBLE);
RowContent header = lc.getHeader();
SliceItem mainTitle = header.getTitleItem();
CharSequence title = mainTitle != null ? mainTitle.getText() : null;
mTitle.setText(title);
if (header.getPrimaryAction() != null
&& header.getPrimaryAction().getAction() != null) {
mClickActions.put(mTitle, header.getPrimaryAction().getAction());
}
}
final int subItemsCount = subItems.size();
final int blendedColor = getTextColor();
final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it
mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);
LinearLayout.LayoutParams layoutParams = (LayoutParams) mRow.getLayoutParams();
layoutParams.topMargin = mHasHeader ? mRowWithHeaderPadding : mRowPadding;
mRow.setLayoutParams(layoutParams);
for (int i = startIndex; i < subItemsCount; i++) {
RowContent rc = (RowContent) subItems.get(i);
SliceItem item = rc.getSliceItem();
final Uri itemTag = item.getSlice().getUri();
// Try to reuse the view if already exists in the layout
KeyguardSliceTextView button = mRow.findViewWithTag(itemTag);
if (button == null) {
button = new KeyguardSliceTextView(mContext);
button.setTextColor(blendedColor);
button.setTag(itemTag);
final int viewIndex = i - (mHasHeader ? 1 : 0);
mRow.addView(button, viewIndex);
}
PendingIntent pendingIntent = null;
if (rc.getPrimaryAction() != null) {
pendingIntent = rc.getPrimaryAction().getAction();
}
mClickActions.put(button, pendingIntent);
final SliceItem titleItem = rc.getTitleItem();
button.setText(titleItem == null ? null : titleItem.getText());
button.setContentDescription(rc.getContentDescription());
button.setTextSize(TypedValue.COMPLEX_UNIT_PX,
mHasHeader ? mRowWithHeaderTextSize : mRowTextSize);
Drawable iconDrawable = null;
SliceItem icon = SliceQuery.find(item.getSlice(),
android.app.slice.SliceItem.FORMAT_IMAGE);
if (icon != null) {
final int iconSize = mHasHeader ? mIconSizeWithHeader : mIconSize;
iconDrawable = icon.getIcon().loadDrawable(mContext);
if (iconDrawable != null) {
final int width = (int) (iconDrawable.getIntrinsicWidth()
/ (float) iconDrawable.getIntrinsicHeight() * iconSize);
iconDrawable.setBounds(0, 0, Math.max(width, 1), iconSize);
}
}
button.setCompoundDrawables(iconDrawable, null, null, null);
button.setOnClickListener(this);
button.setClickable(pendingIntent != null);
}
// Removing old views
for (int i = 0; i < mRow.getChildCount(); i++) {
View child = mRow.getChildAt(i);
if (!mClickActions.containsKey(child)) {
mRow.removeView(child);
i--;
}
}
if (mContentChangeListener != null) {
mContentChangeListener.run();
}
Trace.endSection();
}
public void setDarkAmount(float darkAmount) {
mDarkAmount = darkAmount;
mRow.setDarkAmount(darkAmount);
updateTextColors();
}
private void updateTextColors() {
final int blendedColor = getTextColor();
mTitle.setTextColor(blendedColor);
int childCount = mRow.getChildCount();
for (int i = 0; i < childCount; i++) {
View v = mRow.getChildAt(i);
if (v instanceof TextView) {
((TextView) v).setTextColor(blendedColor);
}
}
}
@Override
public void onClick(View v) {
final PendingIntent action = mClickActions.get(v);
if (action != null) {
mActivityStarter.startPendingIntentDismissingKeyguard(action);
}
}
/**
* Runnable that gets invoked every time the title or the row visibility changes.
* @param contentChangeListener The listener.
*/
public void setContentChangeListener(Runnable contentChangeListener) {
mContentChangeListener = contentChangeListener;
}
/**
* LiveData observer lifecycle.
* @param slice the new slice content.
*/
@Override
public void onChanged(Slice slice) {
mSlice = slice;
showSlice();
}
@Override
public void onTuningChanged(String key, String newValue) {
setupUri(newValue);
}
/**
* Sets the slice provider Uri.
*/
public void setupUri(String uriString) {
if (uriString == null) {
uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI;
}
boolean wasObserving = false;
if (mLiveData != null && mLiveData.hasActiveObservers()) {
wasObserving = true;
mLiveData.removeObserver(this);
}
mKeyguardSliceUri = Uri.parse(uriString);
mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);
if (wasObserving) {
mLiveData.observeForever(this);
}
}
@VisibleForTesting
int getTextColor() {
return ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
}
@VisibleForTesting
void setTextColor(@ColorInt int textColor) {
mTextColor = textColor;
updateTextColors();
}
@Override
public void onDensityOrFontScaleChanged() {
mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.widget_icon_size);
mIconSizeWithHeader = (int) mContext.getResources().getDimension(R.dimen.header_icon_size);
mRowTextSize = mContext.getResources().getDimensionPixelSize(
R.dimen.widget_label_font_size);
mRowWithHeaderTextSize = mContext.getResources().getDimensionPixelSize(
R.dimen.header_row_font_size);
}
public void refresh() {
Slice slice;
Trace.beginSection("KeyguardSliceView#refresh");
// We can optimize performance and avoid binder calls when we know that we're bound
// to a Slice on the same process.
if (KeyguardSliceProvider.KEYGUARD_SLICE_URI.equals(mKeyguardSliceUri.toString())) {
KeyguardSliceProvider instance = KeyguardSliceProvider.getAttachedInstance();
if (instance != null) {
slice = instance.onBindSlice(mKeyguardSliceUri);
} else {
Log.w(TAG, "Keyguard slice not bound yet?");
slice = null;
}
} else {
slice = SliceViewManager.getInstance(getContext()).bindSlice(mKeyguardSliceUri);
}
onChanged(slice);
Trace.endSection();
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("KeyguardSliceView:");
pw.println(" mClickActions: " + mClickActions);
pw.println(" mTitle: " + (mTitle == null ? "null" : mTitle.getVisibility() == VISIBLE));
pw.println(" mRow: " + (mRow == null ? "null" : mRow.getVisibility() == VISIBLE));
pw.println(" mTextColor: " + Integer.toHexString(mTextColor));
pw.println(" mDarkAmount: " + mDarkAmount);
pw.println(" mSlice: " + mSlice);
pw.println(" mHasHeader: " + mHasHeader);
}
public static class Row extends LinearLayout {
/**
* This view is visible in AOD, which means that the device will sleep if we
* don't hold a wake lock. We want to enter doze only after all views have reached
* their desired positions.
*/
private final Animation.AnimationListener mKeepAwakeListener;
private LayoutTransition mLayoutTransition;
private float mDarkAmount;
public Row(Context context) {
this(context, null);
}
public Row(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public Row(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public Row(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mKeepAwakeListener = new KeepAwakeAnimationListener(mContext);
}
@Override
protected void onFinishInflate() {
mLayoutTransition = new LayoutTransition();
mLayoutTransition.setDuration(DEFAULT_ANIM_DURATION);
PropertyValuesHolder left = PropertyValuesHolder.ofInt("left", 0, 1);
PropertyValuesHolder right = PropertyValuesHolder.ofInt("right", 0, 1);
ObjectAnimator changeAnimator = ObjectAnimator.ofPropertyValuesHolder((Object) null,
left, right);
mLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, changeAnimator);
mLayoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, changeAnimator);
mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_APPEARING,
Interpolators.ACCELERATE_DECELERATE);
mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING,
Interpolators.ACCELERATE_DECELERATE);
mLayoutTransition.setStartDelay(LayoutTransition.CHANGE_APPEARING,
DEFAULT_ANIM_DURATION);
mLayoutTransition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING,
DEFAULT_ANIM_DURATION);
ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
mLayoutTransition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
mLayoutTransition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING,
Interpolators.ALPHA_OUT);
mLayoutTransition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 4);
mLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
mLayoutTransition.setAnimateParentHierarchy(false);
}
@Override
public void onVisibilityAggregated(boolean isVisible) {
super.onVisibilityAggregated(isVisible);
setLayoutTransition(isVisible ? mLayoutTransition : null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child instanceof KeyguardSliceTextView) {
((KeyguardSliceTextView) child).setMaxWidth(width / childCount);
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void setDarkAmount(float darkAmount) {
boolean isAwake = darkAmount != 0;
boolean wasAwake = mDarkAmount != 0;
if (isAwake == wasAwake) {
return;
}
mDarkAmount = darkAmount;
setLayoutAnimationListener(isAwake ? null : mKeepAwakeListener);
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
}
/**
* Representation of an item that appears under the clock on main keyguard message.
*/
@VisibleForTesting
static class KeyguardSliceTextView extends TextView implements
ConfigurationController.ConfigurationListener {
@StyleRes
private static int sStyleId = R.style.TextAppearance_Keyguard_Secondary;
KeyguardSliceTextView(Context context) {
super(context, null /* attrs */, 0 /* styleAttr */, sStyleId);
onDensityOrFontScaleChanged();
setEllipsize(TruncateAt.END);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Dependency.get(ConfigurationController.class).addCallback(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Dependency.get(ConfigurationController.class).removeCallback(this);
}
@Override
public void onDensityOrFontScaleChanged() {
updatePadding();
}
@Override
public void onOverlayChanged() {
setTextAppearance(sStyleId);
}
@Override
public void setText(CharSequence text, BufferType type) {
super.setText(text, type);
updatePadding();
}
private void updatePadding() {
boolean hasText = !TextUtils.isEmpty(getText());
int horizontalPadding = (int) getContext().getResources()
.getDimension(R.dimen.widget_horizontal_padding) / 2;
setPadding(horizontalPadding, 0, horizontalPadding * (hasText ? 1 : -1), 0);
setCompoundDrawablePadding((int) mContext.getResources()
.getDimension(R.dimen.widget_icon_padding));
}
@Override
public void setTextColor(int color) {
super.setTextColor(color);
updateDrawableColors();
}
@Override
public void setCompoundDrawables(Drawable left, Drawable top, Drawable right,
Drawable bottom) {
super.setCompoundDrawables(left, top, right, bottom);
updateDrawableColors();
updatePadding();
}
private void updateDrawableColors() {
final int color = getCurrentTextColor();
for (Drawable drawable : getCompoundDrawables()) {
if (drawable != null) {
drawable.setTint(color);
}
}
}
}
}