• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Context;
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.userId = root.userId;
167             rootRecent.deriveFields();
168             push(rootRecent);
169         }
170     }
171 
172     /** This will return true even when the initial location is set.
173      * To get a read on if the user has changed something, use {@link #hasInitialLocationChanged()}.
174      */
hasLocationChanged()175     public boolean hasLocationChanged() {
176         return mStackTouched;
177     }
178 
getTitle()179     public String getTitle() {
180         if (mList.size() == 1 && mRoot != null) {
181             return mRoot.title;
182         } else if (mList.size() > 1) {
183             return peek().displayName;
184         } else {
185             return null;
186         }
187     }
188 
isRecents()189     public boolean isRecents() {
190         return mRoot != null && mRoot.isRecents() && size() == 1;
191     }
192 
193     /**
194      * Resets this stack to the given stack. It takes the reference of {@link #mList} and
195      * {@link #mRoot} instead of making a copy.
196      */
reset(DocumentStack stack)197     public void reset(DocumentStack stack) {
198         if (DEBUG) {
199             Log.d(TAG, "Resetting the whole darn stack to: " + stack);
200         }
201 
202         mList = stack.mList;
203         mRoot = stack.mRoot;
204         mStackTouched = true;
205     }
206 
207     @Override
toString()208     public String toString() {
209         return "DocumentStack{"
210                 + "root=" + mRoot
211                 + ", docStack=" + mList
212                 + ", stackTouched=" + mStackTouched
213                 + "}";
214     }
215 
216     @Override
reset()217     public void reset() {
218         mList.clear();
219         mRoot = null;
220     }
221 
updateRoot(Collection<RootInfo> matchingRoots)222     private void updateRoot(Collection<RootInfo> matchingRoots) throws FileNotFoundException {
223         for (RootInfo root : matchingRoots) {
224             // RootInfo's equals() only checks authority and rootId, so this will update RootInfo if
225             // its flag has changed.
226             if (root.equals(this.mRoot)) {
227                 this.mRoot = root;
228                 return;
229             }
230         }
231         throw new FileNotFoundException("Failed to find matching mRoot for " + mRoot);
232     }
233 
234     /**
235      * Update a possibly stale restored stack against a live
236      * {@link DocumentsProvider}.
237      */
updateDocuments(Context context)238     private void updateDocuments(Context context) throws FileNotFoundException {
239         for (DocumentInfo info : mList) {
240             info.updateSelf(info.userId.getContentResolver(context), info.userId);
241         }
242     }
243 
fromLastAccessedCursor( Cursor cursor, Collection<RootInfo> matchingRoots, Context context)244     public static @Nullable DocumentStack fromLastAccessedCursor(
245             Cursor cursor, Collection<RootInfo> matchingRoots, Context context)
246             throws IOException {
247 
248         if (cursor.moveToFirst()) {
249             DocumentStack stack = new DocumentStack();
250             final byte[] rawStack = cursor.getBlob(
251                     cursor.getColumnIndex(LastAccessedProvider.Columns.STACK));
252             DurableUtils.readFromArray(rawStack, stack);
253 
254             stack.updateRoot(matchingRoots);
255             stack.updateDocuments(context);
256 
257             return stack;
258         }
259 
260         return null;
261     }
262 
263     @Override
equals(Object o)264     public boolean equals(Object o) {
265         if (this == o) {
266             return true;
267         }
268 
269         if (!(o instanceof DocumentStack)) {
270             return false;
271         }
272 
273         DocumentStack other = (DocumentStack) o;
274         return Objects.equals(mRoot, other.mRoot)
275                 && mList.equals(other.mList);
276     }
277 
278     @Override
hashCode()279     public int hashCode() {
280         return Objects.hash(mRoot, mList);
281     }
282 
283     @Override
read(DataInputStream in)284     public void read(DataInputStream in) throws IOException {
285         final int version = in.readInt();
286         switch (version) {
287             case VERSION_INIT:
288                 throw new ProtocolException("Ignored upgrade");
289             case VERSION_ADD_ROOT:
290                 if (in.readBoolean()) {
291                     mRoot = new RootInfo();
292                     mRoot.read(in);
293                 }
294                 final int size = in.readInt();
295                 for (int i = 0; i < size; i++) {
296                     final DocumentInfo doc = new DocumentInfo();
297                     doc.read(in);
298                     mList.add(doc);
299                 }
300                 mStackTouched = in.readInt() != 0;
301                 break;
302             default:
303                 throw new ProtocolException("Unknown version " + version);
304         }
305     }
306 
307     @Override
write(DataOutputStream out)308     public void write(DataOutputStream out) throws IOException {
309         out.writeInt(VERSION_ADD_ROOT);
310         if (mRoot != null) {
311             out.writeBoolean(true);
312             mRoot.write(out);
313         } else {
314             out.writeBoolean(false);
315         }
316         final int size = mList.size();
317         out.writeInt(size);
318         for (int i = 0; i < size; i++) {
319             final DocumentInfo doc = mList.get(i);
320             doc.write(out);
321         }
322         out.writeInt(mStackTouched ? 1 : 0);
323     }
324 
325     @Override
describeContents()326     public int describeContents() {
327         return 0;
328     }
329 
330     @Override
writeToParcel(Parcel dest, int flags)331     public void writeToParcel(Parcel dest, int flags) {
332         DurableUtils.writeToParcel(dest, this);
333     }
334 
335     public static final Creator<DocumentStack> CREATOR = new Creator<DocumentStack>() {
336         @Override
337         public DocumentStack createFromParcel(Parcel in) {
338             final DocumentStack stack = new DocumentStack();
339             DurableUtils.readFromParcel(in, stack);
340             return stack;
341         }
342 
343         @Override
344         public DocumentStack[] newArray(int size) {
345             return new DocumentStack[size];
346         }
347     };
348 }
349