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