diff --git a/samples/ReceiveContentDemo/Android.bp b/samples/ReceiveContentDemo/Android.bp index 7a42e2d8e..3c7f12e26 100644 --- a/samples/ReceiveContentDemo/Android.bp +++ b/samples/ReceiveContentDemo/Android.bp @@ -8,6 +8,9 @@ android_app { static_libs: [ "guava", "jsr305", + "androidx.appcompat_appcompat", + "androidx.recyclerview_recyclerview", + "com.google.android.material_material", ], sdk_version: "current", dex_preopt: { diff --git a/samples/ReceiveContentDemo/AndroidManifest.xml b/samples/ReceiveContentDemo/AndroidManifest.xml index e765a0f9c..6b51ac423 100644 --- a/samples/ReceiveContentDemo/AndroidManifest.xml +++ b/samples/ReceiveContentDemo/AndroidManifest.xml @@ -15,21 +15,29 @@ ~ limitations under the License. --> - - - + + - - + + + + + + diff --git a/samples/ReceiveContentDemo/res/drawable/container_background.xml b/samples/ReceiveContentDemo/res/drawable/container_background.xml new file mode 100644 index 000000000..4b4d37291 --- /dev/null +++ b/samples/ReceiveContentDemo/res/drawable/container_background.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/samples/ReceiveContentDemo/res/drawable/thumbnail_background.xml b/samples/ReceiveContentDemo/res/drawable/thumbnail_background.xml new file mode 100644 index 000000000..354029afc --- /dev/null +++ b/samples/ReceiveContentDemo/res/drawable/thumbnail_background.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/samples/ReceiveContentDemo/res/layout/activity_main.xml b/samples/ReceiveContentDemo/res/layout/activity_main.xml new file mode 100644 index 000000000..3c6835bf9 --- /dev/null +++ b/samples/ReceiveContentDemo/res/layout/activity_main.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ReceiveContentDemo/res/layout/attachment.xml b/samples/ReceiveContentDemo/res/layout/attachment.xml new file mode 100644 index 000000000..0d9792712 --- /dev/null +++ b/samples/ReceiveContentDemo/res/layout/attachment.xml @@ -0,0 +1,26 @@ + + + + diff --git a/samples/ReceiveContentDemo/res/layout/demo.xml b/samples/ReceiveContentDemo/res/layout/demo.xml deleted file mode 100644 index 49a0f356e..000000000 --- a/samples/ReceiveContentDemo/res/layout/demo.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - diff --git a/samples/ReceiveContentDemo/res/menu/app_menu.xml b/samples/ReceiveContentDemo/res/menu/app_menu.xml new file mode 100644 index 000000000..52f9cc678 --- /dev/null +++ b/samples/ReceiveContentDemo/res/menu/app_menu.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/samples/ReceiveContentDemo/res/values/colors.xml b/samples/ReceiveContentDemo/res/values/colors.xml new file mode 100644 index 000000000..376199d65 --- /dev/null +++ b/samples/ReceiveContentDemo/res/values/colors.xml @@ -0,0 +1,21 @@ + + + + + #5f6368 + #ffffff + diff --git a/samples/ReceiveContentDemo/res/values/strings.xml b/samples/ReceiveContentDemo/res/values/strings.xml index 57c7a6043..f7d1bfe05 100644 --- a/samples/ReceiveContentDemo/res/values/strings.xml +++ b/samples/ReceiveContentDemo/res/values/strings.xml @@ -6,7 +6,7 @@ ~ 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 + ~ 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, @@ -16,12 +16,8 @@ --> - Receive Content Demo - Default EditText - - EditText with a listener that handles images - - - EditText with a listener that handles all content - + Android: Receive Content Demo + Clear attachments + Try inserting text, images and other content + using copy/paste or drag-and-drop diff --git a/samples/ReceiveContentDemo/res/values/styles.xml b/samples/ReceiveContentDemo/res/values/styles.xml new file mode 100644 index 000000000..73ed947e4 --- /dev/null +++ b/samples/ReceiveContentDemo/res/values/styles.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/samples/ReceiveContentDemo/res/xml/file_paths.xml b/samples/ReceiveContentDemo/res/xml/file_paths.xml new file mode 100644 index 000000000..ffb103e61 --- /dev/null +++ b/samples/ReceiveContentDemo/res/xml/file_paths.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/AttachmentsRecyclerViewAdapter.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/AttachmentsRecyclerViewAdapter.java new file mode 100644 index 000000000..cfdf32d6d --- /dev/null +++ b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/AttachmentsRecyclerViewAdapter.java @@ -0,0 +1,75 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * 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.example.android.receivecontent; + +import android.net.Uri; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +final class AttachmentsRecyclerViewAdapter extends + RecyclerView.Adapter { + + static final class MyViewHolder extends RecyclerView.ViewHolder { + public AppCompatImageView mAttachmentThumbnailView; + + MyViewHolder(AppCompatImageView attachmentThumbnailView) { + super(attachmentThumbnailView); + mAttachmentThumbnailView = attachmentThumbnailView; + } + } + + private final List mAttachments; + + AttachmentsRecyclerViewAdapter(List attachments) { + mAttachments = new ArrayList<>(attachments); + } + + public void addAttachment(Uri uri) { + mAttachments.add(uri); + } + + public void clearAttachments() { + mAttachments.clear(); + } + + @Override + public int getItemCount() { + return mAttachments.size(); + } + + @NonNull + @Override + public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + AppCompatImageView view = (AppCompatImageView) LayoutInflater.from(parent.getContext()) + .inflate(R.layout.attachment, parent, false); + return new MyViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { + Uri uri = mAttachments.get(position); + holder.mAttachmentThumbnailView.setImageURI(uri); + holder.mAttachmentThumbnailView.setClipToOutline(true); + } +} diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/AttachmentsRepo.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/AttachmentsRepo.java new file mode 100644 index 000000000..228db9351 --- /dev/null +++ b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/AttachmentsRepo.java @@ -0,0 +1,111 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * 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.example.android.receivecontent; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteStreams; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +/** + * Stores attachments as files in the app's private storage directory (see + * {@link Context#getDataDir()}, {@link Context#getFilesDir()}, etc). + */ +final class AttachmentsRepo { + + // This matches the name declared in AndroidManifest.xml + private static final String FILE_PROVIDER_AUTHORITY = + "com.example.android.receivecontent.fileprovider"; + + private final Context mContext; + private final File mAttachmentsDir; + + AttachmentsRepo(@NonNull Context context) { + mContext = context; + mAttachmentsDir = new File(mContext.getFilesDir(), "attachments"); + } + + /** + * Reads the content at the given URI and writes it to private storage. Then returns a content + * URI referencing the newly written file. + */ + @NonNull + public Uri write(@NonNull Uri uri) { + ContentResolver contentResolver = mContext.getContentResolver(); + String mimeType = contentResolver.getType(uri); + String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + try (InputStream is = contentResolver.openInputStream(uri)) { + if (is == null) { + throw new IllegalArgumentException(String.valueOf(uri)); + } + mAttachmentsDir.mkdirs(); + String fileName = "a-" + UUID.randomUUID().toString() + "." + ext; + File newAttachment = new File(mAttachmentsDir, fileName); + try (OutputStream os = new FileOutputStream(newAttachment);) { + ByteStreams.copy(is, os); + } + Log.i(Logcat.TAG, + "Wrote file [" + fileName + "]: " + newAttachment.length() + " bytes"); + return getUriForFile(newAttachment); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + } + + public void deleteAll() { + File[] files = mAttachmentsDir.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + file.delete(); + } + } + + @NonNull + public ImmutableList getAllUris() { + File[] files = mAttachmentsDir.listFiles(); + if (files == null || files.length == 0) { + return ImmutableList.of(); + } + ImmutableList.Builder uris = ImmutableList.builderWithExpectedSize(files.length); + for (File file : files) { + uris.add(getUriForFile(file)); + } + return uris.build(); + } + + @NonNull + private Uri getUriForFile(@NonNull File file) { + return FileProvider.getUriForFile(mContext, FILE_PROVIDER_AUTHORITY, file); + } +} diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/Logcat.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/Logcat.java new file mode 100644 index 000000000..2bafa31e7 --- /dev/null +++ b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/Logcat.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * 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.example.android.receivecontent; + +final class Logcat { + private Logcat() {} + + public static final String TAG = "ReceiveContentDemo"; +} diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyExecutors.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyExecutors.java index e2c73b2f8..9dbef3b3a 100644 --- a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyExecutors.java +++ b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyExecutors.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,39 @@ package com.example.android.receivecontent; -import java.util.concurrent.ExecutorService; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.google.common.util.concurrent.ListeningScheduledExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.concurrent.Executor; import java.util.concurrent.Executors; -public class MyExecutors { - private static final ExecutorService mBg = Executors.newSingleThreadExecutor(); +final class MyExecutors { + private MyExecutors() {} - public static ExecutorService getBg() { - return mBg; + private static final ListeningScheduledExecutorService BG = + MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor()); + + private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()); + private static final Executor MAIN_EXECUTOR = + runnable -> { + if (!MAIN_HANDLER.post(runnable)) { + Log.e(Logcat.TAG, "Failed to post runnable on main thread"); + } + }; + + @NonNull + public static ListeningScheduledExecutorService bg() { + return BG; + } + + @NonNull + public static Executor main() { + return MAIN_EXECUTOR; } } diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyListenerAllContent.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyListenerAllContent.java deleted file mode 100644 index 3cc7efa24..000000000 --- a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyListenerAllContent.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * 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.example.android.receivecontent; - -import static com.example.android.receivecontent.ReceiveContentDemoActivity.LOG_TAG; -import static com.example.android.receivecontent.Utils.matchesAny; -import static com.example.android.receivecontent.Utils.showMessage; - -import android.content.ClipData; -import android.content.ClipDescription; -import android.content.ContentResolver; -import android.net.Uri; -import android.util.Log; -import android.view.ContentInfo; -import android.view.OnReceiveContentListener; -import android.view.View; - -import java.util.ArrayList; - -/** - * Sample implementation that: - *
    - *
  • Accepts images and mp4 videos. - *
  • Rejects other content URIs. - *
  • Coerces all other content to lower-case, plain text and delegates its insertion to the - * platform. - *
- */ -public class MyListenerAllContent implements OnReceiveContentListener { - static final String[] SUPPORTED_MIME_TYPES = new String[]{"image/*", "video/mp4"}; - - @Override - public ContentInfo onReceiveContent(View view, ContentInfo payload) { - ClipData clip = payload.getClip(); - ClipDescription description = clip.getDescription(); - ArrayList remainingItems = new ArrayList<>(); - for (int i = 0; i < clip.getItemCount(); i++) { - ClipData.Item item = clip.getItemAt(i); - Uri uri = item.getUri(); - if (uri != null) { - receive(view, uri); - continue; - } - CharSequence text = item.coerceToText(view.getContext()); - text = text.toString().toLowerCase(); - remainingItems.add(new ClipData.Item( - text, item.getHtmlText(), item.getIntent(), item.getUri())); - } - - if (!remainingItems.isEmpty()) { - Log.i(LOG_TAG, "Delegating " + remainingItems.size() + " item(s) to platform"); - ClipData newClip = new ClipData(description, remainingItems.get(0)); - for (int i = 1; i < remainingItems.size(); i++) { - newClip.addItem(remainingItems.get(i)); - } - return new ContentInfo.Builder(payload).setClip(newClip).build(); - } - - return null; - } - - private static void receive(View view, Uri contentUri) { - final String viewClassName = view.getClass().getSimpleName(); - MyExecutors.getBg().submit(() -> { - ContentResolver contentResolver = view.getContext().getContentResolver(); - String mimeType = contentResolver.getType(contentUri); - if (!matchesAny(mimeType, SUPPORTED_MIME_TYPES)) { - showMessage(view, "Content of type " + mimeType + " is not supported"); - return; - } - showMessage(view, viewClassName + ": Received " + mimeType + ": " + contentUri); - }); - } -} diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyListenerImages.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyListenerImages.java deleted file mode 100644 index b64e9ff4d..000000000 --- a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyListenerImages.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * 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.example.android.receivecontent; - -import static com.example.android.receivecontent.Utils.matchesAny; -import static com.example.android.receivecontent.Utils.showMessage; - -import android.content.ClipData; -import android.content.ContentResolver; -import android.net.Uri; -import android.util.Pair; -import android.view.ContentInfo; -import android.view.OnReceiveContentListener; -import android.view.View; - -/** - * Sample implementation that accepts images, rejects other URIs, and delegates handling for all - * non-URI content to the platform. - */ -public class MyListenerImages implements OnReceiveContentListener { - static final String[] SUPPORTED_MIME_TYPES = new String[]{"image/*"}; - - @Override - public ContentInfo onReceiveContent(View view, ContentInfo payload) { - Pair split = Utils.partition(payload, - item -> item.getUri() != null); - ContentInfo uriContent = split.first; - ContentInfo remaining = split.second; - if (uriContent != null) { - ClipData clip = uriContent.getClip(); - for (int i = 0; i < clip.getItemCount(); i++) { - receive(view, clip.getItemAt(i).getUri()); - } - } - return remaining; - } - - private static void receive(View view, Uri contentUri) { - MyExecutors.getBg().submit(() -> { - ContentResolver contentResolver = view.getContext().getContentResolver(); - String mimeType = contentResolver.getType(contentUri); - if (!matchesAny(mimeType, SUPPORTED_MIME_TYPES)) { - showMessage(view, "Content of type " + mimeType + " is not supported"); - return; - } - showMessage(view, "Received " + mimeType + ": " + contentUri); - }); - } -} diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyReceiver.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyReceiver.java new file mode 100644 index 000000000..bb180e6cc --- /dev/null +++ b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/MyReceiver.java @@ -0,0 +1,140 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * 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.example.android.receivecontent; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.util.Log; +import android.util.Pair; +import android.view.ContentInfo; +import android.view.OnReceiveContentListener; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.io.FileNotFoundException; + +/** + * Sample {@link OnReceiveContentListener} implementation that accepts all URIs, and delegates + * handling for all other content to the platform. + */ +final class MyReceiver implements OnReceiveContentListener { + public static final String[] SUPPORTED_MIME_TYPES = new String[]{"image/*"}; + + private final AttachmentsRepo mAttachmentsRepo; + private final AttachmentsRecyclerViewAdapter mAttachmentsRecyclerViewAdapter; + + MyReceiver(@NonNull AttachmentsRepo attachmentsRepo, + @NonNull AttachmentsRecyclerViewAdapter attachmentsRecyclerViewAdapter) { + mAttachmentsRepo = attachmentsRepo; + mAttachmentsRecyclerViewAdapter = attachmentsRecyclerViewAdapter; + } + + @Nullable + @Override + public ContentInfo onReceiveContent(@NonNull View view, + @NonNull ContentInfo contentInfo) { + // Split the incoming content into two groups: content URIs and everything else. + // This way we can implement custom handling for URIs and delegate the rest. + Pair split = Utils.partition(contentInfo, + item -> item.getUri() != null); + ContentInfo uriContent = split.first; + ContentInfo remaining = split.second; + if (uriContent != null) { + ContentResolver contentResolver = view.getContext().getContentResolver(); + ClipData clip = uriContent.getClip(); + for (int i = 0; i < clip.getItemCount(); i++) { + Uri uri = clip.getItemAt(i).getUri(); + String mimeType = contentResolver.getType(uri); + receive(view, uri, mimeType); + } + } + // Return anything that we didn't handle ourselves. This preserves the default platform + // behavior for text and anything else for which we are not implementing custom handling. + return remaining; + } + + /** + * Handles incoming content URIs. If the content is an image, stores it as an attachment in the + * app's private storage. If the content is any other type, simply shows a toast with the type + * of the content and its size in bytes. + */ + private void receive(@NonNull View view, @NonNull Uri uri, @NonNull String mimeType) { + Log.i(Logcat.TAG, "Receiving " + mimeType + ": " + uri); + if (ClipDescription.compareMimeTypes(mimeType, "image/*")) { + createAttachment(uri, mimeType); + } else { + showMessage(view, uri, mimeType); + } + } + + /** + * Reads the image at the given URI and writes it to private storage. Then shows the image in + * the UI by passing the URI pointing to the locally stored copy to the recycler view adapter. + */ + private void createAttachment(@NonNull Uri uri, @NonNull String mimeType) { + ListenableFuture addAttachmentFuture = MyExecutors.bg().submit(() -> + mAttachmentsRepo.write(uri) + ); + Futures.addCallback(addAttachmentFuture, new FutureCallback() { + @Override + public void onSuccess(Uri result) { + mAttachmentsRecyclerViewAdapter.addAttachment(result); + mAttachmentsRecyclerViewAdapter.notifyDataSetChanged(); + } + @Override + public void onFailure(@NonNull Throwable t) { + Log.e(Logcat.TAG, + "Error receiving content: uri=" + uri + ", mimeType" + mimeType, t); + } + }, MyExecutors.main()); + } + + /** + * Reads the size of the given content URI and shows a toast with the type of the content and + * its size in bytes. + */ + private void showMessage(@NonNull View view, @NonNull Uri uri, @NonNull String mimeType) { + Context applicationContext = view.getContext().getApplicationContext(); + MyExecutors.bg().execute(() -> { + ContentResolver contentResolver = applicationContext.getContentResolver(); + long lengthBytes; + try { + AssetFileDescriptor fd = contentResolver.openAssetFileDescriptor(uri, "r"); + lengthBytes = fd.getLength(); + } catch (FileNotFoundException e) { + Log.e(Logcat.TAG, "Error opening content URI: " + uri, e); + return; + } + String msg = "Received " + mimeType + " (" + lengthBytes + " bytes): " + uri; + Log.i(Logcat.TAG, msg); + MyExecutors.main().execute(() -> { + Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG).show(); + }); + }); + } +} diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/ReceiveContentDemoActivity.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/ReceiveContentDemoActivity.java index 5f6dfb51d..6aa205921 100644 --- a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/ReceiveContentDemoActivity.java +++ b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/ReceiveContentDemoActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,32 +16,87 @@ package com.example.android.receivecontent; -import android.app.Activity; +import android.net.Uri; import android.os.Bundle; -import android.view.View; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.widget.EditText; +import android.widget.LinearLayout; -public class ReceiveContentDemoActivity extends Activity { - public static final String LOG_TAG = "ReceiveContentDemo"; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +/** Main activity for the demo. */ +public class ReceiveContentDemoActivity extends AppCompatActivity { + private AttachmentsRepo mAttachmentsRepo; + private AttachmentsRecyclerViewAdapter mAttachmentsRecyclerViewAdapter; @Override - public void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.demo); + setContentView(R.layout.activity_main); - EditText editTextImagesOnly = findViewById(R.id.edittext_images); - editTextImagesOnly.setOnReceiveContentListener( - MyListenerImages.SUPPORTED_MIME_TYPES, - new MyListenerImages()); + // Setup the app toolbar. + Toolbar toolbar = findViewById(R.id.app_toolbar); + setSupportActionBar(toolbar); - EditText editTextAllTypes = findViewById(R.id.edittext_all_types); - editTextAllTypes.setOnReceiveContentListener( - MyListenerAllContent.SUPPORTED_MIME_TYPES, - new MyListenerAllContent()); + // Setup the repository and recycler view for attachments. + mAttachmentsRepo = new AttachmentsRepo(this); + ImmutableList attachments = mAttachmentsRepo.getAllUris(); + RecyclerView attachmentsRecyclerView = findViewById(R.id.attachments_recycler_view); + attachmentsRecyclerView.setHasFixedSize(true); + mAttachmentsRecyclerViewAdapter = new AttachmentsRecyclerViewAdapter(attachments); + attachmentsRecyclerView.setAdapter(mAttachmentsRecyclerViewAdapter); - View container = findViewById(R.id.container); - container.setOnReceiveContentListener( - MyListenerAllContent.SUPPORTED_MIME_TYPES, - new MyListenerAllContent()); + // Setup the listener for receiving content. + MyReceiver receiver = new MyReceiver(mAttachmentsRepo, mAttachmentsRecyclerViewAdapter); + LinearLayout container = findViewById(R.id.container); + container.setOnReceiveContentListener(MyReceiver.SUPPORTED_MIME_TYPES, receiver); + EditText textInput = findViewById(R.id.text_input); + textInput.setOnReceiveContentListener(MyReceiver.SUPPORTED_MIME_TYPES, receiver); + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.app_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_clear_attachments) { + deleteAllAttachments(); + return true; + } + return false; + } + + private void deleteAllAttachments() { + ListenableFuture deleteAllFuture = MyExecutors.bg().submit(() -> { + mAttachmentsRepo.deleteAll(); + return null; + }); + Futures.addCallback(deleteAllFuture, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + mAttachmentsRecyclerViewAdapter.clearAttachments(); + mAttachmentsRecyclerViewAdapter.notifyDataSetChanged(); + } + @Override + public void onFailure(@NonNull Throwable t) { + Log.e(Logcat.TAG, "Error deleting attachments", t); + } + }, MyExecutors.main()); } } diff --git a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/Utils.java b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/Utils.java index c97ab9a76..5e43f7f60 100644 --- a/samples/ReceiveContentDemo/src/com/example/android/receivecontent/Utils.java +++ b/samples/ReceiveContentDemo/src/com/example/android/receivecontent/Utils.java @@ -16,15 +16,10 @@ package com.example.android.receivecontent; -import static com.example.android.receivecontent.ReceiveContentDemoActivity.LOG_TAG; - import android.content.ClipData; import android.content.ClipDescription; -import android.util.Log; import android.util.Pair; import android.view.ContentInfo; -import android.view.View; -import android.widget.Toast; import java.util.ArrayList; import java.util.List; @@ -33,24 +28,8 @@ import java.util.function.Predicate; final class Utils { private Utils() {} - public static boolean matchesAny(String mimeType, String[] targetMimeTypes) { - for (String targetMimeType : targetMimeTypes) { - if (ClipDescription.compareMimeTypes(mimeType, targetMimeType)) { - return true; - } - } - return false; - } - - public static void showMessage(View view, String msg) { - Log.i(LOG_TAG, msg); - view.getHandler().post(() -> - Toast.makeText(view.getContext(), msg, Toast.LENGTH_LONG).show() - ); - } - /** - * If you use the support library, use {@code androidx.core.view.ContentInfoCompat.partition()}. + * If you use Jetpack, use {@code androidx.core.view.ContentInfoCompat.partition()}. */ public static Pair partition(ContentInfo payload, Predicate itemPredicate) {