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 
17 package com.android.camera;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.graphics.BitmapFactory;
22 import android.location.Location;
23 import android.net.Uri;
24 import android.os.AsyncTask;
25 import android.provider.MediaStore.Video;
26 
27 import com.android.camera.app.MediaSaver;
28 import com.android.camera.data.FilmstripItemData;
29 import com.android.camera.debug.Log;
30 import com.android.camera.exif.ExifInterface;
31 
32 import java.io.File;
33 import java.io.IOException;
34 
35 /**
36  * A class implementing {@link com.android.camera.app.MediaSaver}.
37  */
38 public 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      */
MediaSaverImpl(ContentResolver contentResolver)58     public MediaSaverImpl(ContentResolver contentResolver) {
59         mContentResolver = contentResolver;
60         mMemoryUse = 0;
61     }
62 
63     @Override
isQueueFull()64     public boolean isQueueFull() {
65         return (mMemoryUse >= SAVE_TASK_MEMORY_LIMIT);
66     }
67 
68     @Override
addImage(final byte[] data, String title, long date, Location loc, int width, int height, int orientation, ExifInterface exif, OnMediaSavedListener l)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
addImage(final byte[] data, String title, long date, Location loc, int width, int height, int orientation, ExifInterface exif, OnMediaSavedListener l, String mimeType)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
addImage(final byte[] data, String title, long date, Location loc, int orientation, ExifInterface exif, OnMediaSavedListener l)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
addImage(final byte[] data, String title, Location loc, int width, int height, int orientation, ExifInterface exif, OnMediaSavedListener l)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
addVideo(String path, ContentValues values, OnMediaSavedListener l)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
setQueueListener(QueueListener l)117     public void setQueueListener(QueueListener l) {
118         mQueueListener = l;
119         if (l == null) {
120             return;
121         }
122         l.onQueueStatus(isQueueFull());
123     }
124 
onQueueFull()125     private void onQueueFull() {
126         if (mQueueListener != null) {
127             mQueueListener.onQueueStatus(true);
128         }
129     }
130 
onQueueAvailable()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 
ImageSaveTask(byte[] data, String title, long date, Location loc, int width, int height, int orientation, String mimeType, ExifInterface exif, ContentResolver resolver, OnMediaSavedListener listener)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
onPreExecute()167         protected void onPreExecute() {
168             // do nothing.
169         }
170 
171         @Override
doInBackground(Void... v)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.instance().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
onPostExecute(Uri uri)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 
VideoSaveTask(String path, ContentValues values, OnMediaSavedListener l, ContentResolver r)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
doInBackground(Void... v)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
onPostExecute(Uri uri)246         protected void onPostExecute(Uri uri) {
247             if (listener != null) {
248                 listener.onMediaSaved(uri);
249             }
250         }
251     }
252 }
253