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