1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.chrome.browser;
6
7import android.content.Context;
8import android.graphics.Bitmap;
9import android.graphics.Color;
10import android.graphics.drawable.BitmapDrawable;
11import android.graphics.drawable.ColorDrawable;
12import android.graphics.drawable.Drawable;
13import android.text.TextUtils;
14import android.view.Gravity;
15import android.view.View;
16import android.view.ViewGroup;
17import android.widget.AdapterView;
18import android.widget.BaseAdapter;
19import android.widget.HeaderViewListAdapter;
20import android.widget.ListPopupWindow;
21import android.widget.ListView;
22import android.widget.ListView.FixedViewInfo;
23import android.widget.PopupWindow;
24import android.widget.TextView;
25
26import org.chromium.base.CalledByNative;
27import org.chromium.base.ThreadUtils;
28import org.chromium.ui.LocalizationUtils;
29import org.chromium.chrome.R;
30import org.chromium.content.browser.ContentViewCore;
31import org.chromium.content.browser.NavigationClient;
32import org.chromium.content.browser.NavigationEntry;
33import org.chromium.content.browser.NavigationHistory;
34
35import java.util.ArrayList;
36import java.util.HashSet;
37import java.util.Set;
38
39/**
40 * A popup that handles displaying the navigation history for a given tab.
41 */
42public class NavigationPopup extends ListPopupWindow implements AdapterView.OnItemClickListener {
43
44    private static final int FAVICON_SIZE_DP = 16;
45
46    private static final int MAXIMUM_HISTORY_ITEMS = 8;
47
48    private final Context mContext;
49    private final NavigationClient mNavigationClient;
50    private final NavigationHistory mHistory;
51    private final NavigationAdapter mAdapter;
52    private final ListItemFactory mListItemFactory;
53
54    private final int mFaviconSize;
55
56    private int mNativeNavigationPopup;
57
58    /**
59     * Constructs a new popup with the given history information.
60     *
61     * @param context The context used for building the popup.
62     * @param navigationClient The owner of the history being displayed.
63     * @param isForward Whether to request forward navigation entries.
64     */
65    public NavigationPopup(
66            Context context, NavigationClient navigationClient, boolean isForward) {
67        super(context, null, android.R.attr.popupMenuStyle);
68        mContext = context;
69        mNavigationClient = navigationClient;
70        mHistory = mNavigationClient.getDirectedNavigationHistory(
71                isForward, MAXIMUM_HISTORY_ITEMS);
72        mAdapter = new NavigationAdapter();
73
74        float density = mContext.getResources().getDisplayMetrics().density;
75        mFaviconSize = (int) (density * FAVICON_SIZE_DP);
76
77        setModal(true);
78        setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
79        setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
80        setOnItemClickListener(this);
81
82        setAdapter(new HeaderViewListAdapter(null, null, mAdapter));
83
84        mListItemFactory = new ListItemFactory(context);
85    }
86
87    /**
88     * @return Whether a navigation popup is valid for the given page.
89     */
90    public boolean shouldBeShown() {
91        return mHistory.getEntryCount() > 0;
92    }
93
94    @Override
95    public void show() {
96        if (mNativeNavigationPopup == 0) initializeNative();
97        super.show();
98    }
99
100    @Override
101    public void dismiss() {
102        if (mNativeNavigationPopup != 0) {
103            nativeDestroy(mNativeNavigationPopup);
104            mNativeNavigationPopup = 0;
105        }
106        super.dismiss();
107    }
108
109    private void initializeNative() {
110        ThreadUtils.assertOnUiThread();
111        mNativeNavigationPopup = nativeInit();
112
113        Set<String> requestedUrls = new HashSet<String>();
114        for (int i = 0; i < mHistory.getEntryCount(); i++) {
115            NavigationEntry entry = mHistory.getEntryAtIndex(i);
116            if (entry.getFavicon() != null) continue;
117            String url = entry.getUrl();
118            if (!requestedUrls.contains(url)) {
119                nativeFetchFaviconForUrl(mNativeNavigationPopup, url);
120                requestedUrls.add(url);
121            }
122        }
123        nativeFetchFaviconForUrl(mNativeNavigationPopup, nativeGetHistoryUrl());
124    }
125
126    @CalledByNative
127    private void onFaviconUpdated(String url, Object favicon) {
128        for (int i = 0; i < mHistory.getEntryCount(); i++) {
129            NavigationEntry entry = mHistory.getEntryAtIndex(i);
130            if (TextUtils.equals(url, entry.getUrl())) entry.updateFavicon((Bitmap) favicon);
131        }
132        mAdapter.notifyDataSetChanged();
133    }
134
135    @Override
136    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
137        NavigationEntry entry = (NavigationEntry) parent.getItemAtPosition(position);
138        mNavigationClient.goToNavigationIndex(entry.getIndex());
139        dismiss();
140    }
141
142    private void updateBitmapForTextView(TextView view, Bitmap bitmap) {
143        Drawable faviconDrawable = null;
144        if (bitmap != null) {
145            faviconDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
146            ((BitmapDrawable) faviconDrawable).setGravity(Gravity.FILL);
147        } else {
148            faviconDrawable = new ColorDrawable(Color.TRANSPARENT);
149        }
150        faviconDrawable.setBounds(0, 0, mFaviconSize, mFaviconSize);
151        view.setCompoundDrawables(faviconDrawable, null, null, null);
152    }
153
154    private static class ListItemFactory {
155        private static final int LIST_ITEM_HEIGHT_DP = 48;
156        private static final int PADDING_DP = 8;
157        private static final int TEXT_SIZE_SP = 18;
158        private static final float FADE_LENGTH_DP = 25.0f;
159        private static final float FADE_STOP = 0.75f;
160
161        int mFadeEdgeLength;
162        int mFadePadding;
163        int mListItemHeight;
164        int mPadding;
165        boolean mIsLayoutDirectionRTL;
166        Context mContext;
167
168        public ListItemFactory(Context context) {
169            mContext = context;
170            computeFadeDimensions();
171        }
172
173        private void computeFadeDimensions() {
174            // Fade with linear gradient starting 25dp from right margin.
175            // Reaches 0% opacity at 75% length. (Simulated with extra padding)
176            float density = mContext.getResources().getDisplayMetrics().density;
177            float fadeLength = (FADE_LENGTH_DP * density);
178            mFadeEdgeLength = (int)(fadeLength * FADE_STOP);
179            mFadePadding = (int)(fadeLength * (1 - FADE_STOP));
180            mListItemHeight = (int) (density * LIST_ITEM_HEIGHT_DP);
181            mPadding = (int) (density * PADDING_DP);
182            mIsLayoutDirectionRTL = LocalizationUtils.isSystemLayoutDirectionRtl();
183        }
184
185        public TextView createListItem() {
186            TextView view = new TextView(mContext);
187            view.setFadingEdgeLength(mFadeEdgeLength);
188            view.setHorizontalFadingEdgeEnabled(true);
189            view.setSingleLine();
190            view.setTextSize(TEXT_SIZE_SP);
191            view.setMinimumHeight(mListItemHeight);
192            view.setGravity(Gravity.CENTER_VERTICAL);
193            view.setCompoundDrawablePadding(mPadding);
194            if (!mIsLayoutDirectionRTL) {
195                view.setPadding(mPadding, 0, mPadding + mFadePadding , 0);
196            }
197            else {
198                view.setPadding(mPadding + mFadePadding, 0, mPadding, 0);
199            }
200            return view;
201        }
202    }
203
204    private class NavigationAdapter extends BaseAdapter {
205        @Override
206        public int getCount() {
207            return mHistory.getEntryCount();
208        }
209
210        @Override
211        public Object getItem(int position) {
212            return mHistory.getEntryAtIndex(position);
213        }
214
215        @Override
216        public long getItemId(int position) {
217            return ((NavigationEntry) getItem(position)).getIndex();
218        }
219
220        @Override
221        public View getView(int position, View convertView, ViewGroup parent) {
222            TextView view;
223            if (convertView != null && convertView instanceof TextView) {
224                view = (TextView) convertView;
225            } else {
226                view = mListItemFactory.createListItem();
227            }
228            NavigationEntry entry = (NavigationEntry) getItem(position);
229
230            String entryText = entry.getTitle();
231            if (TextUtils.isEmpty(entryText)) entryText = entry.getVirtualUrl();
232            if (TextUtils.isEmpty(entryText)) entryText = entry.getUrl();
233            view.setText(entryText);
234            updateBitmapForTextView(view, entry.getFavicon());
235
236            return view;
237        }
238    }
239
240    private static native String nativeGetHistoryUrl();
241
242    private native int nativeInit();
243    private native void nativeDestroy(int nativeNavigationPopup);
244    private native void nativeFetchFaviconForUrl(int nativeNavigationPopup, String url);
245}
246