| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.documentsui; |
| |
| import static com.android.documentsui.base.SharedMinimal.DEBUG; |
| import static com.android.documentsui.base.SharedMinimal.VERBOSE; |
| |
| import android.app.AuthenticationRequiredException; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.provider.DocumentsContract; |
| import android.util.Log; |
| |
| import androidx.annotation.IntDef; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.recyclerview.selection.Selection; |
| |
| import com.android.documentsui.base.DocumentFilters; |
| import com.android.documentsui.base.DocumentInfo; |
| import com.android.documentsui.base.EventListener; |
| import com.android.documentsui.base.Features; |
| import com.android.documentsui.base.UserId; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.function.Predicate; |
| |
| /** |
| * The data model for the current loaded directory. |
| */ |
| @VisibleForTesting |
| public class Model { |
| |
| private static final String TAG = "Model"; |
| |
| public @Nullable String info; |
| public @Nullable String error; |
| public @Nullable DocumentInfo doc; |
| |
| private final Features mFeatures; |
| |
| /** Maps Model ID to cursor positions, for looking up items by Model ID. */ |
| private final Map<String, Integer> mPositions = new HashMap<>(); |
| private final Set<String> mFileNames = new HashSet<>(); |
| |
| private boolean mIsLoading; |
| private List<EventListener<Update>> mUpdateListeners = new ArrayList<>(); |
| private @Nullable Cursor mCursor; |
| private int mCursorCount; |
| private String mIds[] = new String[0]; |
| |
| public Model(Features features) { |
| mFeatures = features; |
| } |
| |
| public void addUpdateListener(EventListener<Update> listener) { |
| mUpdateListeners.add(listener); |
| } |
| |
| public void removeUpdateListener(EventListener<Update> listener) { |
| mUpdateListeners.remove(listener); |
| } |
| |
| private void notifyUpdateListeners() { |
| for (EventListener<Update> handler: mUpdateListeners) { |
| handler.accept(Update.UPDATE); |
| } |
| } |
| |
| private void notifyUpdateListeners(Exception e) { |
| Update error = new Update(e, mFeatures.isRemoteActionsEnabled()); |
| for (EventListener<Update> handler: mUpdateListeners) { |
| handler.accept(error); |
| } |
| } |
| |
| public void reset() { |
| mCursor = null; |
| mCursorCount = 0; |
| mIds = new String[0]; |
| mPositions.clear(); |
| info = null; |
| error = null; |
| doc = null; |
| mIsLoading = false; |
| mFileNames.clear(); |
| notifyUpdateListeners(); |
| } |
| |
| @VisibleForTesting |
| public void update(DirectoryResult result) { |
| assert(result != null); |
| if (DEBUG) { |
| Log.i(TAG, "Updating model with new result set."); |
| } |
| |
| if (result.exception != null) { |
| Log.e(TAG, "Error while loading directory contents", result.exception); |
| reset(); // Resets this model to avoid access to old cursors. |
| notifyUpdateListeners(result.exception); |
| return; |
| } |
| |
| mCursor = result.getCursor(); |
| mCursorCount = mCursor.getCount(); |
| doc = result.doc; |
| |
| if (result.getModelIds() != null && result.getFileNames() != null) { |
| mIds = result.getModelIds(); |
| mFileNames.clear(); |
| mFileNames.addAll(result.getFileNames()); |
| |
| // Populate the positions. |
| mPositions.clear(); |
| for (int i = 0; i < mCursorCount; ++i) { |
| mPositions.put(mIds[i], i); |
| } |
| } |
| |
| final Bundle extras = mCursor.getExtras(); |
| if (extras != null) { |
| info = extras.getString(DocumentsContract.EXTRA_INFO); |
| error = extras.getString(DocumentsContract.EXTRA_ERROR); |
| mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false); |
| } |
| |
| notifyUpdateListeners(); |
| } |
| |
| @VisibleForTesting |
| public int getItemCount() { |
| return mCursorCount; |
| } |
| |
| public boolean hasFileWithName(String name) { |
| return mFileNames.contains(name); |
| } |
| |
| public @Nullable Cursor getItem(String modelId) { |
| Integer pos = mPositions.get(modelId); |
| if (pos == null) { |
| if (DEBUG) { |
| Log.d(TAG, "Unabled to find cursor position for modelId: " + modelId); |
| } |
| return null; |
| } |
| |
| if (!mCursor.moveToPosition(pos)) { |
| if (DEBUG) { |
| Log.d(TAG, |
| "Unabled to move cursor to position " + pos + " for modelId: " + modelId); |
| } |
| return null; |
| } |
| |
| return mCursor; |
| } |
| |
| public boolean isLoading() { |
| return mIsLoading; |
| } |
| |
| public List<DocumentInfo> getDocuments(Selection<String> selection) { |
| return loadDocuments(selection, DocumentFilters.ANY); |
| } |
| |
| public @Nullable DocumentInfo getDocument(String modelId) { |
| final Cursor cursor = getItem(modelId); |
| return (cursor == null) |
| ? null |
| : DocumentInfo.fromDirectoryCursor(cursor); |
| } |
| |
| public List<DocumentInfo> loadDocuments(Selection<String> selection, Predicate<Cursor> filter) { |
| final int size = (selection != null) ? selection.size() : 0; |
| |
| final List<DocumentInfo> docs = new ArrayList<>(size); |
| DocumentInfo doc; |
| for (String modelId: selection) { |
| doc = loadDocument(modelId, filter); |
| if (doc != null) { |
| docs.add(doc); |
| } |
| } |
| return docs; |
| } |
| |
| public boolean hasDocuments(Selection<String> selection, Predicate<Cursor> filter) { |
| for (String modelId: selection) { |
| if (loadDocument(modelId, filter) != null) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * @return DocumentInfo, or null. If filter returns false, null will be returned. |
| */ |
| private @Nullable DocumentInfo loadDocument(String modelId, Predicate<Cursor> filter) { |
| final Cursor cursor = getItem(modelId); |
| |
| if (cursor == null) { |
| Log.w(TAG, "Unable to obtain document for modelId: " + modelId); |
| return null; |
| } |
| |
| if (filter.test(cursor)) { |
| return DocumentInfo.fromDirectoryCursor(cursor); |
| } |
| |
| if (VERBOSE) Log.v(TAG, "Filtered out document from results: " + modelId); |
| return null; |
| } |
| |
| public Uri getItemUri(String modelId) { |
| final Cursor cursor = getItem(modelId); |
| return DocumentInfo.getUri(cursor); |
| } |
| |
| public UserId getItemUserId(String modelId) { |
| final Cursor cursor = getItem(modelId); |
| return DocumentInfo.getUserId(cursor); |
| } |
| |
| /** |
| * @return An ordered array of model IDs representing the documents in the model. It is sorted |
| * according to the current sort order, which was set by the last model update. |
| */ |
| public String[] getModelIds() { |
| return mIds; |
| } |
| |
| public static class Update { |
| |
| public static final Update UPDATE = new Update(); |
| |
| @IntDef(value = { |
| TYPE_UPDATE, |
| TYPE_UPDATE_EXCEPTION |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface UpdateType {} |
| public static final int TYPE_UPDATE = 0; |
| public static final int TYPE_UPDATE_EXCEPTION = 1; |
| |
| private final @UpdateType int mUpdateType; |
| private final @Nullable Exception mException; |
| private final boolean mRemoteActionEnabled; |
| |
| private Update() { |
| mUpdateType = TYPE_UPDATE; |
| mException = null; |
| mRemoteActionEnabled = false; |
| } |
| |
| public Update(Exception exception, boolean remoteActionsEnabled) { |
| assert(exception != null); |
| mUpdateType = TYPE_UPDATE_EXCEPTION; |
| mException = exception; |
| mRemoteActionEnabled = remoteActionsEnabled; |
| } |
| |
| public boolean isUpdate() { |
| return mUpdateType == TYPE_UPDATE; |
| } |
| |
| public boolean hasException() { |
| return mUpdateType == TYPE_UPDATE_EXCEPTION; |
| } |
| |
| public boolean hasAuthenticationException() { |
| return mRemoteActionEnabled |
| && hasException() |
| && mException instanceof AuthenticationRequiredException; |
| } |
| |
| public boolean hasCrossProfileException() { |
| return mRemoteActionEnabled |
| && hasException() |
| && mException instanceof CrossProfileException; |
| } |
| |
| public @Nullable Exception getException() { |
| return mException; |
| } |
| } |
| } |