| /* |
| * Copyright (C) 2014 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.camera.processing; |
| |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.Service; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.os.IBinder; |
| import android.os.PowerManager; |
| import android.os.PowerManager.WakeLock; |
| import android.os.Process; |
| import androidx.localbroadcastmanager.content.LocalBroadcastManager; |
| |
| import com.android.camera.app.CameraServices; |
| import com.android.camera.app.CameraServicesImpl; |
| import com.android.camera.debug.Log; |
| import com.android.camera.session.CaptureSession; |
| import com.android.camera.session.CaptureSession.ProgressListener; |
| import com.android.camera.session.CaptureSessionManager; |
| import com.android.camera.util.AndroidServices; |
| import com.android.camera2.R; |
| |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| /** |
| * A service that processes a {@code ProcessingTask}. The service uses a fifo |
| * queue so that only one {@code ProcessingTask} is processed at a time. |
| * <p> |
| * The service is meant to be called via {@code ProcessingService.addTask}, |
| * which takes care of starting the service and enqueueing the |
| * {@code ProcessingTask} task: |
| * |
| * <pre> |
| * {@code |
| * ProcessingTask task = new MyProcessingTask(...); |
| * ProcessingService.addTask(task); |
| * } |
| * </pre> |
| */ |
| public class ProcessingService extends Service implements ProgressListener { |
| /** |
| * Class used to receive broadcast and control the service accordingly. |
| */ |
| public class ServiceController extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (intent.getAction() == ACTION_PAUSE_PROCESSING_SERVICE) { |
| ProcessingService.this.pause(); |
| } else if (intent.getAction() == ACTION_RESUME_PROCESSING_SERVICE) { |
| ProcessingService.this.resume(); |
| } |
| } |
| } |
| |
| private static final Log.Tag TAG = new Log.Tag("ProcessingService"); |
| private static final int THREAD_PRIORITY = Process.THREAD_PRIORITY_BACKGROUND; |
| private static final int CAMERA_NOTIFICATION_ID = 2; |
| private Notification.Builder mNotificationBuilder; |
| private NotificationManager mNotificationManager; |
| |
| /** Sending this broadcast intent will cause the processing to pause. */ |
| public static final String ACTION_PAUSE_PROCESSING_SERVICE = |
| "com.android.camera.processing.PAUSE"; |
| /** |
| * Sending this broadcast intent will cause the processing to resume after |
| * it has been paused. |
| */ |
| public static final String ACTION_RESUME_PROCESSING_SERVICE = |
| "com.android.camera.processing.RESUME"; |
| |
| private WakeLock mWakeLock; |
| private final ServiceController mServiceController = new ServiceController(); |
| |
| /** Manages the capture session. */ |
| private CaptureSessionManager mSessionManager; |
| |
| private ProcessingServiceManager mProcessingServiceManager; |
| private Thread mProcessingThread; |
| private volatile boolean mPaused = false; |
| private ProcessingTask mCurrentTask; |
| private final Lock mSuspendStatusLock = new ReentrantLock(); |
| |
| @Override |
| public void onCreate() { |
| mProcessingServiceManager = ProcessingServiceManager.instance(); |
| mSessionManager = getServices().getCaptureSessionManager(); |
| |
| // Keep CPU awake while allowing screen and keyboard to switch off. |
| PowerManager powerManager = AndroidServices.instance().providePowerManager(); |
| mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG.toString()); |
| mWakeLock.acquire(); |
| |
| IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(ACTION_PAUSE_PROCESSING_SERVICE); |
| intentFilter.addAction(ACTION_RESUME_PROCESSING_SERVICE); |
| LocalBroadcastManager.getInstance(this).registerReceiver(mServiceController, intentFilter); |
| mNotificationBuilder = createInProgressNotificationBuilder(); |
| mNotificationManager = AndroidServices.instance().provideNotificationManager(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| Log.d(TAG, "Shutting down"); |
| // TODO: Cancel session in progress... |
| |
| // Unlock the power manager, i.e. let power management kick in if |
| // needed. |
| if (mWakeLock.isHeld()) { |
| mWakeLock.release(); |
| } |
| LocalBroadcastManager.getInstance(this).unregisterReceiver(mServiceController); |
| stopForeground(true); |
| } |
| |
| @Override |
| public int onStartCommand(Intent intent, int flags, int startId) { |
| Log.d(TAG, "Starting in foreground."); |
| |
| // We need to start this service in foreground so that it's not getting |
| // killed easily when memory pressure is building up. |
| startForeground(CAMERA_NOTIFICATION_ID, mNotificationBuilder.build()); |
| |
| asyncProcessAllTasksAndShutdown(); |
| |
| // We want this service to continue running until it is explicitly |
| // stopped, so return sticky. |
| return START_STICKY; |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| // We don't provide binding, so return null. |
| return null; |
| } |
| |
| private void pause() { |
| Log.d(TAG, "Pausing"); |
| try { |
| mSuspendStatusLock.lock(); |
| mPaused = true; |
| if (mCurrentTask != null) { |
| mCurrentTask.suspend(); |
| } |
| } finally { |
| mSuspendStatusLock.unlock(); |
| } |
| } |
| |
| private void resume() { |
| Log.d(TAG, "Resuming"); |
| try { |
| mSuspendStatusLock.lock(); |
| mPaused = false; |
| if (mCurrentTask != null) { |
| mCurrentTask.resume(); |
| } |
| } finally { |
| mSuspendStatusLock.unlock(); |
| } |
| } |
| |
| /** |
| * Starts a thread to process all tasks. When no more tasks are in the |
| * queue, it exits the thread and shuts down the service. |
| */ |
| private void asyncProcessAllTasksAndShutdown() { |
| if (mProcessingThread != null) { |
| return; |
| } |
| mProcessingThread = new Thread("CameraProcessingThread") { |
| @Override |
| public void run() { |
| // Set the thread priority |
| android.os.Process.setThreadPriority(THREAD_PRIORITY); |
| |
| ProcessingTask task; |
| while ((task = mProcessingServiceManager.popNextSession()) != null) { |
| mCurrentTask = task; |
| try { |
| mSuspendStatusLock.lock(); |
| if (mPaused) { |
| mCurrentTask.suspend(); |
| } |
| } finally { |
| mSuspendStatusLock.unlock(); |
| } |
| processAndNotify(task); |
| } |
| stopSelf(); |
| } |
| }; |
| mProcessingThread.start(); |
| } |
| |
| /** |
| * Processes a {@code ProcessingTask} and updates the notification bar. |
| */ |
| void processAndNotify(ProcessingTask task) { |
| if (task == null) { |
| Log.e(TAG, "Reference to ProcessingTask is null"); |
| return; |
| } |
| CaptureSession session = task.getSession(); |
| |
| // TODO: Get rid of this null check. There should not be a task without |
| // a session. |
| if (session == null) { |
| // TODO: Timestamp is not required right now, refactor this to make it clearer. |
| session = mSessionManager.createNewSession(task.getName(), 0, task.getLocation()); |
| } |
| resetNotification(); |
| |
| // Adding the listener also causes it to get called for the session's |
| // current status message and percent completed. |
| session.addProgressListener(this); |
| |
| System.gc(); |
| Log.d(TAG, "Processing start"); |
| task.process(this, getServices(), session); |
| Log.d(TAG, "Processing done"); |
| } |
| |
| private void resetNotification() { |
| mNotificationBuilder.setContentText("…").setProgress(100, 0, false); |
| postNotification(); |
| } |
| |
| /** |
| * Returns the common camera services. |
| */ |
| private CameraServices getServices() { |
| return CameraServicesImpl.instance(); |
| } |
| |
| private void postNotification() { |
| mNotificationManager.notify(CAMERA_NOTIFICATION_ID, mNotificationBuilder.build()); |
| } |
| |
| /** |
| * Creates a notification to indicate that a computation is in progress. |
| */ |
| private Notification.Builder createInProgressNotificationBuilder() { |
| return new Notification.Builder(this) |
| .setSmallIcon(R.drawable.ic_notification) |
| .setWhen(System.currentTimeMillis()) |
| .setOngoing(true) |
| .setContentTitle(this.getText(R.string.app_name)); |
| } |
| |
| @Override |
| public void onProgressChanged(int progress) { |
| mNotificationBuilder.setProgress(100, progress, false); |
| postNotification(); |
| } |
| |
| @Override |
| public void onStatusMessageChanged(int messageId) { |
| mNotificationBuilder.setContentText(messageId > 0 ? getString(messageId) : ""); |
| postNotification(); |
| } |
| } |