1 /* 2 * Copyright 2021 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.example.android.receivecontent; 18 19 import android.content.ClipDescription; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.res.AssetFileDescriptor; 23 import android.net.Uri; 24 import android.util.Log; 25 import android.util.Pair; 26 import android.view.ContentInfo; 27 import android.view.OnReceiveContentListener; 28 import android.view.View; 29 import android.widget.Toast; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 34 import com.google.common.util.concurrent.FutureCallback; 35 import com.google.common.util.concurrent.Futures; 36 import com.google.common.util.concurrent.ListenableFuture; 37 38 import java.io.FileNotFoundException; 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * Sample {@link OnReceiveContentListener} implementation that accepts all URIs, and delegates 44 * handling for all other content to the platform. 45 */ 46 final class MyReceiver implements OnReceiveContentListener { 47 public static final String[] SUPPORTED_MIME_TYPES = new String[]{"image/*"}; 48 49 private final AttachmentsRepo mAttachmentsRepo; 50 private final AttachmentsRecyclerViewAdapter mAttachmentsRecyclerViewAdapter; 51 MyReceiver(@onNull AttachmentsRepo attachmentsRepo, @NonNull AttachmentsRecyclerViewAdapter attachmentsRecyclerViewAdapter)52 MyReceiver(@NonNull AttachmentsRepo attachmentsRepo, 53 @NonNull AttachmentsRecyclerViewAdapter attachmentsRecyclerViewAdapter) { 54 mAttachmentsRepo = attachmentsRepo; 55 mAttachmentsRecyclerViewAdapter = attachmentsRecyclerViewAdapter; 56 } 57 58 @Nullable 59 @Override onReceiveContent(@onNull View view, @NonNull ContentInfo contentInfo)60 public ContentInfo onReceiveContent(@NonNull View view, 61 @NonNull ContentInfo contentInfo) { 62 // Split the incoming content into two groups: content URIs and everything else. 63 // This way we can implement custom handling for URIs and delegate the rest. 64 Pair<ContentInfo, ContentInfo> split = Utils.partition(contentInfo, 65 item -> item.getUri() != null); 66 ContentInfo uriContent = split.first; 67 ContentInfo remaining = split.second; 68 if (uriContent != null) { 69 receive(view.getContext(), uriContent); 70 } 71 // Return anything that we didn't handle ourselves. This preserves the default platform 72 // behavior for text and anything else for which we are not implementing custom handling. 73 return remaining; 74 } 75 76 /** 77 * Handles incoming content URIs. If the content is an image, stores it as an attachment in the 78 * app's private storage. If the content is any other type, simply shows a toast with the type 79 * of the content and its size in bytes. 80 * 81 * <p><strong>Important:</strong> It is significant that we pass along the {@code payload} 82 * object to the worker thread that will process the content, because URI permissions are tied 83 * to the payload object's lifecycle. If that object is not passed along, it could be garbage 84 * collected and permissions would be revoked prematurely (before we have a chance to process 85 * the content). 86 */ receive(@onNull Context context, @NonNull ContentInfo payload)87 private void receive(@NonNull Context context, @NonNull ContentInfo payload) { 88 Context applicationContext = context.getApplicationContext(); 89 ContentResolver contentResolver = applicationContext.getContentResolver(); 90 ListenableFuture<List<Uri>> addAttachmentsFuture = MyExecutors.bg().submit(() -> { 91 List<Uri> uris = Utils.collectUris(payload.getClip()); 92 List<Uri> localUris = new ArrayList<>(uris.size()); 93 for (Uri uri : uris) { 94 String mimeType = contentResolver.getType(uri); 95 Log.i(Logcat.TAG, "Processing " + mimeType + ": " + uri); 96 if (ClipDescription.compareMimeTypes(mimeType, "image/*")) { 97 // Read the image at the given URI and write it to private storage. 98 localUris.add(mAttachmentsRepo.write(uri)); 99 } else { 100 showMessage(applicationContext, uri, mimeType); 101 } 102 } 103 return localUris; 104 }); 105 Futures.addCallback(addAttachmentsFuture, new FutureCallback<List<Uri>>() { 106 @Override 107 public void onSuccess(List<Uri> localUris) { 108 // Show the image in the UI by passing the URI pointing to the locally stored copy 109 // to the recycler view adapter. 110 mAttachmentsRecyclerViewAdapter.addAttachments(localUris); 111 mAttachmentsRecyclerViewAdapter.notifyDataSetChanged(); 112 Log.i(Logcat.TAG, "Processed content: " + payload); 113 } 114 @Override 115 public void onFailure(@NonNull Throwable t) { 116 Log.e(Logcat.TAG,"Error processing content: " + payload, t); 117 } 118 }, MyExecutors.main()); 119 } 120 121 /** 122 * Reads the size of the given content URI and shows a toast with the type of the content and 123 * its size in bytes. 124 */ showMessage(@onNull Context applicationContext, @NonNull Uri uri, @NonNull String mimeType)125 private void showMessage(@NonNull Context applicationContext, 126 @NonNull Uri uri, @NonNull String mimeType) { 127 MyExecutors.bg().execute(() -> { 128 ContentResolver contentResolver = applicationContext.getContentResolver(); 129 long lengthBytes; 130 try { 131 AssetFileDescriptor fd = contentResolver.openAssetFileDescriptor(uri, "r"); 132 lengthBytes = fd.getLength(); 133 } catch (FileNotFoundException e) { 134 Log.e(Logcat.TAG, "Error opening content URI: " + uri, e); 135 return; 136 } 137 String msg = "Received " + mimeType + " (" + lengthBytes + " bytes): " + uri; 138 Log.i(Logcat.TAG, msg); 139 MyExecutors.main().execute(() -> { 140 Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG).show(); 141 }); 142 }); 143 } 144 } 145