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