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.Shared.DEBUG; 21 import static com.android.documentsui.base.Shared.VERBOSE; 22 23 import android.annotation.IntDef; 24 import android.app.AuthenticationRequiredException; 25 import android.database.Cursor; 26 import android.database.MergeCursor; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.provider.DocumentsContract; 30 import android.provider.DocumentsContract.Document; 31 import android.support.annotation.Nullable; 32 import android.support.annotation.VisibleForTesting; 33 import android.util.Log; 34 35 import com.android.documentsui.DirectoryResult; 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.roots.RootCursorWrapper; 41 import com.android.documentsui.selection.Selection; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Set; 51 import java.util.function.Predicate; 52 53 /** 54 * The data model for the current loaded directory. 55 */ 56 @VisibleForTesting 57 public class Model { 58 59 private static final String TAG = "Model"; 60 61 public @Nullable String info; 62 public @Nullable String error; 63 public @Nullable DocumentInfo doc; 64 65 private final Features mFeatures; 66 67 /** Maps Model ID to cursor positions, for looking up items by Model ID. */ 68 private final Map<String, Integer> mPositions = new HashMap<>(); 69 private final Set<String> mFileNames = new HashSet<>(); 70 71 private boolean mIsLoading; 72 private List<EventListener<Update>> mUpdateListeners = new ArrayList<>(); 73 private @Nullable Cursor mCursor; 74 private int mCursorCount; 75 private String mIds[] = new String[0]; 76 Model(Features features)77 public Model(Features features) { 78 mFeatures = features; 79 } 80 addUpdateListener(EventListener<Update> listener)81 public void addUpdateListener(EventListener<Update> listener) { 82 mUpdateListeners.add(listener); 83 } 84 removeUpdateListener(EventListener<Update> listener)85 public void removeUpdateListener(EventListener<Update> listener) { 86 mUpdateListeners.remove(listener); 87 } 88 notifyUpdateListeners()89 private void notifyUpdateListeners() { 90 for (EventListener<Update> handler: mUpdateListeners) { 91 handler.accept(Update.UPDATE); 92 } 93 } 94 notifyUpdateListeners(Exception e)95 private void notifyUpdateListeners(Exception e) { 96 Update error = new Update(e, mFeatures.isRemoteActionsEnabled()); 97 for (EventListener<Update> handler: mUpdateListeners) { 98 handler.accept(error); 99 } 100 } 101 reset()102 public void reset() { 103 mCursor = null; 104 mCursorCount = 0; 105 mIds = new String[0]; 106 mPositions.clear(); 107 info = null; 108 error = null; 109 doc = null; 110 mIsLoading = false; 111 mFileNames.clear(); 112 notifyUpdateListeners(); 113 } 114 115 @VisibleForTesting update(DirectoryResult result)116 protected void update(DirectoryResult result) { 117 assert(result != null); 118 119 if (DEBUG) Log.i(TAG, "Updating model with new result set."); 120 121 if (result.exception != null) { 122 Log.e(TAG, "Error while loading directory contents", result.exception); 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 // If the cursor is a merged cursor over multiple authorities, then prefix the ids 164 // with the authority to avoid collisions. 165 if (mCursor instanceof MergeCursor) { 166 mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY) 167 + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); 168 } else { 169 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); 170 } 171 mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME)); 172 } 173 174 // Populate the positions. 175 mPositions.clear(); 176 for (int i = 0; i < mCursorCount; ++i) { 177 mPositions.put(mIds[i], i); 178 } 179 } 180 hasFileWithName(String name)181 public boolean hasFileWithName(String name) { 182 return mFileNames.contains(name); 183 } 184 getItem(String modelId)185 public @Nullable Cursor getItem(String modelId) { 186 Integer pos = mPositions.get(modelId); 187 if (pos == null) { 188 if (DEBUG) Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId); 189 return null; 190 } 191 192 if (!mCursor.moveToPosition(pos)) { 193 if (DEBUG) Log.d(TAG, 194 "Unabled to move cursor to position " + pos + " for modelId: " + modelId); 195 return null; 196 } 197 198 return mCursor; 199 } 200 isEmpty()201 public boolean isEmpty() { 202 return mCursorCount == 0; 203 } 204 isLoading()205 public boolean isLoading() { 206 return mIsLoading; 207 } 208 getDocuments(Selection selection)209 public List<DocumentInfo> getDocuments(Selection selection) { 210 return loadDocuments(selection, DocumentFilters.ANY); 211 } 212 getDocument(String modelId)213 public @Nullable DocumentInfo getDocument(String modelId) { 214 final Cursor cursor = getItem(modelId); 215 return (cursor == null) 216 ? null 217 : DocumentInfo.fromDirectoryCursor(cursor); 218 } 219 loadDocuments(Selection selection, Predicate<Cursor> filter)220 public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) { 221 final int size = (selection != null) ? selection.size() : 0; 222 223 final List<DocumentInfo> docs = new ArrayList<>(size); 224 DocumentInfo doc; 225 for (String modelId: selection) { 226 doc = loadDocument(modelId, filter); 227 if (doc != null) { 228 docs.add(doc); 229 } 230 } 231 return docs; 232 } 233 hasDocuments(Selection selection, Predicate<Cursor> filter)234 public boolean hasDocuments(Selection selection, Predicate<Cursor> filter) { 235 for (String modelId: selection) { 236 if (loadDocument(modelId, filter) != null) { 237 return true; 238 } 239 } 240 return false; 241 } 242 243 /** 244 * @return DocumentInfo, or null. If filter returns false, null will be returned. 245 */ loadDocument(String modelId, Predicate<Cursor> filter)246 private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) { 247 final Cursor cursor = getItem(modelId); 248 249 if (cursor == null) { 250 Log.w(TAG, "Unable to obtain document for modelId: " + modelId); 251 return null; 252 } 253 254 if (filter.test(cursor)) { 255 return DocumentInfo.fromDirectoryCursor(cursor); 256 } 257 258 if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId); 259 return null; 260 } 261 getItemUri(String modelId)262 public Uri getItemUri(String modelId) { 263 final Cursor cursor = getItem(modelId); 264 return DocumentInfo.getUri(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 getException()319 public @Nullable Exception getException() { 320 return mException; 321 } 322 } 323 } 324