1 /* 2 * Copyright (C) 2017 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 com.example.android.contentproviderpaging 18 19 import android.content.* 20 import android.database.Cursor 21 import android.database.MatrixCursor 22 import android.net.Uri 23 import android.os.Bundle 24 import android.os.CancellationSignal 25 import android.util.Log 26 import java.io.File 27 import java.io.IOException 28 29 /** 30 * ContentProvider that demonstrates how the paging support works introduced in Android O. 31 * This class fetches the images from the local storage but the storage could be 32 * other locations such as a remote server. 33 */ 34 class ImageProvider : ContentProvider() { 35 36 private var mBaseDir: File? = null 37 38 override fun onCreate(): Boolean { 39 Log.d(TAG, "onCreate") 40 41 val context = context ?: return false 42 mBaseDir = context.filesDir 43 writeDummyFilesToStorage(context) 44 45 return true 46 } 47 48 override fun query(uri: Uri, strings: Array<String>?, s: String?, 49 strings1: Array<String>?, s1: String?): Cursor? { 50 throw UnsupportedOperationException() 51 } 52 53 override fun query(uri: Uri, projection: Array<String>?, queryArgs: Bundle, 54 cancellationSignal: CancellationSignal?): Cursor? { 55 val match = sUriMatcher.match(uri) 56 // We only support a query for multiple images, return null for other form of queries 57 // including a query for a single image. 58 when (match) { 59 IMAGES -> { 60 } 61 else -> return null 62 } 63 val result = MatrixCursor(resolveDocumentProjection(projection)) 64 65 val files = mBaseDir!!.listFiles() 66 val offset = queryArgs.getInt(ContentResolver.QUERY_ARG_OFFSET, 0) 67 val limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, Integer.MAX_VALUE) 68 Log.d(TAG, "queryChildDocuments with Bundle, Uri: " + 69 uri + ", offset: " + offset + ", limit: " + limit) 70 if (offset < 0) { 71 throw IllegalArgumentException("Offset must not be less than 0") 72 } 73 if (limit < 0) { 74 throw IllegalArgumentException("Limit must not be less than 0") 75 } 76 77 if (offset >= files.size) { 78 return result 79 } 80 81 val maxIndex = Math.min(offset + limit, files.size) 82 for (i in offset..maxIndex) { 83 includeFile(result, files[i]) 84 } 85 86 val bundle = constructExtras(queryArgs, files) 87 result.extras = bundle 88 return result 89 } 90 91 private fun constructExtras(queryArgs: Bundle, files: Array<File>): Bundle { 92 val bundle = Bundle() 93 bundle.putInt(ContentResolver.EXTRA_TOTAL_SIZE, files.size) 94 var size = 0 95 if (queryArgs.containsKey(ContentResolver.QUERY_ARG_OFFSET)) { 96 size++ 97 } 98 if (queryArgs.containsKey(ContentResolver.QUERY_ARG_LIMIT)) { 99 size++ 100 } 101 if (size > 0) { 102 val honoredArgs = arrayOfNulls<String>(size) 103 var index = 0 104 if (queryArgs.containsKey(ContentResolver.QUERY_ARG_OFFSET)) { 105 honoredArgs[index++] = ContentResolver.QUERY_ARG_OFFSET 106 } 107 if (queryArgs.containsKey(ContentResolver.QUERY_ARG_LIMIT)) { 108 honoredArgs[index++] = ContentResolver.QUERY_ARG_LIMIT 109 } 110 bundle.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, honoredArgs) 111 } 112 return bundle 113 } 114 115 override fun getType(uri: Uri): String? { 116 val match = sUriMatcher.match(uri) 117 when (match) { 118 IMAGES -> return "vnd.android.cursor.dir/images" 119 IMAGE_ID -> return "vnd.android.cursor.item/images" 120 else -> throw IllegalArgumentException(String.format("Unknown URI: %s", uri)) 121 } 122 } 123 124 override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { 125 throw UnsupportedOperationException() 126 } 127 128 override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int { 129 throw UnsupportedOperationException() 130 } 131 132 override fun update(uri: Uri, contentValues: ContentValues?, s: String?, 133 strings: Array<String>?): Int { 134 throw UnsupportedOperationException() 135 } 136 137 /** 138 * Add a representation of a file to a cursor. 139 140 * @param result the cursor to modify 141 * * 142 * @param file the File object representing the desired file (may be null if given docID) 143 */ 144 private fun includeFile(result: MatrixCursor, file: File) { 145 val row = result.newRow() 146 row.add(ImageContract.Columns.DISPLAY_NAME, file.name) 147 row.add(ImageContract.Columns.SIZE, file.length()) 148 row.add(ImageContract.Columns.ABSOLUTE_PATH, file.absolutePath) 149 } 150 151 /** 152 * Preload sample files packaged in the apk into the internal storage directory. This is a 153 * dummy function specific to this demo. The MyCloud mock cloud service doesn't actually 154 * have a backend, so it simulates by reading content from the device's internal storage. 155 */ 156 private fun writeDummyFilesToStorage(context: Context) { 157 if (mBaseDir!!.list().isNotEmpty()) { 158 return 159 } 160 161 val imageResIds = getResourceIdArray(context, R.array.image_res_ids) 162 for (i in 0..REPEAT_COUNT_WRITE_FILES - 1) { 163 for (resId in imageResIds) { 164 writeFileToInternalStorage(context, resId, "-$i.jpeg") 165 } 166 } 167 } 168 169 /** 170 * Write a file to internal storage. Used to set up our dummy "cloud server". 171 172 * @param context the Context 173 * * 174 * @param resId the resource ID of the file to write to internal storage 175 * * 176 * @param extension the file extension (ex. .png, .mp3) 177 */ 178 private fun writeFileToInternalStorage(context: Context, resId: Int, extension: String) { 179 val ins = context.resources.openRawResource(resId) 180 val buffer = ByteArray(1024) 181 try { 182 val filename = context.resources.getResourceEntryName(resId) + extension 183 val fos = context.openFileOutput(filename, Context.MODE_PRIVATE) 184 while (true) { 185 val size = ins.read(buffer, 0, 1024) 186 if (size < 0) break 187 fos.write(buffer, 0, size) 188 } 189 ins.close() 190 fos.write(buffer) 191 fos.close() 192 } catch (e: IOException) { 193 throw RuntimeException(e) 194 } 195 196 } 197 198 private fun getResourceIdArray(context: Context, arrayResId: Int): IntArray { 199 val ar = context.resources.obtainTypedArray(arrayResId) 200 val len = ar.length() 201 val resIds = IntArray(len) 202 for (i in 0..len - 1) { 203 resIds[i] = ar.getResourceId(i, 0) 204 } 205 ar.recycle() 206 return resIds 207 } 208 209 companion object { 210 211 private val TAG = "ImageDocumentsProvider" 212 213 private val IMAGES = 1 214 215 private val IMAGE_ID = 2 216 217 private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH) 218 219 init { 220 sUriMatcher.addURI(ImageContract.AUTHORITY, "images", IMAGES) 221 sUriMatcher.addURI(ImageContract.AUTHORITY, "images/#", IMAGE_ID) 222 } 223 224 // Indicated how many same images are going to be written as dummy images 225 private val REPEAT_COUNT_WRITE_FILES = 10 226 227 private fun resolveDocumentProjection(projection: Array<String>?): Array<String> { 228 return projection ?: ImageContract.PROJECTION_ALL 229 } 230 } 231 } 232