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