1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.keyguard; 18 19 import android.animation.LayoutTransition; 20 import android.animation.ObjectAnimator; 21 import android.animation.PropertyValuesHolder; 22 import android.annotation.ColorInt; 23 import android.annotation.StyleRes; 24 import android.app.PendingIntent; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.graphics.Color; 28 import android.graphics.drawable.Drawable; 29 import android.graphics.drawable.InsetDrawable; 30 import android.graphics.text.LineBreaker; 31 import android.net.Uri; 32 import android.os.Trace; 33 import android.text.TextUtils; 34 import android.text.TextUtils.TruncateAt; 35 import android.util.AttributeSet; 36 import android.view.Gravity; 37 import android.view.View; 38 import android.view.animation.Animation; 39 import android.widget.LinearLayout; 40 import android.widget.TextView; 41 42 import androidx.slice.SliceItem; 43 import androidx.slice.core.SliceQuery; 44 import androidx.slice.widget.RowContent; 45 import androidx.slice.widget.SliceContent; 46 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.internal.graphics.ColorUtils; 49 import com.android.settingslib.Utils; 50 import com.android.systemui.R; 51 import com.android.systemui.animation.Interpolators; 52 import com.android.systemui.util.wakelock.KeepAwakeAnimationListener; 53 54 import java.io.PrintWriter; 55 import java.util.HashMap; 56 import java.util.HashSet; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.Set; 60 61 /** 62 * View visible under the clock on the lock screen and AoD. 63 */ 64 public class KeyguardSliceView extends LinearLayout { 65 66 private static final String TAG = "KeyguardSliceView"; 67 public static final int DEFAULT_ANIM_DURATION = 550; 68 69 private final LayoutTransition mLayoutTransition; 70 @VisibleForTesting 71 TextView mTitle; 72 private Row mRow; 73 private int mTextColor; 74 private float mDarkAmount = 0; 75 76 private int mIconSize; 77 private int mIconSizeWithHeader; 78 /** 79 * Runnable called whenever the view contents change. 80 */ 81 private Runnable mContentChangeListener; 82 private boolean mHasHeader; 83 private View.OnClickListener mOnClickListener; 84 KeyguardSliceView(Context context, AttributeSet attrs)85 public KeyguardSliceView(Context context, AttributeSet attrs) { 86 super(context, attrs); 87 88 Resources resources = context.getResources(); 89 mLayoutTransition = new LayoutTransition(); 90 mLayoutTransition.setStagger(LayoutTransition.CHANGE_APPEARING, DEFAULT_ANIM_DURATION / 2); 91 mLayoutTransition.setDuration(LayoutTransition.APPEARING, DEFAULT_ANIM_DURATION); 92 mLayoutTransition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 2); 93 mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING); 94 mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); 95 mLayoutTransition.setInterpolator(LayoutTransition.APPEARING, 96 Interpolators.FAST_OUT_SLOW_IN); 97 mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT); 98 mLayoutTransition.setAnimateParentHierarchy(false); 99 } 100 101 @Override onFinishInflate()102 protected void onFinishInflate() { 103 super.onFinishInflate(); 104 mTitle = findViewById(R.id.title); 105 mRow = findViewById(R.id.row); 106 mTextColor = Utils.getColorAttrDefaultColor(mContext, R.attr.wallpaperTextColor); 107 mIconSize = (int) mContext.getResources().getDimension(R.dimen.widget_icon_size); 108 mIconSizeWithHeader = (int) mContext.getResources().getDimension(R.dimen.header_icon_size); 109 mTitle.setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED); 110 } 111 112 @Override onVisibilityAggregated(boolean isVisible)113 public void onVisibilityAggregated(boolean isVisible) { 114 super.onVisibilityAggregated(isVisible); 115 setLayoutTransition(isVisible ? mLayoutTransition : null); 116 } 117 118 /** 119 * Returns whether the current visible slice has a title/header. 120 */ hasHeader()121 public boolean hasHeader() { 122 return mHasHeader; 123 } 124 hideSlice()125 void hideSlice() { 126 mTitle.setVisibility(GONE); 127 mRow.setVisibility(GONE); 128 mHasHeader = false; 129 if (mContentChangeListener != null) { 130 mContentChangeListener.run(); 131 } 132 } 133 showSlice(RowContent header, List<SliceContent> subItems)134 Map<View, PendingIntent> showSlice(RowContent header, List<SliceContent> subItems) { 135 Trace.beginSection("KeyguardSliceView#showSlice"); 136 mHasHeader = header != null; 137 Map<View, PendingIntent> clickActions = new HashMap<>(); 138 139 if (!mHasHeader) { 140 mTitle.setVisibility(GONE); 141 } else { 142 mTitle.setVisibility(VISIBLE); 143 144 SliceItem mainTitle = header.getTitleItem(); 145 CharSequence title = mainTitle != null ? mainTitle.getText() : null; 146 mTitle.setText(title); 147 if (header.getPrimaryAction() != null 148 && header.getPrimaryAction().getAction() != null) { 149 clickActions.put(mTitle, header.getPrimaryAction().getAction()); 150 } 151 } 152 153 final int subItemsCount = subItems.size(); 154 final int blendedColor = getTextColor(); 155 final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it 156 mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE); 157 LinearLayout.LayoutParams layoutParams = (LayoutParams) mRow.getLayoutParams(); 158 layoutParams.gravity = Gravity.START; 159 mRow.setLayoutParams(layoutParams); 160 161 for (int i = startIndex; i < subItemsCount; i++) { 162 RowContent rc = (RowContent) subItems.get(i); 163 SliceItem item = rc.getSliceItem(); 164 final Uri itemTag = item.getSlice().getUri(); 165 // Try to reuse the view if already exists in the layout 166 KeyguardSliceTextView button = mRow.findViewWithTag(itemTag); 167 if (button == null) { 168 button = new KeyguardSliceTextView(mContext); 169 button.setTextColor(blendedColor); 170 button.setTag(itemTag); 171 final int viewIndex = i - (mHasHeader ? 1 : 0); 172 mRow.addView(button, viewIndex); 173 } 174 175 PendingIntent pendingIntent = null; 176 if (rc.getPrimaryAction() != null) { 177 pendingIntent = rc.getPrimaryAction().getAction(); 178 } 179 clickActions.put(button, pendingIntent); 180 181 final SliceItem titleItem = rc.getTitleItem(); 182 button.setText(titleItem == null ? null : titleItem.getText()); 183 button.setContentDescription(rc.getContentDescription()); 184 185 Drawable iconDrawable = null; 186 SliceItem icon = SliceQuery.find(item.getSlice(), 187 android.app.slice.SliceItem.FORMAT_IMAGE); 188 if (icon != null) { 189 final int iconSize = mHasHeader ? mIconSizeWithHeader : mIconSize; 190 iconDrawable = icon.getIcon().loadDrawable(mContext); 191 if (iconDrawable != null) { 192 if (iconDrawable instanceof InsetDrawable) { 193 // System icons (DnD) use insets which are fine for centered slice content 194 // but will cause a slight indent for left/right-aligned slice views 195 iconDrawable = ((InsetDrawable) iconDrawable).getDrawable(); 196 } 197 final int width = (int) (iconDrawable.getIntrinsicWidth() 198 / (float) iconDrawable.getIntrinsicHeight() * iconSize); 199 iconDrawable.setBounds(0, 0, Math.max(width, 1), iconSize); 200 } 201 } 202 button.setCompoundDrawablesRelative(iconDrawable, null, null, null); 203 button.setOnClickListener(mOnClickListener); 204 button.setClickable(pendingIntent != null); 205 } 206 207 // Removing old views 208 for (int i = 0; i < mRow.getChildCount(); i++) { 209 View child = mRow.getChildAt(i); 210 if (!clickActions.containsKey(child)) { 211 mRow.removeView(child); 212 i--; 213 } 214 } 215 216 if (mContentChangeListener != null) { 217 mContentChangeListener.run(); 218 } 219 Trace.endSection(); 220 221 return clickActions; 222 } 223 setDarkAmount(float darkAmount)224 public void setDarkAmount(float darkAmount) { 225 mDarkAmount = darkAmount; 226 mRow.setDarkAmount(darkAmount); 227 updateTextColors(); 228 } 229 updateTextColors()230 private void updateTextColors() { 231 final int blendedColor = getTextColor(); 232 mTitle.setTextColor(blendedColor); 233 int childCount = mRow.getChildCount(); 234 for (int i = 0; i < childCount; i++) { 235 View v = mRow.getChildAt(i); 236 if (v instanceof TextView) { 237 ((TextView) v).setTextColor(blendedColor); 238 } 239 } 240 } 241 242 /** 243 * Runnable that gets invoked every time the title or the row visibility changes. 244 * @param contentChangeListener The listener. 245 */ setContentChangeListener(Runnable contentChangeListener)246 public void setContentChangeListener(Runnable contentChangeListener) { 247 mContentChangeListener = contentChangeListener; 248 } 249 250 @VisibleForTesting getTextColor()251 int getTextColor() { 252 return ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); 253 } 254 255 @VisibleForTesting setTextColor(@olorInt int textColor)256 void setTextColor(@ColorInt int textColor) { 257 mTextColor = textColor; 258 updateTextColors(); 259 } 260 onDensityOrFontScaleChanged()261 void onDensityOrFontScaleChanged() { 262 mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.widget_icon_size); 263 mIconSizeWithHeader = (int) mContext.getResources().getDimension(R.dimen.header_icon_size); 264 265 for (int i = 0; i < mRow.getChildCount(); i++) { 266 View child = mRow.getChildAt(i); 267 if (child instanceof KeyguardSliceTextView) { 268 ((KeyguardSliceTextView) child).onDensityOrFontScaleChanged(); 269 } 270 } 271 } 272 onOverlayChanged()273 void onOverlayChanged() { 274 for (int i = 0; i < mRow.getChildCount(); i++) { 275 View child = mRow.getChildAt(i); 276 if (child instanceof KeyguardSliceTextView) { 277 ((KeyguardSliceTextView) child).onOverlayChanged(); 278 } 279 } 280 } dump(PrintWriter pw, String[] args)281 public void dump(PrintWriter pw, String[] args) { 282 pw.println("KeyguardSliceView:"); 283 pw.println(" mTitle: " + (mTitle == null ? "null" : mTitle.getVisibility() == VISIBLE)); 284 pw.println(" mRow: " + (mRow == null ? "null" : mRow.getVisibility() == VISIBLE)); 285 pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); 286 pw.println(" mDarkAmount: " + mDarkAmount); 287 pw.println(" mHasHeader: " + mHasHeader); 288 } 289 290 @Override setOnClickListener(View.OnClickListener onClickListener)291 public void setOnClickListener(View.OnClickListener onClickListener) { 292 mOnClickListener = onClickListener; 293 mTitle.setOnClickListener(onClickListener); 294 } 295 296 public static class Row extends LinearLayout { 297 private Set<KeyguardSliceTextView> mKeyguardSliceTextViewSet = new HashSet(); 298 299 /** 300 * This view is visible in AOD, which means that the device will sleep if we 301 * don't hold a wake lock. We want to enter doze only after all views have reached 302 * their desired positions. 303 */ 304 private final Animation.AnimationListener mKeepAwakeListener; 305 private LayoutTransition mLayoutTransition; 306 private float mDarkAmount; 307 Row(Context context)308 public Row(Context context) { 309 this(context, null); 310 } 311 Row(Context context, AttributeSet attrs)312 public Row(Context context, AttributeSet attrs) { 313 this(context, attrs, 0); 314 } 315 Row(Context context, AttributeSet attrs, int defStyleAttr)316 public Row(Context context, AttributeSet attrs, int defStyleAttr) { 317 this(context, attrs, defStyleAttr, 0); 318 } 319 Row(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)320 public Row(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 321 super(context, attrs, defStyleAttr, defStyleRes); 322 mKeepAwakeListener = new KeepAwakeAnimationListener(mContext); 323 } 324 325 @Override onFinishInflate()326 protected void onFinishInflate() { 327 mLayoutTransition = new LayoutTransition(); 328 mLayoutTransition.setDuration(DEFAULT_ANIM_DURATION); 329 330 PropertyValuesHolder left = PropertyValuesHolder.ofInt("left", 0, 1); 331 PropertyValuesHolder right = PropertyValuesHolder.ofInt("right", 0, 1); 332 ObjectAnimator changeAnimator = ObjectAnimator.ofPropertyValuesHolder((Object) null, 333 left, right); 334 mLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, changeAnimator); 335 mLayoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, changeAnimator); 336 mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_APPEARING, 337 Interpolators.ACCELERATE_DECELERATE); 338 mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING, 339 Interpolators.ACCELERATE_DECELERATE); 340 mLayoutTransition.setStartDelay(LayoutTransition.CHANGE_APPEARING, 341 DEFAULT_ANIM_DURATION); 342 mLayoutTransition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 343 DEFAULT_ANIM_DURATION); 344 345 ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); 346 mLayoutTransition.setAnimator(LayoutTransition.APPEARING, appearAnimator); 347 mLayoutTransition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN); 348 349 ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f); 350 mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING, 351 Interpolators.ALPHA_OUT); 352 mLayoutTransition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 4); 353 mLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator); 354 355 mLayoutTransition.setAnimateParentHierarchy(false); 356 } 357 358 @Override onVisibilityAggregated(boolean isVisible)359 public void onVisibilityAggregated(boolean isVisible) { 360 super.onVisibilityAggregated(isVisible); 361 setLayoutTransition(isVisible ? mLayoutTransition : null); 362 } 363 364 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)365 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 366 int width = MeasureSpec.getSize(widthMeasureSpec); 367 int childCount = getChildCount(); 368 369 for (int i = 0; i < childCount; i++) { 370 View child = getChildAt(i); 371 if (child instanceof KeyguardSliceTextView) { 372 ((KeyguardSliceTextView) child).setMaxWidth(Integer.MAX_VALUE); 373 } 374 } 375 376 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 377 } 378 379 /** 380 * Set the amount (ratio) that the device has transitioned to doze. 381 * 382 * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. 383 */ setDarkAmount(float darkAmount)384 public void setDarkAmount(float darkAmount) { 385 boolean isDozing = darkAmount != 0; 386 boolean wasDozing = mDarkAmount != 0; 387 if (isDozing == wasDozing) { 388 return; 389 } 390 mDarkAmount = darkAmount; 391 setLayoutAnimationListener(isDozing ? null : mKeepAwakeListener); 392 } 393 394 @Override hasOverlappingRendering()395 public boolean hasOverlappingRendering() { 396 return false; 397 } 398 399 @Override addView(View view, int index)400 public void addView(View view, int index) { 401 super.addView(view, index); 402 403 if (view instanceof KeyguardSliceTextView) { 404 mKeyguardSliceTextViewSet.add((KeyguardSliceTextView) view); 405 } 406 } 407 408 @Override removeView(View view)409 public void removeView(View view) { 410 super.removeView(view); 411 if (view instanceof KeyguardSliceTextView) { 412 mKeyguardSliceTextViewSet.remove((KeyguardSliceTextView) view); 413 } 414 } 415 } 416 417 /** 418 * Representation of an item that appears under the clock on main keyguard message. 419 */ 420 @VisibleForTesting 421 static class KeyguardSliceTextView extends TextView { 422 423 @StyleRes 424 private static int sStyleId = R.style.TextAppearance_Keyguard_Secondary; 425 KeyguardSliceTextView(Context context)426 KeyguardSliceTextView(Context context) { 427 super(context, null /* attrs */, 0 /* styleAttr */, sStyleId); 428 onDensityOrFontScaleChanged(); 429 setEllipsize(TruncateAt.END); 430 } 431 onDensityOrFontScaleChanged()432 public void onDensityOrFontScaleChanged() { 433 updatePadding(); 434 } 435 onOverlayChanged()436 public void onOverlayChanged() { 437 setTextAppearance(sStyleId); 438 } 439 440 @Override setText(CharSequence text, BufferType type)441 public void setText(CharSequence text, BufferType type) { 442 super.setText(text, type); 443 updatePadding(); 444 } 445 updatePadding()446 private void updatePadding() { 447 boolean hasText = !TextUtils.isEmpty(getText()); 448 int padding = (int) getContext().getResources() 449 .getDimension(R.dimen.widget_horizontal_padding) / 2; 450 // orientation is vertical, so add padding to top & bottom 451 setPadding(0, padding, 0, hasText ? padding : 0); 452 453 setCompoundDrawablePadding((int) mContext.getResources() 454 .getDimension(R.dimen.widget_icon_padding)); 455 } 456 457 @Override setTextColor(int color)458 public void setTextColor(int color) { 459 super.setTextColor(color); 460 updateDrawableColors(); 461 } 462 463 @Override setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, Drawable bottom)464 public void setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, 465 Drawable bottom) { 466 super.setCompoundDrawablesRelative(start, top, end, bottom); 467 updateDrawableColors(); 468 updatePadding(); 469 } 470 updateDrawableColors()471 private void updateDrawableColors() { 472 final int color = getCurrentTextColor(); 473 for (Drawable drawable : getCompoundDrawables()) { 474 if (drawable != null) { 475 drawable.setTint(color); 476 } 477 } 478 } 479 } 480 } 481