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 androidx.appcompat.widget; 18 19 import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT; 20 import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD; 21 import static androidx.core.view.ContentInfoCompat.SOURCE_DRAG_AND_DROP; 22 23 import android.app.Activity; 24 import android.content.ClipData; 25 import android.content.ClipboardManager; 26 import android.content.Context; 27 import android.content.ContextWrapper; 28 import android.os.Build; 29 import android.text.Selection; 30 import android.text.Spannable; 31 import android.util.Log; 32 import android.view.DragEvent; 33 import android.view.View; 34 import android.widget.TextView; 35 36 import androidx.annotation.RequiresApi; 37 import androidx.core.view.ContentInfoCompat; 38 import androidx.core.view.ViewCompat; 39 40 import org.jspecify.annotations.NonNull; 41 import org.jspecify.annotations.Nullable; 42 43 /** 44 * Common code for handling content via {@link ViewCompat#performReceiveContent}. 45 */ 46 final class AppCompatReceiveContentHelper { AppCompatReceiveContentHelper()47 private AppCompatReceiveContentHelper() {} 48 49 private static final String LOG_TAG = "ReceiveContent"; 50 51 /** 52 * If the SDK is <= 30 and the view has a {@link androidx.core.view.OnReceiveContentListener}, 53 * use the listener to handle the "Paste" and "Paste as plain text" actions. 54 * 55 * @return true if the action was handled; false otherwise 56 */ maybeHandleMenuActionViaPerformReceiveContent(@onNull TextView view, int actionId)57 static boolean maybeHandleMenuActionViaPerformReceiveContent(@NonNull TextView view, 58 int actionId) { 59 if (Build.VERSION.SDK_INT >= 31 60 || ViewCompat.getOnReceiveContentMimeTypes(view) == null 61 || !(actionId == android.R.id.paste || actionId == android.R.id.pasteAsPlainText)) { 62 return false; 63 } 64 ClipboardManager cm = (ClipboardManager) view.getContext().getSystemService( 65 Context.CLIPBOARD_SERVICE); 66 ClipData clip = (cm == null) ? null : cm.getPrimaryClip(); 67 if (clip != null && clip.getItemCount() > 0) { 68 ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD) 69 .setFlags((actionId == android.R.id.paste) ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT) 70 .build(); 71 ViewCompat.performReceiveContent(view, payload); 72 } 73 return true; 74 } 75 76 /** 77 * If the SDK is <= 30 (but >= 24) and the view has a 78 * {@link androidx.core.view.OnReceiveContentListener}, try to handle drag-and-drop via the 79 * listener. 80 * 81 * @return true if the event was handled; false otherwise 82 */ maybeHandleDragEventViaPerformReceiveContent(@onNull View view, @NonNull DragEvent event)83 static boolean maybeHandleDragEventViaPerformReceiveContent(@NonNull View view, 84 @NonNull DragEvent event) { 85 if (Build.VERSION.SDK_INT >= 31 86 || Build.VERSION.SDK_INT < 24 87 || event.getLocalState() != null 88 || ViewCompat.getOnReceiveContentMimeTypes(view) == null) { 89 return false; 90 } 91 // We make a best effort to find the activity for this view by unwrapping the context. 92 // If we are not able to find it, we can't provide default drag-and-drop handling via 93 // OnReceiveContentListener. If that happens an app can still implement custom handling 94 // using an OnDragListener or by overriding onDragEvent(). 95 final Activity activity = tryGetActivity(view); 96 if (activity == null) { 97 Log.i(LOG_TAG, "Can't handle drop: no activity: view=" + view); 98 return false; 99 } 100 if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) { 101 // We need onDragEvent to return true for ACTION_DRAG_STARTED in order to be notified 102 // of further drag events for the current drag action. TextView has the appropriate 103 // logic to return true for ACTION_DRAG_STARTED if the TextView is editable. Other 104 // widgets don't have default handling for drag-and-drop, so we return true ourselves 105 // here. 106 return !(view instanceof TextView); 107 } 108 if (event.getAction() == DragEvent.ACTION_DROP) { 109 return (view instanceof TextView) 110 ? OnDropApi24Impl.onDropForTextView(event, (TextView) view, activity) 111 : OnDropApi24Impl.onDropForView(event, view, activity); 112 } 113 return false; 114 } 115 116 @RequiresApi(24) // For Activity.requestDragAndDropPermissions() 117 private static final class OnDropApi24Impl { OnDropApi24Impl()118 private OnDropApi24Impl() {} 119 onDropForTextView(@onNull DragEvent event, @NonNull TextView view, @NonNull Activity activity)120 static boolean onDropForTextView(@NonNull DragEvent event, @NonNull TextView view, 121 @NonNull Activity activity) { 122 activity.requestDragAndDropPermissions(event); 123 final int offset = view.getOffsetForPosition(event.getX(), event.getY()); 124 view.beginBatchEdit(); 125 try { 126 Selection.setSelection((Spannable) view.getText(), offset); 127 final ContentInfoCompat payload = new ContentInfoCompat.Builder( 128 event.getClipData(), SOURCE_DRAG_AND_DROP).build(); 129 ViewCompat.performReceiveContent(view, payload); 130 } finally { 131 view.endBatchEdit(); 132 } 133 return true; 134 } 135 onDropForView(@onNull DragEvent event, @NonNull View view, @NonNull Activity activity)136 static boolean onDropForView(@NonNull DragEvent event, @NonNull View view, 137 @NonNull Activity activity) { 138 activity.requestDragAndDropPermissions(event); 139 final ContentInfoCompat payload = new ContentInfoCompat.Builder( 140 event.getClipData(), SOURCE_DRAG_AND_DROP).build(); 141 ViewCompat.performReceiveContent(view, payload); 142 return true; 143 } 144 } 145 146 /** 147 * Attempts to find the activity for the given view by unwrapping the view's context. This is 148 * a "best effort" approach that's not guaranteed to get the activity, since a view's context 149 * is not necessarily an activity. 150 * 151 * @param view The target view. 152 * @return The activity if found; null otherwise. 153 */ tryGetActivity(@onNull View view)154 static @Nullable Activity tryGetActivity(@NonNull View view) { 155 Context context = view.getContext(); 156 while (context instanceof ContextWrapper) { 157 if (context instanceof Activity) { 158 return (Activity) context; 159 } 160 context = ((ContextWrapper) context).getBaseContext(); 161 } 162 return null; 163 } 164 } 165