1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.mail.ui;
18
19import android.animation.ObjectAnimator;
20import android.app.LoaderManager;
21import android.content.Context;
22import android.content.res.Resources;
23import android.os.Bundle;
24import android.support.annotation.LayoutRes;
25import android.util.AttributeSet;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.ViewGroup;
29import android.view.animation.DecelerateInterpolator;
30import android.widget.AbsListView;
31import android.widget.ImageView;
32import android.widget.LinearLayout;
33import android.widget.TextView;
34
35import com.android.mail.R;
36import com.android.mail.browse.ConversationCursor;
37import com.android.mail.providers.Folder;
38import com.android.mail.utils.LogTag;
39
40/**
41 * Base class to display tip teasers in the thread list.
42 * Supports two-line text and start/end icons.
43 */
44public abstract class ConversationTipView extends LinearLayout
45        implements ConversationSpecialItemView, SwipeableItemView, View.OnClickListener {
46    protected static final String LOG_TAG = LogTag.getLogTag();
47
48    protected Context mContext;
49    protected AnimatedAdapter mAdapter;
50
51    private int mScrollSlop;
52    private int mShrinkAnimationDuration;
53    private int mAnimatedHeight = -1;
54
55    protected View mSwipeableContent;
56    private View mContent;
57    private TextView mText;
58
59    public ConversationTipView(Context context) {
60        this(context, null);
61    }
62
63    public ConversationTipView(Context context, AttributeSet attrs) {
64        super(context, attrs);
65        mContext = context;
66
67        final Resources resources = context.getResources();
68        mScrollSlop = resources.getInteger(R.integer.swipeScrollSlop);
69        mShrinkAnimationDuration = resources.getInteger(
70                R.integer.shrink_animation_duration);
71
72        // Inflate the actual content and add it to this view
73        mContent = LayoutInflater.from(mContext).inflate(getChildLayout(), this, false);
74        addView(mContent);
75        setupViews();
76    }
77
78    @Override
79    public ViewGroup.LayoutParams getLayoutParams() {
80        ViewGroup.LayoutParams params = super.getLayoutParams();
81        if (params != null) {
82            params.width = ViewGroup.LayoutParams.MATCH_PARENT;
83            params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
84        }
85        return params;
86    }
87
88    protected @LayoutRes int getChildLayout() {
89        // Should override setupViews as well if this is overridden.
90        return R.layout.conversation_tip_view;
91    }
92
93    protected void setupViews() {
94        // If this is overridden, child classes cannot rely on setText/getStartIconAttr/etc.
95        mSwipeableContent = mContent.findViewById(R.id.conversation_tip_swipeable_content);
96        mText = (TextView) mContent.findViewById(R.id.conversation_tip_text);
97        final ImageView startImage = (ImageView) mContent.findViewById(R.id.conversation_tip_icon1);
98        final ImageView dismiss = (ImageView) mContent.findViewById(R.id.dismiss_icon);
99
100        // Bind content (text content must be bound by calling setText(..))
101        bindIcon(startImage, getStartIconAttr());
102
103        // Bind listeners
104        dismiss.setOnClickListener(this);
105        mText.setOnClickListener(getTextAreaOnClickListener());
106    }
107
108    /**
109     * Helper function to bind the additional attributes to the icon, or make the icon GONE.
110     */
111    private void bindIcon(ImageView image, ImageAttrSet attr) {
112        if (attr != null) {
113            image.setVisibility(VISIBLE);
114            image.setContentDescription(attr.contentDescription);
115            // Must override resId for the actual icon, so no need to check -1 here.
116            image.setImageResource(attr.resId);
117            if (attr.background != -1) {
118                image.setBackgroundResource(attr.background);
119            }
120        } else {
121            image.setVisibility(GONE);
122        }
123    }
124
125    @Override
126    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
127        if (mAnimatedHeight == -1) {
128            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
129        } else {
130            setMeasuredDimension(View.MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
131        }
132    }
133
134    protected ImageAttrSet getStartIconAttr() {
135        return null;
136    }
137
138    protected void setText(CharSequence text) {
139        mText.setText(text);
140    }
141
142    protected OnClickListener getTextAreaOnClickListener() {
143        return null;
144    }
145
146    @Override
147    public void onClick(View view) {
148        // Default on click for the default dismiss button
149        dismiss();
150    }
151
152    @Override
153    public void onUpdate(Folder folder, ConversationCursor cursor) {
154        // Do nothing by default
155    }
156
157    @Override
158    public void onGetView() {
159        // Do nothing by default
160    }
161
162    @Override
163    public int getPosition() {
164        // By default the tip teasers go on top of the list.
165        return 0;
166    }
167
168    @Override
169    public void setAdapter(AnimatedAdapter adapter) {
170        mAdapter = adapter;
171    }
172
173    @Override
174    public void bindFragment(LoaderManager loaderManager, Bundle savedInstanceState) {
175        // Do nothing by default
176    }
177
178    @Override
179    public void cleanup() {
180        // Do nothing by default
181    }
182
183    @Override
184    public void onConversationSelected() {
185        // Do nothing by default
186    }
187
188    @Override
189    public void onCabModeEntered() {
190        // Do nothing by default
191    }
192
193    @Override
194    public void onCabModeExited() {
195        // Do nothing by default
196    }
197
198    @Override
199    public boolean acceptsUserTaps() {
200        return true;
201    }
202
203    @Override
204    public void onConversationListVisibilityChanged(boolean visible) {
205        // Do nothing by default
206    }
207
208    @Override
209    public void saveInstanceState(Bundle outState) {
210        // Do nothing by default
211    }
212
213    @Override
214    public boolean commitLeaveBehindItem() {
215        // Tip has no leave-behind by default
216        return false;
217    }
218
219    @Override
220    public SwipeableView getSwipeableView() {
221        return SwipeableView.from(mSwipeableContent);
222    }
223
224    @Override
225    public boolean canChildBeDismissed() {
226        return true;
227    }
228
229    @Override
230    public void dismiss() {
231        startDestroyAnimation();
232    }
233
234    @Override
235    public float getMinAllowScrollDistance() {
236        return mScrollSlop;
237    }
238
239    private void startDestroyAnimation() {
240        final int start = getHeight();
241        final int end = 0;
242        mAnimatedHeight = start;
243        final ObjectAnimator heightAnimator =
244                ObjectAnimator.ofInt(this, "animatedHeight", start, end);
245        heightAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
246        heightAnimator.setDuration(mShrinkAnimationDuration);
247        heightAnimator.start();
248
249        /*
250         * Ideally, we would like to call mAdapter.notifyDataSetChanged() in a listener's
251         * onAnimationEnd(), but we are in the middle of a touch event, and this will cause all the
252         * views to get recycled, which will cause problems.
253         *
254         * Instead, we'll just leave the item in the list with a height of 0, and the next
255         * notifyDatasetChanged() will remove it from the adapter.
256         */
257    }
258
259    /**
260     * This method is used by the animator.  It is explicitly kept in proguard.flags to prevent it
261     * from being removed, inlined, or obfuscated.
262     * Edit ./vendor/unbundled/packages/apps/UnifiedGmail/proguard.flags
263     * In the future, we want to use @Keep
264     */
265    public void setAnimatedHeight(final int height) {
266        mAnimatedHeight = height;
267        requestLayout();
268    }
269
270    public static class ImageAttrSet {
271        // -1 for these resIds to not override the default value.
272        public int resId;
273        public int background;
274        public String contentDescription;
275
276        public ImageAttrSet(int resId, int background, String contentDescription) {
277            this.resId = resId;
278            this.background = background;
279            this.contentDescription = contentDescription;
280        }
281    }
282}
283