/* * Copyright (C) 2015 AChep@xda <artemchep@gmail.com> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ package com.achep.acdisplay.ui.widgets.notification; import android.content.Context; import android.graphics.ColorFilter; import android.graphics.ColorMatrixColorFilter; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.RemoteInput; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.transition.TransitionManager; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; import com.achep.acdisplay.R; import com.achep.acdisplay.notifications.Action; import com.achep.acdisplay.notifications.NotificationUtils; import com.achep.acdisplay.notifications.OpenNotification; import com.achep.base.Device; import com.achep.base.tests.Check; import java.util.HashMap; /** * @author Artem Chepurnoy * @since 3.1 */ public class NotificationActions extends LinearLayout { public interface Callback { void onRiiStateChanged(@NonNull NotificationActions na, boolean shown); /** * Called on action's button click. */ void onActionClick(@NonNull NotificationActions na, @NonNull View view, @NonNull Action action); /** * Called on action's button click. * * @param remoteInput the chosen {@link android.support.v4.app.RemoteInput} to reply to * @param text the text of the quick reply */ void onActionClick(@NonNull NotificationActions na, @NonNull View view, @NonNull Action action, @NonNull RemoteInput remoteInput, @NonNull CharSequence text); } /** * Disables the {@link #mView current action view} if the text is * {@link android.text.TextUtils#isEmpty(CharSequence) empty}. */ protected final Textable.OnTextChangedListener mOnTextChangedListener = new Textable.OnTextChangedListener() { @Override public void onTextChanged(@Nullable CharSequence text) { assert mView != null; mView.setEnabled(!TextUtils.isEmpty(text)); } }; private final HashMap<Action, RemoteInput> mRemoteInputsMap = new HashMap<>(); private final HashMap<View, Action> mActionsMap = new HashMap<>(); private final OnClickListener mActionsOnClick = new OnClickListener() { @Override public void onClick(View v) { Action action = mActionsMap.get(v); assert action != null; onActionClick(v, action); } }; private final OnLongClickListener mActionsOnLongClick = new OnLongClickListener() { @Override public boolean onLongClick(View v) { sendAction(v, mActionsMap.get(v)); hideRii(); return true; } }; /** * You know what is it for. */ @Nullable private Callback mCallback; @Nullable private RemoteInput mRemoteInput; @Nullable private Textable mTextable; @Nullable private View mView; private LinearLayout.LayoutParams mLayoutParams; private Typeface mTypeface; public NotificationActions(Context context, AttributeSet attrs) { super(context, attrs); } public void setCallback(@Nullable Callback callback) { mCallback = callback; } protected void onActionClick(@NonNull View view, @NonNull Action action) { if (isRiiShowing()) { if (mView != view) { // Ignore this click. This may happen because of // the animation delays. return; } // Send the callback with performed remote input. assert mRemoteInput != null; assert mTextable != null; CharSequence text = mTextable.getText(); Check.getInstance().isFalse(TextUtils.isEmpty(text)); assert text != null; sendActionWithRemoteInput(view, action, mRemoteInput, text); hideRii(); } else if ((mRemoteInput = mRemoteInputsMap.get(action)) != null) { // Initialize and show the remote input graphic // user interface. mView = view; mTextable = onCreateTextable(mRemoteInput); mOnTextChangedListener.onTextChanged(mTextable.getText()); if (Device.hasKitKatApi() && isLaidOut()) { TransitionManager.beginDelayedTransition(this); } mLayoutParams = (LayoutParams) mView.getLayoutParams(); LayoutParams lp = new LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); mView.setLayoutParams(lp); // Hide all other actions for (int i = getChildCount() - 1; i >= 0; i--) { View v = getChildAt(i); if (v != mView) v.setVisibility(GONE); } // Add the textable view addView(mTextable.getView(), 0); mTextable.getView().requestFocus(); if (mCallback != null) mCallback.onRiiStateChanged(this, true); } else { sendAction(view, action); } } private void sendAction(@NonNull View view, @NonNull Action action) { if (mCallback != null) mCallback.onActionClick(this, view, action); } private void sendActionWithRemoteInput(@NonNull View view, @NonNull Action action, @NonNull RemoteInput remoteInput, @NonNull CharSequence text) { if (mCallback != null) mCallback.onActionClick(this, view, action, remoteInput, text); } /** * Returns the appropriate {@link NotificationActions.Textable} for this * {@link android.support.v4.app.RemoteInput remote input}. * * @see android.support.v4.app.RemoteInput#getAllowFreeFormInput() */ @NonNull protected Textable onCreateTextable(@NonNull RemoteInput remoteInput) { return remoteInput.getAllowFreeFormInput() ? new TextableFreeForm(this, remoteInput, mOnTextChangedListener) : new TextableRestrictedForm(this, remoteInput, mOnTextChangedListener); } public void hideRii() { Check.getInstance().isInMainThread(); if (!isRiiShowing()) return; assert mTextable != null; assert mView != null; removeView(mTextable.getView()); mView.setLayoutParams(mLayoutParams); // Pop-up all other actions back. for (int i = getChildCount() - 1; i >= 0; i--) { View v = getChildAt(i); if (v != mView) v.setVisibility(VISIBLE); } mView = null; mTextable = null; mRemoteInput = null; mLayoutParams = null; if (mCallback != null) mCallback.onRiiStateChanged(this, false); } public boolean isRiiShowing() { return mRemoteInput != null; } /** * Sets new actions. * * @param notification the host notification * @param actions the actions to set */ public void setActions(@Nullable OpenNotification notification, @Nullable Action[] actions) { Check.getInstance().isInMainThread(); mRemoteInputsMap.clear(); mActionsMap.clear(); hideRii(); if (actions == null) { // Free actions' container. removeAllViews(); return; } else { assert notification != null; } int count = actions.length; View[] views = new View[count]; // Find available views. int childCount = getChildCount(); int a = Math.min(childCount, count); for (int i = 0; i < a; i++) { views[i] = getChildAt(i); } // Remove redundant views. for (int i = childCount - 1; i >= count; i--) { removeViewAt(i); } LayoutInflater inflater = null; for (int i = 0; i < count; i++) { final Action action = actions[i]; View root = views[i]; if (root == null) { // Initialize layout inflater only when we really need it. if (inflater == null) { inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); assert inflater != null; } root = inflater.inflate(getActionLayoutResource(), this, false); root = onCreateActionView(root); // We need to keep all IDs unique to make // TransitionManager.beginDelayedTransition(viewGroup, null) // work correctly! root.setId(getChildCount() + 1); addView(root); } mActionsMap.put(root, action); int style = Typeface.NORMAL; root.setOnLongClickListener(null); if (action.intent != null) { root.setEnabled(true); root.setOnClickListener(mActionsOnClick); RemoteInput remoteInput = getRemoteInput(action); if (remoteInput != null) { mRemoteInputsMap.put(action, remoteInput); root.setOnLongClickListener(mActionsOnLongClick); // Highlight the action style = Typeface.ITALIC; } } else { root.setEnabled(false); root.setOnClickListener(null); } // Get message view and apply the content. TextView textView = root instanceof TextView ? (TextView) root : (TextView) root.findViewById(android.R.id.title); textView.setText(action.title); if (mTypeface == null) mTypeface = textView.getTypeface(); textView.setTypeface(mTypeface, style); Drawable icon = NotificationUtils.getDrawable(getContext(), notification, action.icon); if (icon != null) icon = onCreateActionIcon(icon); if (Device.hasJellyBeanMR1Api()) { textView.setCompoundDrawablesRelative(icon, null, null, null); } else { textView.setCompoundDrawables(icon, null, null, null); } } } @NonNull protected View onCreateActionView(@NonNull View view) { return view; } @Nullable protected Drawable onCreateActionIcon(@NonNull Drawable icon) { int size = getResources().getDimensionPixelSize(R.dimen.notification_action_icon_size); icon = icon.mutate(); icon.setBounds(0, 0, size, size); // The matrix is stored in a single array, and its treated as follows: // [ a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t ] // When applied to a color [r, g, b, a], the resulting color is computed as (after clamping) // R' = a*R + b*G + c*B + d*A + e; // G' = f*R + g*G + h*B + i*A + j; // B' = k*R + l*G + m*B + n*A + o; // A' = p*R + q*G + r*B + s*A + t; ColorFilter colorFilter = new ColorMatrixColorFilter(new float[]{ 0, 0, 0, 0, 255, // Red 0, 0, 0, 0, 255, // Green 0, 0, 0, 0, 255, // Blue 0, 0, 0, 1, 0 // Alpha }); icon.setColorFilter(colorFilter); // force white color return icon; } @Nullable // FIXME: Which RemoteInput should I use? protected RemoteInput getRemoteInput(@NonNull Action action) { return null; /* if (action.remoteInputs == null || action.remoteInputs.length == 0) return null; for (RemoteInput ri : action.remoteInputs) { if (ri.getAllowFreeFormInput()) { return ri; } } return null; */ } @LayoutRes protected int getActionLayoutResource() { return R.layout.notification_action; } //-- TEXTABLE ------------------------------------------------------------- /** * Base class for the {@link android.support.v4.app.RemoteInput} view fields. For example: * the UI should provide the dropdown only if the * {@link android.support.v4.app.RemoteInput#getAllowFreeFormInput()} if {@code false}, * free text form otherwise. * * @author Artem Chepurnoy * @see android.support.v4.app.RemoteInput * @since 3.1 */ private static abstract class Textable { public interface OnTextChangedListener { /** * Called on {@link #getText()} text has changed. */ void onTextChanged(@Nullable CharSequence text); } @NonNull protected final Context mContext; @NonNull protected final RemoteInput mRemoteInput; @NonNull protected final NotificationActions mContainer; @NonNull protected final OnTextChangedListener mListener; public Textable(@NonNull NotificationActions container, @NonNull RemoteInput remoteInput, @NonNull OnTextChangedListener listener) { mContainer = container; mRemoteInput = remoteInput; mListener = listener; mContext = container.getContext(); } /** * @return the view of this {@code Textable}. */ @NonNull public abstract View getView(); /** * @return the text the {@code Textable} is displaying. */ @Nullable public abstract CharSequence getText(); /** * Inflates a new view hierarchy from the specified xml resource. The view's root * is the {@link #mContainer}. * * @return the root View of the inflated hierarchy. */ @NonNull protected final View inflate(@LayoutRes int layoutRes) { LayoutInflater inflater = (LayoutInflater) mContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); return inflater.inflate(layoutRes, mContainer, false); } } /** * @author Artem Chepurnoy * @since 3.1 */ protected static class TextableFreeForm extends Textable { private EditText mEditText; private final TextWatcher mTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { /* unused */ } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { mListener.onTextChanged(s); } @Override public void afterTextChanged(Editable s) { /* unused */ } }; public TextableFreeForm(@NonNull NotificationActions container, @NonNull RemoteInput remoteInput, @NonNull OnTextChangedListener listener) { super(container, remoteInput, listener); mEditText = onCreateEditText(); mEditText.setHint(remoteInput.getLabel()); mEditText.addTextChangedListener(mTextWatcher); } /** * {@inheritDoc} */ @NonNull @Override public View getView() { return mEditText; } /** * {@inheritDoc} */ @Nullable @Override public CharSequence getText() { return mEditText.getText(); } @NonNull protected EditText onCreateEditText() { return (EditText) inflate(R.layout.notification_reply_free_form); } } /** * @author Artem Chepurnoy * @since 3.1 */ protected static class TextableRestrictedForm extends Textable { private final Spinner mSpinner; public TextableRestrictedForm(@NonNull NotificationActions container, @NonNull RemoteInput remoteInput, @NonNull OnTextChangedListener listener) { super(container, remoteInput, listener); ArrayAdapter<CharSequence> adapter = new ArrayAdapter<>( mContext, android.R.layout.simple_spinner_dropdown_item, remoteInput.getChoices()); mSpinner = onCreateSpinner(); mSpinner.setAdapter(adapter); } /** * {@inheritDoc} */ @NonNull @Override public View getView() { return mSpinner; } /** * {@inheritDoc} */ @Nullable @Override public CharSequence getText() { int pos = mSpinner.getSelectedItemPosition(); return mRemoteInput.getChoices()[pos]; } @NonNull protected Spinner onCreateSpinner() { return (Spinner) inflate(R.layout.notification_reply_restricted_form); } } }