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