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.dvr.recorder; 18 19 import android.app.Notification; 20 import android.app.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.app.Service; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.os.Build; 26 import android.os.IBinder; 27 import android.support.annotation.MainThread; 28 import android.support.annotation.Nullable; 29 import android.support.annotation.RequiresApi; 30 import android.support.annotation.VisibleForTesting; 31 import android.util.Log; 32 import com.android.tv.InputSessionManager; 33 import com.android.tv.InputSessionManager.OnRecordingSessionChangeListener; 34 import com.android.tv.R; 35 import com.android.tv.Starter; 36 import com.android.tv.TvSingletons; 37 import com.android.tv.common.SoftPreconditions; 38 import com.android.tv.common.feature.CommonFeatures; 39 import com.android.tv.common.util.Clock; 40 import com.android.tv.dvr.WritableDvrDataManager; 41 import com.android.tv.util.RecurringRunner; 42 43 /** 44 * DVR recording service. This service should be a foreground service and send a notification to 45 * users to do long-running recording task. 46 * 47 * <p>This service is waken up when there's a scheduled recording coming soon and at boot completed 48 * since schedules have to be loaded from databases in order to set new recording alarms, which 49 * might take a long time. 50 */ 51 @RequiresApi(Build.VERSION_CODES.N) 52 public class DvrRecordingService extends Service { 53 private static final String TAG = "DvrRecordingService"; 54 private static final boolean DEBUG = false; 55 56 private static final String DVR_NOTIFICATION_CHANNEL_ID = "dvr_notification_channel"; 57 private static final int ONGOING_NOTIFICATION_ID = 1; 58 @VisibleForTesting static final String EXTRA_START_FOR_RECORDING = "start_for_recording"; 59 60 private static DvrRecordingService sInstance; 61 private NotificationChannel mNotificationChannel; 62 private String mContentTitle; 63 private String mContentTextRecording; 64 private String mContentTextLoading; 65 66 /** 67 * Starts the service in foreground. 68 * 69 * @param startForRecording {@code true} if there are upcoming recordings in {@link 70 * RecordingScheduler#SOON_DURATION_IN_MS} and the service is started in foreground for 71 * those recordings. 72 */ 73 @MainThread startForegroundService(Context context, boolean startForRecording)74 static void startForegroundService(Context context, boolean startForRecording) { 75 if (sInstance == null) { 76 Intent intent = new Intent(context, DvrRecordingService.class); 77 intent.putExtra(EXTRA_START_FOR_RECORDING, startForRecording); 78 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 79 context.startForegroundService(intent); 80 } else { 81 context.startService(intent); 82 } 83 } else { 84 sInstance.startForeground(startForRecording); 85 } 86 } 87 88 @MainThread stopForegroundIfNotRecording()89 static void stopForegroundIfNotRecording() { 90 if (sInstance != null) { 91 sInstance.stopForegroundIfNotRecordingInternal(); 92 } 93 } 94 95 private RecurringRunner mReaperRunner; 96 private InputSessionManager mSessionManager; 97 98 @VisibleForTesting boolean mIsRecording; 99 private boolean mForeground; 100 101 @VisibleForTesting 102 final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener = 103 new OnRecordingSessionChangeListener() { 104 @Override 105 public void onRecordingSessionChange(final boolean create, final int count) { 106 mIsRecording = count > 0; 107 if (create) { 108 startForeground(true); 109 } else { 110 stopForegroundIfNotRecordingInternal(); 111 } 112 } 113 }; 114 115 @Override onCreate()116 public void onCreate() { 117 Starter.start(this); 118 if (DEBUG) Log.d(TAG, "onCreate"); 119 super.onCreate(); 120 SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); 121 sInstance = this; 122 TvSingletons singletons = TvSingletons.getSingletons(this); 123 WritableDvrDataManager dataManager = 124 (WritableDvrDataManager) singletons.getDvrDataManager(); 125 mSessionManager = singletons.getInputSessionManager(); 126 mSessionManager.addOnRecordingSessionChangeListener(mOnRecordingSessionChangeListener); 127 mReaperRunner = 128 new RecurringRunner( 129 this, 130 java.util.concurrent.TimeUnit.DAYS.toMillis(1), 131 new ScheduledProgramReaper(dataManager, Clock.SYSTEM), 132 null); 133 mReaperRunner.start(); 134 mContentTitle = getString(R.string.dvr_notification_content_title); 135 mContentTextRecording = getString(R.string.dvr_notification_content_text_recording); 136 mContentTextLoading = getString(R.string.dvr_notification_content_text_loading); 137 createNotificationChannel(); 138 } 139 140 @Override onStartCommand(Intent intent, int flags, int startId)141 public int onStartCommand(Intent intent, int flags, int startId) { 142 if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")"); 143 if (intent != null) { 144 startForeground(intent.getBooleanExtra(EXTRA_START_FOR_RECORDING, false)); 145 } 146 return START_STICKY; 147 } 148 149 @Override onDestroy()150 public void onDestroy() { 151 if (DEBUG) Log.d(TAG, "onDestroy"); 152 mReaperRunner.stop(); 153 mSessionManager.removeRecordingSessionChangeListener(mOnRecordingSessionChangeListener); 154 sInstance = null; 155 super.onDestroy(); 156 } 157 158 @Nullable 159 @Override onBind(Intent intent)160 public IBinder onBind(Intent intent) { 161 return null; 162 } 163 164 @VisibleForTesting stopForegroundIfNotRecordingInternal()165 protected void stopForegroundIfNotRecordingInternal() { 166 if (mForeground && !mIsRecording) { 167 stopForeground(); 168 } 169 } 170 startForeground(boolean hasUpcomingRecording)171 private void startForeground(boolean hasUpcomingRecording) { 172 if (!mForeground || hasUpcomingRecording) { 173 // We may need to update notification for upcoming recordings. 174 mForeground = true; 175 startForegroundInternal(hasUpcomingRecording); 176 } 177 } 178 stopForeground()179 private void stopForeground() { 180 stopForegroundInternal(); 181 mForeground = false; 182 } 183 184 @VisibleForTesting startForegroundInternal(boolean hasUpcomingRecording)185 protected void startForegroundInternal(boolean hasUpcomingRecording) { 186 Notification.Builder builder = 187 new Notification.Builder(this) 188 .setContentTitle(mContentTitle) 189 .setContentText( 190 hasUpcomingRecording ? mContentTextRecording : mContentTextLoading) 191 .setSmallIcon(R.drawable.ic_dvr); 192 Notification notification = 193 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O 194 ? builder.setChannelId(DVR_NOTIFICATION_CHANNEL_ID).build() 195 : builder.build(); 196 startForeground(ONGOING_NOTIFICATION_ID, notification); 197 } 198 199 @VisibleForTesting stopForegroundInternal()200 protected void stopForegroundInternal() { 201 stopForeground(true); 202 } 203 createNotificationChannel()204 private void createNotificationChannel() { 205 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 206 mNotificationChannel = 207 new NotificationChannel( 208 DVR_NOTIFICATION_CHANNEL_ID, 209 getString(R.string.dvr_notification_channel_name), 210 NotificationManager.IMPORTANCE_LOW); 211 ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)) 212 .createNotificationChannel(mNotificationChannel); 213 } 214 } 215 } 216