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.android.documentsui; 18 19 import static com.android.documentsui.base.DocumentInfo.getCursorString; 20 import static com.android.documentsui.base.SharedMinimal.DEBUG; 21 import static com.android.documentsui.base.SharedMinimal.VERBOSE; 22 23 import androidx.annotation.IntDef; 24 import android.app.AuthenticationRequiredException; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.provider.DocumentsContract; 29 import android.provider.DocumentsContract.Document; 30 import android.util.Log; 31 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.recyclerview.selection.Selection; 35 36 import com.android.documentsui.base.DocumentFilters; 37 import com.android.documentsui.base.DocumentInfo; 38 import com.android.documentsui.base.EventListener; 39 import com.android.documentsui.base.Features; 40 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Set; 49 import java.util.function.Predicate; 50 51 /** 52 * The data model for the current loaded directory. 53 */ 54 @VisibleForTesting 55 public class Model { 56 57 private static final String TAG = "Model"; 58 59 public @Nullable String info; 60 public @Nullable String error; 61 public @Nullable DocumentInfo doc; 62 63 private final Features mFeatures; 64 65 /** Maps Model ID to cursor positions, for looking up items by Model ID. */ 66 private final Map<String, Integer> mPositions = new HashMap<>(); 67 private final Set<String> mFileNames = new HashSet<>(); 68 69 private boolean mIsLoading; 70 private List<EventListener<Update>> mUpdateListeners = new ArrayList<>(); 71 private @Nullable Cursor mCursor; 72 private int mCursorCount; 73 private String mIds[] = new String[0]; 74 Model(Features features)75 public Model(Features features) { 76 mFeatures = features; 77 } 78 addUpdateListener(EventListener<Update> listener)79 public void addUpdateListener(EventListener<Update> listener) { 80 mUpdateListeners.add(listener); 81 } 82 removeUpdateListener(EventListener<Update> listener)83 public void removeUpdateListener(EventListener<Update> listener) { 84 mUpdateListeners.remove(listener); 85 } 86 notifyUpdateListeners()87 private void notifyUpdateListeners() { 88 for (EventListener<Update> handler: mUpdateListeners) { 89 handler.accept(Update.UPDATE); 90 } 91 } 92 notifyUpdateListeners(Exception e)93 private void notifyUpdateListeners(Exception e) { 94 Update error = new Update(e, mFeatures.isRemoteActionsEnabled()); 95 for (EventListener<Update> handler: mUpdateListeners) { 96 handler.accept(error); 97 } 98 } 99 reset()100 public void reset() { 101 mCursor = null; 102 mCursorCount = 0; 103 mIds = new String[0]; 104 mPositions.clear(); 105 info = null; 106 error = null; 107 doc = null; 108 mIsLoading = false; 109 mFileNames.clear(); 110 notifyUpdateListeners(); 111 } 112 113 @VisibleForTesting update(DirectoryResult result)114 public void update(DirectoryResult result) { 115 assert(result != null); 116 if (DEBUG) { 117 Log.i(TAG, "Updating model with new result set."); 118 } 119 120 if (result.exception != null) { 121 Log.e(TAG, "Error while loading directory contents", result.exception); 122 reset(); // Resets this model to avoid access to old cursors. 123 notifyUpdateListeners(result.exception); 124 return; 125 } 126 127 mCursor = result.cursor; 128 mCursorCount = mCursor.getCount(); 129 doc = result.doc; 130 131 updateModelData(); 132 133 final Bundle extras = mCursor.getExtras(); 134 if (extras != null) { 135 info = extras.getString(DocumentsContract.EXTRA_INFO); 136 error = extras.getString(DocumentsContract.EXTRA_ERROR); 137 mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false); 138 } 139 140 notifyUpdateListeners(); 141 } 142 143 @VisibleForTesting getItemCount()144 public int getItemCount() { 145 return mCursorCount; 146 } 147 148 /** 149 * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs 150 * according to the current sort order. 151 */ updateModelData()152 private void updateModelData() { 153 mIds = new String[mCursorCount]; 154 mFileNames.clear(); 155 mCursor.moveToPosition(-1); 156 for (int pos = 0; pos < mCursorCount; ++pos) { 157 if (!mCursor.moveToNext()) { 158 Log.e(TAG, "Fail to move cursor to next pos: " + pos); 159 return; 160 } 161 // Generates a Model ID for a cursor entry that refers to a document. The Model ID is a 162 // unique string that can be used to identify the document referred to by the cursor. 163 // Prefix the ids with the authority to avoid collisions. 164 mIds[pos] = ModelId.build(mCursor); 165 mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME)); 166 } 167 168 // Populate the positions. 169 mPositions.clear(); 170 for (int i = 0; i < mCursorCount; ++i) { 171 mPositions.put(mIds[i], i); 172 } 173 } 174 hasFileWithName(String name)175 public boolean hasFileWithName(String name) { 176 return mFileNames.contains(name); 177 } 178 getItem(String modelId)179 public @Nullable Cursor getItem(String modelId) { 180 Integer pos = mPositions.get(modelId); 181 if (pos == null) { 182 if (DEBUG) { 183 Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId); 184 } 185 return null; 186 } 187 188 if (!mCursor.moveToPosition(pos)) { 189 if (DEBUG) { 190 Log.d(TAG, 191 "Unabled to move cursor to position " + pos + " for modelId: " + modelId); 192 } 193 return null; 194 } 195 196 return mCursor; 197 } 198 isLoading()199 public boolean isLoading() { 200 return mIsLoading; 201 } 202 getDocuments(Selection<String> selection)203 public List<DocumentInfo> getDocuments(Selection<String> selection) { 204 return loadDocuments(selection, DocumentFilters.ANY); 205 } 206 getDocument(String modelId)207 public @Nullable DocumentInfo getDocument(String modelId) { 208 final Cursor cursor = getItem(modelId); 209 return (cursor == null) 210 ? null 211 : DocumentInfo.fromDirectoryCursor(cursor); 212 } 213 loadDocuments(Selection<String> selection, Predicate<Cursor> filter)214 public List<DocumentInfo> loadDocuments(Selection<String> selection, Predicate<Cursor> filter) { 215 final int size = (selection != null) ? selection.size() : 0; 216 217 final List<DocumentInfo> docs = new ArrayList<>(size); 218 DocumentInfo doc; 219 for (String modelId: selection) { 220 doc = loadDocument(modelId, filter); 221 if (doc != null) { 222 docs.add(doc); 223 } 224 } 225 return docs; 226 } 227 hasDocuments(Selection<String> selection, Predicate<Cursor> filter)228 public boolean hasDocuments(Selection<String> selection, Predicate<Cursor> filter) { 229 for (String modelId: selection) { 230 if (loadDocument(modelId, filter) != null) { 231 return true; 232 } 233 } 234 return false; 235 } 236 237 /** 238 * @return DocumentInfo, or null. If filter returns false, null will be returned. 239 */ loadDocument(String modelId, Predicate<Cursor> filter)240 private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) { 241 final Cursor cursor = getItem(modelId); 242 243 if (cursor == null) { 244 Log.w(TAG, "Unable to obtain document for modelId: " + modelId); 245 return null; 246 } 247 248 if (filter.test(cursor)) { 249 return DocumentInfo.fromDirectoryCursor(cursor); 250 } 251 252 if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId); 253 return null; 254 } 255 getItemUri(String modelId)256 public Uri getItemUri(String modelId) { 257 final Cursor cursor = getItem(modelId); 258 return DocumentInfo.getUri(cursor); 259 } 260 261 /** 262 * @return An ordered array of model IDs representing the documents in the model. It is sorted 263 * according to the current sort order, which was set by the last model update. 264 */ getModelIds()265 public String[] getModelIds() { 266 return mIds; 267 } 268 269 public static class Update { 270 271 public static final Update UPDATE = new Update(); 272 273 @IntDef(value = { 274 TYPE_UPDATE, 275 TYPE_UPDATE_EXCEPTION 276 }) 277 @Retention(RetentionPolicy.SOURCE) 278 public @interface UpdateType {} 279 public static final int TYPE_UPDATE = 0; 280 public static final int TYPE_UPDATE_EXCEPTION = 1; 281 282 private final @UpdateType int mUpdateType; 283 private final @Nullable Exception mException; 284 private final boolean mRemoteActionEnabled; 285 Update()286 private Update() { 287 mUpdateType = TYPE_UPDATE; 288 mException = null; 289 mRemoteActionEnabled = false; 290 } 291 Update(Exception exception, boolean remoteActionsEnabled)292 public Update(Exception exception, boolean remoteActionsEnabled) { 293 assert(exception != null); 294 mUpdateType = TYPE_UPDATE_EXCEPTION; 295 mException = exception; 296 mRemoteActionEnabled = remoteActionsEnabled; 297 } 298 isUpdate()299 public boolean isUpdate() { 300 return mUpdateType == TYPE_UPDATE; 301 } 302 hasException()303 public boolean hasException() { 304 return mUpdateType == TYPE_UPDATE_EXCEPTION; 305 } 306 hasAuthenticationException()307 public boolean hasAuthenticationException() { 308 return mRemoteActionEnabled 309 && hasException() 310 && mException instanceof AuthenticationRequiredException; 311 } 312 getException()313 public @Nullable Exception getException() { 314 return mException; 315 } 316 } 317 } 318