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