/** * Copyright 2010-present Facebook. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook; import android.content.Context; import android.graphics.Bitmap; import android.util.Log; import com.facebook.internal.Utility; import com.facebook.internal.Validate; import java.io.*; import java.net.URLEncoder; import java.util.*; /** * <p>This class works in conjunction with {@link NativeAppCallContentProvider} to allow apps to attach binary * attachments (e.g., images) to native dialogs launched via the {@link com.facebook.widget.FacebookDialog} * class. It stores attachments in temporary files and allows the Facebook application to retrieve them via * the content provider.</p> * * <p>Callers are generally not expected to need to use this class directly; * see {@link com.facebook.widget.FacebookDialog.OpenGraphActionDialogBuilder#setImageAttachmentsForObject(String, * java.util.List) OpenGraphActionDialogBuilder.setImageAttachmentsForObject} for an example of a function * that will accept attachments, attach them to the native dialog call, and add them to the content provider * automatically.</p> **/ public final class NativeAppCallAttachmentStore implements NativeAppCallContentProvider.AttachmentDataSource { private static final String TAG = NativeAppCallAttachmentStore.class.getName(); static final String ATTACHMENTS_DIR_NAME = "com.facebook.NativeAppCallAttachmentStore.files"; private static File attachmentsDirectory; /** * Adds a number of bitmap attachments associated with a native app call. The attachments will be * served via {@link NativeAppCallContentProvider#openFile(android.net.Uri, String) openFile}. * * @param context the Context the call is being made from * @param callId the unique ID of the call * @param imageAttachments a Map of attachment names to Bitmaps; the attachment names will be part of * the URI processed by openFile * @throws java.io.IOException */ public void addAttachmentsForCall(Context context, UUID callId, Map<String, Bitmap> imageAttachments) { Validate.notNull(context, "context"); Validate.notNull(callId, "callId"); Validate.containsNoNulls(imageAttachments.values(), "imageAttachments"); Validate.containsNoNullOrEmpty(imageAttachments.keySet(), "imageAttachments"); addAttachments(context, callId, imageAttachments, new ProcessAttachment<Bitmap>() { @Override public void processAttachment(Bitmap attachment, File outputFile) throws IOException { FileOutputStream outputStream = new FileOutputStream(outputFile); try { attachment.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); } finally { Utility.closeQuietly(outputStream); } } }); } /** * Adds a number of bitmap attachment files associated with a native app call. The attachments will be * served via {@link NativeAppCallContentProvider#openFile(android.net.Uri, String) openFile}. * * @param context the Context the call is being made from * @param callId the unique ID of the call * @param imageAttachments a Map of attachment names to Files containing the bitmaps; the attachment names will be * part of the URI processed by openFile * @throws java.io.IOException */ public void addAttachmentFilesForCall(Context context, UUID callId, Map<String, File> imageAttachmentFiles) { Validate.notNull(context, "context"); Validate.notNull(callId, "callId"); Validate.containsNoNulls(imageAttachmentFiles.values(), "imageAttachmentFiles"); Validate.containsNoNullOrEmpty(imageAttachmentFiles.keySet(), "imageAttachmentFiles"); addAttachments(context, callId, imageAttachmentFiles, new ProcessAttachment<File>() { @Override public void processAttachment(File attachment, File outputFile) throws IOException { FileOutputStream outputStream = new FileOutputStream(outputFile); FileInputStream inputStream = null; try { inputStream = new FileInputStream(attachment); byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, len); } } finally { Utility.closeQuietly(outputStream); Utility.closeQuietly(inputStream); } } }); } private <T> void addAttachments(Context context, UUID callId, Map<String, T> attachments, ProcessAttachment<T> processor) { if (attachments.size() == 0) { return; } // If this is the first time we've been instantiated, clean up any existing attachments. if (attachmentsDirectory == null) { cleanupAllAttachments(context); } ensureAttachmentsDirectoryExists(context); List<File> filesToCleanup = new ArrayList<File>(); try { for (Map.Entry<String, T> entry : attachments.entrySet()) { String attachmentName = entry.getKey(); T attachment = entry.getValue(); File file = getAttachmentFile(callId, attachmentName, true); filesToCleanup.add(file); processor.processAttachment(attachment, file); } } catch (IOException exception) { Log.e(TAG, "Got unexpected exception:" + exception); for (File file : filesToCleanup) { try { file.delete(); } catch (Exception e) { // Always try to delete other files. } } throw new FacebookException(exception); } } interface ProcessAttachment<T> { void processAttachment(T attachment, File outputFile) throws IOException; } /** * Removes any temporary files associated with a particular native app call. * * @param context the Context the call is being made from * @param callId the unique ID of the call */ public void cleanupAttachmentsForCall(Context context, UUID callId) { File dir = getAttachmentsDirectoryForCall(callId, false); Utility.deleteDirectory(dir); } @Override public File openAttachment(UUID callId, String attachmentName) throws FileNotFoundException { if (Utility.isNullOrEmpty(attachmentName) || callId == null) { throw new FileNotFoundException(); } try { return getAttachmentFile(callId, attachmentName, false); } catch (IOException e) { // We don't try to create the file, so we shouldn't get any IOExceptions. But if we do, just // act like the file wasn't found. throw new FileNotFoundException(); } } synchronized static File getAttachmentsDirectory(Context context) { if (attachmentsDirectory == null) { attachmentsDirectory = new File(context.getCacheDir(), ATTACHMENTS_DIR_NAME); } return attachmentsDirectory; } File ensureAttachmentsDirectoryExists(Context context) { File dir = getAttachmentsDirectory(context); dir.mkdirs(); return dir; } File getAttachmentsDirectoryForCall(UUID callId, boolean create) { if (attachmentsDirectory == null) { return null; } File dir = new File(attachmentsDirectory, callId.toString()); if (create && !dir.exists()) { dir.mkdirs(); } return dir; } File getAttachmentFile(UUID callId, String attachmentName, boolean createDirs) throws IOException { File dir = getAttachmentsDirectoryForCall(callId, createDirs); if (dir == null) { return null; } try { return new File(dir, URLEncoder.encode(attachmentName, "UTF-8")); } catch (UnsupportedEncodingException e) { return null; } } void cleanupAllAttachments(Context context) { // Attachments directory may or may not exist; we won't create it if not, since we are just going to delete it. File dir = getAttachmentsDirectory(context); Utility.deleteDirectory(dir); } }