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
17package com.android.documentsui.dirlist;
18
19import android.content.Context;
20import android.database.Cursor;
21import android.support.v7.widget.GridLayoutManager;
22import android.support.v7.widget.RecyclerView.AdapterDataObserver;
23import android.util.SparseArray;
24import android.view.ViewGroup;
25import android.widget.Space;
26
27import com.android.documentsui.R;
28import com.android.documentsui.State;
29
30import java.util.List;
31
32/**
33 * Adapter wrapper that inserts a sort of line break item between directories and regular files.
34 * Only needs to be used in GRID mode...at this time.
35 */
36final class SectionBreakDocumentsAdapterWrapper extends DocumentsAdapter {
37
38    private static final String TAG = "SectionBreakDocumentsAdapterWrapper";
39    private static final int ITEM_TYPE_SECTION_BREAK = Integer.MAX_VALUE;
40
41    private final Environment mEnv;
42    private final DocumentsAdapter mDelegate;
43
44    private int mBreakPosition = -1;
45
46    SectionBreakDocumentsAdapterWrapper(Environment environment, DocumentsAdapter delegate) {
47        mEnv = environment;
48        mDelegate = delegate;
49
50        // Relay events published by our delegate to our listeners (presumably RecyclerView)
51        // with adjusted positions.
52        mDelegate.registerAdapterDataObserver(new EventRelay());
53    }
54
55    public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() {
56        return new GridLayoutManager.SpanSizeLookup() {
57            @Override
58            public int getSpanSize(int position) {
59                // Make layout whitespace span the grid. This has the effect of breaking
60                // grid rows whenever layout whitespace is encountered.
61                if (getItemViewType(position) == ITEM_TYPE_SECTION_BREAK) {
62                    return mEnv.getColumnCount();
63                } else {
64                    return 1;
65                }
66            }
67        };
68    }
69
70    @Override
71    public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
72        if (viewType == ITEM_TYPE_SECTION_BREAK) {
73            return new EmptyDocumentHolder(mEnv.getContext());
74        } else {
75            return mDelegate.createViewHolder(parent, viewType);
76        }
77    }
78
79    @Override
80    public void onBindViewHolder(DocumentHolder holder, int p, List<Object> payload) {
81        if (holder.getItemViewType() != ITEM_TYPE_SECTION_BREAK) {
82            mDelegate.onBindViewHolder(holder, toDelegatePosition(p), payload);
83        } else {
84            ((EmptyDocumentHolder)holder).bind(mEnv.getDisplayState());
85        }
86    }
87
88    @Override
89    public void onBindViewHolder(DocumentHolder holder, int p) {
90        if (holder.getItemViewType() != ITEM_TYPE_SECTION_BREAK) {
91            mDelegate.onBindViewHolder(holder, toDelegatePosition(p));
92        } else {
93            ((EmptyDocumentHolder)holder).bind(mEnv.getDisplayState());
94        }
95    }
96
97    @Override
98    public int getItemCount() {
99        return mBreakPosition == -1
100                ? mDelegate.getItemCount()
101                : mDelegate.getItemCount() + 1;
102    }
103
104    @Override
105    public void onModelUpdate(Model model) {
106        mDelegate.onModelUpdate(model);
107        mBreakPosition = -1;
108
109        // Walk down the list of IDs till we encounter something that's not a directory, and
110        // insert a whitespace element - this introduces a visual break in the grid between
111        // folders and documents.
112        // TODO: This code makes assumptions about the model, namely, that it performs a
113        // bucketed sort where directories will always be ordered before other files. CBB.
114        List<String> modelIds = mDelegate.getModelIds();
115        for (int i = 0; i < modelIds.size(); i++) {
116            if (!isDirectory(model, i)) {
117                // If the break is the first thing in the list, then there are actually no
118                // directories. In that case, don't insert a break at all.
119                if (i > 0) {
120                    mBreakPosition = i;
121                }
122                break;
123            }
124        }
125    }
126
127    @Override
128    public void onModelUpdateFailed(Exception e) {
129        mDelegate.onModelUpdateFailed(e);
130    }
131
132    @Override
133    public int getItemViewType(int p) {
134        if (p == mBreakPosition) {
135            return ITEM_TYPE_SECTION_BREAK;
136        } else {
137            return mDelegate.getItemViewType(toDelegatePosition(p));
138        }
139    }
140
141    /**
142     * Returns the position of an item in the delegate, adjusting
143     * values that are greater than the break position.
144     *
145     * @param p Position within the view
146     * @return Position within the delegate
147     */
148    private int toDelegatePosition(int p) {
149        return (mBreakPosition != -1 && p > mBreakPosition) ? p - 1 : p;
150    }
151
152    /**
153     * Returns the position of an item in the view, adjusting
154     * values that are greater than the break position.
155     *
156     * @param p Position within the delegate
157     * @return Position within the view
158     */
159    private int toViewPosition(int p) {
160        // If position is greater than or equal to the break, increase by one.
161        return (mBreakPosition != -1 && p >= mBreakPosition) ? p + 1 : p;
162    }
163
164    @Override
165    public SparseArray<String> hide(String... ids) {
166        // NOTE: We hear about these changes and adjust break position
167        // in our AdapterDataObserver.
168        return mDelegate.hide(ids);
169    }
170
171    @Override
172    List<String> getModelIds() {
173        return mDelegate.getModelIds();
174    }
175
176    @Override
177    String getModelId(int p) {
178        return (p == mBreakPosition) ? null : mDelegate.getModelId(toDelegatePosition(p));
179    }
180
181    @Override
182    public void onItemSelectionChanged(String id) {
183        mDelegate.onItemSelectionChanged(id);
184    }
185
186    // Listener we add to our delegate. This allows us to relay events published
187    // by the delegate to our listeners (presumably RecyclerView) with adjusted positions.
188    private final class EventRelay extends AdapterDataObserver {
189        public void onChanged() {
190            throw new UnsupportedOperationException();
191        }
192
193        public void onItemRangeChanged(int positionStart, int itemCount) {
194            throw new UnsupportedOperationException();
195        }
196
197        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
198            assert(itemCount == 1);
199            notifyItemRangeChanged(toViewPosition(positionStart), itemCount, payload);
200        }
201
202        public void onItemRangeInserted(int positionStart, int itemCount) {
203            assert(itemCount == 1);
204            if (positionStart < mBreakPosition) {
205                mBreakPosition++;
206            }
207            notifyItemRangeInserted(toViewPosition(positionStart), itemCount);
208        }
209
210        public void onItemRangeRemoved(int positionStart, int itemCount) {
211            assert(itemCount == 1);
212            if (positionStart < mBreakPosition) {
213                mBreakPosition--;
214            }
215            notifyItemRangeRemoved(toViewPosition(positionStart), itemCount);
216        }
217
218        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
219            throw new UnsupportedOperationException();
220        }
221    }
222
223    /**
224     * The most elegant transparent blank box that spans N rows ever conceived.
225     */
226    private static final class EmptyDocumentHolder extends DocumentHolder {
227        final int mVisibleHeight;
228
229        public EmptyDocumentHolder(Context context) {
230            super(context, new Space(context));
231
232            // Per UX spec, this puts a bigger gap between the folders and documents in the grid.
233            mVisibleHeight = context.getResources().getDimensionPixelSize(
234                    R.dimen.grid_item_margin);
235        }
236
237        public void bind(State state) {
238            bind(null, null, state);
239        }
240
241        @Override
242        public void bind(Cursor cursor, String modelId, State state) {
243            if (state.derivedMode == State.MODE_GRID) {
244                itemView.setMinimumHeight(mVisibleHeight);
245            } else {
246                itemView.setMinimumHeight(0);
247            }
248            return;
249        }
250    }
251}
252