blob: 5d324ca5bb5e155e65c5148557b503c40d8c9095 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.tv.dvr.recorder;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.tv.ApplicationSingletons;
import com.android.tv.InputSessionManager;
import com.android.tv.InputSessionManager.OnRecordingSessionChangeListener;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.dvr.WritableDvrDataManager;
import com.android.tv.util.Clock;
import com.android.tv.util.RecurringRunner;
/**
* DVR recording service. This service should be a foreground service and send a notification
* to users to do long-running recording task.
*
* <p>This service is waken up when there's a scheduled recording coming soon and at boot completed
* since schedules have to be loaded from databases in order to set new recording alarms, which
* might take a long time.
*/
@RequiresApi(Build.VERSION_CODES.N)
public class DvrRecordingService extends Service {
private static final String TAG = "DvrRecordingService";
private static final boolean DEBUG = false;
private static final String DVR_NOTIFICATION_CHANNEL_ID = "dvr_notification_channel";
private static final int ONGOING_NOTIFICATION_ID = 1;
@VisibleForTesting static final String EXTRA_START_FOR_RECORDING = "start_for_recording";
private static DvrRecordingService sInstance;
private NotificationChannel mNotificationChannel;
private String mContentTitle;
private String mContentTextRecording;
private String mContentTextLoading;
/**
* Starts the service in foreground.
*
* @param startForRecording {@code true} if there are upcoming recordings in
* {@link RecordingScheduler#SOON_DURATION_IN_MS} and the service is
* started in foreground for those recordings.
*/
@MainThread
static void startForegroundService(Context context, boolean startForRecording) {
if (sInstance == null) {
Intent intent = new Intent(context, DvrRecordingService.class);
intent.putExtra(EXTRA_START_FOR_RECORDING, startForRecording);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
} else {
sInstance.startForeground(startForRecording);
}
}
@MainThread
static void stopForegroundIfNotRecording() {
if (sInstance != null) {
sInstance.stopForegroundIfNotRecordingInternal();
}
}
private RecurringRunner mReaperRunner;
private InputSessionManager mSessionManager;
@VisibleForTesting boolean mIsRecording;
private boolean mForeground;
@VisibleForTesting final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener =
new OnRecordingSessionChangeListener() {
@Override
public void onRecordingSessionChange(final boolean create, final int count) {
mIsRecording = count > 0;
if (create) {
startForeground(true);
} else {
stopForegroundIfNotRecordingInternal();
}
}
};
@Override
public void onCreate() {
TvApplication.setCurrentRunningProcess(this, true);
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate();
SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG);
sInstance = this;
ApplicationSingletons singletons = TvApplication.getSingletons(this);
WritableDvrDataManager dataManager =
(WritableDvrDataManager) singletons.getDvrDataManager();
mSessionManager = singletons.getInputSessionManager();
mSessionManager.addOnRecordingSessionChangeListener(mOnRecordingSessionChangeListener);
mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1),
new ScheduledProgramReaper(dataManager, Clock.SYSTEM), null);
mReaperRunner.start();
mContentTitle = getString(R.string.dvr_notification_content_title);
mContentTextRecording = getString(R.string.dvr_notification_content_text_recording);
mContentTextLoading = getString(R.string.dvr_notification_content_text_loading);
createNotificationChannel();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")");
if (intent != null) {
startForeground(intent.getBooleanExtra(EXTRA_START_FOR_RECORDING, false));
}
return START_STICKY;
}
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy");
mReaperRunner.stop();
mSessionManager.removeRecordingSessionChangeListener(mOnRecordingSessionChangeListener);
sInstance = null;
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@VisibleForTesting
protected void stopForegroundIfNotRecordingInternal() {
if (mForeground && !mIsRecording) {
stopForeground();
}
}
private void startForeground(boolean hasUpcomingRecording) {
if (!mForeground || hasUpcomingRecording) {
// We may need to update notification for upcoming recordings.
mForeground = true;
startForegroundInternal(hasUpcomingRecording);
}
}
private void stopForeground() {
stopForegroundInternal();
mForeground = false;
}
@VisibleForTesting
protected void startForegroundInternal(boolean hasUpcomingRecording) {
// STOPSHIP: Replace the content title with real UX strings
Notification.Builder builder = new Notification.Builder(this)
.setContentTitle(mContentTitle)
.setContentText(hasUpcomingRecording ? mContentTextRecording : mContentTextLoading)
.setSmallIcon(R.drawable.ic_dvr);
Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
builder.setChannelId(DVR_NOTIFICATION_CHANNEL_ID).build() : builder.build();
startForeground(ONGOING_NOTIFICATION_ID, notification);
}
@VisibleForTesting
protected void stopForegroundInternal() {
stopForeground(true);
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// STOPSHIP: Replace the channel name with real UX strings
mNotificationChannel = new NotificationChannel(DVR_NOTIFICATION_CHANNEL_ID,
getString(R.string.dvr_notification_channel_name),
NotificationManager.IMPORTANCE_LOW);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
.createNotificationChannel(mNotificationChannel);
}
}
}