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