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