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