Home | History | Annotate | Download | only in dirlist
      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