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 17 package com.android.browser.view; 18 19 import android.content.Context; 20 import android.database.DataSetObserver; 21 import android.provider.BrowserContract; 22 import android.util.AttributeSet; 23 import android.view.ContextMenu; 24 import android.view.ContextMenu.ContextMenuInfo; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.widget.BaseExpandableListAdapter; 29 import android.widget.ExpandableListAdapter; 30 import android.widget.ExpandableListView; 31 import android.widget.FrameLayout; 32 import android.widget.LinearLayout; 33 import android.widget.TextView; 34 35 import com.android.browser.BreadCrumbView; 36 import com.android.browser.BrowserBookmarksAdapter; 37 import com.android.browser.R; 38 import com.android.internal.view.menu.MenuBuilder; 39 40 import org.json.JSONException; 41 import org.json.JSONObject; 42 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 46 public 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