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