1 /* 2 * Copyright (C) 2014 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.camera.processing; 18 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.Service; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.os.IBinder; 27 import android.os.PowerManager; 28 import android.os.PowerManager.WakeLock; 29 import android.os.Process; 30 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 31 32 import com.android.camera.app.CameraServices; 33 import com.android.camera.app.CameraServicesImpl; 34 import com.android.camera.debug.Log; 35 import com.android.camera.session.CaptureSession; 36 import com.android.camera.session.CaptureSession.ProgressListener; 37 import com.android.camera.session.CaptureSessionManager; 38 import com.android.camera.util.AndroidServices; 39 import com.android.camera2.R; 40 41 import java.util.concurrent.locks.Lock; 42 import java.util.concurrent.locks.ReentrantLock; 43 44 /** 45 * A service that processes a {@code ProcessingTask}. The service uses a fifo 46 * queue so that only one {@code ProcessingTask} is processed at a time. 47 * <p> 48 * The service is meant to be called via {@code ProcessingService.addTask}, 49 * which takes care of starting the service and enqueueing the 50 * {@code ProcessingTask} task: 51 * 52 * <pre> 53 * {@code 54 * ProcessingTask task = new MyProcessingTask(...); 55 * ProcessingService.addTask(task); 56 * } 57 * </pre> 58 */ 59 public class ProcessingService extends Service implements ProgressListener { 60 /** 61 * Class used to receive broadcast and control the service accordingly. 62 */ 63 public class ServiceController extends BroadcastReceiver { 64 @Override onReceive(Context context, Intent intent)65 public void onReceive(Context context, Intent intent) { 66 if (intent.getAction() == ACTION_PAUSE_PROCESSING_SERVICE) { 67 ProcessingService.this.pause(); 68 } else if (intent.getAction() == ACTION_RESUME_PROCESSING_SERVICE) { 69 ProcessingService.this.resume(); 70 } 71 } 72 } 73 74 private static final Log.Tag TAG = new Log.Tag("ProcessingService"); 75 private static final int THREAD_PRIORITY = Process.THREAD_PRIORITY_BACKGROUND; 76 private static final int CAMERA_NOTIFICATION_ID = 2; 77 private Notification.Builder mNotificationBuilder; 78 private NotificationManager mNotificationManager; 79 80 /** Sending this broadcast intent will cause the processing to pause. */ 81 public static final String ACTION_PAUSE_PROCESSING_SERVICE = 82 "com.android.camera.processing.PAUSE"; 83 /** 84 * Sending this broadcast intent will cause the processing to resume after 85 * it has been paused. 86 */ 87 public static final String ACTION_RESUME_PROCESSING_SERVICE = 88 "com.android.camera.processing.RESUME"; 89 90 private WakeLock mWakeLock; 91 private final ServiceController mServiceController = new ServiceController(); 92 93 /** Manages the capture session. */ 94 private CaptureSessionManager mSessionManager; 95 96 private ProcessingServiceManager mProcessingServiceManager; 97 private Thread mProcessingThread; 98 private volatile boolean mPaused = false; 99 private ProcessingTask mCurrentTask; 100 private final Lock mSuspendStatusLock = new ReentrantLock(); 101 102 @Override onCreate()103 public void onCreate() { 104 mProcessingServiceManager = ProcessingServiceManager.instance(); 105 mSessionManager = getServices().getCaptureSessionManager(); 106 107 // Keep CPU awake while allowing screen and keyboard to switch off. 108 PowerManager powerManager = AndroidServices.instance().providePowerManager(); 109 mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG.toString()); 110 mWakeLock.acquire(); 111 112 IntentFilter intentFilter = new IntentFilter(); 113 intentFilter.addAction(ACTION_PAUSE_PROCESSING_SERVICE); 114 intentFilter.addAction(ACTION_RESUME_PROCESSING_SERVICE); 115 LocalBroadcastManager.getInstance(this).registerReceiver(mServiceController, intentFilter); 116 mNotificationBuilder = createInProgressNotificationBuilder(); 117 mNotificationManager = AndroidServices.instance().provideNotificationManager(); 118 } 119 120 @Override onDestroy()121 public void onDestroy() { 122 Log.d(TAG, "Shutting down"); 123 // TODO: Cancel session in progress... 124 125 // Unlock the power manager, i.e. let power management kick in if 126 // needed. 127 if (mWakeLock.isHeld()) { 128 mWakeLock.release(); 129 } 130 LocalBroadcastManager.getInstance(this).unregisterReceiver(mServiceController); 131 stopForeground(true); 132 } 133 134 @Override onStartCommand(Intent intent, int flags, int startId)135 public int onStartCommand(Intent intent, int flags, int startId) { 136 Log.d(TAG, "Starting in foreground."); 137 138 // We need to start this service in foreground so that it's not getting 139 // killed easily when memory pressure is building up. 140 startForeground(CAMERA_NOTIFICATION_ID, mNotificationBuilder.build()); 141 142 asyncProcessAllTasksAndShutdown(); 143 144 // We want this service to continue running until it is explicitly 145 // stopped, so return sticky. 146 return START_STICKY; 147 } 148 149 @Override onBind(Intent intent)150 public IBinder onBind(Intent intent) { 151 // We don't provide binding, so return null. 152 return null; 153 } 154 pause()155 private void pause() { 156 Log.d(TAG, "Pausing"); 157 try { 158 mSuspendStatusLock.lock(); 159 mPaused = true; 160 if (mCurrentTask != null) { 161 mCurrentTask.suspend(); 162 } 163 } finally { 164 mSuspendStatusLock.unlock(); 165 } 166 } 167 resume()168 private void resume() { 169 Log.d(TAG, "Resuming"); 170 try { 171 mSuspendStatusLock.lock(); 172 mPaused = false; 173 if (mCurrentTask != null) { 174 mCurrentTask.resume(); 175 } 176 } finally { 177 mSuspendStatusLock.unlock(); 178 } 179 } 180 181 /** 182 * Starts a thread to process all tasks. When no more tasks are in the 183 * queue, it exits the thread and shuts down the service. 184 */ asyncProcessAllTasksAndShutdown()185 private void asyncProcessAllTasksAndShutdown() { 186 if (mProcessingThread != null) { 187 return; 188 } 189 mProcessingThread = new Thread("CameraProcessingThread") { 190 @Override 191 public void run() { 192 // Set the thread priority 193 android.os.Process.setThreadPriority(THREAD_PRIORITY); 194 195 ProcessingTask task; 196 while ((task = mProcessingServiceManager.popNextSession()) != null) { 197 mCurrentTask = task; 198 try { 199 mSuspendStatusLock.lock(); 200 if (mPaused) { 201 mCurrentTask.suspend(); 202 } 203 } finally { 204 mSuspendStatusLock.unlock(); 205 } 206 processAndNotify(task); 207 } 208 stopSelf(); 209 } 210 }; 211 mProcessingThread.start(); 212 } 213 214 /** 215 * Processes a {@code ProcessingTask} and updates the notification bar. 216 */ processAndNotify(ProcessingTask task)217 void processAndNotify(ProcessingTask task) { 218 if (task == null) { 219 Log.e(TAG, "Reference to ProcessingTask is null"); 220 return; 221 } 222 CaptureSession session = task.getSession(); 223 224 // TODO: Get rid of this null check. There should not be a task without 225 // a session. 226 if (session == null) { 227 // TODO: Timestamp is not required right now, refactor this to make it clearer. 228 session = mSessionManager.createNewSession(task.getName(), 0, task.getLocation()); 229 } 230 resetNotification(); 231 232 // Adding the listener also causes it to get called for the session's 233 // current status message and percent completed. 234 session.addProgressListener(this); 235 236 System.gc(); 237 Log.d(TAG, "Processing start"); 238 task.process(this, getServices(), session); 239 Log.d(TAG, "Processing done"); 240 } 241 resetNotification()242 private void resetNotification() { 243 mNotificationBuilder.setContentText("…").setProgress(100, 0, false); 244 postNotification(); 245 } 246 247 /** 248 * Returns the common camera services. 249 */ getServices()250 private CameraServices getServices() { 251 return CameraServicesImpl.instance(); 252 } 253 postNotification()254 private void postNotification() { 255 mNotificationManager.notify(CAMERA_NOTIFICATION_ID, mNotificationBuilder.build()); 256 } 257 258 /** 259 * Creates a notification to indicate that a computation is in progress. 260 */ createInProgressNotificationBuilder()261 private Notification.Builder createInProgressNotificationBuilder() { 262 return new Notification.Builder(this) 263 .setSmallIcon(R.drawable.ic_notification) 264 .setWhen(System.currentTimeMillis()) 265 .setOngoing(true) 266 .setContentTitle(this.getText(R.string.app_name)); 267 } 268 269 @Override onProgressChanged(int progress)270 public void onProgressChanged(int progress) { 271 mNotificationBuilder.setProgress(100, progress, false); 272 postNotification(); 273 } 274 275 @Override onStatusMessageChanged(int messageId)276 public void onStatusMessageChanged(int messageId) { 277 mNotificationBuilder.setContentText(messageId > 0 ? getString(messageId) : ""); 278 postNotification(); 279 } 280 } 281