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