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