1 /* 2 * Copyright (C) 2013 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 package leakcanary.internal 17 18 import android.content.ContentProvider 19 import android.content.ContentValues 20 import android.content.Context 21 import android.content.Intent 22 import android.content.pm.PackageManager 23 import android.content.pm.ProviderInfo 24 import android.database.Cursor 25 import android.database.MatrixCursor 26 import android.net.Uri 27 import android.os.Build 28 import android.os.Environment 29 import android.os.ParcelFileDescriptor 30 import android.os.StrictMode 31 import android.provider.OpenableColumns 32 import android.text.TextUtils 33 import android.webkit.MimeTypeMap 34 import java.io.File 35 import java.io.FileNotFoundException 36 import java.io.IOException 37 import org.xmlpull.v1.XmlPullParser.END_DOCUMENT 38 import org.xmlpull.v1.XmlPullParser.START_TAG 39 import org.xmlpull.v1.XmlPullParserException 40 41 /** 42 * Copy of androidx.core.content.FileProvider, converted to Kotlin. 43 */ 44 internal class LeakCanaryFileProvider : ContentProvider() { 45 46 private lateinit var mStrategy: PathStrategy 47 48 /** 49 * The default FileProvider implementation does not need to be initialized. If you want to 50 * override this method, you must provide your own subclass of FileProvider. 51 */ onCreatenull52 override fun onCreate(): Boolean = true 53 54 /** 55 * After the FileProvider is instantiated, this method is called to provide the system with 56 * information about the provider. 57 * 58 * @param context A [Context] for the current component. 59 * @param info A [ProviderInfo] for the new provider. 60 */ 61 override fun attachInfo( 62 context: Context, 63 info: ProviderInfo 64 ) { 65 super.attachInfo(context, info) 66 67 // Sanity check our security 68 if (info.exported) { 69 throw SecurityException("Provider must not be exported") 70 } 71 if (!info.grantUriPermissions) { 72 throw SecurityException("Provider must grant uri permissions") 73 } 74 75 mStrategy = getPathStrategy(context, info.authority)!! 76 } 77 78 /** 79 * Use a content URI returned by 80 * [getUriForFile()][.getUriForFile] to get information about a file 81 * managed by the FileProvider. 82 * FileProvider reports the column names defined in [android.provider.OpenableColumns]: 83 * 84 * * [android.provider.OpenableColumns.DISPLAY_NAME] 85 * * [android.provider.OpenableColumns.SIZE] 86 * 87 * For more information, see 88 * [ ContentProvider.query()][ContentProvider.query]. 89 * 90 * @param uri A content URI returned by [.getUriForFile]. 91 * @param projectionArg The list of columns to put into the [Cursor]. If null all columns are 92 * included. 93 * @param selection Selection criteria to apply. If null then all data that matches the content 94 * URI is returned. 95 * @param selectionArgs An array of [java.lang.String], containing arguments to bind to 96 * the *selection* parameter. The *query* method scans *selection* from left to 97 * right and iterates through *selectionArgs*, replacing the current "?" character in 98 * *selection* with the value at the current position in *selectionArgs*. The 99 * values are bound to *selection* as [java.lang.String] values. 100 * @param sortOrder A [java.lang.String] containing the column name(s) on which to sort 101 * the resulting [Cursor]. 102 * @return A [Cursor] containing the results of the query. 103 */ querynull104 override fun query( 105 uri: Uri, 106 projectionArg: Array<String>?, 107 selection: String?, 108 selectionArgs: Array<String>?, 109 sortOrder: String? 110 ): Cursor { 111 val projection = projectionArg ?: COLUMNS 112 // ContentProvider has already checked granted permissions 113 val file = mStrategy.getFileForUri(uri) 114 115 var cols = arrayOfNulls<String>(projection.size) 116 var values = arrayOfNulls<Any>(projection.size) 117 var i = 0 118 for (col in projection) { 119 if (OpenableColumns.DISPLAY_NAME == col) { 120 cols[i] = OpenableColumns.DISPLAY_NAME 121 values[i++] = file.name 122 } else if (OpenableColumns.SIZE == col) { 123 cols[i] = OpenableColumns.SIZE 124 values[i++] = file.length() 125 } 126 } 127 128 cols = copyOfStringArray(cols, i) 129 values = copyOfAnyArray(values, i) 130 131 val cursor = MatrixCursor(cols, 1) 132 cursor.addRow(values) 133 return cursor 134 } 135 136 /** 137 * Returns the MIME type of a content URI returned by 138 * [getUriForFile()][.getUriForFile]. 139 * 140 * @param uri A content URI returned by 141 * [getUriForFile()][.getUriForFile]. 142 * @return If the associated file has an extension, the MIME type associated with that 143 * extension; otherwise `application/octet-stream`. 144 */ getTypenull145 override fun getType(uri: Uri): String { 146 // ContentProvider has already checked granted permissions 147 val file = mStrategy.getFileForUri(uri) 148 149 val lastDot = file.name.lastIndexOf('.') 150 if (lastDot >= 0) { 151 val extension = file.name.substring(lastDot + 1) 152 val mime = MimeTypeMap.getSingleton() 153 .getMimeTypeFromExtension(extension) 154 if (mime != null) { 155 return mime 156 } 157 } 158 159 return "application/octet-stream" 160 } 161 162 /** 163 * By default, this method throws an [java.lang.UnsupportedOperationException]. You must 164 * subclass FileProvider if you want to provide different functionality. 165 */ insertnull166 override fun insert( 167 uri: Uri, 168 values: ContentValues? 169 ): Uri? { 170 throw UnsupportedOperationException("No external inserts") 171 } 172 173 /** 174 * By default, this method throws an [java.lang.UnsupportedOperationException]. You must 175 * subclass FileProvider if you want to provide different functionality. 176 */ updatenull177 override fun update( 178 uri: Uri, 179 values: ContentValues?, 180 selection: String?, 181 selectionArgs: Array<String>? 182 ): Int { 183 throw UnsupportedOperationException("No external updates") 184 } 185 186 /** 187 * Deletes the file associated with the specified content URI, as 188 * returned by [getUriForFile()][.getUriForFile]. Notice that this 189 * method does **not** throw an [java.io.IOException]; you must check its return value. 190 * 191 * @param uri A content URI for a file, as returned by 192 * [getUriForFile()][.getUriForFile]. 193 * @param selection Ignored. Set to `null`. 194 * @param selectionArgs Ignored. Set to `null`. 195 * @return 1 if the delete succeeds; otherwise, 0. 196 */ deletenull197 override fun delete( 198 uri: Uri, 199 selection: String?, 200 selectionArgs: Array<String>? 201 ): Int { 202 // ContentProvider has already checked granted permissions 203 val file = mStrategy.getFileForUri(uri) 204 return if (file.delete()) 1 else 0 205 } 206 207 /** 208 * By default, FileProvider automatically returns the 209 * [ParcelFileDescriptor] for a file associated with a `content://` 210 * [Uri]. To get the [ParcelFileDescriptor], call 211 * [ ContentResolver.openFileDescriptor][android.content.ContentResolver.openFileDescriptor]. 212 * 213 * To override this method, you must provide your own subclass of FileProvider. 214 * 215 * @param uri A content URI associated with a file, as returned by 216 * [getUriForFile()][.getUriForFile]. 217 * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and 218 * write access, or "rwt" for read and write access that truncates any existing file. 219 * @return A new [ParcelFileDescriptor] with which you can access the file. 220 */ 221 @Throws(FileNotFoundException::class) openFilenull222 override fun openFile( 223 uri: Uri, 224 mode: String 225 ): ParcelFileDescriptor? { 226 // ContentProvider has already checked granted permissions 227 val file = mStrategy.getFileForUri(uri) 228 val fileMode = modeToMode(mode) 229 return ParcelFileDescriptor.open(file, fileMode) 230 } 231 232 /** 233 * Strategy for mapping between [File] and [Uri]. 234 * 235 * 236 * Strategies must be symmetric so that mapping a [File] to a 237 * [Uri] and then back to a [File] points at the original 238 * target. 239 * 240 * 241 * Strategies must remain consistent across app launches, and not rely on 242 * dynamic state. This ensures that any generated [Uri] can still be 243 * resolved if your process is killed and later restarted. 244 * 245 * @see SimplePathStrategy 246 */ 247 internal interface PathStrategy { 248 /** 249 * Return a [Uri] that represents the given [File]. 250 */ getUriForFilenull251 fun getUriForFile(file: File): Uri 252 253 /** 254 * Return a [File] that represents the given [Uri]. 255 */ 256 fun getFileForUri(uri: Uri): File 257 } 258 259 /** 260 * Strategy that provides access to files living under a narrow allowlist of 261 * filesystem roots. It will throw [SecurityException] if callers try 262 * accessing files outside the configured roots. 263 * 264 * 265 * For example, if configured with 266 * `addRoot("myfiles", context.getFilesDir())`, then 267 * `context.getFileStreamPath("foo.txt")` would map to 268 * `content://myauthority/myfiles/foo.txt`. 269 */ 270 internal class SimplePathStrategy(private val mAuthority: String) : PathStrategy { 271 private val mRoots = HashMap<String, File>() 272 273 /** 274 * Add a mapping from a name to a filesystem root. The provider only offers 275 * access to files that live under configured roots. 276 */ 277 fun addRoot( 278 name: String, 279 root: File 280 ) { 281 282 if (TextUtils.isEmpty(name)) { 283 throw IllegalArgumentException("Name must not be empty") 284 } 285 286 mRoots[name] = try { 287 // Resolve to canonical path to keep path checking fast 288 root.canonicalFile 289 } catch (e: IOException) { 290 throw IllegalArgumentException( 291 "Failed to resolve canonical path for $root", e 292 ) 293 } 294 } 295 296 override fun getUriForFile(file: File): Uri { 297 var path: String 298 try { 299 path = file.canonicalPath 300 } catch (e: IOException) { 301 throw IllegalArgumentException("Failed to resolve canonical path for $file") 302 } 303 304 // Find the most-specific root path 305 var mostSpecific: MutableMap.MutableEntry<String, File>? = null 306 for (root in mRoots.entries) { 307 val rootPath = root.value.path 308 if (path.startsWith( 309 rootPath 310 ) && (mostSpecific == null || rootPath.length > mostSpecific.value.path.length) 311 ) { 312 mostSpecific = root 313 } 314 } 315 316 if (mostSpecific == null) { 317 throw IllegalArgumentException( 318 "Failed to find configured root that contains $path" 319 ) 320 } 321 322 // Start at first char of path under root 323 val rootPath = mostSpecific.value.path 324 val startIndex = if (rootPath.endsWith("/")) rootPath.length else rootPath.length + 1 325 path = path.substring(startIndex) 326 327 // Encode the tag and path separately 328 path = Uri.encode(mostSpecific.key) + '/'.toString() + Uri.encode(path, "/") 329 return Uri.Builder() 330 .scheme("content") 331 .authority(mAuthority) 332 .encodedPath(path) 333 .build() 334 } 335 336 override fun getFileForUri(uri: Uri): File { 337 var path = uri.encodedPath!! 338 339 val splitIndex = path.indexOf('/', 1) 340 val tag = Uri.decode(path.substring(1, splitIndex)) 341 path = Uri.decode(path.substring(splitIndex + 1)) 342 343 val root = mRoots[tag] 344 ?: throw IllegalArgumentException("Unable to find configured root for $uri") 345 346 var file = File(root, path) 347 try { 348 file = file.canonicalFile 349 } catch (e: IOException) { 350 throw IllegalArgumentException("Failed to resolve canonical path for $file") 351 } 352 353 if (!file.path.startsWith(root.path)) { 354 throw SecurityException("Resolved path jumped beyond configured root") 355 } 356 357 return file 358 } 359 } 360 361 companion object { 362 private val COLUMNS = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE) 363 364 private const val META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS" 365 366 private const val TAG_ROOT_PATH = "root-path" 367 private const val TAG_FILES_PATH = "files-path" 368 private const val TAG_CACHE_PATH = "cache-path" 369 private const val TAG_EXTERNAL = "external-path" 370 private const val TAG_EXTERNAL_FILES = "external-files-path" 371 private const val TAG_EXTERNAL_CACHE = "external-cache-path" 372 private const val TAG_EXTERNAL_MEDIA = "external-media-path" 373 374 private const val ATTR_NAME = "name" 375 private const val ATTR_PATH = "path" 376 377 private val DEVICE_ROOT = File("/") 378 379 private val sCache = HashMap<String, PathStrategy>() 380 381 /** 382 * Return a content URI for a given [File]. Specific temporary 383 * permissions for the content URI can be set with 384 * [Context.grantUriPermission], or added 385 * to an [Intent] by calling [setData()][Intent.setData] and then 386 * [setFlags()][Intent.setFlags]; in both cases, the applicable flags are 387 * [Intent.FLAG_GRANT_READ_URI_PERMISSION] and 388 * [Intent.FLAG_GRANT_WRITE_URI_PERMISSION]. A FileProvider can only return a 389 * `content` [Uri] for file paths defined in their `<paths>` 390 * meta-data element. See the Class Overview for more information. 391 * 392 * @param context A [Context] for the current component. 393 * @param authority The authority of a [FileProvider] defined in a 394 * `<provider>` element in your app's manifest. 395 * @param file A [File] pointing to the filename for which you want a 396 * `content` [Uri]. 397 * @return A content URI for the file. 398 * @throws IllegalArgumentException When the given [File] is outside 399 * the paths supported by the provider. 400 */ getUriForFilenull401 fun getUriForFile( 402 context: Context, 403 authority: String, 404 file: File 405 ): Uri { 406 val strategy = getPathStrategy(context, authority) 407 return strategy!!.getUriForFile(file) 408 } 409 410 /** 411 * Return [PathStrategy] for given authority, either by parsing or 412 * returning from cache. 413 */ getPathStrategynull414 private fun getPathStrategy( 415 context: Context, 416 authority: String 417 ): PathStrategy? { 418 var strat: PathStrategy? 419 synchronized(sCache) { 420 strat = sCache[authority] 421 if (strat == null) { 422 // Minimal "fix" for https://github.com/square/leakcanary/issues/2202 423 try { 424 val previousPolicy = StrictMode.getThreadPolicy() 425 try { 426 StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().build()) 427 strat = parsePathStrategy(context, authority) 428 } finally { 429 StrictMode.setThreadPolicy(previousPolicy) 430 } 431 } catch (e: IOException) { 432 throw IllegalArgumentException( 433 "Failed to parse $META_DATA_FILE_PROVIDER_PATHS meta-data", e 434 ) 435 } catch (e: XmlPullParserException) { 436 throw IllegalArgumentException( 437 "Failed to parse $META_DATA_FILE_PROVIDER_PATHS meta-data", e 438 ) 439 } 440 sCache[authority] = strat!! 441 } 442 } 443 return strat 444 } 445 446 /** 447 * Parse and return [PathStrategy] for given authority as defined in 448 * [.META_DATA_FILE_PROVIDER_PATHS] `<meta-data>`. 449 * 450 * @see .getPathStrategy 451 */ 452 @Throws(IOException::class, XmlPullParserException::class) parsePathStrategynull453 private fun parsePathStrategy( 454 context: Context, 455 authority: String 456 ): PathStrategy { 457 val strat = SimplePathStrategy(authority) 458 459 val info = context.packageManager 460 .resolveContentProvider(authority, PackageManager.GET_META_DATA) 461 ?: throw IllegalArgumentException( 462 "Couldn't find meta-data for provider with authority $authority" 463 ) 464 val resourceParser = info.loadXmlMetaData( 465 context.packageManager, META_DATA_FILE_PROVIDER_PATHS 466 ) ?: throw IllegalArgumentException( 467 "Missing $META_DATA_FILE_PROVIDER_PATHS meta-data" 468 ) 469 470 var type: Int 471 while (run { 472 type = resourceParser.next() 473 (type) 474 } != END_DOCUMENT) { 475 if (type == START_TAG) { 476 val tag = resourceParser.name 477 478 val name = resourceParser.getAttributeValue(null, ATTR_NAME) 479 val path = resourceParser.getAttributeValue(null, ATTR_PATH) 480 481 var target: File? = null 482 if (TAG_ROOT_PATH == tag) { 483 target = DEVICE_ROOT 484 } else if (TAG_FILES_PATH == tag) { 485 target = context.filesDir 486 } else if (TAG_CACHE_PATH == tag) { 487 target = context.cacheDir 488 } else if (TAG_EXTERNAL == tag) { 489 target = Environment.getExternalStorageDirectory() 490 } else if (TAG_EXTERNAL_FILES == tag) { 491 val externalFilesDirs = getExternalFilesDirs(context, null) 492 if (externalFilesDirs.isNotEmpty()) { 493 target = externalFilesDirs[0] 494 } 495 } else if (TAG_EXTERNAL_CACHE == tag) { 496 val externalCacheDirs = getExternalCacheDirs(context) 497 if (externalCacheDirs.isNotEmpty()) { 498 target = externalCacheDirs[0] 499 } 500 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && TAG_EXTERNAL_MEDIA == tag) { 501 val externalMediaDirs = context.externalMediaDirs 502 if (externalMediaDirs.isNotEmpty()) { 503 target = externalMediaDirs[0] 504 } 505 } 506 507 if (target != null) { 508 strat.addRoot(name, buildPath(target, path)) 509 } 510 } 511 } 512 513 return strat 514 } 515 getExternalFilesDirsnull516 private fun getExternalFilesDirs( 517 context: Context, 518 type: String? 519 ): Array<File> { 520 return if (Build.VERSION.SDK_INT >= 19) { 521 context.getExternalFilesDirs(type) 522 } else { 523 arrayOf(context.getExternalFilesDir(type)!!) 524 } 525 } 526 getExternalCacheDirsnull527 private fun getExternalCacheDirs(context: Context): Array<File> { 528 return if (Build.VERSION.SDK_INT >= 19) { 529 context.externalCacheDirs 530 } else { 531 arrayOf(context.externalCacheDir!!) 532 } 533 } 534 535 /** 536 * Copied from ContentResolver.java 537 */ modeToModenull538 private fun modeToMode(mode: String): Int { 539 return when (mode) { 540 "r" -> ParcelFileDescriptor.MODE_READ_ONLY 541 "w", "wt" -> ( 542 ParcelFileDescriptor.MODE_WRITE_ONLY 543 or ParcelFileDescriptor.MODE_CREATE 544 or ParcelFileDescriptor.MODE_TRUNCATE 545 ) 546 "wa" -> ( 547 ParcelFileDescriptor.MODE_WRITE_ONLY 548 or ParcelFileDescriptor.MODE_CREATE 549 or ParcelFileDescriptor.MODE_APPEND 550 ) 551 "rw" -> ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE 552 "rwt" -> ( 553 ParcelFileDescriptor.MODE_READ_WRITE 554 or ParcelFileDescriptor.MODE_CREATE 555 or ParcelFileDescriptor.MODE_TRUNCATE 556 ) 557 else -> throw IllegalArgumentException("Invalid mode: $mode") 558 } 559 } 560 buildPathnull561 private fun buildPath( 562 base: File, 563 vararg segments: String 564 ): File { 565 var cur = base 566 for (segment in segments) { 567 cur = File(cur, segment) 568 } 569 return cur 570 } 571 copyOfStringArraynull572 private fun copyOfStringArray( 573 original: Array<String?>, 574 newLength: Int 575 ): Array<String?> { 576 val result = arrayOfNulls<String>(newLength) 577 System.arraycopy(original, 0, result, 0, newLength) 578 return result 579 } 580 copyOfAnyArraynull581 private fun copyOfAnyArray( 582 original: Array<Any?>, 583 newLength: Int 584 ): Array<Any?> { 585 val result = arrayOfNulls<Any>(newLength) 586 System.arraycopy(original, 0, result, 0, newLength) 587 return result 588 } 589 } 590 } 591