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