| /* | |
| * Copyright (C) 2013 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.base; | |
| import static androidx.core.util.Preconditions.checkArgument; | |
| import static com.android.documentsui.base.SharedMinimal.DEBUG; | |
| import android.content.Context; | |
| import android.database.Cursor; | |
| import android.os.Parcel; | |
| import android.os.Parcelable; | |
| import android.provider.DocumentsProvider; | |
| import android.util.Log; | |
| import com.android.documentsui.picker.LastAccessedProvider; | |
| import java.io.DataInputStream; | |
| import java.io.DataOutputStream; | |
| import java.io.FileNotFoundException; | |
| import java.io.IOException; | |
| import java.net.ProtocolException; | |
| import java.util.Collection; | |
| import java.util.LinkedList; | |
| import java.util.List; | |
| import java.util.Objects; | |
| import javax.annotation.Nullable; | |
| /** | |
| * Representation of a stack of {@link DocumentInfo}, usually the result of a | |
| * user-driven traversal. | |
| */ | |
| public class DocumentStack implements Durable, Parcelable { | |
| private static final String TAG = "DocumentStack"; | |
| private static final int VERSION_INIT = 1; | |
| private static final int VERSION_ADD_ROOT = 2; | |
| private LinkedList<DocumentInfo> mList; | |
| private @Nullable RootInfo mRoot; | |
| private boolean mStackTouched; | |
| public DocumentStack() { | |
| mList = new LinkedList<>(); | |
| } | |
| /** | |
| * Creates an instance, and pushes all docs to it in the same order as they're passed as | |
| * parameters, i.e. the last document will be at the top of the stack. | |
| */ | |
| public DocumentStack(RootInfo root, DocumentInfo... docs) { | |
| mList = new LinkedList<>(); | |
| for (int i = 0; i < docs.length; ++i) { | |
| mList.add(docs[i]); | |
| } | |
| mRoot = root; | |
| } | |
| /** | |
| * Same as {@link #DocumentStack(DocumentStack, DocumentInfo...)} except it takes a {@link List} | |
| * instead of an array. | |
| */ | |
| public DocumentStack(RootInfo root, List<DocumentInfo> docs) { | |
| mList = new LinkedList<>(docs); | |
| mRoot = root; | |
| } | |
| /** | |
| * Makes a new copy, and pushes all docs to the new copy in the same order as they're | |
| * passed as parameters, i.e. the last document will be at the top of the stack. | |
| */ | |
| public DocumentStack(DocumentStack src, DocumentInfo... docs) { | |
| mList = new LinkedList<>(src.mList); | |
| for (DocumentInfo doc : docs) { | |
| push(doc); | |
| } | |
| mStackTouched = false; | |
| mRoot = src.mRoot; | |
| } | |
| public boolean isInitialized() { | |
| return mRoot != null; | |
| } | |
| public @Nullable RootInfo getRoot() { | |
| return mRoot; | |
| } | |
| public boolean isEmpty() { | |
| return mList.isEmpty(); | |
| } | |
| public int size() { | |
| return mList.size(); | |
| } | |
| public DocumentInfo peek() { | |
| return mList.peekLast(); | |
| } | |
| /** | |
| * Returns {@link DocumentInfo} at index counted from the bottom of this stack. | |
| */ | |
| public DocumentInfo get(int index) { | |
| return mList.get(index); | |
| } | |
| public void push(DocumentInfo info) { | |
| checkArgument(!mList.contains(info)); | |
| if (DEBUG) { | |
| Log.d(TAG, "Adding doc to stack: " + info); | |
| } | |
| mList.addLast(info); | |
| mStackTouched = true; | |
| } | |
| public DocumentInfo pop() { | |
| if (DEBUG) { | |
| Log.d(TAG, "Popping doc off stack."); | |
| } | |
| final DocumentInfo result = mList.removeLast(); | |
| mStackTouched = true; | |
| return result; | |
| } | |
| public void popToRootDocument() { | |
| if (DEBUG) { | |
| Log.d(TAG, "Popping docs to root folder."); | |
| } | |
| while (mList.size() > 1) { | |
| mList.removeLast(); | |
| } | |
| mStackTouched = true; | |
| } | |
| public void changeRoot(RootInfo root) { | |
| if (DEBUG) { | |
| Log.d(TAG, "Root changed to: " + root); | |
| } | |
| reset(); | |
| mRoot = root; | |
| // Add this for keep stack size is 1 on recent root. | |
| if (root.isRecents()) { | |
| DocumentInfo rootRecent = new DocumentInfo(); | |
| rootRecent.userId = root.userId; | |
| rootRecent.deriveFields(); | |
| push(rootRecent); | |
| } | |
| } | |
| /** This will return true even when the initial location is set. | |
| * To get a read on if the user has changed something, use {@link #hasInitialLocationChanged()}. | |
| */ | |
| public boolean hasLocationChanged() { | |
| return mStackTouched; | |
| } | |
| public String getTitle() { | |
| if (mList.size() == 1 && mRoot != null) { | |
| return mRoot.title; | |
| } else if (mList.size() > 1) { | |
| return peek().displayName; | |
| } else { | |
| return null; | |
| } | |
| } | |
| public boolean isRecents() { | |
| return mRoot != null && mRoot.isRecents() && size() == 1; | |
| } | |
| /** | |
| * Resets this stack to the given stack. It takes the reference of {@link #mList} and | |
| * {@link #mRoot} instead of making a copy. | |
| */ | |
| public void reset(DocumentStack stack) { | |
| if (DEBUG) { | |
| Log.d(TAG, "Resetting the whole darn stack to: " + stack); | |
| } | |
| mList = stack.mList; | |
| mRoot = stack.mRoot; | |
| mStackTouched = true; | |
| } | |
| @Override | |
| public String toString() { | |
| return "DocumentStack{" | |
| + "root=" + mRoot | |
| + ", docStack=" + mList | |
| + ", stackTouched=" + mStackTouched | |
| + "}"; | |
| } | |
| @Override | |
| public void reset() { | |
| mList.clear(); | |
| mRoot = null; | |
| } | |
| private void updateRoot(Collection<RootInfo> matchingRoots) throws FileNotFoundException { | |
| for (RootInfo root : matchingRoots) { | |
| // RootInfo's equals() only checks authority and rootId, so this will update RootInfo if | |
| // its flag has changed. | |
| if (root.equals(this.mRoot)) { | |
| this.mRoot = root; | |
| return; | |
| } | |
| } | |
| throw new FileNotFoundException("Failed to find matching mRoot for " + mRoot); | |
| } | |
| /** | |
| * Update a possibly stale restored stack against a live | |
| * {@link DocumentsProvider}. | |
| */ | |
| private void updateDocuments(Context context) throws FileNotFoundException { | |
| for (DocumentInfo info : mList) { | |
| info.updateSelf(info.userId.getContentResolver(context), info.userId); | |
| } | |
| } | |
| public static @Nullable DocumentStack fromLastAccessedCursor( | |
| Cursor cursor, Collection<RootInfo> matchingRoots, Context context) | |
| throws IOException { | |
| if (cursor.moveToFirst()) { | |
| DocumentStack stack = new DocumentStack(); | |
| final byte[] rawStack = cursor.getBlob( | |
| cursor.getColumnIndex(LastAccessedProvider.Columns.STACK)); | |
| DurableUtils.readFromArray(rawStack, stack); | |
| stack.updateRoot(matchingRoots); | |
| stack.updateDocuments(context); | |
| return stack; | |
| } | |
| return null; | |
| } | |
| @Override | |
| public boolean equals(Object o) { | |
| if (this == o) { | |
| return true; | |
| } | |
| if (!(o instanceof DocumentStack)) { | |
| return false; | |
| } | |
| DocumentStack other = (DocumentStack) o; | |
| return Objects.equals(mRoot, other.mRoot) | |
| && mList.equals(other.mList); | |
| } | |
| @Override | |
| public int hashCode() { | |
| return Objects.hash(mRoot, mList); | |
| } | |
| @Override | |
| public void read(DataInputStream in) throws IOException { | |
| final int version = in.readInt(); | |
| switch (version) { | |
| case VERSION_INIT: | |
| throw new ProtocolException("Ignored upgrade"); | |
| case VERSION_ADD_ROOT: | |
| if (in.readBoolean()) { | |
| mRoot = new RootInfo(); | |
| mRoot.read(in); | |
| } | |
| final int size = in.readInt(); | |
| for (int i = 0; i < size; i++) { | |
| final DocumentInfo doc = new DocumentInfo(); | |
| doc.read(in); | |
| mList.add(doc); | |
| } | |
| mStackTouched = in.readInt() != 0; | |
| break; | |
| default: | |
| throw new ProtocolException("Unknown version " + version); | |
| } | |
| } | |
| @Override | |
| public void write(DataOutputStream out) throws IOException { | |
| out.writeInt(VERSION_ADD_ROOT); | |
| if (mRoot != null) { | |
| out.writeBoolean(true); | |
| mRoot.write(out); | |
| } else { | |
| out.writeBoolean(false); | |
| } | |
| final int size = mList.size(); | |
| out.writeInt(size); | |
| for (int i = 0; i < size; i++) { | |
| final DocumentInfo doc = mList.get(i); | |
| doc.write(out); | |
| } | |
| out.writeInt(mStackTouched ? 1 : 0); | |
| } | |
| @Override | |
| public int describeContents() { | |
| return 0; | |
| } | |
| @Override | |
| public void writeToParcel(Parcel dest, int flags) { | |
| DurableUtils.writeToParcel(dest, this); | |
| } | |
| public static final Creator<DocumentStack> CREATOR = new Creator<DocumentStack>() { | |
| @Override | |
| public DocumentStack createFromParcel(Parcel in) { | |
| final DocumentStack stack = new DocumentStack(); | |
| DurableUtils.readFromParcel(in, stack); | |
| return stack; | |
| } | |
| @Override | |
| public DocumentStack[] newArray(int size) { | |
| return new DocumentStack[size]; | |
| } | |
| }; | |
| } |