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