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