blob: be9af0ff6041eec5a8877ee46972ff99acbb1428 [file] [log] [blame]
/*
* 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];
}
};
}