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        Integer groupPosition = (Integer) originalView.getTag(R.id.group_position);
187        Integer childPosition = (Integer) originalView.getTag(R.id.child_position);
188
189        if (groupPosition == null || childPosition == null) {
190            return false;
191        }
192
193        mContextMenuInfo = new BookmarkContextMenuInfo(childPosition, groupPosition);
194        if (getParent() != null) {
195            getParent().showContextMenuForChild(this);
196        }
197
198        return true;
199    }
200
201    @Override
202    public void onTop(BreadCrumbView view, int level, Object data) {
203        if (mBreadcrumbController != null) {
204            mBreadcrumbController.onTop(view, level, data);
205        }
206    }
207
208    public void setBreadcrumbController(BreadCrumbView.Controller controller) {
209        mBreadcrumbController = controller;
210    }
211
212    @Override
213    protected ContextMenuInfo getContextMenuInfo() {
214        return mContextMenuInfo;
215    }
216
217    public BrowserBookmarksAdapter getChildAdapter(int groupPosition) {
218        return mAdapter.mChildren.get(groupPosition);
219    }
220
221    private OnClickListener mChildClickListener = new OnClickListener() {
222
223        @Override
224        public void onClick(View v) {
225            if (v.getVisibility() != View.VISIBLE) {
226                return;
227            }
228            int groupPosition = (Integer) v.getTag(R.id.group_position);
229            int childPosition = (Integer) v.getTag(R.id.child_position);
230            if (mAdapter.getGroupCount() <= groupPosition
231                    || mAdapter.mChildren.get(groupPosition).getCount() <= childPosition) {
232                return;
233            }
234            long id = mAdapter.mChildren.get(groupPosition).getItemId(childPosition);
235            if (mOnChildClickListener != null) {
236                mOnChildClickListener.onChildClick(BookmarkExpandableView.this,
237                        v, groupPosition, childPosition, id);
238            }
239        }
240    };
241
242    private OnClickListener mGroupOnClickListener = new OnClickListener() {
243
244        @Override
245        public void onClick(View v) {
246            int groupPosition = (Integer) v.getTag(R.id.group_position);
247            if (isGroupExpanded(groupPosition)) {
248                collapseGroup(groupPosition);
249            } else {
250                expandGroup(groupPosition, true);
251            }
252        }
253    };
254
255    public BreadCrumbView getBreadCrumbs(int groupPosition) {
256        return mAdapter.getBreadCrumbView(groupPosition);
257    }
258
259    public JSONObject saveGroupState() throws JSONException {
260        JSONObject obj = new JSONObject();
261        int count = mAdapter.getGroupCount();
262        for (int i = 0; i < count; i++) {
263            String acctName = mAdapter.mGroups.get(i);
264            if (!isGroupExpanded(i)) {
265                obj.put(acctName != null ? acctName : LOCAL_ACCOUNT_NAME, false);
266            }
267        }
268        return obj;
269    }
270
271    class BookmarkAccountAdapter extends BaseExpandableListAdapter {
272        ArrayList<BrowserBookmarksAdapter> mChildren;
273        ArrayList<String> mGroups;
274        HashMap<Integer, BreadCrumbView> mBreadcrumbs =
275                new HashMap<Integer, BreadCrumbView>();
276        LayoutInflater mInflater;
277        int mRowCount = 1; // assume at least 1 child fits in a row
278        int mLastViewWidth = -1;
279        int mRowPadding = -1;
280        DataSetObserver mObserver = new DataSetObserver() {
281            @Override
282            public void onChanged() {
283                notifyDataSetChanged();
284            }
285
286            @Override
287            public void onInvalidated() {
288                notifyDataSetInvalidated();
289            }
290        };
291
292        public BookmarkAccountAdapter(Context context) {
293            mContext = context;
294            mInflater = LayoutInflater.from(mContext);
295            mChildren = new ArrayList<BrowserBookmarksAdapter>();
296            mGroups = new ArrayList<String>();
297        }
298
299        public void clear() {
300            mGroups.clear();
301            mChildren.clear();
302            notifyDataSetChanged();
303        }
304
305        @Override
306        public Object getChild(int groupPosition, int childPosition) {
307            return mChildren.get(groupPosition).getItem(childPosition);
308        }
309
310        @Override
311        public long getChildId(int groupPosition, int childPosition) {
312            return childPosition;
313        }
314
315        @Override
316        public View getChildView(int groupPosition, int childPosition,
317                boolean isLastChild, View convertView, ViewGroup parent) {
318            if (convertView == null) {
319                convertView = mInflater.inflate(R.layout.bookmark_grid_row, parent, false);
320            }
321            BrowserBookmarksAdapter childAdapter = mChildren.get(groupPosition);
322            int rowCount = mRowCount;
323            LinearLayout row = (LinearLayout) convertView;
324            if (row.getChildCount() > rowCount) {
325                row.removeViews(rowCount, row.getChildCount() - rowCount);
326            }
327            for (int i = 0; i < rowCount; i++) {
328                View cv = null;
329                if (row.getChildCount() > i) {
330                    cv = row.getChildAt(i);
331                }
332                int realChildPosition = (childPosition * rowCount) + i;
333                if (realChildPosition < childAdapter.getCount()) {
334                    View v = childAdapter.getView(realChildPosition, cv, row);
335                    v.setTag(R.id.group_position, groupPosition);
336                    v.setTag(R.id.child_position, realChildPosition);
337                    v.setOnClickListener(mChildClickListener);
338                    v.setLongClickable(mLongClickable);
339                    if (cv == null) {
340                        row.addView(v);
341                    } else if (cv != v) {
342                        row.removeViewAt(i);
343                        row.addView(v, i);
344                    } else {
345                        cv.setVisibility(View.VISIBLE);
346                    }
347                } else if (cv != null) {
348                    cv.setVisibility(View.GONE);
349                }
350            }
351            return row;
352        }
353
354        @Override
355        public int getChildrenCount(int groupPosition) {
356            BrowserBookmarksAdapter adapter = mChildren.get(groupPosition);
357            return (int) Math.ceil(adapter.getCount() / (float)mRowCount);
358        }
359
360        @Override
361        public Object getGroup(int groupPosition) {
362            return mChildren.get(groupPosition);
363        }
364
365        @Override
366        public int getGroupCount() {
367            return mGroups.size();
368        }
369
370        public void measureChildren(int viewWidth) {
371            if (mLastViewWidth == viewWidth) return;
372
373            int rowCount = viewWidth / mColumnWidth;
374            if (mMaxColumnCount > 0) {
375                rowCount = Math.min(rowCount, mMaxColumnCount);
376            }
377            int rowPadding = (viewWidth - (rowCount * mColumnWidth)) / 2;
378            boolean notify = rowCount != mRowCount || rowPadding != mRowPadding;
379            mRowCount = rowCount;
380            mRowPadding = rowPadding;
381            mLastViewWidth = viewWidth;
382            if (notify) {
383                notifyDataSetChanged();
384            }
385        }
386
387        @Override
388        public long getGroupId(int groupPosition) {
389            return groupPosition;
390        }
391
392        @Override
393        public View getGroupView(int groupPosition, boolean isExpanded,
394                View view, ViewGroup parent) {
395            if (view == null) {
396                view = mInflater.inflate(R.layout.bookmark_group_view, parent, false);
397                view.setOnClickListener(mGroupOnClickListener);
398            }
399            view.setTag(R.id.group_position, groupPosition);
400            FrameLayout crumbHolder = (FrameLayout) view.findViewById(R.id.crumb_holder);
401            crumbHolder.removeAllViews();
402            BreadCrumbView crumbs = getBreadCrumbView(groupPosition);
403            if (crumbs.getParent() != null) {
404                ((ViewGroup)crumbs.getParent()).removeView(crumbs);
405            }
406            crumbHolder.addView(crumbs);
407            TextView name = (TextView) view.findViewById(R.id.group_name);
408            String groupName = mGroups.get(groupPosition);
409            if (groupName == null) {
410                groupName = mContext.getString(R.string.local_bookmarks);
411            }
412            name.setText(groupName);
413            return view;
414        }
415
416        public BreadCrumbView getBreadCrumbView(int groupPosition) {
417            BreadCrumbView crumbs = mBreadcrumbs.get(groupPosition);
418            if (crumbs == null) {
419                crumbs = (BreadCrumbView)
420                        mInflater.inflate(R.layout.bookmarks_header, null);
421                crumbs.setController(BookmarkExpandableView.this);
422                crumbs.setUseBackButton(true);
423                crumbs.setMaxVisible(2);
424                String bookmarks = mContext.getString(R.string.bookmarks);
425                crumbs.pushView(bookmarks, false,
426                        BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER);
427                crumbs.setTag(R.id.group_position, groupPosition);
428                crumbs.setVisibility(View.GONE);
429                mBreadcrumbs.put(groupPosition, crumbs);
430            }
431            return crumbs;
432        }
433
434        @Override
435        public boolean hasStableIds() {
436            return false;
437        }
438
439        @Override
440        public boolean isChildSelectable(int groupPosition, int childPosition) {
441            return true;
442        }
443    }
444
445    public static class BookmarkContextMenuInfo implements ContextMenuInfo {
446
447        private BookmarkContextMenuInfo(int childPosition, int groupPosition) {
448            this.childPosition = childPosition;
449            this.groupPosition = groupPosition;
450        }
451
452        public int childPosition;
453        public int groupPosition;
454    }
455
456}
457