FileDocCategorySizeDatePackage
BrowserHistoryPage.javaAPI DocAndroid 1.5 API17139Wed May 06 22:42:42 BST 2009com.android.browser

BrowserHistoryPage.java

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.browser;

import android.app.Activity;
import android.app.ExpandableListActivity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.os.ServiceManager;
import android.provider.Browser;
import android.text.IClipboard;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ContextMenu.ContextMenuInfo;
import android.webkit.DateSorter;
import android.webkit.WebIconDatabase.IconListener;
import android.widget.AdapterView;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
import android.widget.TextView;

import java.util.List;
import java.util.Vector;

/**
 * Activity for displaying the browser's history, divided into
 * days of viewing.
 */
public class BrowserHistoryPage extends ExpandableListActivity {
    private HistoryAdapter          mAdapter;
    private DateSorter              mDateSorter;
    private boolean                 mMaxTabsOpen;

    private final static String LOGTAG = "browser";

    // Implementation of WebIconDatabase.IconListener
    private class IconReceiver implements IconListener {
        public void onReceivedIcon(String url, Bitmap icon) {
            setListAdapter(mAdapter);
        }
    }
    // Instance of IconReceiver
    private final IconReceiver mIconReceiver = new IconReceiver();

    /**
     * Report back to the calling activity to load a site.
     * @param url   Site to load.
     * @param newWindow True if the URL should be loaded in a new window
     */
    private void loadUrl(String url, boolean newWindow) {
        Intent intent = new Intent().setAction(url);
        if (newWindow) {
            Bundle b = new Bundle();
            b.putBoolean("new_window", true);
            intent.putExtras(b);
        }
        setResultToParent(RESULT_OK, intent);
        finish();
    }
    
    private void copy(CharSequence text) {
        try {
            IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
            if (clip != null) {
                clip.setClipboardText(text);
            }
        } catch (android.os.RemoteException e) {
            Log.e(LOGTAG, "Copy failed", e);
        }
    }

    @Override
    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setTitle(R.string.browser_history);
        
        mDateSorter = new DateSorter(this);

        mAdapter = new HistoryAdapter();
        setListAdapter(mAdapter);
        final ExpandableListView list = getExpandableListView();
        list.setOnCreateContextMenuListener(this);
        LayoutInflater factory = LayoutInflater.from(this);
        View v = factory.inflate(R.layout.empty_history, null);
        addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT,
                LayoutParams.FILL_PARENT));
        list.setEmptyView(v);
        // Do not post the runnable if there is nothing in the list.
        if (list.getExpandableListAdapter().getGroupCount() > 0) {
            list.post(new Runnable() {
                public void run() {
                    // In case the history gets cleared before this event
                    // happens.
                    if (list.getExpandableListAdapter().getGroupCount() > 0) {
                        list.expandGroup(0);
                    }
                }
            });
        }
        mMaxTabsOpen = getIntent().getBooleanExtra("maxTabsOpen", false);
        CombinedBookmarkHistoryActivity.getIconListenerSet(getContentResolver())
                .addListener(mIconReceiver);
        
        // initialize the result to canceled, so that if the user just presses
        // back then it will have the correct result
        setResultToParent(RESULT_CANCELED, null);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.history, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver()));
        return true;
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.clear_history_menu_id:
                // FIXME: Need to clear the tab control in browserActivity 
                // as well
                Browser.clearHistory(getContentResolver());
                mAdapter.refreshData();
                return true;
                
            default:
                break;
        }  
        return super.onOptionsItemSelected(item);
    }
    
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v,
            ContextMenuInfo menuInfo) {
        ExpandableListContextMenuInfo i = 
            (ExpandableListContextMenuInfo) menuInfo;
        // Do not allow a context menu to come up from the group views.
        if (!(i.targetView instanceof HistoryItem)) {
            return;
        }

        // Inflate the menu
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.historycontext, menu);

        // Setup the header
        menu.setHeaderTitle(((HistoryItem)i.targetView).getUrl());

        // Only show open in new tab if we have not maxed out available tabs
        menu.findItem(R.id.new_window_context_menu_id).setVisible(!mMaxTabsOpen);
        
        // decide whether to show the share link option
        PackageManager pm = getPackageManager();
        Intent send = new Intent(Intent.ACTION_SEND);
        send.setType("text/plain");
        ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
        menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null);
        
        super.onCreateContextMenu(menu, v, menuInfo);
    }
    
    @Override
    public boolean onContextItemSelected(MenuItem item) {
        ExpandableListContextMenuInfo i = 
            (ExpandableListContextMenuInfo) item.getMenuInfo();
        String url = ((HistoryItem)i.targetView).getUrl();
        String title = ((HistoryItem)i.targetView).getName();
        switch (item.getItemId()) {
            case R.id.open_context_menu_id:
                loadUrl(url, false);
                return true;
            case R.id.new_window_context_menu_id:
                loadUrl(url, true);
                return true;
            case R.id.save_to_bookmarks_menu_id:
                Browser.saveBookmark(this, title, url);
                return true;
            case R.id.share_link_context_menu_id:
                Browser.sendString(this, url);
                return true;
            case R.id.copy_context_menu_id:
                copy(url);
                return true;
            case R.id.delete_context_menu_id:
                Browser.deleteFromHistory(getContentResolver(), url);
                mAdapter.refreshData();
                return true;
            default:
                break;
        }
        return super.onContextItemSelected(item);
    }
    
    @Override
    public boolean onChildClick(ExpandableListView parent, View v,
            int groupPosition, int childPosition, long id) {
        if (v instanceof HistoryItem) {
            loadUrl(((HistoryItem) v).getUrl(), false);
            return true;
        }
        return false;
    }

    // This Activity is generally a sub-Activity of CombinedHistoryActivity. In
    // that situation, we need to pass our result code up to our parent.
    // However, if someone calls this Activity directly, then this has no
    // parent, and it needs to set it on itself.
    private void setResultToParent(int resultCode, Intent data) {
        Activity a = getParent() == null ? this : getParent();
        a.setResult(resultCode, data);
    }

    private class ChangeObserver extends ContentObserver {
        public ChangeObserver() {
            super(new Handler());
        }

        @Override
        public boolean deliverSelfNotifications() {
            return true;
        }

        @Override
        public void onChange(boolean selfChange) {
            mAdapter.refreshData();
        }
    }
    
    private class HistoryAdapter implements ExpandableListAdapter {
        
        // Array for each of our bins.  Each entry represents how many items are
        // in that bin.
        int mItemMap[];
        // This is our GroupCount.  We will have at most DateSorter.DAY_COUNT
        // bins, less if the user has no items in one or more bins.
        int mNumberOfBins;
        Vector<DataSetObserver> mObservers;
        Cursor mCursor;
        
        HistoryAdapter() {
            mObservers = new Vector<DataSetObserver>();
            
            String whereClause = Browser.BookmarkColumns.VISITS + " > 0 ";
            String orderBy = Browser.BookmarkColumns.DATE + " DESC";
           
            mCursor = managedQuery(
                    Browser.BOOKMARKS_URI,
                    Browser.HISTORY_PROJECTION,
                    whereClause, null, orderBy);
            
            buildMap();
            mCursor.registerContentObserver(new ChangeObserver());
        }
        
        void refreshData() {
            mCursor.requery();
            buildMap();
            for (DataSetObserver o : mObservers) {
                o.onChanged();
            }
        }
        
        private void buildMap() {
            // The cursor is sorted by date
            // The ItemMap will store the number of items in each bin.
            int array[] = new int[DateSorter.DAY_COUNT];
            // Zero out the array.
            for (int j = 0; j < DateSorter.DAY_COUNT; j++) {
                array[j] = 0;
            }
            mNumberOfBins = 0;
            int dateIndex = -1;
            if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
                while (!mCursor.isAfterLast()) {
                    long date = mCursor.getLong(Browser.HISTORY_PROJECTION_DATE_INDEX);
                    int index = mDateSorter.getIndex(date);
                    if (index > dateIndex) {
                        mNumberOfBins++;
                        if (index == DateSorter.DAY_COUNT - 1) {
                            // We are already in the last bin, so it will
                            // include all the remaining items
                            array[index] = mCursor.getCount()
                                    - mCursor.getPosition();
                            break;
                        }
                        dateIndex = index;
                    }
                    array[dateIndex]++;
                    mCursor.moveToNext();
                }
            }
            mItemMap = array;
        }

        // This translates from a group position in the Adapter to a position in
        // our array.  This is necessary because some positions in the array
        // have no history items, so we simply do not present those positions
        // to the Adapter.
        private int groupPositionToArrayPosition(int groupPosition) {
            if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
                throw new AssertionError("group position out of range");
            }
            if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) {
                // In the first case, we have exactly the same number of bins
                // as our maximum possible, so there is no need to do a
                // conversion
                // The second statement is in case this method gets called when
                // the array is empty, in which case the provided groupPosition
                // will do fine.
                return groupPosition;
            }
            int arrayPosition = -1;
            while (groupPosition > -1) {
                arrayPosition++;
                if (mItemMap[arrayPosition] != 0) {
                    groupPosition--;
                }
            }
            return arrayPosition;
        }

        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
                View convertView, ViewGroup parent) {
            groupPosition = groupPositionToArrayPosition(groupPosition);
            HistoryItem item;
            if (null == convertView || !(convertView instanceof HistoryItem)) {
                item = new HistoryItem(BrowserHistoryPage.this);
                // Add padding on the left so it will be indented from the
                // arrows on the group views.
                item.setPadding(item.getPaddingLeft() + 10,
                        item.getPaddingTop(),
                        item.getPaddingRight(),
                        item.getPaddingBottom());
            } else {
                item = (HistoryItem) convertView;
            }
            int index = childPosition;
            for (int i = 0; i < groupPosition; i++) {
                index += mItemMap[i];
            }
            mCursor.moveToPosition(index);
            item.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
            String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
            item.setUrl(url);
            item.setFavicon(CombinedBookmarkHistoryActivity.getIconListenerSet(
                    getContentResolver()).getFavicon(url));
            item.setIsBookmark(1 ==
                    mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX));
            return item;
        }
        
        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
            groupPosition = groupPositionToArrayPosition(groupPosition);
            TextView item;
            if (null == convertView || !(convertView instanceof TextView)) {
                LayoutInflater factory = 
                        LayoutInflater.from(BrowserHistoryPage.this);
                item = (TextView) 
                        factory.inflate(R.layout.history_header, null);
            } else {
                item = (TextView) convertView;
            }
            item.setText(mDateSorter.getLabel(groupPosition));
            return item;
        }

        public boolean areAllItemsEnabled() {
            return true;
        }

        public boolean isChildSelectable(int groupPosition, int childPosition) {
            return true;
        }

        public int getGroupCount() {
            return mNumberOfBins;
        }

        public int getChildrenCount(int groupPosition) {
            return mItemMap[groupPositionToArrayPosition(groupPosition)];
        }

        public Object getGroup(int groupPosition) {
            return null;
        }

        public Object getChild(int groupPosition, int childPosition) {
            return null;
        }

        public long getGroupId(int groupPosition) {
            return groupPosition;
        }

        public long getChildId(int groupPosition, int childPosition) {
            return (childPosition << 3) + groupPosition;
        }

        public boolean hasStableIds() {
            return true;
        }

        public void registerDataSetObserver(DataSetObserver observer) {
            mObservers.add(observer);
        }

        public void unregisterDataSetObserver(DataSetObserver observer) {
            mObservers.remove(observer);
        }

        public void onGroupExpanded(int groupPosition) {
        
        }

        public void onGroupCollapsed(int groupPosition) {
        
        }

        public long getCombinedChildId(long groupId, long childId) {
            return childId;
        }

        public long getCombinedGroupId(long groupId) {
            return groupId;
        }

        public boolean isEmpty() {
            return mCursor.getCount() == 0;
        }
    }
}