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 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 
    119     public NotificationService() {
    120         mRecommendationType = TYPE_ROUTINE_WATCH_AND_FAVORITE_CHANNEL_RECOMMENDATION;
    121     }
    122 
    123     @Override
    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
    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 
    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 
    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 
    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 
    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
    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
    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
    254     public IBinder onBind(Intent intent) {
    255         return null;
    256     }
    257 
    258     @Override
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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
    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