com.achep.acdisplay.ui.widgets.notification.NotificationActions.java Source code

Java tutorial

Introduction

Here is the source code for com.achep.acdisplay.ui.widgets.notification.NotificationActions.java

Source

/*
 * 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);
        }

    }

}