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