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