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