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                                 () -> {
152                                     mChannelItemListView.setFocusable(true);
153                                     mMainActivity.tuneToChannel(channel);
154                                     mTracker.sendChannelNumberItemClicked();
155                                 },
156                                 mRippleAnimDurationMillis);
157                     }
158                 });
159         mChannelItemListView.setOnItemSelectedListener(
160                 new AdapterView.OnItemSelectedListener() {
161                     @Override
162                     public void onItemSelected(
163                             AdapterView<?> parent, View view, int position, long id) {
164                         if (position >= mAdapter.getCount()) {
165                             // It can happen during closing.
166                             mSelectedChannel = null;
167                         } else {
168                             mSelectedChannel = (Channel) mAdapter.getItem(position);
169                         }
170                         if (position != 0 && !mNavigated) {
171                             mNavigated = true;
172                             mTracker.sendChannelInputNavigated();
173                         }
174                     }
175 
176                     @Override
177                     public void onNothingSelected(AdapterView<?> parent) {
178                         mSelectedChannel = null;
179                     }
180                 });
181     }
182 
183     @Override
dispatchKeyEvent(KeyEvent event)184     public boolean dispatchKeyEvent(KeyEvent event) {
185         scheduleHide();
186         return super.dispatchKeyEvent(event);
187     }
188 
189     @Override
onKeyUp(int keyCode, KeyEvent event)190     public boolean onKeyUp(int keyCode, KeyEvent event) {
191         SoftPreconditions.checkNotNull(mChannels, TAG, "mChannels");
192         if (isChannelNumberKey(keyCode)) {
193             onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0);
194             return true;
195         }
196         if (ChannelNumber.isChannelNumberDelimiterKey(keyCode)) {
197             onDelimiterKeyUp();
198             return true;
199         }
200         return super.onKeyUp(keyCode, event);
201     }
202 
203     @Override
onEnterAction(boolean fromEmptyScene)204     public void onEnterAction(boolean fromEmptyScene) {
205         reset();
206         if (fromEmptyScene) {
207             ViewUtils.setTransitionAlpha(mChannelItemListView, 1f);
208         }
209         mNavigated = false;
210         mViewDurationTimer.start();
211         mTracker.sendShowChannelSwitch();
212         mTracker.sendScreenView(SCREEN_NAME);
213         updateView();
214         scheduleHide();
215     }
216 
217     @Override
onExitAction()218     public void onExitAction() {
219         mCurrentHeight = 0;
220         mTracker.sendHideChannelSwitch(mViewDurationTimer.reset());
221         cancelHide();
222     }
223 
scheduleHide()224     private void scheduleHide() {
225         cancelHide();
226         postDelayed(mHideRunnable, mShowDurationMillis);
227     }
228 
cancelHide()229     private void cancelHide() {
230         removeCallbacks(mHideRunnable);
231     }
232 
reset()233     private void reset() {
234         mTypedChannelNumber.reset();
235         mSelectedChannel = null;
236         mChannelCandidates.clear();
237         mAdapter.notifyDataSetChanged();
238     }
239 
setChannels(@ullable List<Channel> channels)240     public void setChannels(@Nullable List<Channel> channels) {
241         mChannels = channels;
242     }
243 
isChannelNumberKey(int keyCode)244     public static boolean isChannelNumberKey(int keyCode) {
245         return keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9;
246     }
247 
onNumberKeyUp(int num)248     public void onNumberKeyUp(int num) {
249         // Reset typed channel number in some cases.
250         if (mTypedChannelNumber.majorNumber == null) {
251             mTypedChannelNumber.reset();
252         } else if (!mTypedChannelNumber.hasDelimiter
253                 && mTypedChannelNumber.majorNumber.length() >= MAX_CHANNEL_NUMBER_DIGIT) {
254             mTypedChannelNumber.reset();
255         } else if (mTypedChannelNumber.hasDelimiter
256                 && mTypedChannelNumber.minorNumber != null
257                 && mTypedChannelNumber.minorNumber.length() >= MAX_MINOR_CHANNEL_NUMBER_DIGIT) {
258             mTypedChannelNumber.reset();
259         }
260 
261         if (!mTypedChannelNumber.hasDelimiter) {
262             mTypedChannelNumber.majorNumber += String.valueOf(num);
263         } else {
264             mTypedChannelNumber.minorNumber += String.valueOf(num);
265         }
266         mTracker.sendChannelNumberInput();
267         updateView();
268     }
269 
onDelimiterKeyUp()270     private void onDelimiterKeyUp() {
271         if (mTypedChannelNumber.hasDelimiter || mTypedChannelNumber.majorNumber.length() == 0) {
272             return;
273         }
274         mTypedChannelNumber.hasDelimiter = true;
275         mTracker.sendChannelNumberInput();
276         updateView();
277     }
278 
updateView()279     private void updateView() {
280         mChannelNumberView.setText(mTypedChannelNumber.toString() + "_");
281         mChannelCandidates.clear();
282         ArrayList<Channel> secondaryChannelCandidates = new ArrayList<>();
283         for (Channel channel : mChannels) {
284             ChannelNumber chNumber = ChannelNumber.parseChannelNumber(channel.getDisplayNumber());
285             if (chNumber == null) {
286                 Log.i(
287                         TAG,
288                         "Malformed channel number (name="
289                                 + channel.getDisplayName()
290                                 + ", number="
291                                 + channel.getDisplayNumber()
292                                 + ")");
293                 continue;
294             }
295             if (matchChannelNumber(mTypedChannelNumber, chNumber)) {
296                 mChannelCandidates.add(channel);
297             } else if (!mTypedChannelNumber.hasDelimiter) {
298                 // Even if a user doesn't type '-', we need to match the typed number to not only
299                 // the major number but also the minor number. For example, when a user types '111'
300                 // without delimiter, it should be matched to '111', '1-11' and '11-1'.
301                 if (channel.getDisplayNumber()
302                         .replaceAll(CHANNEL_DELIMITERS_REGEX, "")
303                         .startsWith(mTypedChannelNumber.majorNumber)) {
304                     secondaryChannelCandidates.add(channel);
305                 }
306             }
307         }
308         mChannelCandidates.addAll(secondaryChannelCandidates);
309         mAdapter.notifyDataSetChanged();
310         if (mAdapter.getCount() > 0) {
311             mChannelItemListView.requestFocus();
312             mChannelItemListView.setSelection(0);
313             mSelectedChannel = mChannelCandidates.get(0);
314         }
315 
316         updateViewHeight();
317     }
318 
updateViewHeight()319     private void updateViewHeight() {
320         int itemListHeight = mItemHeight * Math.min(MAX_CHANNEL_ITEM, mAdapter.getCount());
321         int targetHeight = mBaseViewHeight + itemListHeight;
322         if (mResizeAnimator != null) {
323             mResizeAnimator.cancel();
324             mResizeAnimator = null;
325         }
326 
327         if (mCurrentHeight == 0) {
328             // Do not add the resize animation when the banner has not been shown before.
329             mCurrentHeight = targetHeight;
330             setViewHeight(this, targetHeight);
331         } else if (mCurrentHeight != targetHeight) {
332             mResizeAnimator = createResizeAnimator(targetHeight);
333             mResizeAnimator.start();
334         }
335     }
336 
createResizeAnimator(int targetHeight)337     private Animator createResizeAnimator(int targetHeight) {
338         ValueAnimator animator = ValueAnimator.ofInt(mCurrentHeight, targetHeight);
339         animator.addUpdateListener(
340                 new ValueAnimator.AnimatorUpdateListener() {
341                     @Override
342                     public void onAnimationUpdate(ValueAnimator animation) {
343                         int value = (Integer) animation.getAnimatedValue();
344                         setViewHeight(KeypadChannelSwitchView.this, value);
345                         mCurrentHeight = value;
346                     }
347                 });
348         animator.setDuration(mResizeAnimDuration);
349         animator.addListener(
350                 new AnimatorListenerAdapter() {
351                     @Override
352                     public void onAnimationEnd(Animator animator) {
353                         mResizeAnimator = null;
354                     }
355                 });
356         animator.setInterpolator(mResizeInterpolator);
357         return animator;
358     }
359 
setViewHeight(View view, int height)360     private void setViewHeight(View view, int height) {
361         ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
362         if (height != layoutParams.height) {
363             layoutParams.height = height;
364             view.setLayoutParams(layoutParams);
365         }
366     }
367 
matchChannelNumber(ChannelNumber typedChNumber, ChannelNumber chNumber)368     private static boolean matchChannelNumber(ChannelNumber typedChNumber, ChannelNumber chNumber) {
369         if (!chNumber.majorNumber.equals(typedChNumber.majorNumber)) {
370             return false;
371         }
372         if (typedChNumber.hasDelimiter) {
373             if (!chNumber.hasDelimiter) {
374                 return false;
375             }
376             if (!chNumber.minorNumber.startsWith(typedChNumber.minorNumber)) {
377                 return false;
378             }
379         }
380         return true;
381     }
382 
383     class ChannelItemAdapter extends BaseAdapter {
384         @Override
getCount()385         public int getCount() {
386             return mChannelCandidates.size();
387         }
388 
389         @Override
getItem(int position)390         public Object getItem(int position) {
391             return mChannelCandidates.get(position);
392         }
393 
394         @Override
getItemId(int position)395         public long getItemId(int position) {
396             return position;
397         }
398 
399         @Override
getView(int position, View convertView, ViewGroup parent)400         public View getView(int position, View convertView, ViewGroup parent) {
401             final Channel channel = mChannelCandidates.get(position);
402             View v = convertView;
403             if (v == null) {
404                 v = mLayoutInflater.inflate(R.layout.keypad_channel_switch_item, parent, false);
405             }
406 
407             TextView channelNumberView = (TextView) v.findViewById(R.id.number);
408             channelNumberView.setText(channel.getDisplayNumber());
409 
410             TextView channelNameView = (TextView) v.findViewById(R.id.name);
411             channelNameView.setText(channel.getDisplayName());
412             return v;
413         }
414     }
415 }
416