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