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