Home | History | Annotate | Download | only in recommendation
      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 
    122     public NotificationService() {
    123         mRecommendationType = TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION;
    124     }
    125 
    126     @Override
    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
    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 
    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 
    191     private void handleShowRecommendation() {
    192         if (!mRecommender.isReady()) {
    193             mShowRecommendationAfterRecommenderReady = true;
    194         } else {
    195             showRecommendation();
    196         }
    197     }
    198 
    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 
    206     private void handleHideRecommendation() {
    207         if (!mRecommender.isReady()) {
    208             mShowRecommendationAfterRecommenderReady = false;
    209         } else {
    210             hideAllRecommendation();
    211         }
    212     }
    213 
    214     @Override
    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
    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
    250     public IBinder onBind(Intent intent) {
    251         return null;
    252     }
    253 
    254     @Override
    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
    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 
    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 
    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 
    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 
    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 
    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
    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