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;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.graphics.BitmapFactory;
22import android.location.Location;
23import android.net.Uri;
24import android.os.AsyncTask;
25import android.provider.MediaStore.Video;
26
27import com.android.camera.app.MediaSaver;
28import com.android.camera.data.FilmstripItemData;
29import com.android.camera.debug.Log;
30import com.android.camera.exif.ExifInterface;
31
32import java.io.File;
33import java.io.IOException;
34
35/**
36 * A class implementing {@link com.android.camera.app.MediaSaver}.
37 */
38public class MediaSaverImpl implements MediaSaver {
39    private static final Log.Tag TAG = new Log.Tag("MediaSaverImpl");
40    private static final String VIDEO_BASE_URI = "content://media/external/video/media";
41
42    /** The memory limit for unsaved image is 30MB. */
43    // TODO: Revert this back to 20 MB when CaptureSession API supports saving
44    // bursts.
45    private static final int SAVE_TASK_MEMORY_LIMIT = 30 * 1024 * 1024;
46
47    private final ContentResolver mContentResolver;
48
49    /** Memory used by the total queued save request, in bytes. */
50    private long mMemoryUse;
51
52    private QueueListener mQueueListener;
53
54    /**
55     * @param contentResolver The {@link android.content.ContentResolver} to be
56     *                 updated.
57     */
58    public MediaSaverImpl(ContentResolver contentResolver) {
59        mContentResolver = contentResolver;
60        mMemoryUse = 0;
61    }
62
63    @Override
64    public boolean isQueueFull() {
65        return (mMemoryUse >= SAVE_TASK_MEMORY_LIMIT);
66    }
67
68    @Override
69    public void addImage(final byte[] data, String title, long date, Location loc, int width,
70            int height, int orientation, ExifInterface exif, OnMediaSavedListener l) {
71        addImage(data, title, date, loc, width, height, orientation, exif, l,
72                FilmstripItemData.MIME_TYPE_JPEG);
73    }
74
75    @Override
76    public void addImage(final byte[] data, String title, long date, Location loc, int width,
77            int height, int orientation, ExifInterface exif, OnMediaSavedListener l,
78            String mimeType) {
79        if (isQueueFull()) {
80            Log.e(TAG, "Cannot add image when the queue is full");
81            return;
82        }
83        ImageSaveTask t = new ImageSaveTask(data, title, date,
84                (loc == null) ? null : new Location(loc),
85                width, height, orientation, mimeType, exif, mContentResolver, l);
86
87        mMemoryUse += data.length;
88        if (isQueueFull()) {
89            onQueueFull();
90        }
91        t.execute();
92    }
93
94    @Override
95    public void addImage(final byte[] data, String title, long date, Location loc, int orientation,
96            ExifInterface exif, OnMediaSavedListener l) {
97        // When dimensions are unknown, pass 0 as width and height,
98        // and decode image for width and height later in a background thread
99        addImage(data, title, date, loc, 0, 0, orientation, exif, l,
100                FilmstripItemData.MIME_TYPE_JPEG);
101    }
102    @Override
103    public void addImage(final byte[] data, String title, Location loc, int width, int height,
104            int orientation, ExifInterface exif, OnMediaSavedListener l) {
105        addImage(data, title, System.currentTimeMillis(), loc, width, height, orientation, exif, l,
106                FilmstripItemData.MIME_TYPE_JPEG);
107    }
108
109    @Override
110    public void addVideo(String path, ContentValues values, OnMediaSavedListener l) {
111        // We don't set a queue limit for video saving because the file
112        // is already in the storage. Only updating the database.
113        new VideoSaveTask(path, values, l, mContentResolver).execute();
114    }
115
116    @Override
117    public void setQueueListener(QueueListener l) {
118        mQueueListener = l;
119        if (l == null) {
120            return;
121        }
122        l.onQueueStatus(isQueueFull());
123    }
124
125    private void onQueueFull() {
126        if (mQueueListener != null) {
127            mQueueListener.onQueueStatus(true);
128        }
129    }
130
131    private void onQueueAvailable() {
132        if (mQueueListener != null) {
133            mQueueListener.onQueueStatus(false);
134        }
135    }
136
137    private class ImageSaveTask extends AsyncTask <Void, Void, Uri> {
138        private final byte[] data;
139        private final String title;
140        private final long date;
141        private final Location loc;
142        private int width, height;
143        private final int orientation;
144        private final String mimeType;
145        private final ExifInterface exif;
146        private final ContentResolver resolver;
147        private final OnMediaSavedListener listener;
148
149        public ImageSaveTask(byte[] data, String title, long date, Location loc,
150                             int width, int height, int orientation, String mimeType,
151                             ExifInterface exif, ContentResolver resolver,
152                             OnMediaSavedListener listener) {
153            this.data = data;
154            this.title = title;
155            this.date = date;
156            this.loc = loc;
157            this.width = width;
158            this.height = height;
159            this.orientation = orientation;
160            this.mimeType = mimeType;
161            this.exif = exif;
162            this.resolver = resolver;
163            this.listener = listener;
164        }
165
166        @Override
167        protected void onPreExecute() {
168            // do nothing.
169        }
170
171        @Override
172        protected Uri doInBackground(Void... v) {
173            if (width == 0 || height == 0) {
174                // Decode bounds
175                BitmapFactory.Options options = new BitmapFactory.Options();
176                options.inJustDecodeBounds = true;
177                BitmapFactory.decodeByteArray(data, 0, data.length, options);
178                width = options.outWidth;
179                height = options.outHeight;
180            }
181            try {
182                return Storage.addImage(
183                        resolver, title, date, loc, orientation, exif, data, width, height,
184                        mimeType);
185            } catch (IOException e) {
186                Log.e(TAG, "Failed to write data", e);
187                return null;
188            }
189        }
190
191        @Override
192        protected void onPostExecute(Uri uri) {
193            if (listener != null) {
194                listener.onMediaSaved(uri);
195            }
196            boolean previouslyFull = isQueueFull();
197            mMemoryUse -= data.length;
198            if (isQueueFull() != previouslyFull) {
199                onQueueAvailable();
200            }
201        }
202    }
203
204    private class VideoSaveTask extends AsyncTask <Void, Void, Uri> {
205        private String path;
206        private final ContentValues values;
207        private final OnMediaSavedListener listener;
208        private final ContentResolver resolver;
209
210        public VideoSaveTask(String path, ContentValues values, OnMediaSavedListener l,
211                             ContentResolver r) {
212            this.path = path;
213            this.values = new ContentValues(values);
214            this.listener = l;
215            this.resolver = r;
216        }
217
218        @Override
219        protected Uri doInBackground(Void... v) {
220            Uri uri = null;
221            try {
222                Uri videoTable = Uri.parse(VIDEO_BASE_URI);
223                uri = resolver.insert(videoTable, values);
224
225                // Rename the video file to the final name. This avoids other
226                // apps reading incomplete data.  We need to do it after we are
227                // certain that the previous insert to MediaProvider is completed.
228                String finalName = values.getAsString(Video.Media.DATA);
229                File finalFile = new File(finalName);
230                if (new File(path).renameTo(finalFile)) {
231                    path = finalName;
232                }
233                resolver.update(uri, values, null, null);
234            } catch (Exception e) {
235                // We failed to insert into the database. This can happen if
236                // the SD card is unmounted.
237                Log.e(TAG, "failed to add video to media store", e);
238                uri = null;
239            } finally {
240                Log.v(TAG, "Current video URI: " + uri);
241            }
242            return uri;
243        }
244
245        @Override
246        protected void onPostExecute(Uri uri) {
247            if (listener != null) {
248                listener.onMediaSaved(uri);
249            }
250        }
251    }
252}
253