• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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.gallery3d.filtershow.cache;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.database.Cursor;
23 import android.database.sqlite.SQLiteException;
24 import android.graphics.Bitmap;
25 import android.graphics.Bitmap.CompressFormat;
26 import android.graphics.BitmapFactory;
27 import android.graphics.BitmapRegionDecoder;
28 import android.graphics.Matrix;
29 import android.graphics.Rect;
30 import android.graphics.Bitmap.CompressFormat;
31 import android.net.Uri;
32 import android.provider.MediaStore;
33 import android.util.Log;
34 
35 import com.adobe.xmp.XMPException;
36 import com.adobe.xmp.XMPMeta;
37 import com.android.gallery3d.R;
38 import com.android.gallery3d.common.Utils;
39 import com.android.gallery3d.exif.ExifTag;
40 import com.android.gallery3d.exif.ExifInterface;
41 import com.android.gallery3d.filtershow.FilterShowActivity;
42 import com.android.gallery3d.filtershow.HistoryAdapter;
43 import com.android.gallery3d.filtershow.filters.FiltersManager;
44 import com.android.gallery3d.filtershow.imageshow.ImageShow;
45 import com.android.gallery3d.filtershow.imageshow.MasterImage;
46 import com.android.gallery3d.filtershow.presets.ImagePreset;
47 import com.android.gallery3d.filtershow.tools.BitmapTask;
48 import com.android.gallery3d.filtershow.tools.SaveCopyTask;
49 import com.android.gallery3d.util.InterruptableOutputStream;
50 import com.android.gallery3d.util.XmpUtilHelper;
51 
52 import java.io.ByteArrayInputStream;
53 import java.io.Closeable;
54 import java.io.File;
55 import java.io.FileInputStream;
56 import java.io.FileNotFoundException;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.io.OutputStream;
60 import java.util.Vector;
61 import java.util.concurrent.locks.ReentrantLock;
62 
63 
64 // TODO: this class has waaaay to much bitmap copying.  Cleanup.
65 public class ImageLoader {
66 
67     private static final String LOGTAG = "ImageLoader";
68     private final Vector<ImageShow> mListeners = new Vector<ImageShow>();
69     private Bitmap mOriginalBitmapSmall = null;
70     private Bitmap mOriginalBitmapLarge = null;
71     private Bitmap mOriginalBitmapHighres = null;
72     private Bitmap mBackgroundBitmap = null;
73 
74     private final ZoomCache mZoomCache = new ZoomCache();
75 
76     private int mOrientation = 0;
77     private HistoryAdapter mAdapter = null;
78 
79     private FilterShowActivity mActivity = null;
80 
81     public static final String JPEG_MIME_TYPE = "image/jpeg";
82 
83     public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
84     public static final int DEFAULT_COMPRESS_QUALITY = 95;
85 
86     public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
87     public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
88     public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
89     public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
90     public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT;
91     public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT;
92     public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP;
93     public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM;
94 
95     private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
96     private Context mContext = null;
97     private Uri mUri = null;
98 
99     private Rect mOriginalBounds = null;
100     private static int mZoomOrientation = ORI_NORMAL;
101 
102     static final int MAX_BITMAP_DIM = 900;
103 
104     private ReentrantLock mLoadingLock = new ReentrantLock();
105 
ImageLoader(FilterShowActivity activity, Context context)106     public ImageLoader(FilterShowActivity activity, Context context) {
107         mActivity = activity;
108         mContext = context;
109     }
110 
getZoomOrientation()111     public static int getZoomOrientation() {
112         return mZoomOrientation;
113     }
114 
getActivity()115     public FilterShowActivity getActivity() {
116         return mActivity;
117     }
118 
loadBitmap(Uri uri, int size)119     public boolean loadBitmap(Uri uri, int size) {
120         mLoadingLock.lock();
121         mUri = uri;
122         mOrientation = getOrientation(mContext, uri);
123         mOriginalBitmapSmall = loadScaledBitmap(uri, 160);
124         if (mOriginalBitmapSmall == null) {
125             // Couldn't read the bitmap, let's exit
126             mLoadingLock.unlock();
127             return false;
128         }
129         mOriginalBitmapLarge = loadScaledBitmap(uri, size);
130         if (mOriginalBitmapLarge == null) {
131             mLoadingLock.unlock();
132             return false;
133         }
134         if (MasterImage.getImage().supportsHighRes()) {
135             int highresPreviewSize = mOriginalBitmapLarge.getWidth() * 2;
136             if (highresPreviewSize > mOriginalBounds.width()) {
137                 highresPreviewSize = mOriginalBounds.width();
138             }
139             mOriginalBitmapHighres = loadScaledBitmap(uri, highresPreviewSize, false);
140         }
141         updateBitmaps();
142         mLoadingLock.unlock();
143         return true;
144     }
145 
getUri()146     public Uri getUri() {
147         return mUri;
148     }
149 
getOriginalBounds()150     public Rect getOriginalBounds() {
151         return mOriginalBounds;
152     }
153 
getOrientation(Context context, Uri uri)154     public static int getOrientation(Context context, Uri uri) {
155         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
156             String mimeType = context.getContentResolver().getType(uri);
157             if (mimeType != ImageLoader.JPEG_MIME_TYPE) {
158                 return -1;
159             }
160             String path = uri.getPath();
161             int orientation = -1;
162             InputStream is = null;
163             ExifInterface exif = new ExifInterface();
164             try {
165                 exif.readExif(path);
166                 orientation = ExifInterface.getRotationForOrientationValue(
167                         exif.getTagIntValue(ExifInterface.TAG_ORIENTATION).shortValue());
168             } catch (IOException e) {
169                 Log.w(LOGTAG, "Failed to read EXIF orientation", e);
170             }
171             return orientation;
172         }
173         Cursor cursor = null;
174         try {
175             cursor = context.getContentResolver().query(uri,
176                     new String[] {
177                         MediaStore.Images.ImageColumns.ORIENTATION
178                     },
179                     null, null, null);
180             if (cursor.moveToNext()) {
181                 int ori = cursor.getInt(0);
182 
183                 switch (ori) {
184                     case 0:
185                         return ORI_NORMAL;
186                     case 90:
187                         return ORI_ROTATE_90;
188                     case 270:
189                         return ORI_ROTATE_270;
190                     case 180:
191                         return ORI_ROTATE_180;
192                     default:
193                         return -1;
194                 }
195             } else {
196                 return -1;
197             }
198         } catch (SQLiteException e) {
199             return -1;
200         } catch (IllegalArgumentException e) {
201             return -1;
202         } finally {
203             Utils.closeSilently(cursor);
204         }
205     }
206 
updateBitmaps()207     private void updateBitmaps() {
208         if (mOrientation > 1) {
209             mOriginalBitmapSmall = rotateToPortrait(mOriginalBitmapSmall, mOrientation);
210             mOriginalBitmapLarge = rotateToPortrait(mOriginalBitmapLarge, mOrientation);
211             if (mOriginalBitmapHighres != null) {
212                 mOriginalBitmapHighres = rotateToPortrait(mOriginalBitmapHighres, mOrientation);
213             }
214         }
215         mZoomOrientation = mOrientation;
216         warnListeners();
217     }
218 
decodeImage(int id, BitmapFactory.Options options)219     public Bitmap decodeImage(int id, BitmapFactory.Options options) {
220         return BitmapFactory.decodeResource(mContext.getResources(), id, options);
221     }
222 
rotateToPortrait(Bitmap bitmap, int ori)223     public static Bitmap rotateToPortrait(Bitmap bitmap, int ori) {
224         Matrix matrix = new Matrix();
225         int w = bitmap.getWidth();
226         int h = bitmap.getHeight();
227         if (ori == ORI_ROTATE_90 ||
228                 ori == ORI_ROTATE_270 ||
229                 ori == ORI_TRANSPOSE ||
230                 ori == ORI_TRANSVERSE) {
231             int tmp = w;
232             w = h;
233             h = tmp;
234         }
235         switch (ori) {
236             case ORI_ROTATE_90:
237                 matrix.setRotate(90, w / 2f, h / 2f);
238                 break;
239             case ORI_ROTATE_180:
240                 matrix.setRotate(180, w / 2f, h / 2f);
241                 break;
242             case ORI_ROTATE_270:
243                 matrix.setRotate(270, w / 2f, h / 2f);
244                 break;
245             case ORI_FLIP_HOR:
246                 matrix.preScale(-1, 1);
247                 break;
248             case ORI_FLIP_VERT:
249                 matrix.preScale(1, -1);
250                 break;
251             case ORI_TRANSPOSE:
252                 matrix.setRotate(90, w / 2f, h / 2f);
253                 matrix.preScale(1, -1);
254                 break;
255             case ORI_TRANSVERSE:
256                 matrix.setRotate(270, w / 2f, h / 2f);
257                 matrix.preScale(1, -1);
258                 break;
259             case ORI_NORMAL:
260             default:
261                 return bitmap;
262         }
263 
264         return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
265                 bitmap.getHeight(), matrix, true);
266     }
267 
loadRegionBitmap(Uri uri, BitmapFactory.Options options, Rect bounds)268     private Bitmap loadRegionBitmap(Uri uri, BitmapFactory.Options options, Rect bounds) {
269         InputStream is = null;
270         try {
271             is = mContext.getContentResolver().openInputStream(uri);
272             BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
273             Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
274             // return null if bounds are not entirely within the bitmap
275             if (!r.contains(bounds)) {
276                 return null;
277             }
278             return decoder.decodeRegion(bounds, options);
279         } catch (FileNotFoundException e) {
280             Log.e(LOGTAG, "FileNotFoundException: " + uri);
281         } catch (Exception e) {
282             e.printStackTrace();
283         } finally {
284             Utils.closeSilently(is);
285         }
286         return null;
287     }
288 
loadScaledBitmap(Uri uri, int size)289     private Bitmap loadScaledBitmap(Uri uri, int size) {
290         return loadScaledBitmap(uri, size, true);
291     }
292 
loadScaledBitmap(Uri uri, int size, boolean enforceSize)293     private Bitmap loadScaledBitmap(Uri uri, int size, boolean enforceSize) {
294         InputStream is = null;
295         try {
296             is = mContext.getContentResolver().openInputStream(uri);
297             Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: "
298                     + is);
299             BitmapFactory.Options o = new BitmapFactory.Options();
300             o.inJustDecodeBounds = true;
301             BitmapFactory.decodeStream(is, null, o);
302 
303             int width_tmp = o.outWidth;
304             int height_tmp = o.outHeight;
305 
306             mOriginalBounds = new Rect(0, 0, width_tmp, height_tmp);
307 
308             int scale = 1;
309             while (true) {
310                 if (width_tmp <= 2 || height_tmp <= 2) {
311                     break;
312                 }
313                 if (!enforceSize
314                         || (width_tmp <= MAX_BITMAP_DIM
315                         && height_tmp <= MAX_BITMAP_DIM)) {
316                     if (width_tmp / 2 < size || height_tmp / 2 < size) {
317                         break;
318                     }
319                 }
320                 width_tmp /= 2;
321                 height_tmp /= 2;
322                 scale *= 2;
323             }
324 
325             // decode with inSampleSize
326             BitmapFactory.Options o2 = new BitmapFactory.Options();
327             o2.inSampleSize = scale;
328             o2.inMutable = true;
329 
330             Utils.closeSilently(is);
331             is = mContext.getContentResolver().openInputStream(uri);
332             return BitmapFactory.decodeStream(is, null, o2);
333         } catch (FileNotFoundException e) {
334             Log.e(LOGTAG, "FileNotFoundException: " + uri);
335         } catch (Exception e) {
336             e.printStackTrace();
337         } finally {
338             Utils.closeSilently(is);
339         }
340         return null;
341     }
342 
getBackgroundBitmap(Resources resources)343     public Bitmap getBackgroundBitmap(Resources resources) {
344         if (mBackgroundBitmap == null) {
345             mBackgroundBitmap = BitmapFactory.decodeResource(resources,
346                     R.drawable.filtershow_background);
347         }
348         return mBackgroundBitmap;
349 
350     }
351 
getOriginalBitmapSmall()352     public Bitmap getOriginalBitmapSmall() {
353         return mOriginalBitmapSmall;
354     }
355 
getOriginalBitmapLarge()356     public Bitmap getOriginalBitmapLarge() {
357         return mOriginalBitmapLarge;
358     }
359 
getOriginalBitmapHighres()360     public Bitmap getOriginalBitmapHighres() {
361         return mOriginalBitmapHighres;
362     }
363 
addListener(ImageShow imageShow)364     public void addListener(ImageShow imageShow) {
365         mLoadingLock.lock();
366         if (!mListeners.contains(imageShow)) {
367             mListeners.add(imageShow);
368         }
369         mLoadingLock.unlock();
370     }
371 
warnListeners()372     private void warnListeners() {
373         mActivity.runOnUiThread(mWarnListenersRunnable);
374     }
375 
376     private Runnable mWarnListenersRunnable = new Runnable() {
377 
378         @Override
379         public void run() {
380             for (int i = 0; i < mListeners.size(); i++) {
381                 ImageShow imageShow = mListeners.elementAt(i);
382                 imageShow.imageLoaded();
383             }
384         }
385     };
386 
getScaleOneImageForPreset(Rect bounds, Rect destination)387     public Bitmap getScaleOneImageForPreset(Rect bounds, Rect destination) {
388         mLoadingLock.lock();
389         BitmapFactory.Options options = new BitmapFactory.Options();
390         options.inMutable = true;
391         if (destination != null) {
392             if (bounds.width() > destination.width()) {
393                 int sampleSize = 1;
394                 int w = bounds.width();
395                 while (w > destination.width()) {
396                     sampleSize *= 2;
397                     w /= sampleSize;
398                 }
399                 options.inSampleSize = sampleSize;
400             }
401         }
402         Bitmap bmp = loadRegionBitmap(mUri, options, bounds);
403         mLoadingLock.unlock();
404         return bmp;
405     }
406 
saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, File destination)407     public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
408             File destination) {
409         new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
410 
411             @Override
412             public void onComplete(Uri result) {
413                 filterShowActivity.completeSaveImage(result);
414             }
415 
416         }).execute(preset);
417     }
418 
loadMutableBitmap(Context context, Uri sourceUri)419     public static Bitmap loadMutableBitmap(Context context, Uri sourceUri) {
420         BitmapFactory.Options options = new BitmapFactory.Options();
421         return loadMutableBitmap(context, sourceUri, options);
422     }
423 
loadMutableBitmap(Context context, Uri sourceUri, BitmapFactory.Options options)424     public static Bitmap loadMutableBitmap(Context context, Uri sourceUri,
425             BitmapFactory.Options options) {
426         // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
427         // exist)
428         options.inMutable = true;
429 
430         Bitmap bitmap = decodeUriWithBackouts(context, sourceUri, options);
431         if (bitmap == null) {
432             return null;
433         }
434         int orientation = ImageLoader.getOrientation(context, sourceUri);
435         bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
436         return bitmap;
437     }
438 
decodeUriWithBackouts(Context context, Uri sourceUri, BitmapFactory.Options options)439     public static Bitmap decodeUriWithBackouts(Context context, Uri sourceUri,
440             BitmapFactory.Options options) {
441         boolean noBitmap = true;
442         int num_tries = 0;
443         InputStream is = getInputStream(context, sourceUri);
444 
445         if (options.inSampleSize < 1) {
446             options.inSampleSize = 1;
447         }
448         // Stopgap fix for low-memory devices.
449         Bitmap bmap = null;
450         while (noBitmap) {
451             if (is == null) {
452                 return null;
453             }
454             try {
455                 // Try to decode, downsample if low-memory.
456                 bmap = BitmapFactory.decodeStream(is, null, options);
457                 noBitmap = false;
458             } catch (java.lang.OutOfMemoryError e) {
459                 // Try 5 times before failing for good.
460                 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
461                     throw e;
462                 }
463                 is = null;
464                 bmap = null;
465                 System.gc();
466                 is = getInputStream(context, sourceUri);
467                 options.inSampleSize *= 2;
468             }
469         }
470         Utils.closeSilently(is);
471         return bmap;
472     }
473 
getInputStream(Context context, Uri sourceUri)474     private static InputStream getInputStream(Context context, Uri sourceUri) {
475         InputStream is = null;
476         try {
477             is = context.getContentResolver().openInputStream(sourceUri);
478         } catch (FileNotFoundException e) {
479             Log.w(LOGTAG, "could not load bitmap ", e);
480             Utils.closeSilently(is);
481             is = null;
482         }
483         return is;
484     }
485 
decodeResourceWithBackouts(Resources res, BitmapFactory.Options options, int id)486     public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
487             int id) {
488         boolean noBitmap = true;
489         int num_tries = 0;
490         if (options.inSampleSize < 1) {
491             options.inSampleSize = 1;
492         }
493         // Stopgap fix for low-memory devices.
494         Bitmap bmap = null;
495         while (noBitmap) {
496             try {
497                 // Try to decode, downsample if low-memory.
498                 bmap = BitmapFactory.decodeResource(
499                         res, id, options);
500                 noBitmap = false;
501             } catch (java.lang.OutOfMemoryError e) {
502                 // Try 5 times before failing for good.
503                 if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
504                     throw e;
505                 }
506                 bmap = null;
507                 System.gc();
508                 options.inSampleSize *= 2;
509             }
510         }
511         return bmap;
512     }
513 
returnFilteredResult(ImagePreset preset, final FilterShowActivity filterShowActivity)514     public void returnFilteredResult(ImagePreset preset,
515             final FilterShowActivity filterShowActivity) {
516         BitmapTask.Callbacks<ImagePreset> cb = new BitmapTask.Callbacks<ImagePreset>() {
517 
518             @Override
519             public void onComplete(Bitmap result) {
520                 filterShowActivity.onFilteredResult(result);
521             }
522 
523             @Override
524             public void onCancel() {
525             }
526 
527             @Override
528             public Bitmap onExecute(ImagePreset param) {
529                 if (param == null || mUri == null) {
530                     return null;
531                 }
532                 BitmapFactory.Options options = new BitmapFactory.Options();
533                 boolean noBitmap = true;
534                 int num_tries = 0;
535                 if (options.inSampleSize < 1) {
536                     options.inSampleSize = 1;
537                 }
538                 Bitmap bitmap = null;
539                 // Stopgap fix for low-memory devices.
540                 while (noBitmap) {
541                     try {
542                         // Try to do bitmap operations, downsample if low-memory
543                         bitmap = loadMutableBitmap(mContext, mUri, options);
544                         if (bitmap == null) {
545                             Log.w(LOGTAG, "Failed to save image!");
546                             return null;
547                         }
548                         CachingPipeline pipeline = new CachingPipeline(
549                                 FiltersManager.getManager(), "Saving");
550                         bitmap = pipeline.renderFinalImage(bitmap, param);
551                         noBitmap = false;
552                     } catch (java.lang.OutOfMemoryError e) {
553                         // Try 5 times before failing for good.
554                         if (++num_tries >= 5) {
555                             throw e;
556                         }
557                         bitmap = null;
558                         System.gc();
559                         options.inSampleSize *= 2;
560                     }
561                 }
562                 return bitmap;
563             }
564         };
565 
566         (new BitmapTask<ImagePreset>(cb)).execute(preset);
567     }
568 
getFileExtension(String requestFormat)569     private String getFileExtension(String requestFormat) {
570         String outputFormat = (requestFormat == null)
571                 ? "jpg"
572                 : requestFormat;
573         outputFormat = outputFormat.toLowerCase();
574         return (outputFormat.equals("png") || outputFormat.equals("gif"))
575                 ? "png" // We don't support gif compression.
576                 : "jpg";
577     }
578 
convertExtensionToCompressFormat(String extension)579     private CompressFormat convertExtensionToCompressFormat(String extension) {
580         return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
581     }
582 
saveToUri(Bitmap bmap, Uri uri, final String outputFormat, final FilterShowActivity filterShowActivity)583     public void saveToUri(Bitmap bmap, Uri uri, final String outputFormat,
584             final FilterShowActivity filterShowActivity) {
585 
586         OutputStream out = null;
587         try {
588             out = filterShowActivity.getContentResolver().openOutputStream(uri);
589         } catch (FileNotFoundException e) {
590             Log.w(LOGTAG, "cannot write output", e);
591             out = null;
592         } finally {
593             if (bmap == null || out == null) {
594                 return;
595             }
596         }
597 
598         final InterruptableOutputStream ios = new InterruptableOutputStream(out);
599 
600         BitmapTask.Callbacks<Bitmap> cb = new BitmapTask.Callbacks<Bitmap>() {
601 
602             @Override
603             public void onComplete(Bitmap result) {
604                 filterShowActivity.done();
605             }
606 
607             @Override
608             public void onCancel() {
609                 ios.interrupt();
610             }
611 
612             @Override
613             public Bitmap onExecute(Bitmap param) {
614                 CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(outputFormat));
615                 param.compress(cf, DEFAULT_COMPRESS_QUALITY, ios);
616                 Utils.closeSilently(ios);
617                 return null;
618             }
619         };
620 
621         (new BitmapTask<Bitmap>(cb)).execute(bmap);
622     }
623 
setAdapter(HistoryAdapter adapter)624     public void setAdapter(HistoryAdapter adapter) {
625         mAdapter = adapter;
626     }
627 
getHistory()628     public HistoryAdapter getHistory() {
629         return mAdapter;
630     }
631 
getXmpObject()632     public XMPMeta getXmpObject() {
633         try {
634             InputStream is = mContext.getContentResolver().openInputStream(getUri());
635             return XmpUtilHelper.extractXMPMeta(is);
636         } catch (FileNotFoundException e) {
637             return null;
638         }
639     }
640 
641     /**
642      * Determine if this is a light cycle 360 image
643      *
644      * @return true if it is a light Cycle image that is full 360
645      */
queryLightCycle360()646     public boolean queryLightCycle360() {
647         InputStream is = null;
648         try {
649             is = mContext.getContentResolver().openInputStream(getUri());
650             XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
651             if (meta == null) {
652                 return false;
653             }
654             String name = meta.getPacketHeader();
655             String namespace = "http://ns.google.com/photos/1.0/panorama/";
656             String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
657             String fullWidthName = "GPano:FullPanoWidthPixels";
658 
659             if (!meta.doesPropertyExist(namespace, cropWidthName)) {
660                 return false;
661             }
662             if (!meta.doesPropertyExist(namespace, fullWidthName)) {
663                 return false;
664             }
665 
666             Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
667             Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
668 
669             // Definition of a 360:
670             // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
671             if (cropValue != null && fullValue != null) {
672                 return cropValue.equals(fullValue);
673             }
674 
675             return false;
676         } catch (FileNotFoundException e) {
677             return false;
678         } catch (XMPException e) {
679             return false;
680         } finally {
681             Utils.closeSilently(is);
682         }
683     }
684 }
685