1 /* 2 * Copyright (C) 2015 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.dirlist; 18 19 import android.support.v7.widget.GridLayoutManager; 20 import android.support.v7.widget.RecyclerView.AdapterDataObserver; 21 import android.view.ViewGroup; 22 23 import com.android.documentsui.Model; 24 import com.android.documentsui.Model.Update; 25 import com.android.documentsui.base.EventListener; 26 import com.android.documentsui.dirlist.Message.HeaderMessage; 27 import com.android.documentsui.dirlist.Message.InflateMessage; 28 29 import java.util.List; 30 31 /** 32 * Adapter wrapper that embellishes the directory list by inserting Holder views inbetween 33 * items. 34 */ 35 final class DirectoryAddonsAdapter extends DocumentsAdapter { 36 37 private static final String TAG = "SectioningDocumentsAdapterWrapper"; 38 39 private final Environment mEnv; 40 private final DocumentsAdapter mDelegate; 41 private final EventListener<Update> mModelUpdateListener; 42 43 private int mBreakPosition = -1; 44 // TODO: There should be two header messages (or more here). Defaulting to showing only one for 45 // now. 46 private final Message mHeaderMessage; 47 private final Message mInflateMessage; 48 49 DirectoryAddonsAdapter(Environment environment, DocumentsAdapter delegate) { 50 mEnv = environment; 51 mDelegate = delegate; 52 // TODO: We should not instantiate the messages here, but rather instantiate them 53 // when we get an update event. 54 mHeaderMessage = new HeaderMessage(environment, this::onDismissHeaderMessage); 55 mInflateMessage = new InflateMessage(environment, this::onDismissHeaderMessage); 56 57 // Relay events published by our delegate to our listeners (presumably RecyclerView) 58 // with adjusted positions. 59 mDelegate.registerAdapterDataObserver(new EventRelay()); 60 61 mModelUpdateListener = this::onModelUpdate; 62 } 63 64 @Override 65 EventListener<Update> getModelUpdateListener() { 66 return mModelUpdateListener; 67 } 68 69 @Override 70 public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() { 71 return new GridLayoutManager.SpanSizeLookup() { 72 @Override 73 public int getSpanSize(int position) { 74 // Make layout whitespace span the grid. This has the effect of breaking 75 // grid rows whenever layout whitespace is encountered. 76 if (getItemViewType(position) == ITEM_TYPE_SECTION_BREAK 77 || getItemViewType(position) == ITEM_TYPE_HEADER_MESSAGE 78 || getItemViewType(position) == ITEM_TYPE_INFLATED_MESSAGE) { 79 return mEnv.getColumnCount(); 80 } else { 81 return 1; 82 } 83 } 84 }; 85 } 86 87 @Override 88 public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) { 89 DocumentHolder holder = null; 90 switch (viewType) { 91 case ITEM_TYPE_SECTION_BREAK: 92 holder = new TransparentDividerDocumentHolder(mEnv.getContext()); 93 mEnv.initDocumentHolder(holder); 94 break; 95 case ITEM_TYPE_HEADER_MESSAGE: 96 holder = new HeaderMessageDocumentHolder(mEnv.getContext(), parent); 97 mEnv.initDocumentHolder(holder); 98 break; 99 case ITEM_TYPE_INFLATED_MESSAGE: 100 holder = new InflateMessageDocumentHolder(mEnv.getContext(), parent); 101 mEnv.initDocumentHolder(holder); 102 break; 103 default: 104 holder = mDelegate.createViewHolder(parent, viewType); 105 } 106 return holder; 107 } 108 109 private void onDismissHeaderMessage() { 110 mHeaderMessage.reset(); 111 if (mBreakPosition > 0) { 112 mBreakPosition--; 113 } 114 notifyItemRemoved(0); 115 } 116 117 @Override 118 public void onBindViewHolder(DocumentHolder holder, int p, List<Object> payload) { 119 switch (holder.getItemViewType()) { 120 case ITEM_TYPE_SECTION_BREAK: 121 ((TransparentDividerDocumentHolder) holder).bind(mEnv.getDisplayState()); 122 break; 123 case ITEM_TYPE_HEADER_MESSAGE: 124 ((HeaderMessageDocumentHolder) holder).bind(mHeaderMessage); 125 break; 126 case ITEM_TYPE_INFLATED_MESSAGE: 127 ((InflateMessageDocumentHolder) holder).bind(mInflateMessage); 128 break; 129 default: 130 mDelegate.onBindViewHolder(holder, toDelegatePosition(p), payload); 131 break; 132 } 133 } 134 135 @Override 136 public void onBindViewHolder(DocumentHolder holder, int p) { 137 switch (holder.getItemViewType()) { 138 case ITEM_TYPE_SECTION_BREAK: 139 ((TransparentDividerDocumentHolder) holder).bind(mEnv.getDisplayState()); 140 break; 141 case ITEM_TYPE_HEADER_MESSAGE: 142 ((HeaderMessageDocumentHolder) holder).bind(mHeaderMessage); 143 break; 144 case ITEM_TYPE_INFLATED_MESSAGE: 145 ((InflateMessageDocumentHolder) holder).bind(mInflateMessage); 146 break; 147 default: 148 mDelegate.onBindViewHolder(holder, toDelegatePosition(p)); 149 break; 150 } 151 } 152 153 @Override 154 public int getItemCount() { 155 int addons = mHeaderMessage.shouldShow() ? 1 : 0; 156 addons += mInflateMessage.shouldShow() ? 1 : 0; 157 return mBreakPosition == -1 158 ? mDelegate.getItemCount() + addons 159 : mDelegate.getItemCount() + addons + 1; 160 } 161 162 private void onModelUpdate(Update event) { 163 // make sure the delegate handles the update before we do. 164 // This isn't ideal since the delegate might be listening 165 // the updates itself. But this is the safe thing to do 166 // since we read model ids from the delegate 167 // in our update handler. 168 mDelegate.getModelUpdateListener().accept(event); 169 170 mBreakPosition = -1; 171 mInflateMessage.update(event); 172 mHeaderMessage.update(event); 173 // If there's any fatal error (exceptions), then no need to update the rest. 174 if (event.hasException()) { 175 return; 176 } 177 178 // Walk down the list of IDs till we encounter something that's not a directory, and 179 // insert a whitespace element - this introduces a visual break in the grid between 180 // folders and documents. 181 // TODO: This code makes assumptions about the model, namely, that it performs a 182 // bucketed sort where directories will always be ordered before other files. CBB. 183 Model model = mEnv.getModel(); 184 for (int i = 0; i < model.getModelIds().length; i++) { 185 if (!isDirectory(model, i)) { 186 // If the break is the first thing in the list, then there are actually no 187 // directories. In that case, don't insert a break at all. 188 if (i > 0) { 189 mBreakPosition = i + (mHeaderMessage.shouldShow() ? 1 : 0); 190 } 191 break; 192 } 193 } 194 } 195 196 @Override 197 public int getItemViewType(int p) { 198 if (p == 0 && mHeaderMessage.shouldShow()) { 199 return ITEM_TYPE_HEADER_MESSAGE; 200 } 201 202 if (p == mBreakPosition) { 203 return ITEM_TYPE_SECTION_BREAK; 204 } 205 206 if (p == getItemCount() - 1 && mInflateMessage.shouldShow()) { 207 return ITEM_TYPE_INFLATED_MESSAGE; 208 } 209 210 return mDelegate.getItemViewType(toDelegatePosition(p)); 211 } 212 213 /** 214 * Returns the position of an item in the delegate, adjusting 215 * values that are greater than the break position. 216 * 217 * @param p Position within the view 218 * @return Position within the delegate 219 */ 220 private int toDelegatePosition(int p) { 221 int topOffset = mHeaderMessage.shouldShow() ? 1 : 0; 222 return (mBreakPosition != -1 && p > mBreakPosition) ? p - 1 - topOffset : p - topOffset; 223 } 224 225 /** 226 * Returns the position of an item in the view, adjusting 227 * values that are greater than the break position. 228 * 229 * @param p Position within the delegate 230 * @return Position within the view 231 */ 232 private int toViewPosition(int p) { 233 int topOffset = mHeaderMessage.shouldShow() ? 1 : 0; 234 // Offset it first so we can compare break position correctly 235 p += topOffset; 236 // If position is greater than or equal to the break, increase by one. 237 return (mBreakPosition != -1 && p >= mBreakPosition) ? p + 1 : p; 238 } 239 240 @Override 241 public List<String> getStableIds() { 242 return mDelegate.getStableIds(); 243 } 244 245 @Override 246 public int getAdapterPosition(String modelId) { 247 return toViewPosition(mDelegate.getAdapterPosition(modelId)); 248 } 249 250 @Override 251 public String getStableId(int p) { 252 if (p == mBreakPosition) { 253 return null; 254 } 255 256 if (p == 0 && mHeaderMessage.shouldShow()) { 257 return null; 258 } 259 260 if (p == getItemCount() - 1 && mInflateMessage.shouldShow()) { 261 return null; 262 } 263 264 return mDelegate.getStableId(toDelegatePosition(p)); 265 } 266 267 @Override 268 public int getPosition(String id) { 269 return toViewPosition(mDelegate.getPosition(id)); 270 } 271 272 // Listener we add to our delegate. This allows us to relay events published 273 // by the delegate to our listeners (presumably RecyclerView) with adjusted positions. 274 private final class EventRelay extends AdapterDataObserver { 275 @Override 276 public void onChanged() { 277 throw new UnsupportedOperationException(); 278 } 279 280 @Override 281 public void onItemRangeChanged(int positionStart, int itemCount) { 282 throw new UnsupportedOperationException(); 283 } 284 285 @Override 286 public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { 287 assert(itemCount == 1); 288 notifyItemRangeChanged(toViewPosition(positionStart), itemCount, payload); 289 } 290 291 @Override 292 public void onItemRangeInserted(int positionStart, int itemCount) { 293 assert(itemCount == 1); 294 if (positionStart < mBreakPosition) { 295 mBreakPosition++; 296 } 297 notifyItemRangeInserted(toViewPosition(positionStart), itemCount); 298 } 299 300 @Override 301 public void onItemRangeRemoved(int positionStart, int itemCount) { 302 assert(itemCount == 1); 303 if (positionStart < mBreakPosition) { 304 mBreakPosition--; 305 } 306 notifyItemRangeRemoved(toViewPosition(positionStart), itemCount); 307 } 308 309 @Override 310 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { 311 throw new UnsupportedOperationException(); 312 } 313 } 314 } 315