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 77 public Model(Features features) { 78 mFeatures = features; 79 } 80 81 public void addUpdateListener(EventListener<Update> listener) { 82 mUpdateListeners.add(listener); 83 } 84 85 public void removeUpdateListener(EventListener<Update> listener) { 86 mUpdateListeners.remove(listener); 87 } 88 89 private void notifyUpdateListeners() { 90 for (EventListener<Update> handler: mUpdateListeners) { 91 handler.accept(Update.UPDATE); 92 } 93 } 94 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 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 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 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 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 */ 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 // If the cursor is a merged cursor over multiple authorities, then prefix the ids 165 // with the authority to avoid collisions. 166 if (mCursor instanceof MergeCursor) { 167 mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY) 168 + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); 169 } else { 170 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); 171 } 172 mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME)); 173 } 174 175 // Populate the positions. 176 mPositions.clear(); 177 for (int i = 0; i < mCursorCount; ++i) { 178 mPositions.put(mIds[i], i); 179 } 180 } 181 182 public boolean hasFileWithName(String name) { 183 return mFileNames.contains(name); 184 } 185 186 public @Nullable Cursor getItem(String modelId) { 187 Integer pos = mPositions.get(modelId); 188 if (pos == null) { 189 if (DEBUG) Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId); 190 return null; 191 } 192 193 if (!mCursor.moveToPosition(pos)) { 194 if (DEBUG) Log.d(TAG, 195 "Unabled to move cursor to position " + pos + " for modelId: " + modelId); 196 return null; 197 } 198 199 return mCursor; 200 } 201 202 public boolean isLoading() { 203 return mIsLoading; 204 } 205 206 public List<DocumentInfo> getDocuments(Selection selection) { 207 return loadDocuments(selection, DocumentFilters.ANY); 208 } 209 210 public @Nullable DocumentInfo getDocument(String modelId) { 211 final Cursor cursor = getItem(modelId); 212 return (cursor == null) 213 ? null 214 : DocumentInfo.fromDirectoryCursor(cursor); 215 } 216 217 public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) { 218 final int size = (selection != null) ? selection.size() : 0; 219 220 final List<DocumentInfo> docs = new ArrayList<>(size); 221 DocumentInfo doc; 222 for (String modelId: selection) { 223 doc = loadDocument(modelId, filter); 224 if (doc != null) { 225 docs.add(doc); 226 } 227 } 228 return docs; 229 } 230 231 public boolean hasDocuments(Selection selection, Predicate<Cursor> filter) { 232 for (String modelId: selection) { 233 if (loadDocument(modelId, filter) != null) { 234 return true; 235 } 236 } 237 return false; 238 } 239 240 /** 241 * @return DocumentInfo, or null. If filter returns false, null will be returned. 242 */ 243 private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) { 244 final Cursor cursor = getItem(modelId); 245 246 if (cursor == null) { 247 Log.w(TAG, "Unable to obtain document for modelId: " + modelId); 248 return null; 249 } 250 251 if (filter.test(cursor)) { 252 return DocumentInfo.fromDirectoryCursor(cursor); 253 } 254 255 if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId); 256 return null; 257 } 258 259 public Uri getItemUri(String modelId) { 260 final Cursor cursor = getItem(modelId); 261 return DocumentInfo.getUri(cursor); 262 } 263 264 /** 265 * @return An ordered array of model IDs representing the documents in the model. It is sorted 266 * according to the current sort order, which was set by the last model update. 267 */ 268 public String[] getModelIds() { 269 return mIds; 270 } 271 272 public static class Update { 273 274 public static final Update UPDATE = new Update(); 275 276 @IntDef(value = { 277 TYPE_UPDATE, 278 TYPE_UPDATE_EXCEPTION 279 }) 280 @Retention(RetentionPolicy.SOURCE) 281 public @interface UpdateType {} 282 public static final int TYPE_UPDATE = 0; 283 public static final int TYPE_UPDATE_EXCEPTION = 1; 284 285 private final @UpdateType int mUpdateType; 286 private final @Nullable Exception mException; 287 private final boolean mRemoteActionEnabled; 288 289 private Update() { 290 mUpdateType = TYPE_UPDATE; 291 mException = null; 292 mRemoteActionEnabled = false; 293 } 294 295 public Update(Exception exception, boolean remoteActionsEnabled) { 296 assert(exception != null); 297 mUpdateType = TYPE_UPDATE_EXCEPTION; 298 mException = exception; 299 mRemoteActionEnabled = remoteActionsEnabled; 300 } 301 302 public boolean isUpdate() { 303 return mUpdateType == TYPE_UPDATE; 304 } 305 306 public boolean hasException() { 307 return mUpdateType == TYPE_UPDATE_EXCEPTION; 308 } 309 310 public boolean hasAuthenticationException() { 311 return mRemoteActionEnabled 312 && hasException() 313 && mException instanceof AuthenticationRequiredException; 314 } 315 316 public @Nullable Exception getException() { 317 return mException; 318 } 319 } 320 } 321