1/*
2 * Copyright (C) 2013 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
17package com.android.camera.session;
18
19import android.graphics.Bitmap;
20import android.location.Location;
21import android.net.Uri;
22
23import com.android.camera.async.MainThread;
24import com.android.camera.debug.Log;
25
26import java.io.File;
27import java.io.IOException;
28import java.util.HashMap;
29import java.util.LinkedList;
30import java.util.Map;
31
32/**
33 * Implementation for the {@link CaptureSessionManager}.
34 * <p>
35 * Basic usage:
36 * <ul>
37 * <li>Create a new capture session.</li>
38 * <li>Pass it around to anywhere where the status of a session needs to be
39 * updated.</li>
40 * <li>If this is a longer operation, use one of the start* methods to indicate
41 * that processing of this session has started. The Camera app right now will
42 * use this to add a new item to the filmstrip and indicate the current
43 * progress.</li>
44 * <li>If the final result is already available and no processing is required,
45 * store the final image using saveAndFinish</li>
46 * <li>For longer operations, update the thumbnail and status message using the
47 * provided methods.</li>
48 * <li>For longer operations, update the thumbnail and status message using the
49 * provided methods.</li>
50 * <li>Once processing is done, the final image can be saved using saveAndFinish
51 * </li>
52 * </ul>
53 * </p>
54 * It's OK to call saveAndFinish either before or after the session has been
55 * started.
56 * <p>
57 * If startSession is called after the session has been finished, it will be
58 * treated as a no-op.
59 * </p>
60 */
61public class CaptureSessionManagerImpl implements CaptureSessionManager {
62
63    private final class SessionNotifierImpl implements SessionNotifier {
64        /**
65         * Notifies all task listeners that the task with the given URI has been
66         * queued.
67         */
68        @Override
69        public void notifyTaskQueued(final Uri uri) {
70            mMainHandler.execute(new Runnable() {
71                @Override
72                public void run() {
73                    synchronized (mTaskListeners) {
74                        for (SessionListener listener : mTaskListeners) {
75                            listener.onSessionQueued(uri);
76                        }
77                    }
78                }
79            });
80        }
81
82        /**
83         * Notifies all task listeners that the task with the given URI has been
84         * finished.
85         */
86        @Override
87        public void notifyTaskDone(final Uri uri) {
88            mMainHandler.execute(new Runnable() {
89                @Override
90                public void run() {
91                    synchronized (mTaskListeners) {
92                        for (SessionListener listener : mTaskListeners) {
93                            listener.onSessionDone(uri);
94                        }
95                    }
96                    finalizeSession(uri);
97                }
98            });
99        }
100
101        /**
102         * Notifies all task listeners that the task with the given URI has been
103         * failed to process.
104         */
105        @Override
106        public void notifyTaskFailed(final Uri uri, final int failureMessageId,
107                final boolean removeFromFilmstrip) {
108            mMainHandler.execute(new Runnable() {
109                @Override
110                public void run() {
111                    synchronized (mTaskListeners) {
112                        for (SessionListener listener : mTaskListeners) {
113                            listener.onSessionFailed(uri, failureMessageId, removeFromFilmstrip);
114                        }
115                    }
116                    finalizeSession(uri);
117                }
118            });
119        }
120
121        @Override
122        public void notifyTaskCanceled(final Uri uri) {
123            mMainHandler.execute(new Runnable() {
124                @Override
125                public void run() {
126                    synchronized (mTaskListeners) {
127                        for (SessionListener listener : mTaskListeners) {
128                            listener.onSessionCanceled(uri);
129                        }
130                    }
131                    finalizeSession(uri);
132                }
133            });
134        }
135
136        /**
137         * Notifies all task listeners that the task with the given URI has
138         * progressed to the given state.
139         */
140        @Override
141        public void notifyTaskProgress(final Uri uri, final int progressPercent) {
142            mMainHandler.execute(new Runnable() {
143                @Override
144                public void run() {
145                    synchronized (mTaskListeners) {
146                        for (SessionListener listener : mTaskListeners) {
147                            listener.onSessionProgress(uri, progressPercent);
148                        }
149                    }
150                }
151            });
152        }
153
154        /**
155         * Notifies all task listeners that the task with the given URI has
156         * changed its progress message.
157         */
158        @Override
159        public void notifyTaskProgressText(final Uri uri, final int messageId) {
160            mMainHandler.execute(new Runnable() {
161                @Override
162                public void run() {
163                    synchronized (mTaskListeners) {
164                        for (SessionListener listener : mTaskListeners) {
165                            listener.onSessionProgressText(uri, messageId);
166                        }
167                    }
168                }
169            });
170        }
171
172        /**
173         * Notifies all task listeners that the media associated with the task
174         * has been updated.
175         */
176        @Override
177        public void notifySessionUpdated(final Uri uri) {
178            mMainHandler.execute(new Runnable() {
179                @Override
180                public void run() {
181                    synchronized (mTaskListeners) {
182                        for (SessionListener listener : mTaskListeners) {
183                            listener.onSessionUpdated(uri);
184                        }
185                    }
186                }
187            });
188        }
189
190        /**
191         * Notifies all task listeners that the task with the given URI has
192         * updated its media.
193         *
194         * @param indicator the bitmap that should be used for the capture
195         *            indicator
196         * @param rotationDegrees the rotation of the updated preview
197         */
198        @Override
199        public void notifySessionCaptureIndicatorAvailable(final Bitmap indicator, final int
200                rotationDegrees) {
201            mMainHandler.execute(new Runnable() {
202                @Override
203                public void run() {
204                    synchronized (mTaskListeners) {
205                        for (SessionListener listener : mTaskListeners) {
206                            listener.onSessionCaptureIndicatorUpdate(indicator, rotationDegrees);
207                        }
208                    }
209                }
210            });
211        }
212
213        @Override
214        public void notifySessionThumbnailAvailable(final Bitmap thumbnail) {
215            mMainHandler.execute(new Runnable() {
216                @Override
217                public void run() {
218                    synchronized (mTaskListeners) {
219                        for (SessionListener listener : mTaskListeners) {
220                            listener.onSessionThumbnailUpdate(thumbnail);
221                        }
222                    }
223                }
224            });
225        }
226
227        @Override
228        public void notifySessionPictureDataAvailable(
229                final byte[] pictureData, final int orientation) {
230            mMainHandler.execute(new Runnable() {
231                @Override
232                public void run() {
233                    synchronized (mTaskListeners) {
234                        for (SessionListener listener : mTaskListeners) {
235                            listener.onSessionPictureDataUpdate(pictureData, orientation);
236                        }
237                    }
238                }
239            });
240        }
241    }
242
243    private static final Log.Tag TAG = new Log.Tag("CaptureSessMgrImpl");
244
245    /** Sessions in progress, keyed by URI. */
246    private final Map<String, CaptureSession> mSessions;
247    private final SessionNotifier mSessionNotifier;
248    private final CaptureSessionFactory mSessionFactory;
249    private final SessionStorageManager mSessionStorageManager;
250    /** Used to fire events to the session listeners from the main thread. */
251    private final MainThread mMainHandler;
252
253    /** Failed session messages. Uri -> message ID. */
254    private final HashMap<Uri, Integer> mFailedSessionMessages = new HashMap<>();
255
256    /** Listeners interested in task update events. */
257    private final LinkedList<SessionListener> mTaskListeners = new LinkedList<SessionListener>();
258
259    /**
260     * Initializes a new {@link CaptureSessionManager} implementation.
261     *
262     * @param sessionFactory used to create new capture session objects.
263     * @param sessionStorageManager used to tell modules where to store
264     *            temporary session data
265     * @param mainHandler the main handler which listener callback is executed on.
266     */
267    public CaptureSessionManagerImpl(
268            CaptureSessionFactory sessionFactory,
269            SessionStorageManager sessionStorageManager,
270            MainThread mainHandler) {
271        mSessionFactory = sessionFactory;
272        mSessions = new HashMap<>();
273        mSessionNotifier = new SessionNotifierImpl();
274        mSessionStorageManager = sessionStorageManager;
275        mMainHandler = mainHandler;
276    }
277
278    @Override
279    public CaptureSession createNewSession(String title, long sessionStartMillis, Location location) {
280        return mSessionFactory.createNewSession(this, mSessionNotifier, title, sessionStartMillis,
281                location);
282    }
283
284    @Override
285    public void putSession(Uri sessionUri, CaptureSession session) {
286        synchronized (mSessions) {
287            mSessions.put(sessionUri.toString(), session);
288        }
289    }
290
291    @Override
292    public CaptureSession getSession(Uri sessionUri) {
293        synchronized (mSessions) {
294            return mSessions.get(sessionUri.toString());
295        }
296    }
297
298    @Override
299    public CaptureSession removeSession(Uri sessionUri) {
300        synchronized (mSessions) {
301            return mSessions.remove(sessionUri.toString());
302        }
303    }
304
305    @Override
306    public void addSessionListener(SessionListener listener) {
307        synchronized (mTaskListeners) {
308            mTaskListeners.add(listener);
309        }
310    }
311
312    @Override
313    public void removeSessionListener(SessionListener listener) {
314        synchronized (mTaskListeners) {
315            mTaskListeners.remove(listener);
316        }
317    }
318
319    @Override
320    public File getSessionDirectory(String subDirectory) throws IOException {
321        return mSessionStorageManager.getSessionDirectory(subDirectory);
322    }
323
324    @Override
325    public boolean hasErrorMessage(Uri uri) {
326        return mFailedSessionMessages.containsKey(uri);
327    }
328
329    @Override
330    public int getErrorMessageId(Uri uri) {
331        Integer messageId = mFailedSessionMessages.get(uri);
332        if (messageId != null) {
333            return messageId;
334        }
335        return -1;
336    }
337
338    @Override
339    public void removeErrorMessage(Uri uri) {
340        mFailedSessionMessages.remove(uri);
341    }
342
343    @Override
344    public void putErrorMessage(Uri uri, int failureMessageId) {
345        mFailedSessionMessages.put(uri, failureMessageId);
346    }
347
348    @Override
349    public void fillTemporarySession(final SessionListener listener) {
350        mMainHandler.execute(new Runnable() {
351            @Override
352            public void run() {
353                synchronized (mSessions) {
354                    for (String sessionUri : mSessions.keySet()) {
355                        CaptureSession session = mSessions.get(sessionUri);
356                        listener.onSessionQueued(session.getUri());
357                        listener.onSessionProgress(session.getUri(), session.getProgress());
358                        listener.onSessionProgressText(session.getUri(),
359                                session.getProgressMessageId());
360                    }
361                }
362            }
363        });
364    }
365
366    /**
367     * When done with a session, remove it from internal map and finalize it.
368     *
369     * @param uri Uri of the session to remove and finalize
370     */
371    private void finalizeSession(Uri uri) {
372        CaptureSession session;
373        synchronized (mSessions) {
374            session = removeSession(uri);
375        }
376        if (session != null) {
377            session.finalizeSession();
378        }
379    }
380}
381