1 /*
2  * Copyright (C) 2024 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 android.server.wm.dndsourceapp;
18 
19 import android.app.Activity;
20 import android.content.ClipData;
21 import android.content.ClipDescription;
22 import android.content.ContentValues;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.Bundle;
26 import android.os.PersistableBundle;
27 import android.server.wm.TestLogClient;
28 import android.server.wm.dndsourceapp.R;
29 import android.view.ContentInfo;
30 import android.view.DragAndDropPermissions;
31 import android.view.DragEvent;
32 import android.view.OnReceiveContentListener;
33 import android.view.View;
34 import android.widget.TextView;
35 
36 /**
37  * Copy of dndtargetapp's DropTarget.
38  */
39 public class DropTarget extends Activity {
40     private static final String RESULT_KEY_DRAG_STARTED = "DRAG_STARTED";
41     private static final String RESULT_KEY_DRAG_ENDED = "DRAG_ENDED";
42     private static final String RESULT_KEY_EXTRAS = "EXTRAS";
43     private static final String RESULT_KEY_DROP_RESULT = "DROP";
44     private static final String RESULT_KEY_DETAILS = "DETAILS";
45     private static final String RESULT_KEY_ACCESS_AFTER = "AFTER";
46     private static final String RESULT_KEY_ACCESS_BEFORE = "BEFORE";
47     private static final String RESULT_KEY_CLIP_DATA_ERROR = "CLIP_DATA_ERROR";
48     private static final String RESULT_KEY_CLIP_DESCR_ERROR = "CLIP_DESCR_ERROR";
49     private static final String RESULT_KEY_LOCAL_STATE_ERROR = "LOCAL_STATE_ERROR";
50 
51     public static final String RESULT_OK = "OK";
52     public static final String RESULT_EXCEPTION = "Exception";
53     public static final String RESULT_MISSING = "MISSING";
54     public static final String RESULT_LEAKING = "LEAKING";
55 
56     protected static final String MAGIC_VALUE = "42";
57 
58     private TextView mTextView;
59     private TestLogClient mLogClient;
60 
61     @Override
onCreate(Bundle savedInstanceState)62     public void onCreate(Bundle savedInstanceState) {
63         super.onCreate(savedInstanceState);
64 
65         mLogClient = new TestLogClient(this, getIntent().getStringExtra("logtag"));
66 
67         View view = getLayoutInflater().inflate(R.layout.target_activity, null);
68         setContentView(view);
69 
70         setUpDropTarget("request_none", new OnDragUriReadListener(false));
71         setUpDropTarget("request_read", new OnDragUriReadListener());
72         setUpDropTarget("request_write", new OnDragUriWriteListener());
73         setUpDropTarget("request_read_nested", new OnDragUriReadPrefixListener());
74         setUpDropTarget("request_take_persistable", new OnDragUriTakePersistableListener());
75         setUpDropTarget("textview_on_receive_content_listener",
76                 new UriReadOnReceiveContentListener());
77         setUpDropTarget("edittext_on_receive_content_listener",
78                 new UriReadOnReceiveContentListener());
79         setUpDropTarget("linearlayout_on_receive_content_listener",
80                 new UriReadOnReceiveContentListener());
81     }
82 
setUpDropTarget(String mode, OnDragUriListener listener)83     private void setUpDropTarget(String mode, OnDragUriListener listener) {
84         if (!mode.equals(getIntent().getStringExtra("mode"))) {
85             return;
86         }
87         mTextView = (TextView)findViewById(R.id.drag_target);
88         mTextView.setText(mode);
89         mTextView.setOnDragListener(listener);
90     }
91 
setUpDropTarget(String mode, OnReceiveContentListener listener)92     private void setUpDropTarget(String mode, OnReceiveContentListener listener) {
93         if (!mode.equals(getIntent().getStringExtra("mode"))) {
94             return;
95         }
96         TextView defaultDropTarget = findViewById(R.id.drag_target);
97         String typeOfViewToTest = mode.substring(0, mode.indexOf('_'));
98         View dropTarget;
99         switch (typeOfViewToTest) {
100             case "textview":
101                 mTextView = defaultDropTarget;
102                 dropTarget = mTextView;
103                 break;
104             case "edittext":
105                 defaultDropTarget.setVisibility(View.GONE);
106                 mTextView = findViewById(R.id.editable_drag_target);
107                 dropTarget = mTextView;
108                 break;
109             case "linearlayout":
110                 defaultDropTarget.setVisibility(View.GONE);
111                 mTextView = findViewById(R.id.textview_in_drag_target);
112                 dropTarget = findViewById(R.id.linearlayout_drag_target);
113                 break;
114             default: throw new IllegalArgumentException("Invalid mode: " + mode);
115         }
116         mTextView.setText(mode);
117         dropTarget.setVisibility(View.VISIBLE);
118         dropTarget.setOnReceiveContentListener(new String[] {"text/*", "image/*"}, listener);
119     }
120 
checkExtraValue(DragEvent event)121     private String checkExtraValue(DragEvent event) {
122         PersistableBundle extras = event.getClipDescription().getExtras();
123         if (extras == null) {
124             return "Null";
125         }
126 
127         final String value = extras.getString("extraKey");
128         if ("extraValue".equals(value)) {
129             return RESULT_OK;
130         }
131         return value;
132     }
133 
logResult(String key, String value)134     private void logResult(String key, String value) {
135         mLogClient.record(key, value);
136         mTextView.setText(mTextView.getText() + "\n" + key + "=" + value);
137     }
138 
139     private abstract class OnDragUriListener implements View.OnDragListener {
140         private final boolean requestPermissions;
141 
OnDragUriListener(boolean requestPermissions)142         public OnDragUriListener(boolean requestPermissions) {
143             this.requestPermissions = requestPermissions;
144         }
145 
146         @Override
onDrag(View v, DragEvent event)147         public boolean onDrag(View v, DragEvent event) {
148             checkDragEvent(event);
149 
150             switch (event.getAction()) {
151                 case DragEvent.ACTION_DRAG_STARTED:
152                     logResult(RESULT_KEY_DRAG_STARTED, RESULT_OK);
153                     logResult(RESULT_KEY_EXTRAS, checkExtraValue(event));
154                     return true;
155 
156                 case DragEvent.ACTION_DRAG_ENTERED:
157                     return true;
158 
159                 case DragEvent.ACTION_DRAG_LOCATION:
160                     return true;
161 
162                 case DragEvent.ACTION_DRAG_EXITED:
163                     return true;
164 
165                 case DragEvent.ACTION_DROP:
166                     // Try accessing the Uri without the permissions grant.
167                     accessContent(event, RESULT_KEY_ACCESS_BEFORE, false);
168 
169                     // Try accessing the Uri with the permission grant (if required);
170                     accessContent(event, RESULT_KEY_DROP_RESULT, requestPermissions);
171 
172                     // Try accessing the Uri after the permissions have been released.
173                     accessContent(event, RESULT_KEY_ACCESS_AFTER, false);
174                     return true;
175 
176                 case DragEvent.ACTION_DRAG_ENDED:
177                     logResult(RESULT_KEY_DRAG_ENDED, RESULT_OK);
178                     return true;
179 
180                 default:
181                     return false;
182             }
183         }
184 
accessContent(DragEvent event, String resultKey, boolean requestPermissions)185         private void accessContent(DragEvent event, String resultKey, boolean requestPermissions) {
186             String result;
187             try {
188                 result = processDrop(event, requestPermissions);
189             } catch (SecurityException e) {
190                 result = RESULT_EXCEPTION;
191                 if (resultKey.equals(RESULT_KEY_DROP_RESULT)) {
192                     logResult(RESULT_KEY_DETAILS, e.getMessage());
193                 }
194             }
195             logResult(resultKey, result);
196         }
197 
processDrop(DragEvent event, boolean requestPermissions)198         private String processDrop(DragEvent event, boolean requestPermissions) {
199             final ClipData clipData = event.getClipData();
200             if (clipData == null) {
201                 return "Null ClipData";
202             }
203             if (clipData.getItemCount() == 0) {
204                 return "Empty ClipData";
205             }
206             ClipData.Item item = clipData.getItemAt(0);
207             if (item == null) {
208                 return "Null ClipData.Item";
209             }
210             Uri uri = item.getUri();
211             if (uri == null) {
212                 return "Null Uri";
213             }
214 
215             DragAndDropPermissions permissions = null;
216             if (requestPermissions) {
217                 permissions = requestDragAndDropPermissions(event);
218                 if (permissions == null) {
219                     return "Null DragAndDropPermissions";
220                 }
221             }
222 
223             try {
224                 return processUri(uri);
225             } finally {
226                 if (permissions != null) {
227                     permissions.release();
228                 }
229             }
230         }
231 
processUri(Uri uri)232         abstract protected String processUri(Uri uri);
233     }
234 
checkDragEvent(DragEvent event)235     private void checkDragEvent(DragEvent event) {
236         final int action = event.getAction();
237 
238         // ClipData should be available for ACTION_DROP only.
239         final ClipData clipData = event.getClipData();
240         if (action == DragEvent.ACTION_DROP) {
241             if (clipData == null) {
242                 logResult(RESULT_KEY_CLIP_DATA_ERROR, RESULT_MISSING);
243             }
244         } else {
245             if (clipData != null) {
246                 logResult(RESULT_KEY_CLIP_DATA_ERROR, RESULT_LEAKING + action);
247             }
248         }
249 
250         // ClipDescription should be always available except for ACTION_DRAG_ENDED.
251         final ClipDescription clipDescription = event.getClipDescription();
252         if (action != DragEvent.ACTION_DRAG_ENDED) {
253             if (clipDescription == null) {
254                 logResult(RESULT_KEY_CLIP_DESCR_ERROR, RESULT_MISSING + action);
255             }
256         } else {
257             if (clipDescription != null) {
258                 logResult(RESULT_KEY_CLIP_DESCR_ERROR, RESULT_LEAKING);
259             }
260         }
261 
262         // Local state should be always null for cross-app drags.
263         final Object localState = event.getLocalState();
264         if (localState != null) {
265             logResult(RESULT_KEY_LOCAL_STATE_ERROR, RESULT_LEAKING + action);
266         }
267     }
268 
269     private class OnDragUriReadListener extends OnDragUriListener {
OnDragUriReadListener(boolean requestPermissions)270         OnDragUriReadListener(boolean requestPermissions) {
271             super(requestPermissions);
272         }
273 
OnDragUriReadListener()274         OnDragUriReadListener() {
275             super(true);
276         }
277 
processUri(Uri uri)278         protected String processUri(Uri uri) {
279             return checkQueryResult(uri, MAGIC_VALUE);
280         }
281 
checkQueryResult(Uri uri, String expectedValue)282         protected String checkQueryResult(Uri uri, String expectedValue) {
283             Cursor cursor = null;
284             try {
285                 cursor = getContentResolver().query(uri, null, null, null, null);
286                 if (cursor == null) {
287                     return "Null Cursor";
288                 }
289                 cursor.moveToPosition(0);
290                 String value = cursor.getString(0);
291                 if (!expectedValue.equals(value)) {
292                     return "Wrong value: " + value;
293                 }
294                 return RESULT_OK;
295             } finally {
296                 if (cursor != null) {
297                     cursor.close();
298                 }
299             }
300         }
301     }
302 
303     private class OnDragUriWriteListener extends OnDragUriListener {
OnDragUriWriteListener()304         OnDragUriWriteListener() {
305             super(true);
306         }
307 
processUri(Uri uri)308         protected String processUri(Uri uri) {
309             ContentValues values = new ContentValues();
310             values.put("key", 100);
311             getContentResolver().update(uri, values, null, null);
312             return RESULT_OK;
313         }
314     }
315 
316     private class OnDragUriReadPrefixListener extends OnDragUriReadListener {
317         @Override
processUri(Uri uri)318         protected String processUri(Uri uri) {
319             final String result1 = queryPrefixed(uri, "1");
320             if (!result1.equals(RESULT_OK)) {
321                 return result1;
322             }
323             final String result2 = queryPrefixed(uri, "2");
324             if (!result2.equals(RESULT_OK)) {
325                 return result2;
326             }
327             return queryPrefixed(uri, "3");
328         }
329 
queryPrefixed(Uri uri, String selector)330         private String queryPrefixed(Uri uri, String selector) {
331             final Uri prefixedUri = Uri.parse(uri.toString() + "/" + selector);
332             return checkQueryResult(prefixedUri, selector);
333         }
334     }
335 
336     private class OnDragUriTakePersistableListener extends OnDragUriListener {
OnDragUriTakePersistableListener()337         OnDragUriTakePersistableListener() {
338             super(true);
339         }
340 
341         @Override
processUri(Uri uri)342         protected String processUri(Uri uri) {
343             getContentResolver().takePersistableUriPermission(
344                     uri, View.DRAG_FLAG_GLOBAL_URI_READ);
345             getContentResolver().releasePersistableUriPermission(
346                     uri, View.DRAG_FLAG_GLOBAL_URI_READ);
347             return RESULT_OK;
348         }
349     }
350 
351     private class UriReadOnReceiveContentListener implements OnReceiveContentListener {
352         @Override
onReceiveContent(View view, ContentInfo payload)353         public ContentInfo onReceiveContent(View view, ContentInfo payload) {
354             String result;
355             try {
356                 result = accessContent(payload.getClip().getItemAt(0).getUri());
357             } catch (SecurityException e) {
358                 result = RESULT_EXCEPTION;
359                 logResult(RESULT_KEY_DETAILS, e.getMessage());
360             }
361             logResult(RESULT_KEY_DROP_RESULT, result);
362             return null;
363         }
364 
accessContent(Uri uri)365         private String accessContent(Uri uri) {
366             try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)) {
367                 if (cursor == null) {
368                     return "Null Cursor";
369                 }
370                 cursor.moveToPosition(0);
371                 String value = cursor.getString(0);
372                 if (!MAGIC_VALUE.equals(value)) {
373                     return "Wrong value: " + value;
374                 }
375                 return RESULT_OK;
376             }
377         }
378     }
379 }
380