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