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
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.PopupWindow;
22import android.widget.TextView;
23
24import org.chromium.base.CalledByNative;
25import org.chromium.base.ThreadUtils;
26import org.chromium.content.browser.NavigationClient;
27import org.chromium.content_public.browser.NavigationEntry;
28import org.chromium.content_public.browser.NavigationHistory;
29import org.chromium.ui.base.LocalizationUtils;
30
31import java.util.HashSet;
32import java.util.Set;
33
34/**
35 * A popup that handles displaying the navigation history for a given tab.
36 */
37public 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