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