blob: cb732c4c3b2c8af9c5ac87e6b95dc7e82e5de239 [file] [log] [blame]
Lucas Dupin957e50c2017-10-10 11:23:27 -07001/*
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
17package com.android.keyguard;
18
19import android.app.PendingIntent;
Lucas Dupin6bd86012017-12-05 17:58:57 -080020import android.arch.lifecycle.LiveData;
21import android.arch.lifecycle.Observer;
Lucas Dupin957e50c2017-10-10 11:23:27 -070022import android.content.Context;
Lucas Dupin6bd86012017-12-05 17:58:57 -080023import android.graphics.Canvas;
Lucas Dupin957e50c2017-10-10 11:23:27 -070024import android.graphics.Color;
Lucas Dupin6bd86012017-12-05 17:58:57 -080025import android.graphics.Paint;
26import android.graphics.drawable.Drawable;
Lucas Dupin957e50c2017-10-10 11:23:27 -070027import android.net.Uri;
Lucas Dupin6bd86012017-12-05 17:58:57 -080028import android.provider.Settings;
Lucas Dupin2a3c3e32018-01-05 17:02:43 -080029import android.text.Layout;
30import android.text.TextUtils;
31import android.text.TextUtils.TruncateAt;
Lucas Dupin957e50c2017-10-10 11:23:27 -070032import android.util.AttributeSet;
Lucas Dupin6bd86012017-12-05 17:58:57 -080033import android.util.Log;
34import android.view.View;
35import android.widget.Button;
Lucas Dupin957e50c2017-10-10 11:23:27 -070036import android.widget.LinearLayout;
37import android.widget.TextView;
38
39import com.android.internal.graphics.ColorUtils;
Lucas Dupin6bd86012017-12-05 17:58:57 -080040import com.android.settingslib.Utils;
41import com.android.systemui.Dependency;
Lucas Dupin957e50c2017-10-10 11:23:27 -070042import com.android.systemui.R;
43import com.android.systemui.keyguard.KeyguardSliceProvider;
Lucas Dupin6bd86012017-12-05 17:58:57 -080044import com.android.systemui.tuner.TunerService;
Lucas Dupin957e50c2017-10-10 11:23:27 -070045
Lucas Dupin2a3c3e32018-01-05 17:02:43 -080046import java.util.ArrayList;
Lucas Dupin6bd86012017-12-05 17:58:57 -080047import java.util.HashMap;
48import java.util.List;
49import java.util.function.Consumer;
50
51import androidx.app.slice.Slice;
52import androidx.app.slice.SliceItem;
53import androidx.app.slice.core.SliceQuery;
Mady Mellor90e9fce2018-01-22 17:30:40 -080054import androidx.app.slice.widget.ListContent;
55import androidx.app.slice.widget.RowContent;
Lucas Dupin6bd86012017-12-05 17:58:57 -080056import androidx.app.slice.widget.SliceLiveData;
Jason Monk2af19982017-11-07 19:38:27 -050057
Lucas Dupin957e50c2017-10-10 11:23:27 -070058/**
59 * View visible under the clock on the lock screen and AoD.
60 */
Lucas Dupin6bd86012017-12-05 17:58:57 -080061public class KeyguardSliceView extends LinearLayout implements View.OnClickListener,
62 Observer<Slice>, TunerService.Tunable {
Lucas Dupin957e50c2017-10-10 11:23:27 -070063
Lucas Dupin6bd86012017-12-05 17:58:57 -080064 private static final String TAG = "KeyguardSliceView";
65 private final HashMap<View, PendingIntent> mClickActions;
66 private Uri mKeyguardSliceUri;
Lucas Dupin957e50c2017-10-10 11:23:27 -070067 private TextView mTitle;
Lucas Dupin6bd86012017-12-05 17:58:57 -080068 private LinearLayout mRow;
Lucas Dupin957e50c2017-10-10 11:23:27 -070069 private int mTextColor;
70 private float mDarkAmount = 0;
71
Lucas Dupin6bd86012017-12-05 17:58:57 -080072 private LiveData<Slice> mLiveData;
73 private int mIconSize;
74 private Consumer<Boolean> mListener;
75 private boolean mHasHeader;
Lucas Dupin957e50c2017-10-10 11:23:27 -070076
77 public KeyguardSliceView(Context context) {
78 this(context, null, 0);
79 }
80
81 public KeyguardSliceView(Context context, AttributeSet attrs) {
82 this(context, attrs, 0);
83 }
84
85 public KeyguardSliceView(Context context, AttributeSet attrs, int defStyle) {
86 super(context, attrs, defStyle);
Lucas Dupin6bd86012017-12-05 17:58:57 -080087
88 TunerService tunerService = Dependency.get(TunerService.class);
89 tunerService.addTunable(this, Settings.Secure.KEYGUARD_SLICE_URI);
90
91 mClickActions = new HashMap<>();
Lucas Dupin957e50c2017-10-10 11:23:27 -070092 }
93
94 @Override
95 protected void onFinishInflate() {
96 super.onFinishInflate();
97 mTitle = findViewById(R.id.title);
Lucas Dupin6bd86012017-12-05 17:58:57 -080098 mRow = findViewById(R.id.row);
99 mTextColor = Utils.getColorAttr(mContext, R.attr.wallpaperTextColor);
100 mIconSize = (int) mContext.getResources().getDimension(R.dimen.widget_icon_size);
Lucas Dupin957e50c2017-10-10 11:23:27 -0700101 }
102
103 @Override
104 protected void onAttachedToWindow() {
105 super.onAttachedToWindow();
106
Lucas Dupin957e50c2017-10-10 11:23:27 -0700107 // Make sure we always have the most current slice
Lucas Dupin6bd86012017-12-05 17:58:57 -0800108 mLiveData.observeForever(this);
Lucas Dupin957e50c2017-10-10 11:23:27 -0700109 }
110
111 @Override
112 protected void onDetachedFromWindow() {
113 super.onDetachedFromWindow();
114
Lucas Dupin6bd86012017-12-05 17:58:57 -0800115 mLiveData.removeObserver(this);
Lucas Dupin957e50c2017-10-10 11:23:27 -0700116 }
117
118 private void showSlice(Slice slice) {
Lucas Dupin957e50c2017-10-10 11:23:27 -0700119
Mady Mellor90e9fce2018-01-22 17:30:40 -0800120 ListContent lc = new ListContent(slice);
121 mHasHeader = lc.hasHeader();
122 List<SliceItem> subItems = lc.getRowItems();
Lucas Dupin6bd86012017-12-05 17:58:57 -0800123 if (!mHasHeader) {
Lucas Dupin957e50c2017-10-10 11:23:27 -0700124 mTitle.setVisibility(GONE);
125 } else {
126 mTitle.setVisibility(VISIBLE);
Mady Mellor90e9fce2018-01-22 17:30:40 -0800127 // If there's a header it'll be the first subitem
128 RowContent header = new RowContent(subItems.get(0), true /* showStartItem */);
129 SliceItem mainTitle = header.getTitleItem();
130 CharSequence title = mainTitle != null ? mainTitle.getText() : null;
Lucas Dupin2a3c3e32018-01-05 17:02:43 -0800131 mTitle.setText(title);
132
133 // Check if we're already ellipsizing the text.
134 // We're going to figure out the best possible line break if not.
135 Layout layout = mTitle.getLayout();
136 if (layout != null){
137 final int lineCount = layout.getLineCount();
138 if (lineCount > 0) {
139 if (layout.getEllipsisCount(lineCount - 1) == 0) {
140 mTitle.setText(findBestLineBreak(title));
141 }
142 }
143 }
Lucas Dupin957e50c2017-10-10 11:23:27 -0700144 }
145
Lucas Dupin6bd86012017-12-05 17:58:57 -0800146 mClickActions.clear();
147 final int subItemsCount = subItems.size();
Lucas Dupina2a7a402018-01-12 17:45:51 -0800148 final int blendedColor = getTextColor();
Mady Mellor90e9fce2018-01-22 17:30:40 -0800149 final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it
150 for (int i = startIndex; i < subItemsCount; i++) {
Lucas Dupin6bd86012017-12-05 17:58:57 -0800151 SliceItem item = subItems.get(i);
Mady Mellor90e9fce2018-01-22 17:30:40 -0800152 RowContent rc = new RowContent(item, true /* showStartItem */);
Lucas Dupin6bd86012017-12-05 17:58:57 -0800153 final Uri itemTag = item.getSlice().getUri();
154 // Try to reuse the view if already exists in the layout
155 KeyguardSliceButton button = mRow.findViewWithTag(itemTag);
156 if (button == null) {
157 button = new KeyguardSliceButton(mContext);
Lucas Dupina2a7a402018-01-12 17:45:51 -0800158 button.setTextColor(blendedColor);
Lucas Dupin6bd86012017-12-05 17:58:57 -0800159 button.setTag(itemTag);
160 } else {
161 mRow.removeView(button);
162 }
163 button.setHasDivider(i < subItemsCount - 1);
164 mRow.addView(button, i);
165
Mady Mellor90e9fce2018-01-22 17:30:40 -0800166 PendingIntent pendingIntent = null;
167 if (rc.getContentIntent() != null) {
168 pendingIntent = rc.getContentIntent().getAction();
Lucas Dupin6bd86012017-12-05 17:58:57 -0800169 }
170 mClickActions.put(button, pendingIntent);
171
Mady Mellor90e9fce2018-01-22 17:30:40 -0800172 button.setText(rc.getTitleItem().getText());
Lucas Dupin6bd86012017-12-05 17:58:57 -0800173
174 Drawable iconDrawable = null;
175 SliceItem icon = SliceQuery.find(item.getSlice(),
176 android.app.slice.SliceItem.FORMAT_IMAGE);
177 if (icon != null) {
178 iconDrawable = icon.getIcon().loadDrawable(mContext);
179 final int width = (int) (iconDrawable.getIntrinsicWidth()
180 / (float) iconDrawable.getIntrinsicHeight() * mIconSize);
181 iconDrawable.setBounds(0, 0, Math.max(width, 1), mIconSize);
182 }
183 button.setCompoundDrawablesRelative(iconDrawable, null, null, null);
184 button.setOnClickListener(this);
Lucas Dupin957e50c2017-10-10 11:23:27 -0700185 }
186
Lucas Dupin6bd86012017-12-05 17:58:57 -0800187 // Removing old views
188 for (int i = 0; i < mRow.getChildCount(); i++) {
189 View child = mRow.getChildAt(i);
190 if (!mClickActions.containsKey(child)) {
191 mRow.removeView(child);
192 i--;
193 }
194 }
195
196 final int visibility = mHasHeader || subItemsCount > 0 ? VISIBLE : GONE;
Lucas Dupin957e50c2017-10-10 11:23:27 -0700197 if (visibility != getVisibility()) {
198 setVisibility(visibility);
199 }
Lucas Dupin6bd86012017-12-05 17:58:57 -0800200
201 mListener.accept(mHasHeader);
Lucas Dupin957e50c2017-10-10 11:23:27 -0700202 }
203
Lucas Dupin2a3c3e32018-01-05 17:02:43 -0800204 /**
205 * Breaks a string in 2 lines where both have similar character count
206 * but first line is always longer.
207 *
208 * @param charSequence Original text.
209 * @return Optimal string.
210 */
211 private CharSequence findBestLineBreak(CharSequence charSequence) {
212 if (TextUtils.isEmpty(charSequence)) {
213 return charSequence;
214 }
215
216 String source = charSequence.toString();
217 // Ignore if there is only 1 word,
218 // or if line breaks were manually set.
219 if (source.contains("\n") || !source.contains(" ")) {
220 return source;
221 }
222
223 final String[] words = source.split(" ");
224 final StringBuilder optimalString = new StringBuilder(source.length());
225 int current = 0;
226 while (optimalString.length() < source.length() - optimalString.length()) {
227 optimalString.append(words[current]);
228 if (current < words.length - 1) {
229 optimalString.append(" ");
230 }
231 current++;
232 }
233 optimalString.append("\n");
234 for (int i = current; i < words.length; i++) {
235 optimalString.append(words[i]);
236 if (current < words.length - 1) {
237 optimalString.append(" ");
238 }
239 }
240
241 return optimalString.toString();
242 }
243
Lucas Dupin957e50c2017-10-10 11:23:27 -0700244 public void setDark(float darkAmount) {
245 mDarkAmount = darkAmount;
246 updateTextColors();
247 }
248
Lucas Dupin957e50c2017-10-10 11:23:27 -0700249 private void updateTextColors() {
Lucas Dupina2a7a402018-01-12 17:45:51 -0800250 final int blendedColor = getTextColor();
Lucas Dupin957e50c2017-10-10 11:23:27 -0700251 mTitle.setTextColor(blendedColor);
Lucas Dupin6bd86012017-12-05 17:58:57 -0800252 int childCount = mRow.getChildCount();
253 for (int i = 0; i < childCount; i++) {
254 View v = mRow.getChildAt(i);
255 if (v instanceof Button) {
256 ((Button) v).setTextColor(blendedColor);
257 }
258 }
Lucas Dupin957e50c2017-10-10 11:23:27 -0700259 }
260
Lucas Dupin6bd86012017-12-05 17:58:57 -0800261 @Override
262 public void onClick(View v) {
263 final PendingIntent action = mClickActions.get(v);
264 if (action != null) {
265 try {
266 action.send();
267 } catch (PendingIntent.CanceledException e) {
268 Log.i(TAG, "Pending intent cancelled, nothing to launch", e);
269 }
270 }
271 }
272
273 public void setListener(Consumer<Boolean> listener) {
274 mListener = listener;
275 }
276
277 public boolean hasHeader() {
278 return mHasHeader;
279 }
280
281 /**
282 * LiveData observer lifecycle.
283 * @param slice the new slice content.
284 */
285 @Override
286 public void onChanged(Slice slice) {
287 showSlice(slice);
288 }
289
290 @Override
291 public void onTuningChanged(String key, String newValue) {
292 setupUri(newValue);
293 }
294
295 public void setupUri(String uriString) {
296 if (uriString == null) {
297 uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI;
298 }
299
300 boolean wasObserving = false;
301 if (mLiveData != null && mLiveData.hasActiveObservers()) {
302 wasObserving = true;
303 mLiveData.removeObserver(this);
304 }
305
306 mKeyguardSliceUri = Uri.parse(uriString);
307 mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);
308
309 if (wasObserving) {
310 mLiveData.observeForever(this);
Lucas Dupin6bd86012017-12-05 17:58:57 -0800311 }
312 }
313
Lucas Dupina2a7a402018-01-12 17:45:51 -0800314 public int getTextColor() {
315 return ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
316 }
317
Lucas Dupin6bd86012017-12-05 17:58:57 -0800318 /**
319 * Representation of an item that appears under the clock on main keyguard message.
320 * Shows optional separator.
321 */
322 private class KeyguardSliceButton extends Button {
323
Lucas Dupina6c9e342018-01-22 22:00:21 -0800324 private static final float SEPARATOR_HEIGHT = 0.7f;
Lucas Dupin6bd86012017-12-05 17:58:57 -0800325 private final Paint mPaint;
326 private boolean mHasDivider;
327
328 public KeyguardSliceButton(Context context) {
329 super(context, null /* attrs */,
330 com.android.keyguard.R.style.TextAppearance_Keyguard_Secondary);
331 mPaint = new Paint();
332 mPaint.setStyle(Paint.Style.STROKE);
333 float dividerWidth = context.getResources()
334 .getDimension(R.dimen.widget_separator_thickness);
335 mPaint.setStrokeWidth(dividerWidth);
336 int horizontalPadding = (int) context.getResources()
337 .getDimension(R.dimen.widget_horizontal_padding);
338 setPadding(horizontalPadding, 0, horizontalPadding, 0);
339 setCompoundDrawablePadding((int) context.getResources()
340 .getDimension(R.dimen.widget_icon_padding));
Lucas Dupin2a3c3e32018-01-05 17:02:43 -0800341 setMaxWidth(KeyguardSliceView.this.getWidth() / 2);
342 setMaxLines(1);
343 setEllipsize(TruncateAt.END);
Lucas Dupin6bd86012017-12-05 17:58:57 -0800344 }
345
346 public void setHasDivider(boolean hasDivider) {
347 mHasDivider = hasDivider;
Lucas Dupin957e50c2017-10-10 11:23:27 -0700348 }
349
350 @Override
Lucas Dupin6bd86012017-12-05 17:58:57 -0800351 public void setTextColor(int color) {
352 super.setTextColor(color);
353 mPaint.setColor(color);
Lucas Dupin957e50c2017-10-10 11:23:27 -0700354 }
355
356 @Override
Lucas Dupin6bd86012017-12-05 17:58:57 -0800357 protected void onDraw(Canvas canvas) {
358 super.onDraw(canvas);
359 if (mHasDivider) {
360 final int lineX = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : getWidth();
Lucas Dupina6c9e342018-01-22 22:00:21 -0800361 final int height = (int) (getHeight() * SEPARATOR_HEIGHT);
362 final int startY = getHeight() / 2 - height / 2;
363 canvas.drawLine(lineX, startY, lineX, startY + height, mPaint);
Lucas Dupin6bd86012017-12-05 17:58:57 -0800364 }
Lucas Dupin957e50c2017-10-10 11:23:27 -0700365 }
366 }
367}