• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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