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 
18 package com.android.contacts.util;
19 
20 import android.content.ClipData;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.net.Uri;
27 import android.provider.MediaStore;
28 import androidx.core.content.FileProvider;
29 import android.util.Log;
30 import com.android.contacts.R;
31 
32 import com.google.common.io.Closeables;
33 import java.io.ByteArrayOutputStream;
34 import java.io.File;
35 import java.io.FileNotFoundException;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.text.SimpleDateFormat;
40 import java.util.Date;
41 import java.util.Locale;
42 
43 /**
44  * Utilities related to loading/saving contact photos.
45  *
46  */
47 public class ContactPhotoUtils {
48     private static final String TAG = "ContactPhotoUtils";
49 
50     private static final String PHOTO_DATE_FORMAT = "'IMG'_yyyyMMdd_HHmmss";
51 
52     /**
53      * Generate a new, unique file to be used as an out-of-band communication
54      * channel, since hi-res Bitmaps are too big to serialize into a Bundle.
55      * This file will be passed (as a uri) to other activities (such as the gallery/camera/
56      *  cropper/etc.), and read by us once they are finished writing it.
57      */
generateTempImageUri(Context context)58     public static Uri generateTempImageUri(Context context) {
59         final String  fileProviderAuthority = context.getResources().getString(
60                 R.string.photo_file_provider_authority);
61         return FileProvider.getUriForFile(context, fileProviderAuthority,
62                 new File(pathForTempPhoto(context, generateTempPhotoFileName())));
63     }
64 
generateTempCroppedImageUri(Context context)65     public static Uri generateTempCroppedImageUri(Context context) {
66         final String  fileProviderAuthority = context.getResources().getString(
67                 R.string.photo_file_provider_authority);
68         return FileProvider.getUriForFile(context, fileProviderAuthority,
69                 new File(pathForTempPhoto(context, generateTempCroppedPhotoFileName())));
70     }
71 
pathForTempPhoto(Context context, String fileName)72     private static String pathForTempPhoto(Context context, String fileName) {
73         final File dir = context.getCacheDir();
74         dir.mkdirs();
75         final File f = new File(dir, fileName);
76         return f.getAbsolutePath();
77     }
78 
generateTempPhotoFileName()79     private static String generateTempPhotoFileName() {
80         final Date date = new Date(System.currentTimeMillis());
81         SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT, Locale.US);
82         return "ContactPhoto-" + dateFormat.format(date) + ".jpg";
83     }
84 
generateTempCroppedPhotoFileName()85     private static String generateTempCroppedPhotoFileName() {
86         final Date date = new Date(System.currentTimeMillis());
87         SimpleDateFormat dateFormat = new SimpleDateFormat(PHOTO_DATE_FORMAT, Locale.US);
88         return "ContactPhoto-" + dateFormat.format(date) + "-cropped.jpg";
89     }
90 
91     /**
92      * Given a uri pointing to a bitmap, reads it into a bitmap and returns it.
93      * @throws FileNotFoundException
94      */
getBitmapFromUri(Context context, Uri uri)95     public static Bitmap getBitmapFromUri(Context context, Uri uri) throws FileNotFoundException {
96         final InputStream imageStream = context.getContentResolver().openInputStream(uri);
97         try {
98             return BitmapFactory.decodeStream(imageStream);
99         } finally {
100             Closeables.closeQuietly(imageStream);
101         }
102     }
103 
104     /**
105      * Creates a byte[] containing the PNG-compressed bitmap, or null if
106      * something goes wrong.
107      */
compressBitmap(Bitmap bitmap)108     public static byte[] compressBitmap(Bitmap bitmap) {
109         final int size = bitmap.getWidth() * bitmap.getHeight() * 4;
110         final ByteArrayOutputStream out = new ByteArrayOutputStream(size);
111         try {
112             bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
113             out.flush();
114             out.close();
115             return out.toByteArray();
116         } catch (IOException e) {
117             Log.w(TAG, "Unable to serialize photo: " + e.toString());
118             return null;
119         }
120     }
121 
addCropExtras(Intent intent, int photoSize)122     public static void addCropExtras(Intent intent, int photoSize) {
123         intent.putExtra("crop", "true");
124         intent.putExtra("scale", true);
125         intent.putExtra("scaleUpIfNeeded", true);
126         intent.putExtra("aspectX", 1);
127         intent.putExtra("aspectY", 1);
128         intent.putExtra("outputX", photoSize);
129         intent.putExtra("outputY", photoSize);
130     }
131 
132     /**
133      * Adds common extras to gallery intents.
134      *
135      * @param intent The intent to add extras to.
136      * @param photoUri The uri of the file to save the image to.
137      */
addPhotoPickerExtras(Intent intent, Uri photoUri)138     public static void addPhotoPickerExtras(Intent intent, Uri photoUri) {
139         intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
140         intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
141                 Intent.FLAG_GRANT_READ_URI_PERMISSION);
142         intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, photoUri));
143     }
144 
145     /**
146      * Given an input photo stored in a uri, save it to a destination uri
147      */
savePhotoFromUriToUri(Context context, Uri inputUri, Uri outputUri, boolean deleteAfterSave)148     public static boolean savePhotoFromUriToUri(Context context, Uri inputUri, Uri outputUri,
149             boolean deleteAfterSave) {
150         if (inputUri == null || outputUri == null || isFilePathAndNotStorage(inputUri)) {
151             return false;
152         }
153         try (FileOutputStream outputStream = context.getContentResolver()
154                  .openAssetFileDescriptor(outputUri, "rw").createOutputStream();
155              InputStream inputStream = context.getContentResolver().openInputStream(inputUri)) {
156 
157             final byte[] buffer = new byte[16 * 1024];
158             int length;
159             int totalLength = 0;
160             while ((length = inputStream.read(buffer)) > 0) {
161                 outputStream.write(buffer, 0, length);
162                 totalLength += length;
163             }
164             if (Log.isLoggable(TAG, Log.VERBOSE)) {
165                 Log.v(TAG, "Wrote " + totalLength + " bytes for photo " + inputUri.toString());
166             }
167         } catch (IOException | NullPointerException e) {
168             Log.e(TAG, "Failed to write photo: " + inputUri.toString() + " because: " + e);
169             return false;
170         } finally {
171             if (deleteAfterSave) {
172                 context.getContentResolver().delete(inputUri, null, null);
173             }
174         }
175         return true;
176     }
177 
178     /**
179      * Returns {@code true} if the {@code inputUri} is a FILE scheme and it does not point to
180      * the storage directory.
181      */
isFilePathAndNotStorage(Uri inputUri)182     private static boolean isFilePathAndNotStorage(Uri inputUri) {
183         if (ContentResolver.SCHEME_FILE.equals(inputUri.getScheme())) {
184             try {
185                 File file = new File(inputUri.getPath()).getCanonicalFile();
186                 return !file.getCanonicalPath().startsWith("/storage/");
187             } catch (IOException e) {
188                 return false;
189             }
190         }
191         return false;
192     }
193 }
194