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.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 
    124     public NotificationService() {
    125         mRecommendationType = TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION;
    126     }
    127 
    128     @Override
    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
    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 
    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 
    199     private void handleShowRecommendation() {
    200         if (!mRecommender.isReady()) {
    201             mShowRecommendationAfterRecommenderReady = true;
    202         } else {
    203             showRecommendation();
    204         }
    205     }
    206 
    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 
    214     private void handleHideRecommendation() {
    215         if (!mRecommender.isReady()) {
    216             mShowRecommendationAfterRecommenderReady = false;
    217         } else {
    218             hideAllRecommendation();
    219         }
    220     }
    221 
    222     @Override
    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
    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
    258     public IBinder onBind(Intent intent) {
    259         return null;
    260     }
    261 
    262     @Override
    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
    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 
    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 
    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 
    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 
    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 
    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
    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