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