1/*
2 * Copyright (C) 2015 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.ui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.support.annotation.Nullable;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.view.KeyEvent;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.animation.AnimationUtils;
32import android.view.animation.Interpolator;
33import android.widget.AdapterView;
34import android.widget.BaseAdapter;
35import android.widget.LinearLayout;
36import android.widget.ListView;
37import android.widget.TextView;
38
39import com.android.tv.MainActivity;
40import com.android.tv.R;
41import com.android.tv.TvApplication;
42import com.android.tv.analytics.DurationTimer;
43import com.android.tv.analytics.Tracker;
44import com.android.tv.common.SoftPreconditions;
45import com.android.tv.data.Channel;
46import com.android.tv.data.ChannelNumber;
47
48import java.util.ArrayList;
49import java.util.List;
50
51public class KeypadChannelSwitchView extends LinearLayout implements
52        TvTransitionManager.TransitionLayout {
53    private static final String TAG = "KeypadChannelSwitchView";
54
55    private static final int MAX_CHANNEL_NUMBER_DIGIT = 4;
56    private static final int MAX_MINOR_CHANNEL_NUMBER_DIGIT = 3;
57    private static final int MAX_CHANNEL_ITEM = 8;
58    private static final String CHANNEL_DELIMITERS_REGEX = "[-\\.\\s]";
59    public static final String SCREEN_NAME = "Channel switch";
60
61    private final MainActivity mMainActivity;
62    private final Tracker mTracker;
63    private final DurationTimer mViewDurationTimer = new DurationTimer();
64    private boolean mNavigated = false;
65    @Nullable  //Once mChannels is set to null it should not be used again.
66    private List<Channel> mChannels;
67    private TextView mChannelNumberView;
68    private ListView mChannelItemListView;
69    private final ChannelNumber mTypedChannelNumber = new ChannelNumber();
70    private final ArrayList<Channel> mChannelCandidates = new ArrayList<>();
71    protected final ChannelItemAdapter mAdapter = new ChannelItemAdapter();
72    private final LayoutInflater mLayoutInflater;
73    private Channel mSelectedChannel;
74
75    private final Runnable mHideRunnable = new Runnable() {
76        @Override
77        public void run() {
78            mCurrentHeight = 0;
79            if (mSelectedChannel != null) {
80                mMainActivity.tuneToChannel(mSelectedChannel);
81                mTracker.sendChannelNumberItemChosenByTimeout();
82            } else {
83                mMainActivity.getOverlayManager().hideOverlays(
84                        TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
85                        | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
86                        | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
87                        | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
88                        | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
89            }
90        }
91    };
92    private final long mShowDurationMillis;
93    private final long mRippleAnimDurationMillis;
94    private final int mBaseViewHeight;
95    private final int mItemHeight;
96    private final int mResizeAnimDuration;
97    private Animator mResizeAnimator;
98    private final Interpolator mResizeInterpolator;
99    // NOTE: getHeight() will be updated after layout() is called. mCurrentHeight is needed for
100    // getting the latest updated value of the view height before layout().
101    private int mCurrentHeight;
102
103    public KeypadChannelSwitchView(Context context) {
104        this(context, null, 0);
105    }
106
107    public KeypadChannelSwitchView(Context context, AttributeSet attrs) {
108        this(context, attrs, 0);
109    }
110
111    public KeypadChannelSwitchView(Context context, AttributeSet attrs, int defStyleAttr) {
112        super(context, attrs, defStyleAttr);
113
114        mMainActivity = (MainActivity) context;
115        mTracker = TvApplication.getSingletons(context).getTracker();
116        Resources resources = getResources();
117        mLayoutInflater = LayoutInflater.from(context);
118        mShowDurationMillis = resources.getInteger(R.integer.keypad_channel_switch_show_duration);
119        mRippleAnimDurationMillis = resources.getInteger(
120                R.integer.keypad_channel_switch_ripple_anim_duration);
121        mBaseViewHeight = resources.getDimensionPixelSize(
122                R.dimen.keypad_channel_switch_base_height);
123        mItemHeight = resources.getDimensionPixelSize(R.dimen.keypad_channel_switch_item_height);
124        mResizeAnimDuration = resources.getInteger(R.integer.keypad_channel_switch_anim_duration);
125        mResizeInterpolator = AnimationUtils.loadInterpolator(context,
126                android.R.interpolator.linear_out_slow_in);
127    }
128
129    @Override
130    protected void onFinishInflate(){
131        super.onFinishInflate();
132        mChannelNumberView = (TextView) findViewById(R.id.channel_number);
133        mChannelItemListView = (ListView) findViewById(R.id.channel_list);
134        mChannelItemListView.setAdapter(mAdapter);
135        mChannelItemListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
136            @Override
137            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
138                if (position >= mAdapter.getCount()) {
139                    // It can happen during closing.
140                    return;
141                }
142                mChannelItemListView.setFocusable(false);
143                final Channel channel = ((Channel) mAdapter.getItem(position));
144                postDelayed(new Runnable() {
145                    @Override
146                    public void run() {
147                        mChannelItemListView.setFocusable(true);
148                        mMainActivity.tuneToChannel(channel);
149                        mTracker.sendChannelNumberItemClicked();
150                    }
151                }, mRippleAnimDurationMillis);
152            }
153        });
154        mChannelItemListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
155            @Override
156            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
157                if (position >= mAdapter.getCount()) {
158                    // It can happen during closing.
159                    mSelectedChannel = null;
160                } else {
161                    mSelectedChannel = (Channel) mAdapter.getItem(position);
162                }
163                if (position != 0 && !mNavigated) {
164                    mNavigated = true;
165                    mTracker.sendChannelInputNavigated();
166                }
167            }
168
169            @Override
170            public void onNothingSelected(AdapterView<?> parent) {
171                mSelectedChannel = null;
172            }
173        });
174    }
175
176    @Override
177    public boolean dispatchKeyEvent(KeyEvent event) {
178        scheduleHide();
179        return super.dispatchKeyEvent(event);
180    }
181
182    @Override
183    public boolean onKeyUp(int keyCode, KeyEvent event) {
184        SoftPreconditions.checkNotNull(mChannels, TAG, "mChannels");
185        if (isChannelNumberKey(keyCode)) {
186            onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0);
187            return true;
188        }
189        if (ChannelNumber.isChannelNumberDelimiterKey(keyCode)) {
190            onDelimiterKeyUp();
191            return true;
192        }
193        return super.onKeyUp(keyCode, event);
194    }
195
196    @Override
197    public void onEnterAction(boolean fromEmptyScene) {
198        reset();
199        if (fromEmptyScene) {
200            ViewUtils.setTransitionAlpha(mChannelItemListView, 1f);
201        }
202        mNavigated = false;
203        mViewDurationTimer.start();
204        mTracker.sendShowChannelSwitch();
205        mTracker.sendScreenView(SCREEN_NAME);
206        updateView();
207        scheduleHide();
208    }
209
210    @Override
211    public void onExitAction() {
212        mCurrentHeight = 0;
213        mTracker.sendHideChannelSwitch(mViewDurationTimer.reset());
214        cancelHide();
215    }
216
217    private void scheduleHide() {
218        cancelHide();
219        postDelayed(mHideRunnable, mShowDurationMillis);
220    }
221
222    private void cancelHide() {
223        removeCallbacks(mHideRunnable);
224    }
225
226    private void reset() {
227        mTypedChannelNumber.reset();
228        mSelectedChannel = null;
229        mChannelCandidates.clear();
230        mAdapter.notifyDataSetChanged();
231    }
232
233    public void setChannels(@Nullable List<Channel> channels) {
234        mChannels = channels;
235    }
236
237    public static boolean isChannelNumberKey(int keyCode) {
238        return keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9;
239    }
240
241    public void onNumberKeyUp(int num) {
242        // Reset typed channel number in some cases.
243        if (mTypedChannelNumber.majorNumber == null) {
244            mTypedChannelNumber.reset();
245        } else if (!mTypedChannelNumber.hasDelimiter
246                && mTypedChannelNumber.majorNumber.length() >= MAX_CHANNEL_NUMBER_DIGIT) {
247            mTypedChannelNumber.reset();
248        } else if (mTypedChannelNumber.hasDelimiter
249                && mTypedChannelNumber.minorNumber != null
250                && mTypedChannelNumber.minorNumber.length() >= MAX_MINOR_CHANNEL_NUMBER_DIGIT) {
251            mTypedChannelNumber.reset();
252        }
253
254        if (!mTypedChannelNumber.hasDelimiter) {
255            mTypedChannelNumber.majorNumber += String.valueOf(num);
256        } else {
257            mTypedChannelNumber.minorNumber += String.valueOf(num);
258        }
259        mTracker.sendChannelNumberInput();
260        updateView();
261    }
262
263    private void onDelimiterKeyUp() {
264        if (mTypedChannelNumber.hasDelimiter || mTypedChannelNumber.majorNumber.length() == 0) {
265            return;
266        }
267        mTypedChannelNumber.hasDelimiter = true;
268        mTracker.sendChannelNumberInput();
269        updateView();
270    }
271
272    private void updateView() {
273        mChannelNumberView.setText(mTypedChannelNumber.toString() + "_");
274        mChannelCandidates.clear();
275        ArrayList<Channel> secondaryChannelCandidates = new ArrayList<>();
276        for (Channel channel : mChannels) {
277            ChannelNumber chNumber = ChannelNumber.parseChannelNumber(channel.getDisplayNumber());
278            if (chNumber == null) {
279                Log.i(TAG, "Malformed channel number (name=" + channel.getDisplayName()
280                        + ", number=" + channel.getDisplayNumber() + ")");
281                continue;
282            }
283            if (matchChannelNumber(mTypedChannelNumber, chNumber)) {
284                mChannelCandidates.add(channel);
285            } else if (!mTypedChannelNumber.hasDelimiter) {
286                // Even if a user doesn't type '-', we need to match the typed number to not only
287                // the major number but also the minor number. For example, when a user types '111'
288                // without delimiter, it should be matched to '111', '1-11' and '11-1'.
289                if (channel.getDisplayNumber().replaceAll(CHANNEL_DELIMITERS_REGEX, "")
290                        .startsWith(mTypedChannelNumber.majorNumber)) {
291                    secondaryChannelCandidates.add(channel);
292                }
293            }
294        }
295        mChannelCandidates.addAll(secondaryChannelCandidates);
296        mAdapter.notifyDataSetChanged();
297        if (mAdapter.getCount() > 0) {
298            mChannelItemListView.requestFocus();
299            mChannelItemListView.setSelection(0);
300            mSelectedChannel = mChannelCandidates.get(0);
301        }
302
303        updateViewHeight();
304    }
305
306    private void updateViewHeight() {
307        int itemListHeight = mItemHeight * Math.min(MAX_CHANNEL_ITEM, mAdapter.getCount());
308        int targetHeight = mBaseViewHeight + itemListHeight;
309        if (mResizeAnimator != null) {
310            mResizeAnimator.cancel();
311            mResizeAnimator = null;
312        }
313
314        if (mCurrentHeight == 0) {
315            // Do not add the resize animation when the banner has not been shown before.
316            mCurrentHeight = targetHeight;
317            setViewHeight(this, targetHeight);
318        } else if (mCurrentHeight != targetHeight){
319            mResizeAnimator = createResizeAnimator(targetHeight);
320            mResizeAnimator.start();
321        }
322    }
323
324    private Animator createResizeAnimator(int targetHeight) {
325        ValueAnimator animator = ValueAnimator.ofInt(mCurrentHeight, targetHeight);
326        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
327            @Override
328            public void onAnimationUpdate(ValueAnimator animation) {
329                int value = (Integer) animation.getAnimatedValue();
330                setViewHeight(KeypadChannelSwitchView.this, value);
331                mCurrentHeight = value;
332            }
333        });
334        animator.setDuration(mResizeAnimDuration);
335        animator.addListener(new AnimatorListenerAdapter() {
336            @Override
337            public void onAnimationEnd(Animator animator) {
338                mResizeAnimator = null;
339            }
340        });
341        animator.setInterpolator(mResizeInterpolator);
342        return animator;
343    }
344
345    private void setViewHeight(View view, int height) {
346        ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
347        if (height != layoutParams.height) {
348            layoutParams.height = height;
349            view.setLayoutParams(layoutParams);
350        }
351    }
352
353    private static boolean matchChannelNumber(ChannelNumber typedChNumber, ChannelNumber chNumber) {
354        if (!chNumber.majorNumber.equals(typedChNumber.majorNumber)) {
355            return false;
356        }
357        if (typedChNumber.hasDelimiter) {
358            if (!chNumber.hasDelimiter) {
359                return false;
360            }
361            if (!chNumber.minorNumber.startsWith(typedChNumber.minorNumber)) {
362                return false;
363            }
364        }
365        return true;
366    }
367
368    class ChannelItemAdapter extends BaseAdapter {
369        @Override
370        public int getCount() {
371            return mChannelCandidates.size();
372        }
373
374        @Override
375        public Object getItem(int position) {
376            return mChannelCandidates.get(position);
377        }
378
379        @Override
380        public long getItemId(int position) {
381            return position;
382        }
383
384        @Override
385        public View getView(int position, View convertView, ViewGroup parent) {
386            final Channel channel = mChannelCandidates.get(position);
387            View v = convertView;
388            if (v == null) {
389                v = mLayoutInflater.inflate(R.layout.keypad_channel_switch_item, parent, false);
390            }
391
392            TextView channelNumberView = (TextView) v.findViewById(R.id.number);
393            channelNumberView.setText(channel.getDisplayNumber());
394
395            TextView channelNameView = (TextView) v.findViewById(R.id.name);
396            channelNameView.setText(channel.getDisplayName());
397            return v;
398        }
399    }
400}
401