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.tv.settings.dialog.old;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.drawable.Drawable;
26import android.media.AudioManager;
27import android.net.Uri;
28import android.text.TextUtils;
29import android.util.Log;
30import android.util.TypedValue;
31import android.view.KeyEvent;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
35import android.view.WindowManager;
36import android.view.animation.DecelerateInterpolator;
37import android.view.animation.Interpolator;
38import android.widget.BaseAdapter;
39import android.widget.ImageView;
40import android.widget.TextView;
41
42import com.android.tv.settings.R;
43import com.android.tv.settings.widget.BitmapDownloader;
44import com.android.tv.settings.widget.BitmapDownloader.BitmapCallback;
45import com.android.tv.settings.widget.BitmapWorkerOptions;
46import com.android.tv.settings.widget.ScrollAdapter;
47import com.android.tv.settings.widget.ScrollAdapterBase;
48import com.android.tv.settings.widget.ScrollAdapterView;
49import com.android.tv.settings.widget.ScrollAdapterView.OnScrollListener;
50
51import java.util.ArrayList;
52import java.util.List;
53
54/**
55 * Adapter class which creates actions.
56 */
57public class ActionAdapter extends BaseAdapter implements ScrollAdapter,
58        OnScrollListener, View.OnKeyListener, View.OnClickListener {
59    private static final String TAG = "ActionAdapter";
60
61    private static final boolean DEBUG = false;
62
63    private static final int SELECT_ANIM_DURATION = 100;
64    private static final int SELECT_ANIM_DELAY = 0;
65    private static final float SELECT_ANIM_SELECTED_ALPHA = 0.2f;
66    private static final float SELECT_ANIM_UNSELECTED_ALPHA = 1.0f;
67    private static final float CHECKMARK_ANIM_UNSELECTED_ALPHA = 0.0f;
68    private static final float CHECKMARK_ANIM_SELECTED_ALPHA = 1.0f;
69
70    private static Integer sDescriptionMaxHeight = null;
71
72    // TODO: this constant is only in KLP: update when KLP has a more standard SDK.
73    private static final int FX_KEYPRESS_INVALID = 9; // AudioManager.FX_KEYPRESS_INVALID;
74
75    /**
76     * Object listening for adapter events.
77     */
78    public interface Listener {
79
80        /**
81         * Called when the user clicks on an action.
82         */
83        public void onActionClicked(Action action);
84    }
85
86    public interface OnFocusListener {
87
88        /**
89         * Called when the user focuses on an action.
90         */
91        public void onActionFocused(Action action);
92    }
93
94    /**
95     * Object listening for adapter action select/unselect events.
96     */
97    public interface OnKeyListener {
98
99        /**
100         * Called when user finish selecting an action.
101         */
102        public void onActionSelect(Action action);
103
104        /**
105         * Called when user finish unselecting an action.
106         */
107        public void onActionUnselect(Action action);
108    }
109
110
111    private final Context mContext;
112    private final float mUnselectedAlpha;
113    private final float mSelectedTitleAlpha;
114    private final float mDisabledTitleAlpha;
115    private final float mSelectedDescriptionAlpha;
116    private final float mDisabledDescriptionAlpha;
117    private final float mUnselectedDescriptionAlpha;
118    private final float mSelectedChevronAlpha;
119    private final float mDisabledChevronAlpha;
120    private final List<Action> mActions;
121    private Listener mListener;
122    private OnFocusListener mOnFocusListener;
123    private OnKeyListener mOnKeyListener;
124    private boolean mKeyPressed;
125    private ScrollAdapterView mScrollAdapterView;
126    private final int mAnimationDuration;
127    private View mSelectedView = null;
128
129    public ActionAdapter(Context context) {
130        super();
131        mContext = context;
132        final Resources res = context.getResources();
133
134        mAnimationDuration = res.getInteger(R.integer.dialog_animation_duration);
135        mUnselectedAlpha = getFloat(R.dimen.list_item_unselected_text_alpha);
136
137        mSelectedTitleAlpha = getFloat(R.dimen.list_item_selected_title_text_alpha);
138        mDisabledTitleAlpha = getFloat(R.dimen.list_item_disabled_title_text_alpha);
139
140        mSelectedDescriptionAlpha = getFloat(R.dimen.list_item_selected_description_text_alpha);
141        mUnselectedDescriptionAlpha = getFloat(R.dimen.list_item_unselected_description_text_alpha);
142        mDisabledDescriptionAlpha = getFloat(R.dimen.list_item_disabled_description_text_alpha);
143
144        mSelectedChevronAlpha = getFloat(R.dimen.list_item_selected_chevron_background_alpha);
145        mDisabledChevronAlpha = getFloat(R.dimen.list_item_disabled_chevron_background_alpha);
146
147        mActions = new ArrayList<>();
148        mKeyPressed = false;
149    }
150
151    @Override
152    public void viewRemoved(View view) {
153        // Do nothing.
154    }
155
156    @Override
157    public View getScrapView(ViewGroup parent) {
158        LayoutInflater inflater = LayoutInflater.from(mContext);
159        View view = inflater.inflate(R.layout.settings_list_item, parent, false);
160        return view;
161    }
162
163    @Override
164    public int getCount() {
165        return mActions.size();
166    }
167
168    @Override
169    public Object getItem(int position) {
170        return mActions.get(position);
171    }
172
173    @Override
174    public long getItemId(int position) {
175        return position;
176    }
177
178    @Override
179    public boolean hasStableIds() {
180        return true;
181    }
182
183    @Override
184    public View getView(int position, View convertView, ViewGroup parent) {
185        if (convertView == null) {
186            convertView = getScrapView(parent);
187        }
188        Action action = mActions.get(position);
189        TextView title = (TextView) convertView.findViewById(R.id.action_title);
190        TextView description = (TextView) convertView.findViewById(R.id.action_description);
191        description.setText(action.getDescription());
192        description.setVisibility(
193                TextUtils.isEmpty(action.getDescription()) ? View.GONE : View.VISIBLE);
194        title.setText(action.getTitle());
195        ImageView checkmarkView = (ImageView) convertView.findViewById(R.id.action_checkmark);
196        checkmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE);
197
198        ImageView indicatorView = (ImageView) convertView.findViewById(R.id.action_icon);
199        setIndicator(indicatorView, action);
200
201        ImageView chevronView = (ImageView) convertView.findViewById(R.id.action_next_chevron);
202        chevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.GONE);
203
204        View chevronBackgroundView = convertView.findViewById(R.id.action_next_chevron_background);
205        chevronBackgroundView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE);
206
207        final Resources res = convertView.getContext().getResources();
208        if (action.hasMultilineDescription()) {
209            title.setMaxLines(res.getInteger(R.integer.action_title_max_lines));
210            description.setMaxHeight(
211                    getDescriptionMaxHeight(convertView.getContext(), title, description));
212        } else {
213            title.setMaxLines(res.getInteger(R.integer.action_title_min_lines));
214            description.setMaxLines(
215                    res.getInteger(R.integer.action_description_min_lines));
216        }
217
218        convertView.setTag(R.id.action_title, action);
219        convertView.setOnKeyListener(this);
220        convertView.setOnClickListener(this);
221        changeFocus(convertView, false /* hasFocus */, false /* shouldAnimate */);
222
223        return convertView;
224    }
225
226    @Override
227    public ScrollAdapterBase getExpandAdapter() {
228        return null;
229    }
230
231    public void setListener(Listener listener) {
232        mListener = listener;
233    }
234
235    public void setOnFocusListener(OnFocusListener onFocusListener) {
236        mOnFocusListener = onFocusListener;
237    }
238
239    public void setOnKeyListener(OnKeyListener onKeyListener) {
240        mOnKeyListener = onKeyListener;
241    }
242
243    public void addAction(Action action) {
244        mActions.add(action);
245        notifyDataSetChanged();
246    }
247
248    /**
249     * Used for serialization only.
250     */
251    public ArrayList<Action> getActions() {
252        return new ArrayList<>(mActions);
253    }
254
255    public void setActions(ArrayList<Action> actions) {
256        changeFocus(mSelectedView, false /* hasFocus */, false /* shouldAnimate */);
257        mActions.clear();
258        mActions.addAll(actions);
259        notifyDataSetChanged();
260    }
261
262    // We want to highlight a view if we've stopped scrolling on it (mainPosition = 0).
263    // If mainPosition is not 0, we don't want to do anything with view, but we do want to ensure
264    // we dim the last highlighted view so that while a user is scrolling, nothing is highlighted.
265    @Override
266    public void onScrolled(View view, int position, float mainPosition, float secondPosition) {
267        boolean hasFocus = (mainPosition == 0.0);
268        if (hasFocus) {
269            if (view != null) {
270                changeFocus(view, true /* hasFocus */, true /* shouldAniamte */);
271                mSelectedView = view;
272            }
273        } else if (mSelectedView != null) {
274            changeFocus(mSelectedView, false /* hasFocus */, true /* shouldAniamte */);
275            mSelectedView = null;
276        }
277    }
278
279    private void changeFocus(View v, boolean hasFocus, boolean shouldAnimate) {
280        if (v == null) {
281            return;
282        }
283        Action action = (Action) v.getTag(R.id.action_title);
284
285        float titleAlpha = action.isEnabled() && !action.infoOnly()
286                ? (hasFocus ? mSelectedTitleAlpha : mUnselectedAlpha) : mDisabledTitleAlpha;
287        float descriptionAlpha = (!hasFocus || action.infoOnly()) ? mUnselectedDescriptionAlpha
288                : (action.isEnabled() ? mSelectedDescriptionAlpha : mDisabledDescriptionAlpha);
289        float chevronAlpha = action.hasNext() && !action.infoOnly()
290                ? (action.isEnabled() ? mSelectedChevronAlpha : mDisabledChevronAlpha) : 0;
291
292        TextView title = (TextView) v.findViewById(R.id.action_title);
293        setAlpha(title, shouldAnimate, titleAlpha);
294
295        TextView description = (TextView) v.findViewById(R.id.action_description);
296        setAlpha(description, shouldAnimate, descriptionAlpha);
297
298        ImageView checkmark = (ImageView) v.findViewById(R.id.action_checkmark);
299        setAlpha(checkmark, shouldAnimate, titleAlpha);
300
301        ImageView icon = (ImageView) v.findViewById(R.id.action_icon);
302        setAlpha(icon, shouldAnimate, titleAlpha);
303
304        View chevronBackground = v.findViewById(R.id.action_next_chevron_background);
305        setAlpha(chevronBackground, shouldAnimate, chevronAlpha);
306
307        if (mOnFocusListener != null && hasFocus) {
308            // We still call onActionFocused so that listeners can clear state if they want.
309            mOnFocusListener.onActionFocused((Action) v.getTag(R.id.action_title));
310        }
311    }
312
313    private void setIndicator(final ImageView indicatorView, Action action) {
314
315        Drawable indicator = action.getIndicator(mContext);
316        if (indicator != null) {
317            indicatorView.setImageDrawable(indicator);
318            indicatorView.setVisibility(View.VISIBLE);
319        } else {
320            Uri iconUri = action.getIconUri();
321            if (iconUri != null) {
322                indicatorView.setVisibility(View.INVISIBLE);
323
324                BitmapDownloader.getInstance(mContext).getBitmap(new BitmapWorkerOptions.Builder(
325                        mContext).resource(iconUri)
326                        .width(indicatorView.getLayoutParams().width).build(),
327                        new BitmapCallback() {
328                            @Override
329                            public void onBitmapRetrieved(Bitmap bitmap) {
330                                if (bitmap != null) {
331                                    indicatorView.setVisibility(View.VISIBLE);
332                                    indicatorView.setImageBitmap(bitmap);
333                                    fadeIn(indicatorView);
334                                }
335                            }
336                        });
337            } else {
338                indicatorView.setVisibility(View.GONE);
339            }
340        }
341    }
342
343    private void fadeIn(View v) {
344        v.setAlpha(0f);
345        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", 1f);
346        alphaAnimator.setDuration(mContext.getResources().getInteger(
347                android.R.integer.config_mediumAnimTime));
348        alphaAnimator.start();
349    }
350
351    private void setAlpha(View view, boolean shouldAnimate, float alpha) {
352        if (shouldAnimate) {
353            view.animate().alpha(alpha)
354                    .setDuration(mAnimationDuration)
355                    .setInterpolator(new DecelerateInterpolator(2F))
356                    .start();
357        } else {
358            view.setAlpha(alpha);
359        }
360    }
361
362    void setScrollAdapterView(ScrollAdapterView scrollAdapterView) {
363        mScrollAdapterView = scrollAdapterView;
364    }
365
366    @Override
367    public void onClick(View v) {
368        if (v != null && v.getWindowToken() != null && mListener != null) {
369            final Action action = (Action) v.getTag(R.id.action_title);
370            mListener.onActionClicked(action);
371        }
372    }
373
374    /**
375     * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
376     */
377    @Override
378    public boolean onKey(View v, int keyCode, KeyEvent event) {
379        if (v == null) {
380            return false;
381        }
382        boolean handled = false;
383        Action action = (Action) v.getTag(R.id.action_title);
384        switch (keyCode) {
385            case KeyEvent.KEYCODE_DPAD_CENTER:
386            case KeyEvent.KEYCODE_NUMPAD_ENTER:
387            case KeyEvent.KEYCODE_BUTTON_X:
388            case KeyEvent.KEYCODE_BUTTON_Y:
389            case KeyEvent.KEYCODE_ENTER:
390                AudioManager manager = (AudioManager) v.getContext().getSystemService(
391                        Context.AUDIO_SERVICE);
392                if (!action.isEnabled() || action.infoOnly()) {
393                    if (v.isSoundEffectsEnabled() && event.getAction() == KeyEvent.ACTION_DOWN) {
394                        manager.playSoundEffect(FX_KEYPRESS_INVALID);
395                    }
396                    return true;
397                }
398
399                switch (event.getAction()) {
400                    case KeyEvent.ACTION_DOWN:
401                        if (!mKeyPressed) {
402                            mKeyPressed = true;
403
404                            if (v.isSoundEffectsEnabled()) {
405                                manager.playSoundEffect(AudioManager.FX_KEY_CLICK);
406                            }
407
408                            if (DEBUG) {
409                                Log.d(TAG, "Enter Key down");
410                            }
411
412                            prepareAndAnimateView(v, SELECT_ANIM_UNSELECTED_ALPHA,
413                                    SELECT_ANIM_SELECTED_ALPHA, SELECT_ANIM_DURATION,
414                                    SELECT_ANIM_DELAY, null, mKeyPressed);
415                            handled = true;
416                        }
417                        break;
418                    case KeyEvent.ACTION_UP:
419                        if (mKeyPressed) {
420                            mKeyPressed = false;
421
422                            if (DEBUG) {
423                                Log.d(TAG, "Enter Key up");
424                            }
425
426                            prepareAndAnimateView(v, SELECT_ANIM_SELECTED_ALPHA,
427                                    SELECT_ANIM_UNSELECTED_ALPHA, SELECT_ANIM_DURATION,
428                                    SELECT_ANIM_DELAY, null, mKeyPressed);
429                            handled = true;
430                        }
431                        break;
432                    default:
433                        break;
434                }
435                break;
436            default:
437                break;
438        }
439        return handled;
440    }
441
442    private void prepareAndAnimateView(final View v, float initAlpha, float destAlpha, int duration,
443            int delay, Interpolator interpolator, final boolean pressed) {
444        if (v != null && v.getWindowToken() != null) {
445            final Action action = (Action) v.getTag(R.id.action_title);
446
447            if (!pressed) {
448                fadeCheckmarks(v, action, duration, delay, interpolator);
449            }
450
451            v.setAlpha(initAlpha);
452            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
453            v.buildLayer();
454            v.animate().alpha(destAlpha).setDuration(duration).setStartDelay(delay);
455            if (interpolator != null) {
456                v.animate().setInterpolator(interpolator);
457            }
458            v.animate().setListener(new AnimatorListenerAdapter() {
459                @Override
460                public void onAnimationEnd(Animator animation) {
461
462                    v.setLayerType(View.LAYER_TYPE_NONE, null);
463                    if (pressed) {
464                        if (mOnKeyListener != null) {
465                            mOnKeyListener.onActionSelect(action);
466                        }
467                    } else {
468                        if (mOnKeyListener != null) {
469                            mOnKeyListener.onActionUnselect(action);
470                        }
471                        if (mListener != null) {
472                            mListener.onActionClicked(action);
473                        }
474                    }
475                }
476            });
477            v.animate().start();
478        }
479    }
480
481    private void fadeCheckmarks(final View v, final Action action, int duration, int delay,
482            Interpolator interpolator) {
483        int actionCheckSetId = action.getCheckSetId();
484        if (actionCheckSetId != Action.NO_CHECK_SET) {
485            // Find any actions that are checked and are in the same group
486            // as the selected action. Fade their checkmarks out.
487            for (int i = 0, size = mActions.size(); i < size; i++) {
488                Action a = mActions.get(i);
489                if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
490                    a.setChecked(false);
491                    if (mScrollAdapterView != null) {
492                        View viewToAnimateOut = mScrollAdapterView.getItemView(i);
493                        if (viewToAnimateOut != null) {
494                            final View checkView = viewToAnimateOut.findViewById(
495                                    R.id.action_checkmark);
496                            checkView.animate().alpha(CHECKMARK_ANIM_UNSELECTED_ALPHA)
497                                    .setDuration(duration).setStartDelay(delay);
498                            if (interpolator != null) {
499                                checkView.animate().setInterpolator(interpolator);
500                            }
501                            checkView.animate().setListener(new AnimatorListenerAdapter() {
502                                @Override
503                                public void onAnimationEnd(Animator animation) {
504                                    checkView.setVisibility(View.INVISIBLE);
505                                }
506                            });
507                        }
508                    }
509                }
510            }
511
512            // If we we'ren't already checked, fade our checkmark in.
513            if (!action.isChecked()) {
514                action.setChecked(true);
515                if (mScrollAdapterView != null) {
516                    final View checkView = v.findViewById(R.id.action_checkmark);
517                    checkView.setVisibility(View.VISIBLE);
518                    checkView.setAlpha(CHECKMARK_ANIM_UNSELECTED_ALPHA);
519                    checkView.animate().alpha(CHECKMARK_ANIM_SELECTED_ALPHA).setDuration(duration)
520                            .setStartDelay(delay);
521                    if (interpolator != null) {
522                        checkView.animate().setInterpolator(interpolator);
523                    }
524                    checkView.animate().setListener(null);
525                }
526            }
527        }
528    }
529
530    /**
531     * @return the max height in pixels the description can be such that the
532     * action nicely takes up the entire screen.
533     */
534    private static Integer getDescriptionMaxHeight(Context context, TextView title,
535            TextView description) {
536        if (sDescriptionMaxHeight == null) {
537            final Resources res = context.getResources();
538            final float verticalPadding = res.getDimension(R.dimen.list_item_vertical_padding);
539            final int titleMaxLines = res.getInteger(R.integer.action_title_max_lines);
540            final int displayHeight = ((WindowManager) context.getSystemService(
541                    Context.WINDOW_SERVICE)).getDefaultDisplay().getHeight();
542
543            // The 2 multiplier on the title height calculation is a conservative
544            // estimate for font padding which can not be calculated at this stage
545            // since the view hasn't been rendered yet.
546            sDescriptionMaxHeight = (int) (displayHeight -
547                    2 * verticalPadding - 2 * titleMaxLines * title.getLineHeight());
548        }
549        return sDescriptionMaxHeight;
550    }
551
552    private float getFloat(int resourceId) {
553        TypedValue buffer = new TypedValue();
554        mContext.getResources().getValue(resourceId, buffer, true);
555        return buffer.getFloat();
556    }
557}
558