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 | |
5 | package org.chromium.chrome.browser; |
6 | |
7 | import android.content.Context; |
8 | import android.graphics.Bitmap; |
9 | import android.graphics.Color; |
10 | import android.graphics.drawable.BitmapDrawable; |
11 | import android.graphics.drawable.ColorDrawable; |
12 | import android.graphics.drawable.Drawable; |
13 | import android.text.TextUtils; |
14 | import android.view.Gravity; |
15 | import android.view.View; |
16 | import android.view.ViewGroup; |
17 | import android.widget.AdapterView; |
18 | import android.widget.BaseAdapter; |
19 | import android.widget.HeaderViewListAdapter; |
20 | import android.widget.ListPopupWindow; |
21 | import android.widget.ListView; |
22 | import android.widget.ListView.FixedViewInfo; |
23 | import android.widget.PopupWindow; |
24 | import android.widget.TextView; |
25 | |
26 | import org.chromium.base.CalledByNative; |
27 | import org.chromium.base.ThreadUtils; |
28 | import org.chromium.ui.LocalizationUtils; |
29 | import org.chromium.chrome.R; |
30 | import org.chromium.content.browser.ContentViewCore; |
31 | import org.chromium.content.browser.NavigationClient; |
32 | import org.chromium.content.browser.NavigationEntry; |
33 | import org.chromium.content.browser.NavigationHistory; |
34 | |
35 | import java.util.ArrayList; |
36 | import java.util.HashSet; |
37 | import java.util.Set; |
38 | |
39 | /** |
40 | * A popup that handles displaying the navigation history for a given tab. |
41 | */ |
42 | public 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 | } |