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