diff options
| author | Adrian Roos <roosa@google.com> | 2016-04-05 14:54:55 -0700 |
|---|---|---|
| committer | Adrian Roos <roosa@google.com> | 2016-04-12 11:52:49 -0700 |
| commit | c1a80b08f08cfb038625cec537390705d16db3f5 (patch) | |
| tree | dbd75786414be9fe1a418331bfbe48d3fc95e638 /core/java | |
| parent | 250c617d13216a210f3ebca25c6f765c29334a8d (diff) | |
Notification MessagingStyle: Add handset views
Bug: 27250207
Change-Id: I499cf2beaeeb34f0f189815fc0911f3b8954bd50
Diffstat (limited to 'core/java')
| -rw-r--r-- | core/java/android/app/Notification.java | 126 | ||||
| -rw-r--r-- | core/java/android/widget/RemoteViews.java | 17 | ||||
| -rw-r--r-- | core/java/com/android/internal/widget/ImageFloatingTextView.java | 38 | ||||
| -rw-r--r-- | core/java/com/android/internal/widget/MessagingLinearLayout.java | 278 |
4 files changed, 442 insertions, 17 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index a759719dcd3c..520acf502f0d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -29,6 +29,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; @@ -43,6 +44,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.os.UserHandle; +import android.text.BidiFormatter; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -588,8 +590,8 @@ public class Notification implements Parcelable private static final int COLOR_INVALID = 1; /** - * Sphere of visibility of this notification, which affects how and when the SystemUI reveals - * the notification's presence and contents in untrusted situations (namely, on the secure + * Sphere of visibility of this notification, which affects how and when the SystemUI reveals + * the notification's presence and contents in untrusted situations (namely, on the secure * lockscreen). * * The default level, {@link #VISIBILITY_PRIVATE}, behaves exactly as notifications have always @@ -2227,7 +2229,8 @@ public class Notification implements Parcelable Log.d(TAG, "Unknown style class: " + templateClass); } else { try { - final Constructor<? extends Style> ctor = styleClass.getConstructor(); + final Constructor<? extends Style> ctor = + styleClass.getDeclaredConstructor(); ctor.setAccessible(true); final Style style = ctor.newInstance(); style.restoreFromExtras(mN.extras); @@ -3126,6 +3129,18 @@ public class Notification implements Parcelable * @param hasProgress whether the progress bar should be shown and set */ private RemoteViews applyStandardTemplate(int resId, boolean hasProgress) { + final Bundle ex = mN.extras; + + CharSequence title = processLegacyText(ex.getCharSequence(EXTRA_TITLE)); + CharSequence text = processLegacyText(ex.getCharSequence(EXTRA_TEXT)); + return applyStandardTemplate(resId, hasProgress, title, text); + } + + /** + * @param hasProgress whether the progress bar should be shown and set + */ + private RemoteViews applyStandardTemplate(int resId, boolean hasProgress, + CharSequence title, CharSequence text) { RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId); resetStandardTemplate(contentView); @@ -3134,17 +3149,15 @@ public class Notification implements Parcelable bindNotificationHeader(contentView); bindLargeIcon(contentView); - if (ex.getCharSequence(EXTRA_TITLE) != null) { + if (title != null) { contentView.setViewVisibility(R.id.title, View.VISIBLE); - contentView.setTextViewText(R.id.title, - processLegacyText(ex.getCharSequence(EXTRA_TITLE))); + contentView.setTextViewText(R.id.title, title); } boolean showProgress = handleProgressBar(hasProgress, contentView, ex); - if (ex.getCharSequence(EXTRA_TEXT) != null) { + if (text != null) { int textId = showProgress ? com.android.internal.R.id.text_line_1 : com.android.internal.R.id.text; - contentView.setTextViewText(textId, processLegacyText( - ex.getCharSequence(EXTRA_TEXT))); + contentView.setTextViewText(textId, text); contentView.setViewVisibility(textId, View.VISIBLE); } @@ -3749,6 +3762,10 @@ public class Notification implements Parcelable return R.layout.notification_template_material_inbox; } + private int getMessagingLayoutResource() { + return R.layout.notification_template_material_messaging; + } + private int getActionLayoutResource() { return R.layout.notification_material_action; } @@ -4375,13 +4392,100 @@ public class Notification implements Parcelable /** * @hide */ + @Override + public RemoteViews makeContentView() { + Message m = findLatestIncomingMessage(); + CharSequence title = mConversationTitle != null + ? mConversationTitle + : (m == null) ? null : m.mSender; + CharSequence text = (m == null) + ? null + : mConversationTitle != null ? makeMessageLine(m) : m.mText; + + return mBuilder.applyStandardTemplate(mBuilder.getBaseLayoutResource(), + false /* hasProgress */, + title, + text); + } + + private Message findLatestIncomingMessage() { + for (int i = mMessages.size() - 1; i >= 0; i--) { + Message m = mMessages.get(i); + // Incoming messages have a non-empty sender. + if (!TextUtils.isEmpty(m.mSender)) { + return m; + } + } + return null; + } + + /** + * @hide + */ + @Override public RemoteViews makeBigContentView() { - // TODO handset to write implementation - RemoteViews contentView = getStandardView(mBuilder.getBigTextLayoutResource()); + CharSequence title = !TextUtils.isEmpty(super.mBigContentTitle) + ? super.mBigContentTitle + : mConversationTitle; + boolean hasTitle = !TextUtils.isEmpty(title); + + RemoteViews contentView = mBuilder.applyStandardTemplate( + mBuilder.getMessagingLayoutResource(), + false /* hasProgress */, + title, + null /* text */); + + int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3, + R.id.inbox_text4, R.id.inbox_text5, R.id.inbox_text6}; + + // Make sure all rows are gone in case we reuse a view. + for (int rowId : rowIds) { + contentView.setViewVisibility(rowId, View.GONE); + } + + int i=0; + int titlePadding = mBuilder.mContext.getResources().getDimensionPixelSize( + R.dimen.notification_messaging_spacing); + contentView.setViewLayoutMarginBottom(R.id.line1, hasTitle ? titlePadding : 0); + contentView.setInt(R.id.notification_messaging, "setNumIndentLines", + mBuilder.mN.mLargeIcon == null ? 0 : (hasTitle ? 1 : 2)); + + int firstMessage = Math.max(0, mMessages.size() - rowIds.length); + while (firstMessage + i < mMessages.size() && i < rowIds.length) { + Message m = mMessages.get(firstMessage + i); + int rowId = rowIds[i]; + + contentView.setViewVisibility(rowId, View.VISIBLE); + contentView.setTextViewText(rowId, makeMessageLine(m)); + i++; + } return contentView; } + private CharSequence makeMessageLine(Message m) { + BidiFormatter bidi = BidiFormatter.getInstance(); + SpannableStringBuilder sb = new SpannableStringBuilder(); + if (TextUtils.isEmpty(m.mSender)) { + CharSequence replyName = mUserDisplayName == null ? "" : mUserDisplayName; + sb.append(bidi.unicodeWrap(replyName), + makeFontColorSpan(mBuilder.resolveContrastColor()), + 0 /* flags */); + } else { + sb.append(bidi.unicodeWrap(m.mSender), + makeFontColorSpan(Color.BLACK), + 0 /* flags */); + } + CharSequence text = m.mText == null ? "" : m.mText; + sb.append(" ").append(bidi.unicodeWrap(text)); + return sb; + } + + private static TextAppearanceSpan makeFontColorSpan(int color) { + return new TextAppearanceSpan(null, 0, 0, + ColorStateList.valueOf(color), null); + } + public static final class Message implements Parcelable { private final CharSequence mText; diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 6d2cea6b5c3c..a9b7f4e97e7c 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -1856,6 +1856,7 @@ public class RemoteViews implements Parcelable, Filter { public static final int LAYOUT_MARGIN_END = 1; /** Set width */ public static final int LAYOUT_WIDTH = 2; + public static final int LAYOUT_MARGIN_BOTTOM = 3; /** * @param viewId ID of the view alter @@ -1898,6 +1899,12 @@ public class RemoteViews implements Parcelable, Filter { target.setLayoutParams(layoutParams); } break; + case LAYOUT_MARGIN_BOTTOM: + if (layoutParams instanceof ViewGroup.MarginLayoutParams) { + ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = value; + target.setLayoutParams(layoutParams); + } + break; case LAYOUT_WIDTH: layoutParams.width = value; target.setLayoutParams(layoutParams); @@ -2870,6 +2877,16 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Equivalent to setting {@link android.view.ViewGroup.MarginLayoutParams#bottomMargin}. + * + * @hide + */ + public void setViewLayoutMarginBottom(int viewId, int bottomMargin) { + addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_MARGIN_BOTTOM, + bottomMargin)); + } + + /** * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width}. * @hide */ diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java index 78c5e34ba108..e2d8ffc4d9b1 100644 --- a/core/java/com/android/internal/widget/ImageFloatingTextView.java +++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java @@ -16,13 +16,18 @@ package com.android.internal.widget; +import com.android.internal.R; + import android.annotation.Nullable; import android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; import android.text.BoringLayout; import android.text.Layout; import android.text.StaticLayout; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.TypedValue; import android.view.RemotableViewMethod; import android.widget.RemoteViews; import android.widget.TextView; @@ -35,7 +40,8 @@ import android.widget.TextView; @RemoteViews.RemoteView public class ImageFloatingTextView extends TextView { - private boolean mHasImage; + /** Number of lines from the top to indent */ + private int mIndentLines; public ImageFloatingTextView(Context context) { this(context, null); @@ -69,10 +75,16 @@ public class ImageFloatingTextView extends TextView { .setEllipsizedWidth(ellipsisWidth) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); - // we set the endmargin on the first 2 lines. this works just in our case but that's - // sufficient for now. - int endMargin = (int) (getResources().getDisplayMetrics().density * 52); - int[] margins = mHasImage ? new int[] {endMargin, endMargin, 0} : null; + // we set the endmargin on the requested number of lines. + int endMargin = getContext().getResources().getDimensionPixelSize( + R.dimen.notification_content_picture_margin); + int[] margins = null; + if (mIndentLines > 0) { + margins = new int[mIndentLines + 1]; + for (int i = 0; i < mIndentLines; i++) { + margins[i] = endMargin; + } + } if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) { builder.setIndents(margins, null); } else { @@ -84,8 +96,22 @@ public class ImageFloatingTextView extends TextView { @RemotableViewMethod public void setHasImage(boolean hasImage) { - mHasImage = hasImage; + mIndentLines = hasImage ? 2 : 0; // The new layout will be automatically created when the text is // set again by the notification. } + + /** + * @param lines the number of lines at the top that should be indented by indentEnd + * @return whether a change was made + */ + public boolean setNumIndentLines(int lines) { + if (mIndentLines != lines) { + mIndentLines = lines; + // Invalidate layout. + setHint(getHint()); + return true; + } + return false; + } } diff --git a/core/java/com/android/internal/widget/MessagingLinearLayout.java b/core/java/com/android/internal/widget/MessagingLinearLayout.java new file mode 100644 index 000000000000..dc7b7f5b9646 --- /dev/null +++ b/core/java/com/android/internal/widget/MessagingLinearLayout.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2016 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.internal.widget; + +import com.android.internal.R; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.RemotableViewMethod; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RemoteViews; + +/** + * A custom-built layout for the Notification.MessagingStyle. + * + * Evicts children until they all fit. + */ +@RemoteViews.RemoteView +public class MessagingLinearLayout extends ViewGroup { + + /** + * Spacing to be applied between views. + */ + private int mSpacing; + + /** + * The maximum height allowed. + */ + private int mMaxHeight; + + private int mIndentLines; + + public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.MessagingLinearLayout, 0, + 0); + + final int N = a.getIndexCount(); + for (int i = 0; i < N; i++) { + int attr = a.getIndex(i); + switch (attr) { + case R.styleable.MessagingLinearLayout_maxHeight: + mMaxHeight = a.getDimensionPixelSize(i, 0); + break; + case R.styleable.MessagingLinearLayout_spacing: + mSpacing = a.getDimensionPixelSize(i, 0); + break; + } + } + + if (mMaxHeight <= 0) { + throw new IllegalStateException( + "MessagingLinearLayout: Must specify positive maxHeight"); + } + + a.recycle(); + } + + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // This is essentially a bottom-up linear layout that only adds children that fit entirely + // up to a maximum height. + + switch (MeasureSpec.getMode(heightMeasureSpec)) { + case MeasureSpec.AT_MOST: + heightMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.min(mMaxHeight, MeasureSpec.getSize(heightMeasureSpec)), + MeasureSpec.AT_MOST); + break; + case MeasureSpec.UNSPECIFIED: + heightMeasureSpec = MeasureSpec.makeMeasureSpec( + mMaxHeight, + MeasureSpec.AT_MOST); + break; + case MeasureSpec.EXACTLY: + break; + } + final int targetHeight = MeasureSpec.getSize(heightMeasureSpec); + final int count = getChildCount(); + + for (int i = 0; i < count; ++i) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.hide = true; + } + + int totalHeight = mPaddingTop + mPaddingBottom; + boolean first = true; + + // Starting from the bottom: we measure every view as if it were the only one. If it still + // fits, we take it, otherwise we stop there. + for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) { + if (getChildAt(i).getVisibility() == GONE) { + continue; + } + final View child = getChildAt(i); + LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + + if (child instanceof ImageFloatingTextView) { + // Pretend we need the image padding for all views, we don't know which + // one will end up needing to do this (might end up not using all the space, + // but calculating this exactly would be more expensive). + ((ImageFloatingTextView) child).setNumIndentLines(mIndentLines); + } + + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + + final int childHeight = child.getMeasuredHeight(); + int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin + + lp.bottomMargin + (first ? 0 : mSpacing)); + first = false; + + if (newHeight <= targetHeight) { + totalHeight = newHeight; + lp.hide = false; + } else { + break; + } + } + + // Now that we know which views to take, fix up the indents and see what width we get. + int measuredWidth = mPaddingLeft + mPaddingRight; + int imageLines = mIndentLines; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (child.getVisibility() == GONE || lp.hide) { + continue; + } + + if (child instanceof ImageFloatingTextView) { + ImageFloatingTextView textChild = (ImageFloatingTextView) child; + if (imageLines == 2 && textChild.getLineCount() > 2) { + // HACK: If we need indent for two lines, and they're coming from the same + // view, we need extra spacing to compensate for the lack of margins, + // so add an extra line of indent. + imageLines = 3; + } + boolean changed = textChild.setNumIndentLines(Math.max(0, imageLines)); + if (changed) { + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + } + imageLines -= textChild.getLineCount(); + } + + measuredWidth = Math.max(measuredWidth, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + + mPaddingLeft + mPaddingRight); + } + + + setMeasuredDimension( + resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), + widthMeasureSpec), + resolveSize(Math.max(getSuggestedMinimumHeight(), totalHeight), + heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int paddingLeft = mPaddingLeft; + + int childTop; + + // Where right end of child should go + final int width = right - left; + final int childRight = width - mPaddingRight; + + final int layoutDirection = getLayoutDirection(); + final int count = getChildCount(); + + childTop = mPaddingTop; + + boolean first = true; + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (child.getVisibility() == GONE || lp.hide) { + continue; + } + + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + int childLeft; + if (layoutDirection == LAYOUT_DIRECTION_RTL) { + childLeft = childRight - childWidth - lp.rightMargin; + } else { + childLeft = paddingLeft + lp.leftMargin; + } + + if (!first) { + childTop += mSpacing; + } + + childTop += lp.topMargin; + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + + childTop += childHeight + lp.bottomMargin; + + first = false; + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.hide) { + return true; + } + return super.drawChild(canvas, child, drawingTime); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(mContext, attrs); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + LayoutParams copy = new LayoutParams(lp.width, lp.height); + if (lp instanceof MarginLayoutParams) { + copy.copyMarginsFrom((MarginLayoutParams) lp); + } + return copy; + } + + @RemotableViewMethod + /** + * Sets how many lines should be indented to avoid a floating image. + */ + public void setNumIndentLines(int numberLines) { + mIndentLines = numberLines; + } + + public static class LayoutParams extends MarginLayoutParams { + + boolean hide = false; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + } +} |
