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