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 */ 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