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.data;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.graphics.Bitmap;
22 import android.graphics.Point;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.net.Uri;
25 import android.provider.MediaStore;
26 import android.view.View;
27 import android.widget.ImageView;
28 
29 import com.android.camera.Storage;
30 import com.android.camera.data.FilmstripItemAttributes.Attributes;
31 import com.android.camera.debug.Log;
32 import com.android.camera.util.CameraUtil;
33 import com.android.camera.util.Size;
34 import com.android.camera2.R;
35 import com.bumptech.glide.DrawableRequestBuilder;
36 import com.bumptech.glide.GenericRequestBuilder;
37 import com.bumptech.glide.Glide;
38 import com.bumptech.glide.load.resource.drawable.GlideDrawable;
39 import com.google.common.base.Optional;
40 
41 import java.io.FileInputStream;
42 import java.io.FileNotFoundException;
43 
44 import javax.annotation.Nonnull;
45 
46 /**
47  * Backing data for a single photo displayed in the filmstrip.
48  */
49 public class PhotoItem extends FilmstripItemBase<FilmstripItemData> {
50     private static final Log.Tag TAG = new Log.Tag("PhotoItem");
51     private static final int MAX_PEEK_BITMAP_PIXELS = 1600000; // 1.6 * 4 MBs.
52 
53     private static final FilmstripItemAttributes PHOTO_ITEM_ATTRIBUTES =
54           new FilmstripItemAttributes.Builder()
55               .with(Attributes.CAN_SHARE)
56               .with(Attributes.CAN_EDIT)
57               .with(Attributes.CAN_DELETE)
58               .with(Attributes.CAN_SWIPE_AWAY)
59               .with(Attributes.CAN_ZOOM_IN_PLACE)
60               .with(Attributes.HAS_DETAILED_CAPTURE_INFO)
61               .with(Attributes.IS_IMAGE)
62               .build();
63 
64     private final PhotoItemFactory mPhotoItemFactory;
65 
66     private Optional<Bitmap> mSessionPlaceholderBitmap = Optional.absent();
67 
PhotoItem(Context context, GlideFilmstripManager manager, FilmstripItemData data, PhotoItemFactory photoItemFactory)68     public PhotoItem(Context context, GlideFilmstripManager manager, FilmstripItemData data,
69           PhotoItemFactory photoItemFactory) {
70         super(context, manager, data, PHOTO_ITEM_ATTRIBUTES);
71         mPhotoItemFactory = photoItemFactory;
72     }
73 
74     /**
75      * A bitmap that if present, is a high resolution bitmap from a temporary
76      * session, that should be used as a placeholder in place of placeholder/
77      * thumbnail loading.
78      *
79      * @param sessionPlaceholderBitmap a Bitmap to set as a placeholder
80      */
setSessionPlaceholderBitmap(Optional<Bitmap> sessionPlaceholderBitmap)81     public void setSessionPlaceholderBitmap(Optional<Bitmap> sessionPlaceholderBitmap) {
82         mSessionPlaceholderBitmap = sessionPlaceholderBitmap;
83     }
84 
85     @Override
toString()86     public String toString() {
87         return "PhotoItem: " + mData.toString();
88     }
89 
90     @Override
delete()91     public boolean delete() {
92         ContentResolver cr = mContext.getContentResolver();
93         cr.delete(PhotoDataQuery.CONTENT_URI,
94               MediaStore.Images.ImageColumns._ID + "=" + mData.getContentId(), null);
95         return super.delete();
96     }
97 
98     @Override
getMediaDetails()99     public Optional<MediaDetails> getMediaDetails() {
100         Optional<MediaDetails> optionalDetails = super.getMediaDetails();
101         if (optionalDetails.isPresent()) {
102             MediaDetails mediaDetails = optionalDetails.get();
103             MediaDetails.extractExifInfo(mediaDetails, mData.getFilePath());
104             mediaDetails.addDetail(MediaDetails.INDEX_ORIENTATION, mData.getOrientation());
105         }
106         return optionalDetails;
107     }
108 
109     @Override
refresh()110     public FilmstripItem refresh() {
111         // TODO: Consider simply replacing the data inline
112         return mPhotoItemFactory.get(mData.getUri());
113     }
114 
115     @Override
getView(Optional<View> optionalView, LocalFilmstripDataAdapter adapter, boolean isInProgress, VideoClickedCallback videoClickedCallback)116     public View getView(Optional<View> optionalView, LocalFilmstripDataAdapter adapter,
117           boolean isInProgress, VideoClickedCallback videoClickedCallback) {
118         ImageView imageView;
119 
120         if (optionalView.isPresent()) {
121             imageView = (ImageView) optionalView.get();
122         } else {
123             imageView = new ImageView(mContext);
124             imageView.setTag(R.id.mediadata_tag_viewtype, getItemViewType().ordinal());
125         }
126 
127         fillImageView(imageView);
128 
129         return imageView;
130     }
131 
fillImageView(final ImageView imageView)132     protected void fillImageView(final ImageView imageView) {
133         renderTinySize(mData.getUri()).into(imageView);
134 
135         // TODO consider having metadata have a "get description" string
136         // or some other way of selecting rendering details based on metadata.
137         int stringId = R.string.photo_date_content_description;
138         if (getMetadata().isPanorama() ||
139               getMetadata().isPanorama360()) {
140             stringId = R.string.panorama_date_content_description;
141         } else if (getMetadata().isUsePanoramaViewer()) {
142             // assume it's a PhotoSphere
143             stringId = R.string.photosphere_date_content_description;
144         } else if (this.getMetadata().isHasRgbzData()) {
145             stringId = R.string.refocus_date_content_description;
146         }
147 
148         imageView.setContentDescription(mContext.getResources().getString(
149               stringId,
150               mDateFormatter.format(mData.getLastModifiedDate())));
151     }
152 
153     @Override
recycle(@onnull View view)154     public void recycle(@Nonnull View view) {
155         Glide.clear(view);
156         mSessionPlaceholderBitmap = Optional.absent();
157     }
158 
159     @Override
getItemViewType()160     public FilmstripItemType getItemViewType() {
161         return FilmstripItemType.PHOTO;
162     }
163 
164     @Override
renderTiny(@onnull View view)165     public void renderTiny(@Nonnull View view) {
166         if (view instanceof ImageView) {
167             renderTinySize(mData.getUri()).into((ImageView) view);
168         } else {
169             Log.w(TAG, "renderTiny was called with an object that is not an ImageView!");
170         }
171     }
172 
173     @Override
renderThumbnail(@onnull View view)174     public void renderThumbnail(@Nonnull View view) {
175         if (view instanceof ImageView) {
176             renderScreenSize(mData.getUri()).into((ImageView) view);
177         } else {
178             Log.w(TAG, "renderThumbnail was called with an object that is not an ImageView!");
179         }
180     }
181 
182     @Override
renderFullRes(@onnull View view)183     public void renderFullRes(@Nonnull View view) {
184         if (view instanceof ImageView) {
185             renderFullSize(mData.getUri()).into((ImageView) view);
186         } else {
187             Log.w(TAG, "renderFullRes was called with an object that is not an ImageView!");
188         }
189     }
190 
renderTinySize(Uri uri)191     private GenericRequestBuilder<Uri, ?, ?, GlideDrawable> renderTinySize(Uri uri) {
192         return mGlideManager.loadTinyThumb(uri, generateSignature(mData));
193     }
194 
renderScreenSize(Uri uri)195     private DrawableRequestBuilder<Uri> renderScreenSize(Uri uri) {
196         DrawableRequestBuilder<Uri> request =
197               mGlideManager.loadScreen(uri, generateSignature(mData), mSuggestedSize);
198 
199         // If we have a non-null placeholder, use that and do NOT ever render a
200         // tiny thumbnail to prevent un-intended "flash of low resolution image"
201         if (mSessionPlaceholderBitmap.isPresent()) {
202             Log.v(TAG, "using session bitmap as placeholder");
203             return request.placeholder(new BitmapDrawable(mContext.getResources(),
204                   mSessionPlaceholderBitmap.get()));
205         }
206 
207         // If we do not have a placeholder bitmap, render a thumbnail with
208         // the default placeholder resource like normal.
209         return request
210               .thumbnail(renderTinySize(uri));
211     }
212 
renderFullSize(Uri uri)213     private DrawableRequestBuilder<Uri> renderFullSize(Uri uri) {
214         Size size = mData.getDimensions();
215         return mGlideManager.loadFull(uri, generateSignature(mData), size)
216               .thumbnail(renderScreenSize(uri));
217     }
218 
219     @Override
generateThumbnail(int boundingWidthPx, int boundingHeightPx)220     public Optional<Bitmap> generateThumbnail(int boundingWidthPx, int boundingHeightPx) {
221         FilmstripItemData data = getData();
222         final Bitmap bitmap;
223 
224         if (getAttributes().isRendering()) {
225             return Storage.instance().getPlaceholderForSession(data.getUri());
226         } else {
227 
228             FileInputStream stream;
229 
230             try {
231                 stream = new FileInputStream(data.getFilePath());
232             } catch (FileNotFoundException e) {
233                 Log.e(TAG, "File not found:" + data.getFilePath());
234                 return Optional.absent();
235             }
236             int width = data.getDimensions().getWidth();
237             int height = data.getDimensions().getHeight();
238             int orientation = data.getOrientation();
239 
240             Point dim = CameraUtil.resizeToFill(
241                   width,
242                   height,
243                   orientation,
244                   boundingWidthPx,
245                   boundingHeightPx);
246 
247             // If the orientation is not vertical
248             if (orientation % 180 != 0) {
249                 int temp = dim.x;
250                 dim.x = dim.y;
251                 dim.y = temp;
252             }
253 
254             bitmap = FilmstripItemUtils
255                   .loadImageThumbnailFromStream(
256                         stream,
257                         data.getDimensions().getWidth(),
258                         data.getDimensions().getHeight(),
259                         (int) (dim.x * 0.7f), (int) (dim.y * 0.7),
260                         data.getOrientation(), MAX_PEEK_BITMAP_PIXELS);
261 
262             return Optional.fromNullable(bitmap);
263         }
264     }
265 }
266