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