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