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