OSDN Git Service

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