/WebVox/src/com/marvin/webvox/BrowserHistoryPage.java

http://eyes-free.googlecode.com/ · Java · 525 lines · 382 code · 59 blank · 84 comment · 49 complexity · dc5d0a8d61abdd8b591a0036f07b755b MD5 · raw file

  1. /*
  2. * Copyright (C) 2008 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. package com.marvin.webvox;
  17. import com.marvin.webvox.R;
  18. import android.app.Activity;
  19. import android.app.ExpandableListActivity;
  20. import android.content.Intent;
  21. import android.content.pm.PackageManager;
  22. import android.content.pm.ResolveInfo;
  23. import android.database.ContentObserver;
  24. import android.database.Cursor;
  25. import android.database.DataSetObserver;
  26. import android.graphics.Bitmap;
  27. import android.graphics.BitmapFactory;
  28. import android.os.Bundle;
  29. import android.os.Handler;
  30. //import android.os.ServiceManager;
  31. import android.provider.Browser;
  32. //import android.text.IClipboard;
  33. import android.util.Log;
  34. import android.view.ContextMenu;
  35. import android.view.KeyEvent;
  36. import android.view.LayoutInflater;
  37. import android.view.Menu;
  38. import android.view.MenuInflater;
  39. import android.view.MenuItem;
  40. import android.view.View;
  41. import android.view.ViewGroup;
  42. import android.view.ViewGroup.LayoutParams;
  43. import android.view.ContextMenu.ContextMenuInfo;
  44. import android.view.ViewStub;
  45. import android.webkit.DateSorter;
  46. import android.webkit.WebIconDatabase.IconListener;
  47. import android.widget.AdapterView;
  48. import android.widget.ExpandableListAdapter;
  49. import android.widget.ExpandableListView;
  50. import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
  51. import android.widget.TextView;
  52. import android.widget.Toast;
  53. import java.util.List;
  54. import java.util.Vector;
  55. /**
  56. * Activity for displaying the browser's history, divided into
  57. * days of viewing.
  58. */
  59. public class BrowserHistoryPage extends ExpandableListActivity {
  60. private HistoryAdapter mAdapter;
  61. private DateSorter mDateSorter;
  62. private boolean mDisableNewWindow;
  63. private HistoryItem mContextHeader;
  64. private final static String LOGTAG = "browser";
  65. // Implementation of WebIconDatabase.IconListener
  66. private class IconReceiver implements IconListener {
  67. public void onReceivedIcon(String url, Bitmap icon) {
  68. setListAdapter(mAdapter);
  69. }
  70. }
  71. // Instance of IconReceiver
  72. private final IconReceiver mIconReceiver = new IconReceiver();
  73. /**
  74. * Report back to the calling activity to load a site.
  75. * @param url Site to load.
  76. * @param newWindow True if the URL should be loaded in a new window
  77. */
  78. private void loadUrl(String url, boolean newWindow) {
  79. Intent intent = new Intent().setAction(url);
  80. if (newWindow) {
  81. Bundle b = new Bundle();
  82. b.putBoolean("new_window", true);
  83. intent.putExtras(b);
  84. }
  85. setResultToParent(RESULT_OK, intent);
  86. finish();
  87. }
  88. private void copy(CharSequence text) {
  89. /*
  90. try {
  91. IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
  92. if (clip != null) {
  93. clip.setClipboardText(text);
  94. }
  95. } catch (android.os.RemoteException e) {
  96. Log.e(LOGTAG, "Copy failed", e);
  97. }
  98. */
  99. }
  100. @Override
  101. protected void onCreate(Bundle icicle) {
  102. super.onCreate(icicle);
  103. setTitle(R.string.browser_history);
  104. mDateSorter = new DateSorter(this);
  105. mAdapter = new HistoryAdapter();
  106. setListAdapter(mAdapter);
  107. final ExpandableListView list = getExpandableListView();
  108. list.setOnCreateContextMenuListener(this);
  109. View v = new ViewStub(this, R.layout.empty_history);
  110. addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT,
  111. LayoutParams.FILL_PARENT));
  112. list.setEmptyView(v);
  113. // Do not post the runnable if there is nothing in the list.
  114. if (list.getExpandableListAdapter().getGroupCount() > 0) {
  115. list.post(new Runnable() {
  116. public void run() {
  117. // In case the history gets cleared before this event
  118. // happens.
  119. if (list.getExpandableListAdapter().getGroupCount() > 0) {
  120. list.expandGroup(0);
  121. }
  122. }
  123. });
  124. }
  125. mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window",
  126. false);
  127. CombinedBookmarkHistoryActivity.getIconListenerSet()
  128. .addListener(mIconReceiver);
  129. // initialize the result to canceled, so that if the user just presses
  130. // back then it will have the correct result
  131. setResultToParent(RESULT_CANCELED, null);
  132. }
  133. @Override
  134. protected void onDestroy() {
  135. super.onDestroy();
  136. CombinedBookmarkHistoryActivity.getIconListenerSet()
  137. .removeListener(mIconReceiver);
  138. }
  139. @Override
  140. public boolean onCreateOptionsMenu(Menu menu) {
  141. super.onCreateOptionsMenu(menu);
  142. MenuInflater inflater = getMenuInflater();
  143. inflater.inflate(R.menu.history, menu);
  144. return true;
  145. }
  146. @Override
  147. public boolean onPrepareOptionsMenu(Menu menu) {
  148. menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver()));
  149. return true;
  150. }
  151. @Override
  152. public boolean onOptionsItemSelected(MenuItem item) {
  153. switch (item.getItemId()) {
  154. case R.id.clear_history_menu_id:
  155. // FIXME: Need to clear the tab control in browserActivity
  156. // as well
  157. Browser.clearHistory(getContentResolver());
  158. mAdapter.refreshData();
  159. return true;
  160. default:
  161. break;
  162. }
  163. return super.onOptionsItemSelected(item);
  164. }
  165. @Override
  166. public void onCreateContextMenu(ContextMenu menu, View v,
  167. ContextMenuInfo menuInfo) {
  168. ExpandableListContextMenuInfo i =
  169. (ExpandableListContextMenuInfo) menuInfo;
  170. // Do not allow a context menu to come up from the group views.
  171. if (!(i.targetView instanceof HistoryItem)) {
  172. return;
  173. }
  174. // Inflate the menu
  175. MenuInflater inflater = getMenuInflater();
  176. inflater.inflate(R.menu.historycontext, menu);
  177. HistoryItem historyItem = (HistoryItem) i.targetView;
  178. // Setup the header
  179. if (mContextHeader == null) {
  180. mContextHeader = new HistoryItem(this);
  181. } else if (mContextHeader.getParent() != null) {
  182. ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader);
  183. }
  184. historyItem.copyTo(mContextHeader);
  185. menu.setHeaderView(mContextHeader);
  186. // Only show open in new tab if it was not explicitly disabled
  187. if (mDisableNewWindow) {
  188. menu.findItem(R.id.new_window_context_menu_id).setVisible(false);
  189. }
  190. // For a bookmark, provide the option to remove it from bookmarks
  191. if (historyItem.isBookmark()) {
  192. MenuItem item = menu.findItem(R.id.save_to_bookmarks_menu_id);
  193. item.setTitle(R.string.remove_from_bookmarks);
  194. }
  195. // decide whether to show the share link option
  196. PackageManager pm = getPackageManager();
  197. Intent send = new Intent(Intent.ACTION_SEND);
  198. send.setType("text/plain");
  199. ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
  200. menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null);
  201. super.onCreateContextMenu(menu, v, menuInfo);
  202. }
  203. @Override
  204. public boolean onContextItemSelected(MenuItem item) {
  205. ExpandableListContextMenuInfo i =
  206. (ExpandableListContextMenuInfo) item.getMenuInfo();
  207. HistoryItem historyItem = (HistoryItem) i.targetView;
  208. String url = historyItem.getUrl();
  209. String title = historyItem.getName();
  210. switch (item.getItemId()) {
  211. case R.id.open_context_menu_id:
  212. loadUrl(url, false);
  213. return true;
  214. case R.id.new_window_context_menu_id:
  215. loadUrl(url, true);
  216. return true;
  217. case R.id.save_to_bookmarks_menu_id:
  218. if (historyItem.isBookmark()) {
  219. Bookmarks.removeFromBookmarks(this, getContentResolver(),
  220. url, title);
  221. } else {
  222. Browser.saveBookmark(this, title, url);
  223. }
  224. return true;
  225. case R.id.share_link_context_menu_id:
  226. // Browser.sendString(this, url,
  227. // getText(R.string.choosertitle_sharevia).toString());
  228. return true;
  229. case R.id.copy_url_context_menu_id:
  230. copy(url);
  231. return true;
  232. case R.id.delete_context_menu_id:
  233. Browser.deleteFromHistory(getContentResolver(), url);
  234. mAdapter.refreshData();
  235. return true;
  236. case R.id.homepage_context_menu_id:
  237. BrowserSettings.getInstance().setHomePage(this, url);
  238. Toast.makeText(this, R.string.homepage_set,
  239. Toast.LENGTH_LONG).show();
  240. return true;
  241. default:
  242. break;
  243. }
  244. return super.onContextItemSelected(item);
  245. }
  246. @Override
  247. public boolean onChildClick(ExpandableListView parent, View v,
  248. int groupPosition, int childPosition, long id) {
  249. if (v instanceof HistoryItem) {
  250. loadUrl(((HistoryItem) v).getUrl(), false);
  251. return true;
  252. }
  253. return false;
  254. }
  255. // This Activity is generally a sub-Activity of CombinedHistoryActivity. In
  256. // that situation, we need to pass our result code up to our parent.
  257. // However, if someone calls this Activity directly, then this has no
  258. // parent, and it needs to set it on itself.
  259. private void setResultToParent(int resultCode, Intent data) {
  260. Activity a = getParent() == null ? this : getParent();
  261. a.setResult(resultCode, data);
  262. }
  263. private class ChangeObserver extends ContentObserver {
  264. public ChangeObserver() {
  265. super(new Handler());
  266. }
  267. @Override
  268. public boolean deliverSelfNotifications() {
  269. return true;
  270. }
  271. @Override
  272. public void onChange(boolean selfChange) {
  273. mAdapter.refreshData();
  274. }
  275. }
  276. private class HistoryAdapter implements ExpandableListAdapter {
  277. // Array for each of our bins. Each entry represents how many items are
  278. // in that bin.
  279. private int mItemMap[];
  280. // This is our GroupCount. We will have at most DateSorter.DAY_COUNT
  281. // bins, less if the user has no items in one or more bins.
  282. private int mNumberOfBins;
  283. private Vector<DataSetObserver> mObservers;
  284. private Cursor mCursor;
  285. HistoryAdapter() {
  286. mObservers = new Vector<DataSetObserver>();
  287. final String whereClause = Browser.BookmarkColumns.VISITS + " > 0"
  288. // In AddBookmarkPage, where we save new bookmarks, we add
  289. // three visits to newly created bookmarks, so that
  290. // bookmarks that have not been visited will show up in the
  291. // most visited, and higher in the goto search box.
  292. // However, this puts the site in the history, unless we
  293. // ignore sites with a DATE of 0, which the next line does.
  294. + " AND " + Browser.BookmarkColumns.DATE + " > 0";
  295. final String orderBy = Browser.BookmarkColumns.DATE + " DESC";
  296. mCursor = managedQuery(
  297. Browser.BOOKMARKS_URI,
  298. Browser.HISTORY_PROJECTION,
  299. whereClause, null, orderBy);
  300. buildMap();
  301. mCursor.registerContentObserver(new ChangeObserver());
  302. }
  303. void refreshData() {
  304. if (mCursor.isClosed()) {
  305. return;
  306. }
  307. mCursor.requery();
  308. buildMap();
  309. for (DataSetObserver o : mObservers) {
  310. o.onChanged();
  311. }
  312. }
  313. private void buildMap() {
  314. // The cursor is sorted by date
  315. // The ItemMap will store the number of items in each bin.
  316. int array[] = new int[DateSorter.DAY_COUNT];
  317. // Zero out the array.
  318. for (int j = 0; j < DateSorter.DAY_COUNT; j++) {
  319. array[j] = 0;
  320. }
  321. mNumberOfBins = 0;
  322. int dateIndex = -1;
  323. if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
  324. while (!mCursor.isAfterLast()) {
  325. long date = mCursor.getLong(Browser.HISTORY_PROJECTION_DATE_INDEX);
  326. int index = mDateSorter.getIndex(date);
  327. if (index > dateIndex) {
  328. mNumberOfBins++;
  329. if (index == DateSorter.DAY_COUNT - 1) {
  330. // We are already in the last bin, so it will
  331. // include all the remaining items
  332. array[index] = mCursor.getCount()
  333. - mCursor.getPosition();
  334. break;
  335. }
  336. dateIndex = index;
  337. }
  338. array[dateIndex]++;
  339. mCursor.moveToNext();
  340. }
  341. }
  342. mItemMap = array;
  343. }
  344. // This translates from a group position in the Adapter to a position in
  345. // our array. This is necessary because some positions in the array
  346. // have no history items, so we simply do not present those positions
  347. // to the Adapter.
  348. private int groupPositionToArrayPosition(int groupPosition) {
  349. if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
  350. throw new AssertionError("group position out of range");
  351. }
  352. if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) {
  353. // In the first case, we have exactly the same number of bins
  354. // as our maximum possible, so there is no need to do a
  355. // conversion
  356. // The second statement is in case this method gets called when
  357. // the array is empty, in which case the provided groupPosition
  358. // will do fine.
  359. return groupPosition;
  360. }
  361. int arrayPosition = -1;
  362. while (groupPosition > -1) {
  363. arrayPosition++;
  364. if (mItemMap[arrayPosition] != 0) {
  365. groupPosition--;
  366. }
  367. }
  368. return arrayPosition;
  369. }
  370. public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
  371. View convertView, ViewGroup parent) {
  372. groupPosition = groupPositionToArrayPosition(groupPosition);
  373. HistoryItem item;
  374. if (null == convertView || !(convertView instanceof HistoryItem)) {
  375. item = new HistoryItem(BrowserHistoryPage.this);
  376. // Add padding on the left so it will be indented from the
  377. // arrows on the group views.
  378. item.setPadding(item.getPaddingLeft() + 10,
  379. item.getPaddingTop(),
  380. item.getPaddingRight(),
  381. item.getPaddingBottom());
  382. } else {
  383. item = (HistoryItem) convertView;
  384. }
  385. int index = childPosition;
  386. for (int i = 0; i < groupPosition; i++) {
  387. index += mItemMap[i];
  388. }
  389. mCursor.moveToPosition(index);
  390. item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
  391. String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
  392. item.setUrl(url);
  393. byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
  394. if (data != null) {
  395. item.setFavicon(BitmapFactory.decodeByteArray(data, 0,
  396. data.length));
  397. } else {
  398. item.setFavicon(CombinedBookmarkHistoryActivity
  399. .getIconListenerSet().getFavicon(url));
  400. }
  401. item.setIsBookmark(1 ==
  402. mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX));
  403. return item;
  404. }
  405. public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
  406. groupPosition = groupPositionToArrayPosition(groupPosition);
  407. TextView item;
  408. if (null == convertView || !(convertView instanceof TextView)) {
  409. LayoutInflater factory =
  410. LayoutInflater.from(BrowserHistoryPage.this);
  411. item = (TextView)
  412. factory.inflate(R.layout.history_header, null);
  413. } else {
  414. item = (TextView) convertView;
  415. }
  416. item.setText(mDateSorter.getLabel(groupPosition));
  417. return item;
  418. }
  419. public boolean areAllItemsEnabled() {
  420. return true;
  421. }
  422. public boolean isChildSelectable(int groupPosition, int childPosition) {
  423. return true;
  424. }
  425. public int getGroupCount() {
  426. return mNumberOfBins;
  427. }
  428. public int getChildrenCount(int groupPosition) {
  429. return mItemMap[groupPositionToArrayPosition(groupPosition)];
  430. }
  431. public Object getGroup(int groupPosition) {
  432. return null;
  433. }
  434. public Object getChild(int groupPosition, int childPosition) {
  435. return null;
  436. }
  437. public long getGroupId(int groupPosition) {
  438. return groupPosition;
  439. }
  440. public long getChildId(int groupPosition, int childPosition) {
  441. return (childPosition << 3) + groupPosition;
  442. }
  443. public boolean hasStableIds() {
  444. return true;
  445. }
  446. public void registerDataSetObserver(DataSetObserver observer) {
  447. mObservers.add(observer);
  448. }
  449. public void unregisterDataSetObserver(DataSetObserver observer) {
  450. mObservers.remove(observer);
  451. }
  452. public void onGroupExpanded(int groupPosition) {
  453. }
  454. public void onGroupCollapsed(int groupPosition) {
  455. }
  456. public long getCombinedChildId(long groupId, long childId) {
  457. return childId;
  458. }
  459. public long getCombinedGroupId(long groupId) {
  460. return groupId;
  461. }
  462. public boolean isEmpty() {
  463. return mCursor.getCount() == 0;
  464. }
  465. }
  466. }