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