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