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.support.v7.widget.GridLayoutManager;
20import android.support.v7.widget.RecyclerView.AdapterDataObserver;
21import android.view.ViewGroup;
22
23import com.android.documentsui.Model;
24import com.android.documentsui.base.EventListener;
25import com.android.documentsui.dirlist.Message.HeaderMessage;
26import com.android.documentsui.dirlist.Message.InflateMessage;
27import com.android.documentsui.Model.Update;
28
29import java.util.List;
30
31/**
32 * Adapter wrapper that embellishes the directory list by inserting Holder views inbetween
33 * items.
34 */
35final 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> getModelIds() {
242        return mDelegate.getModelIds();
243    }
244
245    @Override
246    public int getAdapterPosition(String modelId) {
247        return toViewPosition(mDelegate.getAdapterPosition(modelId));
248    }
249
250    @Override
251    public String getModelId(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.getModelId(toDelegatePosition(p));
265    }
266
267    @Override
268    public void onItemSelectionChanged(String id) {
269        mDelegate.onItemSelectionChanged(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