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 
17 package com.android.tv.ui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.support.annotation.Nullable;
25 import android.util.AttributeSet;
26 import android.util.Log;
27 import android.view.KeyEvent;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.animation.AnimationUtils;
32 import android.view.animation.Interpolator;
33 import android.widget.AdapterView;
34 import android.widget.BaseAdapter;
35 import android.widget.LinearLayout;
36 import android.widget.ListView;
37 import android.widget.TextView;
38 
39 import com.android.tv.MainActivity;
40 import com.android.tv.R;
41 import com.android.tv.TvApplication;
42 import com.android.tv.analytics.DurationTimer;
43 import com.android.tv.analytics.Tracker;
44 import com.android.tv.common.SoftPreconditions;
45 import com.android.tv.data.Channel;
46 import com.android.tv.data.ChannelNumber;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 
51 public 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 
KeypadChannelSwitchView(Context context)103     public KeypadChannelSwitchView(Context context) {
104         this(context, null, 0);
105     }
106 
KeypadChannelSwitchView(Context context, AttributeSet attrs)107     public KeypadChannelSwitchView(Context context, AttributeSet attrs) {
108         this(context, attrs, 0);
109     }
110 
KeypadChannelSwitchView(Context context, AttributeSet attrs, int defStyleAttr)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
onFinishInflate()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
dispatchKeyEvent(KeyEvent event)177     public boolean dispatchKeyEvent(KeyEvent event) {
178         scheduleHide();
179         return super.dispatchKeyEvent(event);
180     }
181 
182     @Override
onKeyUp(int keyCode, KeyEvent event)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
onEnterAction(boolean fromEmptyScene)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
onExitAction()211     public void onExitAction() {
212         mCurrentHeight = 0;
213         mTracker.sendHideChannelSwitch(mViewDurationTimer.reset());
214         cancelHide();
215     }
216 
scheduleHide()217     private void scheduleHide() {
218         cancelHide();
219         postDelayed(mHideRunnable, mShowDurationMillis);
220     }
221 
cancelHide()222     private void cancelHide() {
223         removeCallbacks(mHideRunnable);
224     }
225 
reset()226     private void reset() {
227         mTypedChannelNumber.reset();
228         mSelectedChannel = null;
229         mChannelCandidates.clear();
230         mAdapter.notifyDataSetChanged();
231     }
232 
setChannels(@ullable List<Channel> channels)233     public void setChannels(@Nullable List<Channel> channels) {
234         mChannels = channels;
235     }
236 
isChannelNumberKey(int keyCode)237     public static boolean isChannelNumberKey(int keyCode) {
238         return keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9;
239     }
240 
onNumberKeyUp(int num)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 
onDelimiterKeyUp()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 
updateView()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 
updateViewHeight()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 
createResizeAnimator(int targetHeight)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 
setViewHeight(View view, int height)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 
matchChannelNumber(ChannelNumber typedChNumber, ChannelNumber chNumber)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
getCount()370         public int getCount() {
371             return mChannelCandidates.size();
372         }
373 
374         @Override
getItem(int position)375         public Object getItem(int position) {
376             return mChannelCandidates.get(position);
377         }
378 
379         @Override
getItemId(int position)380         public long getItemId(int position) {
381             return position;
382         }
383 
384         @Override
getView(int position, View convertView, ViewGroup parent)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