/* * Copyright (C) 2015 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 tk.wasdennnoch.androidn_ify.systemui.notifications; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.RemotableViewMethod; import android.view.TouchDelegate; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Chronometer; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RemoteViews; import android.widget.TextView; import java.util.ArrayList; import tk.wasdennnoch.androidn_ify.R; import tk.wasdennnoch.androidn_ify.utils.ResourceUtils; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; /** * A header of a notification view */ @SuppressWarnings({"deprecation", "SameParameterValue", "WeakerAccess"}) @RemoteViews.RemoteView public class NotificationHeaderView extends ViewGroup { public static final int NO_COLOR = -1; private final int mChildMinWidth; private final int mContentEndMargin; private View mAppName; private View mHeaderText; private OnClickListener mExpandClickListener; private final HeaderTouchListener mTouchListener = new HeaderTouchListener(); private ImageView mExpandButton; private View mIcon; private View mProfileBadge; private View mInfo; private int mIconColor; private int mOriginalNotificationColor; private boolean mExpanded; private boolean mShowWorkBadgeAtEnd; private Drawable mBackground; private final int mHeaderBackgroundHeight; private final Resources mModuleRes; @SuppressWarnings("deprecation") public static NotificationHeaderView newHeader(Context context) { ResourceUtils res = ResourceUtils.getInstance(context); FrameLayout.LayoutParams headerLp = new FrameLayout.LayoutParams(WRAP_CONTENT, res.getDimensionPixelSize(R.dimen.notification_header_height)); NotificationHeaderView headerView = new NotificationHeaderView(context); headerView.setClipChildren(false); headerView.setId(R.id.notification_header); headerView.setPadding( res.getDimensionPixelSize(R.dimen.notification_content_margin_start), res.getDimensionPixelSize(R.dimen.notification_header_padding_top), res.getDimensionPixelSize(R.dimen.notification_header_padding_end), res.getDimensionPixelSize(R.dimen.notification_header_padding_bottom)); headerView.setLayoutParams(headerLp); int iconSize = res.getDimensionPixelSize(R.dimen.notification_icon_size); int iconMarginEnd = res.getDimensionPixelSize(R.dimen.notification_icon_margin_end); ViewGroup.MarginLayoutParams iconLp = new MarginLayoutParams(iconSize, iconSize); iconLp.setMarginEnd(iconMarginEnd); ImageView icon = new ImageView(context); icon.setId(R.id.icon); icon.setLayoutParams(iconLp); headerView.addView(icon); MarginLayoutParams appNameLp = new MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT); appNameLp.setMarginStart(res.getDimensionPixelSize(R.dimen.notification_app_name_margin_start)); appNameLp.setMarginEnd(res.getDimensionPixelSize(R.dimen.notification_app_name_margin_end)); TextView appName = new TextView(context); appName.setId(R.id.app_name_text); appName.setLayoutParams(appNameLp); appName.setSingleLine(true); appName.setTextAppearance(context, android.R.style.TextAppearance_Material_Notification_Info); headerView.addView(appName); TextView headerTextDivider = newDivider(context, res); headerTextDivider.setId(R.id.header_text_divider); headerView.addView(headerTextDivider); int textMargin = res.getDimensionPixelSize(R.dimen.notification_header_text_margin); MarginLayoutParams headerTextLp = new MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT); headerTextLp.setMargins(textMargin, 0, textMargin, 0); TextView headerText = new TextView(context); headerText.setId(R.id.header_text); headerText.setLayoutParams(headerTextLp); headerText.setSingleLine(true); headerText.setVisibility(GONE); headerText.setTextAppearance(context, android.R.style.TextAppearance_Material_Notification_Info); headerView.addView(headerText); TextView timeDivider = newDivider(context, res); timeDivider.setId(R.id.time_divider); headerView.addView(timeDivider); MarginLayoutParams timeLp = new MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT); timeLp.setMargins(textMargin, 0, textMargin, 0); RelativeDateTimeView time = new RelativeDateTimeView(context); time.setId(R.id.time); time.setLayoutParams(timeLp); time.setSingleLine(true); time.setVisibility(GONE); time.setTextAppearance(context, android.R.style.TextAppearance_Material_Notification_Time); time.setGravity(TEXT_ALIGNMENT_CENTER); headerView.addView(time); MarginLayoutParams chronometerLp = new MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT); chronometerLp.setMargins(textMargin, 0, textMargin, 0); Chronometer chronometer = new Chronometer(context); chronometer.setId(R.id.chronometer); chronometer.setLayoutParams(chronometerLp); chronometer.setSingleLine(true); chronometer.setVisibility(GONE); chronometer.setTextAppearance(context, android.R.style.TextAppearance_Material_Notification_Time); headerView.addView(chronometer); int expandButtonPaddingTop = res.getDimensionPixelSize(R.dimen.notification_header_expand_button_padding_top); MarginLayoutParams expandButtonLp = new MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT); ImageView expandButton = new ImageView(context); expandButton.setId(R.id.expand_button); expandButton.setLayoutParams(expandButtonLp); expandButton.setVisibility(GONE); expandButton.setPadding(0, expandButtonPaddingTop, 0, 0); headerView.addView(expandButton); headerView.initViews(); /* LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); ViewGroup template = (ViewGroup) inflater.inflate(res.getLayout(R.layout.notification_template_header), null, false); int childCount = template.getChildCount(); for (int i = 0; i < childCount; i++) { View child = template.getChildAt(0); template.removeView(child); headerView.addView(child); } String dividerSymbol = res.getString(R.string.notification_header_divider_symbol); ((TextView) headerView.findViewById(R.id.header_text_divider)).setText(dividerSymbol); ((TextView) headerView.findViewById(R.id.time_divider)).setText(dividerSymbol); */ return headerView; } @SuppressWarnings("deprecation") private static TextView newDivider(Context context, ResourceUtils res) { int dividerMargin = res.getDimensionPixelSize(R.dimen.notification_header_text_margin); String dividerSymbol = res.getString(R.string.notification_header_divider_symbol); MarginLayoutParams dividerLp = new MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT); dividerLp.setMargins(dividerMargin, 0, dividerMargin, 0); TextView divider = new TextView(context); divider.setLayoutParams(dividerLp); divider.setVisibility(GONE); divider.setTextAppearance(context, android.R.style.TextAppearance_Material_Notification_Info); divider.setText(dividerSymbol); return divider; } final ViewOutlineProvider mProvider = new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { if (mBackground != null) { outline.setRect(0, 0, getWidth(), mHeaderBackgroundHeight); outline.setAlpha(1f); } } }; final AccessibilityDelegate mExpandDelegate = new AccessibilityDelegate() { @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (super.performAccessibilityAction(host, action, args)) { return true; } if (action == AccessibilityNodeInfo.ACTION_COLLAPSE || action == AccessibilityNodeInfo.ACTION_EXPAND) { mExpandClickListener.onClick(mExpandButton); return true; } return false; } @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); // Avoid that the button description is also spoken info.setClassName(getClass().getName()); if (mExpanded) { info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); } else { info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); } } }; public NotificationHeaderView(Context context) { this(context, null); } public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mModuleRes = ResourceUtils.getInstance(context).getResources(); mChildMinWidth = mModuleRes.getDimensionPixelSize( R.dimen.notification_header_shrink_min_width); mContentEndMargin = mModuleRes.getDimensionPixelSize( R.dimen.notification_content_margin_end); mHeaderBackgroundHeight = mModuleRes.getDimensionPixelSize( R.dimen.notification_header_background_height); } private void initViews() { mAppName = findViewById(R.id.app_name_text); mHeaderText = findViewById(R.id.header_text); mExpandButton = (ImageView) findViewById(R.id.expand_button); if (mExpandButton != null) { mExpandButton.setAccessibilityDelegate(mExpandDelegate); } mIcon = findViewById(com.android.internal.R.id.icon); mProfileBadge = findViewById(R.id.profile_badge); post(new Runnable() { @Override public void run() { Rect r = new Rect(); r.top = 0; r.bottom = (int) (32 * getResources().getDisplayMetrics().density); r.left = 0; r.right = getWidth(); TouchDelegate touchDelegate = new TouchDelegate(r, mExpandButton); setTouchDelegate(touchDelegate); } }); updateTouchListener(); } @Override protected void onFinishInflate() { super.onFinishInflate(); initViews(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int givenWidth = MeasureSpec.getSize(widthMeasureSpec); final int givenHeight = MeasureSpec.getSize(heightMeasureSpec); int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth, MeasureSpec.AT_MOST); int wrapContentHeightSpec = MeasureSpec.makeMeasureSpec(givenHeight, MeasureSpec.AT_MOST); int totalWidth = getPaddingStart() + getPaddingEnd(); for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { // We'll give it the rest of the space in the end continue; } final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec, lp.leftMargin + lp.rightMargin, lp.width); int childHeightSpec = getChildMeasureSpec(wrapContentHeightSpec, lp.topMargin + lp.bottomMargin, lp.height); child.measure(childWidthSpec, childHeightSpec); totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth(); } if (totalWidth > givenWidth) { int overFlow = totalWidth - givenWidth; // We are overflowing, lets shrink the app name first final int appWidth = mAppName.getMeasuredWidth(); if (overFlow > 0 && mAppName.getVisibility() != GONE && appWidth > mChildMinWidth) { int newSize = appWidth - Math.min(appWidth - mChildMinWidth, overFlow); int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST); mAppName.measure(childWidthSpec, wrapContentHeightSpec); ((TextView) mAppName).setEllipsize(TextUtils.TruncateAt.END); overFlow -= appWidth - newSize; } // still overflowing, finaly we shrink the header text if (overFlow > 0 && mHeaderText.getVisibility() != GONE) { // we're still too big final int textWidth = mHeaderText.getMeasuredWidth(); int newSize = Math.max(0, textWidth - overFlow); int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST); mHeaderText.measure(childWidthSpec, wrapContentHeightSpec); ((TextView) mHeaderText).setEllipsize(TextUtils.TruncateAt.END); } } setMeasuredDimension(givenWidth, givenHeight); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int left = getPaddingStart(); int childCount = getChildCount(); int ownHeight = getHeight() - getPaddingTop() - getPaddingBottom(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } int childHeight = child.getMeasuredHeight(); MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); left += params.getMarginStart(); int right = left + child.getMeasuredWidth(); int top = (int) (getPaddingTop() + (ownHeight - childHeight) / 2.0f); int bottom = top + childHeight; int layoutLeft = left; int layoutRight = right; if (child == mProfileBadge) { int paddingEnd = getPaddingEnd(); if (mShowWorkBadgeAtEnd) { paddingEnd = mContentEndMargin; } layoutRight = getWidth() - paddingEnd; layoutLeft = layoutRight - child.getMeasuredWidth(); } if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) { int ltrLeft = layoutLeft; layoutLeft = getWidth() - layoutRight; layoutRight = getWidth() - ltrLeft; } child.layout(layoutLeft, top, layoutRight, bottom); left = right + params.getMarginEnd(); } updateTouchListener(); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new ViewGroup.MarginLayoutParams(getContext(), attrs); } /** * Set a {@link Drawable} to be displayed as a background on the header. */ public void setHeaderBackgroundDrawable(Drawable drawable) { if (drawable != null) { setWillNotDraw(false); mBackground = drawable; mBackground.setCallback(this); setOutlineProvider(mProvider); } else { setWillNotDraw(true); mBackground = null; setOutlineProvider(null); } invalidate(); } @Override protected void onDraw(Canvas canvas) { if (mBackground != null) { mBackground.setBounds(0, 0, getWidth(), mHeaderBackgroundHeight); mBackground.draw(canvas); } } @Override protected boolean verifyDrawable(@NonNull Drawable who) { return super.verifyDrawable(who) || who == mBackground; } @SuppressLint("MissingSuperCall") @Override protected void drawableStateChanged() { if (mBackground != null && mBackground.isStateful()) { mBackground.setState(getDrawableState()); } } private void updateTouchListener() { if (mExpandClickListener != null) { mTouchListener.bindTouchRects(); } } @Override public void setOnClickListener(@Nullable OnClickListener l) { mExpandClickListener = l; setOnTouchListener(mExpandClickListener != null ? mTouchListener : null); mExpandButton.setOnClickListener(mExpandClickListener); updateTouchListener(); } @RemotableViewMethod public void setOriginalIconColor(int color) { mIconColor = color; } public int getOriginalIconColor() { return mIconColor; } @RemotableViewMethod public void setOriginalNotificationColor(int color) { mOriginalNotificationColor = color; } public int getOriginalNotificationColor() { return mOriginalNotificationColor; } @RemotableViewMethod public void setExpanded(boolean expanded) { mExpanded = expanded; updateExpandButton(); } private void updateExpandButton() { int drawableId; if (mExpanded) { drawableId = R.drawable.ic_collapse_notification; } else { drawableId = R.drawable.ic_expand_notification; } mExpandButton.setImageDrawable(mModuleRes.getDrawable(drawableId)); mExpandButton.setColorFilter(mIconColor); } public void setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd) { if (showWorkBadgeAtEnd != mShowWorkBadgeAtEnd) { setClipToPadding(!showWorkBadgeAtEnd); mShowWorkBadgeAtEnd = showWorkBadgeAtEnd; } } public View getWorkProfileIcon() { return mProfileBadge; } public class HeaderTouchListener implements View.OnTouchListener { private final ArrayList<Rect> mTouchRects = new ArrayList<>(); private int mTouchSlop; private boolean mTrackGesture; private float mDownX; private float mDownY; public HeaderTouchListener() { } public void bindTouchRects() { mTouchRects.clear(); if (mIcon != null) addRectAroundViewView(mIcon); addRectAroundViewView(mExpandButton); addWidthRect(); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } private void addWidthRect() { Rect r = new Rect(); r.top = 0; r.bottom = (int) (32 * getResources().getDisplayMetrics().density); r.left = 0; r.right = getWidth(); mTouchRects.add(r); } private void addRectAroundViewView(View view) { final Rect r = getRectAroundView(view); mTouchRects.add(r); } private Rect getRectAroundView(View view) { float size = 48 * getResources().getDisplayMetrics().density; final Rect r = new Rect(); if (view.getVisibility() == GONE) { view = getFirstChildNotGone(); r.left = (int) (view.getLeft() - size / 2.0f); } else { r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - size / 2.0f); } r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - size / 2.0f); r.bottom = (int) (r.top + size); r.right = (int) (r.left + size); return r; } @Override public boolean onTouch(View v, MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getActionMasked() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: mTrackGesture = false; if (isInside(x, y)) { mTrackGesture = true; return true; } break; case MotionEvent.ACTION_MOVE: if (mTrackGesture) { if (Math.abs(mDownX - x) > mTouchSlop || Math.abs(mDownY - y) > mTouchSlop) { mTrackGesture = false; } } break; case MotionEvent.ACTION_UP: if (mTrackGesture) { mExpandClickListener.onClick(NotificationHeaderView.this); } break; } return mTrackGesture; } private boolean isInside(float x, float y) { for (int i = 0; i < mTouchRects.size(); i++) { Rect r = mTouchRects.get(i); if (r.contains((int) x, (int) y)) { mDownX = x; mDownY = y; return true; } } return false; } } private View getFirstChildNotGone() { for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { return child; } } return this; } public ImageView getExpandButton() { return mExpandButton; } @Override public boolean hasOverlappingRendering() { return false; } public boolean isInTouchRect(float x, float y) { return mExpandClickListener != null && mTouchListener.isInside(x, y); } }