openEventsPlatformApp/plugins/cordova-plugin-camera/src/android/CameraLauncher.java
plugins/cordova-plugin-camera/src/android/CameraLauncher.javaRaw
1/*
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18*/
19package org.apache.cordova.camera;
20
21import java.io.ByteArrayOutputStream;
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.FileNotFoundException;
25import java.io.FileOutputStream;
26import java.io.IOException;
27import java.io.InputStream;
28import java.io.OutputStream;
29import java.text.SimpleDateFormat;
30import java.util.Date;
31
32import org.apache.cordova.BuildHelper;
33import org.apache.cordova.CallbackContext;
34import org.apache.cordova.CordovaPlugin;
35import org.apache.cordova.CordovaResourceApi;
36import org.apache.cordova.LOG;
37import org.apache.cordova.PermissionHelper;
38import org.apache.cordova.PluginResult;
39import org.json.JSONArray;
40import org.json.JSONException;
41
42import android.Manifest;
43import android.annotation.TargetApi;
44import android.app.Activity;
45import android.content.ActivityNotFoundException;
46import android.content.ContentValues;
47import android.content.Context;
48import android.content.Intent;
49import android.database.Cursor;
50import android.graphics.Bitmap;
51import android.graphics.Bitmap.CompressFormat;
52import android.graphics.BitmapFactory;
53import android.graphics.Matrix;
54import android.media.ExifInterface;
55import android.media.MediaScannerConnection;
56import android.media.MediaScannerConnection.MediaScannerConnectionClient;
57import android.net.Uri;
58import android.os.Build;
59import android.os.Bundle;
60import android.os.Environment;
61import android.provider.DocumentsContract;
62import android.provider.MediaStore;
63import android.provider.OpenableColumns;
64import android.support.v4.content.FileProvider;
65import android.util.Base64;
66import android.content.pm.PackageManager;
67import android.content.pm.PackageManager.NameNotFoundException;
68
69/**
70 * This class launches the camera view, allows the user to take a picture, closes the camera view,
71 * and returns the captured image. When the camera view is closed, the screen displayed before
72 * the camera view was shown is redisplayed.
73 */
74public class CameraLauncher extends CordovaPlugin implements MediaScannerConnectionClient {
75
76 private static final int DATA_URL = 0; // Return base64 encoded string
77 private static final int FILE_URI = 1; // Return file uri (content://media/external/images/media/2 for Android)
78 private static final int NATIVE_URI = 2; // On Android, this is the same as FILE_URI
79
80 private static final int PHOTOLIBRARY = 0; // Choose image from picture library (same as SAVEDPHOTOALBUM for Android)
81 private static final int CAMERA = 1; // Take picture from camera
82 private static final int SAVEDPHOTOALBUM = 2; // Choose image from picture library (same as PHOTOLIBRARY for Android)
83
84 private static final int PICTURE = 0; // allow selection of still pictures only. DEFAULT. Will return format specified via DestinationType
85 private static final int VIDEO = 1; // allow selection of video only, ONLY RETURNS URL
86 private static final int ALLMEDIA = 2; // allow selection from all media types
87
88 private static final int JPEG = 0; // Take a picture of type JPEG
89 private static final int PNG = 1; // Take a picture of type PNG
90 private static final String GET_PICTURE = "Get Picture";
91 private static final String GET_VIDEO = "Get Video";
92 private static final String GET_All = "Get All";
93
94 public static final int PERMISSION_DENIED_ERROR = 20;
95 public static final int TAKE_PIC_SEC = 0;
96 public static final int SAVE_TO_ALBUM_SEC = 1;
97
98 private static final String LOG_TAG = "CameraLauncher";
99
100 //Where did this come from?
101 private static final int CROP_CAMERA = 100;
102
103 private int mQuality; // Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
104 private int targetWidth; // desired width of the image
105 private int targetHeight; // desired height of the image
106 private CordovaUri imageUri; // Uri of captured image
107 private int encodingType; // Type of encoding to use
108 private int mediaType; // What type of media to retrieve
109 private int destType; // Source type (needs to be saved for the permission handling)
110 private int srcType; // Destination type (needs to be saved for permission handling)
111 private boolean saveToPhotoAlbum; // Should the picture be saved to the device's photo album
112 private boolean correctOrientation; // Should the pictures orientation be corrected
113 private boolean orientationCorrected; // Has the picture's orientation been corrected
114 private boolean allowEdit; // Should we allow the user to crop the image.
115
116 protected final static String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE };
117
118 public CallbackContext callbackContext;
119 private int numPics;
120
121 private MediaScannerConnection conn; // Used to update gallery app with newly-written files
122 private Uri scanMe; // Uri of image to be added to content store
123 private Uri croppedUri;
124 private ExifHelper exifData; // Exif data from source
125 private String applicationId;
126
127
128 /**
129 * Executes the request and returns PluginResult.
130 *
131 * @param action The action to execute.
132 * @param args JSONArry of arguments for the plugin.
133 * @param callbackContext The callback id used when calling back into JavaScript.
134 * @return A PluginResult object with a status and message.
135 */
136 public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
137 this.callbackContext = callbackContext;
138 //Adding an API to CoreAndroid to get the BuildConfigValue
139 //This allows us to not make this a breaking change to embedding
140 this.applicationId = (String) BuildHelper.getBuildConfigValue(cordova.getActivity(), "APPLICATION_ID");
141 this.applicationId = preferences.getString("applicationId", this.applicationId);
142
143
144 if (action.equals("takePicture")) {
145 this.srcType = CAMERA;
146 this.destType = FILE_URI;
147 this.saveToPhotoAlbum = false;
148 this.targetHeight = 0;
149 this.targetWidth = 0;
150 this.encodingType = JPEG;
151 this.mediaType = PICTURE;
152 this.mQuality = 50;
153
154 //Take the values from the arguments if they're not already defined (this is tricky)
155 this.destType = args.getInt(1);
156 this.srcType = args.getInt(2);
157 this.mQuality = args.getInt(0);
158 this.targetWidth = args.getInt(3);
159 this.targetHeight = args.getInt(4);
160 this.encodingType = args.getInt(5);
161 this.mediaType = args.getInt(6);
162 this.allowEdit = args.getBoolean(7);
163 this.correctOrientation = args.getBoolean(8);
164 this.saveToPhotoAlbum = args.getBoolean(9);
165
166 // If the user specifies a 0 or smaller width/height
167 // make it -1 so later comparisons succeed
168 if (this.targetWidth < 1) {
169 this.targetWidth = -1;
170 }
171 if (this.targetHeight < 1) {
172 this.targetHeight = -1;
173 }
174
175 // We don't return full-quality PNG files. The camera outputs a JPEG
176 // so requesting it as a PNG provides no actual benefit
177 if (this.targetHeight == -1 && this.targetWidth == -1 && this.mQuality == 100 &&
178 !this.correctOrientation && this.encodingType == PNG && this.srcType == CAMERA) {
179 this.encodingType = JPEG;
180 }
181
182 try {
183 if (this.srcType == CAMERA) {
184 this.callTakePicture(destType, encodingType);
185 }
186 else if ((this.srcType == PHOTOLIBRARY) || (this.srcType == SAVEDPHOTOALBUM)) {
187 // FIXME: Stop always requesting the permission
188 if(!PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
189 PermissionHelper.requestPermission(this, SAVE_TO_ALBUM_SEC, Manifest.permission.READ_EXTERNAL_STORAGE);
190 } else {
191 this.getImage(this.srcType, destType, encodingType);
192 }
193 }
194 }
195 catch (IllegalArgumentException e)
196 {
197 callbackContext.error("Illegal Argument Exception");
198 PluginResult r = new PluginResult(PluginResult.Status.ERROR);
199 callbackContext.sendPluginResult(r);
200 return true;
201 }
202
203 PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
204 r.setKeepCallback(true);
205 callbackContext.sendPluginResult(r);
206
207 return true;
208 }
209 return false;
210 }
211
212 //--------------------------------------------------------------------------
213 // LOCAL METHODS
214 //--------------------------------------------------------------------------
215
216 private String getTempDirectoryPath() {
217 File cache = null;
218
219 // SD Card Mounted
220 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
221 cache = cordova.getActivity().getExternalCacheDir();
222 }
223 // Use internal storage
224 else {
225 cache = cordova.getActivity().getCacheDir();
226 }
227
228 // Create the cache directory if it doesn't exist
229 cache.mkdirs();
230 return cache.getAbsolutePath();
231 }
232
233 /**
234 * Take a picture with the camera.
235 * When an image is captured or the camera view is cancelled, the result is returned
236 * in CordovaActivity.onActivityResult, which forwards the result to this.onActivityResult.
237 *
238 * The image can either be returned as a base64 string or a URI that points to the file.
239 * To display base64 string in an img tag, set the source to:
240 * img.src="data:image/jpeg;base64,"+result;
241 * or to display URI in an img tag
242 * img.src=result;
243 *
244 * @param returnType Set the type of image to return.
245 * @param encodingType Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
246 */
247 public void callTakePicture(int returnType, int encodingType) {
248 boolean saveAlbumPermission = PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
249 boolean takePicturePermission = PermissionHelper.hasPermission(this, Manifest.permission.CAMERA);
250
251 // CB-10120: The CAMERA permission does not need to be requested unless it is declared
252 // in AndroidManifest.xml. This plugin does not declare it, but others may and so we must
253 // check the package info to determine if the permission is present.
254
255 if (!takePicturePermission) {
256 takePicturePermission = true;
257 try {
258 PackageManager packageManager = this.cordova.getActivity().getPackageManager();
259 String[] permissionsInPackage = packageManager.getPackageInfo(this.cordova.getActivity().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
260 if (permissionsInPackage != null) {
261 for (String permission : permissionsInPackage) {
262 if (permission.equals(Manifest.permission.CAMERA)) {
263 takePicturePermission = false;
264 break;
265 }
266 }
267 }
268 } catch (NameNotFoundException e) {
269 // We are requesting the info for our package, so this should
270 // never be caught
271 }
272 }
273
274 if (takePicturePermission && saveAlbumPermission) {
275 takePicture(returnType, encodingType);
276 } else if (saveAlbumPermission && !takePicturePermission) {
277 PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.CAMERA);
278 } else if (!saveAlbumPermission && takePicturePermission) {
279 PermissionHelper.requestPermission(this, TAKE_PIC_SEC, Manifest.permission.READ_EXTERNAL_STORAGE);
280 } else {
281 PermissionHelper.requestPermissions(this, TAKE_PIC_SEC, permissions);
282 }
283 }
284
285 public void takePicture(int returnType, int encodingType)
286 {
287 // Save the number of images currently on disk for later
288 this.numPics = queryImgDB(whichContentStore()).getCount();
289
290 // Let's use the intent and see what happens
291 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
292
293 // Specify file so that large image is captured and returned
294 File photo = createCaptureFile(encodingType);
295 this.imageUri = new CordovaUri(FileProvider.getUriForFile(cordova.getActivity(),
296 applicationId + ".provider",
297 photo));
298 intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, imageUri.getCorrectUri());
299 //We can write to this URI, this will hopefully allow us to write files to get to the next step
300 intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
301
302 if (this.cordova != null) {
303 // Let's check to make sure the camera is actually installed. (Legacy Nexus 7 code)
304 PackageManager mPm = this.cordova.getActivity().getPackageManager();
305 if(intent.resolveActivity(mPm) != null)
306 {
307
308 this.cordova.startActivityForResult((CordovaPlugin) this, intent, (CAMERA + 1) * 16 + returnType + 1);
309 }
310 else
311 {
312 LOG.d(LOG_TAG, "Error: You don't have a default camera. Your device may not be CTS complaint.");
313 }
314 }
315// else
316// LOG.d(LOG_TAG, "ERROR: You must use the CordovaInterface for this to work correctly. Please implement it in your activity");
317 }
318
319 /**
320 * Create a file in the applications temporary directory based upon the supplied encoding.
321 *
322 * @param encodingType of the image to be taken
323 * @return a File object pointing to the temporary picture
324 */
325 private File createCaptureFile(int encodingType) {
326 return createCaptureFile(encodingType, "");
327 }
328
329 /**
330 * Create a file in the applications temporary directory based upon the supplied encoding.
331 *
332 * @param encodingType of the image to be taken
333 * @param fileName or resultant File object.
334 * @return a File object pointing to the temporary picture
335 */
336 private File createCaptureFile(int encodingType, String fileName) {
337 if (fileName.isEmpty()) {
338 fileName = ".Pic";
339 }
340
341 if (encodingType == JPEG) {
342 fileName = fileName + ".jpg";
343 } else if (encodingType == PNG) {
344 fileName = fileName + ".png";
345 } else {
346 throw new IllegalArgumentException("Invalid Encoding Type: " + encodingType);
347 }
348
349 return new File(getTempDirectoryPath(), fileName);
350 }
351
352
353
354 /**
355 * Get image from photo library.
356 *
357 * @param srcType The album to get image from.
358 * @param returnType Set the type of image to return.
359 * @param encodingType
360 */
361 // TODO: Images selected from SDCARD don't display correctly, but from CAMERA ALBUM do!
362 // TODO: Images from kitkat filechooser not going into crop function
363 public void getImage(int srcType, int returnType, int encodingType) {
364 Intent intent = new Intent();
365 String title = GET_PICTURE;
366 croppedUri = null;
367 if (this.mediaType == PICTURE) {
368 intent.setType("image/*");
369 if (this.allowEdit) {
370 intent.setAction(Intent.ACTION_PICK);
371 intent.putExtra("crop", "true");
372 if (targetWidth > 0) {
373 intent.putExtra("outputX", targetWidth);
374 }
375 if (targetHeight > 0) {
376 intent.putExtra("outputY", targetHeight);
377 }
378 if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) {
379 intent.putExtra("aspectX", 1);
380 intent.putExtra("aspectY", 1);
381 }
382 File photo = createCaptureFile(JPEG);
383 croppedUri = Uri.fromFile(photo);
384 intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, croppedUri);
385 } else {
386 intent.setAction(Intent.ACTION_GET_CONTENT);
387 intent.addCategory(Intent.CATEGORY_OPENABLE);
388 }
389 } else if (this.mediaType == VIDEO) {
390 intent.setType("video/*");
391 title = GET_VIDEO;
392 intent.setAction(Intent.ACTION_GET_CONTENT);
393 intent.addCategory(Intent.CATEGORY_OPENABLE);
394 } else if (this.mediaType == ALLMEDIA) {
395 // I wanted to make the type 'image/*, video/*' but this does not work on all versions
396 // of android so I had to go with the wildcard search.
397 intent.setType("*/*");
398 title = GET_All;
399 intent.setAction(Intent.ACTION_GET_CONTENT);
400 intent.addCategory(Intent.CATEGORY_OPENABLE);
401 }
402 if (this.cordova != null) {
403 this.cordova.startActivityForResult((CordovaPlugin) this, Intent.createChooser(intent,
404 new String(title)), (srcType + 1) * 16 + returnType + 1);
405 }
406 }
407
408
409 /**
410 * Brings up the UI to perform crop on passed image URI
411 *
412 * @param picUri
413 */
414 private void performCrop(Uri picUri, int destType, Intent cameraIntent) {
415 try {
416 Intent cropIntent = new Intent("com.android.camera.action.CROP");
417 // indicate image type and Uri
418 cropIntent.setDataAndType(picUri, "image/*");
419 // set crop properties
420 cropIntent.putExtra("crop", "true");
421
422
423 // indicate output X and Y
424 if (targetWidth > 0) {
425 cropIntent.putExtra("outputX", targetWidth);
426 }
427 if (targetHeight > 0) {
428 cropIntent.putExtra("outputY", targetHeight);
429 }
430 if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) {
431 cropIntent.putExtra("aspectX", 1);
432 cropIntent.putExtra("aspectY", 1);
433 }
434 // create new file handle to get full resolution crop
435 croppedUri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
436 cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
437 cropIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
438 cropIntent.putExtra("output", croppedUri);
439
440
441 // start the activity - we handle returning in onActivityResult
442
443 if (this.cordova != null) {
444 this.cordova.startActivityForResult((CordovaPlugin) this,
445 cropIntent, CROP_CAMERA + destType);
446 }
447 } catch (ActivityNotFoundException anfe) {
448 LOG.e(LOG_TAG, "Crop operation not supported on this device");
449 try {
450 processResultFromCamera(destType, cameraIntent);
451 }
452 catch (IOException e)
453 {
454 e.printStackTrace();
455 LOG.e(LOG_TAG, "Unable to write to file");
456 }
457 }
458 }
459
460 /**
461 * Applies all needed transformation to the image received from the camera.
462 *
463 * @param destType In which form should we return the image
464 * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
465 */
466 private void processResultFromCamera(int destType, Intent intent) throws IOException {
467 int rotate = 0;
468
469 // Create an ExifHelper to save the exif data that is lost during compression
470 ExifHelper exif = new ExifHelper();
471
472 String sourcePath = (this.allowEdit && this.croppedUri != null) ?
473 FileHelper.stripFileProtocol(this.croppedUri.toString()) :
474 this.imageUri.getFilePath();
475
476
477 if (this.encodingType == JPEG) {
478 try {
479 //We don't support PNG, so let's not pretend we do
480 exif.createInFile(sourcePath);
481 exif.readExifData();
482 rotate = exif.getOrientation();
483
484 } catch (IOException e) {
485 e.printStackTrace();
486 }
487 }
488
489 Bitmap bitmap = null;
490 Uri galleryUri = null;
491
492 // CB-5479 When this option is given the unchanged image should be saved
493 // in the gallery and the modified image is saved in the temporary
494 // directory
495 if (this.saveToPhotoAlbum) {
496 galleryUri = Uri.fromFile(new File(getPicturesPath()));
497
498 if (this.allowEdit && this.croppedUri != null) {
499 writeUncompressedImage(croppedUri, galleryUri);
500 } else {
501 Uri imageUri = this.imageUri.getFileUri();
502 writeUncompressedImage(imageUri, galleryUri);
503 }
504
505 refreshGallery(galleryUri);
506 }
507
508 // If sending base64 image back
509 if (destType == DATA_URL) {
510 bitmap = getScaledAndRotatedBitmap(sourcePath);
511
512 if (bitmap == null) {
513 // Try to get the bitmap from intent.
514 bitmap = (Bitmap) intent.getExtras().get("data");
515 }
516
517 // Double-check the bitmap.
518 if (bitmap == null) {
519 LOG.d(LOG_TAG, "I either have a null image path or bitmap");
520 this.failPicture("Unable to create bitmap!");
521 return;
522 }
523
524
525 this.processPicture(bitmap, this.encodingType);
526
527 if (!this.saveToPhotoAlbum) {
528 checkForDuplicateImage(DATA_URL);
529 }
530 }
531
532 // If sending filename back
533 else if (destType == FILE_URI || destType == NATIVE_URI) {
534 // If all this is true we shouldn't compress the image.
535 if (this.targetHeight == -1 && this.targetWidth == -1 && this.mQuality == 100 &&
536 !this.correctOrientation) {
537
538 // If we saved the uncompressed photo to the album, we can just
539 // return the URI we already created
540 if (this.saveToPhotoAlbum) {
541 this.callbackContext.success(galleryUri.toString());
542 } else {
543 Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
544
545 if (this.allowEdit && this.croppedUri != null) {
546 Uri croppedUri = Uri.fromFile(new File(getFileNameFromUri(this.croppedUri)));
547 writeUncompressedImage(croppedUri, uri);
548 } else {
549 Uri imageUri = this.imageUri.getFileUri();
550 writeUncompressedImage(imageUri, uri);
551 }
552
553 this.callbackContext.success(uri.toString());
554 }
555 } else {
556 Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
557 bitmap = getScaledAndRotatedBitmap(sourcePath);
558
559 // Double-check the bitmap.
560 if (bitmap == null) {
561 LOG.d(LOG_TAG, "I either have a null image path or bitmap");
562 this.failPicture("Unable to create bitmap!");
563 return;
564 }
565
566
567 // Add compressed version of captured image to returned media store Uri
568 OutputStream os = this.cordova.getActivity().getContentResolver().openOutputStream(uri);
569 CompressFormat compressFormat = encodingType == JPEG ?
570 CompressFormat.JPEG :
571 CompressFormat.PNG;
572
573 bitmap.compress(compressFormat, this.mQuality, os);
574 os.close();
575
576 // Restore exif data to file
577 if (this.encodingType == JPEG) {
578 String exifPath;
579 exifPath = uri.getPath();
580 //We just finished rotating it by an arbitrary orientation, just make sure it's normal
581 if(rotate != ExifInterface.ORIENTATION_NORMAL)
582 exif.resetOrientation();
583 exif.createOutFile(exifPath);
584 exif.writeExifData();
585 }
586
587 // Send Uri back to JavaScript for viewing image
588 this.callbackContext.success(uri.toString());
589
590 }
591 } else {
592 throw new IllegalStateException();
593 }
594
595 this.cleanup(FILE_URI, this.imageUri.getFileUri(), galleryUri, bitmap);
596 bitmap = null;
597 }
598
599 private String getPicturesPath() {
600 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
601 String imageFileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png");
602 File storageDir = Environment.getExternalStoragePublicDirectory(
603 Environment.DIRECTORY_PICTURES);
604 String galleryPath = storageDir.getAbsolutePath() + "/" + imageFileName;
605 return galleryPath;
606 }
607
608 private void refreshGallery(Uri contentUri) {
609 Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
610 mediaScanIntent.setData(contentUri);
611 this.cordova.getActivity().sendBroadcast(mediaScanIntent);
612 }
613
614 /**
615 * Converts output image format int value to string value of mime type.
616 * @param outputFormat int Output format of camera API.
617 * Must be value of either JPEG or PNG constant
618 * @return String String value of mime type or empty string if mime type is not supported
619 */
620 private String getMimetypeForFormat(int outputFormat) {
621 if (outputFormat == PNG) return "image/png";
622 if (outputFormat == JPEG) return "image/jpeg";
623 return "";
624 }
625
626
627 private String outputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
628 // Some content: URIs do not map to file paths (e.g. picasa).
629 String realPath = FileHelper.getRealPath(uri, this.cordova);
630
631 // Get filename from uri
632 String fileName = realPath != null ?
633 realPath.substring(realPath.lastIndexOf('/') + 1) :
634 "modified." + (this.encodingType == JPEG ? "jpg" : "png");
635
636 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
637 //String fileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png");
638 String modifiedPath = getTempDirectoryPath() + "/" + fileName;
639
640 OutputStream os = new FileOutputStream(modifiedPath);
641 CompressFormat compressFormat = this.encodingType == JPEG ?
642 CompressFormat.JPEG :
643 CompressFormat.PNG;
644
645 bitmap.compress(compressFormat, this.mQuality, os);
646 os.close();
647
648 if (exifData != null && this.encodingType == JPEG) {
649 try {
650 if (this.correctOrientation && this.orientationCorrected) {
651 exifData.resetOrientation();
652 }
653 exifData.createOutFile(modifiedPath);
654 exifData.writeExifData();
655 exifData = null;
656 } catch (IOException e) {
657 e.printStackTrace();
658 }
659 }
660 return modifiedPath;
661 }
662
663
664 /**
665 * Applies all needed transformation to the image received from the gallery.
666 *
667 * @param destType In which form should we return the image
668 * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
669 */
670 private void processResultFromGallery(int destType, Intent intent) {
671 Uri uri = intent.getData();
672 if (uri == null) {
673 if (croppedUri != null) {
674 uri = croppedUri;
675 } else {
676 this.failPicture("null data from photo library");
677 return;
678 }
679 }
680 int rotate = 0;
681
682 String fileLocation = FileHelper.getRealPath(uri, this.cordova);
683 LOG.d(LOG_TAG, "File locaton is: " + fileLocation);
684
685 // If you ask for video or all media type you will automatically get back a file URI
686 // and there will be no attempt to resize any returned data
687 if (this.mediaType != PICTURE) {
688 this.callbackContext.success(fileLocation);
689 }
690 else {
691 String uriString = uri.toString();
692 // Get the path to the image. Makes loading so much easier.
693 String mimeType = FileHelper.getMimeType(uriString, this.cordova);
694
695 // This is a special case to just return the path as no scaling,
696 // rotating, nor compressing needs to be done
697 if (this.targetHeight == -1 && this.targetWidth == -1 &&
698 (destType == FILE_URI || destType == NATIVE_URI) && !this.correctOrientation &&
699 mimeType.equalsIgnoreCase(getMimetypeForFormat(encodingType)))
700 {
701 this.callbackContext.success(uriString);
702 } else {
703 // If we don't have a valid image so quit.
704 if (!("image/jpeg".equalsIgnoreCase(mimeType) || "image/png".equalsIgnoreCase(mimeType))) {
705 LOG.d(LOG_TAG, "I either have a null image path or bitmap");
706 this.failPicture("Unable to retrieve path to picture!");
707 return;
708 }
709 Bitmap bitmap = null;
710 try {
711 bitmap = getScaledAndRotatedBitmap(uriString);
712 } catch (IOException e) {
713 e.printStackTrace();
714 }
715 if (bitmap == null) {
716 LOG.d(LOG_TAG, "I either have a null image path or bitmap");
717 this.failPicture("Unable to create bitmap!");
718 return;
719 }
720
721 // If sending base64 image back
722 if (destType == DATA_URL) {
723 this.processPicture(bitmap, this.encodingType);
724 }
725
726 // If sending filename back
727 else if (destType == FILE_URI || destType == NATIVE_URI) {
728 // Did we modify the image?
729 if ( (this.targetHeight > 0 && this.targetWidth > 0) ||
730 (this.correctOrientation && this.orientationCorrected) ||
731 !mimeType.equalsIgnoreCase(getMimetypeForFormat(encodingType)))
732 {
733 try {
734 String modifiedPath = this.outputModifiedBitmap(bitmap, uri);
735 // The modified image is cached by the app in order to get around this and not have to delete you
736 // application cache I'm adding the current system time to the end of the file url.
737 this.callbackContext.success("file://" + modifiedPath + "?" + System.currentTimeMillis());
738
739 } catch (Exception e) {
740 e.printStackTrace();
741 this.failPicture("Error retrieving image.");
742 }
743 } else {
744 this.callbackContext.success(fileLocation);
745 }
746 }
747 if (bitmap != null) {
748 bitmap.recycle();
749 bitmap = null;
750 }
751 System.gc();
752 }
753 }
754 }
755
756 /**
757 * Called when the camera view exits.
758 *
759 * @param requestCode The request code originally supplied to startActivityForResult(),
760 * allowing you to identify who this result came from.
761 * @param resultCode The integer result code returned by the child activity through its setResult().
762 * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
763 */
764 public void onActivityResult(int requestCode, int resultCode, Intent intent) {
765
766 // Get src and dest types from request code for a Camera Activity
767 int srcType = (requestCode / 16) - 1;
768 int destType = (requestCode % 16) - 1;
769
770 // If Camera Crop
771 if (requestCode >= CROP_CAMERA) {
772 if (resultCode == Activity.RESULT_OK) {
773
774 // Because of the inability to pass through multiple intents, this hack will allow us
775 // to pass arcane codes back.
776 destType = requestCode - CROP_CAMERA;
777 try {
778 processResultFromCamera(destType, intent);
779 } catch (IOException e) {
780 e.printStackTrace();
781 LOG.e(LOG_TAG, "Unable to write to file");
782 }
783
784 }// If cancelled
785 else if (resultCode == Activity.RESULT_CANCELED) {
786 this.failPicture("Camera cancelled.");
787 }
788
789 // If something else
790 else {
791 this.failPicture("Did not complete!");
792 }
793 }
794 // If CAMERA
795 else if (srcType == CAMERA) {
796 // If image available
797 if (resultCode == Activity.RESULT_OK) {
798 try {
799 if (this.allowEdit) {
800 Uri tmpFile = FileProvider.getUriForFile(cordova.getActivity(),
801 applicationId + ".provider",
802 createCaptureFile(this.encodingType));
803 performCrop(tmpFile, destType, intent);
804 } else {
805 this.processResultFromCamera(destType, intent);
806 }
807 } catch (IOException e) {
808 e.printStackTrace();
809 this.failPicture("Error capturing image.");
810 }
811 }
812
813 // If cancelled
814 else if (resultCode == Activity.RESULT_CANCELED) {
815 this.failPicture("Camera cancelled.");
816 }
817
818 // If something else
819 else {
820 this.failPicture("Did not complete!");
821 }
822 }
823 // If retrieving photo from library
824 else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
825 if (resultCode == Activity.RESULT_OK && intent != null) {
826 final Intent i = intent;
827 final int finalDestType = destType;
828 cordova.getThreadPool().execute(new Runnable() {
829 public void run() {
830 processResultFromGallery(finalDestType, i);
831 }
832 });
833 } else if (resultCode == Activity.RESULT_CANCELED) {
834 this.failPicture("Selection cancelled.");
835 } else {
836 this.failPicture("Selection did not complete!");
837 }
838 }
839 }
840
841 private int exifToDegrees(int exifOrientation) {
842 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
843 return 90;
844 } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
845 return 180;
846 } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
847 return 270;
848 } else {
849 return 0;
850 }
851 }
852
853 /**
854 * Write an inputstream to local disk
855 *
856 * @param fis - The InputStream to write
857 * @param dest - Destination on disk to write to
858 * @throws FileNotFoundException
859 * @throws IOException
860 */
861 private void writeUncompressedImage(InputStream fis, Uri dest) throws FileNotFoundException,
862 IOException {
863 OutputStream os = null;
864 try {
865 os = this.cordova.getActivity().getContentResolver().openOutputStream(dest);
866 byte[] buffer = new byte[4096];
867 int len;
868 while ((len = fis.read(buffer)) != -1) {
869 os.write(buffer, 0, len);
870 }
871 os.flush();
872 } finally {
873 if (os != null) {
874 try {
875 os.close();
876 } catch (IOException e) {
877 LOG.d(LOG_TAG, "Exception while closing output stream.");
878 }
879 }
880 if (fis != null) {
881 try {
882 fis.close();
883 } catch (IOException e) {
884 LOG.d(LOG_TAG, "Exception while closing file input stream.");
885 }
886 }
887 }
888 }
889 /**
890 * In the special case where the default width, height and quality are unchanged
891 * we just write the file out to disk saving the expensive Bitmap.compress function.
892 *
893 * @param src
894 * @throws FileNotFoundException
895 * @throws IOException
896 */
897 private void writeUncompressedImage(Uri src, Uri dest) throws FileNotFoundException,
898 IOException {
899
900 FileInputStream fis = new FileInputStream(FileHelper.stripFileProtocol(src.toString()));
901 writeUncompressedImage(fis, dest);
902
903 }
904
905 /**
906 * Create entry in media store for image
907 *
908 * @return uri
909 */
910 private Uri getUriFromMediaStore() {
911 ContentValues values = new ContentValues();
912 values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
913 Uri uri;
914 try {
915 uri = this.cordova.getActivity().getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
916 } catch (RuntimeException e) {
917 LOG.d(LOG_TAG, "Can't write to external media storage.");
918 try {
919 uri = this.cordova.getActivity().getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
920 } catch (RuntimeException ex) {
921 LOG.d(LOG_TAG, "Can't write to internal media storage.");
922 return null;
923 }
924 }
925 return uri;
926 }
927
928 /**
929 * Return a scaled and rotated bitmap based on the target width and height
930 *
931 * @param imageUrl
932 * @return
933 * @throws IOException
934 */
935 private Bitmap getScaledAndRotatedBitmap(String imageUrl) throws IOException {
936 // If no new width or height were specified, and orientation is not needed return the original bitmap
937 if (this.targetWidth <= 0 && this.targetHeight <= 0 && !(this.correctOrientation)) {
938 InputStream fileStream = null;
939 Bitmap image = null;
940 try {
941 fileStream = FileHelper.getInputStreamFromUriString(imageUrl, cordova);
942 image = BitmapFactory.decodeStream(fileStream);
943 } finally {
944 if (fileStream != null) {
945 try {
946 fileStream.close();
947 } catch (IOException e) {
948 LOG.d(LOG_TAG, "Exception while closing file input stream.");
949 }
950 }
951 }
952 return image;
953 }
954
955
956 /* Copy the inputstream to a temporary file on the device.
957 We then use this temporary file to determine the width/height/orientation.
958 This is the only way to determine the orientation of the photo coming from 3rd party providers (Google Drive, Dropbox,etc)
959 This also ensures we create a scaled bitmap with the correct orientation
960
961 We delete the temporary file once we are done
962 */
963 File localFile = null;
964 Uri galleryUri = null;
965 int rotate = 0;
966 try {
967 InputStream fileStream = FileHelper.getInputStreamFromUriString(imageUrl, cordova);
968 if (fileStream != null) {
969 // Generate a temporary file
970 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
971 String fileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png");
972 localFile = new File(getTempDirectoryPath() + fileName);
973 galleryUri = Uri.fromFile(localFile);
974 writeUncompressedImage(fileStream, galleryUri);
975 try {
976 String mimeType = FileHelper.getMimeType(imageUrl.toString(), cordova);
977 if ("image/jpeg".equalsIgnoreCase(mimeType)) {
978 // ExifInterface doesn't like the file:// prefix
979 String filePath = galleryUri.toString().replace("file://", "");
980 // read exifData of source
981 exifData = new ExifHelper();
982 exifData.createInFile(filePath);
983 // Use ExifInterface to pull rotation information
984 if (this.correctOrientation) {
985 ExifInterface exif = new ExifInterface(filePath);
986 rotate = exifToDegrees(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED));
987 }
988 }
989 } catch (Exception oe) {
990 LOG.w(LOG_TAG,"Unable to read Exif data: "+ oe.toString());
991 rotate = 0;
992 }
993 }
994 }
995 catch (Exception e)
996 {
997 LOG.e(LOG_TAG,"Exception while getting input stream: "+ e.toString());
998 return null;
999 }
1000
1001
1002
1003 try {
1004 // figure out the original width and height of the image
1005 BitmapFactory.Options options = new BitmapFactory.Options();
1006 options.inJustDecodeBounds = true;
1007 InputStream fileStream = null;
1008 try {
1009 fileStream = FileHelper.getInputStreamFromUriString(galleryUri.toString(), cordova);
1010 BitmapFactory.decodeStream(fileStream, null, options);
1011 } finally {
1012 if (fileStream != null) {
1013 try {
1014 fileStream.close();
1015 } catch (IOException e) {
1016 LOG.d(LOG_TAG, "Exception while closing file input stream.");
1017 }
1018 }
1019 }
1020
1021
1022 //CB-2292: WTF? Why is the width null?
1023 if (options.outWidth == 0 || options.outHeight == 0) {
1024 return null;
1025 }
1026
1027 // User didn't specify output dimensions, but they need orientation
1028 if (this.targetWidth <= 0 && this.targetHeight <= 0) {
1029 this.targetWidth = options.outWidth;
1030 this.targetHeight = options.outHeight;
1031 }
1032
1033 // Setup target width/height based on orientation
1034 int rotatedWidth, rotatedHeight;
1035 boolean rotated= false;
1036 if (rotate == 90 || rotate == 270) {
1037 rotatedWidth = options.outHeight;
1038 rotatedHeight = options.outWidth;
1039 rotated = true;
1040 } else {
1041 rotatedWidth = options.outWidth;
1042 rotatedHeight = options.outHeight;
1043 }
1044
1045 // determine the correct aspect ratio
1046 int[] widthHeight = calculateAspectRatio(rotatedWidth, rotatedHeight);
1047
1048
1049 // Load in the smallest bitmap possible that is closest to the size we want
1050 options.inJustDecodeBounds = false;
1051 options.inSampleSize = calculateSampleSize(rotatedWidth, rotatedHeight, widthHeight[0], widthHeight[1]);
1052 Bitmap unscaledBitmap = null;
1053 try {
1054 fileStream = FileHelper.getInputStreamFromUriString(galleryUri.toString(), cordova);
1055 unscaledBitmap = BitmapFactory.decodeStream(fileStream, null, options);
1056 } finally {
1057 if (fileStream != null) {
1058 try {
1059 fileStream.close();
1060 } catch (IOException e) {
1061 LOG.d(LOG_TAG, "Exception while closing file input stream.");
1062 }
1063 }
1064 }
1065 if (unscaledBitmap == null) {
1066 return null;
1067 }
1068
1069 int scaledWidth = (!rotated) ? widthHeight[0] : widthHeight[1];
1070 int scaledHeight = (!rotated) ? widthHeight[1] : widthHeight[0];
1071
1072 Bitmap scaledBitmap = Bitmap.createScaledBitmap(unscaledBitmap, scaledWidth, scaledHeight, true);
1073 if (scaledBitmap != unscaledBitmap) {
1074 unscaledBitmap.recycle();
1075 unscaledBitmap = null;
1076 }
1077 if (this.correctOrientation && (rotate != 0)) {
1078 Matrix matrix = new Matrix();
1079 matrix.setRotate(rotate);
1080 try {
1081 scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
1082 this.orientationCorrected = true;
1083 } catch (OutOfMemoryError oom) {
1084 this.orientationCorrected = false;
1085 }
1086 }
1087 return scaledBitmap;
1088 }
1089 finally {
1090 // delete the temporary copy
1091 if (localFile != null) {
1092 localFile.delete();
1093 }
1094 }
1095
1096 }
1097
1098 /**
1099 * Maintain the aspect ratio so the resulting image does not look smooshed
1100 *
1101 * @param origWidth
1102 * @param origHeight
1103 * @return
1104 */
1105 public int[] calculateAspectRatio(int origWidth, int origHeight) {
1106 int newWidth = this.targetWidth;
1107 int newHeight = this.targetHeight;
1108
1109 // If no new width or height were specified return the original bitmap
1110 if (newWidth <= 0 && newHeight <= 0) {
1111 newWidth = origWidth;
1112 newHeight = origHeight;
1113 }
1114 // Only the width was specified
1115 else if (newWidth > 0 && newHeight <= 0) {
1116 newHeight = (int)((double)(newWidth / (double)origWidth) * origHeight);
1117 }
1118 // only the height was specified
1119 else if (newWidth <= 0 && newHeight > 0) {
1120 newWidth = (int)((double)(newHeight / (double)origHeight) * origWidth);
1121 }
1122 // If the user specified both a positive width and height
1123 // (potentially different aspect ratio) then the width or height is
1124 // scaled so that the image fits while maintaining aspect ratio.
1125 // Alternatively, the specified width and height could have been
1126 // kept and Bitmap.SCALE_TO_FIT specified when scaling, but this
1127 // would result in whitespace in the new image.
1128 else {
1129 double newRatio = newWidth / (double) newHeight;
1130 double origRatio = origWidth / (double) origHeight;
1131
1132 if (origRatio > newRatio) {
1133 newHeight = (newWidth * origHeight) / origWidth;
1134 } else if (origRatio < newRatio) {
1135 newWidth = (newHeight * origWidth) / origHeight;
1136 }
1137 }
1138
1139 int[] retval = new int[2];
1140 retval[0] = newWidth;
1141 retval[1] = newHeight;
1142 return retval;
1143 }
1144
1145 /**
1146 * Figure out what ratio we can load our image into memory at while still being bigger than
1147 * our desired width and height
1148 *
1149 * @param srcWidth
1150 * @param srcHeight
1151 * @param dstWidth
1152 * @param dstHeight
1153 * @return
1154 */
1155 public static int calculateSampleSize(int srcWidth, int srcHeight, int dstWidth, int dstHeight) {
1156 final float srcAspect = (float) srcWidth / (float) srcHeight;
1157 final float dstAspect = (float) dstWidth / (float) dstHeight;
1158
1159 if (srcAspect > dstAspect) {
1160 return srcWidth / dstWidth;
1161 } else {
1162 return srcHeight / dstHeight;
1163 }
1164 }
1165
1166 /**
1167 * Creates a cursor that can be used to determine how many images we have.
1168 *
1169 * @return a cursor
1170 */
1171 private Cursor queryImgDB(Uri contentStore) {
1172 return this.cordova.getActivity().getContentResolver().query(
1173 contentStore,
1174 new String[]{MediaStore.Images.Media._ID},
1175 null,
1176 null,
1177 null);
1178 }
1179
1180 /**
1181 * Cleans up after picture taking. Checking for duplicates and that kind of stuff.
1182 *
1183 * @param newImage
1184 */
1185 private void cleanup(int imageType, Uri oldImage, Uri newImage, Bitmap bitmap) {
1186 if (bitmap != null) {
1187 bitmap.recycle();
1188 }
1189
1190 // Clean up initial camera-written image file.
1191 (new File(FileHelper.stripFileProtocol(oldImage.toString()))).delete();
1192
1193 checkForDuplicateImage(imageType);
1194 // Scan for the gallery to update pic refs in gallery
1195 if (this.saveToPhotoAlbum && newImage != null) {
1196 this.scanForGallery(newImage);
1197 }
1198
1199 System.gc();
1200 }
1201
1202 /**
1203 * Used to find out if we are in a situation where the Camera Intent adds to images
1204 * to the content store. If we are using a FILE_URI and the number of images in the DB
1205 * increases by 2 we have a duplicate, when using a DATA_URL the number is 1.
1206 *
1207 * @param type FILE_URI or DATA_URL
1208 */
1209 private void checkForDuplicateImage(int type) {
1210 int diff = 1;
1211 Uri contentStore = whichContentStore();
1212 Cursor cursor = queryImgDB(contentStore);
1213 int currentNumOfImages = cursor.getCount();
1214
1215 if (type == FILE_URI && this.saveToPhotoAlbum) {
1216 diff = 2;
1217 }
1218
1219 // delete the duplicate file if the difference is 2 for file URI or 1 for Data URL
1220 if ((currentNumOfImages - numPics) == diff) {
1221 cursor.moveToLast();
1222 int id = Integer.valueOf(cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media._ID)));
1223 if (diff == 2) {
1224 id--;
1225 }
1226 Uri uri = Uri.parse(contentStore + "/" + id);
1227 this.cordova.getActivity().getContentResolver().delete(uri, null, null);
1228 cursor.close();
1229 }
1230 }
1231
1232 /**
1233 * Determine if we are storing the images in internal or external storage
1234 *
1235 * @return Uri
1236 */
1237 private Uri whichContentStore() {
1238 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
1239 return android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
1240 } else {
1241 return android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI;
1242 }
1243 }
1244
1245 /**
1246 * Compress bitmap using jpeg, convert to Base64 encoded string, and return to JavaScript.
1247 *
1248 * @param bitmap
1249 */
1250 public void processPicture(Bitmap bitmap, int encodingType) {
1251 ByteArrayOutputStream jpeg_data = new ByteArrayOutputStream();
1252 CompressFormat compressFormat = encodingType == JPEG ?
1253 CompressFormat.JPEG :
1254 CompressFormat.PNG;
1255
1256 try {
1257 if (bitmap.compress(compressFormat, mQuality, jpeg_data)) {
1258 byte[] code = jpeg_data.toByteArray();
1259 byte[] output = Base64.encode(code, Base64.NO_WRAP);
1260 String js_out = new String(output);
1261 this.callbackContext.success(js_out);
1262 js_out = null;
1263 output = null;
1264 code = null;
1265 }
1266 } catch (Exception e) {
1267 this.failPicture("Error compressing image.");
1268 }
1269 jpeg_data = null;
1270 }
1271
1272 /**
1273 * Send error message to JavaScript.
1274 *
1275 * @param err
1276 */
1277 public void failPicture(String err) {
1278 this.callbackContext.error(err);
1279 }
1280
1281 private void scanForGallery(Uri newImage) {
1282 this.scanMe = newImage;
1283 if (this.conn != null) {
1284 this.conn.disconnect();
1285 }
1286 this.conn = new MediaScannerConnection(this.cordova.getActivity().getApplicationContext(), this);
1287 conn.connect();
1288 }
1289
1290 public void onMediaScannerConnected() {
1291 try {
1292 this.conn.scanFile(this.scanMe.toString(), "image/*");
1293 } catch (java.lang.IllegalStateException e) {
1294 LOG.e(LOG_TAG, "Can't scan file in MediaScanner after taking picture");
1295 }
1296
1297 }
1298
1299 public void onScanCompleted(String path, Uri uri) {
1300 this.conn.disconnect();
1301 }
1302
1303
1304 public void onRequestPermissionResult(int requestCode, String[] permissions,
1305 int[] grantResults) throws JSONException {
1306 for (int r : grantResults) {
1307 if (r == PackageManager.PERMISSION_DENIED) {
1308 this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
1309 return;
1310 }
1311 }
1312 switch (requestCode) {
1313 case TAKE_PIC_SEC:
1314 takePicture(this.destType, this.encodingType);
1315 break;
1316 case SAVE_TO_ALBUM_SEC:
1317 this.getImage(this.srcType, this.destType, this.encodingType);
1318 break;
1319 }
1320 }
1321
1322 /**
1323 * Taking or choosing a picture launches another Activity, so we need to implement the
1324 * save/restore APIs to handle the case where the CordovaActivity is killed by the OS
1325 * before we get the launched Activity's result.
1326 */
1327 public Bundle onSaveInstanceState() {
1328 Bundle state = new Bundle();
1329 state.putInt("destType", this.destType);
1330 state.putInt("srcType", this.srcType);
1331 state.putInt("mQuality", this.mQuality);
1332 state.putInt("targetWidth", this.targetWidth);
1333 state.putInt("targetHeight", this.targetHeight);
1334 state.putInt("encodingType", this.encodingType);
1335 state.putInt("mediaType", this.mediaType);
1336 state.putInt("numPics", this.numPics);
1337 state.putBoolean("allowEdit", this.allowEdit);
1338 state.putBoolean("correctOrientation", this.correctOrientation);
1339 state.putBoolean("saveToPhotoAlbum", this.saveToPhotoAlbum);
1340
1341 if (this.croppedUri != null) {
1342 state.putString("croppedUri", this.croppedUri.toString());
1343 }
1344
1345 if (this.imageUri != null) {
1346 state.putString("imageUri", this.imageUri.getFileUri().toString());
1347 }
1348
1349 return state;
1350 }
1351
1352 public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {
1353 this.destType = state.getInt("destType");
1354 this.srcType = state.getInt("srcType");
1355 this.mQuality = state.getInt("mQuality");
1356 this.targetWidth = state.getInt("targetWidth");
1357 this.targetHeight = state.getInt("targetHeight");
1358 this.encodingType = state.getInt("encodingType");
1359 this.mediaType = state.getInt("mediaType");
1360 this.numPics = state.getInt("numPics");
1361 this.allowEdit = state.getBoolean("allowEdit");
1362 this.correctOrientation = state.getBoolean("correctOrientation");
1363 this.saveToPhotoAlbum = state.getBoolean("saveToPhotoAlbum");
1364
1365 if (state.containsKey("croppedUri")) {
1366 this.croppedUri = Uri.parse(state.getString("croppedUri"));
1367 }
1368
1369 if (state.containsKey("imageUri")) {
1370 //I have no idea what type of URI is being passed in
1371 this.imageUri = new CordovaUri(Uri.parse(state.getString("imageUri")));
1372 }
1373
1374 this.callbackContext = callbackContext;
1375 }
1376
1377 /*
1378 * This is dirty, but it does the job.
1379 *
1380 * Since the FilesProvider doesn't really provide you a way of getting a URL from the file,
1381 * and since we actually need the Camera to create the file for us most of the time, we don't
1382 * actually write the file, just generate the location based on a timestamp, we need to get it
1383 * back from the Intent.
1384 *
1385 * However, the FilesProvider preserves the path, so we can at least write to it from here, since
1386 * we own the context in this case.
1387 */
1388
1389 private String getFileNameFromUri(Uri uri) {
1390 String fullUri = uri.toString();
1391 String partial_path = fullUri.split("external_files")[1];
1392 File external_storage = Environment.getExternalStorageDirectory();
1393 String path = external_storage.getAbsolutePath() + partial_path;
1394 return path;
1395
1396 }
1397
1398
1399}