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