1 /*
2  * Copyright (C) 2013 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.base;
18 
19 import static androidx.core.util.Preconditions.checkArgument;
20 
21 import static com.android.documentsui.base.SharedMinimal.DEBUG;
22 
23 import android.content.ContentResolver;
24 import android.database.Cursor;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.provider.DocumentsProvider;
28 import android.util.Log;
29 
30 import com.android.documentsui.picker.LastAccessedProvider;
31 
32 import java.io.DataInputStream;
33 import java.io.DataOutputStream;
34 import java.io.FileNotFoundException;
35 import java.io.IOException;
36 import java.net.ProtocolException;
37 import java.util.Collection;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Objects;
41 
42 import javax.annotation.Nullable;
43 
44 /**
45  * Representation of a stack of {@link DocumentInfo}, usually the result of a
46  * user-driven traversal.
47  */
48 public class DocumentStack implements Durable, Parcelable {
49 
50     private static final String TAG = "DocumentStack";
51 
52     private static final int VERSION_INIT = 1;
53     private static final int VERSION_ADD_ROOT = 2;
54 
55     private LinkedList<DocumentInfo> mList;
56     private @Nullable RootInfo mRoot;
57 
58     private boolean mStackTouched;
59 
DocumentStack()60     public DocumentStack() {
61         mList = new LinkedList<>();
62     }
63 
64     /**
65      * Creates an instance, and pushes all docs to it in the same order as they're passed as
66      * parameters, i.e. the last document will be at the top of the stack.
67      */
DocumentStack(RootInfo root, DocumentInfo... docs)68     public DocumentStack(RootInfo root, DocumentInfo... docs) {
69         mList = new LinkedList<>();
70         for (int i = 0; i < docs.length; ++i) {
71             mList.add(docs[i]);
72         }
73 
74         mRoot = root;
75     }
76 
77     /**
78      * Same as {@link #DocumentStack(DocumentStack, DocumentInfo...)} except it takes a {@link List}
79      * instead of an array.
80      */
DocumentStack(RootInfo root, List<DocumentInfo> docs)81     public DocumentStack(RootInfo root, List<DocumentInfo> docs) {
82         mList = new LinkedList<>(docs);
83         mRoot = root;
84     }
85 
86     /**
87      * Makes a new copy, and pushes all docs to the new copy in the same order as they're
88      * passed as parameters, i.e. the last document will be at the top of the stack.
89      */
DocumentStack(DocumentStack src, DocumentInfo... docs)90     public DocumentStack(DocumentStack src, DocumentInfo... docs) {
91         mList = new LinkedList<>(src.mList);
92         for (DocumentInfo doc : docs) {
93             push(doc);
94         }
95 
96         mStackTouched = false;
97         mRoot = src.mRoot;
98     }
99 
isInitialized()100     public boolean isInitialized() {
101         return mRoot != null;
102     }
103 
getRoot()104     public @Nullable RootInfo getRoot() {
105         return mRoot;
106     }
107 
isEmpty()108     public boolean isEmpty() {
109         return mList.isEmpty();
110     }
111 
size()112     public int size() {
113         return mList.size();
114     }
115 
peek()116     public DocumentInfo peek() {
117         return mList.peekLast();
118     }
119 
120     /**
121      * Returns {@link DocumentInfo} at index counted from the bottom of this stack.
122      */
get(int index)123     public DocumentInfo get(int index) {
124         return mList.get(index);
125     }
126 
push(DocumentInfo info)127     public void push(DocumentInfo info) {
128         checkArgument(!mList.contains(info));
129         if (DEBUG) {
130             Log.d(TAG, "Adding doc to stack: " + info);
131         }
132         mList.addLast(info);
133         mStackTouched = true;
134     }
135 
pop()136     public DocumentInfo pop() {
137         if (DEBUG) {
138             Log.d(TAG, "Popping doc off stack.");
139         }
140         final DocumentInfo result = mList.removeLast();
141         mStackTouched = true;
142 
143         return result;
144     }
145 
popToRootDocument()146     public void popToRootDocument() {
147         if (DEBUG) {
148             Log.d(TAG, "Popping docs to root folder.");
149         }
150         while (mList.size() > 1) {
151             mList.removeLast();
152         }
153         mStackTouched = true;
154     }
155 
changeRoot(RootInfo root)156     public void changeRoot(RootInfo root) {
157         if (DEBUG) {
158             Log.d(TAG, "Root changed to: " + root);
159         }
160         reset();
161         mRoot = root;
162 
163         // Add this for keep stack size is 1 on recent root.
164         if (root.isRecents()) {
165             DocumentInfo rootRecent = new DocumentInfo();
166             rootRecent.deriveFields();
167             push(rootRecent);
168         }
169     }
170 
171     /** This will return true even when the initial location is set.
172      * To get a read on if the user has changed something, use {@link #hasInitialLocationChanged()}.
173      */
hasLocationChanged()174     public boolean hasLocationChanged() {
175         return mStackTouched;
176     }
177 
getTitle()178     public String getTitle() {
179         if (mList.size() == 1 && mRoot != null) {
180             return mRoot.title;
181         } else if (mList.size() > 1) {
182             return peek().displayName;
183         } else {
184             return null;
185         }
186     }
187 
isRecents()188     public boolean isRecents() {
189         return mRoot != null && mRoot.isRecents() && size() == 1;
190     }
191 
192     /**
193      * Resets this stack to the given stack. It takes the reference of {@link #mList} and
194      * {@link #mRoot} instead of making a copy.
195      */
reset(DocumentStack stack)196     public void reset(DocumentStack stack) {
197         if (DEBUG) {
198             Log.d(TAG, "Resetting the whole darn stack to: " + stack);
199         }
200 
201         mList = stack.mList;
202         mRoot = stack.mRoot;
203         mStackTouched = true;
204     }
205 
206     @Override
toString()207     public String toString() {
208         return "DocumentStack{"
209                 + "root=" + mRoot
210                 + ", docStack=" + mList
211                 + ", stackTouched=" + mStackTouched
212                 + "}";
213     }
214 
215     @Override
reset()216     public void reset() {
217         mList.clear();
218         mRoot = null;
219     }
220 
updateRoot(Collection<RootInfo> matchingRoots)221     private void updateRoot(Collection<RootInfo> matchingRoots) throws FileNotFoundException {
222         for (RootInfo root : matchingRoots) {
223             // RootInfo's equals() only checks authority and rootId, so this will update RootInfo if
224             // its flag has changed.
225             if (root.equals(this.mRoot)) {
226                 this.mRoot = root;
227                 return;
228             }
229         }
230         throw new FileNotFoundException("Failed to find matching mRoot for " + mRoot);
231     }
232 
233     /**
234      * Update a possibly stale restored stack against a live
235      * {@link DocumentsProvider}.
236      */
updateDocuments(ContentResolver resolver)237     private void updateDocuments(ContentResolver resolver) throws FileNotFoundException {
238         for (DocumentInfo info : mList) {
239             info.updateSelf(resolver);
240         }
241     }
242 
fromLastAccessedCursor( Cursor cursor, Collection<RootInfo> matchingRoots, ContentResolver resolver)243     public static @Nullable DocumentStack fromLastAccessedCursor(
244             Cursor cursor, Collection<RootInfo> matchingRoots, ContentResolver resolver)
245             throws IOException {
246 
247         if (cursor.moveToFirst()) {
248             DocumentStack stack = new DocumentStack();
249             final byte[] rawStack = cursor.getBlob(
250                     cursor.getColumnIndex(LastAccessedProvider.Columns.STACK));
251             DurableUtils.readFromArray(rawStack, stack);
252 
253             stack.updateRoot(matchingRoots);
254             stack.updateDocuments(resolver);
255 
256             return stack;
257         }
258 
259         return null;
260     }
261 
262     @Override
equals(Object o)263     public boolean equals(Object o) {
264         if (this == o) {
265             return true;
266         }
267 
268         if (!(o instanceof DocumentStack)) {
269             return false;
270         }
271 
272         DocumentStack other = (DocumentStack) o;
273         return Objects.equals(mRoot, other.mRoot)
274                 && mList.equals(other.mList);
275     }
276 
277     @Override
hashCode()278     public int hashCode() {
279         return Objects.hash(mRoot, mList);
280     }
281 
282     @Override
read(DataInputStream in)283     public void read(DataInputStream in) throws IOException {
284         final int version = in.readInt();
285         switch (version) {
286             case VERSION_INIT:
287                 throw new ProtocolException("Ignored upgrade");
288             case VERSION_ADD_ROOT:
289                 if (in.readBoolean()) {
290                     mRoot = new RootInfo();
291                     mRoot.read(in);
292                 }
293                 final int size = in.readInt();
294                 for (int i = 0; i < size; i++) {
295                     final DocumentInfo doc = new DocumentInfo();
296                     doc.read(in);
297                     mList.add(doc);
298                 }
299                 mStackTouched = in.readInt() != 0;
300                 break;
301             default:
302                 throw new ProtocolException("Unknown version " + version);
303         }
304     }
305 
306     @Override
write(DataOutputStream out)307     public void write(DataOutputStream out) throws IOException {
308         out.writeInt(VERSION_ADD_ROOT);
309         if (mRoot != null) {
310             out.writeBoolean(true);
311             mRoot.write(out);
312         } else {
313             out.writeBoolean(false);
314         }
315         final int size = mList.size();
316         out.writeInt(size);
317         for (int i = 0; i < size; i++) {
318             final DocumentInfo doc = mList.get(i);
319             doc.write(out);
320         }
321         out.writeInt(mStackTouched ? 1 : 0);
322     }
323 
324     @Override
describeContents()325     public int describeContents() {
326         return 0;
327     }
328 
329     @Override
writeToParcel(Parcel dest, int flags)330     public void writeToParcel(Parcel dest, int flags) {
331         DurableUtils.writeToParcel(dest, this);
332     }
333 
334     public static final Creator<DocumentStack> CREATOR = new Creator<DocumentStack>() {
335         @Override
336         public DocumentStack createFromParcel(Parcel in) {
337             final DocumentStack stack = new DocumentStack();
338             DurableUtils.readFromParcel(in, stack);
339             return stack;
340         }
341 
342         @Override
343         public DocumentStack[] newArray(int size) {
344             return new DocumentStack[size];
345         }
346     };
347 }
348