1/*
2 * Copyright (C) 2009 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.gallery3d.provider;
18
19import android.content.ContentProvider;
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.database.MatrixCursor;
24import android.net.Uri;
25import android.os.AsyncTask;
26import android.os.Binder;
27import android.os.ParcelFileDescriptor;
28import android.provider.MediaStore.Images.ImageColumns;
29import android.util.Log;
30
31import com.android.gallery3d.app.GalleryApp;
32import com.android.gallery3d.common.AsyncTaskUtil;
33import com.android.gallery3d.common.Utils;
34import com.android.gallery3d.data.DataManager;
35import com.android.gallery3d.data.MediaItem;
36import com.android.gallery3d.data.MediaObject;
37import com.android.gallery3d.data.MtpImage;
38import com.android.gallery3d.data.Path;
39import com.android.gallery3d.picasasource.PicasaSource;
40import com.android.gallery3d.util.GalleryUtils;
41
42import java.io.FileNotFoundException;
43import java.io.IOException;
44import java.io.OutputStream;
45
46public class GalleryProvider extends ContentProvider {
47    private static final String TAG = "GalleryProvider";
48
49    public static final String AUTHORITY = "com.android.gallery3d.provider";
50    public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
51
52    public static interface PicasaColumns {
53        public static final String USER_ACCOUNT = "user_account";
54        public static final String PICASA_ID = "picasa_id";
55    }
56
57    private static final String[] SUPPORTED_PICASA_COLUMNS = {
58            PicasaColumns.USER_ACCOUNT,
59            PicasaColumns.PICASA_ID,
60            ImageColumns.DISPLAY_NAME,
61            ImageColumns.SIZE,
62            ImageColumns.MIME_TYPE,
63            ImageColumns.DATE_TAKEN,
64            ImageColumns.LATITUDE,
65            ImageColumns.LONGITUDE,
66            ImageColumns.ORIENTATION};
67
68    private DataManager mDataManager;
69    private static Uri sBaseUri;
70
71    public static String getAuthority(Context context) {
72        return context.getPackageName() + ".provider";
73    }
74
75    public static Uri getUriFor(Context context, Path path) {
76        if (sBaseUri == null) {
77            sBaseUri = Uri.parse("content://" + context.getPackageName() + ".provider");
78        }
79        return sBaseUri.buildUpon()
80                .appendEncodedPath(path.toString().substring(1)) // ignore the leading '/'
81                .build();
82    }
83
84    @Override
85    public int delete(Uri uri, String selection, String[] selectionArgs) {
86        throw new UnsupportedOperationException();
87    }
88
89    // TODO: consider concurrent access
90    @Override
91    public String getType(Uri uri) {
92        long token = Binder.clearCallingIdentity();
93        try {
94            Path path = Path.fromString(uri.getPath());
95            MediaItem item = (MediaItem) mDataManager.getMediaObject(path);
96            return item != null ? item.getMimeType() : null;
97        } finally {
98            Binder.restoreCallingIdentity(token);
99        }
100    }
101
102    @Override
103    public Uri insert(Uri uri, ContentValues values) {
104        throw new UnsupportedOperationException();
105    }
106
107    @Override
108    public boolean onCreate() {
109        GalleryApp app = (GalleryApp) getContext().getApplicationContext();
110        mDataManager = app.getDataManager();
111        return true;
112    }
113
114    // TODO: consider concurrent access
115    @Override
116    public Cursor query(Uri uri, String[] projection,
117            String selection, String[] selectionArgs, String sortOrder) {
118        long token = Binder.clearCallingIdentity();
119        try {
120            Path path = Path.fromString(uri.getPath());
121            MediaObject object = mDataManager.getMediaObject(path);
122            if (object == null) {
123                Log.w(TAG, "cannot find: " + uri);
124                return null;
125            }
126            if (PicasaSource.isPicasaImage(object)) {
127                return queryPicasaItem(object,
128                        projection, selection, selectionArgs, sortOrder);
129            } else if (object instanceof MtpImage) {
130                return queryMtpItem((MtpImage) object,
131                        projection, selection, selectionArgs, sortOrder);
132            } else {
133                    return null;
134            }
135        } finally {
136            Binder.restoreCallingIdentity(token);
137        }
138    }
139
140    private Cursor queryMtpItem(MtpImage image, String[] projection,
141            String selection, String[] selectionArgs, String sortOrder) {
142        Object[] columnValues = new Object[projection.length];
143        for (int i = 0, n = projection.length; i < n; ++i) {
144            String column = projection[i];
145            if (ImageColumns.DISPLAY_NAME.equals(column)) {
146                columnValues[i] = image.getName();
147            } else if (ImageColumns.SIZE.equals(column)){
148                columnValues[i] = image.getSize();
149            } else if (ImageColumns.MIME_TYPE.equals(column)) {
150                columnValues[i] = image.getMimeType();
151            } else if (ImageColumns.DATE_TAKEN.equals(column)) {
152                columnValues[i] = image.getDateInMs();
153            } else {
154                Log.w(TAG, "unsupported column: " + column);
155            }
156        }
157        MatrixCursor cursor = new MatrixCursor(projection);
158        cursor.addRow(columnValues);
159        return cursor;
160    }
161
162    private Cursor queryPicasaItem(MediaObject image, String[] projection,
163            String selection, String[] selectionArgs, String sortOrder) {
164        if (projection == null) projection = SUPPORTED_PICASA_COLUMNS;
165        Object[] columnValues = new Object[projection.length];
166        double latitude = PicasaSource.getLatitude(image);
167        double longitude = PicasaSource.getLongitude(image);
168        boolean isValidLatlong = GalleryUtils.isValidLocation(latitude, longitude);
169
170        for (int i = 0, n = projection.length; i < n; ++i) {
171            String column = projection[i];
172            if (PicasaColumns.USER_ACCOUNT.equals(column)) {
173                columnValues[i] = PicasaSource.getUserAccount(getContext(), image);
174            } else if (PicasaColumns.PICASA_ID.equals(column)) {
175                columnValues[i] = PicasaSource.getPicasaId(image);
176            } else if (ImageColumns.DISPLAY_NAME.equals(column)) {
177                columnValues[i] = PicasaSource.getImageTitle(image);
178            } else if (ImageColumns.SIZE.equals(column)){
179                columnValues[i] = PicasaSource.getImageSize(image);
180            } else if (ImageColumns.MIME_TYPE.equals(column)) {
181                columnValues[i] = PicasaSource.getContentType(image);
182            } else if (ImageColumns.DATE_TAKEN.equals(column)) {
183                columnValues[i] = PicasaSource.getDateTaken(image);
184            } else if (ImageColumns.LATITUDE.equals(column)) {
185                columnValues[i] = isValidLatlong ? latitude : null;
186            } else if (ImageColumns.LONGITUDE.equals(column)) {
187                columnValues[i] = isValidLatlong ? longitude : null;
188            } else if (ImageColumns.ORIENTATION.equals(column)) {
189                columnValues[i] = PicasaSource.getRotation(image);
190            } else {
191                Log.w(TAG, "unsupported column: " + column);
192            }
193        }
194        MatrixCursor cursor = new MatrixCursor(projection);
195        cursor.addRow(columnValues);
196        return cursor;
197    }
198
199    @Override
200    public ParcelFileDescriptor openFile(Uri uri, String mode)
201            throws FileNotFoundException {
202        long token = Binder.clearCallingIdentity();
203        try {
204            if (mode.contains("w")) {
205                throw new FileNotFoundException("cannot open file for write");
206            }
207            Path path = Path.fromString(uri.getPath());
208            MediaObject object = mDataManager.getMediaObject(path);
209            if (object == null) {
210                throw new FileNotFoundException(uri.toString());
211            }
212            if (PicasaSource.isPicasaImage(object)) {
213                return PicasaSource.openFile(getContext(), object, mode);
214            } else if (object instanceof MtpImage) {
215                return openPipeHelper(null,
216                        new MtpPipeDataWriter((MtpImage) object));
217            } else {
218                throw new FileNotFoundException("unspported type: " + object);
219            }
220        } finally {
221            Binder.restoreCallingIdentity(token);
222        }
223    }
224
225    @Override
226    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
227        throw new UnsupportedOperationException();
228    }
229
230    private static interface PipeDataWriter<T> {
231        void writeDataToPipe(ParcelFileDescriptor output, T args);
232    }
233
234    // Modified from ContentProvider.openPipeHelper. We are target at API LEVEL 10.
235    // But openPipeHelper is available in API LEVEL 11.
236    private static <T> ParcelFileDescriptor openPipeHelper(
237            final T args, final PipeDataWriter<T> func) throws FileNotFoundException {
238        try {
239            final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
240            AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() {
241                @Override
242                protected Object doInBackground(Object... params) {
243                    try {
244                        func.writeDataToPipe(pipe[1], args);
245                        return null;
246                    } finally {
247                        Utils.closeSilently(pipe[1]);
248                    }
249                }
250            };
251            AsyncTaskUtil.executeInParallel(task, (Object[]) null);
252            return pipe[0];
253        } catch (IOException e) {
254            throw new FileNotFoundException("failure making pipe");
255        }
256    }
257
258    private final class MtpPipeDataWriter implements PipeDataWriter<Object> {
259        private final MtpImage mImage;
260
261        private MtpPipeDataWriter(MtpImage image) {
262            mImage = image;
263        }
264
265        @Override
266        public void writeDataToPipe(ParcelFileDescriptor output, Object args) {
267            OutputStream os = null;
268            try {
269                os = new ParcelFileDescriptor.AutoCloseOutputStream(output);
270                os.write(mImage.getImageData());
271            } catch (IOException e) {
272                Log.w(TAG, "fail to download: " + mImage.toString(), e);
273            } finally {
274                Utils.closeSilently(os);
275            }
276        }
277    }
278}
279