/* * 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. * *

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); } } }