1/*
2 * Copyright (C) 2011 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.browser.view;
18
19import android.content.Context;
20import android.database.DataSetObserver;
21import android.provider.BrowserContract;
22import android.util.AttributeSet;
23import android.view.ContextMenu;
24import android.view.ContextMenu.ContextMenuInfo;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.BaseExpandableListAdapter;
29import android.widget.ExpandableListAdapter;
30import android.widget.ExpandableListView;
31import android.widget.FrameLayout;
32import android.widget.LinearLayout;
33import android.widget.TextView;
34
35import com.android.browser.BreadCrumbView;
36import com.android.browser.BrowserBookmarksAdapter;
37import com.android.browser.R;
38import com.android.internal.view.menu.MenuBuilder;
39
40import org.json.JSONException;
41import org.json.JSONObject;
42
43import java.util.ArrayList;
44import java.util.HashMap;
45
46public class BookmarkExpandableView extends ExpandableListView
47        implements BreadCrumbView.Controller {
48
49    public static final String LOCAL_ACCOUNT_NAME = "local";
50
51    private BookmarkAccountAdapter mAdapter;
52    private int mColumnWidth;
53    private Context mContext;
54    private OnChildClickListener mOnChildClickListener;
55    private ContextMenuInfo mContextMenuInfo = null;
56    private OnCreateContextMenuListener mOnCreateContextMenuListener;
57    private boolean mLongClickable;
58    private BreadCrumbView.Controller mBreadcrumbController;
59    private int mMaxColumnCount;
60
61    public BookmarkExpandableView(Context context) {
62        super(context);
63        init(context);
64    }
65
66    public BookmarkExpandableView(Context context, AttributeSet attrs) {
67        super(context, attrs);
68        init(context);
69    }
70
71    public BookmarkExpandableView(
72            Context context, AttributeSet attrs, int defStyle) {
73        super(context, attrs, defStyle);
74        init(context);
75    }
76
77    void init(Context context) {
78        mContext = context;
79        setItemsCanFocus(true);
80        setLongClickable(false);
81        mMaxColumnCount = mContext.getResources()
82                .getInteger(R.integer.max_bookmark_columns);
83        setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
84        mAdapter = new BookmarkAccountAdapter(mContext);
85        super.setAdapter(mAdapter);
86    }
87
88    @Override
89    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
90        int width = MeasureSpec.getSize(widthMeasureSpec);
91        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
92        if (width > 0) {
93            mAdapter.measureChildren(width);
94            setPadding(mAdapter.mRowPadding, 0, mAdapter.mRowPadding, 0);
95            widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode);
96        }
97        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
98        if (width != getMeasuredWidth()) {
99            mAdapter.measureChildren(getMeasuredWidth());
100        }
101    }
102
103    @Override
104    public void setAdapter(ExpandableListAdapter adapter) {
105        throw new RuntimeException("Not supported");
106    }
107
108    public void setColumnWidthFromLayout(int layout) {
109        LayoutInflater infalter = LayoutInflater.from(mContext);
110        View v = infalter.inflate(layout, this, false);
111        v.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
112        mColumnWidth = v.getMeasuredWidth();
113    }
114
115    public void clearAccounts() {
116        mAdapter.clear();
117    }
118
119    public void addAccount(String accountName, BrowserBookmarksAdapter adapter,
120            boolean expandGroup) {
121        // First, check if it already exists
122        int indexOf = mAdapter.mGroups.indexOf(accountName);
123        if (indexOf >= 0) {
124            BrowserBookmarksAdapter existing = mAdapter.mChildren.get(indexOf);
125            if (existing != adapter) {
126                existing.unregisterDataSetObserver(mAdapter.mObserver);
127                // Replace the existing one
128                mAdapter.mChildren.remove(indexOf);
129                mAdapter.mChildren.add(indexOf, adapter);
130                adapter.registerDataSetObserver(mAdapter.mObserver);
131            }
132        } else {
133            mAdapter.mGroups.add(accountName);
134            mAdapter.mChildren.add(adapter);
135            adapter.registerDataSetObserver(mAdapter.mObserver);
136        }
137        mAdapter.notifyDataSetChanged();
138        if (expandGroup) {
139            expandGroup(mAdapter.getGroupCount() - 1);
140        }
141    }
142
143    @Override
144    public void setOnChildClickListener(OnChildClickListener onChildClickListener) {
145        mOnChildClickListener = onChildClickListener;
146    }
147
148    @Override
149    public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
150        mOnCreateContextMenuListener = l;
151        if (!mLongClickable) {
152            mLongClickable = true;
153            if (mAdapter != null) {
154                mAdapter.notifyDataSetChanged();
155            }
156        }
157    }
158
159    @Override
160    public void createContextMenu(ContextMenu menu) {
161        // The below is copied from View - we want to bypass the override
162        // in AbsListView
163
164        ContextMenuInfo menuInfo = getContextMenuInfo();
165
166        // Sets the current menu info so all items added to menu will have
167        // my extra info set.
168        ((MenuBuilder)menu).setCurrentMenuInfo(menuInfo);
169
170        onCreateContextMenu(menu);
171        if (mOnCreateContextMenuListener != null) {
172            mOnCreateContextMenuListener.onCreateContextMenu(menu, this, menuInfo);
173        }
174
175        // Clear the extra information so subsequent items that aren't mine don't
176        // have my extra info.
177        ((MenuBuilder)menu).setCurrentMenuInfo(null);
178
179        if (mParent != null) {
180            mParent.createContextMenu(menu);
181        }
182    }
183
184    @Override
185    public boolean showContextMenuForChild(View originalView) {
186        int groupPosition = (Integer) originalView.getTag(R.id.group_position);
187        int childPosition = (Integer) originalView.getTag(R.id.child_position);
188
189        mContextMenuInfo = new BookmarkContextMenuInfo(childPosition,
190                groupPosition);
191        if (getParent() != null) {
192            getParent().showContextMenuForChild(this);
193        }
194
195        return true;
196    }
197
198    @Override
199    public void onTop(BreadCrumbView view, int level, Object data) {
200        if (mBreadcrumbController != null) {
201            mBreadcrumbController.onTop(view, level, data);
202        }
203    }
204
205    public void setBreadcrumbController(BreadCrumbView.Controller controller) {
206        mBreadcrumbController = controller;
207    }
208
209    @Override
210    protected ContextMenuInfo getContextMenuInfo() {
211        return mContextMenuInfo;
212    }
213
214    public BrowserBookmarksAdapter getChildAdapter(int groupPosition) {
215        return mAdapter.mChildren.get(groupPosition);
216    }
217
218    private OnClickListener mChildClickListener = new OnClickListener() {
219
220        @Override
221        public void onClick(View v) {
222            if (v.getVisibility() != View.VISIBLE) {
223                return;
224            }
225            int groupPosition = (Integer) v.getTag(R.id.group_position);
226            int childPosition = (Integer) v.getTag(R.id.child_position);
227            if (mAdapter.getGroupCount() <= groupPosition
228                    || mAdapter.mChildren.get(groupPosition).getCount() <= childPosition) {
229                return;
230            }
231            long id = mAdapter.mChildren.get(groupPosition).getItemId(childPosition);
232            if (mOnChildClickListener != null) {
233                mOnChildClickListener.onChildClick(BookmarkExpandableView.this,
234                        v, groupPosition, childPosition, id);
235            }
236        }
237    };
238
239    private OnClickListener mGroupOnClickListener = new OnClickListener() {
240
241        @Override
242        public void onClick(View v) {
243            int groupPosition = (Integer) v.getTag(R.id.group_position);
244            if (isGroupExpanded(groupPosition)) {
245                collapseGroup(groupPosition);
246            } else {
247                expandGroup(groupPosition, true);
248            }
249        }
250    };
251
252    public BreadCrumbView getBreadCrumbs(int groupPosition) {
253        return mAdapter.getBreadCrumbView(groupPosition);
254    }
255
256    public JSONObject saveGroupState() throws JSONException {
257        JSONObject obj = new JSONObject();
258        int count = mAdapter.getGroupCount();
259        for (int i = 0; i < count; i++) {
260            String acctName = mAdapter.mGroups.get(i);
261            if (!isGroupExpanded(i)) {
262                obj.put(acctName != null ? acctName : LOCAL_ACCOUNT_NAME, false);
263            }
264        }
265        return obj;
266    }
267
268    class BookmarkAccountAdapter extends BaseExpandableListAdapter {
269        ArrayList<BrowserBookmarksAdapter> mChildren;
270        ArrayList<String> mGroups;
271        HashMap<Integer, BreadCrumbView> mBreadcrumbs =
272                new HashMap<Integer, BreadCrumbView>();
273        LayoutInflater mInflater;
274        int mRowCount = 1; // assume at least 1 child fits in a row
275        int mLastViewWidth = -1;
276        int mRowPadding = -1;
277        DataSetObserver mObserver = new DataSetObserver() {
278            @Override
279            public void onChanged() {
280                notifyDataSetChanged();
281            }
282
283            @Override
284            public void onInvalidated() {
285                notifyDataSetInvalidated();
286            }
287        };
288
289        public BookmarkAccountAdapter(Context context) {
290            mContext = context;
291            mInflater = LayoutInflater.from(mContext);
292            mChildren = new ArrayList<BrowserBookmarksAdapter>();
293            mGroups = new ArrayList<String>();
294        }
295
296        public void clear() {
297            mGroups.clear();
298            mChildren.clear();
299            notifyDataSetChanged();
300        }
301
302        @Override
303        public Object getChild(int groupPosition, int childPosition) {
304            return mChildren.get(groupPosition).getItem(childPosition);
305        }
306
307        @Override
308        public long getChildId(int groupPosition, int childPosition) {
309            return childPosition;
310        }
311
312        @Override
313        public View getChildView(int groupPosition, int childPosition,
314                boolean isLastChild, View convertView, ViewGroup parent) {
315            if (convertView == null) {
316                convertView = mInflater.inflate(R.layout.bookmark_grid_row, parent, false);
317            }
318            BrowserBookmarksAdapter childAdapter = mChildren.get(groupPosition);
319            int rowCount = mRowCount;
320            LinearLayout row = (LinearLayout) convertView;
321            if (row.getChildCount() > rowCount) {
322                row.removeViews(rowCount, row.getChildCount() - rowCount);
323            }
324            for (int i = 0; i < rowCount; i++) {
325                View cv = null;
326                if (row.getChildCount() > i) {
327                    cv = row.getChildAt(i);
328                }
329                int realChildPosition = (childPosition * rowCount) + i;
330                if (realChildPosition < childAdapter.getCount()) {
331                    View v = childAdapter.getView(realChildPosition, cv, row);
332                    v.setTag(R.id.group_position, groupPosition);
333                    v.setTag(R.id.child_position, realChildPosition);
334                    v.setOnClickListener(mChildClickListener);
335                    v.setLongClickable(mLongClickable);
336                    if (cv == null) {
337                        row.addView(v);
338                    } else if (cv != v) {
339                        row.removeViewAt(i);
340                        row.addView(v, i);
341                    } else {
342                        cv.setVisibility(View.VISIBLE);
343                    }
344                } else if (cv != null) {
345                    cv.setVisibility(View.GONE);
346                }
347            }
348            return row;
349        }
350
351        @Override
352        public int getChildrenCount(int groupPosition) {
353            BrowserBookmarksAdapter adapter = mChildren.get(groupPosition);
354            return (int) Math.ceil(adapter.getCount() / (float)mRowCount);
355        }
356
357        @Override
358        public Object getGroup(int groupPosition) {
359            return mChildren.get(groupPosition);
360        }
361
362        @Override
363        public int getGroupCount() {
364            return mGroups.size();
365        }
366
367        public void measureChildren(int viewWidth) {
368            if (mLastViewWidth == viewWidth) return;
369
370            int rowCount = viewWidth / mColumnWidth;
371            if (mMaxColumnCount > 0) {
372                rowCount = Math.min(rowCount, mMaxColumnCount);
373            }
374            int rowPadding = (viewWidth - (rowCount * mColumnWidth)) / 2;
375            boolean notify = rowCount != mRowCount || rowPadding != mRowPadding;
376            mRowCount = rowCount;
377            mRowPadding = rowPadding;
378            mLastViewWidth = viewWidth;
379            if (notify) {
380                notifyDataSetChanged();
381            }
382        }
383
384        @Override
385        public long getGroupId(int groupPosition) {
386            return groupPosition;
387        }
388
389        @Override
390        public View getGroupView(int groupPosition, boolean isExpanded,
391                View view, ViewGroup parent) {
392            if (view == null) {
393                view = mInflater.inflate(R.layout.bookmark_group_view, parent, false);
394                view.setOnClickListener(mGroupOnClickListener);
395            }
396            view.setTag(R.id.group_position, groupPosition);
397            FrameLayout crumbHolder = (FrameLayout) view.findViewById(R.id.crumb_holder);
398            crumbHolder.removeAllViews();
399            BreadCrumbView crumbs = getBreadCrumbView(groupPosition);
400            if (crumbs.getParent() != null) {
401                ((ViewGroup)crumbs.getParent()).removeView(crumbs);
402            }
403            crumbHolder.addView(crumbs);
404            TextView name = (TextView) view.findViewById(R.id.group_name);
405            String groupName = mGroups.get(groupPosition);
406            if (groupName == null) {
407                groupName = mContext.getString(R.string.local_bookmarks);
408            }
409            name.setText(groupName);
410            return view;
411        }
412
413        public BreadCrumbView getBreadCrumbView(int groupPosition) {
414            BreadCrumbView crumbs = mBreadcrumbs.get(groupPosition);
415            if (crumbs == null) {
416                crumbs = (BreadCrumbView)
417                        mInflater.inflate(R.layout.bookmarks_header, null);
418                crumbs.setController(BookmarkExpandableView.this);
419                crumbs.setUseBackButton(true);
420                crumbs.setMaxVisible(2);
421                String bookmarks = mContext.getString(R.string.bookmarks);
422                crumbs.pushView(bookmarks, false,
423                        BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER);
424                crumbs.setTag(R.id.group_position, groupPosition);
425                crumbs.setVisibility(View.GONE);
426                mBreadcrumbs.put(groupPosition, crumbs);
427            }
428            return crumbs;
429        }
430
431        @Override
432        public boolean hasStableIds() {
433            return false;
434        }
435
436        @Override
437        public boolean isChildSelectable(int groupPosition, int childPosition) {
438            return true;
439        }
440    }
441
442    public static class BookmarkContextMenuInfo implements ContextMenuInfo {
443
444        private BookmarkContextMenuInfo(int childPosition, int groupPosition) {
445            this.childPosition = childPosition;
446            this.groupPosition = groupPosition;
447        }
448
449        public int childPosition;
450        public int groupPosition;
451    }
452
453}
454