SliceActionView.java

/*
 * Copyright 2018 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 androidx.slice.widget;

import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.slice.core.SliceHints.ICON_IMAGE;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.widget.Checkable;
import android.widget.CompoundButton;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Switch;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.view.R;

/**
 * Supports displaying {@link androidx.slice.core.SliceActionImpl}s.
 * @hide
 */
@RestrictTo(LIBRARY)
public class SliceActionView extends FrameLayout implements View.OnClickListener,
        CompoundButton.OnCheckedChangeListener {
    private static final String TAG = "SliceActionView";

    private static final int[] STATE_CHECKED = {
            android.R.attr.state_checked
    };

    private SliceActionImpl mSliceAction;
    private EventInfo mEventInfo;
    private SliceView.OnSliceActionListener mObserver;

    private View mActionView;

    private int mIconSize;
    private int mImageSize;

    public SliceActionView(Context context) {
        super(context);
        Resources res = getContext().getResources();
        mIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_icon_size);
        mImageSize = res.getDimensionPixelSize(R.dimen.abc_slice_small_image_size);
    }

    /**
     * Populates the view with the provided action.
     */
    public void setAction(@NonNull SliceActionImpl action, EventInfo info,
                          SliceView.OnSliceActionListener listener, int color) {
        mSliceAction = action;
        mEventInfo = info;
        mObserver = listener;
        mActionView = null;

        if (action.isDefaultToggle()) {
            Switch switchView = new Switch(getContext());
            addView(switchView);
            switchView.setChecked(action.isChecked());
            switchView.setOnCheckedChangeListener(this);
            switchView.setMinimumHeight(mImageSize);
            switchView.setMinimumWidth(mImageSize);
            if (color != -1) {
                // TODO - find nice way to tint toggles
            }
            mActionView = switchView;

        } else if (action.getIcon() != null) {
            if (action.isToggle()) {
                ImageToggle imageToggle = new ImageToggle(getContext());
                imageToggle.setChecked(action.isChecked());
                mActionView = imageToggle;
            } else {
                mActionView = new ImageView(getContext());
            }
            addView(mActionView);

            Drawable d = mSliceAction.getIcon().loadDrawable(getContext());
            ((ImageView) mActionView).setImageDrawable(d);
            if (color != -1 && mSliceAction.getImageMode() == ICON_IMAGE && d != null) {
                // TODO - Consider allowing option for untinted custom toggles
                DrawableCompat.setTint(d, color);
            }
            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mActionView.getLayoutParams();
            lp.width = mImageSize;
            lp.height = mImageSize;
            mActionView.setLayoutParams(lp);
            int p = action.getImageMode() == ICON_IMAGE ? mIconSize / 2 : 0;
            mActionView.setPadding(p, p, p, p);
            int touchFeedbackAttr = android.R.attr.selectableItemBackground;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                touchFeedbackAttr = android.R.attr.selectableItemBackgroundBorderless;
            }
            mActionView.setBackground(SliceViewUtil.getDrawable(getContext(), touchFeedbackAttr));
            mActionView.setOnClickListener(this);
        }

        if (mActionView != null) {
            CharSequence contentDescription = action.getContentDescription() != null
                    ? action.getContentDescription()
                    : action.getTitle();
            mActionView.setContentDescription(contentDescription);
        }
    }

    /**
     * Toggles this action if it is toggleable.
     */
    public void toggle() {
        if (mActionView != null && mSliceAction != null && mSliceAction.isToggle()) {
            ((Checkable) mActionView).toggle();
        }
    }

    /**
     * @return the action represented in this view.
     */
    @Nullable
    public SliceActionImpl getAction() {
        return mSliceAction;
    }

    @Override
    public void onClick(View v) {
        if (mSliceAction == null || mActionView == null) {
            return;
        }
        sendAction();
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (mSliceAction == null || mActionView == null) {
            return;
        }
        sendAction();
    }

    private void sendAction() {
        // TODO - Show loading indicator here?
        try {
            if (mSliceAction.isToggle()) {
                // Update the intent extra state
                boolean isChecked = ((Checkable) mActionView).isChecked();
                Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
                mSliceAction.getActionItem().fireAction(getContext(), i);

                // Update event info state
                if (mEventInfo != null) {
                    mEventInfo.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
                }
            } else {
                mSliceAction.getActionItem().fireAction(null, null); 
            }
            if (mObserver != null && mEventInfo != null) {
                mObserver.onSliceAction(mEventInfo, mSliceAction.getSliceItem());
            }
        } catch (PendingIntent.CanceledException e) {
            if (mActionView instanceof Checkable) {
                mActionView.setSelected(!((Checkable) mActionView).isChecked());
            }
            Log.e(TAG, "PendingIntent for slice cannot be sent", e);
        }
    }

    /**
     * Simple class allowing a toggleable image button.
     */
    private static class ImageToggle extends ImageView implements Checkable, View.OnClickListener {
        private boolean mIsChecked;
        private View.OnClickListener mListener;

        ImageToggle(Context context) {
            super(context);
            super.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            toggle();
        }

        @Override
        public void toggle() {
            setChecked(!isChecked());
        }

        @Override
        public void setChecked(boolean checked) {
            if (mIsChecked != checked) {
                mIsChecked = checked;
                refreshDrawableState();
                if (mListener != null) {
                    mListener.onClick(this);
                }
            }
        }

        @Override
        public void setOnClickListener(View.OnClickListener listener) {
            mListener = listener;
        }

        @Override
        public boolean isChecked() {
            return mIsChecked;
        }

        @Override
        public int[] onCreateDrawableState(int extraSpace) {
            final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
            if (mIsChecked) {
                mergeDrawableStates(drawableState, STATE_CHECKED);
            }
            return drawableState;
        }
    }
}