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.recommendation; 18 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.app.Service; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.graphics.Bitmap; 26 import android.graphics.Canvas; 27 import android.graphics.Matrix; 28 import android.graphics.Paint; 29 import android.graphics.Rect; 30 import android.media.tv.TvInputInfo; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.os.Message; 36 import android.support.annotation.NonNull; 37 import android.support.annotation.Nullable; 38 import android.support.annotation.UiThread; 39 import android.text.TextUtils; 40 import android.util.Log; 41 import android.util.SparseLongArray; 42 import android.view.View; 43 import com.android.tv.MainActivityWrapper.OnCurrentChannelChangeListener; 44 import com.android.tv.R; 45 import com.android.tv.Starter; 46 import com.android.tv.TvSingletons; 47 import com.android.tv.common.CommonConstants; 48 import com.android.tv.common.WeakHandler; 49 import com.android.tv.data.Program; 50 import com.android.tv.data.api.Channel; 51 import com.android.tv.util.TvInputManagerHelper; 52 import com.android.tv.util.Utils; 53 import com.android.tv.util.images.BitmapUtils; 54 import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo; 55 import com.android.tv.util.images.ImageLoader; 56 import java.util.ArrayList; 57 import java.util.List; 58 59 /** A local service for notify recommendation at home launcher. */ 60 public class NotificationService extends Service 61 implements Recommender.Listener, OnCurrentChannelChangeListener { 62 private static final String TAG = "NotificationService"; 63 private static final boolean DEBUG = false; 64 65 public static final String ACTION_SHOW_RECOMMENDATION = 66 CommonConstants.BASE_PACKAGE + ".notification.ACTION_SHOW_RECOMMENDATION"; 67 public static final String ACTION_HIDE_RECOMMENDATION = 68 CommonConstants.BASE_PACKAGE + ".notification.ACTION_HIDE_RECOMMENDATION"; 69 70 /** 71 * Recommendation intent has an extra data for the recommendation type. It'll be also sent to a 72 * TV input as a tune parameter. 73 */ 74 public static final String TUNE_PARAMS_RECOMMENDATION_TYPE = 75 CommonConstants.BASE_PACKAGE + ".recommendation_type"; 76 77 private static final String TYPE_RANDOM_RECOMMENDATION = "random"; 78 private static final String TYPE_ROUTINE_WATCH_RECOMMENDATION = "routine_watch"; 79 private static final String TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION = 80 "routine_watch_and_favorite"; 81 82 private static final String NOTIFY_TAG = "tv_recommendation"; 83 // TODO: find out proper number of notifications and whether to make it dynamically 84 // configurable from system property or etc. 85 private static final int NOTIFICATION_COUNT = 3; 86 87 private static final int MSG_INITIALIZE_RECOMMENDER = 1000; 88 private static final int MSG_SHOW_RECOMMENDATION = 1001; 89 private static final int MSG_UPDATE_RECOMMENDATION = 1002; 90 private static final int MSG_HIDE_RECOMMENDATION = 1003; 91 92 private static final long RECOMMENDATION_RETRY_TIME_MS = 5 * 60 * 1000; // 5 min 93 private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS = 10 * 60 * 1000; // 10 min 94 private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90% 95 private static final int MAX_PROGRAM_UPDATE_COUNT = 20; 96 97 private TvInputManagerHelper mTvInputManagerHelper; 98 private Recommender mRecommender; 99 private boolean mShowRecommendationAfterRecommenderReady; 100 private NotificationManager mNotificationManager; 101 private HandlerThread mHandlerThread; 102 private Handler mHandler; 103 private final String mRecommendationType; 104 private int mCurrentNotificationCount; 105 private long[] mNotificationChannels; 106 107 private Channel mPlayingChannel; 108 109 private float mNotificationCardMaxWidth; 110 private float mNotificationCardHeight; 111 private int mCardImageHeight; 112 private int mCardImageMaxWidth; 113 private int mCardImageMinWidth; 114 private int mChannelLogoMaxWidth; 115 private int mChannelLogoMaxHeight; 116 private int mLogoPaddingStart; 117 private int mLogoPaddingBottom; 118 NotificationService()119 public NotificationService() { 120 mRecommendationType = TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION; 121 } 122 123 @Override onCreate()124 public void onCreate() { 125 if (DEBUG) Log.d(TAG, "onCreate"); 126 Starter.start(this); 127 super.onCreate(); 128 mCurrentNotificationCount = 0; 129 mNotificationChannels = new long[NOTIFICATION_COUNT]; 130 for (int i = 0; i < NOTIFICATION_COUNT; ++i) { 131 mNotificationChannels[i] = Channel.INVALID_ID; 132 } 133 mNotificationCardMaxWidth = 134 getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); 135 mNotificationCardHeight = 136 getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); 137 mCardImageHeight = getResources().getDimensionPixelSize(R.dimen.notif_card_img_height); 138 mCardImageMaxWidth = getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width); 139 mCardImageMinWidth = getResources().getDimensionPixelSize(R.dimen.notif_card_img_min_width); 140 mChannelLogoMaxWidth = 141 getResources().getDimensionPixelSize(R.dimen.notif_ch_logo_max_width); 142 mChannelLogoMaxHeight = 143 getResources().getDimensionPixelSize(R.dimen.notif_ch_logo_max_height); 144 mLogoPaddingStart = 145 getResources().getDimensionPixelOffset(R.dimen.notif_ch_logo_padding_start); 146 mLogoPaddingBottom = 147 getResources().getDimensionPixelOffset(R.dimen.notif_ch_logo_padding_bottom); 148 149 mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 150 TvSingletons tvSingletons = TvSingletons.getSingletons(this); 151 mTvInputManagerHelper = tvSingletons.getTvInputManagerHelper(); 152 mHandlerThread = new HandlerThread("tv notification"); 153 mHandlerThread.start(); 154 mHandler = new NotificationHandler(mHandlerThread.getLooper(), this); 155 mHandler.sendEmptyMessage(MSG_INITIALIZE_RECOMMENDER); 156 157 // Just called for early initialization. 158 tvSingletons.getChannelDataManager(); 159 tvSingletons.getProgramDataManager(); 160 tvSingletons.getMainActivityWrapper().addOnCurrentChannelChangeListener(this); 161 } 162 163 @UiThread 164 @Override onCurrentChannelChange(@ullable Channel channel)165 public void onCurrentChannelChange(@Nullable Channel channel) { 166 if (DEBUG) Log.d(TAG, "onCurrentChannelChange"); 167 mPlayingChannel = channel; 168 mHandler.removeMessages(MSG_SHOW_RECOMMENDATION); 169 mHandler.sendEmptyMessage(MSG_SHOW_RECOMMENDATION); 170 } 171 handleInitializeRecommender()172 private void handleInitializeRecommender() { 173 mRecommender = new Recommender(NotificationService.this, NotificationService.this, true); 174 if (TYPE_RANDOM_RECOMMENDATION.equals(mRecommendationType)) { 175 mRecommender.registerEvaluator(new RandomEvaluator()); 176 } else if (TYPE_ROUTINE_WATCH_RECOMMENDATION.equals(mRecommendationType)) { 177 mRecommender.registerEvaluator(new RoutineWatchEvaluator()); 178 } else if (TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION.equals( 179 mRecommendationType)) { 180 mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5); 181 mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0); 182 } else { 183 throw new IllegalStateException( 184 "Undefined recommendation type: " + mRecommendationType); 185 } 186 } 187 handleShowRecommendation()188 private void handleShowRecommendation() { 189 if (mRecommender == null) { 190 return; 191 } 192 if (!mRecommender.isReady()) { 193 mShowRecommendationAfterRecommenderReady = true; 194 } else { 195 showRecommendation(); 196 } 197 } 198 handleUpdateRecommendation(int notificationId, Channel channel)199 private void handleUpdateRecommendation(int notificationId, Channel channel) { 200 if (mNotificationChannels[notificationId] == Channel.INVALID_ID 201 || !sendNotification(channel.getId(), notificationId)) { 202 changeRecommendation(notificationId); 203 } 204 } 205 handleHideRecommendation()206 private void handleHideRecommendation() { 207 if (mRecommender == null) { 208 return; 209 } 210 if (!mRecommender.isReady()) { 211 mShowRecommendationAfterRecommenderReady = false; 212 } else { 213 hideAllRecommendation(); 214 } 215 } 216 217 @Override onDestroy()218 public void onDestroy() { 219 TvSingletons.getSingletons(this) 220 .getMainActivityWrapper() 221 .removeOnCurrentChannelChangeListener(this); 222 if (mRecommender != null) { 223 mRecommender.release(); 224 mRecommender = null; 225 } 226 if (mHandlerThread != null) { 227 mHandlerThread.quit(); 228 mHandlerThread = null; 229 mHandler = null; 230 } 231 super.onDestroy(); 232 } 233 234 @Override onStartCommand(Intent intent, int flags, int startId)235 public int onStartCommand(Intent intent, int flags, int startId) { 236 if (DEBUG) Log.d(TAG, "onStartCommand"); 237 if (intent != null) { 238 String action = intent.getAction(); 239 if (ACTION_SHOW_RECOMMENDATION.equals(action)) { 240 mHandler.removeMessages(MSG_SHOW_RECOMMENDATION); 241 mHandler.removeMessages(MSG_HIDE_RECOMMENDATION); 242 mHandler.obtainMessage(MSG_SHOW_RECOMMENDATION).sendToTarget(); 243 } else if (ACTION_HIDE_RECOMMENDATION.equals(action)) { 244 mHandler.removeMessages(MSG_SHOW_RECOMMENDATION); 245 mHandler.removeMessages(MSG_UPDATE_RECOMMENDATION); 246 mHandler.removeMessages(MSG_HIDE_RECOMMENDATION); 247 mHandler.obtainMessage(MSG_HIDE_RECOMMENDATION).sendToTarget(); 248 } 249 } 250 return START_STICKY; 251 } 252 253 @Override onBind(Intent intent)254 public IBinder onBind(Intent intent) { 255 return null; 256 } 257 258 @Override onRecommenderReady()259 public void onRecommenderReady() { 260 if (DEBUG) Log.d(TAG, "onRecommendationReady"); 261 if (mShowRecommendationAfterRecommenderReady) { 262 mHandler.removeMessages(MSG_SHOW_RECOMMENDATION); 263 mHandler.sendEmptyMessage(MSG_SHOW_RECOMMENDATION); 264 mShowRecommendationAfterRecommenderReady = false; 265 } 266 } 267 268 @Override onRecommendationChanged()269 public void onRecommendationChanged() { 270 if (DEBUG) Log.d(TAG, "onRecommendationChanged"); 271 // Update recommendation on the handler thread. 272 mHandler.removeMessages(MSG_SHOW_RECOMMENDATION); 273 mHandler.sendEmptyMessage(MSG_SHOW_RECOMMENDATION); 274 } 275 showRecommendation()276 private void showRecommendation() { 277 if (DEBUG) Log.d(TAG, "showRecommendation"); 278 SparseLongArray notificationChannels = new SparseLongArray(); 279 for (int i = 0; i < NOTIFICATION_COUNT; ++i) { 280 if (mNotificationChannels[i] == Channel.INVALID_ID) { 281 continue; 282 } 283 notificationChannels.put(i, mNotificationChannels[i]); 284 } 285 List<Channel> channels = recommendChannels(); 286 for (Channel c : channels) { 287 int index = notificationChannels.indexOfValue(c.getId()); 288 if (index >= 0) { 289 notificationChannels.removeAt(index); 290 } 291 } 292 // Cancel notification whose channels are not recommended anymore. 293 if (notificationChannels.size() > 0) { 294 for (int i = 0; i < notificationChannels.size(); ++i) { 295 int notificationId = notificationChannels.keyAt(i); 296 mNotificationManager.cancel(NOTIFY_TAG, notificationId); 297 mNotificationChannels[notificationId] = Channel.INVALID_ID; 298 --mCurrentNotificationCount; 299 } 300 } 301 for (Channel c : channels) { 302 if (mCurrentNotificationCount >= NOTIFICATION_COUNT) { 303 break; 304 } 305 if (!isNotifiedChannel(c.getId())) { 306 sendNotification(c.getId(), getAvailableNotificationId()); 307 } 308 } 309 if (mCurrentNotificationCount < NOTIFICATION_COUNT) { 310 mHandler.sendEmptyMessageDelayed(MSG_SHOW_RECOMMENDATION, RECOMMENDATION_RETRY_TIME_MS); 311 } 312 } 313 changeRecommendation(int notificationId)314 private void changeRecommendation(int notificationId) { 315 if (DEBUG) Log.d(TAG, "changeRecommendation"); 316 List<Channel> channels = recommendChannels(); 317 if (mNotificationChannels[notificationId] != Channel.INVALID_ID) { 318 mNotificationChannels[notificationId] = Channel.INVALID_ID; 319 --mCurrentNotificationCount; 320 } 321 for (Channel c : channels) { 322 if (!isNotifiedChannel(c.getId())) { 323 if (sendNotification(c.getId(), notificationId)) { 324 return; 325 } 326 } 327 } 328 mNotificationManager.cancel(NOTIFY_TAG, notificationId); 329 } 330 recommendChannels()331 private List<Channel> recommendChannels() { 332 List channels = mRecommender.recommendChannels(); 333 if (channels.contains(mPlayingChannel)) { 334 channels = new ArrayList<>(channels); 335 channels.remove(mPlayingChannel); 336 } 337 return channels; 338 } 339 hideAllRecommendation()340 private void hideAllRecommendation() { 341 for (int i = 0; i < NOTIFICATION_COUNT; ++i) { 342 if (mNotificationChannels[i] != Channel.INVALID_ID) { 343 mNotificationChannels[i] = Channel.INVALID_ID; 344 mNotificationManager.cancel(NOTIFY_TAG, i); 345 } 346 } 347 mCurrentNotificationCount = 0; 348 } 349 sendNotification(final long channelId, final int notificationId)350 private boolean sendNotification(final long channelId, final int notificationId) { 351 final ChannelRecord cr = mRecommender.getChannelRecord(channelId); 352 if (cr == null) { 353 return false; 354 } 355 final Channel channel = cr.getChannel(); 356 if (DEBUG) { 357 Log.d( 358 TAG, 359 "sendNotification (channelName=" 360 + channel.getDisplayName() 361 + " notifyId=" 362 + notificationId 363 + ")"); 364 } 365 366 // TODO: Move some checking logic into TvRecommendation. 367 String inputId = Utils.getInputIdForChannel(this, channel.getId()); 368 if (TextUtils.isEmpty(inputId)) { 369 return false; 370 } 371 TvInputInfo inputInfo = mTvInputManagerHelper.getTvInputInfo(inputId); 372 if (inputInfo == null) { 373 return false; 374 } 375 376 final Program program = Utils.getCurrentProgram(this, channel.getId()); 377 if (program == null) { 378 return false; 379 } 380 final long programDurationMs = 381 program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis(); 382 long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis(); 383 final int programProgress = 384 (programDurationMs <= 0) 385 ? -1 386 : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); 387 388 // We recommend those programs that meet the condition only. 389 if (programProgress >= RECOMMENDATION_THRESHOLD_PROGRESS 390 && programLeftTimsMs <= RECOMMENDATION_THRESHOLD_LEFT_TIME_MS) { 391 return false; 392 } 393 394 // We don't trust TIS to provide us with proper sized image 395 ScaledBitmapInfo posterArtBitmapInfo = 396 BitmapUtils.decodeSampledBitmapFromUriString( 397 this, 398 program.getPosterArtUri(), 399 (int) mNotificationCardMaxWidth, 400 (int) mNotificationCardHeight); 401 if (posterArtBitmapInfo == null) { 402 Log.e(TAG, "Failed to decode poster image for " + program.getPosterArtUri()); 403 return false; 404 } 405 final Bitmap posterArtBitmap = posterArtBitmapInfo.bitmap; 406 407 channel.loadBitmap( 408 this, 409 Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, 410 mChannelLogoMaxWidth, 411 mChannelLogoMaxHeight, 412 createChannelLogoCallback(this, notificationId, channel, program, posterArtBitmap)); 413 414 if (mNotificationChannels[notificationId] == Channel.INVALID_ID) { 415 ++mCurrentNotificationCount; 416 } 417 mNotificationChannels[notificationId] = channel.getId(); 418 419 return true; 420 } 421 sendNotification( int notificationId, Bitmap channelLogo, Channel channel, Bitmap posterArtBitmap, Program program)422 private void sendNotification( 423 int notificationId, 424 Bitmap channelLogo, 425 Channel channel, 426 Bitmap posterArtBitmap, 427 Program program) { 428 final long programDurationMs = 429 program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis(); 430 long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis(); 431 final int programProgress = 432 (programDurationMs <= 0) 433 ? -1 434 : 100 - (int) (programLeftTimsMs * 100 / programDurationMs); 435 Intent intent = new Intent(Intent.ACTION_VIEW, channel.getUri()); 436 intent.putExtra(TUNE_PARAMS_RECOMMENDATION_TYPE, mRecommendationType); 437 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 438 final PendingIntent notificationIntent = PendingIntent.getActivity(this, 0, intent, 0); 439 440 // This callback will run on the main thread. 441 Bitmap largeIconBitmap = 442 (channelLogo == null) 443 ? posterArtBitmap 444 : overlayChannelLogo(channelLogo, posterArtBitmap); 445 String channelDisplayName = channel.getDisplayName(); 446 Notification notification = 447 new Notification.Builder(this) 448 .setContentIntent(notificationIntent) 449 .setContentTitle(program.getTitle()) 450 .setContentText( 451 TextUtils.isEmpty(channelDisplayName) 452 ? channel.getDisplayNumber() 453 : channelDisplayName) 454 .setContentInfo(channelDisplayName) 455 .setAutoCancel(true) 456 .setLargeIcon(largeIconBitmap) 457 .setSmallIcon(R.drawable.ic_launcher_s) 458 .setCategory(Notification.CATEGORY_RECOMMENDATION) 459 .setProgress((programProgress > 0) ? 100 : 0, programProgress, false) 460 .setSortKey(mRecommender.getChannelSortKey(channel.getId())) 461 .build(); 462 notification.color = getResources().getColor(R.color.recommendation_card_background, null); 463 if (!TextUtils.isEmpty(program.getThumbnailUri())) { 464 notification.extras.putString( 465 Notification.EXTRA_BACKGROUND_IMAGE_URI, program.getThumbnailUri()); 466 } 467 mNotificationManager.notify(NOTIFY_TAG, notificationId, notification); 468 Message msg = mHandler.obtainMessage(MSG_UPDATE_RECOMMENDATION, notificationId, 0, channel); 469 mHandler.sendMessageDelayed(msg, programDurationMs / MAX_PROGRAM_UPDATE_COUNT); 470 } 471 472 @NonNull createChannelLogoCallback( NotificationService service, final int notificationId, final Channel channel, final Program program, final Bitmap posterArtBitmap)473 private static ImageLoader.ImageLoaderCallback<NotificationService> createChannelLogoCallback( 474 NotificationService service, 475 final int notificationId, 476 final Channel channel, 477 final Program program, 478 final Bitmap posterArtBitmap) { 479 return new ImageLoader.ImageLoaderCallback<NotificationService>(service) { 480 @Override 481 public void onBitmapLoaded(NotificationService service, Bitmap channelLogo) { 482 service.sendNotification( 483 notificationId, channelLogo, channel, posterArtBitmap, program); 484 } 485 }; 486 } 487 488 private Bitmap overlayChannelLogo(Bitmap logo, Bitmap background) { 489 Bitmap result = 490 BitmapUtils.getScaledMutableBitmap(background, Integer.MAX_VALUE, mCardImageHeight); 491 Bitmap scaledLogo = 492 BitmapUtils.scaleBitmap(logo, mChannelLogoMaxWidth, mChannelLogoMaxHeight); 493 Canvas canvas; 494 try { 495 canvas = new Canvas(result); 496 } catch (Exception e) { 497 Log.w(TAG, "Failed to create Canvas", e); 498 return background; 499 } 500 canvas.drawBitmap(result, new Matrix(), null); 501 Rect rect = new Rect(); 502 int startPadding; 503 if (result.getWidth() < mCardImageMinWidth) { 504 // TODO: check the positions. 505 startPadding = mLogoPaddingStart; 506 rect.bottom = result.getHeight() - mLogoPaddingBottom; 507 rect.top = rect.bottom - scaledLogo.getHeight(); 508 } else if (result.getWidth() < mCardImageMaxWidth) { 509 startPadding = mLogoPaddingStart; 510 rect.bottom = result.getHeight() - mLogoPaddingBottom; 511 rect.top = rect.bottom - scaledLogo.getHeight(); 512 } else { 513 int marginStart = (result.getWidth() - mCardImageMaxWidth) / 2; 514 startPadding = mLogoPaddingStart + marginStart; 515 rect.bottom = result.getHeight() - mLogoPaddingBottom; 516 rect.top = rect.bottom - scaledLogo.getHeight(); 517 } 518 if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) { 519 rect.left = startPadding; 520 rect.right = startPadding + scaledLogo.getWidth(); 521 } else { 522 rect.right = result.getWidth() - startPadding; 523 rect.left = rect.right - scaledLogo.getWidth(); 524 } 525 Paint paint = new Paint(); 526 paint.setAlpha(getResources().getInteger(R.integer.notif_card_ch_logo_alpha)); 527 canvas.drawBitmap(scaledLogo, null, rect, paint); 528 return result; 529 } 530 531 private boolean isNotifiedChannel(long channelId) { 532 for (int i = 0; i < NOTIFICATION_COUNT; ++i) { 533 if (mNotificationChannels[i] == channelId) { 534 return true; 535 } 536 } 537 return false; 538 } 539 540 private int getAvailableNotificationId() { 541 for (int i = 0; i < NOTIFICATION_COUNT; ++i) { 542 if (mNotificationChannels[i] == Channel.INVALID_ID) { 543 return i; 544 } 545 } 546 return -1; 547 } 548 549 private static class NotificationHandler extends WeakHandler<NotificationService> { 550 public NotificationHandler(@NonNull Looper looper, NotificationService ref) { 551 super(looper, ref); 552 } 553 554 @Override 555 public void handleMessage(Message msg, @NonNull NotificationService notificationService) { 556 switch (msg.what) { 557 case MSG_INITIALIZE_RECOMMENDER: 558 { 559 notificationService.handleInitializeRecommender(); 560 break; 561 } 562 case MSG_SHOW_RECOMMENDATION: 563 { 564 notificationService.handleShowRecommendation(); 565 break; 566 } 567 case MSG_UPDATE_RECOMMENDATION: 568 { 569 int notificationId = msg.arg1; 570 Channel channel = ((Channel) msg.obj); 571 notificationService.handleUpdateRecommendation(notificationId, channel); 572 break; 573 } 574 case MSG_HIDE_RECOMMENDATION: 575 { 576 notificationService.handleHideRecommendation(); 577 break; 578 } 579 default: 580 { 581 super.handleMessage(msg); 582 } 583 } 584 } 585 } 586 } 587