diff --git a/build/mobile/robocop/Actions.java.in b/build/mobile/robocop/Actions.java.in index 6ac6bf81bcc6..e96420755483 100644 --- a/build/mobile/robocop/Actions.java.in +++ b/build/mobile/robocop/Actions.java.in @@ -82,6 +82,13 @@ public interface Actions { void drag(int startingX, int endingX, int startingY, int endingY); + /** + * This is the implementation of clickLongOnScreen from Robotium 4.0 since this sometimes fails for Robotium 3.6 + * TODO : Remove this when Robotium is updated + */ + + void clickLongOnScreen(float x, float y); + /** * Run a sql query on the specified database */ diff --git a/build/mobile/robocop/FennecNativeActions.java.in b/build/mobile/robocop/FennecNativeActions.java.in index 3eff3c8b7ce5..9813fd8746c5 100644 --- a/build/mobile/robocop/FennecNativeActions.java.in +++ b/build/mobile/robocop/FennecNativeActions.java.in @@ -22,7 +22,9 @@ import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import com.jayway.android.robotium.solo.Solo; @@ -458,6 +460,41 @@ public class FennecNativeActions implements Actions { mSolo.drag(startingX, endingX, startingY, endingY, 10); } + /** + * This is the implementation of clickLongOnScreen from Robotium 4.0 since this sometimes fails for Robotium 3.6 + * TODO : Remove this when Robotium is updated + */ + + public void clickLongOnScreen(float x, float y) { + boolean successfull = false; + int retry = 0; + long downTime = SystemClock.uptimeMillis(); + long eventTime = SystemClock.uptimeMillis(); + MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0); + + while(!successfull && retry < 10) { + try{ + mInstr.sendPointerSync(event); + successfull = true; + }catch(SecurityException e){ + FennecNativeDriver.log(LogLevel.ERROR, e); + retry++; + } + } + + mAsserter.ok(successfull, "Trying to click on long on screen at (" + x + "," + y + ")", "Was able to click long on screen"); + + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x + 1.0f, y + 1.0f, 0); + mInstr.sendPointerSync(event); + mSolo.sleep(((int)(ViewConfiguration.getLongPressTimeout() * 2.5f))); + + eventTime = SystemClock.uptimeMillis(); + event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); + mInstr.sendPointerSync(event); + mSolo.sleep(500); + } + public Cursor querySql(String dbPath, String sql) { try { return (Cursor)mQuerySql.invoke(mRobocopApi, dbPath, sql); diff --git a/mobile/android/base/ActivityHandlerHelper.java b/mobile/android/base/ActivityHandlerHelper.java index 9d389c47f5c8..53a5fecd34ee 100644 --- a/mobile/android/base/ActivityHandlerHelper.java +++ b/mobile/android/base/ActivityHandlerHelper.java @@ -36,7 +36,6 @@ public class ActivityHandlerHelper implements GeckoEventListener { private final ActivityResultHandlerMap mActivityResultHandlerMap; private final FilePickerResultHandlerSync mFilePickerResultHandlerSync; - private final AwesomebarResultHandler mAwesomebarResultHandler; private final CameraImageResultHandler mCameraImageResultHandler; private final CameraVideoResultHandler mCameraVideoResultHandler; @@ -58,7 +57,6 @@ public class ActivityHandlerHelper implements GeckoEventListener { }; mActivityResultHandlerMap = new ActivityResultHandlerMap(); mFilePickerResultHandlerSync = new FilePickerResultHandlerSync(mFilePickerResult); - mAwesomebarResultHandler = new AwesomebarResultHandler(); mCameraImageResultHandler = new CameraImageResultHandler(mFilePickerResult); mCameraVideoResultHandler = new CameraVideoResultHandler(mFilePickerResult); GeckoAppShell.getEventDispatcher().registerEventListener("FilePicker:Show", this); @@ -91,10 +89,6 @@ public class ActivityHandlerHelper implements GeckoEventListener { } } - public int makeRequestCodeForAwesomebar() { - return mActivityResultHandlerMap.put(mAwesomebarResultHandler); - } - public int makeRequestCode(ActivityResultHandler aHandler) { return mActivityResultHandlerMap.put(aHandler); } diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in index e6d58df3740e..07b1754fc727 100644 --- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -230,11 +230,6 @@ - - KeyEvent.getMaxKeyCode()) - return true; - - // This method is called only if the key event was not handled - // by any of the views, which usually means the edit box lost focus - if (keyCode == KeyEvent.KEYCODE_BACK || - keyCode == KeyEvent.KEYCODE_MENU || - keyCode == KeyEvent.KEYCODE_DPAD_UP || - keyCode == KeyEvent.KEYCODE_DPAD_DOWN || - keyCode == KeyEvent.KEYCODE_DPAD_LEFT || - keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || - keyCode == KeyEvent.KEYCODE_DPAD_CENTER || - keyCode == KeyEvent.KEYCODE_DEL || - keyCode == KeyEvent.KEYCODE_VOLUME_UP || - keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || - GamepadUtils.isActionKey(event)) { - return super.onKeyDown(keyCode, event); - } else if (keyCode == KeyEvent.KEYCODE_SEARCH) { - mText.setText(""); - mText.requestFocus(); - return true; - } else { - int prevSelStart = mText.getSelectionStart(); - int prevSelEnd = mText.getSelectionEnd(); - - // Manually dispatch the key event to the AwesomeBar. If selection changed as - // a result of the key event, then give focus back to mText - mText.dispatchKeyEvent(event); - - int curSelStart = mText.getSelectionStart(); - int curSelEnd = mText.getSelectionEnd(); - if (prevSelStart != curSelStart || prevSelEnd != curSelEnd) { - mText.requestFocusFromTouch(); - // Restore the selection, which gets lost due to the focus switch - mText.setSelection(curSelStart, curSelEnd); - } - return true; - } - } - - @Override - public void onResume() { - super.onResume(); - if (mText != null && mText.getText() != null) { - updateGoButton(mText.getText().toString()); - if (mDelayRestartInput) { - // call updateGoButton again to force a restartInput call - updateGoButton(mText.getText().toString()); - } - } - - // Invlidate the cached value that keeps track of whether or - // not desktop bookmarks exist - BrowserDB.invalidateCachedState(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - mAwesomeTabs.destroy(); - } - - @Override - public void onBackPressed() { - // Let mAwesomeTabs try to handle the back press, since we may be in a - // bookmarks sub-folder. - if (mAwesomeTabs.onBackPressed()) - return; - - // Otherwise, just exit the awesome screen - cancelAndFinish(); - } - - static public class ContextMenuSubject { - public int id; - public String url; - public byte[] favicon; - public String title; - public String keyword; - public int display; - - public ContextMenuSubject(int id, String url, byte[] favicon, String title, String keyword) { - this(id, url, favicon, title, keyword, Combined.DISPLAY_NORMAL); - } - - public ContextMenuSubject(int id, String url, byte[] favicon, String title, String keyword, int display) { - this.id = id; - this.url = url; - this.favicon = favicon; - this.title = title; - this.keyword = keyword; - this.display = display; - } - }; - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, view, menuInfo); - AwesomeBarTab tab = mAwesomeTabs.getAwesomeBarTabForView(view); - mContextMenuSubject = tab.getSubject(menu, view, menuInfo); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - if (mContextMenuSubject == null) - return false; - - final int id = mContextMenuSubject.id; - final String url = mContextMenuSubject.url; - final byte[] b = mContextMenuSubject.favicon; - final String title = mContextMenuSubject.title; - final String keyword = mContextMenuSubject.keyword; - final int display = mContextMenuSubject.display; - - final int itemId = item.getItemId(); - if (itemId == R.id.open_private_tab || itemId == R.id.open_new_tab) { - if (url == null) { - Log.e(LOGTAG, "Can't open in new tab because URL is null"); - } - - String newTabUrl = url; - if (display == Combined.DISPLAY_READER) - newTabUrl = ReaderModeUtils.getAboutReaderForUrl(url, true); - - int flags = Tabs.LOADURL_NEW_TAB; - if (item.getItemId() == R.id.open_private_tab) - flags |= Tabs.LOADURL_PRIVATE; - - Tabs.getInstance().loadUrl(newTabUrl, flags); - Toast.makeText(this, R.string.new_tab_opened, Toast.LENGTH_SHORT).show(); - - return true; - } - - if (itemId == R.id.open_in_reader) { - if (url == null) { - Log.e(LOGTAG, "Can't open in reader mode because URL is null"); - } else { - openUrlAndFinish(ReaderModeUtils.getAboutReaderForUrl(url, true)); - } - return true; - } - - if (itemId == R.id.edit_bookmark) { - new EditBookmarkDialog(this).show(id, title, url, keyword); - return true; - } - - if (itemId == R.id.remove_bookmark) { - (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { - private boolean mInReadingList; - - @Override - public void onPreExecute() { - mInReadingList = mAwesomeTabs.isInReadingList(); - } - - @Override - public Integer doInBackground(Void... params) { - BrowserDB.removeBookmark(getContentResolver(), id); - Integer count = mInReadingList ? - BrowserDB.getReadingListCount(getContentResolver()) : 0; - - return count; - } - - @Override - public void onPostExecute(Integer aCount) { - int messageId = R.string.bookmark_removed; - if (mInReadingList) { - messageId = R.string.reading_list_removed; - - GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", url); - GeckoAppShell.sendEventToGecko(e); - - // Delete from Awesomebar context menu can alter reading list bookmark count - e = GeckoEvent.createBroadcastEvent("Reader:ListCountUpdated", Integer.toString(aCount)); - GeckoAppShell.sendEventToGecko(e); - } - - Toast.makeText(AwesomeBar.this, messageId, Toast.LENGTH_SHORT).show(); - } - }).execute(); - - return true; - } - - if (itemId == R.id.remove_history) { - (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { - @Override - public Void doInBackground(Void... params) { - BrowserDB.removeHistoryEntry(getContentResolver(), id); - return null; - } - - @Override - public void onPostExecute(Void result) { - Toast.makeText(AwesomeBar.this, R.string.history_removed, Toast.LENGTH_SHORT).show(); - } - }).execute(); - - return true; - } - - if (itemId == R.id.add_to_launcher) { - if (url == null) { - Log.e(LOGTAG, "Can't add to home screen because URL is null"); - } else { - Bitmap bitmap = null; - if (b != null) { - bitmap = BitmapUtils.decodeByteArray(b); - } - - String shortcutTitle = TextUtils.isEmpty(title) ? url.replaceAll("^([a-z]+://)?(www\\.)?", "") : title; - GeckoAppShell.createShortcut(shortcutTitle, url, bitmap, ""); - } - - return true; - } - - if (itemId == R.id.share) { - if (url == null) { - Log.e(LOGTAG, "Can't share because URL is null"); - } else { - GeckoAppShell.openUriExternal(url, "text/plain", "", "", - Intent.ACTION_SEND, title); - } - - return true; - } - - return super.onContextItemSelected(item); - } - - public static String getReaderForUrl(String url) { - // FIXME: still need to define the final way to open items from - // reading list. For now, we're using an about:reader page. - return "about:reader?url=" + Uri.encode(url) + "&readingList=1"; - } - - private static boolean hasCompositionString(Editable content) { - Object[] spans = content.getSpans(0, content.length(), Object.class); - if (spans != null) { - for (Object span : spans) { - if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { - // Found composition string. - return true; - } - } - } - return false; - } - - // return early if we're backspacing through the string, or have no autocomplete results - public void onAutocomplete(final String result) { - final String text = mText.getText().toString(); - - if (result == null) { - mAutoCompleteResult = ""; - return; - } - - if (!result.startsWith(text) || text.equals(result)) { - return; - } - - mAutoCompleteResult = result; - mText.getText().append(result.substring(text.length())); - mText.setSelection(text.length(), result.length()); - } - - @Override - public void afterTextChanged(final Editable s) { - final String text = s.toString(); - boolean useHandler = false; - boolean reuseAutocomplete = false; - if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) { - useHandler = true; - - // If you're hitting backspace (the string is getting smaller - // or is unchanged), don't autocomplete. - if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) { - useHandler = false; - } else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) { - // If this text already matches our autocomplete text, autocomplete likely - // won't change. Just reuse the old autocomplete value. - useHandler = false; - reuseAutocomplete = true; - } - } - - // If this is the autocomplete text being set, don't run the filter. - if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) { - mAwesomeTabs.filter(text, useHandler ? this : null); - mAutoCompletePrefix = text; - - if (reuseAutocomplete) { - onAutocomplete(mAutoCompleteResult); - } - } - - // If the AwesomeBar has a composition string, don't call updateGoButton(). - // That method resets IME and composition state will be broken. - if (!hasCompositionString(s) || - InputMethods.isGestureKeyboard(mText.getContext())) { - updateGoButton(text); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - // do nothing - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, - int count) { - // do nothing - } -} diff --git a/mobile/android/base/AwesomeBarTabs.java b/mobile/android/base/AwesomeBarTabs.java deleted file mode 100644 index 2782c572890a..000000000000 --- a/mobile/android/base/AwesomeBarTabs.java +++ /dev/null @@ -1,381 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko; - -import android.content.Context; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.StateListDrawable; -import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TabHost; -import android.widget.TabWidget; - -public class AwesomeBarTabs extends TabHost - implements LightweightTheme.OnChangeListener { - private static final String LOGTAG = "GeckoAwesomeBarTabs"; - - private Context mContext; - private GeckoActivity mActivity; - - private boolean mInflated; - private LayoutInflater mInflater; - private OnUrlOpenListener mUrlOpenListener; - private View.OnTouchListener mListTouchListener; - private boolean mSearching = false; - private String mTarget; - private ViewPager mViewPager; - private AwesomePagerAdapter mPagerAdapter; - - private AwesomeBarTab mTabs[]; - - public interface OnUrlOpenListener { - public void onUrlOpen(String url, String title); - public void onSearch(SearchEngine engine, String text); - public void onEditSuggestion(String suggestion); - public void onSwitchToTab(final int tabId); - } - - private class AwesomePagerAdapter extends PagerAdapter { - public AwesomePagerAdapter() { - super(); - } - - @Override - public Object instantiateItem(ViewGroup group, int index) { - AwesomeBarTab tab = mTabs[index]; - group.addView(tab.getView()); - return tab; - } - - @Override - public void destroyItem(ViewGroup group, int index, Object obj) { - AwesomeBarTab tab = (AwesomeBarTab)obj; - group.removeView(tab.getView()); - } - - @Override - public int getCount() { - if (mSearching) - return 1; - return mTabs.length; - } - - @Override - public boolean isViewFromObject(View view, Object object) { - return getAwesomeBarTabForView(view) == object; - } - } - - private AwesomeBarTab getCurrentAwesomeBarTab() { - int index = mViewPager.getCurrentItem(); - return mTabs[index]; - } - - public AwesomeBarTab getAwesomeBarTabForView(View view) { - String tag = (String)view.getTag(); - return getAwesomeBarTabForTag(tag); - } - - public AwesomeBarTab getAwesomeBarTabForTag(String tag) { - for (AwesomeBarTab tab : mTabs) { - if (tag.equals(tab.getTag())) { - return tab; - } - } - return null; - } - - public boolean onBackPressed() { - AwesomeBarTab tab = getCurrentAwesomeBarTab(); - if (tab == null) - return false; - return tab.onBackPressed(); - } - - public AwesomeBarTabs(Context context, AttributeSet attrs) { - super(context, attrs); - - Log.d(LOGTAG, "Creating AwesomeBarTabs"); - - mContext = context; - mActivity = (GeckoActivity) context; - - mInflated = false; - mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - // HACK: Without this, the onFinishInflate is called twice - // This issue is due to a bug when Android inflates a layout with a - // parent. Fixed in Honeycomb - if (mInflated) - return; - - mInflated = true; - - // This should be called before adding any tabs - // to the TabHost. - setup(); - - mListTouchListener = new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - // take focus away from awesome bar to hide the keyboard - requestFocus(); - } - return false; - } - }; - - mTabs = new AwesomeBarTab[] { - new AllPagesTab(mContext), - new BookmarksTab(mContext), - new HistoryTab(mContext) - }; - - final TabWidget tabWidget = (TabWidget) findViewById(android.R.id.tabs); - // hide the strip since we aren't using the TabHost... - tabWidget.setStripEnabled(false); - - mViewPager = (ViewPager) findViewById(R.id.tabviewpager); - mPagerAdapter = new AwesomePagerAdapter(); - mViewPager.setAdapter(mPagerAdapter); - - mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { - @Override - public void onPageScrollStateChanged(int state) { } - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } - @Override - public void onPageSelected(int position) { - tabWidget.setCurrentTab(position); - styleSelectedTab(); - // take focus away from awesome bar to hide the keyboard - requestFocus(); - } - }); - - for (int i = 0; i < mTabs.length; i++) { - mTabs[i].setListTouchListener(mListTouchListener); - addAwesomeTab(mTabs[i].getTag(), - mTabs[i].getTitleStringId(), - i); - } - - // Initialize "All Pages" list with no filter - filter("", null); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - mActivity.getLightweightTheme().addListener(this); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - mActivity.getLightweightTheme().removeListener(this); - } - - @Override - public void onLightweightThemeChanged() { - styleSelectedTab(); - } - - @Override - public void onLightweightThemeReset() { - styleSelectedTab(); - } - - public void setCurrentItemByTag(String tag) { - mViewPager.setCurrentItem(getTabIdByTag(tag)); - } - - public int getTabIdByTag(String tag) { - for (int i = 0; i < mTabs.length; i++) { - if (tag.equals(mTabs[i].getTag())) { - return i; - } - } - return -1; - } - - private void styleSelectedTab() { - int selIndex = mViewPager.getCurrentItem(); - TabWidget tabWidget = getTabWidget(); - boolean isPrivate = false; - - if (mTarget != null && mTarget.equals(AwesomeBar.Target.CURRENT_TAB.name())) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) - isPrivate = tab.isPrivate(); - } - - for (int i = 0; i < tabWidget.getTabCount(); i++) { - GeckoTextView view = (GeckoTextView) tabWidget.getChildTabViewAt(i); - if (isPrivate) { - view.resetTheme(); - view.setPrivateMode((i == selIndex) ? false : true); - } else { - if (i == selIndex) - view.resetTheme(); - else if (mActivity.getLightweightTheme().isEnabled()) - view.setTheme(mActivity.getLightweightTheme().isLightTheme()); - else - view.resetTheme(); - } - - if (i < (selIndex - 1)) - view.getBackground().setLevel(3); - else if (i == (selIndex - 1)) - view.getBackground().setLevel(1); - else if (i == (selIndex + 1)) - view.getBackground().setLevel(2); - else if (i > (selIndex + 1)) - view.getBackground().setLevel(4); - } - - if (selIndex == 0) - findViewById(R.id.tab_widget_left).getBackground().setLevel(1); - else - findViewById(R.id.tab_widget_left).getBackground().setLevel(0); - - if (selIndex == (tabWidget.getTabCount() - 1)) - findViewById(R.id.tab_widget_right).getBackground().setLevel(2); - else - findViewById(R.id.tab_widget_right).getBackground().setLevel(0); - } - - - private View addAwesomeTab(String id, int titleId, final int contentId) { - GeckoTextView indicatorView = (GeckoTextView) mInflater.inflate(R.layout.awesomebar_tab_indicator, null); - indicatorView.setText(titleId); - - getTabWidget().addView(indicatorView); - - // this MUST be done after tw.addView to overwrite the listener added by tabWidget - // which delegates to TabHost (which we don't have) - indicatorView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mViewPager.setCurrentItem(contentId, true); - } - }); - - return indicatorView; - } - - public void setOnUrlOpenListener(OnUrlOpenListener listener) { - mUrlOpenListener = listener; - for (AwesomeBarTab tab : mTabs) { - tab.setUrlListener(listener); - } - } - - public void destroy() { - for (AwesomeBarTab tab : mTabs) { - tab.destroy(); - } - } - - public AllPagesTab getAllPagesTab() { - return (AllPagesTab)getAwesomeBarTabForTag("allPages"); - } - - public BookmarksTab getBookmarksTab() { - return (BookmarksTab)getAwesomeBarTabForTag("bookmarks"); - } - - public HistoryTab getHistoryTab() { - return (HistoryTab)getAwesomeBarTabForTag("history"); - } - - public void filter(String searchTerm, AutocompleteHandler handler) { - - // If searching, disable left / right tab swipes - mSearching = searchTerm.length() != 0; - - // reset the pager adapter to force repopulating the cache - mViewPager.setAdapter(mPagerAdapter); - - // Ensure the 'All Pages' tab is selected - AllPagesTab allPages = getAllPagesTab(); - getTabWidget().setCurrentTab(getTabIdByTag(allPages.getTag())); - styleSelectedTab(); - - // Perform the actual search - allPages.filter(searchTerm, handler); - - // If searching, hide the tabs bar - findViewById(R.id.tab_widget_container).setVisibility(mSearching ? View.GONE : View.VISIBLE); - } - - public boolean isInReadingList() { - return getBookmarksTab().isInReadingList(); - } - - public void setTarget(String target) { - mTarget = target; - styleSelectedTab(); - if (mTarget.equals(AwesomeBar.Target.CURRENT_TAB.name())) { - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null && tab.isPrivate()) - ((BackgroundLayout) findViewById(R.id.tab_widget_container)).setPrivateMode(true); - } - } - - public static class BackgroundLayout extends GeckoLinearLayout { - private GeckoActivity mActivity; - - public BackgroundLayout(Context context, AttributeSet attrs) { - super(context, attrs); - mActivity = (GeckoActivity) context; - } - - @Override - public void onLightweightThemeChanged() { - LightweightThemeDrawable drawable = mActivity.getLightweightTheme().getColorDrawable(this); - if (drawable == null) - return; - - drawable.setAlpha(255, 0); - - StateListDrawable stateList = new StateListDrawable(); - stateList.addState(new int[] { R.attr.state_private }, new ColorDrawable(mActivity.getResources().getColor(R.color.background_private))); - stateList.addState(new int[] {}, drawable); - - int[] padding = new int[] { getPaddingLeft(), - getPaddingTop(), - getPaddingRight(), - getPaddingBottom() - }; - setBackgroundDrawable(stateList); - setPadding(padding[0], padding[1], padding[2], padding[3]); - } - - @Override - public void onLightweightThemeReset() { - int[] padding = new int[] { getPaddingLeft(), - getPaddingTop(), - getPaddingRight(), - getPaddingBottom() - }; - setBackgroundResource(R.drawable.address_bar_bg); - setPadding(padding[0], padding[1], padding[2], padding[3]); - } - } -} diff --git a/mobile/android/base/AwesomebarResultHandler.java b/mobile/android/base/AwesomebarResultHandler.java deleted file mode 100644 index c91b594d70b0..000000000000 --- a/mobile/android/base/AwesomebarResultHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko; - -import org.mozilla.gecko.util.ActivityResultHandler; - -import android.content.Intent; -import android.util.Log; - -class AwesomebarResultHandler implements ActivityResultHandler { - private static final String LOGTAG = "GeckoAwesomebarResultHandler"; - - @Override - public void onActivityResult(int resultCode, Intent data) { - if (data != null) { - String tab = data.getStringExtra(AwesomeBar.TAB_KEY); - if (tab != null) { - Tabs.getInstance().selectTab(Integer.parseInt(tab)); - return; - } - - String url = data.getStringExtra(AwesomeBar.URL_KEY); - AwesomeBar.Target target = AwesomeBar.Target.valueOf(data.getStringExtra(AwesomeBar.TARGET_KEY)); - String searchEngine = data.getStringExtra(AwesomeBar.SEARCH_KEY); - if (url != null && url.length() > 0) { - int flags = Tabs.LOADURL_NONE; - if (target == AwesomeBar.Target.NEW_TAB) { - flags |= Tabs.LOADURL_NEW_TAB; - } - if (data.getBooleanExtra(AwesomeBar.USER_ENTERED_KEY, false)) { - flags |= Tabs.LOADURL_USER_ENTERED; - } - Tabs.getInstance().loadUrl(url, searchEngine, -1, flags); - } - } - } -} diff --git a/mobile/android/base/BackButton.java b/mobile/android/base/BackButton.java index 7f7a25eefb96..763d606df913 100644 --- a/mobile/android/base/BackButton.java +++ b/mobile/android/base/BackButton.java @@ -73,7 +73,7 @@ public class BackButton extends ShapedButton { canvas.drawPath(mBorderPath, isPrivateMode() ? mBorderPrivatePaint : mBorderPaint); } - // The drawable is constructed as per @drawable/address_bar_nav_button. + // The drawable is constructed as per @drawable/url_bar_nav_button. @Override public void onLightweightThemeChanged() { Drawable drawable = mActivity.getLightweightTheme().getDrawable(this); @@ -95,6 +95,6 @@ public class BackButton extends ShapedButton { @Override public void onLightweightThemeReset() { - setBackgroundResource(R.drawable.address_bar_nav_button); + setBackgroundResource(R.drawable.url_bar_nav_button); } } diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 0c0ae7514be7..6183540fba0b 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -14,6 +14,9 @@ import org.mozilla.gecko.gfx.ImmutableViewportMetrics; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.gfx.PanZoomController; import org.mozilla.gecko.health.BrowserHealthReporter; +import org.mozilla.gecko.home.BrowserSearch; +import org.mozilla.gecko.home.HomePager; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.util.Clipboard; import org.mozilla.gecko.util.FloatUtils; @@ -21,7 +24,6 @@ import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.UiAsyncTask; -import org.mozilla.gecko.widget.AboutHome; import org.mozilla.gecko.widget.GeckoActionProvider; import org.mozilla.gecko.widget.ButtonToast; @@ -49,6 +51,8 @@ import android.nfc.NfcAdapter; import android.nfc.NfcEvent; import android.os.Build; import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; import android.text.TextUtils; import android.util.Log; import android.view.InputDevice; @@ -70,6 +74,7 @@ import java.io.File; import java.io.InputStream; import java.net.URL; import java.util.EnumSet; +import java.util.List; import java.util.Vector; abstract public class BrowserApp extends GeckoApp @@ -77,8 +82,10 @@ abstract public class BrowserApp extends GeckoApp PropertyAnimator.PropertyAnimationListener, View.OnKeyListener, GeckoLayerClient.OnMetricsChangedListener, - AboutHome.UriLoadListener, - AboutHome.LoadCompleteListener { + BrowserSearch.OnSearchListener, + BrowserSearch.OnEditSuggestionListener, + HomePager.OnNewTabsListener, + OnUrlOpenListener { private static final String LOGTAG = "GeckoBrowserApp"; private static final String PREF_CHROME_DYNAMICTOOLBAR = "browser.chrome.dynamictoolbar"; @@ -96,8 +103,13 @@ abstract public class BrowserApp extends GeckoApp private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding"; private static final String STATE_DYNAMIC_TOOLBAR_ENABLED = "dynamic_toolbar"; + private static final String BROWSER_SEARCH_TAG = "browser_search"; + private BrowserSearch mBrowserSearch; + private View mBrowserSearchContainer; + public static BrowserToolbar mBrowserToolbar; - private AboutHome mAboutHome; + private HomePager mHomePager; + private View mHomePagerContainer; protected Telemetry.Timer mAboutHomeStartupTimer = null; // Set the default session restore value @@ -152,10 +164,6 @@ abstract public class BrowserApp extends GeckoApp private Integer mPrefObserverId; - // Tag for the AboutHome fragment. The fragment is automatically attached - // after restoring from a saved state, so we use this tag to identify it. - private static final String ABOUTHOME_TAG = "abouthome"; - private SharedPreferencesHelper mSharedPreferencesHelper; private OrderedBroadcastHelper mOrderedBroadcastHelper; @@ -182,14 +190,14 @@ abstract public class BrowserApp extends GeckoApp case SELECTED: if (Tabs.getInstance().isSelectedTab(tab)) { if (isAboutHome(tab)) { - showAboutHome(); + showHomePager(tab.getAboutHomePage()); if (isDynamicToolbarEnabled()) { // Show the toolbar. mLayerView.getLayerMarginsAnimator().showMargins(false); } } else { - hideAboutHome(); + hideHomePager(); } if (mSiteIdentityPopup != null) @@ -242,12 +250,6 @@ abstract public class BrowserApp extends GeckoApp super.onTabChanged(tab, msg, data); } - @Override - void handleClearHistory() { - super.handleClearHistory(); - updateAboutHomeTopSites(); - } - @Override public boolean onKey(View v, int keyCode, KeyEvent event) { // Global onKey handler. This is called if the focused UI doesn't @@ -263,7 +265,7 @@ abstract public class BrowserApp extends GeckoApp case KeyEvent.KEYCODE_BUTTON_Y: // Toggle/focus the address bar on gamepad-y button. if (mBrowserToolbar.isVisible()) { - if (isDynamicToolbarEnabled() && !mAboutHome.getUserVisibleHint()) { + if (isDynamicToolbarEnabled() && !mHomePager.isVisible()) { if (mLayerView != null) { mLayerView.getLayerMarginsAnimator().hideMargins(false); mLayerView.requestFocus(); @@ -330,7 +332,11 @@ abstract public class BrowserApp extends GeckoApp @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (onKey(null, keyCode, event)) { + if (!mBrowserToolbar.isEditing() && onKey(null, keyCode, event)) { + return true; + } + + if (mBrowserToolbar.onKey(keyCode, event)) { return true; } @@ -383,19 +389,6 @@ abstract public class BrowserApp extends GeckoApp }); } - @Override - void onStatePurged() { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - if (mAboutHome != null) - mAboutHome.setLastTabsVisibility(false); - } - }); - - super.onStatePurged(); - } - @Override protected int getSessionRestoreState(Bundle savedInstanceState) { if (mSessionRestore > -1) { @@ -427,29 +420,50 @@ abstract public class BrowserApp extends GeckoApp // If we get a gamepad panning MotionEvent while the focus is not on the layerview, // put the focus on the layerview and carry on if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) { - if (mAboutHome.getUserVisibleHint()) { - mAboutHome.requestFocus(); - } else { + if (mHomePager.isVisible()) { mLayerView.requestFocus(); + } else { + mHomePager.requestFocus(); } } return false; } }); - // Find the Fragment if it was already added from a restored instance state. - mAboutHome = (AboutHome) getSupportFragmentManager().findFragmentByTag(ABOUTHOME_TAG); + mHomePager = (HomePager) findViewById(R.id.home_pager); + mHomePagerContainer = findViewById(R.id.home_pager_container); - if (mAboutHome == null) { - // AboutHome will be dynamically attached and detached as - // about:home is shown. Adding/removing the fragment is not synchronous, - // so we can't use Fragment#isVisible() to determine whether the - // about:home is shown. Instead, we use Fragment#getUserVisibleHint() - // with the hint we set ourselves. - mAboutHome = AboutHome.newInstance(); - mAboutHome.setUserVisibleHint(false); + mBrowserSearchContainer = findViewById(R.id.search_container); + mBrowserSearch = (BrowserSearch) getSupportFragmentManager().findFragmentByTag(BROWSER_SEARCH_TAG); + if (mBrowserSearch == null) { + mBrowserSearch = BrowserSearch.newInstance(); + mBrowserSearch.setUserVisibleHint(false); } + mBrowserToolbar.setOnActivateListener(new BrowserToolbar.OnActivateListener() { + public void onActivate() { + enterEditingMode(); + } + }); + + mBrowserToolbar.setOnCommitListener(new BrowserToolbar.OnCommitListener() { + public void onCommit() { + commitEditingMode(); + } + }); + + mBrowserToolbar.setOnDismissListener(new BrowserToolbar.OnDismissListener() { + public void onDismiss() { + dismissEditingMode(); + } + }); + + mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() { + public void onFilter(String searchText, AutocompleteHandler handler) { + filterEditingMode(searchText, handler); + } + }); + // Intercept key events for gamepad shortcuts mBrowserToolbar.setOnKeyListener(this); @@ -468,6 +482,7 @@ abstract public class BrowserApp extends GeckoApp registerEventListener("Telemetry:Gather"); registerEventListener("Settings:Show"); registerEventListener("Updater:Launch"); + registerEventListener("Reader:GoToReadingList"); Distribution.init(this, getPackageResourcePath()); JavaAddonManager.getInstance().init(getApplicationContext()); @@ -493,7 +508,7 @@ abstract public class BrowserApp extends GeckoApp if (savedInstanceState != null) { mDynamicToolbarEnabled = savedInstanceState.getBoolean(STATE_DYNAMIC_TOOLBAR_ENABLED); - mAboutHome.setTopPadding(savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING)); + mHomePagerContainer.setPadding(0, savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING), 0, 0); } // Listen to the dynamic toolbar pref @@ -526,6 +541,25 @@ abstract public class BrowserApp extends GeckoApp }); } + @Override + public void onBackPressed() { + if (getSupportFragmentManager().getBackStackEntryCount() > 0) { + super.onBackPressed(); + return; + } + + if (dismissEditingMode()) { + return; + } + + if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) { + mSiteIdentityPopup.dismiss(); + return; + } + + super.onBackPressed(); + } + @Override public void onResume() { super.onResume(); @@ -539,8 +573,6 @@ abstract public class BrowserApp extends GeckoApp registerEventListener("Prompt:ShowTop"); } - - private void showBookmarkDialog() { final Tab tab = Tabs.getInstance().getSelectedTab(); final Prompt ps = new Prompt(this, new Prompt.PromptCallback() { @@ -583,14 +615,14 @@ abstract public class BrowserApp extends GeckoApp mLayerView.getLayerClient().setOnMetricsChangedListener(this); } setToolbarMargin(0); - mAboutHome.setTopPadding(mBrowserToolbar.getHeight()); + mHomePagerContainer.setPadding(0, mBrowserToolbar.getHeight(), 0, 0); } else { // Immediately show the toolbar when disabling the dynamic // toolbar. if (mLayerView != null) { mLayerView.getLayerClient().setOnMetricsChangedListener(null); } - mAboutHome.setTopPadding(0); + mHomePagerContainer.setPadding(0, 0, 0, 0); if (mBrowserToolbar != null) { mBrowserToolbar.scrollTo(0, 0); } @@ -609,7 +641,8 @@ abstract public class BrowserApp extends GeckoApp @Override public boolean onSearchRequested() { - return showAwesomebar(AwesomeBar.Target.CURRENT_TAB); + enterEditingMode(); + return true; } @Override @@ -631,7 +664,7 @@ abstract public class BrowserApp extends GeckoApp if (itemId == R.id.paste) { String text = Clipboard.getText(); if (!TextUtils.isEmpty(text)) { - showAwesomebar(AwesomeBar.Target.CURRENT_TAB, text); + enterEditingMode(text); } return true; } @@ -690,41 +723,6 @@ abstract public class BrowserApp extends GeckoApp return false; } - public boolean showAwesomebar(AwesomeBar.Target aTarget) { - return showAwesomebar(aTarget, null); - } - - public boolean showAwesomebar(AwesomeBar.Target aTarget, String aUrl) { - Intent intent = new Intent(getBaseContext(), AwesomeBar.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.putExtra(AwesomeBar.TARGET_KEY, aTarget.name()); - - // If we were passed in a URL, show it. - if (aUrl != null && !TextUtils.isEmpty(aUrl)) { - intent.putExtra(AwesomeBar.CURRENT_URL_KEY, aUrl); - } else if (aTarget == AwesomeBar.Target.CURRENT_TAB) { - // Otherwise, if we're editing the current tab, show its URL. - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - // Check to see if there's a user-entered search term, which we save - // whenever the user performs a search. - aUrl = tab.getUserSearch(); - if (TextUtils.isEmpty(aUrl)) { - aUrl = tab.getURL(); - } - if (aUrl != null) { - intent.putExtra(AwesomeBar.CURRENT_URL_KEY, aUrl); - } - } - } - - int requestCode = GeckoAppShell.sActivityHelper.makeRequestCodeForAwesomebar(); - startActivityForResult(intent, requestCode); - overridePendingTransition (R.anim.awesomebar_fade_in, R.anim.awesomebar_hold_still); - return true; - } - - @Override public void setAccessibilityEnabled(boolean enabled) { if (mAccessibilityEnabled == enabled) { @@ -776,6 +774,7 @@ abstract public class BrowserApp extends GeckoApp unregisterEventListener("Telemetry:Gather"); unregisterEventListener("Settings:Show"); unregisterEventListener("Updater:Launch"); + unregisterEventListener("Reader:GoToReadingList"); if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) { NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this); @@ -843,7 +842,7 @@ abstract public class BrowserApp extends GeckoApp @Override public void onMetricsChanged(ImmutableViewportMetrics aMetrics) { - if (mAboutHome.getUserVisibleHint() || mBrowserToolbar == null) { + if (mHomePager.isVisible() || mBrowserToolbar == null) { return; } @@ -878,7 +877,7 @@ abstract public class BrowserApp extends GeckoApp @Override public void onPanZoomStopped() { - if (!isDynamicToolbarEnabled() || mAboutHome.getUserVisibleHint()) { + if (!isDynamicToolbarEnabled() || mHomePager.isVisible()) { return; } @@ -900,12 +899,19 @@ abstract public class BrowserApp extends GeckoApp height = mBrowserToolbar.getHeight(); } - if (!isDynamicToolbarEnabled()) { + if (!isDynamicToolbarEnabled() || mHomePager.isVisible()) { // Use aVisibleHeight here so that when the dynamic toolbar is // enabled, the padding will animate with the toolbar becoming // visible. - setToolbarMargin(height); - height = 0; + if (isDynamicToolbarEnabled()) { + // When the dynamic toolbar is enabled, set the padding on the + // about:home widget directly - this is to avoid resizing the + // LayerView, which can cause visible artifacts. + mHomePagerContainer.setPadding(0, height, 0, 0); + } else { + setToolbarMargin(height); + height = 0; + } } else { setToolbarMargin(0); } @@ -957,37 +963,11 @@ abstract public class BrowserApp extends GeckoApp } } - @Override - public void onBackPressed() { - if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) { - mSiteIdentityPopup.dismiss(); - return; - } - - super.onBackPressed(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - String url = null; - - // Don't update the url in the toolbar if the activity was cancelled. - if (resultCode == Activity.RESULT_OK && data != null) { - // Don't update the url if the activity was launched to pick a site. - String targetKey = data.getStringExtra(AwesomeBar.TARGET_KEY); - if (!AwesomeBar.Target.PICK_SITE.toString().equals(targetKey)) { - // Update the toolbar with the url that was just entered. - url = data.getStringExtra(AwesomeBar.URL_KEY); - } - } - - // We always need to call fromAwesomeBarSearch to perform the toolbar animation. - mBrowserToolbar.fromAwesomeBarSearch(url); - - // Trigger any tab-related events after we start restoring - // the toolbar state above to make ensure animations happen - // on the correct order. - super.onActivityResult(requestCode, resultCode, data); + public View getActionBarLayout() { + RelativeLayout actionBar = (RelativeLayout) LayoutInflater.from(this).inflate(R.layout.browser_toolbar, null); + actionBar.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT, + (int) getResources().getDimension(R.dimen.browser_toolbar_height))); + return actionBar; } @Override @@ -1155,6 +1135,8 @@ abstract public class BrowserApp extends GeckoApp startActivity(settingsIntent); } else if (event.equals("Updater:Launch")) { handleUpdaterLaunch(); + } else if (event.equals("Reader:GoToReadingList")) { + openReadingList(); } else if (event.equals("Prompt:ShowTop")) { // Bring this activity to front so the prompt is visible.. Intent bringToFrontIntent = new Intent(); @@ -1171,7 +1153,7 @@ abstract public class BrowserApp extends GeckoApp @Override public void addTab() { - showAwesomebar(AwesomeBar.Target.NEW_TAB); + Tabs.getInstance().loadUrl("about:home", Tabs.LOADURL_NEW_TAB); } @Override @@ -1234,7 +1216,7 @@ abstract public class BrowserApp extends GeckoApp } mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator); - mMainLayoutAnimator.setPropertyAnimationListener(this); + mMainLayoutAnimator.addPropertyAnimationListener(this); if (hasTabsSideBar()) { mMainLayoutAnimator.attach(mMainLayout, @@ -1284,7 +1266,66 @@ abstract public class BrowserApp extends GeckoApp super.onSaveInstanceState(outState); mToast.onSaveInstanceState(outState); outState.putBoolean(STATE_DYNAMIC_TOOLBAR_ENABLED, mDynamicToolbarEnabled); - outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mAboutHome.getTopPadding()); + outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomePagerContainer.getPaddingTop()); + } + + /** + * Attempts to switch to an open tab with the given URL. + * + * @return true if we successfully switched to a tab, false otherwise. + */ + private boolean maybeSwitchToTab(String url, EnumSet flags) { + if (!flags.contains(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)) { + return false; + } + + final Tabs tabs = Tabs.getInstance(); + final int tabId = tabs.getTabIdForUrl(url); + if (tabId < 0) { + return false; + } + + // If this tab is already selected, just hide the home pager. + if (tabs.isSelectedTab(tabs.getTab(tabId))) { + hideHomePager(); + } else { + tabs.selectTab(tabId); + } + + hideBrowserSearch(); + mBrowserToolbar.cancelEdit(); + + return true; + } + + private void openUrl(String url) { + openUrl(url, null, false); + } + + private void openUrl(String url, boolean newTab) { + openUrl(url, null, newTab); + } + + private void openUrl(String url, String searchEngine) { + openUrl(url, searchEngine, false); + } + + private void openUrl(String url, String searchEngine, boolean newTab) { + mBrowserToolbar.setProgressVisibility(true); + + int flags = Tabs.LOADURL_NONE; + if (newTab) { + flags |= Tabs.LOADURL_NEW_TAB; + } + + Tabs.getInstance().loadUrl(url, searchEngine, -1, flags); + + hideBrowserSearch(); + mBrowserToolbar.cancelEdit(); + } + + private void openReadingList() { + Tabs.getInstance().loadUrl(ABOUT_HOME, Tabs.LOADURL_READING_LIST); } /* Favicon methods */ @@ -1330,14 +1371,80 @@ abstract public class BrowserApp extends GeckoApp tab.setFaviconLoadId(Favicons.NOT_LOADING); } + private void enterEditingMode() { + String url = null; - /* About:home UI */ - void updateAboutHomeTopSites() { - mAboutHome.update(EnumSet.of(AboutHome.UpdateFlags.TOP_SITES)); + final Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + final String userSearch = tab.getUserSearch(); + + // Check to see if there's a user-entered search term, + // which we save whenever the user performs a search. + url = (TextUtils.isEmpty(userSearch) ? tab.getURL() : userSearch); + } + + enterEditingMode(url); } - private void showAboutHome() { - if (mAboutHome.getUserVisibleHint()) { + /** + * Enters editing mode for the current tab. This method will + * always open the VISITED page on about:home. + */ + private void enterEditingMode(String url) { + if (url == null) { + throw new IllegalArgumentException("Cannot handle null URLs in enterEditingMode"); + } + + final PropertyAnimator animator = new PropertyAnimator(250); + animator.setUseHardwareLayer(false); + + mBrowserToolbar.startEditing(url, animator); + showHomePagerWithAnimator(HomePager.Page.HISTORY, animator); + + animator.start(); + } + + void commitEditingMode() { + if (!mBrowserToolbar.isEditing()) { + return; + } + + final String url = mBrowserToolbar.commitEdit(); + animateHideHomePager(); + hideBrowserSearch(); + + if (!TextUtils.isEmpty(url)) { + Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED); + } + } + + boolean dismissEditingMode() { + if (!mBrowserToolbar.isEditing()) { + return false; + } + + mBrowserToolbar.cancelEdit(); + animateHideHomePager(); + hideBrowserSearch(); + + return true; + } + + void filterEditingMode(String searchTerm, AutocompleteHandler handler) { + if (TextUtils.isEmpty(searchTerm)) { + hideBrowserSearch(); + } else { + showBrowserSearch(); + mBrowserSearch.filter(searchTerm, handler); + } + } + + private void showHomePager(HomePager.Page page) { + showHomePagerWithAnimator(page, null); + } + + private void showHomePagerWithAnimator(HomePager.Page page, PropertyAnimator animator) { + if (mHomePager.isVisible()) { return; } @@ -1350,29 +1457,29 @@ abstract public class BrowserApp extends GeckoApp mLayerView.getLayerMarginsAnimator().showMargins(true); } - // We use commitAllowingStateLoss() instead of commit() here to avoid an - // IllegalStateException. showAboutHome() and hideAboutHome() are - // executed inside of tab's onChange() callback. Since that callback can - // be triggered asynchronously from Gecko, it's possible that this - // method can be called while Fennec is in the background. If that - // happens, using commit() would throw an IllegalStateException since - // it can't be used between the Activity's onSaveInstanceState() and - // onResume(). - getSupportFragmentManager().beginTransaction() - .add(R.id.gecko_layout, mAboutHome, ABOUTHOME_TAG).commitAllowingStateLoss(); - mAboutHome.setUserVisibleHint(true); - - mBrowserToolbar.setNextFocusDownId(R.id.abouthome_content); + mHomePager.show(getSupportFragmentManager(), page, animator); } - private void hideAboutHome() { - if (!mAboutHome.getUserVisibleHint()) { + private void animateHideHomePager() { + hideHomePagerWithAnimation(true); + } + + private void hideHomePager() { + hideHomePagerWithAnimation(false); + } + + private void hideHomePagerWithAnimation(boolean animate) { + if (!mHomePager.isVisible()) { return; } - getSupportFragmentManager().beginTransaction() - .remove(mAboutHome).commitAllowingStateLoss(); - mAboutHome.setUserVisibleHint(false); + final Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null && isAboutHome(tab)) { + return; + } + + // FIXME: do animation if animate is true + mHomePager.hide(); mBrowserToolbar.setShadowVisibility(true); mBrowserToolbar.setNextFocusDownId(R.id.layer_view); @@ -1381,6 +1488,30 @@ abstract public class BrowserApp extends GeckoApp refreshToolbarHeight(); } + private void showBrowserSearch() { + if (mBrowserSearch.getUserVisibleHint()) { + return; + } + + mBrowserSearchContainer.setVisibility(View.VISIBLE); + + getSupportFragmentManager().beginTransaction() + .add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss(); + mBrowserSearch.setUserVisibleHint(true); + } + + private void hideBrowserSearch() { + if (!mBrowserSearch.getUserVisibleHint()) { + return; + } + + mBrowserSearchContainer.setVisibility(View.INVISIBLE); + + getSupportFragmentManager().beginTransaction() + .remove(mBrowserSearch).commitAllowingStateLoss(); + mBrowserSearch.setUserVisibleHint(false); + } + private class HideTabsTouchListener implements TouchEventInterceptor { private boolean mIsHidingTabs = false; @@ -1994,15 +2125,32 @@ abstract public class BrowserApp extends GeckoApp }).execute(); } + // HomePager.OnNewTabsListener @Override - public void onAboutHomeUriLoad(String url) { - mBrowserToolbar.setProgressVisibility(true); - Tabs.getInstance().loadUrl(url); + public void onNewTabs(String[] urls) { + for (String url : urls) { + openUrl(url, true); + } } + // HomePager.OnUrlOpenListener @Override - public void onAboutHomeLoadComplete() { - mAboutHomeStartupTimer.stop(); + public void onUrlOpen(String url, EnumSet flags) { + if (!maybeSwitchToTab(url, flags)) { + openUrl(url); + } + } + + // BrowserSearch.OnSearchListener + @Override + public void onSearch(String engineId, String text) { + openUrl(text, engineId); + } + + // BrowserSearch.OnEditSuggestionListener + @Override + public void onEditSuggestion(String suggestion) { + mBrowserToolbar.onEditSuggestion(suggestion); } @Override diff --git a/mobile/android/base/BrowserToolbar.java b/mobile/android/base/BrowserToolbar.java index dcb521f540d1..d2ff3ffd6eec 100644 --- a/mobile/android/base/BrowserToolbar.java +++ b/mobile/android/base/BrowserToolbar.java @@ -9,6 +9,7 @@ import org.mozilla.gecko.animation.PropertyAnimator; import org.mozilla.gecko.animation.ViewHelper; import org.mozilla.gecko.gfx.ImmutableViewportMetrics; import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.menu.MenuPopup; import org.mozilla.gecko.PageActionLayout; @@ -34,12 +35,17 @@ import android.graphics.drawable.StateListDrawable; import android.os.Build; import android.os.SystemClock; import android.text.style.ForegroundColorSpan; +import android.text.Editable; +import android.text.InputType; import android.text.Spannable; import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextUtils; +import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.ContextMenu; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MotionEvent; @@ -54,8 +60,10 @@ import android.view.animation.AnimationUtils; import android.view.animation.AlphaAnimation; import android.view.animation.Interpolator; import android.view.animation.TranslateAnimation; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; +import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -68,17 +76,38 @@ import java.util.Arrays; import java.util.List; public class BrowserToolbar extends GeckoRelativeLayout - implements Tabs.OnTabsChangedListener, + implements TextWatcher, + AutocompleteHandler, + Tabs.OnTabsChangedListener, GeckoMenu.ActionItemBarPresenter, Animation.AnimationListener, GeckoEventListener { private static final String LOGTAG = "GeckoToolbar"; public static final String PREF_TITLEBAR_MODE = "browser.chrome.titlebarMode"; + + public interface OnActivateListener { + public void onActivate(); + } + + public interface OnCommitListener { + public void onCommit(); + } + + public interface OnDismissListener { + public void onDismiss(); + } + + public interface OnFilterListener { + public void onFilter(String searchText, AutocompleteHandler handler); + } + private LayoutParams mAwesomeBarParams; private View mUrlDisplayContainer; - private View mAwesomeBarEntry; - private ImageView mAwesomeBarRightEdge; - private BrowserToolbarBackground mAddressBarBg; + private View mUrlEditContainer; + private CustomEditText mUrlEditText; + private View mUrlBarEntry; + private ImageView mUrlBarRightEdge; + private BrowserToolbarBackground mUrlBarBackground; private GeckoTextView mTitle; private int mTitlePadding; private boolean mSiteSecurityVisible; @@ -89,6 +118,7 @@ public class BrowserToolbar extends GeckoRelativeLayout public ImageButton mFavicon; public ImageButton mStop; public ImageButton mSiteSecurity; + public ImageButton mGo; public PageActionLayout mPageActionLayout; private Animation mProgressSpinner; private TabCounter mTabsCounter; @@ -98,6 +128,10 @@ public class BrowserToolbar extends GeckoRelativeLayout private LinearLayout mActionItemBar; private MenuPopup mMenuPopup; private List mFocusOrder; + private OnActivateListener mActivateListener; + private OnCommitListener mCommitListener; + private OnDismissListener mDismissListener; + private OnFilterListener mFilterListener; final private BrowserApp mActivity; private boolean mHasSoftMenuButton; @@ -106,13 +140,20 @@ public class BrowserToolbar extends GeckoRelativeLayout private boolean mShowReader; private boolean mSpinnerVisible; + private boolean mDelayRestartInput; + // The previous autocomplete result returned to us + private String mAutoCompleteResult = ""; + // The user typed part of the autocomplete result + private String mAutoCompletePrefix = null; + + private boolean mIsEditing; private boolean mAnimatingEntry; private AlphaAnimation mLockFadeIn; private TranslateAnimation mTitleSlideLeft; private TranslateAnimation mTitleSlideRight; - private int mAddressBarViewOffset; + private int mUrlBarViewOffset; private int mDefaultForwardMargin; private PropertyAnimator mForwardAnim = null; @@ -150,6 +191,7 @@ public class BrowserToolbar extends GeckoRelativeLayout Tabs.registerOnTabsChangedListener(this); mSwitchingTabs = true; + mIsEditing = false; mAnimatingEntry = false; mShowUrl = false; @@ -195,19 +237,22 @@ public class BrowserToolbar extends GeckoRelativeLayout mAnimatingEntry = false; - mAddressBarBg = (BrowserToolbarBackground) findViewById(R.id.address_bar_bg); - mAddressBarViewOffset = res.getDimensionPixelSize(R.dimen.addressbar_offset_left); + mUrlBarBackground = (BrowserToolbarBackground) findViewById(R.id.url_bar_bg); + mUrlBarViewOffset = res.getDimensionPixelSize(R.dimen.url_bar_offset_left); mDefaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset); - mUrlDisplayContainer = findViewById(R.id.awesome_bar_display_container); - mAwesomeBarEntry = findViewById(R.id.awesome_bar_entry); + mUrlDisplayContainer = findViewById(R.id.url_display_container); + mUrlBarEntry = findViewById(R.id.url_bar_entry); + + mUrlEditContainer = findViewById(R.id.url_edit_container); + mUrlEditText = (CustomEditText) findViewById(R.id.url_edit_text); // This will clip the right edge's image at half of its width - mAwesomeBarRightEdge = (ImageView) findViewById(R.id.awesome_bar_right_edge); - if (mAwesomeBarRightEdge != null) { - mAwesomeBarRightEdge.getDrawable().setLevel(5000); + mUrlBarRightEdge = (ImageView) findViewById(R.id.url_bar_right_edge); + if (mUrlBarRightEdge != null) { + mUrlBarRightEdge.getDrawable().setLevel(5000); } - mTitle = (GeckoTextView) findViewById(R.id.awesome_bar_title); + mTitle = (GeckoTextView) findViewById(R.id.url_bar_title); mTitlePadding = mTitle.getPaddingRight(); mTabs = (ShapedButton) findViewById(R.id.tabs); @@ -258,14 +303,20 @@ public class BrowserToolbar extends GeckoRelativeLayout setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { - mActivity.autoHideTabs(); - onAwesomeBarSearch(); + if (mActivateListener != null) { + mActivateListener.onActivate(); + } } }); setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + // We don't the context menu while editing + if (isEditing()) { + return; + } + MenuInflater inflater = mActivity.getMenuInflater(); inflater.inflate(R.menu.titlebar_contextmenu, menu); @@ -296,6 +347,96 @@ public class BrowserToolbar extends GeckoRelativeLayout } }); + mUrlEditText.addTextChangedListener(this); + + mUrlEditText.setOnKeyPreImeListener(new CustomEditText.OnKeyPreImeListener() { + @Override + public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) { + // We only want to process one event per tap + if (event.getAction() != KeyEvent.ACTION_DOWN) + return false; + + if (keyCode == KeyEvent.KEYCODE_ENTER) { + // If the edit text has a composition string, don't submit the text yet. + // ENTER is needed to commit the composition string. + Editable content = mUrlEditText.getText(); + if (!hasCompositionString(content)) { + if (mCommitListener != null) { + mCommitListener.onCommit(); + } + return true; + } + } + + if (keyCode == KeyEvent.KEYCODE_BACK) { + clearFocus(); + return true; + } + + return false; + } + }); + + mUrlEditText.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) { + if (event.getAction() != KeyEvent.ACTION_DOWN) + return true; + + if (mCommitListener != null) { + mCommitListener.onCommit(); + } + return true; + } else if (GamepadUtils.isBackKey(event)) { + if (mDismissListener != null) { + mDismissListener.onDismiss(); + } + return true; + } + + return false; + } + }); + + mUrlEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (v == null) { + return; + } + + setSelected(hasFocus); + if (hasFocus) { + return; + } + + InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); + try { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } catch (NullPointerException e) { + Log.e(LOGTAG, "InputMethodManagerService, why are you throwing" + + " a NullPointerException? See bug 782096", e); + } + } + }); + + mUrlEditText.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (Build.VERSION.SDK_INT >= 11) { + CustomEditText text = (CustomEditText) v; + + if (text.getSelectionStart() == text.getSelectionEnd()) + return false; + + return false; + } + + return false; + } + }); + mTabs.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { @@ -360,6 +501,17 @@ public class BrowserToolbar extends GeckoRelativeLayout } }); + mGo = (ImageButton) findViewById(R.id.go); + mGo.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View v) { + if (mCommitListener != null) { + mCommitListener.onCommit(); + } + } + }); + + mShadow = (ImageView) findViewById(R.id.shadow); mShadow.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { @@ -416,6 +568,54 @@ public class BrowserToolbar extends GeckoRelativeLayout } } + public boolean onKey(int keyCode, KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + + // Galaxy Note sends key events for the stylus that are outside of the + // valid keyCode range (see bug 758427) + if (keyCode > KeyEvent.getMaxKeyCode()) { + return true; + } + + // This method is called only if the key event was not handled + // by any of the views, which usually means the edit box lost focus + if (keyCode == KeyEvent.KEYCODE_BACK || + keyCode == KeyEvent.KEYCODE_MENU || + keyCode == KeyEvent.KEYCODE_DPAD_UP || + keyCode == KeyEvent.KEYCODE_DPAD_DOWN || + keyCode == KeyEvent.KEYCODE_DPAD_LEFT || + keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || + keyCode == KeyEvent.KEYCODE_DPAD_CENTER || + keyCode == KeyEvent.KEYCODE_DEL || + keyCode == KeyEvent.KEYCODE_VOLUME_UP || + keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + return false; + } else if (isEditing()) { + final int prevSelStart = mUrlEditText.getSelectionStart(); + final int prevSelEnd = mUrlEditText.getSelectionEnd(); + + // Manually dispatch the key event to the edit text. If selection changed as + // a result of the key event, then give focus back to mUrlEditText + mUrlEditText.dispatchKeyEvent(event); + + final int curSelStart = mUrlEditText.getSelectionStart(); + final int curSelEnd = mUrlEditText.getSelectionEnd(); + + if (prevSelStart != curSelStart || prevSelEnd != curSelEnd) { + mUrlEditText.requestFocusFromTouch(); + + // Restore the selection, which gets lost due to the focus switch + mUrlEditText.setSelection(curSelStart, curSelEnd); + } + + return true; + } + + return false; + } + @Override public boolean onTouchEvent(MotionEvent event) { // If the motion event has occured below the toolbar (due to the scroll @@ -515,6 +715,78 @@ public class BrowserToolbar extends GeckoRelativeLayout } } + // Return early if we're backspacing through the string, or + // have no autocomplete results + @Override + public void onAutocomplete(final String result) { + final String text = mUrlEditText.getText().toString(); + + if (result == null) { + mAutoCompleteResult = ""; + return; + } + + if (!result.startsWith(text) || text.equals(result)) { + return; + } + + mAutoCompleteResult = result; + mUrlEditText.getText().append(result.substring(text.length())); + mUrlEditText.setSelection(text.length(), result.length()); + } + + @Override + public void afterTextChanged(final Editable s) { + final String text = s.toString(); + boolean useHandler = false; + boolean reuseAutocomplete = false; + if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) { + useHandler = true; + + // If you're hitting backspace (the string is getting smaller + // or is unchanged), don't autocomplete. + if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) { + useHandler = false; + } else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) { + // If this text already matches our autocomplete text, autocomplete likely + // won't change. Just reuse the old autocomplete value. + useHandler = false; + reuseAutocomplete = true; + } + } + + // If this is the autocomplete text being set, don't run the filter. + if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) { + if (isEditing() && mFilterListener != null) { + mFilterListener.onFilter(text, useHandler ? this : null); + } + mAutoCompletePrefix = text; + + if (reuseAutocomplete) { + onAutocomplete(mAutoCompleteResult); + } + } + + // If the edit text has a composition string, don't call updateGoButton(). + // That method resets IME and composition state will be broken. + if (!hasCompositionString(s) || + InputMethods.isGestureKeyboard(mUrlEditText.getContext())) { + updateGoButton(text); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + // do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + // do nothing + } + public boolean isVisible() { return getScrollY() == 0; } @@ -559,190 +831,25 @@ public class BrowserToolbar extends GeckoRelativeLayout } } - private int getAwesomeBarEntryTranslation() { - return getWidth() - mAwesomeBarEntry.getRight(); + private int getUrlBarEntryTranslation() { + return getWidth() - mUrlBarEntry.getRight(); } - private int getAwesomeBarCurveTranslation() { + private int getUrlBarCurveTranslation() { return getWidth() - mTabs.getLeft(); } - public void fromAwesomeBarSearch(String url) { - // Update the title with the url that was just entered. Don't update the title if - // the AwesomeBar activity was cancelled, or if the user entered an empty string. - if (url != null && url.length() > 0) { - setTitle(url); - } - - if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) { - return; - } - - // If the awesomebar entry is not selected at this point, this means that - // we had to reinflate the toolbar layout for some reason (device rotation - // while in awesome screen, activity was killed in background, etc). In this - // case, we have to ensure the toolbar is in the correct initial state to - // shrink back. - if (!isSelected()) { - // Keep the entry highlighted during the animation - setSelected(true); - - final int entryTranslation = getAwesomeBarEntryTranslation(); - final int curveTranslation = getAwesomeBarCurveTranslation(); - - if (mAwesomeBarRightEdge != null) { - ViewHelper.setTranslationX(mAwesomeBarRightEdge, entryTranslation); + private static boolean hasCompositionString(Editable content) { + Object[] spans = content.getSpans(0, content.length(), Object.class); + if (spans != null) { + for (Object span : spans) { + if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { + // Found composition string. + return true; + } } - - ViewHelper.setTranslationX(mTabs, curveTranslation); - ViewHelper.setTranslationX(mTabsCounter, curveTranslation); - ViewHelper.setTranslationX(mActionItemBar, curveTranslation); - - if (mHasSoftMenuButton) { - ViewHelper.setTranslationX(mMenu, curveTranslation); - ViewHelper.setTranslationX(mMenuIcon, curveTranslation); - } - - ViewHelper.setAlpha(mPageActionLayout, 0); - ViewHelper.setAlpha(mStop, 0); } - - final PropertyAnimator contentAnimator = new PropertyAnimator(250); - contentAnimator.setUseHardwareLayer(false); - - // Shrink the awesome entry back to its original size - - if (mAwesomeBarRightEdge != null) { - contentAnimator.attach(mAwesomeBarRightEdge, - PropertyAnimator.Property.TRANSLATION_X, - 0); - } - - contentAnimator.attach(mTabs, - PropertyAnimator.Property.TRANSLATION_X, - 0); - contentAnimator.attach(mTabsCounter, - PropertyAnimator.Property.TRANSLATION_X, - 0); - contentAnimator.attach(mActionItemBar, - PropertyAnimator.Property.TRANSLATION_X, - 0); - - if (mHasSoftMenuButton) { - contentAnimator.attach(mMenu, - PropertyAnimator.Property.TRANSLATION_X, - 0); - contentAnimator.attach(mMenuIcon, - PropertyAnimator.Property.TRANSLATION_X, - 0); - } - - contentAnimator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - } - - @Override - public void onPropertyAnimationEnd() { - // Turn off selected state on the entry - setSelected(false); - - PropertyAnimator buttonsAnimator = new PropertyAnimator(300); - - // Fade toolbar buttons (reader, stop) after the entry - // is schrunk back to its original size. - buttonsAnimator.attach(mPageActionLayout, - PropertyAnimator.Property.ALPHA, - 1); - buttonsAnimator.attach(mStop, - PropertyAnimator.Property.ALPHA, - 1); - - buttonsAnimator.start(); - - mAnimatingEntry = false; - - // Trigger animation to update the tabs counter once the - // tabs button is back on screen. - updateTabCount(Tabs.getInstance().getDisplayCount()); - } - }); - - mAnimatingEntry = true; - - postDelayed(new Runnable() { - @Override - public void run() { - contentAnimator.start(); - } - }, 500); - } - - private void onAwesomeBarSearch() { - // This animation doesn't make much sense in a sidebar UI - if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) { - mActivity.onSearchRequested(); - return; - } - - if (mAnimatingEntry) - return; - - final PropertyAnimator contentAnimator = new PropertyAnimator(250); - contentAnimator.setUseHardwareLayer(false); - - final int entryTranslation = getAwesomeBarEntryTranslation(); - final int curveTranslation = getAwesomeBarCurveTranslation(); - - // Keep the entry highlighted during the animation - setSelected(true); - - // Hide stop/reader buttons immediately - ViewHelper.setAlpha(mPageActionLayout, 0); - ViewHelper.setAlpha(mStop, 0); - - // Slide the right side elements of the toolbar - - if (mAwesomeBarRightEdge != null) { - contentAnimator.attach(mAwesomeBarRightEdge, - PropertyAnimator.Property.TRANSLATION_X, - entryTranslation); - } - - contentAnimator.attach(mTabs, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - contentAnimator.attach(mTabsCounter, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - contentAnimator.attach(mActionItemBar, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - - if (mHasSoftMenuButton) { - contentAnimator.attach(mMenu, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - contentAnimator.attach(mMenuIcon, - PropertyAnimator.Property.TRANSLATION_X, - curveTranslation); - } - - contentAnimator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { - @Override - public void onPropertyAnimationStart() { - } - - @Override - public void onPropertyAnimationEnd() { - // Once the entry is fully expanded, start awesome screen - mActivity.onSearchRequested(); - mAnimatingEntry = false; - } - }); - - mAnimatingEntry = true; - contentAnimator.start(); + return false; } private void addTab() { @@ -769,12 +876,32 @@ public class BrowserToolbar extends GeckoRelativeLayout } } - public void updateTabCount(int count) { - // If toolbar is selected, this means the entry is expanded and the + public void updateTabCountAndAnimate(int count) { + // Don't animate if the toolbar is hidden. + if (!isVisible()) { + updateTabCount(count); + return; + } + + // If toolbar is in edit mode, this means the entry is expanded and the // tabs button is translated offscreen. Don't trigger tabs counter // updates until the tabs button is back on screen. - // See fromAwesomeBarSearch() - if (isSelected()) { + // See stopEditing() + if (!isEditing()) { + mTabsCounter.setCount(count); + + mTabs.setContentDescription((count > 1) ? + mActivity.getString(R.string.num_tabs, count) : + mActivity.getString(R.string.one_tab)); + } + } + + public void updateTabCount(int count) { + // If toolbar is in edit mode, this means the entry is expanded and the + // tabs button is translated offscreen. Don't trigger tabs counter + // updates until the tabs button is back on screen. + // See stopEditing() + if (isEditing()) { return; } @@ -919,15 +1046,28 @@ public class BrowserToolbar extends GeckoRelativeLayout String url = tab.getURL(); - // Only set shadow to visible when not on about screens except about:blank. - visible &= !(url == null || (url.startsWith("about:") && - !url.equals("about:blank"))); + // Only set shadow to visible when not on about screens (except about:blank) + // and when not in editing mode. + visible &= !(url == null || (url.startsWith("about:") && + !url.equals("about:blank"))) && !isEditing(); if ((mShadow.getVisibility() == View.VISIBLE) != visible) { mShadow.setVisibility(visible ? View.VISIBLE : View.GONE); } } + public void onEditSuggestion(String suggestion) { + if (!isEditing()) { + return; + } + + mUrlEditText.setText(suggestion); + mUrlEditText.setSelection(mUrlEditText.getText().length()); + mUrlEditText.requestFocus(); + + showSoftInput(); + } + private void setTitle(CharSequence title) { mTitle.setText(title); setContentDescription(title != null ? title : mTitle.getHint()); @@ -942,6 +1082,11 @@ public class BrowserToolbar extends GeckoRelativeLayout } String url = tab.getURL(); + + if (!isEditing()) { + mUrlEditText.setText(url); + } + // Setting a null title will ensure we just see the "Enter Search or Address" placeholder text. if ("about:home".equals(url) || "about:privatebrowsing".equals(url)) { setTitle(null); @@ -1027,6 +1172,391 @@ public class BrowserToolbar extends GeckoRelativeLayout } } + public void finishTabsAnimation(boolean tabsAreShown) { + if (tabsAreShown) { + return; + } + + PropertyAnimator animator = new PropertyAnimator(150); + + animator.attach(mTabsCounter, + PropertyAnimator.Property.ALPHA, + 1.0f); + + if (mHasSoftMenuButton && !HardwareUtils.isTablet()) { + animator.attach(mMenuIcon, + PropertyAnimator.Property.ALPHA, + 1.0f); + } + + animator.start(); + } + + public void setOnActivateListener(OnActivateListener listener) { + mActivateListener = listener; + } + + public void setOnCommitListener(OnCommitListener listener) { + mCommitListener = listener; + } + + public void setOnDismissListener(OnDismissListener listener) { + mDismissListener = listener; + } + + public void setOnFilterListener(OnFilterListener listener) { + mFilterListener = listener; + } + + private void showSoftInput() { + InputMethodManager imm = + (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mUrlEditText, InputMethodManager.SHOW_IMPLICIT); + } + + private void showUrlEditContainer() { + setUrlEditContainerVisibility(true, null); + } + + private void showUrlEditContainer(PropertyAnimator animator) { + setUrlEditContainerVisibility(true, animator); + } + + private void hideUrlEditContainer() { + setUrlEditContainerVisibility(false, null); + } + + private void hideUrlEditContainer(PropertyAnimator animator) { + setUrlEditContainerVisibility(false, animator); + } + + private void setUrlEditContainerVisibility(final boolean showEditContainer, PropertyAnimator animator) { + final View viewToShow = (showEditContainer ? mUrlEditContainer : mUrlDisplayContainer); + final View viewToHide = (showEditContainer ? mUrlDisplayContainer : mUrlEditContainer); + + if (animator == null) { + viewToHide.setVisibility(View.GONE); + viewToShow.setVisibility(View.VISIBLE); + + if (showEditContainer) { + mUrlEditText.requestFocus(); + showSoftInput(); + } + + return; + } + + ViewHelper.setAlpha(viewToShow, 0.0f); + animator.attach(viewToShow, + PropertyAnimator.Property.ALPHA, + 1.0f); + + animator.attach(viewToHide, + PropertyAnimator.Property.ALPHA, + 0.0f); + + animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + @Override + public void onPropertyAnimationStart() { + viewToShow.setVisibility(View.VISIBLE); + + if (showEditContainer) { + ViewHelper.setAlpha(mGo, 0.0f); + mUrlEditText.requestFocus(); + } + } + + @Override + public void onPropertyAnimationEnd() { + viewToHide.setVisibility(View.GONE); + ViewHelper.setAlpha(viewToHide, 1.0f); + + if (showEditContainer) { + ViewHelper.setAlpha(mGo, 1.0f); + showSoftInput(); + } + } + }); + } + + /** + * Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new + * tab button). Note that selection state is independent of editing mode. + */ + public boolean isEditing() { + return mIsEditing; + } + + public void startEditing(String url, PropertyAnimator animator) { + if (isEditing()) { + return; + } + + mUrlEditText.setText(url != null ? url : ""); + mIsEditing = true; + + final int entryTranslation = getUrlBarEntryTranslation(); + final int curveTranslation = getUrlBarCurveTranslation(); + + // This animation doesn't make much sense in a sidebar UI + if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) { + showUrlEditContainer(); + + if (!HardwareUtils.isTablet()) { + if (mUrlBarRightEdge != null) { + ViewHelper.setTranslationX(mUrlBarRightEdge, entryTranslation); + } + + ViewHelper.setTranslationX(mTabs, curveTranslation); + ViewHelper.setTranslationX(mTabsCounter, curveTranslation); + ViewHelper.setTranslationX(mActionItemBar, curveTranslation); + + if (mHasSoftMenuButton) { + ViewHelper.setTranslationX(mMenu, curveTranslation); + ViewHelper.setTranslationX(mMenuIcon, curveTranslation); + } + } + + return; + } + + if (mAnimatingEntry) + return; + + // Highlight the toolbar from the start of the animation. + setSelected(true); + + // Hide page actions/stop buttons immediately + ViewHelper.setAlpha(mPageActionLayout, 0); + ViewHelper.setAlpha(mStop, 0); + + // Slide the right side elements of the toolbar + + if (mUrlBarRightEdge != null) { + animator.attach(mUrlBarRightEdge, + PropertyAnimator.Property.TRANSLATION_X, + entryTranslation); + } + + animator.attach(mTabs, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + animator.attach(mTabsCounter, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + animator.attach(mActionItemBar, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + + if (mHasSoftMenuButton) { + animator.attach(mMenu, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + + animator.attach(mMenuIcon, + PropertyAnimator.Property.TRANSLATION_X, + curveTranslation); + } + + showUrlEditContainer(animator); + + animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + @Override + public void onPropertyAnimationStart() { + } + + @Override + public void onPropertyAnimationEnd() { + mAnimatingEntry = false; + } + }); + + mAnimatingEntry = true; + } + + /** + * Exits edit mode without updating the toolbar title. + * + * @return the url that was entered + */ + public String cancelEdit() { + return stopEditing(); + } + + /** + * Exits edit mode, updating the toolbar title with the url that was just entered. + * + * @return the url that was entered + */ + public String commitEdit() { + final String url = stopEditing(); + if (!TextUtils.isEmpty(url)) { + setTitle(url); + } + return url; + } + + private String stopEditing() { + final String url = mUrlEditText.getText().toString(); + if (!isEditing()) { + return url; + } + mIsEditing = false; + + if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) { + hideUrlEditContainer(); + updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount()); + + if (!HardwareUtils.isTablet()) { + if (mUrlBarRightEdge != null) { + ViewHelper.setTranslationX(mUrlBarRightEdge, 0); + } + + ViewHelper.setTranslationX(mTabs, 0); + ViewHelper.setTranslationX(mTabsCounter, 0); + ViewHelper.setTranslationX(mActionItemBar, 0); + + if (mHasSoftMenuButton) { + ViewHelper.setTranslationX(mMenu, 0); + ViewHelper.setTranslationX(mMenuIcon, 0); + } + } + + return url; + } + + final PropertyAnimator contentAnimator = new PropertyAnimator(250); + contentAnimator.setUseHardwareLayer(false); + + // Shrink the urlbar entry back to its original size + + if (mUrlBarRightEdge != null) { + contentAnimator.attach(mUrlBarRightEdge, + PropertyAnimator.Property.TRANSLATION_X, + 0); + } + + contentAnimator.attach(mTabs, + PropertyAnimator.Property.TRANSLATION_X, + 0); + contentAnimator.attach(mTabsCounter, + PropertyAnimator.Property.TRANSLATION_X, + 0); + contentAnimator.attach(mActionItemBar, + PropertyAnimator.Property.TRANSLATION_X, + 0); + + if (mHasSoftMenuButton) { + contentAnimator.attach(mMenu, + PropertyAnimator.Property.TRANSLATION_X, + 0); + + contentAnimator.attach(mMenuIcon, + PropertyAnimator.Property.TRANSLATION_X, + 0); + } + + hideUrlEditContainer(contentAnimator); + + contentAnimator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + @Override + public void onPropertyAnimationStart() { + } + + @Override + public void onPropertyAnimationEnd() { + setShadowVisibility(true); + + PropertyAnimator buttonsAnimator = new PropertyAnimator(300); + + // Fade toolbar buttons (page actions, stop) after the entry + // is schrunk back to its original size. + buttonsAnimator.attach(mPageActionLayout, + PropertyAnimator.Property.ALPHA, + 1); + buttonsAnimator.attach(mStop, + PropertyAnimator.Property.ALPHA, + 1); + + buttonsAnimator.start(); + + mAnimatingEntry = false; + + // Trigger animation to update the tabs counter once the + // tabs button is back on screen. + updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount()); + } + }); + + mAnimatingEntry = true; + contentAnimator.start(); + + return url; + } + + private void updateGoButton(String text) { + if (text.length() == 0) { + mGo.setVisibility(View.GONE); + return; + } + + mGo.setVisibility(View.VISIBLE); + + int imageResource = R.drawable.ic_url_bar_go; + String contentDescription = mActivity.getString(R.string.go); + int imeAction = EditorInfo.IME_ACTION_GO; + + int actionBits = mUrlEditText.getImeOptions() & EditorInfo.IME_MASK_ACTION; + if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) { + imageResource = R.drawable.ic_url_bar_search; + contentDescription = mActivity.getString(R.string.search); + imeAction = EditorInfo.IME_ACTION_SEARCH; + } + + InputMethodManager imm = InputMethods.getInputMethodManager(mUrlEditText.getContext()); + if (imm == null) { + return; + } + boolean restartInput = false; + if (actionBits != imeAction) { + int optionBits = mUrlEditText.getImeOptions() & ~EditorInfo.IME_MASK_ACTION; + mUrlEditText.setImeOptions(optionBits | imeAction); + + mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) && + (InputMethods.shouldDelayUrlBarUpdate(mUrlEditText.getContext())); + if (!mDelayRestartInput) { + restartInput = true; + } + } else if (mDelayRestartInput) { + // Only call delayed restartInput when actionBits == imeAction + // so if there are two restarts in a row, the first restarts will + // be discarded and the second restart will be properly delayed + mDelayRestartInput = false; + restartInput = true; + } + if (restartInput) { + updateKeyboardInputType(); + imm.restartInput(mUrlEditText); + mGo.setImageResource(imageResource); + mGo.setContentDescription(contentDescription); + } + } + + private void updateKeyboardInputType() { + // If the user enters a space, then we know they are entering search terms, not a URL. + // We can then switch to text mode so, + // 1) the IME auto-inserts spaces between words + // 2) the IME doesn't reset input keyboard to Latin keyboard. + String text = mUrlEditText.getText().toString(); + int currentInputType = mUrlEditText.getInputType(); + int newInputType = StringUtils.isSearchQuery(text, false) + ? (currentInputType & ~InputType.TYPE_TEXT_VARIATION_URI) // Text mode + : (currentInputType | InputType.TYPE_TEXT_VARIATION_URI); // URL mode + if (newInputType != currentInputType) { + mUrlEditText.setRawInputType(newInputType); + } + } + public void updateBackButton(boolean enabled) { Drawable drawable = mBack.getDrawable(); if (drawable != null) @@ -1050,7 +1580,7 @@ public class BrowserToolbar extends GeckoRelativeLayout mForwardAnim = new PropertyAnimator(mSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION); final int width = mForward.getWidth() / 2; - mForwardAnim.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + mForwardAnim.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { @Override public void onPropertyAnimationStart() { if (!enabled) { @@ -1059,7 +1589,12 @@ public class BrowserToolbar extends GeckoRelativeLayout ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)mUrlDisplayContainer.getLayoutParams(); layoutParams.leftMargin = 0; - mUrlDisplayContainer.requestLayout(); + + // Do the same on the URL edit container + layoutParams = (ViewGroup.MarginLayoutParams)mUrlEditContainer.getLayoutParams(); + layoutParams.leftMargin = 0; + + requestLayout(); // Note, we already translated the favicon, site security, and text field // in prepareForwardAnimation, so they should appear to have not moved at // all at this point. @@ -1071,7 +1606,10 @@ public class BrowserToolbar extends GeckoRelativeLayout if (enabled) { ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)mUrlDisplayContainer.getLayoutParams(); - layoutParams.leftMargin = mAddressBarViewOffset; + layoutParams.leftMargin = mUrlBarViewOffset; + + layoutParams = (ViewGroup.MarginLayoutParams)mUrlEditContainer.getLayoutParams(); + layoutParams.leftMargin = mUrlBarViewOffset; ViewHelper.setTranslationX(mTitle, 0); ViewHelper.setTranslationX(mFavicon, 0); @@ -1083,7 +1621,7 @@ public class BrowserToolbar extends GeckoRelativeLayout layoutParams.leftMargin = mDefaultForwardMargin + (mForward.isEnabled() ? width : 0); ViewHelper.setTranslationX(mForward, 0); - mUrlDisplayContainer.requestLayout(); + requestLayout(); mForwardAnim = null; } }); @@ -1113,9 +1651,9 @@ public class BrowserToolbar extends GeckoRelativeLayout // We're hiding the forward button. We're going to reset the margin before // the animation starts, so we shift these items to the right so that they don't // appear to move initially. - ViewHelper.setTranslationX(mTitle, mAddressBarViewOffset); - ViewHelper.setTranslationX(mFavicon, mAddressBarViewOffset); - ViewHelper.setTranslationX(mSiteSecurity, mAddressBarViewOffset); + ViewHelper.setTranslationX(mTitle, mUrlBarViewOffset); + ViewHelper.setTranslationX(mFavicon, mUrlBarViewOffset); + ViewHelper.setTranslationX(mSiteSecurity, mUrlBarViewOffset); } else { anim.attach(mForward, PropertyAnimator.Property.TRANSLATION_X, @@ -1125,13 +1663,13 @@ public class BrowserToolbar extends GeckoRelativeLayout 1); anim.attach(mTitle, PropertyAnimator.Property.TRANSLATION_X, - mAddressBarViewOffset); + mUrlBarViewOffset); anim.attach(mFavicon, PropertyAnimator.Property.TRANSLATION_X, - mAddressBarViewOffset); + mUrlBarViewOffset); anim.attach(mSiteSecurity, PropertyAnimator.Property.TRANSLATION_X, - mAddressBarViewOffset); + mUrlBarViewOffset); } } @@ -1166,12 +1704,13 @@ public class BrowserToolbar extends GeckoRelativeLayout updateForwardButton(tab.canDoForward()); final boolean isPrivate = tab.isPrivate(); - mAddressBarBg.setPrivateMode(isPrivate); + mUrlBarBackground.setPrivateMode(isPrivate); setPrivateMode(isPrivate); mTabs.setPrivateMode(isPrivate); mTitle.setPrivateMode(isPrivate); mMenu.setPrivateMode(isPrivate); mMenuIcon.setPrivateMode(isPrivate); + mUrlEditText.setPrivateMode(isPrivate); if (mBack instanceof BackButton) ((BackButton) mBack).setPrivateMode(isPrivate); diff --git a/mobile/android/base/BrowserToolbarBackground.java b/mobile/android/base/BrowserToolbarBackground.java index e56a0689dbfa..26079ee9652d 100644 --- a/mobile/android/base/BrowserToolbarBackground.java +++ b/mobile/android/base/BrowserToolbarBackground.java @@ -37,6 +37,6 @@ public class BrowserToolbarBackground extends GeckoLinearLayout { @Override public void onLightweightThemeReset() { - setBackgroundResource(R.drawable.address_bar_bg); + setBackgroundResource(R.drawable.url_bar_bg); } } diff --git a/mobile/android/base/Favicons.java b/mobile/android/base/Favicons.java index 473580cfd12e..0f6915b3af7d 100644 --- a/mobile/android/base/Favicons.java +++ b/mobile/android/base/Favicons.java @@ -243,10 +243,10 @@ public class Favicons { public void attachToContext(Context context) { mContext = context; if (sFaviconSmallSize < 0) { - sFaviconSmallSize = Math.round(mContext.getResources().getDimension(R.dimen.awesomebar_row_favicon_size_small)); + sFaviconSmallSize = Math.round(mContext.getResources().getDimension(R.dimen.favicon_size_small)); } if (sFaviconLargeSize < 0) { - sFaviconLargeSize = Math.round(mContext.getResources().getDimension(R.dimen.awesomebar_row_favicon_size_large)); + sFaviconLargeSize = Math.round(mContext.getResources().getDimension(R.dimen.favicon_size_large)); } } diff --git a/mobile/android/base/ForwardButton.java b/mobile/android/base/ForwardButton.java index 44e1435de6b6..a27db2054465 100644 --- a/mobile/android/base/ForwardButton.java +++ b/mobile/android/base/ForwardButton.java @@ -66,7 +66,7 @@ public class ForwardButton extends ShapedButton { canvas.drawPath(mBorderPath, isPrivateMode() ? mBorderPrivatePaint : mBorderPaint); } - // The drawable is constructed as per @drawable/address_bar_nav_button. + // The drawable is constructed as per @drawable/url_bar_nav_button. @Override public void onLightweightThemeChanged() { Drawable drawable = mActivity.getLightweightTheme().getDrawable(this); @@ -88,6 +88,6 @@ public class ForwardButton extends ShapedButton { @Override public void onLightweightThemeReset() { - setBackgroundResource(R.drawable.address_bar_nav_button); + setBackgroundResource(R.drawable.url_bar_nav_button); } } diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 1d2e5a288dc8..4a9be3ea931a 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -567,8 +567,6 @@ abstract public class GeckoApp } else if (event.equals("Reader:FaviconRequest")) { final String url = message.getString("url"); handleFaviconRequest(url); - } else if (event.equals("Reader:GoToReadingList")) { - showReadingList(); } else if (event.equals("Gecko:Ready")) { mGeckoReadyStartupTimer.stop(); geckoConnected(); @@ -1479,7 +1477,6 @@ abstract public class GeckoApp registerEventListener("Reader:Removed"); registerEventListener("Reader:Share"); registerEventListener("Reader:FaviconRequest"); - registerEventListener("Reader:GoToReadingList"); registerEventListener("onCameraCapture"); registerEventListener("Menu:Add"); registerEventListener("Menu:Remove"); @@ -2028,7 +2025,6 @@ abstract public class GeckoApp unregisterEventListener("Reader:Removed"); unregisterEventListener("Reader:Share"); unregisterEventListener("Reader:FaviconRequest"); - unregisterEventListener("Reader:GoToReadingList"); unregisterEventListener("onCameraCapture"); unregisterEventListener("Menu:Add"); unregisterEventListener("Menu:Remove"); @@ -2261,18 +2257,13 @@ abstract public class GeckoApp return mPromptService; } - public void showReadingList() { - Intent intent = new Intent(getBaseContext(), AwesomeBar.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.putExtra(AwesomeBar.TARGET_KEY, AwesomeBar.Target.CURRENT_TAB.toString()); - intent.putExtra(AwesomeBar.READING_LIST_KEY, true); - - int requestCode = GeckoAppShell.sActivityHelper.makeRequestCodeForAwesomebar(); - startActivityForResult(intent, requestCode); - } - @Override public void onBackPressed() { + if (getSupportFragmentManager().getBackStackEntryCount() > 0) { + super.onBackPressed(); + return; + } + if (autoHideTabs()) { return; } diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 381a2236fbef..4fa8e24f79f3 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -697,8 +697,8 @@ public class GeckoAppShell createShortcut(aTitle, aURI, aURI, aIconData, aType); } - // internal, for non-webapps - static void createShortcut(String aTitle, String aURI, Bitmap aBitmap, String aType) { + // for non-webapps + public static void createShortcut(String aTitle, String aURI, Bitmap aBitmap, String aType) { createShortcut(aTitle, aURI, aURI, aBitmap, aType); } @@ -1069,12 +1069,12 @@ public class GeckoAppShell * @param title the title to use in ACTION_SEND intents. * @return true if the activity started successfully; false otherwise. */ - static boolean openUriExternal(String targetURI, - String mimeType, - String packageName, - String className, - String action, - String title) { + public static boolean openUriExternal(String targetURI, + String mimeType, + String packageName, + String className, + String action, + String title) { final Context context = getContext(); final Intent intent = getOpenURIIntent(context, targetURI, mimeType, action, title); diff --git a/mobile/android/base/InputMethods.java b/mobile/android/base/InputMethods.java index be20949ff35e..f191bd8f3e29 100644 --- a/mobile/android/base/InputMethods.java +++ b/mobile/android/base/InputMethods.java @@ -61,7 +61,7 @@ final class InputMethods { return METHOD_HTC_TOUCH_INPUT.equals(inputMethod); } - public static boolean shouldDelayAwesomebarUpdate(Context context) { + public static boolean shouldDelayUrlBarUpdate(Context context) { String inputMethod = getCurrentInputMethod(context); return METHOD_SAMSUNG.equals(inputMethod) || METHOD_SWIFTKEY.equals(inputMethod); diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 68fced083368..ea4961e88f44 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -54,18 +54,12 @@ FENNEC_JAVA_FILES = \ AndroidImportPreference.java \ AnimatedHeightLayout.java \ AppNotificationClient.java \ - AwesomeBar.java \ - AwesomebarResultHandler.java \ - AwesomeBarTabs.java \ + AutocompleteHandler.java \ animation/AnimatorProxy.java \ animation/HeightChangeAnimation.java \ animation/PropertyAnimator.java \ animation/Rotate3DAnimation.java \ animation/ViewHelper.java \ - awesomebar/AwesomeBarTab.java \ - awesomebar/AllPagesTab.java \ - awesomebar/BookmarksTab.java \ - awesomebar/HistoryTab.java \ BackButton.java \ BrowserApp.java \ BrowserToolbar.java \ @@ -138,7 +132,6 @@ FENNEC_JAVA_FILES = \ PromptInput.java \ PromptService.java \ Restarter.java \ - SearchEngine.java \ sqlite/ByteBufferInputStream.java \ sqlite/MatrixBlobCursor.java \ sqlite/SQLiteBridge.java \ @@ -146,7 +139,6 @@ FENNEC_JAVA_FILES = \ ReaderModeUtils.java \ RemoteTabs.java \ RobocopAPI.java \ - SearchEngineRow.java \ ServiceNotificationClient.java \ ScrollAnimator.java \ SessionParser.java \ @@ -154,7 +146,6 @@ FENNEC_JAVA_FILES = \ SharedPreferencesHelper.java \ SiteIdentityPopup.java \ SmsManager.java \ - SuggestClient.java \ SurfaceBits.java \ SyncPreference.java \ Tab.java \ @@ -224,6 +215,36 @@ FENNEC_JAVA_FILES = \ gfx/TouchEventHandler.java \ gfx/ViewTransform.java \ gfx/VirtualLayer.java \ + home/BookmarksListAdapter.java \ + home/BookmarksListView.java \ + home/BookmarksPage.java \ + home/BookmarkFolderView.java \ + home/BookmarkThumbnailView.java \ + home/BrowserSearch.java \ + home/HistoryPage.java \ + home/HomeCursorLoaderCallbacks.java \ + home/HomeFragment.java \ + home/HomeListView.java \ + home/HomePager.java \ + home/HomePagerTabStrip.java \ + home/FadedTextView.java \ + home/FaviconsLoader.java \ + home/LastTabsPage.java \ + home/MostRecentPage.java \ + home/MostVisitedPage.java \ + home/MultiTypeCursorAdapter.java \ + home/PinBookmarkDialog.java \ + home/ReadingListPage.java \ + home/SearchEngine.java \ + home/SearchEngineRow.java \ + home/SearchLoader.java \ + home/SimpleCursorLoader.java \ + home/SuggestClient.java \ + home/TabMenuStrip.java \ + home/TopBookmarkItemView.java \ + home/TopBookmarksAdapter.java \ + home/TopBookmarksView.java \ + home/TwoLinePageRow.java \ menu/GeckoMenu.java \ menu/GeckoMenuInflater.java \ menu/GeckoMenuItem.java \ @@ -235,11 +256,7 @@ FENNEC_JAVA_FILES = \ menu/MenuPopup.java \ preferences/SearchPreferenceCategory.java \ preferences/SearchEnginePreference.java \ - widget/AboutHome.java \ - widget/AboutHomeView.java \ - widget/AboutHomeSection.java \ widget/ActivityChooserModel.java \ - widget/AddonsSection.java \ widget/ButtonToast.java \ widget/ArrowPopup.java \ widget/DateTimePicker.java \ @@ -248,11 +265,6 @@ FENNEC_JAVA_FILES = \ widget/GeckoPopupMenu.java \ widget/GeckoActionProvider.java \ widget/IconTabWidget.java \ - widget/LastTabsSection.java \ - widget/LinkTextView.java \ - widget/PromoBox.java \ - widget/RemoteTabsSection.java \ - widget/TopSitesView.java \ widget/TabRow.java \ widget/ThumbnailView.java \ widget/TwoWayView.java \ @@ -441,20 +453,13 @@ endif RES_LAYOUT = \ $(SYNC_RES_LAYOUT) \ - res/layout/abouthome_content.xml \ res/layout/arrow_popup.xml \ res/layout/autocomplete_list.xml \ res/layout/autocomplete_list_item.xml \ - res/layout/awesomebar.xml \ - res/layout/awesomebar_folder_row.xml \ - res/layout/awesomebar_header_row.xml \ - res/layout/awesomebar_allpages_list.xml \ - res/layout/awesomebar_row.xml \ - res/layout/awesomebar_search.xml \ - res/layout/awesomebar_suggestion_prompt.xml \ - res/layout/awesomebar_tab_indicator.xml \ - res/layout/awesomebar_tabs.xml \ res/layout/bookmark_edit.xml \ + res/layout/bookmark_folder_row.xml \ + res/layout/bookmark_item_row.xml \ + res/layout/browser_search.xml \ res/layout/browser_toolbar.xml \ res/layout/datetime_picker.xml \ res/layout/doorhanger.xml \ @@ -462,7 +467,21 @@ RES_LAYOUT = \ res/layout/find_in_page_content.xml \ res/layout/font_size_preference.xml \ res/layout/gecko_app.xml \ + res/layout/home_bookmarks_page.xml \ + res/layout/home_empty_page.xml \ + res/layout/home_empty_reading_page.xml \ + res/layout/home_item_row.xml \ + res/layout/home_header_row.xml \ + res/layout/home_history_page.xml \ + res/layout/home_history_tabs_indicator.xml \ + res/layout/home_last_tabs_page.xml \ + res/layout/home_history_list.xml \ + res/layout/home_most_recent_page.xml \ + res/layout/home_most_visited_page.xml \ + res/layout/home_pager.xml \ + res/layout/home_reading_list_page.xml \ res/layout/home_search_item_row.xml \ + res/layout/home_suggestion_prompt.xml \ res/layout/web_app.xml \ res/layout/launch_app_list.xml \ res/layout/launch_app_listitem.xml \ @@ -472,6 +491,7 @@ RES_LAYOUT = \ res/layout/notification_icon_text.xml \ res/layout/notification_progress.xml \ res/layout/notification_progress_text.xml \ + res/layout/pin_bookmark_dialog.xml \ res/layout/preference_rightalign_icon.xml \ res/layout/preference_search_tip.xml \ res/layout/search_engine_row.xml \ @@ -482,6 +502,8 @@ RES_LAYOUT = \ res/layout/suggestion_item.xml \ res/layout/remote_tabs_child.xml \ res/layout/remote_tabs_group.xml \ + res/layout/search_engine_row.xml \ + res/layout/tab_menu_strip.xml \ res/layout/tabs_panel.xml \ res/layout/tabs_counter.xml \ res/layout/tabs_panel_header.xml \ @@ -489,42 +511,41 @@ RES_LAYOUT = \ res/layout/tabs_item_cell.xml \ res/layout/tabs_item_row.xml \ res/layout/text_selection_handles.xml \ + res/layout/top_bookmark_item_view.xml \ + res/layout/two_line_page_row.xml \ res/layout/list_item_header.xml \ res/layout/select_dialog_list.xml \ res/layout/select_dialog_multichoice.xml \ res/layout/select_dialog_singlechoice.xml \ res/layout/simple_dropdown_item_1line.xml \ - res/layout/abouthome_addon_row.xml \ - res/layout/abouthome_last_tabs_row.xml \ - res/layout/abouthome_section.xml \ - res/layout/abouthome_remote_tab_row.xml \ - res/layout/abouthome_topsite_item.xml \ + res/layout/suggestion_item.xml \ res/layout/validation_message.xml \ res/layout/videoplayer.xml \ $(NULL) RES_LAYOUT_LARGE_V11 = \ - res/layout-large-v11/awesomebar_search.xml \ res/layout-large-v11/browser_toolbar.xml \ + res/layout-large-v11/home_pager.xml \ $(NULL) RES_LAYOUT_LARGE_LAND_V11 = \ + res/layout-large-land-v11/home_history_page.xml \ + res/layout-large-land-v11/home_history_tabs_indicator.xml \ + res/layout-large-land-v11/home_history_list.xml \ res/layout-large-land-v11/tabs_panel.xml \ res/layout-large-land-v11/tabs_panel_header.xml \ res/layout-large-land-v11/tabs_panel_footer.xml \ $(NULL) RES_LAYOUT_XLARGE_V11 = \ - res/layout-xlarge-v11/awesomebar_search.xml \ res/layout-xlarge-v11/font_size_preference.xml \ + res/layout-xlarge-v11/home_history_page.xml \ + res/layout-xlarge-v11/home_history_tabs_indicator.xml \ + res/layout-xlarge-v11/home_history_list.xml \ res/layout-xlarge-v11/remote_tabs_child.xml \ res/layout-xlarge-v11/remote_tabs_group.xml \ $(NULL) -RES_LAYOUT_XLARGE_LAND_V11 = \ - res/layout-xlarge-land-v11/abouthome_content.xml \ - $(NULL) - RES_VALUES = \ $(SYNC_RES_VALUES) \ res/values/attrs.xml \ @@ -560,6 +581,7 @@ RES_VALUES_LARGE_V11 = \ $(NULL) RES_VALUES_LARGE_LAND_V11 = \ + res/values-large-land-v11/dimens.xml \ res/values-large-land-v11/styles.xml \ $(NULL) @@ -569,10 +591,19 @@ RES_VALUES_XLARGE_V11 = \ res/values-xlarge-v11/styles.xml \ $(NULL) +RES_VALUES_XLARGE_LAND_V11 = \ + res/values-xlarge-land-v11/dimens.xml \ + res/values-xlarge-land-v11/styles.xml \ + $(NULL) + RES_VALUES_V14 = \ res/values-v14/styles.xml \ $(NULL) +RES_VALUES_V16 = \ + res/values-v16/styles.xml \ + $(NULL) + RES_XML = \ res/xml/preferences_display.xml \ res/xml/preferences_search.xml \ @@ -590,11 +621,8 @@ RES_XML_V11 = \ $(NULL) RES_ANIM = \ - res/anim/awesomebar_fade_in.xml \ res/anim/popup_show.xml \ res/anim/popup_hide.xml \ - res/anim/awesomebar_fade_out.xml \ - res/anim/awesomebar_hold_still.xml \ res/anim/grow_fade_in.xml \ res/anim/grow_fade_in_center.xml \ res/anim/progress_spinner.xml \ @@ -606,17 +634,7 @@ RES_DRAWABLE_MDPI = \ res/drawable-mdpi/blank.png \ res/drawable-mdpi/favicon.png \ res/drawable-mdpi/folder.png \ - res/drawable-mdpi/abouthome_icon.png \ - res/drawable-mdpi/abouthome_logo_dark.png \ - res/drawable-mdpi/abouthome_logo_light.png \ - res/drawable-mdpi/abouthome_promo_box_bg.9.png \ - res/drawable-mdpi/abouthome_promo_box_pressed_bg.9.png \ - res/drawable-mdpi/abouthome_promo_logo_apps.png \ - res/drawable-mdpi/abouthome_promo_logo_sync.png \ res/drawable-mdpi/abouthome_thumbnail.png \ - res/drawable-mdpi/abouthome_thumbnail_bg.png \ - res/drawable-mdpi/abouthome_thumbnail_add.png \ - res/drawable-mdpi/address_bar_bg_shadow.png \ res/drawable-mdpi/alert_addon.png \ res/drawable-mdpi/alert_app.png \ res/drawable-mdpi/alert_download.png \ @@ -625,18 +643,10 @@ RES_DRAWABLE_MDPI = \ res/drawable-mdpi/alert_mic_camera.png \ res/drawable-mdpi/arrow_popup_bg.9.png \ res/drawable-mdpi/autocomplete_list_bg.9.png \ - res/drawable-mdpi/awesomebar_tab_center.9.png \ - res/drawable-mdpi/awesomebar_tab_left.9.png \ - res/drawable-mdpi/awesomebar_tab_right.9.png \ - res/drawable-mdpi/awesomebar_sep_left.9.png \ - res/drawable-mdpi/awesomebar_sep_right.9.png \ + res/drawable-mdpi/bookmark_folder_closed.png \ + res/drawable-mdpi/bookmark_folder_opened.png \ res/drawable-mdpi/desktop_notification.png \ - res/drawable-mdpi/ic_addons_empty.png \ - res/drawable-mdpi/ic_awesomebar_go.png \ - res/drawable-mdpi/ic_awesomebar_reader.png \ - res/drawable-mdpi/ic_awesomebar_search.png \ - res/drawable-mdpi/ic_awesomebar_star.png \ - res/drawable-mdpi/ic_awesomebar_tab.png \ + res/drawable-mdpi/home_tab_menu_strip.9.png \ res/drawable-mdpi/ic_menu_addons_filler.png \ res/drawable-mdpi/ic_menu_bookmark_add.png \ res/drawable-mdpi/ic_menu_bookmark_remove.png \ @@ -648,7 +658,19 @@ RES_DRAWABLE_MDPI = \ res/drawable-mdpi/ic_menu_new_tab.png \ res/drawable-mdpi/ic_menu_reload.png \ res/drawable-mdpi/ic_status_logo.png \ + res/drawable-mdpi/ic_url_bar_go.png \ + res/drawable-mdpi/ic_url_bar_reader.png \ + res/drawable-mdpi/ic_url_bar_search.png \ + res/drawable-mdpi/ic_url_bar_star.png \ + res/drawable-mdpi/ic_url_bar_tab.png \ + res/drawable-mdpi/icon_last_tabs.png \ + res/drawable-mdpi/icon_last_tabs_empty.png \ + res/drawable-mdpi/icon_most_recent.png \ + res/drawable-mdpi/icon_most_recent_empty.png \ + res/drawable-mdpi/icon_most_visited.png \ + res/drawable-mdpi/icon_most_visited_empty.png \ res/drawable-mdpi/icon_pageaction.png \ + res/drawable-mdpi/icon_reading_list_empty.png \ res/drawable-mdpi/progress_spinner.png \ res/drawable-mdpi/tab_indicator_divider.9.png \ res/drawable-mdpi/tab_indicator_selected.9.png \ @@ -663,15 +685,16 @@ RES_DRAWABLE_MDPI = \ res/drawable-mdpi/tab_thumbnail_shadow.png \ res/drawable-mdpi/tabs_count.png \ res/drawable-mdpi/tabs_count_foreground.png \ + res/drawable-mdpi/url_bar_bg_shadow.png \ + res/drawable-mdpi/url_bar_entry_default.9.png \ + res/drawable-mdpi/url_bar_entry_default_pb.9.png \ + res/drawable-mdpi/url_bar_entry_pressed.9.png \ + res/drawable-mdpi/url_bar_entry_pressed_pb.9.png \ res/drawable-mdpi/tip_addsearch.png \ res/drawable-mdpi/toast.9.png \ res/drawable-mdpi/toast_button_focused.9.png \ res/drawable-mdpi/toast_button_pressed.9.png \ res/drawable-mdpi/toast_divider.9.png \ - res/drawable-mdpi/address_bar_url_default.9.png \ - res/drawable-mdpi/address_bar_url_default_pb.9.png \ - res/drawable-mdpi/address_bar_url_pressed.9.png \ - res/drawable-mdpi/address_bar_url_pressed_pb.9.png \ res/drawable-mdpi/find_close.png \ res/drawable-mdpi/find_next.png \ res/drawable-mdpi/find_prev.png \ @@ -692,8 +715,10 @@ RES_DRAWABLE_MDPI = \ res/drawable-mdpi/tabs_normal.png \ res/drawable-mdpi/tabs_private.png \ res/drawable-mdpi/tabs_synced.png \ + res/drawable-mdpi/top_bookmark_add.png \ res/drawable-mdpi/urlbar_stop.png \ res/drawable-mdpi/reader.png \ + res/drawable-mdpi/reader_cropped.png \ res/drawable-mdpi/reader_active.png \ res/drawable-mdpi/reading_list.png \ res/drawable-mdpi/validation_arrow.png \ @@ -708,6 +733,7 @@ RES_DRAWABLE_MDPI = \ res/drawable-mdpi/shadow.png \ res/drawable-mdpi/start.png \ res/drawable-mdpi/marketplace.png \ + res/drawable-mdpi/history_tabs_indicator_selected.9.png \ res/drawable-mdpi/warning.png \ res/drawable-mdpi/warning_doorhanger.png \ $(NULL) @@ -723,35 +749,17 @@ RES_DRAWABLE_HDPI = \ res/drawable-hdpi/folder.png \ res/drawable-hdpi/home_bg.png \ res/drawable-hdpi/home_star.png \ - res/drawable-hdpi/abouthome_icon.png \ - res/drawable-hdpi/abouthome_logo_dark.png \ - res/drawable-hdpi/abouthome_logo_light.png \ - res/drawable-hdpi/abouthome_promo_box_bg.9.png \ - res/drawable-hdpi/abouthome_promo_box_pressed_bg.9.png \ - res/drawable-hdpi/abouthome_promo_logo_apps.png \ - res/drawable-hdpi/abouthome_promo_logo_sync.png \ res/drawable-hdpi/abouthome_thumbnail.png \ - res/drawable-hdpi/abouthome_thumbnail_bg.png \ - res/drawable-hdpi/abouthome_thumbnail_add.png \ - res/drawable-hdpi/address_bar_bg_shadow.png \ res/drawable-hdpi/alert_addon.png \ res/drawable-hdpi/alert_app.png \ res/drawable-hdpi/alert_download.png \ + res/drawable-hdpi/bookmark_folder_closed.png \ + res/drawable-hdpi/bookmark_folder_opened.png \ res/drawable-hdpi/alert_camera.png \ res/drawable-hdpi/alert_mic.png \ res/drawable-hdpi/alert_mic_camera.png \ res/drawable-hdpi/arrow_popup_bg.9.png \ - res/drawable-hdpi/awesomebar_tab_center.9.png \ - res/drawable-hdpi/awesomebar_tab_left.9.png \ - res/drawable-hdpi/awesomebar_tab_right.9.png \ - res/drawable-hdpi/awesomebar_sep_left.9.png \ - res/drawable-hdpi/awesomebar_sep_right.9.png \ - res/drawable-hdpi/ic_addons_empty.png \ - res/drawable-hdpi/ic_awesomebar_go.png \ - res/drawable-hdpi/ic_awesomebar_reader.png \ - res/drawable-hdpi/ic_awesomebar_search.png \ - res/drawable-hdpi/ic_awesomebar_star.png \ - res/drawable-hdpi/ic_awesomebar_tab.png \ + res/drawable-hdpi/home_tab_menu_strip.9.png \ res/drawable-hdpi/ic_menu_addons_filler.png \ res/drawable-hdpi/ic_menu_bookmark_add.png \ res/drawable-hdpi/ic_menu_bookmark_remove.png \ @@ -763,7 +771,19 @@ RES_DRAWABLE_HDPI = \ res/drawable-hdpi/ic_menu_new_tab.png \ res/drawable-hdpi/ic_menu_reload.png \ res/drawable-hdpi/ic_status_logo.png \ + res/drawable-hdpi/ic_url_bar_go.png \ + res/drawable-hdpi/ic_url_bar_reader.png \ + res/drawable-hdpi/ic_url_bar_search.png \ + res/drawable-hdpi/ic_url_bar_star.png \ + res/drawable-hdpi/ic_url_bar_tab.png \ + res/drawable-hdpi/icon_last_tabs.png \ + res/drawable-hdpi/icon_last_tabs_empty.png \ + res/drawable-hdpi/icon_most_recent.png \ + res/drawable-hdpi/icon_most_recent_empty.png \ + res/drawable-hdpi/icon_most_visited.png \ + res/drawable-hdpi/icon_most_visited_empty.png \ res/drawable-hdpi/icon_pageaction.png \ + res/drawable-hdpi/icon_reading_list_empty.png \ res/drawable-hdpi/tab_indicator_divider.9.png \ res/drawable-hdpi/tab_indicator_selected.9.png \ res/drawable-hdpi/tab_indicator_selected_focused.9.png \ @@ -777,11 +797,12 @@ RES_DRAWABLE_HDPI = \ res/drawable-hdpi/tab_thumbnail_shadow.png \ res/drawable-hdpi/tabs_count.png \ res/drawable-hdpi/tabs_count_foreground.png \ + res/drawable-hdpi/url_bar_bg_shadow.png \ + res/drawable-hdpi/url_bar_entry_default.9.png \ + res/drawable-hdpi/url_bar_entry_default_pb.9.png \ + res/drawable-hdpi/url_bar_entry_pressed.9.png \ + res/drawable-hdpi/url_bar_entry_pressed_pb.9.png \ res/drawable-hdpi/tip_addsearch.png \ - res/drawable-hdpi/address_bar_url_default.9.png \ - res/drawable-hdpi/address_bar_url_default_pb.9.png \ - res/drawable-hdpi/address_bar_url_pressed.9.png \ - res/drawable-hdpi/address_bar_url_pressed_pb.9.png \ res/drawable-hdpi/find_close.png \ res/drawable-hdpi/find_next.png \ res/drawable-hdpi/find_prev.png \ @@ -802,8 +823,10 @@ RES_DRAWABLE_HDPI = \ res/drawable-hdpi/tabs_normal.png \ res/drawable-hdpi/tabs_private.png \ res/drawable-hdpi/tabs_synced.png \ + res/drawable-hdpi/top_bookmark_add.png \ res/drawable-hdpi/urlbar_stop.png \ res/drawable-hdpi/reader.png \ + res/drawable-hdpi/reader_cropped.png \ res/drawable-hdpi/reader_active.png \ res/drawable-hdpi/reading_list.png \ res/drawable-hdpi/validation_arrow.png \ @@ -812,6 +835,7 @@ RES_DRAWABLE_HDPI = \ res/drawable-hdpi/handle_end.png \ res/drawable-hdpi/handle_middle.png \ res/drawable-hdpi/handle_start.png \ + res/drawable-hdpi/history_tabs_indicator_selected.9.png \ res/drawable-hdpi/warning.png \ res/drawable-hdpi/warning_doorhanger.png \ $(NULL) @@ -820,39 +844,22 @@ RES_DRAWABLE_XHDPI = \ res/drawable-xhdpi/blank.png \ res/drawable-xhdpi/favicon.png \ res/drawable-xhdpi/folder.png \ - res/drawable-xhdpi/abouthome_icon.png \ - res/drawable-xhdpi/abouthome_logo_dark.png \ - res/drawable-xhdpi/abouthome_logo_light.png \ - res/drawable-xhdpi/abouthome_promo_box_bg.9.png \ - res/drawable-xhdpi/abouthome_promo_box_pressed_bg.9.png \ - res/drawable-xhdpi/abouthome_promo_logo_apps.png \ - res/drawable-xhdpi/abouthome_promo_logo_sync.png \ res/drawable-xhdpi/abouthome_thumbnail.png \ - res/drawable-xhdpi/abouthome_thumbnail_bg.png \ - res/drawable-xhdpi/abouthome_thumbnail_add.png \ - res/drawable-xhdpi/address_bar_bg_shadow.png \ - res/drawable-xhdpi/address_bar_url_default.9.png \ - res/drawable-xhdpi/address_bar_url_default_pb.9.png \ - res/drawable-xhdpi/address_bar_url_pressed.9.png \ - res/drawable-xhdpi/address_bar_url_pressed_pb.9.png \ + res/drawable-xhdpi/url_bar_bg_shadow.png \ + res/drawable-xhdpi/url_bar_entry_default.9.png \ + res/drawable-xhdpi/url_bar_entry_default_pb.9.png \ + res/drawable-xhdpi/url_bar_entry_pressed.9.png \ + res/drawable-xhdpi/url_bar_entry_pressed_pb.9.png \ res/drawable-xhdpi/alert_addon.png \ res/drawable-xhdpi/alert_app.png \ res/drawable-xhdpi/alert_download.png \ + res/drawable-xhdpi/bookmark_folder_closed.png \ + res/drawable-xhdpi/bookmark_folder_opened.png \ res/drawable-xhdpi/alert_camera.png \ res/drawable-xhdpi/alert_mic.png \ res/drawable-xhdpi/alert_mic_camera.png \ res/drawable-xhdpi/arrow_popup_bg.9.png \ - res/drawable-xhdpi/awesomebar_tab_center.9.png \ - res/drawable-xhdpi/awesomebar_tab_left.9.png \ - res/drawable-xhdpi/awesomebar_tab_right.9.png \ - res/drawable-xhdpi/awesomebar_sep_left.9.png \ - res/drawable-xhdpi/awesomebar_sep_right.9.png \ - res/drawable-xhdpi/ic_addons_empty.png \ - res/drawable-xhdpi/ic_awesomebar_go.png \ - res/drawable-xhdpi/ic_awesomebar_reader.png \ - res/drawable-xhdpi/ic_awesomebar_search.png \ - res/drawable-xhdpi/ic_awesomebar_star.png \ - res/drawable-xhdpi/ic_awesomebar_tab.png \ + res/drawable-xhdpi/home_tab_menu_strip.9.png \ res/drawable-xhdpi/ic_menu_addons_filler.png \ res/drawable-xhdpi/ic_menu_bookmark_add.png \ res/drawable-xhdpi/ic_menu_bookmark_remove.png \ @@ -864,7 +871,19 @@ RES_DRAWABLE_XHDPI = \ res/drawable-xhdpi/ic_menu_new_tab.png \ res/drawable-xhdpi/ic_menu_reload.png \ res/drawable-xhdpi/ic_status_logo.png \ + res/drawable-xhdpi/ic_url_bar_go.png \ + res/drawable-xhdpi/ic_url_bar_reader.png \ + res/drawable-xhdpi/ic_url_bar_search.png \ + res/drawable-xhdpi/ic_url_bar_star.png \ + res/drawable-xhdpi/ic_url_bar_tab.png \ + res/drawable-xhdpi/icon_last_tabs.png \ + res/drawable-xhdpi/icon_last_tabs_empty.png \ + res/drawable-xhdpi/icon_most_recent.png \ + res/drawable-xhdpi/icon_most_recent_empty.png \ + res/drawable-xhdpi/icon_most_visited.png \ + res/drawable-xhdpi/icon_most_visited_empty.png \ res/drawable-xhdpi/icon_pageaction.png \ + res/drawable-xhdpi/icon_reading_list_empty.png \ res/drawable-xhdpi/spinner_default.9.png \ res/drawable-xhdpi/spinner_focused.9.png \ res/drawable-xhdpi/spinner_pressed.9.png \ @@ -879,8 +898,10 @@ RES_DRAWABLE_XHDPI = \ res/drawable-xhdpi/find_close.png \ res/drawable-xhdpi/find_next.png \ res/drawable-xhdpi/find_prev.png \ + res/drawable-xhdpi/top_bookmark_add.png \ res/drawable-xhdpi/urlbar_stop.png \ res/drawable-xhdpi/reader.png \ + res/drawable-xhdpi/reader_cropped.png \ res/drawable-xhdpi/reader_active.png \ res/drawable-xhdpi/reading_list.png \ res/drawable-xhdpi/larry.png \ @@ -909,6 +930,7 @@ RES_DRAWABLE_XHDPI = \ res/drawable-xhdpi/handle_end.png \ res/drawable-xhdpi/handle_middle.png \ res/drawable-xhdpi/handle_start.png \ + res/drawable-xhdpi/history_tabs_indicator_selected.9.png \ res/drawable-xhdpi/warning.png \ res/drawable-xhdpi/warning_doorhanger.png \ $(NULL) @@ -1000,6 +1022,10 @@ RES_DRAWABLE_XHDPI_V11 = \ res/drawable-xhdpi-v11/ic_status_logo.png \ $(NULL) +RES_DRAWABLE_LARGE_LAND_V11 = \ + res/drawable-large-land-v11/home_history_tabs_indicator.xml \ + $(NULL) + RES_DRAWABLE_LARGE_MDPI_V11 = \ res/drawable-large-mdpi-v11/arrow_popup_bg.9.png \ res/drawable-large-mdpi-v11/ic_menu_reload.png \ @@ -1021,42 +1047,26 @@ RES_DRAWABLE_LARGE_XHDPI_V11 = \ res/drawable-large-xhdpi-v11/menu.png \ $(NULL) +RES_DRAWABLE_XLARGE_V11 = \ + res/drawable-xlarge-v11/home_history_tabs_indicator.xml \ + $(NULL) + RES_DRAWABLE_XLARGE_MDPI_V11 = \ - res/drawable-xlarge-mdpi-v11/awesomebar_tab_center.9.png \ - res/drawable-xlarge-mdpi-v11/awesomebar_tab_left.9.png \ - res/drawable-xlarge-mdpi-v11/awesomebar_tab_right.9.png \ - res/drawable-xlarge-mdpi-v11/awesomebar_sep_left.9.png \ - res/drawable-xlarge-mdpi-v11/awesomebar_sep_right.9.png \ res/drawable-xlarge-mdpi-v11/ic_menu_bookmark_add.png \ res/drawable-xlarge-mdpi-v11/ic_menu_bookmark_remove.png \ $(NULL) RES_DRAWABLE_XLARGE_HDPI_V11 = \ - res/drawable-xlarge-hdpi-v11/awesomebar_tab_center.9.png \ - res/drawable-xlarge-hdpi-v11/awesomebar_tab_left.9.png \ - res/drawable-xlarge-hdpi-v11/awesomebar_tab_right.9.png \ - res/drawable-xlarge-hdpi-v11/awesomebar_sep_left.9.png \ - res/drawable-xlarge-hdpi-v11/awesomebar_sep_right.9.png \ res/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png \ res/drawable-xlarge-hdpi-v11/ic_menu_bookmark_remove.png \ $(NULL) RES_DRAWABLE_XLARGE_XHDPI_V11 = \ - res/drawable-xlarge-xhdpi-v11/awesomebar_tab_center.9.png \ - res/drawable-xlarge-xhdpi-v11/awesomebar_tab_left.9.png \ - res/drawable-xlarge-xhdpi-v11/awesomebar_tab_right.9.png \ - res/drawable-xlarge-xhdpi-v11/awesomebar_sep_left.9.png \ - res/drawable-xlarge-xhdpi-v11/awesomebar_sep_right.9.png \ res/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png \ res/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_remove.png \ $(NULL) RES_COLOR = \ - res/color/abouthome_section_more_text.xml \ - res/color/abouthome_section_subtitle.xml \ - res/color/abouthome_section_title.xml \ - res/color/awesome_bar_title.xml \ - res/color/awesome_bar_title_hint.xml \ res/color/primary_text.xml \ res/color/primary_text_inverse.xml \ res/color/secondary_text.xml \ @@ -1064,14 +1074,17 @@ RES_COLOR = \ res/color/select_item_multichoice.xml \ res/color/tertiary_text.xml \ res/color/tertiary_text_inverse.xml \ + res/color/top_bookmark_item_title.xml \ + res/color/url_bar_title.xml \ + res/color/url_bar_title_hint.xml \ $(NULL) RES_MENU = \ - res/menu/abouthome_topsites_contextmenu.xml \ - res/menu/awesomebar_contextmenu.xml \ res/menu/browser_app_menu.xml \ res/menu/gecko_app_menu.xml \ + res/menu/home_contextmenu.xml \ res/menu/titlebar_contextmenu.xml \ + res/menu/top_bookmarks_contextmenu.xml \ res/menu-large-v11/browser_app_menu.xml \ res/menu-v11/browser_app_menu.xml \ res/menu-xlarge-v11/browser_app_menu.xml \ @@ -1086,25 +1099,23 @@ RES_LAYOUT += res/layout/crash_reporter.xml endif RES_DRAWABLE += \ - $(SYNC_RES_DRAWABLE) \ - res/drawable/abouthome_logo.xml \ - res/drawable/abouthome_promo_box.xml \ + $(SYNC_RES_DRAWABLE) \ res/drawable/action_bar_button.xml \ res/drawable/action_bar_button_inverse.xml \ - res/drawable/address_bar_bg.xml \ - res/drawable/address_bar_bg_shadow_repeat.xml \ - res/drawable/address_bar_nav_button.xml \ - res/drawable/address_bar_right_edge.xml \ - res/drawable/address_bar_url.xml \ - res/drawable/awesomebar_listview_divider.xml \ - res/drawable/awesomebar_header_row.xml \ - res/drawable/awesomebar_tab_indicator.xml \ - res/drawable/awesomebar_tab_selected.xml \ - res/drawable/awesomebar_tab_unselected.xml \ + res/drawable/bookmark_thumbnail_bg.xml \ + res/drawable/url_bar_bg.xml \ + res/drawable/url_bar_bg_shadow_repeat.xml \ + res/drawable/url_bar_entry.xml \ + res/drawable/url_bar_nav_button.xml \ + res/drawable/url_bar_right_edge.xml \ + res/drawable/bookmark_folder.xml \ + res/drawable/divider_horizontal.xml \ res/drawable/divider_vertical.xml \ res/drawable/favicon_bg.xml \ res/drawable/handle_end_level.xml \ res/drawable/handle_start_level.xml \ + res/drawable/home_history_tabs_indicator.xml \ + res/drawable/home_page_title_background.xml \ res/drawable/ic_menu_back.xml \ res/drawable/ic_menu_desktop_mode_off.xml \ res/drawable/ic_menu_desktop_mode_on.xml \ @@ -1131,6 +1142,7 @@ RESOURCES = \ $(RES_DRAWABLE) \ $(RES_DRAWABLE_HDPI) \ $(RES_DRAWABLE_HDPI_V11) \ + $(RES_DRAWABLE_LARGE_LAND_V11) \ $(RES_DRAWABLE_LARGE_HDPI_V11) \ $(RES_DRAWABLE_LARGE_MDPI_V11) \ $(RES_DRAWABLE_LARGE_XHDPI_V11) \ @@ -1139,6 +1151,7 @@ RESOURCES = \ $(RES_DRAWABLE_MDPI_V11) \ $(RES_DRAWABLE_XHDPI) \ $(RES_DRAWABLE_XHDPI_V11) \ + $(RES_DRAWABLE_XLARGE_V11) \ $(RES_DRAWABLE_XLARGE_HDPI_V11) \ $(RES_DRAWABLE_XLARGE_MDPI_V11) \ $(RES_DRAWABLE_XLARGE_XHDPI_V11) \ @@ -1155,6 +1168,8 @@ RESOURCES = \ $(RES_VALUES_LARGE_V11) \ $(RES_VALUES_V11) \ $(RES_VALUES_V14) \ + $(RES_VALUES_V16) \ + $(RES_VALUES_XLARGE_LAND_V11) \ $(RES_VALUES_XLARGE_V11) \ $(RES_XML) \ $(RES_XML_V11) \ @@ -1165,12 +1180,13 @@ RES_DIRS= \ res/layout-large-v11 \ res/layout-large-land-v11 \ res/layout-xlarge-v11 \ - res/layout-xlarge-land-v11 \ res/values \ res/values-v11 \ res/values-large-v11 \ + res/values-xlarge-land-v11 \ res/values-xlarge-v11 \ - res/values-land-v14 \ + res/values-v14 \ + res/values-v16 \ res/xml \ res/xml-v11 \ res/anim \ @@ -1182,9 +1198,11 @@ RES_DIRS= \ res/drawable-mdpi-v11 \ res/drawable-hdpi-v11 \ res/drawable-xhdpi-v11 \ + res/drawable-large-land-v11 \ res/drawable-large-mdpi-v11 \ res/drawable-large-hdpi-v11 \ res/drawable-large-xhdpi-v11 \ + res/drawable-xlarge-v11 \ res/drawable-xlarge-mdpi-v11 \ res/drawable-xlarge-hdpi-v11 \ res/drawable-xlarge-xhdpi-v11 \ diff --git a/mobile/android/base/PageActionLayout.java b/mobile/android/base/PageActionLayout.java index 04fe8649a8f2..4bb7a8081b32 100644 --- a/mobile/android/base/PageActionLayout.java +++ b/mobile/android/base/PageActionLayout.java @@ -146,7 +146,7 @@ public class PageActionLayout extends LinearLayout implements GeckoEventListener } private ImageButton createImageButton() { - ImageButton imageButton = new ImageButton(mContext, null, R.style.AddressBar_ImageButton_Icon); + ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton_Icon); imageButton.setLayoutParams(new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width), LayoutParams.MATCH_PARENT)); imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); imageButton.setOnClickListener(this); diff --git a/mobile/android/base/Tab.java b/mobile/android/base/Tab.java index 8715612a65b9..5b9c7f8494e0 100644 --- a/mobile/android/base/Tab.java +++ b/mobile/android/base/Tab.java @@ -7,6 +7,7 @@ package org.mozilla.gecko; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.gfx.Layer; +import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.util.ThreadUtils; import org.json.JSONException; @@ -49,6 +50,7 @@ public class Tab { private int mHistoryIndex; private int mHistorySize; private int mParentId; + private HomePager.Page mAboutHomePage; private boolean mExternal; private boolean mBookmark; private boolean mReadingListItem; @@ -73,10 +75,12 @@ public class Tab { public static final int STATE_SUCCESS = 2; public static final int STATE_ERROR = 3; + private static final int DEFAULT_BACKGROUND_COLOR = Color.WHITE; + public enum ErrorType { CERT_ERROR, // Pages with certificate problems BLOCKED, // Pages blocked for phishing or malware warnings - NET_ERROR, // All other types of error + NET_ERROR, // All other types of error NONE // Non error pages } @@ -89,6 +93,7 @@ public class Tab { mUserSearch = ""; mExternal = external; mParentId = parentId; + mAboutHomePage = HomePager.Page.BOOKMARKS; mTitle = title == null ? "" : title; mFavicon = null; mFaviconUrl = null; @@ -112,7 +117,7 @@ public class Tab { // At startup, the background is set to a color specified by LayerView // when the LayerView is created. Shortly after, this background color // will be used before the tab's content is shown. - mBackgroundColor = getBackgroundColorForUrl(url); + mBackgroundColor = DEFAULT_BACKGROUND_COLOR; } private ContentResolver getContentResolver() { @@ -139,6 +144,15 @@ public class Tab { return mParentId; } + public HomePager.Page getAboutHomePage() { + return mAboutHomePage; + } + + private void setAboutHomePage(HomePager.Page page) { + mAboutHomePage = page; + } + + // may be null if user-entered query hasn't yet been resolved to a URI public synchronized String getURL() { return mUrl; @@ -607,9 +621,14 @@ public class Tab { setReaderEnabled(false); setZoomConstraints(new ZoomConstraints(true)); setHasTouchListeners(false); - setBackgroundColor(getBackgroundColorForUrl(uri)); + setBackgroundColor(DEFAULT_BACKGROUND_COLOR); setErrorType(ErrorType.NONE); + final String homePage = message.getString("aboutHomePage"); + if (!TextUtils.isEmpty(homePage)) { + setAboutHomePage(HomePager.Page.valueOf(homePage)); + } + Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, uri); } @@ -617,13 +636,6 @@ public class Tab { return "about:home".equals(url) || ReaderModeUtils.isAboutReader(url); } - private int getBackgroundColorForUrl(String url) { - if ("about:home".equals(url)) { - return mAppContext.getResources().getColor(R.color.background_normal); - } - return Color.WHITE; - } - void handleDocumentStart(boolean showProgress, String url) { setState(shouldShowProgress(url) ? STATE_SUCCESS : STATE_LOADING); updateIdentityData(null); diff --git a/mobile/android/base/Tabs.java b/mobile/android/base/Tabs.java index 8506cf58cdfb..219ffbd45d4c 100644 --- a/mobile/android/base/Tabs.java +++ b/mobile/android/base/Tabs.java @@ -6,6 +6,8 @@ package org.mozilla.gecko; import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.home.HomePager; +import org.mozilla.gecko.ReaderModeUtils; import org.mozilla.gecko.sync.setup.SyncAccounts; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; @@ -22,6 +24,7 @@ import android.database.ContentObserver; import android.graphics.Color; import android.net.Uri; import android.os.Handler; +import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; @@ -57,6 +60,7 @@ public class Tabs implements GeckoEventListener { public static final int LOADURL_DESKTOP = 1 << 5; public static final int LOADURL_BACKGROUND = 1 << 6; public static final int LOADURL_EXTERNAL = 1 << 7; + public static final int LOADURL_READING_LIST = 1 << 8; private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 5; @@ -581,6 +585,22 @@ public class Tabs implements GeckoEventListener { GeckoAppShell.getEventDispatcher().registerEventListener(event, this); } + /** + * Looks for an open tab with the given URL. + * + * @return id of an open tab with the given URL; -1 if the tab doesn't exist. + */ + public int getTabIdForUrl(String url) { + for (Tab tab : mOrder) { + if (TextUtils.equals(tab.getURL(), url) || + TextUtils.equals(ReaderModeUtils.getUrlFromAboutReader(tab.getURL()), url)) { + return tab.getId(); + } + } + + return -1; + } + /** * Loads a tab with the given URL in the currently selected tab. * @@ -637,6 +657,7 @@ public class Tabs implements GeckoEventListener { args.put("delayLoad", delayLoad); args.put("desktopMode", desktopMode); args.put("selected", !background); + args.put("aboutHomePage", (flags & LOADURL_READING_LIST) != 0 ? HomePager.Page.READING_LIST : ""); if ((flags & LOADURL_NEW_TAB) != 0) { int tabId = getNextTabId(); diff --git a/mobile/android/base/TabsPanel.java b/mobile/android/base/TabsPanel.java index a983af2e7d56..70976f268ee3 100644 --- a/mobile/android/base/TabsPanel.java +++ b/mobile/android/base/TabsPanel.java @@ -105,19 +105,11 @@ public class TabsPanel extends LinearLayout } }); - ImageButton button; - Resources resources = getContext().getResources(); - mTabWidget = (IconTabWidget) findViewById(R.id.tab_widget); - button = mTabWidget.addTab(R.drawable.tabs_normal); - button.setContentDescription(resources.getString(R.string.tabs_normal)); - - button = mTabWidget.addTab(R.drawable.tabs_private); - button.setContentDescription(resources.getString(R.string.tabs_private)); - - button = mTabWidget.addTab(R.drawable.tabs_synced); - button.setContentDescription(resources.getString(R.string.tabs_synced)); + mTabWidget.addTab(R.drawable.tabs_normal, R.string.tabs_normal); + mTabWidget.addTab(R.drawable.tabs_private, R.string.tabs_private); + mTabWidget.addTab(R.drawable.tabs_synced, R.string.tabs_synced); mTabWidget.setTabSelectionListener(this); } diff --git a/mobile/android/base/TabsTray.java b/mobile/android/base/TabsTray.java index e1c840f47964..8464c5fa42e0 100644 --- a/mobile/android/base/TabsTray.java +++ b/mobile/android/base/TabsTray.java @@ -315,7 +315,7 @@ public class TabsTray extends TwoWayView mCloseAnimationCount++; mPendingClosedTabs.add(view); - animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { @Override public void onPropertyAnimationStart() { } @Override @@ -353,7 +353,7 @@ public class TabsTray extends TwoWayView if (mOriginalSize == 0) mOriginalSize = (isVertical ? view.getHeight() : view.getWidth()); - animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { @Override public void onPropertyAnimationStart() { } @Override @@ -377,7 +377,7 @@ public class TabsTray extends TwoWayView animator.attach(view, Property.TRANSLATION_Y, 0); - animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { @Override public void onPropertyAnimationStart() { } @Override diff --git a/mobile/android/base/ThumbnailHelper.java b/mobile/android/base/ThumbnailHelper.java index 3bd2c06724bc..fd57ddeb7885 100644 --- a/mobile/android/base/ThumbnailHelper.java +++ b/mobile/android/base/ThumbnailHelper.java @@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicInteger; public final class ThumbnailHelper { private static final String LOGTAG = "GeckoThumbnailHelper"; - public static final float THUMBNAIL_ASPECT_RATIO = 0.714f; // this is a 5:7 ratio (as per UX decision) + public static final float THUMBNAIL_ASPECT_RATIO = 0.571f; // this is a 4:7 ratio (as per UX decision) // static singleton stuff diff --git a/mobile/android/base/animation/PropertyAnimator.java b/mobile/android/base/animation/PropertyAnimator.java index 4d3642dea9e6..5eb515166426 100644 --- a/mobile/android/base/animation/PropertyAnimator.java +++ b/mobile/android/base/animation/PropertyAnimator.java @@ -11,6 +11,7 @@ import android.os.Handler; import android.view.Choreographer; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; @@ -49,7 +50,7 @@ public class PropertyAnimator implements Runnable { private long mDuration; private float mDurationReciprocal; private List mElementsList; - private PropertyAnimationListener mListener; + private List mListeners; private FramePoster mFramePoster; private boolean mUseHardwareLayer; @@ -64,6 +65,7 @@ public class PropertyAnimator implements Runnable { mElementsList = new ArrayList(); mFramePoster = FramePoster.create(this); mUseHardwareLayer = true; + mListeners = null; } public void setUseHardwareLayer(boolean useHardwareLayer) { @@ -81,8 +83,12 @@ public class PropertyAnimator implements Runnable { mElementsList.add(element); } - public void setPropertyAnimationListener(PropertyAnimationListener listener) { - mListener = listener; + public void addPropertyAnimationListener(PropertyAnimationListener listener) { + if (mListeners == null) { + mListeners = new ArrayList(); + } + + mListeners.add(listener); } public long getDuration() { @@ -144,10 +150,35 @@ public class PropertyAnimator implements Runnable { element.view.setDrawingCacheEnabled(true); } - mFramePoster.postFirstAnimationFrame(); + // Get ViewTreeObserver from any of the participant views + // in the animation. + final ViewTreeObserver treeObserver; + if (mElementsList.size() > 0) { + treeObserver = mElementsList.get(0).view.getViewTreeObserver(); + } else { + treeObserver = null; + } - if (mListener != null) - mListener.onPropertyAnimationStart(); + // Try to start animation after any on-going layout round + // in the current view tree. + if (treeObserver != null && treeObserver.isAlive()) { + treeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + treeObserver.removeOnPreDrawListener(this); + mFramePoster.postFirstAnimationFrame(); + return true; + } + }); + } else { + mFramePoster.postFirstAnimationFrame(); + } + + if (mListeners != null) { + for (PropertyAnimationListener listener : mListeners) { + listener.onPropertyAnimationStart(); + } + } } @@ -173,10 +204,15 @@ public class PropertyAnimator implements Runnable { mElementsList.clear(); - if (mListener != null) { - if (snapToEndPosition) - mListener.onPropertyAnimationEnd(); - mListener = null; + if (mListeners != null) { + if (snapToEndPosition) { + for (PropertyAnimationListener listener : mListeners) { + listener.onPropertyAnimationEnd(); + } + } + + mListeners.clear(); + mListeners = null; } } diff --git a/mobile/android/base/awesomebar/AllPagesTab.java b/mobile/android/base/awesomebar/AllPagesTab.java deleted file mode 100644 index 3e004dea26d7..000000000000 --- a/mobile/android/base/awesomebar/AllPagesTab.java +++ /dev/null @@ -1,962 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko; - -import org.mozilla.gecko.AwesomeBar.ContextMenuSubject; -import org.mozilla.gecko.db.BrowserContract.Combined; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserDB.URLColumns; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.util.GamepadUtils; -import org.mozilla.gecko.util.GeckoEventListener; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UiAsyncTask; -import org.mozilla.gecko.widget.FaviconView; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.app.Activity; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Message; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnLongClickListener; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.view.animation.TranslateAnimation; -import android.widget.AdapterView; -import android.widget.FilterQueryProvider; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.SimpleCursorAdapter; -import android.widget.TextView; - -import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.List; - -public class AllPagesTab extends AwesomeBarTab implements GeckoEventListener { - public static final String LOGTAG = "GeckoAllPagesTab"; - private static final String TAG = "allPages"; - - private static final int SUGGESTION_TIMEOUT = 3000; - private static final int SUGGESTION_MAX = 3; - private static final int ANIMATION_DURATION = 250; - // The maximum number of rows deep in a search we'll dig for an autocomplete result - private static final int MAX_AUTOCOMPLETE_SEARCH = 20; - - private String mSearchTerm; - private ArrayList mSearchEngines; - private SuggestClient mSuggestClient; - private boolean mSuggestionsEnabled; - private AsyncTask> mSuggestTask; - private AwesomeBarCursorAdapter mCursorAdapter = null; - private boolean mTelemetrySent = false; - private LinearLayout mAllPagesView; - private boolean mAnimateSuggestions; - private View mSuggestionsOptInPrompt; - private Handler mHandler; - private ListView mListView; - private volatile AutocompleteHandler mAutocompleteHandler = null; - - private static final int MESSAGE_LOAD_FAVICONS = 1; - private static final int MESSAGE_UPDATE_FAVICONS = 2; - private static final int DELAY_SHOW_THUMBNAILS = 550; - - public AllPagesTab(Context context) { - super(context); - mSearchEngines = new ArrayList(); - - registerEventListener("SearchEngines:Data"); - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null)); - - mHandler = new AllPagesHandler(); - } - - @Override - public boolean onBackPressed() { - return false; - } - - @Override - public int getTitleStringId() { - return R.string.awesomebar_all_pages_title; - } - - @Override - public String getTag() { - return TAG; - } - - private ListView getListView() { - if (mListView == null && mView != null) { - mListView = (ListView) mView.findViewById(R.id.awesomebar_list); - } - return mListView; - } - - @Override - public View getView() { - if (mView == null) { - mView = (LinearLayout) (LayoutInflater.from(mContext).inflate(R.layout.awesomebar_allpages_list, null)); - mView.setTag(TAG); - - final ListView list = getListView(); - list.setTag(TAG); - ((Activity)mContext).registerForContextMenu(list); - list.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - handleItemClick(parent, view, position, id); - } - }); - - AwesomeBarCursorAdapter adapter = getCursorAdapter(); - list.setAdapter(adapter); - list.setOnTouchListener(mListListener); - - final ListSelectionListener listener = new ListSelectionListener(); - list.setOnItemSelectedListener(listener); - list.setOnFocusChangeListener(listener); - - list.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, android.view.KeyEvent event) { - View selected = list.getSelectedView(); - - if (selected instanceof SearchEngineRow) { - return ((SearchEngineRow) selected).onKeyDown(keyCode, event); - } - return false; - } - }); - - } - - return mView; - } - - @Override - public void destroy() { - super.destroy(); - - unregisterEventListener("SearchEngines:Data"); - - mHandler.removeMessages(MESSAGE_UPDATE_FAVICONS); - mHandler.removeMessages(MESSAGE_LOAD_FAVICONS); - mHandler = null; - - // Can't use getters for adapter or listview. They will create them if null. - if (mCursorAdapter != null && mListView != null) { - mListView.setAdapter(null); - final Cursor cursor = mCursorAdapter.getCursor(); - // Gingerbread locks the DB when closing a cursor, so do it in the - // background. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - if (cursor != null && !cursor.isClosed()) - cursor.close(); - } - }); - } - } - - public void filter(String searchTerm, AutocompleteHandler handler) { - mAutocompleteHandler = handler; - - AwesomeBarCursorAdapter adapter = getCursorAdapter(); - adapter.filter(searchTerm); - - filterSuggestions(searchTerm); - if (mSuggestionsOptInPrompt != null) { - int visibility = TextUtils.isEmpty(searchTerm) ? View.GONE : View.VISIBLE; - if (mSuggestionsOptInPrompt.getVisibility() != visibility) { - mSuggestionsOptInPrompt.setVisibility(visibility); - } - } - } - - private void findAutocompleteFor(String searchTerm, Cursor cursor) { - if (TextUtils.isEmpty(searchTerm) || cursor == null || mAutocompleteHandler == null) - return; - - // avoid searching the path if we don't have to. Currently just decided by if there is - // a '/' character in the string - final String res = searchHosts(searchTerm, cursor, searchTerm.indexOf("/") > 0); - - if (res != null) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // Its possible that mAutocompleteHandler has been destroyed - if (mAutocompleteHandler != null) { - mAutocompleteHandler.onAutocomplete(res); - mAutocompleteHandler = null; - } - } - }); - } - } - - private String searchHosts(String searchTerm, Cursor cursor, boolean searchPath) { - int i = 0; - if (cursor.moveToFirst()) { - int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL); - do { - final Uri url = Uri.parse(cursor.getString(urlIndex)); - String host = StringUtils.stripCommonSubdomains(url.getHost()); - // host may be null for about pages - if (host == null) - continue; - - StringBuilder hostBuilder = new StringBuilder(host); - if (hostBuilder.indexOf(searchTerm) == 0) { - return hostBuilder.append("/").toString(); - } - - if (searchPath) { - List path = url.getPathSegments(); - - for (String seg : path) { - hostBuilder.append("/").append(seg); - if (hostBuilder.indexOf(searchTerm) == 0) { - return hostBuilder.append("/").toString(); - } - } - } - - i++; - } while (i < MAX_AUTOCOMPLETE_SEARCH && cursor.moveToNext()); - } - return null; - } - - /** - * Query for suggestions, but don't show them yet. - */ - private void primeSuggestions() { - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - mSuggestClient.query(mSearchTerm); - } - }); - } - - private void filterSuggestions(String searchTerm) { - // cancel previous query - if (mSuggestTask != null) { - mSuggestTask.cancel(true); - } - - if (mSuggestClient != null && mSuggestionsEnabled) { - mSuggestTask = new AsyncTask>() { - @Override - protected ArrayList doInBackground(String... query) { - return mSuggestClient.query(query[0]); - } - - @Override - protected void onPostExecute(ArrayList suggestions) { - setSuggestions(suggestions); - } - }; - mSuggestTask.execute(searchTerm); - } - } - - protected AwesomeBarCursorAdapter getCursorAdapter() { - if (mCursorAdapter == null) { - // Load the list using a custom adapter so we can create the bitmaps - mCursorAdapter = new AwesomeBarCursorAdapter(mContext); - - mCursorAdapter.setFilterQueryProvider(new FilterQueryProvider() { - @Override - public Cursor runQuery(CharSequence constraint) { - long start = SystemClock.uptimeMillis(); - - Cursor c = BrowserDB.filter(getContentResolver(), constraint, MAX_RESULTS); - c.getCount(); - - postLoadFavicons(); - - long end = SystemClock.uptimeMillis(); - if (!mTelemetrySent && TextUtils.isEmpty(constraint)) { - int time = (int)(end - start); - Telemetry.HistogramAdd("FENNEC_AWESOMEBAR_ALLPAGES_EMPTY_TIME", time); - mTelemetrySent = true; - } - - findAutocompleteFor(constraint.toString(), c); - return c; - } - }); - } - return mCursorAdapter; - } - - private interface AwesomeBarItem { - public void onClick(); - public ContextMenuSubject getSubject(); - } - - private class AwesomeBarCursorItem implements AwesomeBarItem { - private Cursor mCursor; - - public AwesomeBarCursorItem(Cursor cursor) { - mCursor = cursor; - } - - @Override - public void onClick() { - String url = mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.URL)); - String title = mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.TITLE)); - sendToListener(url, title); - } - - @Override - public ContextMenuSubject getSubject() { - // Use the history id in order to allow removing history entries - int id = mCursor.getInt(mCursor.getColumnIndexOrThrow(Combined.HISTORY_ID)); - - String keyword = null; - int keywordCol = mCursor.getColumnIndex(URLColumns.KEYWORD); - if (keywordCol != -1) - keyword = mCursor.getString(keywordCol); - - final String url = mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.URL)); - - Bitmap bitmap = Favicons.getInstance().getFaviconFromMemCache(url); - byte[] favicon = null; - - if (bitmap != null) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) { - favicon = stream.toByteArray(); - } else { - Log.w(LOGTAG, "Favicon compression failed."); - } - } - - return new ContextMenuSubject(id, url, favicon, - mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.TITLE)), - keyword, - mCursor.getInt(mCursor.getColumnIndexOrThrow(Combined.DISPLAY))); - } - } - - private class AwesomeBarSearchEngineItem implements AwesomeBarItem { - private SearchEngine mSearchEngine; - - public AwesomeBarSearchEngineItem(SearchEngine searchEngine) { - mSearchEngine = searchEngine; - } - - @Override - public void onClick() { - AwesomeBarTabs.OnUrlOpenListener listener = getUrlListener(); - if (listener != null) - listener.onSearch(mSearchEngine, mSearchTerm); - } - - @Override - public ContextMenuSubject getSubject() { - // Do not show context menu for search engine items - return null; - } - } - - private class AwesomeBarCursorAdapter extends SimpleCursorAdapter { - private static final int ROW_SEARCH = 0; - private static final int ROW_STANDARD = 1; - private static final int ROW_SUGGEST = 2; - - public AwesomeBarCursorAdapter(Context context) { - super(context, -1, null, new String[] {}, new int[] {}); - mSearchTerm = ""; - } - - public void filter(String searchTerm) { - boolean changed = !mSearchTerm.equals(searchTerm); - mSearchTerm = searchTerm; - - if (changed) - mCursorAdapter.notifyDataSetChanged(); - - getFilter().filter(searchTerm); - } - - private int getSuggestEngineCount() { - return (mSearchTerm.length() == 0 || mSuggestClient == null || !mSuggestionsEnabled) ? 0 : 1; - } - - // Add the search engines to the number of reported results. - @Override - public int getCount() { - final int resultCount = super.getCount(); - - // don't show search engines or suggestions if search field is empty - if (mSearchTerm.length() == 0) - return resultCount; - - return resultCount + mSearchEngines.size(); - } - - // If an item is part of the cursor result set, return that entry. - // Otherwise, return the search engine data. - @Override - public Object getItem(int position) { - int engineIndex = getEngineIndex(position); - - if (engineIndex == -1) { - // return awesomebar result - position -= getSuggestEngineCount(); - return new AwesomeBarCursorItem((Cursor) super.getItem(position)); - } - - // return search engine - return new AwesomeBarSearchEngineItem(mSearchEngines.get(engineIndex)); - } - - private int getEngineIndex(int position) { - final int resultCount = super.getCount(); - final int suggestEngineCount = getSuggestEngineCount(); - - // return suggest engine index - if (position < suggestEngineCount) - return position; - - // not an engine - if (position - suggestEngineCount < resultCount) - return -1; - - // return search engine index - return position - resultCount; - } - - @Override - public int getItemViewType(int position) { - int engine = getEngineIndex(position); - if (engine == -1) { - return ROW_STANDARD; - } else if (engine == 0 && mSuggestionsEnabled) { - // Give suggestion views their own type to prevent them from - // sharing other recycled search engine views. Using other - // recycled views for the suggestion row can break animations - // (bug 815937). - return ROW_SUGGEST; - } - return ROW_SEARCH; - } - - @Override - public int getViewTypeCount() { - // view can be either a standard awesomebar row, a search engine - // row, or a suggestion row - return 3; - } - - @Override - public boolean isEnabled(int position) { - // If we're using a gamepad or keyboard, allow the row to be - // focused so it can pass the focus to its child suggestion views. - if (!getListView().isInTouchMode()) { - return true; - } - - // If the suggestion row only contains one item (the user-entered - // query), allow the entire row to be clickable; clicking the row - // has the same effect as clicking the single suggestion. If the - // row contains multiple items, clicking the row will do nothing. - int index = getEngineIndex(position); - if (index != -1) - return mSearchEngines.get(index).suggestions.isEmpty(); - return true; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - int type = getItemViewType(position); - if (type == ROW_SEARCH || type == ROW_SUGGEST) { - if (convertView == null || !(convertView instanceof SearchEngineRow)) { - convertView = (SearchEngineRow) getInflater().inflate(R.layout.home_search_item_row, getListView(), false); - ((SearchEngineRow) convertView).setOnUrlOpenListener(getUrlListener()); - } - - SearchEngineRow searchRow = (SearchEngineRow) convertView; - searchRow.setSearchTerm(mSearchTerm); - - final SearchEngine engine = mSearchEngines.get(getEngineIndex(position)); - final boolean doAnimation = (mAnimateSuggestions && engine.suggestions.size() > 0); - searchRow.updateFromSearchEngine(engine, doAnimation); - if (doAnimation) { - // Only animate suggestions the first time they are shown - mAnimateSuggestions = false; - } - } else { - AwesomeEntryViewHolder viewHolder = null; - - if (convertView == null || !(convertView.getTag() instanceof AwesomeEntryViewHolder)) { - convertView = getInflater().inflate(R.layout.awesomebar_row, null); - - viewHolder = new AwesomeEntryViewHolder(); - viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); - viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); - viewHolder.faviconView = (FaviconView) convertView.findViewById(R.id.favicon); - viewHolder.bookmarkIconView = (ImageView) convertView.findViewById(R.id.bookmark_icon); - - convertView.setTag(viewHolder); - } else { - viewHolder = (AwesomeEntryViewHolder) convertView.getTag(); - } - - position -= getSuggestEngineCount(); - Cursor cursor = getCursor(); - if (!cursor.moveToPosition(position)) - throw new IllegalStateException("Couldn't move cursor to position " + position); - - updateTitle(viewHolder.titleView, cursor); - updateUrl(viewHolder, cursor); - updateBookmarkIcon(viewHolder.bookmarkIconView, cursor); - displayFavicon(viewHolder); - } - - return convertView; - } - }; - - /** - * Sets suggestions associated with the current suggest engine. - * If there is no suggest engine, this does nothing. - */ - private void setSuggestions(final ArrayList suggestions) { - if (mSuggestClient != null) { - mSearchEngines.get(0).suggestions = suggestions; - getCursorAdapter().notifyDataSetChanged(); - } - } - - /** - * Sets search engines to be shown for user-entered queries. - */ - private void setSearchEngines(JSONObject data) { - try { - JSONObject suggest = data.getJSONObject("suggest"); - String suggestEngine = suggest.isNull("engine") ? null : suggest.getString("engine"); - String suggestTemplate = suggest.isNull("template") ? null : suggest.getString("template"); - mSuggestionsEnabled = suggest.getBoolean("enabled"); - boolean suggestionsPrompted = suggest.getBoolean("prompted"); - JSONArray engines = data.getJSONArray("searchEngines"); - - ArrayList searchEngines = new ArrayList(); - for (int i = 0; i < engines.length(); i++) { - JSONObject engineJSON = engines.getJSONObject(i); - String name = engineJSON.getString("name"); - String identifier = engineJSON.getString("identifier"); - String iconURI = engineJSON.getString("iconURI"); - Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconURI); - if (name.equals(suggestEngine) && suggestTemplate != null) { - // suggest engine should be at the front of the list - searchEngines.add(0, new SearchEngine(name, identifier, icon)); - - // The only time Tabs.getInstance().getSelectedTab() should - // be null is when we're restoring after a crash. We should - // never restore private tabs when that happens, so it - // should be safe to assume that null means non-private. - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab == null || !tab.isPrivate()) - mSuggestClient = new SuggestClient(getView().getContext(), suggestTemplate, SUGGESTION_TIMEOUT, SUGGESTION_MAX); - } else { - searchEngines.add(new SearchEngine(name, identifier, icon)); - } - } - - mSearchEngines = searchEngines; - mCursorAdapter.notifyDataSetChanged(); - - // show suggestions opt-in if user hasn't been prompted - if (!suggestionsPrompted && mSuggestClient != null) { - showSuggestionsOptIn(); - } - } catch (JSONException e) { - Log.e(LOGTAG, "Error getting search engine JSON", e); - } - - filterSuggestions(mSearchTerm); - } - - private void showSuggestionsOptIn() { - mSuggestionsOptInPrompt = LayoutInflater.from(mContext).inflate(R.layout.awesomebar_suggestion_prompt, (LinearLayout)getView(), false); - GeckoTextView promptText = (GeckoTextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title); - promptText.setText(getResources().getString(R.string.suggestions_prompt, mSearchEngines.get(0).name)); - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) - promptText.setPrivateMode(tab.isPrivate()); - - final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes); - final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no); - OnClickListener listener = new OnClickListener() { - @Override - public void onClick(View v) { - // Prevent the buttons from being clicked multiple times (bug 816902) - yesButton.setOnClickListener(null); - noButton.setOnClickListener(null); - - setSuggestionsEnabled(v == yesButton); - } - }; - yesButton.setOnClickListener(listener); - noButton.setOnClickListener(listener); - - // If the prompt container gains focus, automatically pass focus to the - // yes button in the prompt. - final View promptContainer = mSuggestionsOptInPrompt.findViewById(R.id.prompt_container); - promptContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - yesButton.requestFocus(); - } - } - }); - - mSuggestionsOptInPrompt.setVisibility(View.GONE); - ((LinearLayout)getView()).addView(mSuggestionsOptInPrompt, 0); - } - - private void setSuggestionsEnabled(final boolean enabled) { - // Clicking the yes/no buttons quickly can cause the click events be - // queued before the listeners are removed above, so it's possible - // setSuggestionsEnabled() can be called twice. mSuggestionsOptInPrompt - // can be null if this happens (bug 828480). - if (mSuggestionsOptInPrompt == null) { - return; - } - - // Make suggestions appear immediately after the user opts in - primeSuggestions(); - - // Pref observer in gecko will also set prompted = true - PrefsHelper.setPref("browser.search.suggest.enabled", enabled); - - TranslateAnimation anim1 = new TranslateAnimation(0, mSuggestionsOptInPrompt.getWidth(), 0, 0); - anim1.setDuration(ANIMATION_DURATION); - anim1.setInterpolator(new AccelerateInterpolator()); - anim1.setFillAfter(true); - final View promptContainer = mSuggestionsOptInPrompt.findViewById(R.id.prompt_container); - - TranslateAnimation anim2 = new TranslateAnimation(0, 0, 0, -1 * mSuggestionsOptInPrompt.getHeight()); - anim2.setDuration(ANIMATION_DURATION); - anim2.setFillAfter(true); - anim2.setStartOffset(anim1.getDuration()); - final LinearLayout view = (LinearLayout)getView(); - anim2.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation a) { - // Increase the height of the view so a gap isn't shown during animation - view.getLayoutParams().height = view.getHeight() + - mSuggestionsOptInPrompt.getHeight(); - view.requestLayout(); - } - @Override - public void onAnimationRepeat(Animation a) {} - @Override - public void onAnimationEnd(Animation a) { - // Removing the view immediately results in a NPE in - // dispatchDraw(), possibly because this callback executes - // before drawing is finished. Posting this as a Runnable fixes - // the issue. - view.post(new Runnable() { - @Override - public void run() { - view.removeView(mSuggestionsOptInPrompt); - getListView().clearAnimation(); - mSuggestionsOptInPrompt = null; - - if (enabled) { - // Reset the view height - view.getLayoutParams().height = LayoutParams.FILL_PARENT; - - mSuggestionsEnabled = enabled; - mAnimateSuggestions = true; - getCursorAdapter().notifyDataSetChanged(); - filterSuggestions(mSearchTerm); - } - } - }); - } - }); - - promptContainer.startAnimation(anim1); - mSuggestionsOptInPrompt.startAnimation(anim2); - getListView().startAnimation(anim2); - } - - @Override - public void handleMessage(String event, final JSONObject message) { - if (event.equals("SearchEngines:Data")) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - setSearchEngines(message); - } - }); - } - } - - public void handleItemClick(AdapterView parent, View view, int position, long id) { - ListView listview = getListView(); - if (listview == null) - return; - - AwesomeBarItem item = (AwesomeBarItem)listview.getItemAtPosition(position); - item.onClick(); - } - - protected void updateBookmarkIcon(ImageView bookmarkIconView, Cursor cursor) { - int bookmarkIdIndex = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID); - long id = cursor.getLong(bookmarkIdIndex); - - int displayIndex = cursor.getColumnIndexOrThrow(Combined.DISPLAY); - int display = cursor.getInt(displayIndex); - - // The bookmark id will be 0 (null in database) when the url - // is not a bookmark. - int visibility = (id == 0 ? View.GONE : View.VISIBLE); - bookmarkIconView.setVisibility(visibility); - - if (display == Combined.DISPLAY_READER) { - bookmarkIconView.setImageResource(R.drawable.ic_awesomebar_reader); - } else { - bookmarkIconView.setImageResource(R.drawable.ic_awesomebar_star); - } - } - - @Override - public ContextMenuSubject getSubject(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - ContextMenuSubject subject = null; - - if (!(menuInfo instanceof AdapterView.AdapterContextMenuInfo)) { - Log.e(LOGTAG, "menuInfo is not AdapterContextMenuInfo"); - return subject; - } - - ListView list = (ListView)view; - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; - subject = ((AwesomeBarItem) list.getItemAtPosition(info.position)).getSubject(); - - if (subject == null) - return subject; - - setupMenu(menu, subject); - - menu.findItem(R.id.remove_bookmark).setVisible(false); - menu.findItem(R.id.edit_bookmark).setVisible(false); - menu.findItem(R.id.open_in_reader).setVisible(subject.display == Combined.DISPLAY_READER); - - return subject; - } - - private void registerEventListener(String event) { - GeckoAppShell.getEventDispatcher().registerEventListener(event, this); - } - - private void unregisterEventListener(String event) { - GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this); - } - - private List getUrlsWithoutFavicon() { - List urls = new ArrayList(); - - Cursor c = mCursorAdapter.getCursor(); - if (c == null || !c.moveToFirst()) - return urls; - - do { - final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); - - // We only want to load favicons from DB if they are not in the - // memory cache yet. - if (Favicons.getInstance().getFaviconFromMemCache(url) != null) - continue; - - urls.add(url); - } while (c.moveToNext()); - - return urls; - } - - public void storeFaviconsInMemCache(Cursor c) { - if (c == null) - return; - - try { - if (!c.moveToFirst()) - return; - - do { - final String url = c.getString(c.getColumnIndexOrThrow(Combined.URL)); - final byte[] b = c.getBlob(c.getColumnIndexOrThrow(Combined.FAVICON)); - if (b == null) - continue; - - Bitmap favicon = BitmapUtils.decodeByteArray(b); - if (favicon == null) - continue; - - favicon = Favicons.getInstance().scaleImage(favicon); - Favicons.getInstance().putFaviconInMemCache(url, favicon); - } while (c.moveToNext()); - } finally { - c.close(); - } - } - - private void loadFaviconsForCurrentResults() { - final List urls = getUrlsWithoutFavicon(); - if (urls.size() == 0) - return; - - (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { - @Override - public Void doInBackground(Void... params) { - Cursor cursor = BrowserDB.getFaviconsForUrls(getContentResolver(), urls); - storeFaviconsInMemCache(cursor); - return null; - } - - @Override - public void onPostExecute(Void result) { - postUpdateFavicons(); - } - }).execute(); - } - - private void displayFavicon(AwesomeEntryViewHolder viewHolder) { - final String url = viewHolder.url; - Bitmap bitmap = Favicons.getInstance().getFaviconFromMemCache(url); - updateFavicon(viewHolder.faviconView, bitmap, url); - } - - private void updateFavicons() { - ListView listView = getListView(); - AwesomeBarCursorAdapter adapter = getCursorAdapter(); - Cursor cursor = adapter.getCursor(); - if (cursor == null) - return; - - for (int i = 0; i < listView.getChildCount(); i++) { - final View view = listView.getChildAt(i); - final Object tag = view.getTag(); - - if (tag == null || !(tag instanceof AwesomeEntryViewHolder)) - continue; - - final AwesomeEntryViewHolder viewHolder = (AwesomeEntryViewHolder) tag; - displayFavicon(viewHolder); - } - - mView.invalidate(); - } - - private void postUpdateFavicons() { - if (mHandler == null) - return; - - Message msg = mHandler.obtainMessage(MESSAGE_UPDATE_FAVICONS, - AllPagesTab.this); - - mHandler.removeMessages(MESSAGE_UPDATE_FAVICONS); - mHandler.sendMessage(msg); - } - - private void postLoadFavicons() { - if (mHandler == null) - return; - - Message msg = mHandler.obtainMessage(MESSAGE_LOAD_FAVICONS, - AllPagesTab.this); - - mHandler.removeMessages(MESSAGE_LOAD_FAVICONS); - mHandler.sendMessageDelayed(msg, 200); - } - - private class AllPagesHandler extends Handler { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_LOAD_FAVICONS: - loadFaviconsForCurrentResults(); - break; - case MESSAGE_UPDATE_FAVICONS: - updateFavicons(); - break; - } - } - } - - private static class ListSelectionListener implements View.OnFocusChangeListener, - AdapterView.OnItemSelectedListener { - private SearchEngineRow mSelectedEngineRow; - - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - View selectedRow = ((ListView) v).getSelectedView(); - if (selectedRow != null) { - selectRow(selectedRow); - } - } else { - deselectRow(); - } - } - - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - deselectRow(); - selectRow(view); - } - - @Override - public void onNothingSelected(AdapterView parent) { - deselectRow(); - } - - private void selectRow(View row) { - if (row instanceof SearchEngineRow) { - mSelectedEngineRow = (SearchEngineRow) row; - mSelectedEngineRow.onSelected(); - } - } - - private void deselectRow() { - if (mSelectedEngineRow != null) { - mSelectedEngineRow.onDeselected(); - mSelectedEngineRow = null; - } - } - } -} diff --git a/mobile/android/base/awesomebar/AwesomeBarTab.java b/mobile/android/base/awesomebar/AwesomeBarTab.java deleted file mode 100644 index f07e52e0a4b6..000000000000 --- a/mobile/android/base/awesomebar/AwesomeBarTab.java +++ /dev/null @@ -1,194 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko; - -import org.mozilla.gecko.AwesomeBar.ContextMenuSubject; -import org.mozilla.gecko.db.BrowserDB.URLColumns; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.widget.FaviconView; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.text.TextUtils; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.ImageView; -import android.widget.TextView; - -import java.util.HashMap; - -abstract public class AwesomeBarTab { - abstract public String getTag(); - abstract public int getTitleStringId(); - abstract public boolean onBackPressed(); - abstract public ContextMenuSubject getSubject(ContextMenu menu, View view, ContextMenuInfo menuInfo); - abstract public View getView(); - - protected View mView = null; - protected View.OnTouchListener mListListener; - private AwesomeBarTabs.OnUrlOpenListener mListener; - private LayoutInflater mInflater = null; - private ContentResolver mContentResolver = null; - private Resources mResources; - // FIXME: This value should probably come from a prefs key - public static final int MAX_RESULTS = 100; - protected Context mContext = null; - public static HashMap sOpenTabs; - - public AwesomeBarTab(Context context) { - mContext = context; - } - - public void destroy() { - sOpenTabs = null; - } - - public void setListTouchListener(View.OnTouchListener listener) { - mListListener = listener; - if (mView != null) - mView.setOnTouchListener(mListListener); - } - - protected class AwesomeEntryViewHolder { - public TextView titleView; - public String url; - public TextView urlView; - public FaviconView faviconView; - public ImageView bookmarkIconView; - } - - protected LayoutInflater getInflater() { - if (mInflater == null) { - mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - return mInflater; - } - - protected AwesomeBarTabs.OnUrlOpenListener getUrlListener() { - return mListener; - } - - protected void setUrlListener(AwesomeBarTabs.OnUrlOpenListener listener) { - mListener = listener; - } - - protected ContentResolver getContentResolver() { - if (mContentResolver == null) { - mContentResolver = mContext.getContentResolver(); - } - return mContentResolver; - } - - protected HashMap getOpenTabs() { - if (sOpenTabs == null || sOpenTabs.isEmpty()) { - Iterable tabs = Tabs.getInstance().getTabsInOrder(); - sOpenTabs = new HashMap(); - for (Tab tab : tabs) { - sOpenTabs.put(tab.getURL(), tab.getId()); - } - } - return sOpenTabs; - } - - protected Resources getResources() { - if (mResources == null) { - mResources = mContext.getResources(); - } - return mResources; - } - - protected void setupMenu(ContextMenu menu, AwesomeBar.ContextMenuSubject subject) { - MenuInflater inflater = new MenuInflater(mContext); - inflater.inflate(R.menu.awesomebar_contextmenu, menu); - - // Show Open Private Tab if we're in private mode, Open New Tab otherwise - boolean isPrivate = false; - Tab tab = Tabs.getInstance().getSelectedTab(); - if (tab != null) { - isPrivate = tab.isPrivate(); - } - menu.findItem(R.id.open_new_tab).setVisible(!isPrivate); - menu.findItem(R.id.open_private_tab).setVisible(isPrivate); - - // Hide "Remove" item if there isn't a valid history ID - if (subject.id < 0) { - menu.findItem(R.id.remove_history).setVisible(false); - } - menu.setHeaderTitle(subject.title); - } - - protected void updateFavicon(FaviconView faviconView, Bitmap bitmap, String key) { - faviconView.updateImage(bitmap, key); - } - - protected void updateTitle(TextView titleView, Cursor cursor) { - int titleIndex = cursor.getColumnIndexOrThrow(URLColumns.TITLE); - String title = cursor.getString(titleIndex); - String url = ""; - - // Use the URL instead of an empty title for consistency with the normal URL - // bar view - this is the equivalent of getDisplayTitle() in Tab.java - if (TextUtils.isEmpty(title)) { - int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL); - url = cursor.getString(urlIndex); - } - - updateTitle(titleView, title, url); - } - - protected void updateTitle(TextView titleView, String title, String url) { - if (TextUtils.isEmpty(title)) { - titleView.setText(url); - } else { - titleView.setText(title); - } - } - - public void sendToListener(String url, String title) { - AwesomeBarTabs.OnUrlOpenListener listener = getUrlListener(); - if (listener == null) - return; - - Integer tabId = getOpenTabs().get(url); - if (tabId != null) { - listener.onSwitchToTab(tabId); - } else { - listener.onUrlOpen(url, title); - } - } - - protected void updateUrl(AwesomeEntryViewHolder holder, Cursor cursor) { - int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL); - String url = cursor.getString(urlIndex); - updateUrl(holder, url); - } - - protected void updateUrl(AwesomeEntryViewHolder holder, String url) { - Integer tabId = getOpenTabs().get(url); - holder.url = url; - if (tabId != null) { - holder.urlView.setText(R.string.awesomebar_switch_to_tab); - holder.urlView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_awesomebar_tab, 0, 0, 0); - } else { - holder.urlView.setText(url); - holder.urlView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); - } - } - - protected boolean hideSoftInput(View view) { - InputMethodManager imm = - (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); - - return imm.hideSoftInputFromWindow(view.getWindowToken(), 0); - } -} diff --git a/mobile/android/base/awesomebar/BookmarksTab.java b/mobile/android/base/awesomebar/BookmarksTab.java deleted file mode 100644 index 6b49953b26f5..000000000000 --- a/mobile/android/base/awesomebar/BookmarksTab.java +++ /dev/null @@ -1,470 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko; - -import org.mozilla.gecko.AwesomeBar.ContextMenuSubject; -import org.mozilla.gecko.db.BrowserContract.Bookmarks; -import org.mozilla.gecko.db.BrowserContract.Combined; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserDB.URLColumns; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.util.GamepadUtils; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.FaviconView; - -import android.app.Activity; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.os.AsyncTask; -import android.util.Log; -import android.util.Pair; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListView; -import android.widget.SimpleCursorAdapter; -import android.widget.TextView; - -import java.util.LinkedList; - -public class BookmarksTab extends AwesomeBarTab { - public static final String LOGTAG = "BOOKMARKS_TAB"; - public static final String TAG = "bookmarks"; - private int mFolderId; - private String mFolderTitle; - private BookmarksListAdapter mCursorAdapter = null; - private BookmarksQueryTask mQueryTask = null; - private boolean mShowReadingList = false; - - @Override - public int getTitleStringId() { - return R.string.awesomebar_bookmarks_title; - } - - @Override - public String getTag() { - return TAG; - } - - public BookmarksTab(Context context) { - super(context); - } - - @Override - public View getView() { - if (mView == null) { - mView = new ListView(mContext, null); - ((Activity)mContext).registerForContextMenu(mView); - mView.setTag(TAG); - mView.setOnTouchListener(mListListener); - - // We need to add the header before we set the adapter, hence make it null - ListView list = (ListView)mView; - list.setAdapter(null); - list.setAdapter(getCursorAdapter()); - list.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - handleItemClick(parent, view, position, id); - } - }); - - if (mShowReadingList) { - String title = getResources().getString(R.string.bookmarks_folder_reading_list); - getCursorAdapter().moveToChildFolder(Bookmarks.FIXED_READING_LIST_ID, title); - } else { - BookmarksQueryTask task = getQueryTask(); - task.execute(); - } - } - return (ListView)mView; - } - - public void setShowReadingList(boolean showReadingList) { - mShowReadingList = showReadingList; - } - - @Override - public void destroy() { - super.destroy(); - // Can't use getters for adapter. It will create one if null. - if (mCursorAdapter != null && mView != null) { - ListView list = (ListView)mView; - list.setAdapter(null); - final Cursor cursor = mCursorAdapter.getCursor(); - // Gingerbread locks the DB when closing a cursor, so do it in the - // background. - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - if (cursor != null && !cursor.isClosed()) - cursor.close(); - } - }); - } - } - - @Override - public boolean onBackPressed() { - // If the soft keyboard is visible in the bookmarks or history tab, the user - // must have explictly brought it up, so we should try hiding it instead of - // exiting the activity or going up a bookmarks folder level. - if (hideSoftInput(getView())) - return true; - - return moveToParentFolder(); - } - - protected BookmarksListAdapter getCursorAdapter() { - return getCursorAdapter(null); - } - - protected BookmarksListAdapter getCursorAdapter(Cursor c) { - if (mCursorAdapter == null) { - mCursorAdapter = new BookmarksListAdapter(mContext, c); - } else if (c != null) { - mCursorAdapter.changeCursor(c); - } else { - // do a quick return if just asking for the cached adapter - return mCursorAdapter; - } - - TextView headerView = mCursorAdapter.getHeaderView(); - if (headerView == null) { - headerView = (TextView) getInflater().inflate(R.layout.awesomebar_header_row, null); - mCursorAdapter.setHeaderView(headerView); - } - - // Add/Remove header based on the root folder - if (mView != null) { - ListView list = (ListView)mView; - if (mFolderId == Bookmarks.FIXED_ROOT_ID) { - if (list.getHeaderViewsCount() == 1) { - list.removeHeaderView(headerView); - } - } else { - if (list.getHeaderViewsCount() == 0) { - list.addHeaderView(headerView, null, true); - } - headerView.setText(mFolderTitle); - } - } - - return mCursorAdapter; - } - - protected BookmarksQueryTask getQueryTask() { - if (mQueryTask == null) { - mQueryTask = new BookmarksQueryTask(); - } - return mQueryTask; - } - - public void handleItemClick(AdapterView parent, View view, int position, long id) { - ListView list = (ListView)getView(); - if (list == null) - return; - - int headerCount = list.getHeaderViewsCount(); - // If we tap on the header view, there's nothing to do - if (headerCount == 1 && position == 0) - return; - - BookmarksListAdapter adapter = getCursorAdapter(); - if (adapter == null) - return; - - Cursor cursor = adapter.getCursor(); - if (cursor == null) - return; - - // The header view takes up a spot in the list - if (headerCount == 1) - position--; - - cursor.moveToPosition(position); - - int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE)); - if (type == Bookmarks.TYPE_FOLDER) { - // If we're clicking on a folder, update adapter to move to that folder - int folderId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); - String folderTitle = adapter.getFolderTitle(position); - - adapter.moveToChildFolder(folderId, folderTitle); - return; - } - - // Otherwise, just open the URL - String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); - String title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); - long parentId = cursor.getLong(cursor.getColumnIndexOrThrow(Bookmarks.PARENT)); - if (parentId == Bookmarks.FIXED_READING_LIST_ID) { - url = ReaderModeUtils.getAboutReaderForUrl(url, true); - } - sendToListener(url, title); - } - - private class BookmarksListAdapter extends SimpleCursorAdapter { - private static final int VIEW_TYPE_ITEM = 0; - private static final int VIEW_TYPE_FOLDER = 1; - private static final int VIEW_TYPE_COUNT = 2; - - private LinkedList> mParentStack; - private TextView mBookmarksTitleView; - - public BookmarksListAdapter(Context context, Cursor c) { - super(context, -1, c, new String[] {}, new int[] {}); - - // mParentStack holds folder id/title pairs that allow us to navigate - // back up the folder heirarchy - mParentStack = new LinkedList>(); - - // Add the root folder to the stack - Pair rootFolder = new Pair(Bookmarks.FIXED_ROOT_ID, ""); - mParentStack.addFirst(rootFolder); - } - - public void refreshCurrentFolder() { - // Cancel any pre-existing async refresh tasks - if (mQueryTask != null) - mQueryTask.cancel(false); - - Pair folderPair = mParentStack.getFirst(); - mQueryTask = new BookmarksQueryTask(folderPair.first, folderPair.second); - mQueryTask.execute(); - } - - // Returns false if there is no parent folder to move to - public boolean moveToParentFolder() { - // If we're already at the root, we can't move to a parent folder - if (mParentStack.size() == 1) - return false; - - mParentStack.removeFirst(); - refreshCurrentFolder(); - return true; - } - - public void moveToChildFolder(int folderId, String folderTitle) { - Pair folderPair = new Pair(folderId, folderTitle); - mParentStack.addFirst(folderPair); - refreshCurrentFolder(); - } - - public boolean isInReadingList() { - Pair folderPair = mParentStack.getFirst(); - return (folderPair.first == Bookmarks.FIXED_READING_LIST_ID); - } - - @Override - public int getItemViewType(int position) { - Cursor c = getCursor(); - - if (c.moveToPosition(position) && - c.getInt(c.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER) - return VIEW_TYPE_FOLDER; - - // Default to retuning normal item type - return VIEW_TYPE_ITEM; - } - - @Override - public int getViewTypeCount() { - return VIEW_TYPE_COUNT; - } - - public String getFolderTitle(int position) { - Cursor c = getCursor(); - if (!c.moveToPosition(position)) - return ""; - - String guid = c.getString(c.getColumnIndexOrThrow(Bookmarks.GUID)); - - // If we don't have a special GUID, just return the folder title from the DB. - if (guid == null || guid.length() == 12) - return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE)); - - // Use localized strings for special folder names. - if (guid.equals(Bookmarks.FAKE_DESKTOP_FOLDER_GUID)) - return getResources().getString(R.string.bookmarks_folder_desktop); - else if (guid.equals(Bookmarks.MENU_FOLDER_GUID)) - return getResources().getString(R.string.bookmarks_folder_menu); - else if (guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID)) - return getResources().getString(R.string.bookmarks_folder_toolbar); - else if (guid.equals(Bookmarks.UNFILED_FOLDER_GUID)) - return getResources().getString(R.string.bookmarks_folder_unfiled); - else if (guid.equals(Bookmarks.READING_LIST_FOLDER_GUID)) - return getResources().getString(R.string.bookmarks_folder_reading_list); - - // If for some reason we have a folder with a special GUID, but it's not one of - // the special folders we expect in the UI, just return the title from the DB. - return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE)); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - int viewType = getItemViewType(position); - AwesomeEntryViewHolder viewHolder = null; - - if (convertView == null) { - if (viewType == VIEW_TYPE_ITEM) - convertView = getInflater().inflate(R.layout.awesomebar_row, null); - else - convertView = getInflater().inflate(R.layout.awesomebar_folder_row, null); - - viewHolder = new AwesomeEntryViewHolder(); - viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); - viewHolder.faviconView = (FaviconView) convertView.findViewById(R.id.favicon); - - if (viewType == VIEW_TYPE_ITEM) - viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); - - convertView.setTag(viewHolder); - } else { - viewHolder = (AwesomeEntryViewHolder) convertView.getTag(); - } - - Cursor cursor = getCursor(); - if (!cursor.moveToPosition(position)) - throw new IllegalStateException("Couldn't move cursor to position " + position); - - if (viewType == VIEW_TYPE_ITEM) { - updateTitle(viewHolder.titleView, cursor); - updateUrl(viewHolder, cursor); - - byte[] b = cursor.getBlob(cursor.getColumnIndexOrThrow(URLColumns.FAVICON)); - Bitmap favicon = null; - if (b != null) { - Bitmap bitmap = BitmapUtils.decodeByteArray(b); - if (bitmap != null) { - favicon = Favicons.getInstance().scaleImage(bitmap); - } - } - String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); - updateFavicon(viewHolder.faviconView, favicon, url); - } else { - viewHolder.titleView.setText(getFolderTitle(position)); - } - - return convertView; - } - - public TextView getHeaderView() { - return mBookmarksTitleView; - } - - public void setHeaderView(TextView titleView) { - mBookmarksTitleView = titleView; - } - } - - private class BookmarksQueryTask extends AsyncTask { - public BookmarksQueryTask() { - mFolderId = Bookmarks.FIXED_ROOT_ID; - mFolderTitle = ""; - } - - public BookmarksQueryTask(int folderId, String folderTitle) { - mFolderId = folderId; - mFolderTitle = folderTitle; - } - - @Override - protected Cursor doInBackground(Void... arg0) { - return BrowserDB.getBookmarksInFolder(getContentResolver(), mFolderId); - } - - @Override - protected void onPostExecute(final Cursor cursor) { - // Hack: force this to the main thread, even though it should already be on it - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - // this will update the cursorAdapter to use the new one if it already exists - // We need to add the header before we set the adapter, hence make it null - ListView list = (ListView)mView; - list.setAdapter(null); - list.setAdapter(getCursorAdapter(cursor)); - } - }); - mQueryTask = null; - } - } - - public boolean moveToParentFolder() { - // If we're not in the bookmarks tab, we have nothing to do. We should - // also return false if mBookmarksAdapter hasn't been initialized yet. - BookmarksListAdapter adapter = getCursorAdapter(); - if (adapter == null) - return false; - - return adapter.moveToParentFolder(); - } - - /** - * Whether the user is in the Reading List bookmarks directory in the - * AwesomeScreen UI. - */ - public boolean isInReadingList() { - if (mCursorAdapter == null) - return false; - - return mCursorAdapter.isInReadingList(); - } - - @Override - public ContextMenuSubject getSubject(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - ContextMenuSubject subject = null; - - if (!(menuInfo instanceof AdapterView.AdapterContextMenuInfo)) { - Log.e(LOGTAG, "menuInfo is not AdapterContextMenuInfo"); - return subject; - } - - ListView list = (ListView)view; - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; - Object selectedItem = list.getItemAtPosition(info.position); - - if (!(selectedItem instanceof Cursor)) { - Log.e(LOGTAG, "item at " + info.position + " is not a Cursor"); - return subject; - } - - Cursor cursor = (Cursor) selectedItem; - - // Don't show the context menu for folders - if (!(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER)) { - String keyword = null; - int keywordCol = cursor.getColumnIndex(URLColumns.KEYWORD); - if (keywordCol != -1) - keyword = cursor.getString(keywordCol); - - int id = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); - - subject = new ContextMenuSubject(id, - cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)), - cursor.getBlob(cursor.getColumnIndexOrThrow(URLColumns.FAVICON)), - cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)), - keyword, - isInReadingList() ? Combined.DISPLAY_READER : Combined.DISPLAY_NORMAL); - } - - if (subject == null) - return subject; - - setupMenu(menu, subject); - - menu.findItem(R.id.remove_history).setVisible(false); - menu.findItem(R.id.open_in_reader).setVisible(false); - - return subject; - } -} diff --git a/mobile/android/base/awesomebar/HistoryTab.java b/mobile/android/base/awesomebar/HistoryTab.java deleted file mode 100644 index 3d2620851360..000000000000 --- a/mobile/android/base/awesomebar/HistoryTab.java +++ /dev/null @@ -1,455 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko; - -import org.mozilla.gecko.AwesomeBar.ContextMenuSubject; -import org.mozilla.gecko.db.BrowserContract.Combined; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserDB.URLColumns; -import org.mozilla.gecko.gfx.BitmapUtils; -import org.mozilla.gecko.util.GamepadUtils; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.widget.FaviconView; - -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.os.AsyncTask; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.KeyEvent; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ExpandableListView; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.SimpleExpandableListAdapter; -import android.widget.TextView; - -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -public class HistoryTab extends AwesomeBarTab { - public static final String LOGTAG = "HISTORY_TAB"; - public static final String TAG = "history"; - private static enum HistorySection { TODAY, YESTERDAY, WEEK, OLDER }; - private ContentObserver mContentObserver; - private ContentResolver mContentResolver; - private HistoryQueryTask mQueryTask = null; - private HistoryListAdapter mCursorAdapter = null; - - public HistoryTab(Context context) { - super(context); - mContentObserver = null; - } - - @Override - public int getTitleStringId() { - return R.string.awesomebar_history_title; - } - - @Override - public String getTag() { - return TAG; - } - - @Override - public ListView getView() { - if (mView == null) { - mView = new ExpandableListView(mContext, null); - ((Activity)mContext).registerForContextMenu(mView); - mView.setTag(TAG); - - ExpandableListView list = (ExpandableListView)mView; - list.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { - @Override - public boolean onChildClick(ExpandableListView parent, View view, - int groupPosition, int childPosition, long id) { - return handleItemClick(groupPosition, childPosition); - } - }); - - // This is to disallow collapsing the expandable groups in the - // history expandable list view to mimic simpler sections. We should - // Remove this if we decide to allow expanding/collapsing groups. - list.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() { - @Override - public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) { - return true; - } - }); - list.setOnKeyListener(new View.OnKeyListener() { - @Override public boolean onKey(View v, int keyCode, KeyEvent event) { - if (GamepadUtils.isActionKeyDown(event)) { - ExpandableListView expando = (ExpandableListView)v; - long selected = expando.getSelectedPosition(); - switch (ExpandableListView.getPackedPositionType(selected)) { - case ExpandableListView.PACKED_POSITION_TYPE_CHILD: - return handleItemClick(ExpandableListView.getPackedPositionGroup(selected), - ExpandableListView.getPackedPositionChild(selected)); - case ExpandableListView.PACKED_POSITION_TYPE_GROUP: - int group = ExpandableListView.getPackedPositionGroup(selected); - return (expando.isGroupExpanded(group) - ? expando.collapseGroup(group) - : expando.expandGroup(group)); - } - } - return false; - } - }); - - mView.setOnTouchListener(mListListener); - - // We need to add the header before we set the adapter, hence make it null - list.setAdapter(getCursorAdapter()); - HistoryQueryTask task = new HistoryQueryTask(); - task.execute(); - } - return (ListView)mView; - } - - @Override - public void destroy() { - super.destroy(); - - if (mContentObserver != null) - BrowserDB.unregisterContentObserver(getContentResolver(), mContentObserver); - } - - @Override - public boolean onBackPressed() { - // If the soft keyboard is visible in the bookmarks or history tab, the user - // must have explictly brought it up, so we should try hiding it instead of - // exiting the activity or going up a bookmarks folder level. - View view = getView(); - if (hideSoftInput(view)) - return true; - - return false; - } - - protected HistoryListAdapter getCursorAdapter() { - return mCursorAdapter; - } - - private class HistoryListAdapter extends SimpleExpandableListAdapter { - public HistoryListAdapter(Context context, List> groupData, - int groupLayout, String[] groupFrom, int[] groupTo, - List>> childData) { - - super(context, groupData, groupLayout, groupFrom, groupTo, - childData, -1, new String[] {}, new int[] {}); - } - - @Override - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, - View convertView, ViewGroup parent) { - AwesomeEntryViewHolder viewHolder = null; - - if (convertView == null) { - convertView = getInflater().inflate(R.layout.awesomebar_row, null); - - viewHolder = new AwesomeEntryViewHolder(); - viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); - viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); - viewHolder.faviconView = (FaviconView) convertView.findViewById(R.id.favicon); - viewHolder.bookmarkIconView = (ImageView) convertView.findViewById(R.id.bookmark_icon); - - convertView.setTag(viewHolder); - } else { - viewHolder = (AwesomeEntryViewHolder) convertView.getTag(); - } - - HistoryListAdapter adapter = getCursorAdapter(); - if (adapter == null) - return null; - - @SuppressWarnings("unchecked") - Map historyItem = - (Map) adapter.getChild(groupPosition, childPosition); - - String title = (String) historyItem.get(URLColumns.TITLE); - String url = (String) historyItem.get(URLColumns.URL); - - updateTitle(viewHolder.titleView, title, url); - updateUrl(viewHolder, url); - - byte[] b = (byte[]) historyItem.get(URLColumns.FAVICON); - Bitmap favicon = null; - - if (b != null) { - Bitmap bitmap = BitmapUtils.decodeByteArray(b); - if (bitmap != null) { - favicon = Favicons.getInstance().scaleImage(bitmap); - } - } - updateFavicon(viewHolder.faviconView, favicon, url); - - Integer bookmarkId = (Integer) historyItem.get(Combined.BOOKMARK_ID); - Integer display = (Integer) historyItem.get(Combined.DISPLAY); - - // The bookmark id will be 0 (null in database) when the url - // is not a bookmark. Reading list items are irrelevant in history - // tab. We should never show any sign or them. - int visibility = (bookmarkId != 0 && display != Combined.DISPLAY_READER ? - View.VISIBLE : View.GONE); - - viewHolder.bookmarkIconView.setVisibility(visibility); - viewHolder.bookmarkIconView.setImageResource(R.drawable.ic_awesomebar_star); - - return convertView; - } - } - - private static class GroupList extends LinkedList> { - private static final long serialVersionUID = 0L; - } - - private static class ChildrenList extends LinkedList> { - private static final long serialVersionUID = 0L; - } - - private class HistoryQueryTask extends AsyncTask>> { - private static final long MS_PER_DAY = 86400000; - private static final long MS_PER_WEEK = MS_PER_DAY * 7; - - @Override - protected Pair> doInBackground(Void... arg0) { - Cursor cursor = BrowserDB.getRecentHistory(getContentResolver(), MAX_RESULTS); - - Date now = new Date(); - now.setHours(0); - now.setMinutes(0); - now.setSeconds(0); - - long today = now.getTime(); - - // Split the list of urls into separate date range groups - // and show it in an expandable list view. - List childrenLists = new LinkedList(); - ChildrenList children = null; - GroupList groups = new GroupList(); - HistorySection section = null; - - // Move cursor before the first row in preparation - // for the iteration. - cursor.moveToPosition(-1); - - // Split the history query results into adapters per time - // section (today, yesterday, week, older). Queries on content - // Browser content provider don't support limitting the number - // of returned rows so we limit it here. - while (cursor.moveToNext()) { - long time = cursor.getLong(cursor.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED)); - HistorySection itemSection = getSectionForTime(time, today); - - if (section != itemSection) { - if (section != null) { - groups.add(createGroupItem(section)); - childrenLists.add(children); - } - - section = itemSection; - children = new ChildrenList(); - } - - children.add(createHistoryItem(cursor)); - } - - // Add any remaining section to the list if it hasn't - // been added to the list after the loop. - if (section != null && children != null) { - groups.add(createGroupItem(section)); - childrenLists.add(children); - } - - // Close the query cursor as we won't use it anymore - cursor.close(); - - // groups and childrenLists will be empty lists if there's no history - return Pair.>create(groups, childrenLists); - } - - public Map createHistoryItem(Cursor cursor) { - Map historyItem = new HashMap(); - - String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); - String title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); - byte[] favicon = cursor.getBlob(cursor.getColumnIndexOrThrow(URLColumns.FAVICON)); - Integer bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID)); - Integer historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID)); - Integer display = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.DISPLAY)); - - // Use the URL instead of an empty title for consistency with the normal URL - // bar view - this is the equivalent of getDisplayTitle() in Tab.java - if (title == null || title.length() == 0) - title = url; - - historyItem.put(URLColumns.URL, url); - historyItem.put(URLColumns.TITLE, title); - - if (favicon != null) - historyItem.put(URLColumns.FAVICON, favicon); - - historyItem.put(Combined.BOOKMARK_ID, bookmarkId); - historyItem.put(Combined.HISTORY_ID, historyId); - historyItem.put(Combined.DISPLAY, display); - - return historyItem; - } - - public Map createGroupItem(HistorySection section) { - Map groupItem = new HashMap(); - - groupItem.put(URLColumns.TITLE, getSectionName(section)); - - return groupItem; - } - - private String getSectionName(HistorySection section) { - Resources resources = mContext.getResources(); - - switch (section) { - case TODAY: - return resources.getString(R.string.history_today_section); - case YESTERDAY: - return resources.getString(R.string.history_yesterday_section); - case WEEK: - return resources.getString(R.string.history_week_section); - case OLDER: - return resources.getString(R.string.history_older_section); - } - - return null; - } - - private void expandAllGroups(ExpandableListView historyList) { - int groupCount = mCursorAdapter.getGroupCount(); - - for (int i = 0; i < groupCount; i++) { - historyList.expandGroup(i); - } - } - - private HistorySection getSectionForTime(long time, long today) { - long delta = today - time; - - if (delta < 0) { - return HistorySection.TODAY; - } - - if (delta < MS_PER_DAY) { - return HistorySection.YESTERDAY; - } - - if (delta < MS_PER_WEEK) { - return HistorySection.WEEK; - } - - return HistorySection.OLDER; - } - - @Override - protected void onPostExecute(Pair> result) { - mCursorAdapter = new HistoryListAdapter( - mContext, - result.first, - R.layout.awesomebar_header_row, - new String[] { URLColumns.TITLE }, - new int[] { R.id.title }, - result.second - ); - - if (mContentObserver == null) { - // Register an observer to update the history tab contents if they change. - mContentObserver = new ContentObserver(ThreadUtils.getBackgroundHandler()) { - @Override - public void onChange(boolean selfChange) { - mQueryTask = new HistoryQueryTask(); - mQueryTask.execute(); - } - }; - BrowserDB.registerHistoryObserver(getContentResolver(), mContentObserver); - } - - final ExpandableListView historyList = (ExpandableListView)getView(); - - // Hack: force this to the main thread, even though it should already be on it - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - historyList.setAdapter(mCursorAdapter); - expandAllGroups(historyList); - } - }); - - mQueryTask = null; - } - } - - public boolean handleItemClick(int groupPosition, int childPosition) { - HistoryListAdapter adapter = getCursorAdapter(); - if (adapter == null) - return false; - - @SuppressWarnings("unchecked") - Map historyItem = (Map) adapter.getChild(groupPosition, childPosition); - - String url = (String) historyItem.get(URLColumns.URL); - String title = (String) historyItem.get(URLColumns.TITLE); - sendToListener(url, title); - - return true; - } - - @Override - public ContextMenuSubject getSubject(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - ContextMenuSubject subject = null; - - if (!(menuInfo instanceof ExpandableListView.ExpandableListContextMenuInfo)) { - Log.e(LOGTAG, "menuInfo is not ExpandableListContextMenuInfo"); - return subject; - } - - ExpandableListView.ExpandableListContextMenuInfo info = (ExpandableListView.ExpandableListContextMenuInfo) menuInfo; - int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition); - int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition); - - // Check if long tap is on a header row - if (groupPosition < 0 || childPosition < 0) - return subject; - - ExpandableListView exList = (ExpandableListView) view; - - // The history list is backed by a SimpleExpandableListAdapter - @SuppressWarnings("rawtypes") - Map map = (Map) exList.getExpandableListAdapter().getChild(groupPosition, childPosition); - subject = new AwesomeBar.ContextMenuSubject((Integer) map.get(Combined.HISTORY_ID), - (String) map.get(URLColumns.URL), - (byte[]) map.get(URLColumns.FAVICON), - (String) map.get(URLColumns.TITLE), - null); - - setupMenu(menu, subject); - - menu.findItem(R.id.remove_bookmark).setVisible(false); - menu.findItem(R.id.edit_bookmark).setVisible(false); - menu.findItem(R.id.open_in_reader).setVisible(false); - - return subject; - } -} diff --git a/mobile/android/base/db/BrowserDB.java b/mobile/android/base/db/BrowserDB.java index 08c0e08af145..34f254b3309f 100644 --- a/mobile/android/base/db/BrowserDB.java +++ b/mobile/android/base/db/BrowserDB.java @@ -87,6 +87,8 @@ public class BrowserDB { public Bitmap getFaviconForUrl(ContentResolver cr, String uri); + public byte[] getFaviconBytesForUrl(ContentResolver cr, String uri); + public Cursor getFaviconsForUrls(ContentResolver cr, List urls); public String getFaviconUrlForHistoryUrl(ContentResolver cr, String url); @@ -237,6 +239,10 @@ public class BrowserDB { return sDb.getFaviconForUrl(cr, uri); } + public static byte[] getFaviconBytesForUrl(ContentResolver cr, String uri) { + return sDb.getFaviconBytesForUrl(cr, uri); + } + public static Cursor getFaviconsForUrls(ContentResolver cr, List urls) { return sDb.getFaviconsForUrls(cr, urls); } diff --git a/mobile/android/base/db/LocalBrowserDB.java b/mobile/android/base/db/LocalBrowserDB.java index 1814cd39cc92..947685876d69 100644 --- a/mobile/android/base/db/LocalBrowserDB.java +++ b/mobile/android/base/db/LocalBrowserDB.java @@ -22,6 +22,7 @@ import android.content.ContentValues; import android.database.ContentObserver; import android.database.Cursor; import android.database.CursorWrapper; +import android.database.DatabaseUtils; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; @@ -52,7 +53,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { // Use wrapped Boolean so that we can have a null state private Boolean mDesktopBookmarksExist; - private Boolean mReadingListItemsExist; private final Uri mBookmarksUriWithProfile; private final Uri mParentsUriWithProfile; @@ -70,15 +70,12 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { Bookmarks.URL, Bookmarks.TITLE, Bookmarks.TYPE, - Bookmarks.PARENT, - Bookmarks.KEYWORD, - Bookmarks.FAVICON }; + Bookmarks.PARENT }; public LocalBrowserDB(String profile) { mProfile = profile; mFolderIdMap = new HashMap(); mDesktopBookmarksExist = null; - mReadingListItemsExist = null; mBookmarksUriWithProfile = appendProfile(Bookmarks.CONTENT_URI); mParentsUriWithProfile = appendProfile(Bookmarks.PARENTS_CONTENT_URI); @@ -100,7 +97,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { @Override public void invalidateCachedState() { mDesktopBookmarksExist = null; - mReadingListItemsExist = null; } private Uri historyUriWithLimit(int limit) { @@ -332,7 +328,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { Combined.HISTORY_ID, Combined.URL, Combined.TITLE, - Combined.FAVICON, Combined.DISPLAY, Combined.DATE_LAST_VISITED, Combined.VISITS }, @@ -373,7 +368,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) { Cursor c = null; boolean addDesktopFolder = false; - boolean addReadingListFolder = false; // We always want to show mobile bookmarks in the root view. if (folderId == Bookmarks.FIXED_ROOT_ID) { @@ -382,10 +376,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { // We'll add a fake "Desktop Bookmarks" folder to the root view if desktop // bookmarks exist, so that the user can still access non-mobile bookmarks. addDesktopFolder = desktopBookmarksExist(cr); - - // We'll add the Reading List folder to the root view if any reading - // list items exist. - addReadingListFolder = readingListItemsExist(cr); } if (folderId == Bookmarks.FAKE_DESKTOP_FOLDER_ID) { @@ -413,9 +403,9 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { null); } - if (addDesktopFolder || addReadingListFolder) { + if (addDesktopFolder) { // Wrap cursor to add fake desktop bookmarks and reading list folders - c = new SpecialFoldersCursorWrapper(c, addDesktopFolder, addReadingListFolder); + c = new SpecialFoldersCursorWrapper(c, addDesktopFolder); } return new LocalDBCursor(c); @@ -452,29 +442,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { return mDesktopBookmarksExist; } - private boolean readingListItemsExist(ContentResolver cr) { - if (mReadingListItemsExist != null) - return mReadingListItemsExist; - - Cursor c = null; - int count = 0; - try { - c = cr.query(bookmarksUriWithLimit(1), - new String[] { Bookmarks._ID }, - Bookmarks.PARENT + " = ?", - new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }, - null); - count = c.getCount(); - } finally { - if (c != null) - c.close(); - } - - // Cache result for future queries - mReadingListItemsExist = (count > 0); - return mReadingListItemsExist; - } - @Override public int getReadingListCount(ContentResolver cr) { // This method is about the Reading List, not normal bookmarks @@ -727,6 +694,16 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { @Override public Bitmap getFaviconForUrl(ContentResolver cr, String uri) { + final byte[] b = getFaviconBytesForUrl(cr, uri); + if (b == null) { + return null; + } + + return BitmapUtils.decodeByteArray(b); + } + + @Override + public byte[] getFaviconBytesForUrl(ContentResolver cr, String uri) { Cursor c = null; byte[] b = null; @@ -737,20 +714,19 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { new String[] { uri }, null); - if (c.moveToFirst()) { - int faviconIndex = c.getColumnIndexOrThrow(Combined.FAVICON); - b = c.getBlob(faviconIndex); + if (!c.moveToFirst()) { + return null; } + + final int faviconIndex = c.getColumnIndexOrThrow(Combined.FAVICON); + b = c.getBlob(faviconIndex); } finally { - if (c != null) + if (c != null) { c.close(); + } } - if (b == null) { - return null; - } - - return BitmapUtils.decodeByteArray(b); + return b; } @Override @@ -777,23 +753,23 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { @Override public Cursor getFaviconsForUrls(ContentResolver cr, List urls) { StringBuilder selection = new StringBuilder(); - String[] selectionArgs = new String[urls.size()]; + selection.append(Favicons.URL + " IN ("); for (int i = 0; i < urls.size(); i++) { - final String url = urls.get(i); + final String url = urls.get(i); - if (i > 0) - selection.append(" OR "); + if (i > 0) + selection.append(", "); - selection.append(Favicons.URL + " = ?"); - selectionArgs[i] = url; + DatabaseUtils.appendEscapedSQLString(selection, url); } + selection.append(")"); + return cr.query(mCombinedUriWithProfile, new String[] { Combined.URL, Combined.FAVICON }, selection.toString(), - selectionArgs, - null); + null, null); } @Override @@ -1073,12 +1049,10 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { private int mIndexOffset; private int mDesktopBookmarksIndex = -1; - private int mReadingListIndex = -1; private boolean mAtDesktopBookmarksPosition = false; - private boolean mAtReadingListPosition = false; - public SpecialFoldersCursorWrapper(Cursor c, boolean showDesktopBookmarks, boolean showReadingList) { + public SpecialFoldersCursorWrapper(Cursor c, boolean showDesktopBookmarks) { super(c); mIndexOffset = 0; @@ -1087,11 +1061,6 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { mDesktopBookmarksIndex = mIndexOffset; mIndexOffset++; } - - if (showReadingList) { - mReadingListIndex = mIndexOffset; - mIndexOffset++; - } } @Override @@ -1102,9 +1071,8 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { @Override public boolean moveToPosition(int position) { mAtDesktopBookmarksPosition = (mDesktopBookmarksIndex == position); - mAtReadingListPosition = (mReadingListIndex == position); - if (mAtDesktopBookmarksPosition || mAtReadingListPosition) + if (mAtDesktopBookmarksPosition) return true; return super.moveToPosition(position - mIndexOffset); @@ -1112,7 +1080,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { @Override public long getLong(int columnIndex) { - if (!mAtDesktopBookmarksPosition && !mAtReadingListPosition) + if (!mAtDesktopBookmarksPosition) return super.getLong(columnIndex); if (columnIndex == getColumnIndex(Bookmarks.PARENT)) { @@ -1124,16 +1092,11 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { @Override public int getInt(int columnIndex) { - if (!mAtDesktopBookmarksPosition && !mAtReadingListPosition) + if (!mAtDesktopBookmarksPosition) return super.getInt(columnIndex); - if (columnIndex == getColumnIndex(Bookmarks._ID)) { - if (mAtDesktopBookmarksPosition) { + if (columnIndex == getColumnIndex(Bookmarks._ID) && mAtDesktopBookmarksPosition) return Bookmarks.FAKE_DESKTOP_FOLDER_ID; - } else if (mAtReadingListPosition) { - return Bookmarks.FIXED_READING_LIST_ID; - } - } if (columnIndex == getColumnIndex(Bookmarks.TYPE)) return Bookmarks.TYPE_FOLDER; @@ -1143,16 +1106,11 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { @Override public String getString(int columnIndex) { - if (!mAtDesktopBookmarksPosition && !mAtReadingListPosition) + if (!mAtDesktopBookmarksPosition) return super.getString(columnIndex); - if (columnIndex == getColumnIndex(Bookmarks.GUID)) { - if (mAtDesktopBookmarksPosition) { + if (columnIndex == getColumnIndex(Bookmarks.GUID) && mAtDesktopBookmarksPosition) return Bookmarks.FAKE_DESKTOP_FOLDER_GUID; - } else if (mAtReadingListPosition) { - return Bookmarks.READING_LIST_FOLDER_GUID; - } - } return ""; } diff --git a/mobile/android/base/gfx/LayerView.java b/mobile/android/base/gfx/LayerView.java index cc223c6311dd..3c1e705e0696 100644 --- a/mobile/android/base/gfx/LayerView.java +++ b/mobile/android/base/gfx/LayerView.java @@ -265,7 +265,7 @@ public class LayerView extends FrameLayout { // created, and it will be shown immediately at startup. Shortly // after, the tab's background color will be used before any content // is shown. - mTextureView.setBackgroundResource(R.color.background_normal); + mTextureView.setBackgroundColor(Color.WHITE); addView(mTextureView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } else { // This will stop PropertyAnimator from creating a drawing cache (i.e. a bitmap) @@ -273,7 +273,7 @@ public class LayerView extends FrameLayout { setWillNotCacheDrawing(false); mSurfaceView = new LayerSurfaceView(getContext(), this); - mSurfaceView.setBackgroundResource(R.color.background_normal); + mSurfaceView.setBackgroundColor(Color.WHITE); addView(mSurfaceView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); SurfaceHolder holder = mSurfaceView.getHolder(); diff --git a/mobile/android/base/home/BookmarkFolderView.java b/mobile/android/base/home/BookmarkFolderView.java new file mode 100644 index 000000000000..996e6d9ba3ad --- /dev/null +++ b/mobile/android/base/home/BookmarkFolderView.java @@ -0,0 +1,55 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TextView; + +public class BookmarkFolderView extends TextView { + private static final int[] STATE_OPEN = { R.attr.state_open }; + + private boolean mIsOpen = false; + + public BookmarkFolderView(Context context) { + super(context); + } + + public BookmarkFolderView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BookmarkFolderView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + + if (mIsOpen) { + mergeDrawableStates(drawableState, STATE_OPEN); + } + + return drawableState; + } + + public void open() { + if (!mIsOpen) { + mIsOpen = true; + refreshDrawableState(); + } + } + + public void close() { + if (mIsOpen) { + mIsOpen = false; + refreshDrawableState(); + } + } +} diff --git a/mobile/android/base/home/BookmarkThumbnailView.java b/mobile/android/base/home/BookmarkThumbnailView.java new file mode 100644 index 000000000000..bc8d04dd30e5 --- /dev/null +++ b/mobile/android/base/home/BookmarkThumbnailView.java @@ -0,0 +1,102 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.ThumbnailHelper; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * A height constrained ImageView to show thumbnails of top bookmarks. + */ +public class BookmarkThumbnailView extends ImageView { + private static final String LOGTAG = "GeckoBookmarkThumbnailView"; + + // 27.34% opacity filter for the dominant color. + private static final int COLOR_FILTER = 0x46FFFFFF; + + // Default filter color for "Add a bookmark" views. + private static final int DEFAULT_COLOR = 0x46ECF0F3; + + // Stroke width for the border. + private final float mStrokeWidth = getResources().getDisplayMetrics().density * 2; + + // Paint for drawing the border. + private static Paint sBorderPaint; + + // Initializing the static border paint. + static { + sBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + sBorderPaint.setColor(0xFFCFD9E1); + sBorderPaint.setStyle(Paint.Style.STROKE); + } + + public BookmarkThumbnailView(Context context) { + this(context, null); + + // A border will be drawn if needed. + setWillNotDraw(false); + } + + public BookmarkThumbnailView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.bookmarkThumbnailViewStyle); + } + + public BookmarkThumbnailView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Measure the view to determine the measured width and height. + * The height is constrained by the measured width. + * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent. + * @param heightMeasureSpec vertical space requirements as imposed by the parent, but ignored. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Default measuring. + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Force the height based on the aspect ratio. + final int width = getMeasuredWidth(); + final int height = (int) (width * ThumbnailHelper.THUMBNAIL_ASPECT_RATIO); + setMeasuredDimension(width, height); + } + + /** + * {@inheritDoc} + */ + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (getBackground() == null) { + sBorderPaint.setStrokeWidth(mStrokeWidth); + canvas.drawRect(0, 0, getWidth(), getHeight(), sBorderPaint); + } + } + + /** + * Sets the background to a Drawable by applying the specified color as a filter. + * + * @param color the color filter to apply over the drawable. + */ + @Override + public void setBackgroundColor(int color) { + int colorFilter = color == 0 ? DEFAULT_COLOR : color & COLOR_FILTER; + Drawable drawable = getResources().getDrawable(R.drawable.bookmark_thumbnail_bg); + drawable.setColorFilter(colorFilter, Mode.SRC_ATOP); + setBackgroundDrawable(drawable); + } +} diff --git a/mobile/android/base/home/BookmarksListAdapter.java b/mobile/android/base/home/BookmarksListAdapter.java new file mode 100644 index 000000000000..b139e1fa293f --- /dev/null +++ b/mobile/android/base/home/BookmarksListAdapter.java @@ -0,0 +1,190 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserContract.Bookmarks; + +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.util.Pair; +import android.view.View; + +import java.util.LinkedList; + +/** + * Adapter to back the BookmarksListView with a list of bookmarks. + */ +class BookmarksListAdapter extends MultiTypeCursorAdapter { + private static final int VIEW_TYPE_ITEM = 0; + private static final int VIEW_TYPE_FOLDER = 1; + + private static final int[] VIEW_TYPES = new int[] { VIEW_TYPE_ITEM, VIEW_TYPE_FOLDER }; + private static final int[] LAYOUT_TYPES = new int[] { R.layout.bookmark_item_row, R.layout.bookmark_folder_row }; + + // A listener that knows how to refresh the list for a given folder id. + // This is usually implemented by the enclosing fragment/activity. + public static interface OnRefreshFolderListener { + // The folder id to refresh the list with. + public void onRefreshFolder(int folderId); + } + + // mParentStack holds folder id/title pairs that allow us to navigate + // back up the folder heirarchy. + private LinkedList> mParentStack; + + // Refresh folder listener. + private OnRefreshFolderListener mListener; + + public BookmarksListAdapter(Context context, Cursor cursor) { + // Initializing with a null cursor. + super(context, cursor, VIEW_TYPES, LAYOUT_TYPES); + + mParentStack = new LinkedList>(); + + // Add the root folder to the stack + Pair rootFolder = new Pair(Bookmarks.FIXED_ROOT_ID, ""); + mParentStack.addFirst(rootFolder); + } + + // Refresh the current folder by executing a new task. + private void refreshCurrentFolder() { + if (mListener != null) { + mListener.onRefreshFolder(mParentStack.peek().first); + } + } + + /** + * Moves to parent folder, if one exists. + */ + public void moveToParentFolder() { + // If we're already at the root, we can't move to a parent folder + if (mParentStack.size() != 1) { + mParentStack.removeFirst(); + refreshCurrentFolder(); + } + } + + /** + * Moves to child folder, given a folderId. + * + * @param folderId The id of the folder to show. + * @param folderTitle The title of the folder to show. + */ + public void moveToChildFolder(int folderId, String folderTitle) { + Pair folderPair = new Pair(folderId, folderTitle); + mParentStack.addFirst(folderPair); + refreshCurrentFolder(); + } + + /** + * Set a listener that can refresh this adapter. + * + * @param listener The listener that can refresh the adapter. + */ + public void setOnRefreshFolderListener(OnRefreshFolderListener listener) { + mListener = listener; + } + + @Override + public int getItemViewType(int position) { + // The position also reflects the opened child folder row. + if (isShowingChildFolder()) { + if (position == 0) { + return VIEW_TYPE_FOLDER; + } + + // Accounting for the folder view. + position--; + } + + final Cursor c = getCursor(position); + if (c.getInt(c.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER) { + return VIEW_TYPE_FOLDER; + } + + // Default to returning normal item type. + return VIEW_TYPE_ITEM; + } + + /** + * Get the title of the folder given a cursor moved to the position. + * + * @param context The context of the view. + * @param cursor A cursor moved to the required position. + * @return The title of the folder at the position. + */ + public String getFolderTitle(Context context, Cursor c) { + String guid = c.getString(c.getColumnIndexOrThrow(Bookmarks.GUID)); + + // If we don't have a special GUID, just return the folder title from the DB. + if (guid == null || guid.length() == 12) { + return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE)); + } + + Resources res = context.getResources(); + + // Use localized strings for special folder names. + if (guid.equals(Bookmarks.FAKE_DESKTOP_FOLDER_GUID)) { + return res.getString(R.string.bookmarks_folder_desktop); + } else if (guid.equals(Bookmarks.MENU_FOLDER_GUID)) { + return res.getString(R.string.bookmarks_folder_menu); + } else if (guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID)) { + return res.getString(R.string.bookmarks_folder_toolbar); + } else if (guid.equals(Bookmarks.UNFILED_FOLDER_GUID)) { + return res.getString(R.string.bookmarks_folder_unfiled); + } + + // If for some reason we have a folder with a special GUID, but it's not one of + // the special folders we expect in the UI, just return the title from the DB. + return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE)); + } + + /** + * @return true, if currently showing a child folder, false otherwise. + */ + public boolean isShowingChildFolder() { + return (mParentStack.peek().first != Bookmarks.FIXED_ROOT_ID); + } + + @Override + public int getCount() { + return super.getCount() + (isShowingChildFolder() ? 1 : 0); + } + + @Override + public void bindView(View view, Context context, int position) { + final int viewType = getItemViewType(position); + + final Cursor cursor; + if (isShowingChildFolder()) { + if (position == 0) { + cursor = null; + } else { + // Accounting for the folder view. + position--; + cursor = getCursor(position); + } + } else { + cursor = getCursor(position); + } + + if (viewType == VIEW_TYPE_ITEM) { + final TwoLinePageRow row = (TwoLinePageRow) view; + row.updateFromCursor(cursor); + } else { + final BookmarkFolderView row = (BookmarkFolderView) view; + if (cursor == null) { + row.setText(mParentStack.peek().second); + row.open(); + } else { + row.setText(getFolderTitle(context, cursor)); + row.close(); + } + } + } +} diff --git a/mobile/android/base/home/BookmarksListView.java b/mobile/android/base/home/BookmarksListView.java new file mode 100644 index 000000000000..c3246aede8e4 --- /dev/null +++ b/mobile/android/base/home/BookmarksListView.java @@ -0,0 +1,144 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserContract.Bookmarks; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; + +import android.content.Context; +import android.database.Cursor; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.AdapterView; +import android.widget.HeaderViewListAdapter; +import android.widget.ListAdapter; +import android.widget.ListView; + +import java.util.EnumSet; + +/** + * A ListView of bookmarks. + */ +public class BookmarksListView extends HomeListView + implements AdapterView.OnItemClickListener{ + public static final String LOGTAG = "GeckoBookmarksListView"; + + // The last motion event that was intercepted. + private MotionEvent mMotionEvent; + + // The default touch slop. + private int mTouchSlop; + + public BookmarksListView(Context context) { + this(context, null); + } + + public BookmarksListView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.bookmarksListViewStyle); + } + + public BookmarksListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Scaled touch slop for this context. + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + setOnItemClickListener(this); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + switch(event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + // Store the event by obtaining a copy. + mMotionEvent = MotionEvent.obtain(event); + break; + } + + case MotionEvent.ACTION_MOVE: { + if ((mMotionEvent != null) && + (Math.abs(event.getY() - mMotionEvent.getY()) > mTouchSlop)) { + // The user is scrolling. Pass the last event to this view, + // and make this view scroll. + onTouchEvent(mMotionEvent); + return true; + } + break; + } + + default: { + mMotionEvent = null; + break; + } + } + + // Do default interception. + return super.onInterceptTouchEvent(event); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final ListView list = (ListView) parent; + final int headerCount = list.getHeaderViewsCount(); + + if (position < headerCount) { + // The click is on a header, don't do anything. + return; + } + + // Absolute position for the adapter. + position -= headerCount; + + BookmarksListAdapter adapter; + ListAdapter listAdapter = getAdapter(); + if (listAdapter instanceof HeaderViewListAdapter) { + adapter = (BookmarksListAdapter) ((HeaderViewListAdapter) listAdapter).getWrappedAdapter(); + } else { + adapter = (BookmarksListAdapter) listAdapter; + } + + if (adapter.isShowingChildFolder()) { + if (position == 0) { + // If we tap on an opened folder, move back to parent folder. + adapter.moveToParentFolder(); + return; + } + + // Accounting for the folder view. + position--; + } + + final Cursor cursor = adapter.getCursor(); + if (cursor == null) { + return; + } + + cursor.moveToPosition(position); + + int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE)); + if (type == Bookmarks.TYPE_FOLDER) { + // If we're clicking on a folder, update adapter to move to that folder + final int folderId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); + final String folderTitle = adapter.getFolderTitle(parent.getContext(), cursor); + adapter.moveToChildFolder(folderId, folderTitle); + } else { + // Otherwise, just open the URL + final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); + + // This item is a TwoLinePageRow, so we allow switch-to-tab. + getOnUrlOpenListener().onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); + } + } +} diff --git a/mobile/android/base/home/BookmarksPage.java b/mobile/android/base/home/BookmarksPage.java new file mode 100644 index 000000000000..ac5d247a65aa --- /dev/null +++ b/mobile/android/base/home/BookmarksPage.java @@ -0,0 +1,593 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.Favicons; +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.db.BrowserContract.Bookmarks; +import org.mozilla.gecko.db.BrowserContract.Thumbnails; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener; +import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; +import org.mozilla.gecko.home.PinBookmarkDialog.OnBookmarkSelectedListener; +import org.mozilla.gecko.home.TopBookmarksAdapter.Thumbnail; +import org.mozilla.gecko.home.TopBookmarksView.OnPinBookmarkListener; +import org.mozilla.gecko.home.TopBookmarksView.TopBookmarksContextMenuInfo; +import org.mozilla.gecko.util.ThreadUtils; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Configuration; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.AsyncTaskLoader; +import android.support.v4.content.Loader; +import android.text.TextUtils; +import android.util.Log; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * A page in about:home that displays a ListView of bookmarks. + */ +public class BookmarksPage extends HomeFragment { + public static final String LOGTAG = "GeckoBookmarksPage"; + + // Cursor loader ID for list of bookmarks. + private static final int LOADER_ID_BOOKMARKS_LIST = 0; + + // Cursor loader ID for grid of bookmarks. + private static final int LOADER_ID_TOP_BOOKMARKS = 1; + + // Loader ID for thumbnails. + private static final int LOADER_ID_THUMBNAILS = 2; + + // Key for bookmarks folder id. + private static final String BOOKMARKS_FOLDER_KEY = "folder_id"; + + // Key for thumbnail urls. + private static final String THUMBNAILS_URLS_KEY = "urls"; + + // List of bookmarks. + private BookmarksListView mList; + + // Grid of top bookmarks. + private TopBookmarksView mTopBookmarks; + + // Adapter for list of bookmarks. + private BookmarksListAdapter mListAdapter; + + // Adapter for grid of bookmarks. + private TopBookmarksAdapter mTopBookmarksAdapter; + + // Callback for cursor loaders. + private CursorLoaderCallbacks mLoaderCallbacks; + + // Callback for thumbnail loader. + private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks; + + // Listener for pinning bookmarks. + private PinBookmarkListener mPinBookmarkListener; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + BookmarksListView list = (BookmarksListView) inflater.inflate(R.layout.home_bookmarks_page, container, false); + + mTopBookmarks = new TopBookmarksView(getActivity()); + list.addHeaderView(mTopBookmarks); + + return list; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + OnUrlOpenListener listener = null; + try { + listener = (OnUrlOpenListener) getActivity(); + } catch (ClassCastException e) { + throw new ClassCastException(getActivity().toString() + + " must implement HomePager.OnUrlOpenListener"); + } + + mPinBookmarkListener = new PinBookmarkListener(); + + mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list); + mList.setTag(HomePager.LIST_TAG_BOOKMARKS); + mList.setOnUrlOpenListener(listener); + + mTopBookmarks.setOnUrlOpenListener(listener); + mTopBookmarks.setOnPinBookmarkListener(mPinBookmarkListener); + + registerForContextMenu(mList); + registerForContextMenu(mTopBookmarks); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Activity activity = getActivity(); + + // Setup the top bookmarks adapter. + mTopBookmarksAdapter = new TopBookmarksAdapter(activity, null); + mTopBookmarks.setAdapter(mTopBookmarksAdapter); + + // Setup the list adapter. + mListAdapter = new BookmarksListAdapter(activity, null); + mListAdapter.setOnRefreshFolderListener(new OnRefreshFolderListener() { + @Override + public void onRefreshFolder(int folderId) { + // Restart the loader with folder as the argument. + Bundle bundle = new Bundle(); + bundle.putInt(BOOKMARKS_FOLDER_KEY, folderId); + getLoaderManager().restartLoader(LOADER_ID_BOOKMARKS_LIST, bundle, mLoaderCallbacks); + } + }); + mList.setAdapter(mListAdapter); + + // Create callbacks before the initial loader is started. + mLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks(); + loadIfVisible(); + } + + @Override + public void onDestroyView() { + mList = null; + mListAdapter = null; + mTopBookmarks = null; + mTopBookmarksAdapter = null; + mPinBookmarkListener = null; + super.onDestroyView(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + // Reattach the fragment, forcing a reinflation of its view. + // We use commitAllowingStateLoss() instead of commit() here to avoid + // an IllegalStateException. If the phone is rotated while Fennec + // is in the background, onConfigurationChanged() is fired. + // onConfigurationChanged() is called before onResume(), so + // using commit() would throw an IllegalStateException since it can't + // be used between the Activity's onSaveInstanceState() and + // onResume(). + if (isVisible()) { + getFragmentManager().beginTransaction() + .detach(this) + .attach(this) + .commitAllowingStateLoss(); + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { + if (menuInfo == null) { + return; + } + + // HomeFragment will handle the default case. + if (menuInfo instanceof HomeContextMenuInfo) { + super.onCreateContextMenu(menu, view, menuInfo); + } + + if (!(menuInfo instanceof TopBookmarksContextMenuInfo)) { + return; + } + + MenuInflater inflater = new MenuInflater(view.getContext()); + inflater.inflate(R.menu.top_bookmarks_contextmenu, menu); + + TopBookmarksContextMenuInfo info = (TopBookmarksContextMenuInfo) menuInfo; + + if (!TextUtils.isEmpty(info.url)) { + if (info.isPinned) { + menu.findItem(R.id.top_bookmarks_pin).setVisible(false); + } else { + menu.findItem(R.id.top_bookmarks_unpin).setVisible(false); + } + } else { + menu.findItem(R.id.top_bookmarks_open_new_tab).setVisible(false); + menu.findItem(R.id.top_bookmarks_open_private_tab).setVisible(false); + menu.findItem(R.id.top_bookmarks_pin).setVisible(false); + menu.findItem(R.id.top_bookmarks_unpin).setVisible(false); + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + ContextMenuInfo menuInfo = item.getMenuInfo(); + + // HomeFragment will handle the default case. + if (menuInfo == null || !(menuInfo instanceof TopBookmarksContextMenuInfo)) { + return false; + } + + TopBookmarksContextMenuInfo info = (TopBookmarksContextMenuInfo) menuInfo; + final Activity activity = getActivity(); + + final int itemId = item.getItemId(); + if (itemId == R.id.top_bookmarks_open_new_tab || itemId == R.id.top_bookmarks_open_private_tab) { + if (info.url == null) { + Log.e(LOGTAG, "Can't open in new tab because URL is null"); + return false; + } + + int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND; + if (item.getItemId() == R.id.top_bookmarks_open_private_tab) + flags |= Tabs.LOADURL_PRIVATE; + + Tabs.getInstance().loadUrl(info.url, flags); + Toast.makeText(activity, R.string.new_tab_opened, Toast.LENGTH_SHORT).show(); + return true; + } + + if (itemId == R.id.top_bookmarks_pin) { + final String url = info.url; + final String title = info.title; + final int position = info.position; + final Context context = getActivity().getApplicationContext(); + + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + BrowserDB.pinSite(context.getContentResolver(), url, title, position); + } + }); + + return true; + } + + if (itemId == R.id.top_bookmarks_unpin) { + final int position = info.position; + final Context context = getActivity().getApplicationContext(); + + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + BrowserDB.unpinSite(context.getContentResolver(), position); + } + }); + + return true; + } + + if (itemId == R.id.top_bookmarks_edit) { + mPinBookmarkListener.onPinBookmark(info.position); + return true; + } + + return false; + } + + @Override + protected void load() { + final LoaderManager manager = getLoaderManager(); + manager.initLoader(LOADER_ID_BOOKMARKS_LIST, null, mLoaderCallbacks); + manager.initLoader(LOADER_ID_TOP_BOOKMARKS, null, mLoaderCallbacks); + } + + /** + * Listener for pinning bookmarks. + */ + private class PinBookmarkListener implements OnPinBookmarkListener, + OnBookmarkSelectedListener { + // Tag for the PinBookmarkDialog fragment. + private static final String TAG_PIN_BOOKMARK = "pin_bookmark"; + + // Position of the pin. + private int mPosition; + + @Override + public void onPinBookmark(int position) { + mPosition = position; + + final FragmentManager manager = getActivity().getSupportFragmentManager(); + PinBookmarkDialog dialog = (PinBookmarkDialog) manager.findFragmentByTag(TAG_PIN_BOOKMARK); + if (dialog == null) { + dialog = PinBookmarkDialog.newInstance(); + } + + dialog.setOnBookmarkSelectedListener(this); + dialog.show(manager, TAG_PIN_BOOKMARK); + } + + @Override + public void onBookmarkSelected(final String url, final String title) { + final int position = mPosition; + final Context context = getActivity().getApplicationContext(); + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + BrowserDB.pinSite(context.getContentResolver(), url, title, position); + } + }); + } + } + + /** + * Loader for the list for bookmarks. + */ + private static class BookmarksLoader extends SimpleCursorLoader { + private final int mFolderId; + + public BookmarksLoader(Context context) { + this(context, Bookmarks.FIXED_ROOT_ID); + } + + public BookmarksLoader(Context context, int folderId) { + super(context); + mFolderId = folderId; + } + + @Override + public Cursor loadCursor() { + return BrowserDB.getBookmarksInFolder(getContext().getContentResolver(), mFolderId); + } + } + + /** + * Loader for the grid for top bookmarks. + */ + private static class TopBookmarksLoader extends SimpleCursorLoader { + public TopBookmarksLoader(Context context) { + super(context); + } + + @Override + public Cursor loadCursor() { + final int max = getContext().getResources().getInteger(R.integer.number_of_top_sites); + return BrowserDB.getTopSites(getContext().getContentResolver(), max); + } + } + + /** + * Loader callbacks for the LoaderManager of this fragment. + */ + private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { + public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { + super(context, loaderManager); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + switch(id) { + case LOADER_ID_BOOKMARKS_LIST: { + if (args == null) { + return new BookmarksLoader(getActivity()); + } else { + return new BookmarksLoader(getActivity(), args.getInt(BOOKMARKS_FOLDER_KEY)); + } + } + + case LOADER_ID_TOP_BOOKMARKS: { + return new TopBookmarksLoader(getActivity()); + } + + default: { + return super.onCreateLoader(id, args); + } + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + final int loaderId = loader.getId(); + switch(loaderId) { + case LOADER_ID_BOOKMARKS_LIST: { + mListAdapter.swapCursor(c); + loadFavicons(c); + break; + } + + case LOADER_ID_TOP_BOOKMARKS: { + mTopBookmarksAdapter.swapCursor(c); + + // Load the thumbnails. + if (c.getCount() > 0 && c.moveToFirst()) { + final ArrayList urls = new ArrayList(); + do { + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + urls.add(url); + } while (c.moveToNext()); + + if (urls.size() > 0) { + Bundle bundle = new Bundle(); + bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls); + getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks); + } + } + break; + } + + default: { + super.onLoadFinished(loader, c); + break; + } + } + } + + @Override + public void onLoaderReset(Loader loader) { + final int loaderId = loader.getId(); + switch(loaderId) { + case LOADER_ID_BOOKMARKS_LIST: { + if (mList != null) { + mListAdapter.swapCursor(null); + } + break; + } + + case LOADER_ID_TOP_BOOKMARKS: { + if (mTopBookmarks != null) { + mTopBookmarksAdapter.swapCursor(null); + break; + } + } + + default: { + super.onLoaderReset(loader); + break; + } + } + } + + @Override + public void onFaviconsLoaded() { + mListAdapter.notifyDataSetChanged(); + } + } + + /** + * An AsyncTaskLoader to load the thumbnails from a cursor. + */ + private static class ThumbnailsLoader extends AsyncTaskLoader> { + private Map mThumbnails; + private ArrayList mUrls; + + public ThumbnailsLoader(Context context, ArrayList urls) { + super(context); + mUrls = urls; + } + + @Override + public Map loadInBackground() { + if (mUrls == null || mUrls.size() == 0) { + return null; + } + + final Map thumbnails = new HashMap(); + + // Query the DB for thumbnails. + final ContentResolver cr = getContext().getContentResolver(); + final Cursor cursor = BrowserDB.getThumbnailsForUrls(cr, mUrls); + + try { + if (cursor != null && cursor.moveToFirst()) { + do { + // Try to get the thumbnail, if cursor is valid. + String url = cursor.getString(cursor.getColumnIndexOrThrow(Thumbnails.URL)); + final byte[] b = cursor.getBlob(cursor.getColumnIndexOrThrow(Thumbnails.DATA)); + final Bitmap bitmap = (b == null ? null : BitmapUtils.decodeByteArray(b)); + + if (bitmap != null) { + thumbnails.put(url, new Thumbnail(bitmap, true)); + } + } while (cursor.moveToNext()); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + // Query the DB for favicons for the urls without thumbnails. + for (String url : mUrls) { + if (!thumbnails.containsKey(url)) { + final Bitmap bitmap = BrowserDB.getFaviconForUrl(cr, url); + if (bitmap != null) { + // Favicons.scaleImage can return several different size favicons, + // but will at least prevent this from being too large. + thumbnails.put(url, new Thumbnail(Favicons.getInstance().scaleImage(bitmap), false)); + } + } + } + + return thumbnails; + } + + @Override + public void deliverResult(Map thumbnails) { + if (isReset()) { + mThumbnails = null; + return; + } + + mThumbnails = thumbnails; + + if (isStarted()) { + super.deliverResult(thumbnails); + } + } + + @Override + protected void onStartLoading() { + if (mThumbnails != null) { + deliverResult(mThumbnails); + } + + if (takeContentChanged() || mThumbnails == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + public void onCanceled(Map thumbnails) { + mThumbnails = null; + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped. + onStopLoading(); + + mThumbnails = null; + } + } + + /** + * Loader callbacks for the thumbnails on TopBookmarksView. + */ + private class ThumbnailsLoaderCallbacks implements LoaderCallbacks> { + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY)); + } + + @Override + public void onLoadFinished(Loader> loader, Map thumbnails) { + if (mTopBookmarksAdapter != null) { + mTopBookmarksAdapter.updateThumbnails(thumbnails); + } + } + + @Override + public void onLoaderReset(Loader> loader) { + if (mTopBookmarksAdapter != null) { + mTopBookmarksAdapter.updateThumbnails(null); + } + } + } +} diff --git a/mobile/android/base/home/BrowserSearch.java b/mobile/android/base/home/BrowserSearch.java new file mode 100644 index 000000000000..e23c66036c24 --- /dev/null +++ b/mobile/android/base/home/BrowserSearch.java @@ -0,0 +1,878 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.AutocompleteHandler; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; +import org.mozilla.gecko.PrefsHelper; +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; +import org.mozilla.gecko.home.SearchLoader.SearchCursorLoader; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.StringUtils; +import org.mozilla.gecko.util.ThreadUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.Activity; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.AsyncTaskLoader; +import android.support.v4.content.Loader; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.view.WindowManager.LayoutParams; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.TranslateAnimation; +import android.widget.AdapterView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * Fragment that displays frecency search results in a ListView. + */ +public class BrowserSearch extends HomeFragment + implements GeckoEventListener { + // Logging tag name + private static final String LOGTAG = "GeckoBrowserSearch"; + + // Cursor loader ID for search query + private static final int LOADER_ID_SEARCH = 0; + + // AsyncTask loader ID for suggestion query + private static final int LOADER_ID_SUGGESTION = 1; + + // Timeout for the suggestion client to respond + private static final int SUGGESTION_TIMEOUT = 3000; + + // Maximum number of results returned by the suggestion client + private static final int SUGGESTION_MAX = 3; + + // The maximum number of rows deep in a search we'll dig + // for an autocomplete result + private static final int MAX_AUTOCOMPLETE_SEARCH = 20; + + // Duration for fade-in animation + private static final int ANIMATION_DURATION = 250; + + // Holds the current search term to use in the query + private String mSearchTerm; + + // Adapter for the list of search results + private SearchAdapter mAdapter; + + // The view shown by the fragment + private LinearLayout mView; + + // The list showing search results + private ListView mList; + + // Client that performs search suggestion queries + private volatile SuggestClient mSuggestClient; + + // List of search engines from gecko + private ArrayList mSearchEngines; + + // Whether search suggestions are enabled or not + private boolean mSuggestionsEnabled; + + // Callbacks used for the search and favicon cursor loaders + private CursorLoaderCallbacks mCursorLoaderCallbacks; + + // Callbacks used for the search suggestion loader + private SuggestionLoaderCallbacks mSuggestionLoaderCallbacks; + + // Inflater used by the adapter + private LayoutInflater mInflater; + + // Autocomplete handler used when filtering results + private AutocompleteHandler mAutocompleteHandler; + + // On URL open listener + private OnUrlOpenListener mUrlOpenListener; + + // On search listener + private OnSearchListener mSearchListener; + + // On edit suggestion listener + private OnEditSuggestionListener mEditSuggestionListener; + + // Whether the suggestions will fade in when shown + private boolean mAnimateSuggestions; + + // Opt-in prompt view for search suggestions + private View mSuggestionsOptInPrompt; + + public interface OnSearchListener { + public void onSearch(String engineId, String text); + } + + public interface OnEditSuggestionListener { + public void onEditSuggestion(String suggestion); + } + + public static BrowserSearch newInstance() { + return new BrowserSearch(); + } + + public BrowserSearch() { + mSearchTerm = ""; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mUrlOpenListener = (OnUrlOpenListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement BrowserSearch.OnUrlOpenListener"); + } + + try { + mSearchListener = (OnSearchListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement BrowserSearch.OnSearchListener"); + } + + try { + mEditSuggestionListener = (OnEditSuggestionListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement BrowserSearch.OnEditSuggestionListener"); + } + + mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public void onDetach() { + super.onDetach(); + + mInflater = null; + mAutocompleteHandler = null; + mUrlOpenListener = null; + mSearchListener = null; + mEditSuggestionListener = null; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mSearchEngines = new ArrayList(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mSearchEngines = null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // All list views are styled to look the same with a global activity theme. + // If the style of the list changes, inflate it from an XML. + mView = (LinearLayout) inflater.inflate(R.layout.browser_search, container, false); + mList = (ListView) mView.findViewById(R.id.home_list_view); + + return mView; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + unregisterEventListener("SearchEngines:Data"); + + mView = null; + mList = null; + mSuggestionsOptInPrompt = null; + mSuggestClient = null; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + // Account for the search engines + position -= getSuggestEngineCount(); + final Cursor c = mAdapter.getCursor(position); + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + + // This item is a TwoLinePageRow, so we allow switch-to-tab. + mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); + } + }); + + final ListSelectionListener listener = new ListSelectionListener(); + mList.setOnItemSelectedListener(listener); + mList.setOnFocusChangeListener(listener); + + mList.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, android.view.KeyEvent event) { + final View selected = mList.getSelectedView(); + + if (selected instanceof SearchEngineRow) { + return selected.onKeyDown(keyCode, event); + } + return false; + } + }); + + registerForContextMenu(mList); + registerEventListener("SearchEngines:Data"); + + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null)); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Activity activity = getActivity(); + + // Intialize the search adapter + mAdapter = new SearchAdapter(activity); + mList.setAdapter(mAdapter); + + // Only create an instance when we need it + mSuggestionLoaderCallbacks = null; + + // Create callbacks before the initial loader is started + mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + loadIfVisible(); + } + + @Override + public void handleMessage(String event, final JSONObject message) { + if (event.equals("SearchEngines:Data")) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + setSearchEngines(message); + } + }); + } + } + + @Override + protected void load() { + getLoaderManager().initLoader(LOADER_ID_SEARCH, null, mCursorLoaderCallbacks); + } + + private void handleAutocomplete(String searchTerm, Cursor c) { + if (TextUtils.isEmpty(mSearchTerm) || c == null || mAutocompleteHandler == null) { + return; + } + + // Avoid searching the path if we don't have to. Currently just + // decided by if there is a '/' character in the string. + final boolean searchPath = (searchTerm.indexOf("/") > 0); + final String autocompletion = findAutocompletion(searchTerm, c, searchPath); + + if (autocompletion != null && mAutocompleteHandler != null) { + mAutocompleteHandler.onAutocomplete(autocompletion); + mAutocompleteHandler = null; + } + } + + private String findAutocompletion(String searchTerm, Cursor c, boolean searchPath) { + if (!c.moveToFirst()) { + return null; + } + + final int urlIndex = c.getColumnIndexOrThrow(URLColumns.URL); + int searchCount = 0; + + do { + final Uri url = Uri.parse(c.getString(urlIndex)); + final String host = StringUtils.stripCommonSubdomains(url.getHost()); + + // Host may be null for about pages + if (host == null) { + continue; + } + + final StringBuilder hostBuilder = new StringBuilder(host); + if (hostBuilder.indexOf(searchTerm) == 0) { + return hostBuilder.append("/").toString(); + } + + if (searchPath) { + final List path = url.getPathSegments(); + + for (String s : path) { + hostBuilder.append("/").append(s); + + if (hostBuilder.indexOf(searchTerm) == 0) { + return hostBuilder.append("/").toString(); + } + } + } + + searchCount++; + } while (searchCount < MAX_AUTOCOMPLETE_SEARCH && c.moveToNext()); + + return null; + } + + private void filterSuggestions() { + if (mSuggestClient == null || !mSuggestionsEnabled) { + return; + } + + if (mSuggestionLoaderCallbacks == null) { + mSuggestionLoaderCallbacks = new SuggestionLoaderCallbacks(); + } + + getLoaderManager().restartLoader(LOADER_ID_SUGGESTION, null, mSuggestionLoaderCallbacks); + } + + private void setSuggestions(ArrayList suggestions) { + mSearchEngines.get(0).suggestions = suggestions; + mAdapter.notifyDataSetChanged(); + } + + private void setSearchEngines(JSONObject data) { + // This method is called via a Runnable posted from the Gecko thread, so + // it's possible the fragment and/or its view has been destroyed by the + // time we get here. If so, just abort. + if (mView == null) { + return; + } + + try { + final JSONObject suggest = data.getJSONObject("suggest"); + final String suggestEngine = suggest.optString("engine", null); + final String suggestTemplate = suggest.optString("template", null); + final boolean suggestionsPrompted = suggest.getBoolean("prompted"); + final JSONArray engines = data.getJSONArray("searchEngines"); + + mSuggestionsEnabled = suggest.getBoolean("enabled"); + + ArrayList searchEngines = new ArrayList(); + for (int i = 0; i < engines.length(); i++) { + final JSONObject engineJSON = engines.getJSONObject(i); + final String name = engineJSON.getString("name"); + final String identifier = engineJSON.getString("identifier"); + final String iconURI = engineJSON.getString("iconURI"); + final Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconURI); + + if (name.equals(suggestEngine) && suggestTemplate != null) { + // Suggest engine should be at the front of the list + searchEngines.add(0, new SearchEngine(name, identifier, icon)); + + // The only time Tabs.getInstance().getSelectedTab() should + // be null is when we're restoring after a crash. We should + // never restore private tabs when that happens, so it + // should be safe to assume that null means non-private. + Tab tab = Tabs.getInstance().getSelectedTab(); + final boolean isPrivate = (tab != null && tab.isPrivate()); + + // Only create a new instance of SuggestClient if it hasn't been + // set yet. e.g. Robocop tests might set it directly before search + // engines are loaded. + if (mSuggestClient == null && !isPrivate) { + mSuggestClient = new SuggestClient(getActivity(), suggestTemplate, + SUGGESTION_TIMEOUT, SUGGESTION_MAX); + } + } else { + searchEngines.add(new SearchEngine(name, identifier, icon)); + } + } + + mSearchEngines = searchEngines; + + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + + // Show suggestions opt-in prompt only if user hasn't been prompted + // and we're not on a private browsing tab. + if (!suggestionsPrompted && mSuggestClient != null) { + showSuggestionsOptIn(); + } + } catch (JSONException e) { + Log.e(LOGTAG, "Error getting search engine JSON", e); + } + + filterSuggestions(); + } + + private void showSuggestionsOptIn() { + mSuggestionsOptInPrompt = ((ViewStub) mView.findViewById(R.id.suggestions_opt_in_prompt)).inflate(); + + TextView promptText = (TextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title); + promptText.setText(getResources().getString(R.string.suggestions_prompt, mSearchEngines.get(0).name)); + + final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes); + final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no); + + final OnClickListener listener = new OnClickListener() { + @Override + public void onClick(View v) { + // Prevent the buttons from being clicked multiple times (bug 816902) + yesButton.setOnClickListener(null); + noButton.setOnClickListener(null); + + setSuggestionsEnabled(v == yesButton); + } + }; + + yesButton.setOnClickListener(listener); + noButton.setOnClickListener(listener); + + // If the prompt gains focus, automatically pass focus to the + // yes button in the prompt. + final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt); + prompt.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + yesButton.requestFocus(); + } + } + }); + } + + private void setSuggestionsEnabled(final boolean enabled) { + // Clicking the yes/no buttons quickly can cause the click events be + // queued before the listeners are removed above, so it's possible + // setSuggestionsEnabled() can be called twice. mSuggestionsOptInPrompt + // can be null if this happens (bug 828480). + if (mSuggestionsOptInPrompt == null) { + return; + } + + // Make suggestions appear immediately after the user opts in + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + SuggestClient client = mSuggestClient; + if (client != null) { + client.query(mSearchTerm); + } + } + }); + + // Pref observer in gecko will also set prompted = true + PrefsHelper.setPref("browser.search.suggest.enabled", enabled); + + TranslateAnimation slideAnimation = new TranslateAnimation(0, mSuggestionsOptInPrompt.getWidth(), 0, 0); + slideAnimation.setDuration(ANIMATION_DURATION); + slideAnimation.setInterpolator(new AccelerateInterpolator()); + slideAnimation.setFillAfter(true); + final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt); + + TranslateAnimation shrinkAnimation = new TranslateAnimation(0, 0, 0, -1 * mSuggestionsOptInPrompt.getHeight()); + shrinkAnimation.setDuration(ANIMATION_DURATION); + shrinkAnimation.setFillAfter(true); + shrinkAnimation.setStartOffset(slideAnimation.getDuration()); + shrinkAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation a) { + // Increase the height of the view so a gap isn't shown during animation + mView.getLayoutParams().height = mView.getHeight() + + mSuggestionsOptInPrompt.getHeight(); + mView.requestLayout(); + } + + @Override + public void onAnimationRepeat(Animation a) {} + + @Override + public void onAnimationEnd(Animation a) { + // Removing the view immediately results in a NPE in + // dispatchDraw(), possibly because this callback executes + // before drawing is finished. Posting this as a Runnable fixes + // the issue. + mView.post(new Runnable() { + @Override + public void run() { + mView.removeView(mSuggestionsOptInPrompt); + mList.clearAnimation(); + mSuggestionsOptInPrompt = null; + + if (enabled) { + // Reset the view height + mView.getLayoutParams().height = LayoutParams.MATCH_PARENT; + + mSuggestionsEnabled = enabled; + mAnimateSuggestions = true; + mAdapter.notifyDataSetChanged(); + filterSuggestions(); + } + } + }); + } + }); + + prompt.startAnimation(slideAnimation); + mSuggestionsOptInPrompt.startAnimation(shrinkAnimation); + mList.startAnimation(shrinkAnimation); + } + + private int getSuggestEngineCount() { + return (TextUtils.isEmpty(mSearchTerm) || mSuggestClient == null || !mSuggestionsEnabled) ? 0 : 1; + } + + private void registerEventListener(String eventName) { + GeckoAppShell.registerEventListener(eventName, this); + } + + private void unregisterEventListener(String eventName) { + GeckoAppShell.unregisterEventListener(eventName, this); + } + + public void filter(String searchTerm, AutocompleteHandler handler) { + if (TextUtils.isEmpty(searchTerm)) { + return; + } + + if (TextUtils.equals(mSearchTerm, searchTerm)) { + return; + } + + mSearchTerm = searchTerm; + mAutocompleteHandler = handler; + + if (isVisible()) { + // The adapter depends on the search term to determine its number + // of items. Make it we notify the view about it. + mAdapter.notifyDataSetChanged(); + + // Restart loaders with the new search term + SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm, false); + filterSuggestions(); + } + } + + private static class SuggestionAsyncLoader extends AsyncTaskLoader> { + private final SuggestClient mSuggestClient; + private final String mSearchTerm; + private ArrayList mSuggestions; + + public SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) { + super(context); + mSuggestClient = suggestClient; + mSearchTerm = searchTerm; + mSuggestions = null; + } + + @Override + public ArrayList loadInBackground() { + return mSuggestClient.query(mSearchTerm); + } + + @Override + public void deliverResult(ArrayList suggestions) { + mSuggestions = suggestions; + + if (isStarted()) { + super.deliverResult(mSuggestions); + } + } + + @Override + protected void onStartLoading() { + if (mSuggestions != null) { + deliverResult(mSuggestions); + } + + if (takeContentChanged() || mSuggestions == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + + onStopLoading(); + mSuggestions = null; + } + } + + private class SearchAdapter extends MultiTypeCursorAdapter { + private static final int ROW_SEARCH = 0; + private static final int ROW_STANDARD = 1; + private static final int ROW_SUGGEST = 2; + + public SearchAdapter(Context context) { + super(context, null, new int[] { ROW_STANDARD, + ROW_SEARCH, + ROW_SUGGEST }, + new int[] { R.layout.home_item_row, + R.layout.home_search_item_row, + R.layout.home_search_item_row }); + } + + @Override + public int getItemViewType(int position) { + final int engine = getEngineIndex(position); + + if (engine == -1) { + return ROW_STANDARD; + } else if (engine == 0 && mSuggestionsEnabled) { + // Give suggestion views their own type to prevent them from + // sharing other recycled search engine views. Using other + // recycled views for the suggestion row can break animations + // (bug 815937). + return ROW_SUGGEST; + } + + return ROW_SEARCH; + } + + @Override + public boolean isEnabled(int position) { + // If we're using a gamepad or keyboard, allow the row to be + // focused so it can pass the focus to its child suggestion views. + if (!mList.isInTouchMode()) { + return true; + } + + // If the suggestion row only contains one item (the user-entered + // query), allow the entire row to be clickable; clicking the row + // has the same effect as clicking the single suggestion. If the + // row contains multiple items, clicking the row will do nothing. + final int index = getEngineIndex(position); + if (index != -1) { + return mSearchEngines.get(index).suggestions.isEmpty(); + } + + return true; + } + + // Add the search engines to the number of reported results. + @Override + public int getCount() { + final int resultCount = super.getCount(); + + // Don't show search engines or suggestions if search field is empty + if (TextUtils.isEmpty(mSearchTerm)) { + return resultCount; + } + + return resultCount + mSearchEngines.size(); + } + + @Override + public void bindView(View view, Context context, int position) { + final int type = getItemViewType(position); + + if (type == ROW_SEARCH || type == ROW_SUGGEST) { + final SearchEngineRow row = (SearchEngineRow) view; + row.setOnUrlOpenListener(mUrlOpenListener); + row.setOnSearchListener(mSearchListener); + row.setOnEditSuggestionListener(mEditSuggestionListener); + row.setSearchTerm(mSearchTerm); + + final SearchEngine engine = mSearchEngines.get(getEngineIndex(position)); + final boolean animate = (mAnimateSuggestions && engine.suggestions.size() > 0); + row.updateFromSearchEngine(engine, animate); + if (animate) { + // Only animate suggestions the first time they are shown + mAnimateSuggestions = false; + } + } else { + // Account for the search engines + position -= getSuggestEngineCount(); + + final Cursor c = getCursor(position); + final TwoLinePageRow row = (TwoLinePageRow) view; + row.updateFromCursor(c); + } + } + + private int getEngineIndex(int position) { + final int resultCount = super.getCount(); + final int suggestEngineCount = getSuggestEngineCount(); + + // Return suggest engine index + if (position < suggestEngineCount) { + return position; + } + + // Not an engine + if (position - suggestEngineCount < resultCount) { + return -1; + } + + // Return search engine index + return position - resultCount; + } + } + + private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { + public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { + super(context, loaderManager); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == LOADER_ID_SEARCH) { + return SearchLoader.createInstance(getActivity(), args); + } else { + return super.onCreateLoader(id, args); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + if (loader.getId() == LOADER_ID_SEARCH) { + mAdapter.swapCursor(c); + + // We should handle autocompletion based on the search term + // associated with the currently loader that has just provided + // the results. + SearchCursorLoader searchLoader = (SearchCursorLoader) loader; + handleAutocomplete(searchLoader.getSearchTerm(), c); + + loadFavicons(c); + } else { + super.onLoadFinished(loader, c); + } + } + + @Override + public void onLoaderReset(Loader loader) { + if (loader.getId() == LOADER_ID_SEARCH) { + mAdapter.swapCursor(null); + } else { + super.onLoaderReset(loader); + } + } + + @Override + public void onFaviconsLoaded() { + mAdapter.notifyDataSetChanged(); + } + } + + private class SuggestionLoaderCallbacks implements LoaderCallbacks> { + @Override + public Loader> onCreateLoader(int id, Bundle args) { + // mSuggestClient is set to null in onDestroyView(), so using it + // safely here relies on the fact that onCreateLoader() is called + // synchronously in restartLoader(). + return new SuggestionAsyncLoader(getActivity(), mSuggestClient, mSearchTerm); + } + + @Override + public void onLoadFinished(Loader> loader, ArrayList suggestions) { + setSuggestions(suggestions); + } + + @Override + public void onLoaderReset(Loader> loader) { + setSuggestions(new ArrayList()); + } + } + + private static class ListSelectionListener implements View.OnFocusChangeListener, + AdapterView.OnItemSelectedListener { + private SearchEngineRow mSelectedEngineRow; + + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + View selectedRow = ((ListView) v).getSelectedView(); + if (selectedRow != null) { + selectRow(selectedRow); + } + } else { + deselectRow(); + } + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + deselectRow(); + selectRow(view); + } + + @Override + public void onNothingSelected(AdapterView parent) { + deselectRow(); + } + + private void selectRow(View row) { + if (row instanceof SearchEngineRow) { + mSelectedEngineRow = (SearchEngineRow) row; + mSelectedEngineRow.onSelected(); + } + } + + private void deselectRow() { + if (mSelectedEngineRow != null) { + mSelectedEngineRow.onDeselected(); + mSelectedEngineRow = null; + } + } + } + + /** + * HomeSearchListView is a list view for displaying search engine results on the awesome screen. + */ + public static class HomeSearchListView extends HomeListView { + public HomeSearchListView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.homeListViewStyle); + } + + public HomeSearchListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + // Dismiss the soft keyboard. + requestFocus(); + } + + return false; + } + } +} diff --git a/mobile/android/base/home/FadedTextView.java b/mobile/android/base/home/FadedTextView.java new file mode 100644 index 000000000000..dee9f5163d10 --- /dev/null +++ b/mobile/android/base/home/FadedTextView.java @@ -0,0 +1,67 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.widget.TextView; + +import org.mozilla.gecko.R; + +/** + * FadedTextView fades the ends of the text by fadeWidth amount, + * if the text is too long and requires an ellipsis. + */ +public class FadedTextView extends TextView { + + // Width of the fade effect from end of the view. + private int mFadeWidth; + + public FadedTextView(Context context) { + this(context, null); + } + + public FadedTextView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.textViewStyle); + } + + public FadedTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView); + mFadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0); + a.recycle(); + } + + /** + * {@inheritDoc} + */ + @Override + public void onDraw(Canvas canvas) { + int width = getMeasuredWidth(); + + // Layout doesn't return a proper width for getWidth(). + // Instead check the width of the first line, as we've restricted to just one line. + if (getLayout().getLineWidth(0) > width) { + int color = getCurrentTextColor(); + float stop = ((float) (width - mFadeWidth) / (float) width); + LinearGradient gradient = new LinearGradient(0, 0, width, 0, + new int[] { color, color, 0x0 }, + new float[] { 0, stop, 1.0f }, + Shader.TileMode.CLAMP); + getPaint().setShader(gradient); + } else { + getPaint().setShader(null); + } + + // Do a default draw. + super.onDraw(canvas); + } +} diff --git a/mobile/android/base/home/FaviconsLoader.java b/mobile/android/base/home/FaviconsLoader.java new file mode 100644 index 000000000000..3a35b15caa52 --- /dev/null +++ b/mobile/android/base/home/FaviconsLoader.java @@ -0,0 +1,119 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.Favicons; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.gfx.BitmapUtils; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; + +import java.util.ArrayList; + +/** + * Encapsulates the implementation of the favicons cursorloader. + */ +class FaviconsLoader { + // Argument containing list of urls for the favicons loader + private static final String FAVICONS_LOADER_URLS_ARG = "urls"; + + private FaviconsLoader() { + } + + private static ArrayList getUrlsWithoutFavicon(Cursor c) { + ArrayList urls = new ArrayList(); + + if (c == null || !c.moveToFirst()) { + return urls; + } + + final Favicons favicons = Favicons.getInstance(); + + do { + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + + // We only want to load favicons from DB if they are not in the + // memory cache yet. The url is null for bookmark folders. + if (url == null || favicons.getFaviconFromMemCache(url) != null) { + continue; + } + + urls.add(url); + } while (c.moveToNext()); + + return urls; + } + + private static void storeFaviconsInMemCache(Cursor c) { + if (c == null || !c.moveToFirst()) { + return; + } + + final Favicons favicons = Favicons.getInstance(); + + do { + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + final byte[] b = c.getBlob(c.getColumnIndexOrThrow(URLColumns.FAVICON)); + + if (b == null) { + continue; + } + + Bitmap favicon = BitmapUtils.decodeByteArray(b); + if (favicon == null) { + continue; + } + + favicon = favicons.scaleImage(favicon); + favicons.putFaviconInMemCache(url, favicon); + } while (c.moveToNext()); + } + + public static void restartFromCursor(LoaderManager manager, int loaderId, + LoaderCallbacks callbacks, Cursor c) { + // If there urls without in-memory favicons, trigger a new loader + // to load the images from disk to memory. + ArrayList urls = getUrlsWithoutFavicon(c); + if (urls.size() > 0) { + Bundle args = new Bundle(); + args.putStringArrayList(FAVICONS_LOADER_URLS_ARG, urls); + + manager.restartLoader(loaderId, args, callbacks); + } + } + + public static Loader createInstance(Context context, Bundle args) { + final ArrayList urls = args.getStringArrayList(FAVICONS_LOADER_URLS_ARG); + return new FaviconsCursorLoader(context, urls); + } + + private static class FaviconsCursorLoader extends SimpleCursorLoader { + private final ArrayList mUrls; + + public FaviconsCursorLoader(Context context, ArrayList urls) { + super(context); + mUrls = urls; + } + + @Override + public Cursor loadCursor() { + final ContentResolver cr = getContext().getContentResolver(); + + Cursor c = BrowserDB.getFaviconsForUrls(cr, mUrls); + storeFaviconsInMemCache(c); + + return c; + } + } +} diff --git a/mobile/android/base/home/HistoryPage.java b/mobile/android/base/home/HistoryPage.java new file mode 100644 index 000000000000..d57ec60b7051 --- /dev/null +++ b/mobile/android/base/home/HistoryPage.java @@ -0,0 +1,116 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.widget.IconTabWidget; +import android.support.v4.app.Fragment; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; + +import android.widget.ImageButton; + +public class HistoryPage extends HomeFragment + implements IconTabWidget.OnTabChangedListener { + // Logging tag name + private static final String LOGTAG = "GeckoHistoryPage"; + private IconTabWidget mTabWidget; + private int mSelectedTab; + private boolean initializeVisitedPage; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.home_history_page, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mTabWidget = (IconTabWidget) view.findViewById(R.id.tab_icon_widget); + + mTabWidget.addTab(R.drawable.icon_most_visited, R.string.home_most_visited_title); + mTabWidget.addTab(R.drawable.icon_most_recent, R.string.home_most_recent_title); + mTabWidget.addTab(R.drawable.icon_last_tabs, R.string.home_last_tabs_title); + + mTabWidget.setTabSelectionListener(this); + mTabWidget.setCurrentTab(mSelectedTab); + + loadIfVisible(); + } + + @Override + public void load() { + // Show most visited page as the initial page. + // Since we detach/attach on config change, this prevents from replacing current fragment. + if (!initializeVisitedPage) { + showMostVisitedPage(); + initializeVisitedPage = true; + } + } + + @Override + public void onTabChanged(int index) { + if (index == mSelectedTab) { + return; + } + + if (index == 0) { + showMostVisitedPage(); + } else if (index == 1) { + showMostRecentPage(); + } else if (index == 2) { + showLastTabsPage(); + } + + mTabWidget.setCurrentTab(index); + mSelectedTab = index; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + // Rotation should detach and re-attach to use a different layout. + if (isVisible()) { + getFragmentManager().beginTransaction() + .detach(this) + .attach(this) + .commitAllowingStateLoss(); + } + } + + private void showSubPage(Fragment subPage) { + final Bundle args = new Bundle(); + args.putBoolean(HomePager.CAN_LOAD_ARG, getCanLoadHint()); + subPage.setArguments(args); + + getChildFragmentManager().beginTransaction() + .addToBackStack(null).replace(R.id.visited_page_container, subPage) + .commitAllowingStateLoss(); + } + + private void showMostVisitedPage() { + final MostVisitedPage mostVisitedPage = MostVisitedPage.newInstance(); + showSubPage(mostVisitedPage); + } + + private void showMostRecentPage() { + final MostRecentPage mostRecentPage = MostRecentPage.newInstance(); + showSubPage(mostRecentPage); + } + + private void showLastTabsPage() { + final LastTabsPage lastTabsPage = LastTabsPage.newInstance(); + showSubPage(lastTabsPage); + } +} diff --git a/mobile/android/base/home/HomeCursorLoaderCallbacks.java b/mobile/android/base/home/HomeCursorLoaderCallbacks.java new file mode 100644 index 000000000000..1ff2d732f6cc --- /dev/null +++ b/mobile/android/base/home/HomeCursorLoaderCallbacks.java @@ -0,0 +1,58 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; + +/** + * Cursor loader callbacks that takes care loading favicons into memory. + */ +abstract class HomeCursorLoaderCallbacks implements LoaderCallbacks { + + // Cursor loader ID for favicons query + private static final int LOADER_ID_FAVICONS = 100; + + private final Context mContext; + private final LoaderManager mLoaderManager; + + public HomeCursorLoaderCallbacks(Context context, LoaderManager loaderManager) { + mContext = context; + mLoaderManager = loaderManager; + } + + public void loadFavicons(Cursor cursor) { + FaviconsLoader.restartFromCursor(mLoaderManager, LOADER_ID_FAVICONS, this, cursor); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == LOADER_ID_FAVICONS) { + return FaviconsLoader.createInstance(mContext, args); + } + + return null; + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + if (loader.getId() == LOADER_ID_FAVICONS) { + onFaviconsLoaded(); + } + } + + @Override + public void onLoaderReset(Loader loader) { + // Do nothing by default. + } + + // Callback for favicons loaded in memory. + public abstract void onFaviconsLoaded(); +} diff --git a/mobile/android/base/home/HomeFragment.java b/mobile/android/base/home/HomeFragment.java new file mode 100644 index 000000000000..dc87504d58d2 --- /dev/null +++ b/mobile/android/base/home/HomeFragment.java @@ -0,0 +1,287 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.EditBookmarkDialog; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; +import org.mozilla.gecko.R; +import org.mozilla.gecko.ReaderModeUtils; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.util.UiAsyncTask; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.TextUtils; +import android.util.Log; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +/** + * HomeFragment is an empty fragment that can be added to the HomePager. + * Subclasses can add their own views. + */ +abstract class HomeFragment extends Fragment { + // Log Tag. + private static final String LOGTAG="GeckoHomeFragment"; + + // Share MIME type. + private static final String SHARE_MIME_TYPE = "text/plain"; + + // URL to Title replacement regex. + private static final String REGEX_URL_TO_TITLE = "^([a-z]+://)?(www\\.)?"; + + // Whether the fragment can load its content or not + // This is used to defer data loading until the editing + // mode animation ends. + private boolean mCanLoadHint; + + // Whether the fragment has loaded its content + private boolean mIsLoaded; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Bundle args = getArguments(); + if (args != null) { + mCanLoadHint = args.getBoolean(HomePager.CAN_LOAD_ARG, false); + } else { + mCanLoadHint = false; + } + + mIsLoaded = false; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { + if (menuInfo == null || !(menuInfo instanceof HomeContextMenuInfo)) { + return; + } + + HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; + + // Don't show the context menu for folders. + if (info.isFolder) { + return; + } + + MenuInflater inflater = new MenuInflater(view.getContext()); + inflater.inflate(R.menu.home_contextmenu, menu); + + menu.setHeaderTitle(info.title); + + // Hide the "Edit" menuitem if this item isn't a bookmark. + if (info.bookmarkId < 0) { + menu.findItem(R.id.home_edit_bookmark).setVisible(false); + } + + // Hide the "Remove" menuitem if this item doesn't have a bookmark or history ID. + if (info.bookmarkId < 0 && info.historyId < 0) { + menu.findItem(R.id.home_remove).setVisible(false); + } + + final boolean canOpenInReader = (info.display == Combined.DISPLAY_READER); + menu.findItem(R.id.home_open_in_reader).setVisible(canOpenInReader); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + // onContextItemSelected() is first dispatched to the activity and + // then dispatched to its fragments. Since fragments cannot "override" + // menu item selection handling, it's better to avoid menu id collisions + // between the activity and its fragments. + + ContextMenuInfo menuInfo = item.getMenuInfo(); + if (menuInfo == null || !(menuInfo instanceof HomeContextMenuInfo)) { + return false; + } + + HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; + final Context context = getActivity().getApplicationContext(); + + final int itemId = item.getItemId(); + if (itemId == R.id.home_share) { + if (info.url == null) { + Log.e(LOGTAG, "Can't share because URL is null"); + } else { + GeckoAppShell.openUriExternal(info.url, SHARE_MIME_TYPE, "", "", + Intent.ACTION_SEND, info.title); + } + } + + if (itemId == R.id.home_add_to_launcher) { + if (info.url == null) { + Log.e(LOGTAG, "Can't add to home screen because URL is null"); + return false; + } + + // FIXME: bug 897772 + Bitmap bitmap = null; + if (info.favicon != null) { + bitmap = BitmapUtils.decodeByteArray(info.favicon); + } + + String shortcutTitle = TextUtils.isEmpty(info.title) ? info.url.replaceAll(REGEX_URL_TO_TITLE, "") : info.title; + GeckoAppShell.createShortcut(shortcutTitle, info.url, bitmap, ""); + return true; + } + + if (itemId == R.id.home_open_private_tab || itemId == R.id.home_open_new_tab) { + if (info.url == null) { + Log.e(LOGTAG, "Can't open in new tab because URL is null"); + return false; + } + + int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND; + if (item.getItemId() == R.id.home_open_private_tab) + flags |= Tabs.LOADURL_PRIVATE; + + final String url = (info.inReadingList ? ReaderModeUtils.getAboutReaderForUrl(info.url, true) : info.url); + Tabs.getInstance().loadUrl(url, flags); + Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show(); + return true; + } + + if (itemId == R.id.home_edit_bookmark) { + // UI Dialog associates to the activity context, not the applications'. + new EditBookmarkDialog(getActivity()).show(info.url); + return true; + } + + if (itemId == R.id.home_open_in_reader) { + final String url = ReaderModeUtils.getAboutReaderForUrl(info.url, true); + Tabs.getInstance().loadUrl(url, Tabs.LOADURL_NONE); + return true; + } + + if (itemId == R.id.home_remove) { + // Prioritize removing a history entry over a bookmark in the case of a combined item. + final int historyId = info.historyId; + if (historyId > -1) { + new RemoveHistoryTask(context, historyId).execute(); + return true; + } + + final int bookmarkId = info.bookmarkId; + if (bookmarkId > -1) { + new RemoveBookmarkTask(context, bookmarkId, info.url, info.inReadingList).execute(); + return true; + } + } + + return false; + } + + @Override + public void setUserVisibleHint (boolean isVisibleToUser) { + if (isVisibleToUser == getUserVisibleHint()) { + return; + } + + super.setUserVisibleHint(isVisibleToUser); + loadIfVisible(); + } + + void setCanLoadHint(boolean canLoadHint) { + if (mCanLoadHint == canLoadHint) { + return; + } + + mCanLoadHint = canLoadHint; + loadIfVisible(); + } + + boolean getCanLoadHint() { + return mCanLoadHint; + } + + protected abstract void load(); + + protected void loadIfVisible() { + if (!mCanLoadHint || !isVisible() || !getUserVisibleHint()) { + return; + } + + if (!mIsLoaded) { + load(); + mIsLoaded = true; + } + } + + private static class RemoveBookmarkTask extends UiAsyncTask { + private final Context mContext; + private final int mId; + private final String mUrl; + private final boolean mInReadingList; + + public RemoveBookmarkTask(Context context, int id, String url, boolean inReadingList) { + super(ThreadUtils.getBackgroundHandler()); + + mContext = context; + mId = id; + mUrl = url; + mInReadingList = inReadingList; + } + + @Override + public Void doInBackground(Void... params) { + ContentResolver cr = mContext.getContentResolver(); + BrowserDB.removeBookmark(cr, mId); + if (mInReadingList) { + GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", mUrl); + GeckoAppShell.sendEventToGecko(e); + + int count = BrowserDB.getReadingListCount(cr); + e = GeckoEvent.createBroadcastEvent("Reader:ListCountUpdated", Integer.toString(count)); + GeckoAppShell.sendEventToGecko(e); + } + return null; + } + + @Override + public void onPostExecute(Void result) { + int messageId = mInReadingList ? R.string.reading_list_removed : R.string.bookmark_removed; + Toast.makeText(mContext, messageId, Toast.LENGTH_SHORT).show(); + } + } + + private static class RemoveHistoryTask extends UiAsyncTask { + private final Context mContext; + private final int mId; + + public RemoveHistoryTask(Context context, int id) { + super(ThreadUtils.getBackgroundHandler()); + + mContext = context; + mId = id; + } + + @Override + public Void doInBackground(Void... params) { + BrowserDB.removeHistoryEntry(mContext.getContentResolver(), mId); + return null; + } + + @Override + public void onPostExecute(Void result) { + Toast.makeText(mContext, R.string.history_removed, Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/mobile/android/base/home/HomeListView.java b/mobile/android/base/home/HomeListView.java new file mode 100644 index 000000000000..f8968ca2e119 --- /dev/null +++ b/mobile/android/base/home/HomeListView.java @@ -0,0 +1,191 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserContract.Bookmarks; +import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.BrowserContract.URLColumns; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.MotionEvent; +import android.view.View; +import android.widget.AbsListView.LayoutParams; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.ListView; + +/** + * HomeListView is a custom extension of ListView, that packs a HomeContextMenuInfo + * when any of its rows is long pressed. + */ +public class HomeListView extends ListView + implements OnItemLongClickListener { + + // ContextMenuInfo associated with the currently long pressed list item. + private HomeContextMenuInfo mContextMenuInfo; + + // On URL open listener + private OnUrlOpenListener mUrlOpenListener; + + // Top divider + private boolean mShowTopDivider; + + public HomeListView(Context context) { + this(context, null); + } + + public HomeListView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.homeListViewStyle); + } + + public HomeListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HomeListView, defStyle, 0); + mShowTopDivider = a.getBoolean(R.styleable.HomeListView_topDivider, false); + a.recycle(); + + setOnItemLongClickListener(this); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + final Drawable divider = getDivider(); + if (mShowTopDivider && divider != null) { + final int dividerHeight = getDividerHeight(); + final View view = new View(getContext()); + view.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, dividerHeight)); + addHeaderView(view); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mUrlOpenListener = null; + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + Object item = parent.getItemAtPosition(position); + + // HomeListView could hold headers too. Add a context menu info only for its children. + if (item instanceof Cursor) { + Cursor cursor = (Cursor) item; + mContextMenuInfo = new HomeContextMenuInfo(view, position, id, cursor); + return showContextMenuForChild(HomeListView.this); + } else { + mContextMenuInfo = null; + return false; + } + } + + @Override + public ContextMenuInfo getContextMenuInfo() { + return mContextMenuInfo; + } + + public OnUrlOpenListener getOnUrlOpenListener() { + return mUrlOpenListener; + } + + public void setOnUrlOpenListener(OnUrlOpenListener listener) { + mUrlOpenListener = listener; + } + + /** + * A ContextMenuInfo for HomeListView that adds details from the cursor. + */ + public static class HomeContextMenuInfo extends AdapterContextMenuInfo { + public int bookmarkId; + public int historyId; + public String url; + public byte[] favicon; + public String title; + public int display; + public boolean isFolder; + public boolean inReadingList; + + /** + * This constructor assumes that the cursor was generated from a query + * to either the combined view or the bookmarks table. + */ + public HomeContextMenuInfo(View targetView, int position, long id, Cursor cursor) { + super(targetView, position, id); + + if (cursor == null) { + return; + } + + final int typeCol = cursor.getColumnIndex(Bookmarks.TYPE); + if (typeCol != -1) { + isFolder = (cursor.getInt(typeCol) == Bookmarks.TYPE_FOLDER); + } else { + isFolder = false; + } + + // We don't show a context menu for folders, so return early. + if (isFolder) { + return; + } + + url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); + title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); + + final int bookmarkIdCol = cursor.getColumnIndex(Combined.BOOKMARK_ID); + if (bookmarkIdCol == -1) { + // If there isn't a bookmark ID column, this must be a bookmarks cursor, + // so the regular ID column will correspond to a bookmark ID. + bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); + } else if (cursor.isNull(bookmarkIdCol)) { + // If this is a combined cursor, we may get a history item without a + // bookmark, in which case the bookmarks ID column value will be null. + bookmarkId = -1; + } else { + bookmarkId = cursor.getInt(bookmarkIdCol); + } + + final int historyIdCol = cursor.getColumnIndex(Combined.HISTORY_ID); + if (historyIdCol != -1) { + historyId = cursor.getInt(historyIdCol); + } else { + historyId = -1; + } + + final int faviconCol = cursor.getColumnIndex(Combined.FAVICON); + if (faviconCol != -1) { + favicon = cursor.getBlob(faviconCol); + } else { + favicon = null; + } + + // We only have the parent column in cursors from getBookmarksInFolder. + final int parentCol = cursor.getColumnIndex(Bookmarks.PARENT); + if (parentCol != -1) { + inReadingList = (cursor.getInt(parentCol) == Bookmarks.FIXED_READING_LIST_ID); + } else { + inReadingList = false; + } + + final int displayCol = cursor.getColumnIndex(Combined.DISPLAY); + if (displayCol != -1) { + display = cursor.getInt(displayCol); + } else { + display = Combined.DISPLAY_NORMAL; + } + } + } +} diff --git a/mobile/android/base/home/HomePager.java b/mobile/android/base/home/HomePager.java new file mode 100644 index 000000000000..abb87acc5cd4 --- /dev/null +++ b/mobile/android/base/home/HomePager.java @@ -0,0 +1,304 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.animation.PropertyAnimator; +import org.mozilla.gecko.animation.ViewHelper; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.view.ViewGroup.LayoutParams; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.view.View; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.EnumSet; + +public class HomePager extends ViewPager { + // Subpage fragment tag + public static final String SUBPAGE_TAG = "home_pager_subpage"; + + private final Context mContext; + private volatile boolean mLoaded; + private Decor mDecor; + + // List of pages in order. + public enum Page { + HISTORY, + BOOKMARKS, + READING_LIST + } + + // This is mostly used by UI tests to easily fetch + // specific list views at runtime. + static final String LIST_TAG_HISTORY = "history"; + static final String LIST_TAG_BOOKMARKS = "bookmarks"; + static final String LIST_TAG_READING_LIST = "reading_list"; + static final String LIST_TAG_MOST_VISITED = "most_visited"; + static final String LIST_TAG_MOST_RECENT = "most_recent"; + static final String LIST_TAG_LAST_TABS = "last_tabs"; + + private EnumMap mPages = new EnumMap(Page.class); + + public interface OnUrlOpenListener { + public enum Flags { + ALLOW_SWITCH_TO_TAB + } + + public void onUrlOpen(String url, EnumSet flags); + } + + public interface OnNewTabsListener { + public void onNewTabs(String[] urls); + } + + interface OnTitleClickListener { + public void onTitleClicked(int index); + } + + /** + * Special type of child views that could be added as pager decorations by default. + */ + interface Decor { + public void onAddPagerView(String title); + public void removeAllPagerViews(); + public void onPageSelected(int position); + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); + public void setOnTitleClickListener(OnTitleClickListener onTitleClickListener); + } + + static final String CAN_LOAD_ARG = "canLoad"; + + public HomePager(Context context) { + this(context, null); + } + + public HomePager(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + + // This is to keep all 3 pages in memory after they are + // selected in the pager. + setOffscreenPageLimit(2); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (child instanceof Decor) { + ((ViewPager.LayoutParams) params).isDecor = true; + mDecor = (Decor) child; + setOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageSelected(int position) { + mDecor.onPageSelected(position); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + @Override + public void onPageScrollStateChanged(int state) { } + }); + } + + super.addView(child, index, params); + } + + /** + * Loads and initializes the pager. + * + * @param fm FragmentManager for the adapter + */ + public void show(FragmentManager fm, Page page, PropertyAnimator animator) { + mLoaded = true; + final TabsAdapter adapter = new TabsAdapter(fm); + + // Only animate on post-HC devices, when a non-null animator is given + final boolean shouldAnimate = (animator != null && Build.VERSION.SDK_INT >= 11); + + // Add the pages to the adapter in order. + adapter.addTab(Page.HISTORY, HistoryPage.class, new Bundle(), + getContext().getString(R.string.home_history_title)); + adapter.addTab(Page.BOOKMARKS, BookmarksPage.class, new Bundle(), + getContext().getString(R.string.bookmarks_title)); + adapter.addTab(Page.READING_LIST, ReadingListPage.class, new Bundle(), + getContext().getString(R.string.reading_list_title)); + + adapter.setCanLoadHint(!shouldAnimate); + + setAdapter(adapter); + + setCurrentItem(adapter.getItemPosition(page), false); + setVisibility(VISIBLE); + + if (shouldAnimate) { + animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { + @Override + public void onPropertyAnimationStart() { + setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + @Override + public void onPropertyAnimationEnd() { + setLayerType(View.LAYER_TYPE_NONE, null); + adapter.setCanLoadHint(true); + } + }); + + ViewHelper.setAlpha(this, 0.0f); + + animator.attach(this, + PropertyAnimator.Property.ALPHA, + 1.0f); + } + } + + /** + * Hides the pager and removes all child fragments. + */ + public void hide() { + mLoaded = false; + setVisibility(GONE); + setAdapter(null); + } + + /** + * Determines whether the pager is visible. + * + * Unlike getVisibility(), this method does not need to be called on the UI + * thread. + * + * @return Whether the pager and its fragments are being displayed + */ + public boolean isVisible() { + return mLoaded; + } + + class TabsAdapter extends FragmentStatePagerAdapter + implements OnTitleClickListener { + private final ArrayList mTabs = new ArrayList(); + + final class TabInfo { + private final Page page; + private final Class clss; + private final Bundle args; + private final String title; + + TabInfo(Page page, Class clss, Bundle args, String title) { + this.page = page; + this.clss = clss; + this.args = args; + this.title = title; + } + } + + public TabsAdapter(FragmentManager fm) { + super(fm); + + if (mDecor != null) { + mDecor.removeAllPagerViews(); + mDecor.setOnTitleClickListener(this); + } + } + + public void addTab(Page page, Class clss, Bundle args, String title) { + TabInfo info = new TabInfo(page, clss, args, title); + mTabs.add(info); + notifyDataSetChanged(); + + if (mDecor != null) { + mDecor.onAddPagerView(title); + } + } + + @Override + public void onTitleClicked(int index) { + setCurrentItem(index, true); + } + + public int getItemPosition(Page page) { + for (int i = 0; i < mTabs.size(); i++) { + TabInfo info = mTabs.get(i); + if (info.page == page) { + return i; + } + } + + return -1; + } + + @Override + public int getCount() { + return mTabs.size(); + } + + @Override + public Fragment getItem(int position) { + TabInfo info = mTabs.get(position); + return Fragment.instantiate(mContext, info.clss.getName(), info.args); + } + + @Override + public CharSequence getPageTitle(int position) { + TabInfo info = mTabs.get(position); + return info.title.toUpperCase(); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + Fragment fragment = (Fragment) super.instantiateItem(container, position); + + mPages.put(mTabs.get(position).page, fragment); + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + super.destroyItem(container, position, object); + + mPages.remove(mTabs.get(position).page); + } + + public void setCanLoadHint(boolean canLoadHint) { + // Update fragment arguments for future instances + for (TabInfo info : mTabs) { + info.args.putBoolean(CAN_LOAD_ARG, canLoadHint); + } + + // Enable/disable loading on all existing pages + for (Fragment page : mPages.values()) { + final HomeFragment homePage = (HomeFragment) page; + homePage.setCanLoadHint(canLoadHint); + } + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + // XXX: Drop the soft keyboard by stealing focus. Note that the HomePager (via XML + // attr) is focusable after its descendants allowing requestFocus to succeed and drop + // the soft keyboard even if there are no other focusable views on the screen (e.g. + // the Reading List is empty). + requestFocus(); + } + + return super.onInterceptTouchEvent(event); + } +} diff --git a/mobile/android/base/home/HomePagerTabStrip.java b/mobile/android/base/home/HomePagerTabStrip.java new file mode 100644 index 000000000000..cd1755cc3bd1 --- /dev/null +++ b/mobile/android/base/home/HomePagerTabStrip.java @@ -0,0 +1,35 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.v4.view.PagerTabStrip; +import android.util.AttributeSet; + +import org.mozilla.gecko.R; + +/** + * HomePagerTabStrip is a custom implementation of PagerTabStrip + * that exposes XML attributes for the public methods. + */ + +class HomePagerTabStrip extends PagerTabStrip { + + public HomePagerTabStrip(Context context) { + super(context); + } + + public HomePagerTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HomePagerTabStrip); + int color = a.getColor(R.styleable.HomePagerTabStrip_tabIndicatorColor, 0x00); + a.recycle(); + + setTabIndicatorColor(color); + } +} diff --git a/mobile/android/base/home/LastTabsPage.java b/mobile/android/base/home/LastTabsPage.java new file mode 100644 index 000000000000..3a08eea29525 --- /dev/null +++ b/mobile/android/base/home/LastTabsPage.java @@ -0,0 +1,304 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.R; +import org.mozilla.gecko.SessionParser; +import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.home.HomePager.OnNewTabsListener; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.MatrixCursor.RowBuilder; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewStub; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +/** + * Fragment that displays tabs from last session in a ListView. + */ +public class LastTabsPage extends HomeFragment { + // Logging tag name + private static final String LOGTAG = "GeckoLastTabsPage"; + + // Cursor loader ID for the session parser + private static final int LOADER_ID_LAST_TABS = 0; + + // Adapter for the list of search results + private LastTabsAdapter mAdapter; + + // The view shown by the fragment. + private ListView mList; + + // The title for this HomeFragment page. + private TextView mTitle; + + // The button view for restoring tabs from last session. + private View mRestoreButton; + + // Reference to the View to display when there are no results. + private View mEmptyView; + + // Callbacks used for the search and favicon cursor loaders + private CursorLoaderCallbacks mCursorLoaderCallbacks; + + // On new tabs listener + private OnNewTabsListener mNewTabsListener; + + public static LastTabsPage newInstance() { + return new LastTabsPage(); + } + + public LastTabsPage() { + mNewTabsListener = null; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mNewTabsListener = (OnNewTabsListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement HomePager.OnNewTabsListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + + mNewTabsListener = null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.home_last_tabs_page, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + mTitle = (TextView) view.findViewById(R.id.title); + if (mTitle != null) { + mTitle.setText(R.string.home_last_tabs_title); + } + + mList = (ListView) view.findViewById(R.id.list); + mList.setTag(HomePager.LIST_TAG_LAST_TABS); + + mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final Cursor c = mAdapter.getCursor(); + if (c == null || !c.moveToPosition(position)) { + return; + } + + final String url = c.getString(c.getColumnIndexOrThrow(Combined.URL)); + mNewTabsListener.onNewTabs(new String[] { url }); + } + }); + + registerForContextMenu(mList); + + mRestoreButton = view.findViewById(R.id.open_all_tabs_button); + mRestoreButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + openAllTabs(); + } + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mList = null; + mTitle = null; + mEmptyView = null; + mRestoreButton = null; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Activity activity = getActivity(); + + // Intialize adapter + mAdapter = new LastTabsAdapter(activity); + mList.setAdapter(mAdapter); + + // Create callbacks before the initial loader is started + mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + loadIfVisible(); + } + + private void updateUiFromCursor(Cursor c) { + if (c != null && c.getCount() > 0) { + if (mTitle != null) { + mTitle.setVisibility(View.VISIBLE); + } + mRestoreButton.setVisibility(View.VISIBLE); + return; + } + + // Cursor is empty, so hide the title and set the + // empty view if it hasn't been set already. + if (mTitle != null) { + mTitle.setVisibility(View.GONE); + } + mRestoreButton.setVisibility(View.GONE); + + if (mEmptyView == null) { + // Set empty page view. We delay this so that the empty view won't flash. + final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub); + mEmptyView = emptyViewStub.inflate(); + + final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image); + emptyIcon.setImageResource(R.drawable.icon_last_tabs_empty); + + final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text); + emptyText.setText(R.string.home_last_tabs_empty); + + mList.setEmptyView(mEmptyView); + } + } + + @Override + protected void load() { + getLoaderManager().initLoader(LOADER_ID_LAST_TABS, null, mCursorLoaderCallbacks); + } + + private void openAllTabs() { + final Cursor c = mAdapter.getCursor(); + if (c == null || !c.moveToFirst()) { + return; + } + + final String[] urls = new String[c.getCount()]; + + do { + urls[c.getPosition()] = c.getString(c.getColumnIndexOrThrow(Combined.URL)); + } while (c.moveToNext()); + + mNewTabsListener.onNewTabs(urls); + } + + private static class LastTabsCursorLoader extends SimpleCursorLoader { + public LastTabsCursorLoader(Context context) { + super(context); + } + + @Override + public Cursor loadCursor() { + final Context context = getContext(); + + final String jsonString = GeckoProfile.get(context).readSessionFile(true); + if (jsonString == null) { + // No previous session data + return null; + } + + final MatrixCursor c = new MatrixCursor(new String[] { Combined._ID, + Combined.URL, + Combined.TITLE }); + + new SessionParser() { + @Override + public void onTabRead(SessionTab tab) { + final String url = tab.getUrl(); + + // Don't show last tabs for about:home + if (url.equals("about:home")) { + return; + } + + final RowBuilder row = c.newRow(); + row.add(-1); + row.add(url); + + final String title = tab.getTitle(); + row.add(title); + } + }.parse(jsonString); + + return c; + } + } + + private static class LastTabsAdapter extends CursorAdapter { + public LastTabsAdapter(Context context) { + super(context, null); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + ((TwoLinePageRow) view).updateFromCursor(cursor); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(context).inflate(R.layout.home_item_row, parent, false); + } + } + + private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { + public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { + super(context, loaderManager); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == LOADER_ID_LAST_TABS) { + return new LastTabsCursorLoader(getActivity()); + } else { + return super.onCreateLoader(id, args); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + if (loader.getId() == LOADER_ID_LAST_TABS) { + mAdapter.swapCursor(c); + updateUiFromCursor(c); + loadFavicons(c); + } else { + super.onLoadFinished(loader, c); + } + } + + @Override + public void onLoaderReset(Loader loader) { + if (loader.getId() == LOADER_ID_LAST_TABS) { + mAdapter.swapCursor(null); + } else { + super.onLoaderReset(loader); + } + } + + @Override + public void onFaviconsLoaded() { + mAdapter.notifyDataSetChanged(); + } + } +} diff --git a/mobile/android/base/home/MostRecentPage.java b/mobile/android/base/home/MostRecentPage.java new file mode 100644 index 000000000000..fe262e9d4e1c --- /dev/null +++ b/mobile/android/base/home/MostRecentPage.java @@ -0,0 +1,402 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; +import org.mozilla.gecko.home.TwoLinePageRow; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewStub; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.Date; +import java.util.EnumSet; + +/** + * Fragment that displays recent history in a ListView. + */ +public class MostRecentPage extends HomeFragment { + // Logging tag name + private static final String LOGTAG = "GeckoMostRecentPage"; + + // Cursor loader ID for history query + private static final int LOADER_ID_HISTORY = 0; + + // Adapter for the list of search results + private MostRecentAdapter mAdapter; + + // The view shown by the fragment. + private ListView mList; + + // The title for this HomeFragment page. + private TextView mTitle; + + // Reference to the View to display when there are no results. + private View mEmptyView; + + // Callbacks used for the search and favicon cursor loaders + private CursorLoaderCallbacks mCursorLoaderCallbacks; + + // On URL open listener + private OnUrlOpenListener mUrlOpenListener; + + public static MostRecentPage newInstance() { + return new MostRecentPage(); + } + + public MostRecentPage() { + mUrlOpenListener = null; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mUrlOpenListener = (OnUrlOpenListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement HomePager.OnUrlOpenListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mUrlOpenListener = null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.home_most_recent_page, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + mTitle = (TextView) view.findViewById(R.id.title); + if (mTitle != null) { + mTitle.setText(R.string.home_most_recent_title); + } + + mList = (ListView) view.findViewById(R.id.list); + mList.setTag(HomePager.LIST_TAG_MOST_RECENT); + + mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + position -= mAdapter.getMostRecentSectionsCountBefore(position); + final Cursor c = mAdapter.getCursor(position); + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + + // This item is a TwoLinePageRow, so we allow switch-to-tab. + mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); + } + }); + + registerForContextMenu(mList); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mList = null; + mTitle = null; + mEmptyView = null; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Activity activity = getActivity(); + + // Intialize adapter + mAdapter = new MostRecentAdapter(activity); + mList.setAdapter(mAdapter); + + // Create callbacks before the initial loader is started + mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + loadIfVisible(); + } + + @Override + protected void load() { + getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks); + } + + private static class MostRecentCursorLoader extends SimpleCursorLoader { + // Max number of history results + private static final int HISTORY_LIMIT = 100; + + public MostRecentCursorLoader(Context context) { + super(context); + } + + @Override + public Cursor loadCursor() { + final ContentResolver cr = getContext().getContentResolver(); + return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT); + } + } + + private void updateUiFromCursor(Cursor c) { + if (c != null && c.getCount() > 0) { + if (mTitle != null) { + mTitle.setVisibility(View.VISIBLE); + } + return; + } + + // Cursor is empty, so hide the title and set the + // empty view if it hasn't been set already. + if (mTitle != null) { + mTitle.setVisibility(View.GONE); + } + + if (mEmptyView == null) { + // Set empty page view. We delay this so that the empty view won't flash. + final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub); + mEmptyView = emptyViewStub.inflate(); + + final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image); + emptyIcon.setImageResource(R.drawable.icon_most_recent_empty); + + final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text); + emptyText.setText(R.string.home_most_recent_empty); + + mList.setEmptyView(mEmptyView); + } + } + + private static class MostRecentAdapter extends MultiTypeCursorAdapter { + private static final int ROW_HEADER = 0; + private static final int ROW_STANDARD = 1; + + private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER }; + private static final int[] LAYOUT_TYPES = new int[] { R.layout.home_item_row, R.layout.home_header_row }; + + // For the time sections in history + private static final long MS_PER_DAY = 86400000; + private static final long MS_PER_WEEK = MS_PER_DAY * 7; + + // The time ranges for each section + private static enum MostRecentSection { + TODAY, + YESTERDAY, + WEEK, + OLDER + }; + + private final Context mContext; + + // Maps headers in the list with their respective sections + private final SparseArray mMostRecentSections; + + public MostRecentAdapter(Context context) { + super(context, null, VIEW_TYPES, LAYOUT_TYPES); + + mContext = context; + + // Initialize map of history sections + mMostRecentSections = new SparseArray(); + } + + @Override + public Object getItem(int position) { + final int type = getItemViewType(position); + + // Header items are not in the cursor + if (type == ROW_HEADER) { + return null; + } + + return super.getItem(position - getMostRecentSectionsCountBefore(position)); + } + + @Override + public int getItemViewType(int position) { + if (mMostRecentSections.get(position) != null) { + return ROW_HEADER; + } + + return ROW_STANDARD; + } + + @Override + public boolean isEnabled(int position) { + return (getItemViewType(position) == ROW_STANDARD); + } + + @Override + public int getCount() { + // Add the history section headers to the number of reported results. + return super.getCount() + mMostRecentSections.size(); + } + + @Override + public Cursor swapCursor(Cursor cursor) { + Cursor oldCursor = super.swapCursor(cursor); + loadMostRecentSections(cursor); + return oldCursor; + } + + @Override + public void bindView(View view, Context context, int position) { + final int type = getItemViewType(position); + + if (type == ROW_HEADER) { + final MostRecentSection section = mMostRecentSections.get(position); + final TextView row = (TextView) view; + row.setText(getMostRecentSectionTitle(section)); + } else { + // Account for the most recent section headers + position -= getMostRecentSectionsCountBefore(position); + final Cursor c = getCursor(position); + final TwoLinePageRow row = (TwoLinePageRow) view; + row.updateFromCursor(c); + } + } + + private String getMostRecentSectionTitle(MostRecentSection section) { + switch (section) { + case TODAY: + return mContext.getString(R.string.history_today_section); + case YESTERDAY: + return mContext.getString(R.string.history_yesterday_section); + case WEEK: + return mContext.getString(R.string.history_week_section); + case OLDER: + return mContext.getString(R.string.history_older_section); + } + + throw new IllegalStateException("Unrecognized history section"); + } + + private int getMostRecentSectionsCountBefore(int position) { + // Account for the number headers before the given position + int sectionsBefore = 0; + + final int historySectionsCount = mMostRecentSections.size(); + for (int i = 0; i < historySectionsCount; i++) { + final int sectionPosition = mMostRecentSections.keyAt(i); + if (sectionPosition > position) { + break; + } + + sectionsBefore++; + } + + return sectionsBefore; + } + + private static MostRecentSection getMostRecentSectionForTime(long from, long time) { + long delta = from - time; + + if (delta < 0) { + return MostRecentSection.TODAY; + } + + if (delta < MS_PER_DAY) { + return MostRecentSection.YESTERDAY; + } + + if (delta < MS_PER_WEEK) { + return MostRecentSection.WEEK; + } + + return MostRecentSection.OLDER; + } + + private void loadMostRecentSections(Cursor c) { + if (c == null || !c.moveToFirst()) { + return; + } + + // Clear any history sections that may have been loaded before. + mMostRecentSections.clear(); + + final Date now = new Date(); + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + + final long today = now.getTime(); + MostRecentSection section = null; + + do { + final int position = c.getPosition(); + final long time = c.getLong(c.getColumnIndexOrThrow(URLColumns.DATE_LAST_VISITED)); + final MostRecentSection itemSection = MostRecentAdapter.getMostRecentSectionForTime(today, time); + + if (section != itemSection) { + section = itemSection; + mMostRecentSections.append(position + mMostRecentSections.size(), section); + } + + // Reached the last section, no need to continue + if (section == MostRecentSection.OLDER) { + break; + } + } while (c.moveToNext()); + } + } + + private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { + public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { + super(context, loaderManager); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == LOADER_ID_HISTORY) { + return new MostRecentCursorLoader(getActivity()); + } else { + return super.onCreateLoader(id, args); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + if (loader.getId() == LOADER_ID_HISTORY) { + mAdapter.swapCursor(c); + updateUiFromCursor(c); + loadFavicons(c); + } else { + super.onLoadFinished(loader, c); + } + } + + @Override + public void onLoaderReset(Loader loader) { + if (loader.getId() == LOADER_ID_HISTORY) { + mAdapter.swapCursor(null); + } else { + super.onLoaderReset(loader); + } + } + + @Override + public void onFaviconsLoaded() { + mAdapter.notifyDataSetChanged(); + } + } +} diff --git a/mobile/android/base/home/MostVisitedPage.java b/mobile/android/base/home/MostVisitedPage.java new file mode 100644 index 000000000000..fff8e9ec0afb --- /dev/null +++ b/mobile/android/base/home/MostVisitedPage.java @@ -0,0 +1,248 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.EnumSet; + +/** + * Fragment that displays frecency search results in a ListView. + */ +public class MostVisitedPage extends HomeFragment { + // Logging tag name + private static final String LOGTAG = "GeckoMostVisitedPage"; + + // Cursor loader ID for search query + private static final int LOADER_ID_FRECENCY = 0; + + // Adapter for the list of search results + private VisitedAdapter mAdapter; + + // The view shown by the fragment. + private ListView mList; + + // The title for this HomeFragment page. + private TextView mTitle; + + // Reference to the View to display when there are no results. + private View mEmptyView; + + // Callbacks used for the search and favicon cursor loaders + private CursorLoaderCallbacks mCursorLoaderCallbacks; + + // On URL open listener + private OnUrlOpenListener mUrlOpenListener; + + public static MostVisitedPage newInstance() { + return new MostVisitedPage(); + } + + public MostVisitedPage() { + mUrlOpenListener = null; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mUrlOpenListener = (OnUrlOpenListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement HomePager.OnUrlOpenListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + + mUrlOpenListener = null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.home_most_visited_page, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + mTitle = (TextView) view.findViewById(R.id.title); + if (mTitle != null) { + mTitle.setText(R.string.home_most_visited_title); + } + + mList = (HomeListView) view.findViewById(R.id.list); + mList.setTag(HomePager.LIST_TAG_MOST_VISITED); + + mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final Cursor c = mAdapter.getCursor(); + if (c == null || !c.moveToPosition(position)) { + return; + } + + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + + // This item is a TwoLinePageRow, so we allow switch-to-tab. + mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); + } + }); + + registerForContextMenu(mList); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mList = null; + mTitle = null; + mEmptyView = null; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Activity activity = getActivity(); + + // Intialize the search adapter + mAdapter = new VisitedAdapter(activity, null); + mList.setAdapter(mAdapter); + + // Create callbacks before the initial loader is started + mCursorLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager()); + loadIfVisible(); + } + + @Override + protected void load() { + getLoaderManager().initLoader(LOADER_ID_FRECENCY, null, mCursorLoaderCallbacks); + } + + private void updateUiFromCursor(Cursor c) { + if (c != null && c.getCount() > 0) { + if (mTitle != null) { + mTitle.setVisibility(View.VISIBLE); + } + return; + } + + // Cursor is empty, so hide the title and set the + // empty view if it hasn't been set already. + if (mTitle != null) { + mTitle.setVisibility(View.GONE); + } + + if (mEmptyView == null) { + // Set empty page view. We delay this so that the empty view won't flash. + ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub); + mEmptyView = emptyViewStub.inflate(); + + final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image); + emptyIcon.setImageResource(R.drawable.icon_most_visited_empty); + + final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text); + emptyText.setText(R.string.home_most_visited_empty); + + mList.setEmptyView(mEmptyView); + } + } + + private static class FrecencyCursorLoader extends SimpleCursorLoader { + // Max number of search results + private static final int SEARCH_LIMIT = 50; + + public FrecencyCursorLoader(Context context) { + super(context); + } + + @Override + public Cursor loadCursor() { + final ContentResolver cr = getContext().getContentResolver(); + return BrowserDB.filter(cr, "", SEARCH_LIMIT); + } + } + + private class VisitedAdapter extends CursorAdapter { + public VisitedAdapter(Context context, Cursor cursor) { + super(context, cursor); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + final TwoLinePageRow row = (TwoLinePageRow) view; + row.updateFromCursor(cursor); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(parent.getContext()).inflate(R.layout.home_item_row, parent, false); + } + } + + private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { + public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { + super(context, loaderManager); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == LOADER_ID_FRECENCY) { + return new FrecencyCursorLoader(getActivity()); + } else { + return super.onCreateLoader(id, args); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + if (loader.getId() == LOADER_ID_FRECENCY) { + mAdapter.swapCursor(c); + updateUiFromCursor(c); + loadFavicons(c); + } else { + super.onLoadFinished(loader, c); + } + } + + @Override + public void onLoaderReset(Loader loader) { + if (loader.getId() == LOADER_ID_FRECENCY) { + mAdapter.swapCursor(null); + } else { + super.onLoaderReset(loader); + } + } + + @Override + public void onFaviconsLoaded() { + mAdapter.notifyDataSetChanged(); + } + } +} diff --git a/mobile/android/base/home/MultiTypeCursorAdapter.java b/mobile/android/base/home/MultiTypeCursorAdapter.java new file mode 100644 index 000000000000..8fbee2ec1e21 --- /dev/null +++ b/mobile/android/base/home/MultiTypeCursorAdapter.java @@ -0,0 +1,95 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * MultiTypeCursorAdapter wraps a cursor and any meta data associated with it. + * A set of view types (corresponding to the cursor and its meta data) + * are mapped to a set of layouts. + */ +abstract class MultiTypeCursorAdapter extends CursorAdapter { + private final int[] mViewTypes; + private final int[] mLayouts; + + // Bind the view for the given position. + abstract public void bindView(View view, Context context, int position); + + public MultiTypeCursorAdapter(Context context, Cursor cursor, int[] viewTypes, int[] layouts) { + super(context, cursor); + + if (viewTypes.length != layouts.length) { + throw new IllegalStateException("The view types and the layouts should be of same size"); + } + + mViewTypes = viewTypes; + mLayouts = layouts; + } + + @Override + public final int getViewTypeCount() { + return mViewTypes.length; + } + + /** + * @return Cursor for the given position. + */ + public final Cursor getCursor(int position) { + final Cursor cursor = getCursor(); + if (cursor == null || !cursor.moveToPosition(position)) { + throw new IllegalStateException("Couldn't move cursor to position " + position); + } + + return cursor; + } + + @Override + public final View getView(int position, View convertView, ViewGroup parent) { + final Context context = parent.getContext(); + if (convertView == null) { + convertView = newView(context, position, parent); + } + + bindView(convertView, context, position); + return convertView; + } + + @Override + public final void bindView(View view, Context context, Cursor cursor) { + // Do nothing. + } + + @Override + public final View newView(Context context, Cursor cursor, ViewGroup parent) { + return null; + } + + /** + * Inflate a new view from a set of view types and layouts based on the position. + * + * @param context Context for inflating the view. + * @param position Position of the view. + * @param parent Parent view group that will hold this view. + */ + private View newView(Context context, int position, ViewGroup parent) { + final int type = getItemViewType(position); + final int count = mViewTypes.length; + + for (int i = 0; i < count; i++) { + if (mViewTypes[i] == type) { + return LayoutInflater.from(context).inflate(mLayouts[i], parent, false); + } + } + + return null; + } +} diff --git a/mobile/android/base/home/PinBookmarkDialog.java b/mobile/android/base/home/PinBookmarkDialog.java new file mode 100644 index 000000000000..3e20ea80578d --- /dev/null +++ b/mobile/android/base/home/PinBookmarkDialog.java @@ -0,0 +1,220 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserDB.URLColumns; + +import android.app.Activity; +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.ListView; + +/** + * Dialog fragment that displays frecency search results, for pinning as a bookmark, in a ListView. + */ +class PinBookmarkDialog extends DialogFragment { + // Listener for url selection + public static interface OnBookmarkSelectedListener { + public void onBookmarkSelected(String url, String title); + } + + // Cursor loader ID for search query + private static final int LOADER_ID_SEARCH = 0; + + // Cursor loader ID for favicons query + private static final int LOADER_ID_FAVICONS = 1; + + // Holds the current search term to use in the query + private String mSearchTerm; + + // Adapter for the list of search results + private SearchAdapter mAdapter; + + // Search entry + private EditText mSearch; + + // Search results + private ListView mList; + + // Callbacks used for the search and favicon cursor loaders + private CursorLoaderCallbacks mLoaderCallbacks; + + // Bookmark selected listener + private OnBookmarkSelectedListener mOnBookmarkSelectedListener; + + public static PinBookmarkDialog newInstance() { + return new PinBookmarkDialog(); + } + + private PinBookmarkDialog() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // All list views are styled to look the same with a global activity theme. + // If the style of the list changes, inflate it from an XML. + return inflater.inflate(R.layout.pin_bookmark_dialog, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mSearch = (EditText) view.findViewById(R.id.search); + mSearch.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + filter(mSearch.getText().toString()); + } + }); + + mList = (HomeListView) view.findViewById(R.id.list); + mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (mOnBookmarkSelectedListener != null) { + final Cursor c = mAdapter.getCursor(); + if (c == null || !c.moveToPosition(position)) { + return; + } + + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + final String title = c.getString(c.getColumnIndexOrThrow(URLColumns.TITLE)); + mOnBookmarkSelectedListener.onBookmarkSelected(url, title); + } + + // Dismiss the fragment and the dialog. + dismiss(); + } + }); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Activity activity = getActivity(); + final LoaderManager manager = getLoaderManager(); + + // Initialize the search adapter + mAdapter = new SearchAdapter(activity); + mList.setAdapter(mAdapter); + + // Create callbacks before the initial loader is started + mLoaderCallbacks = new CursorLoaderCallbacks(activity, manager); + + // Reconnect to the loader only if present + manager.initLoader(LOADER_ID_SEARCH, null, mLoaderCallbacks); + + // Default filter. + filter(""); + } + + private void filter(String searchTerm) { + if (!TextUtils.isEmpty(searchTerm) && + TextUtils.equals(mSearchTerm, searchTerm)) { + return; + } + + mSearchTerm = searchTerm; + + // Restart loaders with the new search term + SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH, mLoaderCallbacks, mSearchTerm); + } + + public void setOnBookmarkSelectedListener(OnBookmarkSelectedListener listener) { + mOnBookmarkSelectedListener = listener; + } + + private static class SearchAdapter extends CursorAdapter { + private LayoutInflater mInflater; + + public SearchAdapter(Context context) { + super(context, null); + mInflater = LayoutInflater.from(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TwoLinePageRow row = (TwoLinePageRow) view; + row.setShowIcons(false); + row.updateFromCursor(cursor); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return (TwoLinePageRow) mInflater.inflate(R.layout.home_item_row, parent, false); + } + } + + private class CursorLoaderCallbacks extends HomeCursorLoaderCallbacks { + public CursorLoaderCallbacks(Context context, LoaderManager loaderManager) { + super(context, loaderManager); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == LOADER_ID_SEARCH) { + return SearchLoader.createInstance(getActivity(), args); + } else { + return super.onCreateLoader(id, args); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + if (loader.getId() == LOADER_ID_SEARCH) { + mAdapter.swapCursor(c); + loadFavicons(c); + } else { + super.onLoadFinished(loader, c); + } + } + + @Override + public void onLoaderReset(Loader loader) { + if (loader.getId() == LOADER_ID_SEARCH) { + mAdapter.swapCursor(null); + } else { + super.onLoaderReset(loader); + } + } + + @Override + public void onFaviconsLoaded() { + mAdapter.notifyDataSetChanged(); + } + } +} diff --git a/mobile/android/base/home/ReadingListPage.java b/mobile/android/base/home/ReadingListPage.java new file mode 100644 index 000000000000..70f48bfa8aba --- /dev/null +++ b/mobile/android/base/home/ReadingListPage.java @@ -0,0 +1,246 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserContract.Bookmarks; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; +import org.mozilla.gecko.home.TwoLinePageRow; +import org.mozilla.gecko.ReaderModeUtils; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ImageSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.EnumSet; + +/** + * Fragment that displays reading list contents in a ListView. + */ +public class ReadingListPage extends HomeFragment { + // Cursor loader ID for reading list + private static final int LOADER_ID_READING_LIST = 0; + + // Adapter for the list of reading list items + private ReadingListAdapter mAdapter; + + // The view shown by the fragment + private ListView mList; + + // Reference to the View to display when there are no results. + private View mEmptyView; + + // Reference to top view. + private View mTopView; + + // Callbacks used for the reading list and favicon cursor loaders + private CursorLoaderCallbacks mCursorLoaderCallbacks; + + // On URL open listener + private OnUrlOpenListener mUrlOpenListener; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mUrlOpenListener = (OnUrlOpenListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement HomePager.OnUrlOpenListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + + mUrlOpenListener = null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.home_reading_list_page, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mTopView = view; + + mList = (ListView) view.findViewById(R.id.list); + mList.setTag(HomePager.LIST_TAG_READING_LIST); + + mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final Cursor c = mAdapter.getCursor(); + if (c == null || !c.moveToPosition(position)) { + return; + } + + String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + url = ReaderModeUtils.getAboutReaderForUrl(url, true); + + // This item is a TwoLinePageRow, so we allow switch-to-tab. + mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); + } + }); + + registerForContextMenu(mList); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mList = null; + mTopView = null; + mEmptyView = null; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + // Detach and reattach the fragment as the layout changes. + if (isVisible()) { + getFragmentManager().beginTransaction() + .detach(this) + .attach(this) + .commitAllowingStateLoss(); + } + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mAdapter = new ReadingListAdapter(getActivity(), null); + mList.setAdapter(mAdapter); + + // Create callbacks before the initial loader is started. + mCursorLoaderCallbacks = new CursorLoaderCallbacks(); + loadIfVisible(); + } + + @Override + protected void load() { + getLoaderManager().initLoader(LOADER_ID_READING_LIST, null, mCursorLoaderCallbacks); + } + + private void updateUiFromCursor(Cursor c) { + // We delay setting the empty view until the cursor is actually empty. + // This avoids image flashing. + if ((c == null || c.getCount() == 0) && mEmptyView == null) { + final ViewStub emptyViewStub = (ViewStub) mTopView.findViewById(R.id.home_empty_view_stub); + mEmptyView = emptyViewStub.inflate(); + + final TextView emptyHint = (TextView) mEmptyView.findViewById(R.id.home_empty_hint); + String readingListHint = emptyHint.getText().toString(); + + // Use an ImageSpan to include the reader icon in the "Tip". + int imageSpanIndex = readingListHint.indexOf("%I"); + if (imageSpanIndex != -1) { + final ImageSpan readingListIcon = new ImageSpan(getActivity(), R.drawable.reader_cropped, ImageSpan.ALIGN_BOTTOM); + final SpannableStringBuilder hintBuilder = new SpannableStringBuilder(readingListHint); + hintBuilder.setSpan(readingListIcon, imageSpanIndex, imageSpanIndex + 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + emptyHint.setText(hintBuilder, TextView.BufferType.SPANNABLE); + } + + mList.setEmptyView(mEmptyView); + } + } + + /** + * Cursor loader for the list of reading list items. + */ + private static class ReadingListLoader extends SimpleCursorLoader { + public ReadingListLoader(Context context) { + super(context); + } + + @Override + public Cursor loadCursor() { + return BrowserDB.getBookmarksInFolder(getContext().getContentResolver(), Bookmarks.FIXED_READING_LIST_ID); + } + } + + /** + * Cursor adapter for the list of reading list items. + */ + private class ReadingListAdapter extends CursorAdapter { + public ReadingListAdapter(Context context, Cursor cursor) { + super(context, cursor); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + final TwoLinePageRow row = (TwoLinePageRow) view; + row.updateFromCursor(cursor); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(parent.getContext()).inflate(R.layout.bookmark_item_row, parent, false); + } + } + + /** + * LoaderCallbacks implementation that interacts with the LoaderManager. + */ + private class CursorLoaderCallbacks implements LoaderCallbacks { + @Override + public Loader onCreateLoader(int id, Bundle args) { + switch(id) { + case LOADER_ID_READING_LIST: + return new ReadingListLoader(getActivity()); + } + return null; + } + + @Override + public void onLoadFinished(Loader loader, Cursor c) { + final int loaderId = loader.getId(); + switch(loaderId) { + case LOADER_ID_READING_LIST: + mAdapter.swapCursor(c); + break; + } + + updateUiFromCursor(c); + } + + @Override + public void onLoaderReset(Loader loader) { + final int loaderId = loader.getId(); + switch(loaderId) { + case LOADER_ID_READING_LIST: + mAdapter.swapCursor(null); + break; + } + } + } +} diff --git a/mobile/android/base/SearchEngine.java b/mobile/android/base/home/SearchEngine.java similarity index 93% rename from mobile/android/base/SearchEngine.java rename to mobile/android/base/home/SearchEngine.java index 2e5fc705c289..b82f7ab7befb 100644 --- a/mobile/android/base/SearchEngine.java +++ b/mobile/android/base/home/SearchEngine.java @@ -3,13 +3,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko; +package org.mozilla.gecko.home; import android.graphics.Bitmap; import java.util.ArrayList; -public class SearchEngine { +class SearchEngine { public String name; public String identifier; public Bitmap icon; diff --git a/mobile/android/base/SearchEngineRow.java b/mobile/android/base/home/SearchEngineRow.java similarity index 77% rename from mobile/android/base/SearchEngineRow.java rename to mobile/android/base/home/SearchEngineRow.java index 435ae593d68c..fc8c5143fee3 100644 --- a/mobile/android/base/SearchEngineRow.java +++ b/mobile/android/base/home/SearchEngineRow.java @@ -3,26 +3,29 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko; +package org.mozilla.gecko.home; -import org.mozilla.gecko.AwesomeBarTabs.OnUrlOpenListener; -import org.mozilla.gecko.util.GamepadUtils; +import org.mozilla.gecko.AnimatedHeightLayout; +import org.mozilla.gecko.FlowLayout; +import org.mozilla.gecko.R; +import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener; +import org.mozilla.gecko.home.BrowserSearch.OnSearchListener; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; import org.mozilla.gecko.util.StringUtils; import org.mozilla.gecko.widget.FaviconView; import android.content.Context; -import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnLongClickListener; import android.view.animation.AlphaAnimation; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import java.util.EnumSet; + class SearchEngineRow extends AnimatedHeightLayout { // Duration for fade-in animation private static final int ANIMATION_DURATION = 250; @@ -39,9 +42,6 @@ class SearchEngineRow extends AnimatedHeightLayout { // Search engine associated with this view private SearchEngine mSearchEngine; - // Selected suggestion view - private int mSelectedView = 0; - // Event listeners for suggestion views private final OnClickListener mClickListener; private final OnLongClickListener mLongClickListener; @@ -49,6 +49,15 @@ class SearchEngineRow extends AnimatedHeightLayout { // On URL open listener private OnUrlOpenListener mUrlOpenListener; + // On search listener + private OnSearchListener mSearchListener; + + // On edit suggestion listener + private OnEditSuggestionListener mEditSuggestionListener; + + // Selected suggestion view + private int mSelectedView = 0; + public SearchEngineRow(Context context) { this(context, null); } @@ -68,12 +77,12 @@ class SearchEngineRow extends AnimatedHeightLayout { // If we're not clicking the user-entered view (the first suggestion item) // and the search matches a URL pattern, go to that URL. Otherwise, do a // search for the term. - if (mUrlOpenListener != null) { - if (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, false)) { - mUrlOpenListener.onUrlOpen(suggestion, null); - } else { - mUrlOpenListener.onSearch(mSearchEngine, suggestion); + if (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, false)) { + if (mUrlOpenListener != null) { + mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class)); } + } else if (mSearchListener != null) { + mSearchListener.onSearch(mSearchEngine.name, suggestion); } } }; @@ -81,9 +90,9 @@ class SearchEngineRow extends AnimatedHeightLayout { mLongClickListener = new OnLongClickListener() { @Override public boolean onLongClick(View v) { - if (mUrlOpenListener != null) { + if (mEditSuggestionListener != null) { final String suggestion = getSuggestionTextFromView(v); - mUrlOpenListener.onEditSuggestion(suggestion); + mEditSuggestionListener.onEditSuggestion(suggestion); return true; } @@ -102,6 +111,31 @@ class SearchEngineRow extends AnimatedHeightLayout { mUserEnteredView.setOnClickListener(mClickListener); mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text); + + // Handle clicks on this row that don't happen on individual suggestion views. + setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // Don't do anything if we are showing suggestions. + if (mSearchEngine.suggestions.size() > 0) { + return; + } + + // Otherwise, perform a search for the user entered term. + String searchTerm = getSuggestionTextFromView(mUserEnteredView); + if (mSearchListener != null) { + mSearchListener.onSearch(mSearchEngine.name, searchTerm); + } + } + }); + + // Intercept long clicks to avoid trying to show a context menu. + setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + return true; + } + }); } private String getSuggestionTextFromView(View v) { @@ -122,7 +156,15 @@ class SearchEngineRow extends AnimatedHeightLayout { mUrlOpenListener = listener; } - public void updateFromSearchEngine(SearchEngine searchEngine, boolean doAnimation) { + public void setOnSearchListener(OnSearchListener listener) { + mSearchListener = listener; + } + + public void setOnEditSuggestionListener(OnEditSuggestionListener listener) { + mEditSuggestionListener = listener; + } + + public void updateFromSearchEngine(SearchEngine searchEngine, boolean animate) { // Update search engine reference mSearchEngine = searchEngine; @@ -156,7 +198,7 @@ class SearchEngineRow extends AnimatedHeightLayout { final String suggestion = mSearchEngine.suggestions.get(i); setSuggestionOnView(suggestionItem, suggestion); - if (doAnimation) { + if (animate) { AlphaAnimation anim = new AlphaAnimation(0, 1); anim.setDuration(ANIMATION_DURATION); anim.setStartOffset(i * ANIMATION_DURATION); diff --git a/mobile/android/base/home/SearchLoader.java b/mobile/android/base/home/SearchLoader.java new file mode 100644 index 000000000000..4b78e98a9f03 --- /dev/null +++ b/mobile/android/base/home/SearchLoader.java @@ -0,0 +1,84 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.db.BrowserDB; + +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.Loader; +import android.text.TextUtils; + +/** + * Encapsulates the implementation of the search cursor loader. + */ +class SearchLoader { + // Key for search terms + private static final String KEY_SEARCH_TERM = "search_term"; + + // Key for performing empty search + private static final String KEY_PERFORM_EMPTY_SEARCH = "perform_empty_search"; + + private SearchLoader() { + } + + public static Loader createInstance(Context context, Bundle args) { + if (args != null) { + final String searchTerm = args.getString(KEY_SEARCH_TERM); + final boolean performEmptySearch = args.getBoolean(KEY_PERFORM_EMPTY_SEARCH, false); + return new SearchCursorLoader(context, searchTerm, performEmptySearch); + } else { + return new SearchCursorLoader(context, "", false); + } + } + + public static void restart(LoaderManager manager, int loaderId, + LoaderCallbacks callbacks, String searchTerm) { + restart(manager, loaderId, callbacks, searchTerm, true); + } + + public static void restart(LoaderManager manager, int loaderId, + LoaderCallbacks callbacks, String searchTerm, boolean performEmptySearch) { + Bundle bundle = new Bundle(); + bundle.putString(SearchLoader.KEY_SEARCH_TERM, searchTerm); + bundle.putBoolean(SearchLoader.KEY_PERFORM_EMPTY_SEARCH, performEmptySearch); + manager.restartLoader(loaderId, bundle, callbacks); + } + + public static class SearchCursorLoader extends SimpleCursorLoader { + // Max number of search results + private static final int SEARCH_LIMIT = 100; + + // The target search term associated with the loader + private final String mSearchTerm; + + // An empty search on the DB + private final boolean mPerformEmptySearch; + + public SearchCursorLoader(Context context, String searchTerm, boolean performEmptySearch) { + super(context); + mSearchTerm = searchTerm; + mPerformEmptySearch = performEmptySearch; + } + + @Override + public Cursor loadCursor() { + if (!mPerformEmptySearch && TextUtils.isEmpty(mSearchTerm)) { + return null; + } + + return BrowserDB.filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT); + } + + public String getSearchTerm() { + return mSearchTerm; + } + } + +} diff --git a/mobile/android/base/home/SimpleCursorLoader.java b/mobile/android/base/home/SimpleCursorLoader.java new file mode 100644 index 000000000000..913991999ceb --- /dev/null +++ b/mobile/android/base/home/SimpleCursorLoader.java @@ -0,0 +1,131 @@ +/* + * This is an adapted version of Android's original CursorLoader + * without all the ContentProvider-specific bits. + * + * Copyright (C) 2011 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 org.mozilla.gecko.home; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.support.v4.content.AsyncTaskLoader; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Arrays; + +abstract class SimpleCursorLoader extends AsyncTaskLoader { + final ForceLoadContentObserver mObserver; + Cursor mCursor; + + public SimpleCursorLoader(Context context) { + super(context); + mObserver = new ForceLoadContentObserver(); + } + + /** + * Loads the target cursor for this loader. This method is called + * on a worker thread. + */ + protected abstract Cursor loadCursor(); + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + Cursor cursor = loadCursor(); + + if (cursor != null) { + // Ensure the cursor window is filled + cursor.getCount(); + cursor.registerContentObserver(mObserver); + } + + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + + return; + } + + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + super.deliverResult(cursor); + } + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + oldCursor.close(); + } + } + + /** + * Starts an asynchronous load of the list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + protected void onStartLoading() { + if (mCursor != null) { + deliverResult(mCursor); + } + + if (takeContentChanged() || mCursor == null) { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + protected void onStopLoading() { + // Attempt to cancel the current load task if possible. + cancelLoad(); + } + + @Override + public void onCanceled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + } + + mCursor = null; + } +} \ No newline at end of file diff --git a/mobile/android/base/SuggestClient.java b/mobile/android/base/home/SuggestClient.java similarity index 98% rename from mobile/android/base/SuggestClient.java rename to mobile/android/base/home/SuggestClient.java index 20c90778ae40..9423684b278f 100644 --- a/mobile/android/base/SuggestClient.java +++ b/mobile/android/base/home/SuggestClient.java @@ -2,7 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko; +package org.mozilla.gecko.home; + +import org.mozilla.gecko.GeckoAppShell; import org.json.JSONArray; @@ -23,7 +25,7 @@ import java.util.ArrayList; /** * Use network-based search suggestions. */ -public class SuggestClient { +class SuggestClient { private static final String LOGTAG = "GeckoSuggestClient"; private static final String USER_AGENT = GeckoAppShell.getGeckoInterface().getDefaultUAString(); diff --git a/mobile/android/base/home/TabMenuStrip.java b/mobile/android/base/home/TabMenuStrip.java new file mode 100644 index 000000000000..ea81420d738d --- /dev/null +++ b/mobile/android/base/home/TabMenuStrip.java @@ -0,0 +1,202 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.Rect; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.util.Log; + +import org.mozilla.gecko.home.HomePager; +import org.mozilla.gecko.R; + +public class TabMenuStrip extends LinearLayout + implements HomePager.Decor, + View.OnFocusChangeListener { + private static final String LOGTAG = "GeckoTabMenuStrip"; + + private HomePager.OnTitleClickListener mOnTitleClickListener; + private Drawable mStrip; + private View mSelectedView; + + // Data associated with the scrolling of the strip drawable. + private View toTab; + private View fromTab; + private float progress; + + // This variable is used to predict the direction of scroll. + private float mPrevProgress; + + public TabMenuStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip); + final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1); + a.recycle(); + + if (stripResId != -1) { + mStrip = getResources().getDrawable(stripResId); + } + + setWillNotDraw(false); + } + + @Override + public void onAddPagerView(String title) { + final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false); + button.setText(title.toUpperCase()); + + addView(button); + button.setOnClickListener(new ViewClickListener(getChildCount() - 1)); + button.setOnFocusChangeListener(this); + } + + @Override + public void removeAllPagerViews() { + removeAllViews(); + } + + @Override + public void onPageSelected(final int position) { + mSelectedView = getChildAt(position); + + // Callback to measure and draw the strip after the view is visible. + ViewTreeObserver vto = mSelectedView.getViewTreeObserver(); + if (vto.isAlive()) { + vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + mSelectedView.getViewTreeObserver().removeGlobalOnLayoutListener(this); + + if (mStrip != null) { + mStrip.setBounds(mSelectedView.getLeft(), + mSelectedView.getTop(), + mSelectedView.getRight(), + mSelectedView.getBottom()); + } + + mPrevProgress = position; + } + }); + } + } + + // Page scroll animates the drawable and it's bounds from the previous to next child view. + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (mStrip == null) { + return; + } + + setScrollingData(position, positionOffset); + + final int fromTabLeft = fromTab.getLeft(); + final int fromTabRight = fromTab.getRight(); + + final int toTabLeft = toTab.getLeft(); + final int toTabRight = toTab.getRight(); + + mStrip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)), + 0, + (int) (fromTabRight + ((toTabRight - fromTabRight) * progress)), + getHeight()); + invalidate(); + } + + /* + * position + positionOffset goes from 0 to 2 as we scroll from page 1 to 3. + * Normalized progress is relative to the the direction the page is being scrolled towards. + * For this, we maintain direction of scroll with a state, and the child view we are moving towards and away from. + */ + private void setScrollingData(int position, float positionOffset) { + if (position >= getChildCount() - 1) { + return; + } + + final float currProgress = position + positionOffset; + + if (mPrevProgress > currProgress) { + toTab = getChildAt(position); + fromTab = getChildAt(position + 1); + progress = 1 - positionOffset; + } else { + toTab = getChildAt(position + 1); + fromTab = getChildAt(position); + progress = positionOffset; + } + + mPrevProgress = currProgress; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mStrip != null) { + mStrip.draw(canvas); + } + } + + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (v == this && hasFocus && getChildCount() > 0) { + mSelectedView.requestFocus(); + return; + } + + if (!hasFocus) { + return; + } + + int i = 0; + final int numTabs = getChildCount(); + + while (i < numTabs) { + View view = getChildAt(i); + if (view == v) { + view.requestFocus(); + if (isShown()) { + // A view is focused so send an event to announce the menu strip state. + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + break; + } + + i++; + } + } + + @Override + public void setOnTitleClickListener(HomePager.OnTitleClickListener onTitleClickListener) { + mOnTitleClickListener = onTitleClickListener; + } + + private class ViewClickListener implements OnClickListener { + private final int mIndex; + + public ViewClickListener(int index) { + mIndex = index; + } + + @Override + public void onClick(View view) { + if (mOnTitleClickListener != null) { + mOnTitleClickListener.onTitleClicked(mIndex); + } + } + } +} diff --git a/mobile/android/base/home/TopBookmarkItemView.java b/mobile/android/base/home/TopBookmarkItemView.java new file mode 100644 index 000000000000..39780ec79cc8 --- /dev/null +++ b/mobile/android/base/home/TopBookmarkItemView.java @@ -0,0 +1,225 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.Favicons; +import org.mozilla.gecko.R; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.ImageView.ScaleType; + +/** + * A view that displays the thumbnail and the title/url for a bookmark. + * If the title/url is longer than the width of the view, they are faded out. + * If there is no valid url, a default string is shown at 50% opacity. + * This is denoted by the empty state. + */ +public class TopBookmarkItemView extends RelativeLayout { + private static final String LOGTAG = "GeckoTopBookmarkItemView"; + + // Empty state, to denote there is no valid url. + private static final int[] STATE_EMPTY = { android.R.attr.state_empty }; + + // A Pin Drawable to denote pinned sites. + private static Drawable sPinDrawable = null; + + // Child views. + private final TextView mTitleView; + private final ImageView mThumbnailView; + private final ImageView mPinView; + + // Data backing this view. + private String mTitle; + private String mUrl; + + // Pinned state. + private boolean mIsPinned = false; + + // Empty state. + private boolean mIsEmpty = true; + + public TopBookmarkItemView(Context context) { + this(context, null); + } + + public TopBookmarkItemView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.topBookmarkItemViewStyle); + } + + public TopBookmarkItemView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + LayoutInflater.from(context).inflate(R.layout.top_bookmark_item_view, this); + + mTitleView = (TextView) findViewById(R.id.title); + mThumbnailView = (ImageView) findViewById(R.id.thumbnail); + mPinView = (ImageView) findViewById(R.id.pin); + } + + /** + * {@inheritDoc} + */ + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + + if (mIsEmpty) { + mergeDrawableStates(drawableState, STATE_EMPTY); + } + + return drawableState; + } + + /** + * @return The title shown by this view. + */ + public String getTitle() { + return (!TextUtils.isEmpty(mTitle) ? mTitle : mUrl); + } + + /** + * @return The url shown by this view. + */ + public String getUrl() { + return mUrl; + } + + /** + * @return true, if this view is pinned, false otherwise. + */ + public boolean isPinned() { + return mIsPinned; + } + + /** + * @param title The title for this view. + */ + public void setTitle(String title) { + if (mTitle != null && mTitle.equals(title)) { + return; + } + + mTitle = title; + updateTitleView(); + } + + /** + * @param url The url for this view. + */ + public void setUrl(String url) { + if (mUrl != null && mUrl.equals(url)) { + return; + } + + mUrl = url; + updateTitleView(); + } + + /** + * @param pinned The pinned state of this view. + */ + public void setPinned(boolean pinned) { + mIsPinned = pinned; + mPinView.setBackgroundDrawable(pinned ? getPinDrawable() : null); + } + + /** + * Display the thumbnail from a resource. + * + * @param resId Resource ID of the drawable to show. + */ + public void displayThumbnail(int resId) { + mThumbnailView.setScaleType(ScaleType.CENTER); + mThumbnailView.setImageResource(resId); + mThumbnailView.setBackgroundColor(0x0); + } + + /** + * Display the thumbnail from a bitmap. + * + * @param thumbnail The bitmap to show as thumbnail. + */ + public void displayThumbnail(Bitmap thumbnail) { + if (thumbnail == null) { + // Show a favicon based view instead. + displayThumbnail(R.drawable.favicon); + return; + } + + mThumbnailView.setScaleType(ScaleType.CENTER_CROP); + mThumbnailView.setImageBitmap(thumbnail); + mThumbnailView.setBackgroundDrawable(null); + } + + /** + * Display the thumbnail from a favicon. + * + * @param favicon The favicon to show as thumbnail. + */ + public void displayFavicon(Bitmap favicon) { + if (favicon == null) { + // Should show default favicon. + displayThumbnail(R.drawable.favicon); + return; + } + + mThumbnailView.setScaleType(ScaleType.CENTER); + mThumbnailView.setImageBitmap(favicon); + mThumbnailView.setBackgroundColor(Favicons.getInstance().getFaviconColor(favicon, mUrl)); + } + + /** + * Update the title shown by this view. If both title and url + * are empty, mark the state as STATE_EMPTY and show a default text. + */ + private void updateTitleView() { + String title = getTitle(); + if (!TextUtils.isEmpty(title)) { + mTitleView.setText(title); + mIsEmpty = false; + } else { + mTitleView.setText(R.string.bookmark_add); + mIsEmpty = true; + } + + // Refresh for state change. + refreshDrawableState(); + } + + /** + * @return Drawable to be used as a pin. + */ + private Drawable getPinDrawable() { + if (sPinDrawable == null) { + int size = getResources().getDimensionPixelSize(R.dimen.top_bookmark_pinsize); + + // Draw a little triangle in the upper right corner. + Path path = new Path(); + path.moveTo(0, 0); + path.lineTo(size, 0); + path.lineTo(size, size); + path.close(); + + sPinDrawable = new ShapeDrawable(new PathShape(path, size, size)); + Paint p = ((ShapeDrawable) sPinDrawable).getPaint(); + p.setColor(getResources().getColor(R.color.top_bookmark_pin)); + } + + return sPinDrawable; + } +} diff --git a/mobile/android/base/home/TopBookmarksAdapter.java b/mobile/android/base/home/TopBookmarksAdapter.java new file mode 100644 index 000000000000..e848ba649d3e --- /dev/null +++ b/mobile/android/base/home/TopBookmarksAdapter.java @@ -0,0 +1,113 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper; +import org.mozilla.gecko.db.BrowserDB.URLColumns; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.support.v4.widget.CursorAdapter; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Map; + +/** + * A cursor adapter holding the pinned and top bookmarks. + */ +public class TopBookmarksAdapter extends CursorAdapter { + // Cache to store the thumbnails. + private Map mThumbnails; + + /** + * Class to hold the bitmap of cached thumbnails/favicons. + */ + public static class Thumbnail { + // Thumbnail or favicon. + private final boolean isThumbnail; + + // Bitmap of thumbnail/favicon. + private final Bitmap bitmap; + + public Thumbnail(Bitmap bitmap, boolean isThumbnail) { + this.bitmap = bitmap; + this.isThumbnail = isThumbnail; + } + } + + public TopBookmarksAdapter(Context context, Cursor cursor) { + super(context, cursor); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onContentChanged() { + // Don't do anything. We don't want to regenerate every time + // our database is updated. + return; + } + + /** + * Update the thumbnails returned by the db. + * + * @param thumbnails A map of urls and their thumbnail bitmaps. + */ + public void updateThumbnails(Map thumbnails) { + mThumbnails = thumbnails; + notifyDataSetChanged(); + } + + /** + * {@inheritDoc} + */ + @Override + public void bindView(View bindView, Context context, Cursor cursor) { + String url = ""; + String title = ""; + boolean pinned = false; + + // Cursor is already moved to required position. + if (!cursor.isAfterLast()) { + url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); + title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); + pinned = ((TopSitesCursorWrapper) cursor).isPinned(); + } + + TopBookmarkItemView view = (TopBookmarkItemView) bindView; + view.setTitle(title); + view.setUrl(url); + view.setPinned(pinned); + + // If there is no url, then show "add bookmark". + if (TextUtils.isEmpty(url)) { + view.displayThumbnail(R.drawable.top_bookmark_add); + } else { + // Show the thumbnail. + Thumbnail thumbnail = (mThumbnails != null ? mThumbnails.get(url) : null); + if (thumbnail == null) { + view.displayThumbnail(null); + } else if (thumbnail.isThumbnail) { + view.displayThumbnail(thumbnail.bitmap); + } else { + view.displayFavicon(thumbnail.bitmap); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new TopBookmarkItemView(context); + } +} diff --git a/mobile/android/base/home/TopBookmarksView.java b/mobile/android/base/home/TopBookmarksView.java new file mode 100644 index 000000000000..84b2cde1b3cf --- /dev/null +++ b/mobile/android/base/home/TopBookmarksView.java @@ -0,0 +1,241 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.ThumbnailHelper; +import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.GridLayoutAnimationController; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.GridView; + +import java.util.EnumSet; + +/** + * A grid view of top bookmarks and pinned tabs. + * Each cell in the grid is a TopBookmarkItemView. + */ +public class TopBookmarksView extends GridView { + private static final String LOGTAG = "GeckoTopBookmarksView"; + + // Listener for pinning bookmarks. + public static interface OnPinBookmarkListener { + public void onPinBookmark(int position); + } + + // Max number of bookmarks that needs to be shown. + private final int mMaxBookmarks; + + // Number of columns to show. + private final int mNumColumns; + + // Horizontal spacing in between the rows. + private final int mHorizontalSpacing; + + // Vertical spacing in between the rows. + private final int mVerticalSpacing; + + // On URL open listener. + private OnUrlOpenListener mUrlOpenListener; + + // Pin bookmark listener. + private OnPinBookmarkListener mPinBookmarkListener; + + // Context menu info. + private TopBookmarksContextMenuInfo mContextMenuInfo; + + public TopBookmarksView(Context context) { + this(context, null); + } + + public TopBookmarksView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.topBookmarksViewStyle); + } + + public TopBookmarksView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mMaxBookmarks = getResources().getInteger(R.integer.number_of_top_sites); + mNumColumns = getResources().getInteger(R.integer.number_of_top_sites_cols); + setNumColumns(mNumColumns); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TopBookmarksView, defStyle, 0); + mHorizontalSpacing = a.getDimensionPixelOffset(R.styleable.TopBookmarksView_android_horizontalSpacing, 0x00); + mVerticalSpacing = a.getDimensionPixelOffset(R.styleable.TopBookmarksView_android_verticalSpacing, 0x00); + a.recycle(); + } + + /** + * {@inheritDoc} + */ + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + TopBookmarkItemView row = (TopBookmarkItemView) view; + String url = row.getUrl(); + + // If the url is empty, the user can pin a bookmark. + // If not, navigate to the page given by the url. + if (!TextUtils.isEmpty(url)) { + if (mUrlOpenListener != null) { + mUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(OnUrlOpenListener.Flags.class)); + } + } else { + if (mPinBookmarkListener != null) { + mPinBookmarkListener.onPinBookmark(position); + } + } + } + }); + + setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + Cursor cursor = (Cursor) parent.getItemAtPosition(position); + mContextMenuInfo = new TopBookmarksContextMenuInfo(view, position, id, cursor); + return showContextMenuForChild(TopBookmarksView.this); + } + }); + + final GridLayoutAnimationController controller = new GridLayoutAnimationController(AnimationUtils.loadAnimation(getContext(), R.anim.grow_fade_in_center)); + setLayoutAnimation(controller); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mUrlOpenListener = null; + mPinBookmarkListener = null; + } + + /** + * {@inheritDoc} + */ + @Override + public int getColumnWidth() { + // This method will be called from onMeasure() too. + // It's better to use getMeasuredWidth(), as it is safe in this case. + final int totalHorizontalSpacing = mNumColumns > 0 ? (mNumColumns - 1) * mHorizontalSpacing : 0; + return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - totalHorizontalSpacing) / mNumColumns; + } + + /** + * {@inheritDoc} + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Sets the padding for this view. + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final int measuredWidth = getMeasuredWidth(); + final int childWidth = getColumnWidth(); + int childHeight = 0; + + // Set the column width as the thumbnail width. + ThumbnailHelper.getInstance().setThumbnailWidth(childWidth); + + // If there's an adapter, use it to calculate the height of this view. + final TopBookmarksAdapter adapter = (TopBookmarksAdapter) getAdapter(); + final int count; + + // There shouldn't be any inherent size (due to padding) if there are no child views. + if (adapter == null || (count = adapter.getCount()) == 0) { + setMeasuredDimension(0, 0); + return; + } + + // Get the first child from the adapter. + final View child = adapter.getView(0, null, this); + if (child != null) { + // Set a default LayoutParams on the child, if it doesn't have one on its own. + AbsListView.LayoutParams params = (AbsListView.LayoutParams) child.getLayoutParams(); + if (params == null) { + params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT, + AbsListView.LayoutParams.WRAP_CONTENT); + child.setLayoutParams(params); + } + + // Measure the exact width of the child, and the height based on the width. + // Note: the child (and BookmarkThumbnailView) takes care of calculating its height. + int childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); + int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + child.measure(childWidthSpec, childHeightSpec); + childHeight = child.getMeasuredHeight(); + } + + // Find the minimum of bookmarks we need to show, and the one given by the cursor. + final int total = Math.min(count > 0 ? count : Integer.MAX_VALUE, mMaxBookmarks); + + // Number of rows required to show these bookmarks. + final int rows = (int) Math.ceil((double) total / mNumColumns); + final int childrenHeight = childHeight * rows; + final int totalVerticalSpacing = rows > 0 ? (rows - 1) * mVerticalSpacing : 0; + + // Total height of this view. + final int measuredHeight = childrenHeight + getPaddingTop() + getPaddingBottom() + totalVerticalSpacing; + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + public ContextMenuInfo getContextMenuInfo() { + return mContextMenuInfo; + } + + /** + * Set an url open listener to be used by this view. + * + * @param listener An url open listener for this view. + */ + public void setOnUrlOpenListener(OnUrlOpenListener listener) { + mUrlOpenListener = listener; + } + + /** + * Set a pin bookmark listener to be used by this view. + * + * @param listener A pin bookmark listener for this view. + */ + public void setOnPinBookmarkListener(OnPinBookmarkListener listener) { + mPinBookmarkListener = listener; + } + + /** + * A ContextMenuInfo for TopBoomarksView that adds details from the cursor. + */ + public static class TopBookmarksContextMenuInfo extends AdapterContextMenuInfo { + public String url; + public String title; + public boolean isPinned; + + public TopBookmarksContextMenuInfo(View targetView, int position, long id, Cursor cursor) { + super(targetView, position, id); + + if (cursor == null) { + return; + } + + url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); + title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); + isPinned = ((TopSitesCursorWrapper) cursor).isPinned(); + } + } +} diff --git a/mobile/android/base/home/TwoLinePageRow.java b/mobile/android/base/home/TwoLinePageRow.java new file mode 100644 index 000000000000..2898beacf6fa --- /dev/null +++ b/mobile/android/base/home/TwoLinePageRow.java @@ -0,0 +1,216 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.Favicons; +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.gecko.widget.FaviconView; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class TwoLinePageRow extends LinearLayout + implements Tabs.OnTabsChangedListener { + private static final int NO_ICON = 0; + + private final TextView mTitle; + private final TextView mUrl; + private final FaviconView mFavicon; + + private int mUrlIconId; + private int mBookmarkIconId; + private boolean mShowIcons; + + // The URL for the page corresponding to this view. + private String mPageUrl; + + public TwoLinePageRow(Context context) { + this(context, null); + } + + public TwoLinePageRow(Context context, AttributeSet attrs) { + super(context, attrs); + + setGravity(Gravity.CENTER_VERTICAL); + + mUrlIconId = NO_ICON; + mBookmarkIconId = NO_ICON; + mShowIcons = true; + + LayoutInflater.from(context).inflate(R.layout.two_line_page_row, this); + mTitle = (TextView) findViewById(R.id.title); + mUrl = (TextView) findViewById(R.id.url); + mFavicon = (FaviconView) findViewById(R.id.favicon); + } + + @Override + protected void onAttachedToWindow() { + Tabs.registerOnTabsChangedListener(this); + } + + @Override + protected void onDetachedFromWindow() { + // Delay removing the listener to avoid modifying mTabsChangedListeners + // while notifyListeners is iterating through the array. + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + Tabs.unregisterOnTabsChangedListener(TwoLinePageRow.this); + } + }); + } + + @Override + public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) { + switch(msg) { + case ADDED: + case CLOSED: + case LOCATION_CHANGE: + updateDisplayedUrl(); + break; + } + } + + private void setTitle(String title) { + mTitle.setText(title); + } + + private void setUrl(String url) { + mUrl.setText(url); + } + + private void setUrl(int stringId) { + mUrl.setText(stringId); + } + + private void setUrlIcon(int urlIconId) { + if (mUrlIconId == urlIconId) { + return; + } + + mUrlIconId = urlIconId; + mUrl.setCompoundDrawablesWithIntrinsicBounds(mUrlIconId, 0, mBookmarkIconId, 0); + } + + private void setFaviconWithUrl(Bitmap favicon, String url) { + mFavicon.updateImage(favicon, url); + } + + private void setBookmarkIcon(int bookmarkIconId) { + if (mBookmarkIconId == bookmarkIconId) { + return; + } + + mBookmarkIconId = bookmarkIconId; + mUrl.setCompoundDrawablesWithIntrinsicBounds(mUrlIconId, 0, mBookmarkIconId, 0); + } + + /** + * Stores the page URL, so that we can use it to replace "Switch to tab" if the open + * tab changes or is closed. + */ + private void updateDisplayedUrl(String url) { + mPageUrl = url; + updateDisplayedUrl(); + } + + /** + * Replaces the page URL with "Switch to tab" if there is already a tab open with that URL. + */ + private void updateDisplayedUrl() { + int tabId = Tabs.getInstance().getTabIdForUrl(mPageUrl); + if (!mShowIcons || tabId < 0) { + setUrl(mPageUrl); + setUrlIcon(NO_ICON); + } else { + setUrl(R.string.switch_to_tab); + setUrlIcon(R.drawable.ic_url_bar_tab); + } + } + + public void setShowIcons(boolean showIcons) { + mShowIcons = showIcons; + } + + public void updateFromCursor(Cursor cursor) { + if (cursor == null) { + return; + } + + int titleIndex = cursor.getColumnIndexOrThrow(URLColumns.TITLE); + final String title = cursor.getString(titleIndex); + + int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL); + final String url = cursor.getString(urlIndex); + + // Use the URL instead of an empty title for consistency with the normal URL + // bar view - this is the equivalent of getDisplayTitle() in Tab.java + setTitle(TextUtils.isEmpty(title) ? url : title); + + updateDisplayedUrl(url); + + int faviconIndex = cursor.getColumnIndex(URLColumns.FAVICON); + if (faviconIndex != -1) { + byte[] b = cursor.getBlob(faviconIndex); + + Bitmap favicon = null; + if (b != null) { + Bitmap bitmap = BitmapUtils.decodeByteArray(b); + if (bitmap != null) { + favicon = Favicons.getInstance().scaleImage(bitmap); + } + } + + setFaviconWithUrl(favicon, url); + } else { + // If favicons is not on the cursor, try to fetch it from the memory cache + setFaviconWithUrl(Favicons.getInstance().getFaviconFromMemCache(url), url); + } + + // Don't show bookmark/reading list icon, if not needed. + if (!mShowIcons) { + return; + } + + final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID); + if (bookmarkIdIndex != -1) { + final long bookmarkId = cursor.getLong(bookmarkIdIndex); + final int displayIndex = cursor.getColumnIndex(Combined.DISPLAY); + + final int display; + if (displayIndex != -1) { + display = cursor.getInt(displayIndex); + } else { + display = Combined.DISPLAY_NORMAL; + } + + // The bookmark id will be 0 (null in database) when the url + // is not a bookmark. + if (bookmarkId == 0) { + setBookmarkIcon(NO_ICON); + } else if (display == Combined.DISPLAY_READER) { + setBookmarkIcon(R.drawable.ic_url_bar_reader); + } else { + setBookmarkIcon(R.drawable.ic_url_bar_star); + } + } else { + setBookmarkIcon(NO_ICON); + } + } +} diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index f696318ef163..c9313ea12bda 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -6,10 +6,13 @@ - - - - + + + + + + @@ -30,10 +33,7 @@ - - - - + @@ -217,8 +217,7 @@ size. --> - - + @@ -227,6 +226,9 @@ size. --> + + + @@ -234,6 +236,7 @@ size. --> + @@ -246,7 +249,6 @@ size. --> - @@ -269,28 +271,22 @@ size. --> - - - - - - - - + + + + + + + + + + - - - - - - + + diff --git a/mobile/android/base/resources/anim/awesomebar_fade_in.xml b/mobile/android/base/resources/anim/awesomebar_fade_in.xml deleted file mode 100644 index 84118a90bb99..000000000000 --- a/mobile/android/base/resources/anim/awesomebar_fade_in.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/mobile/android/base/resources/anim/awesomebar_fade_out.xml b/mobile/android/base/resources/anim/awesomebar_fade_out.xml deleted file mode 100644 index 958d6551da4d..000000000000 --- a/mobile/android/base/resources/anim/awesomebar_fade_out.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/mobile/android/base/resources/color/abouthome_section_subtitle.xml b/mobile/android/base/resources/color/abouthome_section_subtitle.xml deleted file mode 100644 index d77107512df1..000000000000 --- a/mobile/android/base/resources/color/abouthome_section_subtitle.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/color/abouthome_section_title.xml b/mobile/android/base/resources/color/abouthome_section_title.xml deleted file mode 100644 index 3f3daa4d3a68..000000000000 --- a/mobile/android/base/resources/color/abouthome_section_title.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/awesomebar_header_row.xml b/mobile/android/base/resources/color/top_bookmark_item_title.xml similarity index 66% rename from mobile/android/base/resources/drawable/awesomebar_header_row.xml rename to mobile/android/base/resources/color/top_bookmark_item_title.xml index 9c1d9e5ad5e9..293573025391 100644 --- a/mobile/android/base/resources/drawable/awesomebar_header_row.xml +++ b/mobile/android/base/resources/color/top_bookmark_item_title.xml @@ -4,9 +4,11 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - + + + + + diff --git a/mobile/android/base/resources/color/awesome_bar_title.xml b/mobile/android/base/resources/color/url_bar_title.xml similarity index 100% rename from mobile/android/base/resources/color/awesome_bar_title.xml rename to mobile/android/base/resources/color/url_bar_title.xml diff --git a/mobile/android/base/resources/color/awesome_bar_title_hint.xml b/mobile/android/base/resources/color/url_bar_title_hint.xml similarity index 100% rename from mobile/android/base/resources/color/awesome_bar_title_hint.xml rename to mobile/android/base/resources/color/url_bar_title_hint.xml diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_icon.png b/mobile/android/base/resources/drawable-hdpi/abouthome_icon.png deleted file mode 100644 index 45fff4457934..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_icon.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_logo_dark.png b/mobile/android/base/resources/drawable-hdpi/abouthome_logo_dark.png deleted file mode 100644 index a7e92b1d9e30..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_logo_dark.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_logo_light.png b/mobile/android/base/resources/drawable-hdpi/abouthome_logo_light.png deleted file mode 100644 index 15bdd9280dbe..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_logo_light.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_promo_box_bg.9.png b/mobile/android/base/resources/drawable-hdpi/abouthome_promo_box_bg.9.png deleted file mode 100644 index aa1757dcf9ec..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_promo_box_bg.9.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_promo_box_pressed_bg.9.png b/mobile/android/base/resources/drawable-hdpi/abouthome_promo_box_pressed_bg.9.png deleted file mode 100644 index 1477ecf228ca..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_promo_box_pressed_bg.9.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_promo_logo_apps.png b/mobile/android/base/resources/drawable-hdpi/abouthome_promo_logo_apps.png deleted file mode 100644 index ad52006d223a..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_promo_logo_apps.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_promo_logo_sync.png b/mobile/android/base/resources/drawable-hdpi/abouthome_promo_logo_sync.png deleted file mode 100644 index 6ee558a0985b..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_promo_logo_sync.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail.png b/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail.png index 870e1f39bf87..59996e67015a 100644 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail.png and b/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail_add.png b/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail_add.png deleted file mode 100644 index afa1421f72eb..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail_add.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail_bg.png b/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail_bg.png deleted file mode 100644 index b92c28fc25d3..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail_bg.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/bookmark_folder_closed.png b/mobile/android/base/resources/drawable-hdpi/bookmark_folder_closed.png new file mode 100644 index 000000000000..fa29670fbd82 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/bookmark_folder_closed.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/bookmark_folder_opened.png b/mobile/android/base/resources/drawable-hdpi/bookmark_folder_opened.png new file mode 100644 index 000000000000..ab96b22db613 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/bookmark_folder_opened.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/history_tabs_indicator_selected.9.png b/mobile/android/base/resources/drawable-hdpi/history_tabs_indicator_selected.9.png new file mode 100644 index 000000000000..03ead6aa8b1c Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/history_tabs_indicator_selected.9.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/home_tab_menu_strip.9.png b/mobile/android/base/resources/drawable-hdpi/home_tab_menu_strip.9.png new file mode 100644 index 000000000000..9ad4464c910c Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/home_tab_menu_strip.9.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/ic_addons_empty.png b/mobile/android/base/resources/drawable-hdpi/ic_addons_empty.png deleted file mode 100644 index 866a0aaf0531..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/ic_addons_empty.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_reader.png b/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_reader.png deleted file mode 100644 index 7adf4a3e6bfd..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_reader.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_star.png b/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_star.png deleted file mode 100644 index a4aeae9891c7..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_star.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_tab.png b/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_tab.png deleted file mode 100644 index cfbabf863c67..000000000000 Binary files a/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_tab.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_go.png b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_go.png similarity index 100% rename from mobile/android/base/resources/drawable-hdpi/ic_awesomebar_go.png rename to mobile/android/base/resources/drawable-hdpi/ic_url_bar_go.png diff --git a/mobile/android/base/resources/drawable-hdpi/ic_url_bar_reader.png b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_reader.png new file mode 100644 index 000000000000..ce510413a36e Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_reader.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/ic_awesomebar_search.png b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_search.png similarity index 100% rename from mobile/android/base/resources/drawable-hdpi/ic_awesomebar_search.png rename to mobile/android/base/resources/drawable-hdpi/ic_url_bar_search.png diff --git a/mobile/android/base/resources/drawable-hdpi/ic_url_bar_star.png b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_star.png new file mode 100644 index 000000000000..90c1689e7a1b Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_star.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/ic_url_bar_tab.png b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_tab.png new file mode 100644 index 000000000000..cffa0f21555d Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/ic_url_bar_tab.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/icon_last_tabs.png b/mobile/android/base/resources/drawable-hdpi/icon_last_tabs.png new file mode 100644 index 000000000000..8c2b41f9d2f4 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/icon_last_tabs.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/icon_last_tabs_empty.png b/mobile/android/base/resources/drawable-hdpi/icon_last_tabs_empty.png new file mode 100644 index 000000000000..bfd618b8edd2 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/icon_last_tabs_empty.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/icon_most_recent.png b/mobile/android/base/resources/drawable-hdpi/icon_most_recent.png new file mode 100644 index 000000000000..1924bc4a0445 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/icon_most_recent.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/icon_most_recent_empty.png b/mobile/android/base/resources/drawable-hdpi/icon_most_recent_empty.png new file mode 100644 index 000000000000..3259ebcb9e91 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/icon_most_recent_empty.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/icon_most_visited.png b/mobile/android/base/resources/drawable-hdpi/icon_most_visited.png new file mode 100644 index 000000000000..e7cf2b3b9ce1 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/icon_most_visited.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/icon_most_visited_empty.png b/mobile/android/base/resources/drawable-hdpi/icon_most_visited_empty.png new file mode 100644 index 000000000000..93f5bf2d0ddb Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/icon_most_visited_empty.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/icon_reading_list_empty.png b/mobile/android/base/resources/drawable-hdpi/icon_reading_list_empty.png new file mode 100644 index 000000000000..b272b4bae231 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/icon_reading_list_empty.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/reader_cropped.png b/mobile/android/base/resources/drawable-hdpi/reader_cropped.png new file mode 100644 index 000000000000..75943d8afcb2 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/reader_cropped.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/top_bookmark_add.png b/mobile/android/base/resources/drawable-hdpi/top_bookmark_add.png new file mode 100644 index 000000000000..59427ab670b7 Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/top_bookmark_add.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/address_bar_bg_shadow.png b/mobile/android/base/resources/drawable-hdpi/url_bar_bg_shadow.png similarity index 100% rename from mobile/android/base/resources/drawable-hdpi/address_bar_bg_shadow.png rename to mobile/android/base/resources/drawable-hdpi/url_bar_bg_shadow.png diff --git a/mobile/android/base/resources/drawable-hdpi/address_bar_url_default.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default.9.png similarity index 100% rename from mobile/android/base/resources/drawable-hdpi/address_bar_url_default.9.png rename to mobile/android/base/resources/drawable-hdpi/url_bar_entry_default.9.png diff --git a/mobile/android/base/resources/drawable-hdpi/address_bar_url_default_pb.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_default_pb.9.png similarity index 100% rename from mobile/android/base/resources/drawable-hdpi/address_bar_url_default_pb.9.png rename to mobile/android/base/resources/drawable-hdpi/url_bar_entry_default_pb.9.png diff --git a/mobile/android/base/resources/drawable-hdpi/address_bar_url_pressed.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed.9.png similarity index 100% rename from mobile/android/base/resources/drawable-hdpi/address_bar_url_pressed.9.png rename to mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed.9.png diff --git a/mobile/android/base/resources/drawable-hdpi/address_bar_url_pressed_pb.9.png b/mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed_pb.9.png similarity index 100% rename from mobile/android/base/resources/drawable-hdpi/address_bar_url_pressed_pb.9.png rename to mobile/android/base/resources/drawable-hdpi/url_bar_entry_pressed_pb.9.png diff --git a/mobile/android/base/resources/drawable-large-land-v11/home_history_tabs_indicator.xml b/mobile/android/base/resources/drawable-large-land-v11/home_history_tabs_indicator.xml new file mode 100644 index 000000000000..d2c005d21a74 --- /dev/null +++ b/mobile/android/base/resources/drawable-large-land-v11/home_history_tabs_indicator.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_icon.png b/mobile/android/base/resources/drawable-mdpi/abouthome_icon.png deleted file mode 100644 index 115d4ee88956..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_icon.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_logo_dark.png b/mobile/android/base/resources/drawable-mdpi/abouthome_logo_dark.png deleted file mode 100644 index 983bfe730cb8..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_logo_dark.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_logo_light.png b/mobile/android/base/resources/drawable-mdpi/abouthome_logo_light.png deleted file mode 100644 index 6a4bfaad4bde..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_logo_light.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_promo_box_bg.9.png b/mobile/android/base/resources/drawable-mdpi/abouthome_promo_box_bg.9.png deleted file mode 100644 index fe3e40c31a49..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_promo_box_bg.9.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_promo_box_pressed_bg.9.png b/mobile/android/base/resources/drawable-mdpi/abouthome_promo_box_pressed_bg.9.png deleted file mode 100644 index 2aab34ffa354..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_promo_box_pressed_bg.9.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_promo_logo_apps.png b/mobile/android/base/resources/drawable-mdpi/abouthome_promo_logo_apps.png deleted file mode 100644 index 8db8846a255e..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_promo_logo_apps.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_promo_logo_sync.png b/mobile/android/base/resources/drawable-mdpi/abouthome_promo_logo_sync.png deleted file mode 100644 index 0f36ae8704dc..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_promo_logo_sync.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail.png b/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail.png index e87aa340939e..89bb88ac4bf0 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail.png and b/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail_add.png b/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail_add.png deleted file mode 100644 index c1b22593e181..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail_add.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail_bg.png b/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail_bg.png deleted file mode 100644 index b7398d4aedb0..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail_bg.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/bookmark_folder_closed.png b/mobile/android/base/resources/drawable-mdpi/bookmark_folder_closed.png new file mode 100644 index 000000000000..2ca9e2182096 Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/bookmark_folder_closed.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/bookmark_folder_opened.png b/mobile/android/base/resources/drawable-mdpi/bookmark_folder_opened.png new file mode 100644 index 000000000000..9edb231d95ef Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/bookmark_folder_opened.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/history_tabs_indicator_selected.9.png b/mobile/android/base/resources/drawable-mdpi/history_tabs_indicator_selected.9.png new file mode 100644 index 000000000000..5def384c1389 Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/history_tabs_indicator_selected.9.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/home_tab_menu_strip.9.png b/mobile/android/base/resources/drawable-mdpi/home_tab_menu_strip.9.png new file mode 100644 index 000000000000..9ad4464c910c Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/home_tab_menu_strip.9.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/ic_addons_empty.png b/mobile/android/base/resources/drawable-mdpi/ic_addons_empty.png deleted file mode 100644 index 1e46f6a2e94d..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/ic_addons_empty.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_reader.png b/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_reader.png deleted file mode 100644 index dd312a7f629e..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_reader.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_star.png b/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_star.png deleted file mode 100644 index eaf4e4f8b65b..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_star.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_tab.png b/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_tab.png deleted file mode 100644 index 514d02acee7c..000000000000 Binary files a/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_tab.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_go.png b/mobile/android/base/resources/drawable-mdpi/ic_url_bar_go.png similarity index 100% rename from mobile/android/base/resources/drawable-mdpi/ic_awesomebar_go.png rename to mobile/android/base/resources/drawable-mdpi/ic_url_bar_go.png diff --git a/mobile/android/base/resources/drawable-mdpi/ic_url_bar_reader.png b/mobile/android/base/resources/drawable-mdpi/ic_url_bar_reader.png new file mode 100644 index 000000000000..ada0338ea6cc Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/ic_url_bar_reader.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/ic_awesomebar_search.png b/mobile/android/base/resources/drawable-mdpi/ic_url_bar_search.png similarity index 100% rename from mobile/android/base/resources/drawable-mdpi/ic_awesomebar_search.png rename to mobile/android/base/resources/drawable-mdpi/ic_url_bar_search.png diff --git a/mobile/android/base/resources/drawable-mdpi/ic_url_bar_star.png b/mobile/android/base/resources/drawable-mdpi/ic_url_bar_star.png new file mode 100644 index 000000000000..dcfeb870371a Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/ic_url_bar_star.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/ic_url_bar_tab.png b/mobile/android/base/resources/drawable-mdpi/ic_url_bar_tab.png new file mode 100644 index 000000000000..ac9d3a499d9b Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/ic_url_bar_tab.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/icon_last_tabs.png b/mobile/android/base/resources/drawable-mdpi/icon_last_tabs.png new file mode 100644 index 000000000000..f577fd4c46c1 Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/icon_last_tabs.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/icon_last_tabs_empty.png b/mobile/android/base/resources/drawable-mdpi/icon_last_tabs_empty.png new file mode 100644 index 000000000000..228a34dc3611 Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/icon_last_tabs_empty.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/icon_most_recent.png b/mobile/android/base/resources/drawable-mdpi/icon_most_recent.png new file mode 100644 index 000000000000..e0cfd2ddb781 Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/icon_most_recent.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/icon_most_recent_empty.png b/mobile/android/base/resources/drawable-mdpi/icon_most_recent_empty.png new file mode 100644 index 000000000000..918a0546298b Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/icon_most_recent_empty.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/icon_most_visited.png b/mobile/android/base/resources/drawable-mdpi/icon_most_visited.png new file mode 100644 index 000000000000..e7ef4b91e472 Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/icon_most_visited.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/icon_most_visited_empty.png b/mobile/android/base/resources/drawable-mdpi/icon_most_visited_empty.png new file mode 100644 index 000000000000..bb2881758447 Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/icon_most_visited_empty.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/icon_reading_list_empty.png b/mobile/android/base/resources/drawable-mdpi/icon_reading_list_empty.png new file mode 100644 index 000000000000..941aa2b81447 Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/icon_reading_list_empty.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/reader_cropped.png b/mobile/android/base/resources/drawable-mdpi/reader_cropped.png new file mode 100644 index 000000000000..4dd118650bec Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/reader_cropped.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/top_bookmark_add.png b/mobile/android/base/resources/drawable-mdpi/top_bookmark_add.png new file mode 100644 index 000000000000..1f1a4ae5cfda Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/top_bookmark_add.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/address_bar_bg_shadow.png b/mobile/android/base/resources/drawable-mdpi/url_bar_bg_shadow.png similarity index 100% rename from mobile/android/base/resources/drawable-mdpi/address_bar_bg_shadow.png rename to mobile/android/base/resources/drawable-mdpi/url_bar_bg_shadow.png diff --git a/mobile/android/base/resources/drawable-mdpi/address_bar_url_default.9.png b/mobile/android/base/resources/drawable-mdpi/url_bar_entry_default.9.png similarity index 100% rename from mobile/android/base/resources/drawable-mdpi/address_bar_url_default.9.png rename to mobile/android/base/resources/drawable-mdpi/url_bar_entry_default.9.png diff --git a/mobile/android/base/resources/drawable-mdpi/address_bar_url_default_pb.9.png b/mobile/android/base/resources/drawable-mdpi/url_bar_entry_default_pb.9.png similarity index 100% rename from mobile/android/base/resources/drawable-mdpi/address_bar_url_default_pb.9.png rename to mobile/android/base/resources/drawable-mdpi/url_bar_entry_default_pb.9.png diff --git a/mobile/android/base/resources/drawable-mdpi/address_bar_url_pressed.9.png b/mobile/android/base/resources/drawable-mdpi/url_bar_entry_pressed.9.png similarity index 100% rename from mobile/android/base/resources/drawable-mdpi/address_bar_url_pressed.9.png rename to mobile/android/base/resources/drawable-mdpi/url_bar_entry_pressed.9.png diff --git a/mobile/android/base/resources/drawable-mdpi/address_bar_url_pressed_pb.9.png b/mobile/android/base/resources/drawable-mdpi/url_bar_entry_pressed_pb.9.png similarity index 100% rename from mobile/android/base/resources/drawable-mdpi/address_bar_url_pressed_pb.9.png rename to mobile/android/base/resources/drawable-mdpi/url_bar_entry_pressed_pb.9.png diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_icon.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_icon.png deleted file mode 100644 index 1a4cc1497528..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_icon.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_logo_dark.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_logo_dark.png deleted file mode 100644 index b145af558ee1..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_logo_dark.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_logo_light.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_logo_light.png deleted file mode 100644 index e07197aadbc8..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_logo_light.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_box_bg.9.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_box_bg.9.png deleted file mode 100644 index 5816662fb55f..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_box_bg.9.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_box_pressed_bg.9.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_box_pressed_bg.9.png deleted file mode 100644 index 01295441c9d4..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_box_pressed_bg.9.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_logo_apps.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_logo_apps.png deleted file mode 100644 index 7edf146defb8..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_logo_apps.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_logo_sync.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_logo_sync.png deleted file mode 100644 index d916e56efc46..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_promo_logo_sync.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail.png index 96266ac5be47..e2a3df9dec03 100644 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail.png and b/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail_add.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail_add.png deleted file mode 100644 index 14bf1ffa3b42..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail_add.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail_bg.png b/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail_bg.png deleted file mode 100644 index edc521d7b387..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail_bg.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/bookmark_folder_closed.png b/mobile/android/base/resources/drawable-xhdpi/bookmark_folder_closed.png new file mode 100644 index 000000000000..7582d2f07275 Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/bookmark_folder_closed.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/bookmark_folder_opened.png b/mobile/android/base/resources/drawable-xhdpi/bookmark_folder_opened.png new file mode 100644 index 000000000000..03076f7bd6b9 Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/bookmark_folder_opened.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/history_tabs_indicator_selected.9.png b/mobile/android/base/resources/drawable-xhdpi/history_tabs_indicator_selected.9.png new file mode 100644 index 000000000000..86ba13a5f6fd Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/history_tabs_indicator_selected.9.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/home_tab_menu_strip.9.png b/mobile/android/base/resources/drawable-xhdpi/home_tab_menu_strip.9.png new file mode 100644 index 000000000000..448235e41095 Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/home_tab_menu_strip.9.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_addons_empty.png b/mobile/android/base/resources/drawable-xhdpi/ic_addons_empty.png deleted file mode 100644 index 30ebe81ba8f2..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/ic_addons_empty.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_reader.png b/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_reader.png deleted file mode 100644 index 9ea7fc4ed305..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_reader.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_star.png b/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_star.png deleted file mode 100644 index 566707576e45..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_star.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_tab.png b/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_tab.png deleted file mode 100644 index 2d5e6c70236b..000000000000 Binary files a/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_tab.png and /dev/null differ diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_go.png b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_go.png similarity index 100% rename from mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_go.png rename to mobile/android/base/resources/drawable-xhdpi/ic_url_bar_go.png diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_reader.png b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_reader.png new file mode 100644 index 000000000000..e02ea09a932d Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_reader.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_search.png b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_search.png similarity index 100% rename from mobile/android/base/resources/drawable-xhdpi/ic_awesomebar_search.png rename to mobile/android/base/resources/drawable-xhdpi/ic_url_bar_search.png diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_star.png b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_star.png new file mode 100644 index 000000000000..9b63734db6a5 Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_star.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_tab.png b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_tab.png new file mode 100644 index 000000000000..19b90cee88ca Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/ic_url_bar_tab.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_last_tabs.png b/mobile/android/base/resources/drawable-xhdpi/icon_last_tabs.png new file mode 100644 index 000000000000..c102e8c9e17e Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/icon_last_tabs.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_last_tabs_empty.png b/mobile/android/base/resources/drawable-xhdpi/icon_last_tabs_empty.png new file mode 100644 index 000000000000..1f148d0573a9 Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/icon_last_tabs_empty.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_most_recent.png b/mobile/android/base/resources/drawable-xhdpi/icon_most_recent.png new file mode 100644 index 000000000000..7cd12a9abeab Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/icon_most_recent.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_most_recent_empty.png b/mobile/android/base/resources/drawable-xhdpi/icon_most_recent_empty.png new file mode 100644 index 000000000000..cc0361c13c1b Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/icon_most_recent_empty.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_most_visited.png b/mobile/android/base/resources/drawable-xhdpi/icon_most_visited.png new file mode 100644 index 000000000000..ccdb7fca6217 Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/icon_most_visited.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_most_visited_empty.png b/mobile/android/base/resources/drawable-xhdpi/icon_most_visited_empty.png new file mode 100644 index 000000000000..4182b7e4c36a Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/icon_most_visited_empty.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/icon_reading_list_empty.png b/mobile/android/base/resources/drawable-xhdpi/icon_reading_list_empty.png new file mode 100644 index 000000000000..92f86751139f Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/icon_reading_list_empty.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/reader_cropped.png b/mobile/android/base/resources/drawable-xhdpi/reader_cropped.png new file mode 100644 index 000000000000..d066d5ae0041 Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/reader_cropped.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/top_bookmark_add.png b/mobile/android/base/resources/drawable-xhdpi/top_bookmark_add.png new file mode 100644 index 000000000000..da9f1907a519 Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/top_bookmark_add.png differ diff --git a/mobile/android/base/resources/drawable-xhdpi/address_bar_bg_shadow.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_bg_shadow.png similarity index 100% rename from mobile/android/base/resources/drawable-xhdpi/address_bar_bg_shadow.png rename to mobile/android/base/resources/drawable-xhdpi/url_bar_bg_shadow.png diff --git a/mobile/android/base/resources/drawable-xhdpi/address_bar_url_default.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default.9.png similarity index 100% rename from mobile/android/base/resources/drawable-xhdpi/address_bar_url_default.9.png rename to mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default.9.png diff --git a/mobile/android/base/resources/drawable-xhdpi/address_bar_url_default_pb.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default_pb.9.png similarity index 100% rename from mobile/android/base/resources/drawable-xhdpi/address_bar_url_default_pb.9.png rename to mobile/android/base/resources/drawable-xhdpi/url_bar_entry_default_pb.9.png diff --git a/mobile/android/base/resources/drawable-xhdpi/address_bar_url_pressed.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed.9.png similarity index 100% rename from mobile/android/base/resources/drawable-xhdpi/address_bar_url_pressed.9.png rename to mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed.9.png diff --git a/mobile/android/base/resources/drawable-xhdpi/address_bar_url_pressed_pb.9.png b/mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png similarity index 100% rename from mobile/android/base/resources/drawable-xhdpi/address_bar_url_pressed_pb.9.png rename to mobile/android/base/resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png diff --git a/mobile/android/base/resources/drawable-xlarge-v11/home_history_tabs_indicator.xml b/mobile/android/base/resources/drawable-xlarge-v11/home_history_tabs_indicator.xml new file mode 100644 index 000000000000..d2c005d21a74 --- /dev/null +++ b/mobile/android/base/resources/drawable-xlarge-v11/home_history_tabs_indicator.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/base/resources/drawable/abouthome_logo.xml b/mobile/android/base/resources/drawable/abouthome_logo.xml deleted file mode 100644 index 1acd0bf6cfaf..000000000000 --- a/mobile/android/base/resources/drawable/abouthome_logo.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/abouthome_promo_box.xml b/mobile/android/base/resources/drawable/abouthome_promo_box.xml deleted file mode 100644 index 9af27d70966d..000000000000 --- a/mobile/android/base/resources/drawable/abouthome_promo_box.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/awesomebar_tab_indicator.xml b/mobile/android/base/resources/drawable/awesomebar_tab_indicator.xml deleted file mode 100644 index d87ba74eb869..000000000000 --- a/mobile/android/base/resources/drawable/awesomebar_tab_indicator.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/awesomebar_tab_selected.xml b/mobile/android/base/resources/drawable/awesomebar_tab_selected.xml deleted file mode 100644 index be1e6e333c43..000000000000 --- a/mobile/android/base/resources/drawable/awesomebar_tab_selected.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/drawable/awesomebar_tab_unselected.xml b/mobile/android/base/resources/drawable/awesomebar_tab_unselected.xml deleted file mode 100644 index b71b67adc4d4..000000000000 --- a/mobile/android/base/resources/drawable/awesomebar_tab_unselected.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/color/abouthome_section_more_text.xml b/mobile/android/base/resources/drawable/bookmark_folder.xml similarity index 62% rename from mobile/android/base/resources/color/abouthome_section_more_text.xml rename to mobile/android/base/resources/drawable/bookmark_folder.xml index b3ac4d7a8b63..5fa2ad3e23a2 100644 --- a/mobile/android/base/resources/color/abouthome_section_more_text.xml +++ b/mobile/android/base/resources/drawable/bookmark_folder.xml @@ -6,13 +6,11 @@ - - + + - - - - - + + diff --git a/mobile/android/base/resources/drawable/bookmark_thumbnail_bg.xml b/mobile/android/base/resources/drawable/bookmark_thumbnail_bg.xml new file mode 100644 index 000000000000..9d106441e19e --- /dev/null +++ b/mobile/android/base/resources/drawable/bookmark_thumbnail_bg.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/mobile/android/base/resources/drawable/awesomebar_listview_divider.xml b/mobile/android/base/resources/drawable/divider_horizontal.xml similarity index 91% rename from mobile/android/base/resources/drawable/awesomebar_listview_divider.xml rename to mobile/android/base/resources/drawable/divider_horizontal.xml index 0fdc386737bc..868844eb8b15 100644 --- a/mobile/android/base/resources/drawable/awesomebar_listview_divider.xml +++ b/mobile/android/base/resources/drawable/divider_horizontal.xml @@ -5,6 +5,8 @@ + - + + diff --git a/mobile/android/base/resources/drawable/home_history_tabs_indicator.xml b/mobile/android/base/resources/drawable/home_history_tabs_indicator.xml new file mode 100644 index 000000000000..d0c3e6b75dba --- /dev/null +++ b/mobile/android/base/resources/drawable/home_history_tabs_indicator.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/base/resources/drawable/home_page_title_background.xml b/mobile/android/base/resources/drawable/home_page_title_background.xml new file mode 100644 index 000000000000..1e98bd082ae8 --- /dev/null +++ b/mobile/android/base/resources/drawable/home_page_title_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/mobile/android/base/resources/drawable/address_bar_bg.xml b/mobile/android/base/resources/drawable/url_bar_bg.xml similarity index 100% rename from mobile/android/base/resources/drawable/address_bar_bg.xml rename to mobile/android/base/resources/drawable/url_bar_bg.xml diff --git a/mobile/android/base/resources/drawable/address_bar_bg_shadow_repeat.xml b/mobile/android/base/resources/drawable/url_bar_bg_shadow_repeat.xml similarity index 87% rename from mobile/android/base/resources/drawable/address_bar_bg_shadow_repeat.xml rename to mobile/android/base/resources/drawable/url_bar_bg_shadow_repeat.xml index 01394c05dcde..4aa170126cf8 100644 --- a/mobile/android/base/resources/drawable/address_bar_bg_shadow_repeat.xml +++ b/mobile/android/base/resources/drawable/url_bar_bg_shadow_repeat.xml @@ -4,6 +4,6 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> diff --git a/mobile/android/base/resources/drawable/address_bar_url.xml b/mobile/android/base/resources/drawable/url_bar_entry.xml similarity index 61% rename from mobile/android/base/resources/drawable/address_bar_url.xml rename to mobile/android/base/resources/drawable/url_bar_entry.xml index c8b9da2b3f84..4090ceb8e73f 100644 --- a/mobile/android/base/resources/drawable/address_bar_url.xml +++ b/mobile/android/base/resources/drawable/url_bar_entry.xml @@ -9,29 +9,29 @@ + android:drawable="@drawable/url_bar_entry_pressed_pb"/> + android:drawable="@drawable/url_bar_entry_pressed_pb"/> + android:drawable="@drawable/url_bar_entry_pressed_pb"/> + android:drawable="@drawable/url_bar_entry_default_pb"/> + android:drawable="@drawable/url_bar_entry_pressed"/> + android:drawable="@drawable/url_bar_entry_pressed"/> + android:drawable="@drawable/url_bar_entry_pressed"/> - + diff --git a/mobile/android/base/resources/drawable/address_bar_nav_button.xml b/mobile/android/base/resources/drawable/url_bar_nav_button.xml similarity index 100% rename from mobile/android/base/resources/drawable/address_bar_nav_button.xml rename to mobile/android/base/resources/drawable/url_bar_nav_button.xml diff --git a/mobile/android/base/resources/drawable/address_bar_right_edge.xml b/mobile/android/base/resources/drawable/url_bar_right_edge.xml similarity index 81% rename from mobile/android/base/resources/drawable/address_bar_right_edge.xml rename to mobile/android/base/resources/drawable/url_bar_right_edge.xml index 567f7bd2e5fd..379499284a09 100644 --- a/mobile/android/base/resources/drawable/address_bar_right_edge.xml +++ b/mobile/android/base/resources/drawable/url_bar_right_edge.xml @@ -4,6 +4,6 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> \ No newline at end of file diff --git a/mobile/android/base/resources/layout-large-land-v11/home_history_list.xml b/mobile/android/base/resources/layout-large-land-v11/home_history_list.xml new file mode 100644 index 000000000000..41dc59c31399 --- /dev/null +++ b/mobile/android/base/resources/layout-large-land-v11/home_history_list.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/mobile/android/base/resources/layout-large-land-v11/home_history_page.xml b/mobile/android/base/resources/layout-large-land-v11/home_history_page.xml new file mode 100644 index 000000000000..b21e2cf87aee --- /dev/null +++ b/mobile/android/base/resources/layout-large-land-v11/home_history_page.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/mobile/android/base/resources/layout-large-land-v11/home_history_tabs_indicator.xml b/mobile/android/base/resources/layout-large-land-v11/home_history_tabs_indicator.xml new file mode 100644 index 000000000000..816eb4db1aa2 --- /dev/null +++ b/mobile/android/base/resources/layout-large-land-v11/home_history_tabs_indicator.xml @@ -0,0 +1,10 @@ + + + + diff --git a/mobile/android/base/resources/layout-large-land-v11/tabs_panel_header.xml b/mobile/android/base/resources/layout-large-land-v11/tabs_panel_header.xml index 21dc6aa33b48..7da9eddee191 100644 --- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel_header.xml +++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel_header.xml @@ -9,6 +9,7 @@ android:layout_width="wrap_content" android:layout_height="fill_parent" android:tabStripEnabled="false" - android:divider="@drawable/tab_indicator_divider"/> + android:divider="@drawable/tab_indicator_divider" + android:layout="@layout/tabs_panel_indicator"/> diff --git a/mobile/android/base/resources/layout-large-v11/awesomebar_search.xml b/mobile/android/base/resources/layout-large-v11/awesomebar_search.xml deleted file mode 100644 index c7e7f39118af..000000000000 --- a/mobile/android/base/resources/layout-large-v11/awesomebar_search.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml index 02f2ebbbecc5..d796ac4af89b 100644 --- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml +++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml @@ -6,13 +6,13 @@ - + android:background="@drawable/url_bar_bg"/> - + android:background="@drawable/url_bar_entry"/> - + android:background="@drawable/url_bar_nav_button"/> - + + + + + + + + - @@ -112,7 +150,7 @@ android:orientation="horizontal"/> @@ -128,7 +166,7 @@ android:layout_alignWithParentIfMissing="true"/> diff --git a/mobile/android/base/resources/layout-large-v11/home_pager.xml b/mobile/android/base/resources/layout-large-v11/home_pager.xml new file mode 100644 index 000000000000..99656121372f --- /dev/null +++ b/mobile/android/base/resources/layout-large-v11/home_pager.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/mobile/android/base/resources/layout-xlarge-land-v11/abouthome_content.xml b/mobile/android/base/resources/layout-xlarge-land-v11/abouthome_content.xml deleted file mode 100644 index d67a60fb9d24..000000000000 --- a/mobile/android/base/resources/layout-xlarge-land-v11/abouthome_content.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout-xlarge-v11/awesomebar_search.xml b/mobile/android/base/resources/layout-xlarge-v11/awesomebar_search.xml deleted file mode 100644 index dbfd32bc4bed..000000000000 --- a/mobile/android/base/resources/layout-xlarge-v11/awesomebar_search.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout-xlarge-v11/home_history_list.xml b/mobile/android/base/resources/layout-xlarge-v11/home_history_list.xml new file mode 100644 index 000000000000..41dc59c31399 --- /dev/null +++ b/mobile/android/base/resources/layout-xlarge-v11/home_history_list.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/mobile/android/base/resources/layout-xlarge-v11/home_history_page.xml b/mobile/android/base/resources/layout-xlarge-v11/home_history_page.xml new file mode 100644 index 000000000000..b21e2cf87aee --- /dev/null +++ b/mobile/android/base/resources/layout-xlarge-v11/home_history_page.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/mobile/android/base/resources/layout-xlarge-v11/home_history_tabs_indicator.xml b/mobile/android/base/resources/layout-xlarge-v11/home_history_tabs_indicator.xml new file mode 100644 index 000000000000..816eb4db1aa2 --- /dev/null +++ b/mobile/android/base/resources/layout-xlarge-v11/home_history_tabs_indicator.xml @@ -0,0 +1,10 @@ + + + + diff --git a/mobile/android/base/resources/layout/abouthome_addon_row.xml b/mobile/android/base/resources/layout/abouthome_addon_row.xml deleted file mode 100644 index 512ec467b1f8..000000000000 --- a/mobile/android/base/resources/layout/abouthome_addon_row.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/layout/abouthome_content.xml b/mobile/android/base/resources/layout/abouthome_content.xml deleted file mode 100644 index 78eb43230c01..000000000000 --- a/mobile/android/base/resources/layout/abouthome_content.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/abouthome_last_tabs_row.xml b/mobile/android/base/resources/layout/abouthome_last_tabs_row.xml deleted file mode 100644 index 96471853c8f8..000000000000 --- a/mobile/android/base/resources/layout/abouthome_last_tabs_row.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/abouthome_remote_tab_row.xml b/mobile/android/base/resources/layout/abouthome_remote_tab_row.xml deleted file mode 100644 index accc4780a67f..000000000000 --- a/mobile/android/base/resources/layout/abouthome_remote_tab_row.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/layout/abouthome_section.xml b/mobile/android/base/resources/layout/abouthome_section.xml deleted file mode 100644 index ef75c9e5ea0a..000000000000 --- a/mobile/android/base/resources/layout/abouthome_section.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/abouthome_topsite_item.xml b/mobile/android/base/resources/layout/abouthome_topsite_item.xml deleted file mode 100644 index 69cc0c839cf1..000000000000 --- a/mobile/android/base/resources/layout/abouthome_topsite_item.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/awesomebar_folder_row.xml b/mobile/android/base/resources/layout/awesomebar_folder_row.xml deleted file mode 100644 index b8a95bcc13c5..000000000000 --- a/mobile/android/base/resources/layout/awesomebar_folder_row.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/awesomebar_header_row.xml b/mobile/android/base/resources/layout/awesomebar_header_row.xml deleted file mode 100644 index 16426e400d84..000000000000 --- a/mobile/android/base/resources/layout/awesomebar_header_row.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/layout/awesomebar_row.xml b/mobile/android/base/resources/layout/awesomebar_row.xml deleted file mode 100644 index 39e1e30ffdfe..000000000000 --- a/mobile/android/base/resources/layout/awesomebar_row.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/awesomebar_search.xml b/mobile/android/base/resources/layout/awesomebar_search.xml deleted file mode 100644 index 9a3a7e50fa98..000000000000 --- a/mobile/android/base/resources/layout/awesomebar_search.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/awesomebar_suggestion_prompt.xml b/mobile/android/base/resources/layout/awesomebar_suggestion_prompt.xml deleted file mode 100644 index 91d601f9ffae..000000000000 --- a/mobile/android/base/resources/layout/awesomebar_suggestion_prompt.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/awesomebar_tab_indicator.xml b/mobile/android/base/resources/layout/awesomebar_tab_indicator.xml deleted file mode 100644 index 07780ff683ff..000000000000 --- a/mobile/android/base/resources/layout/awesomebar_tab_indicator.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/mobile/android/base/resources/layout/awesomebar_tabs.xml b/mobile/android/base/resources/layout/awesomebar_tabs.xml deleted file mode 100644 index faf0a485702b..000000000000 --- a/mobile/android/base/resources/layout/awesomebar_tabs.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/android/base/resources/layout/bookmark_folder_row.xml b/mobile/android/base/resources/layout/bookmark_folder_row.xml new file mode 100644 index 000000000000..4a21dd0dcd9f --- /dev/null +++ b/mobile/android/base/resources/layout/bookmark_folder_row.xml @@ -0,0 +1,11 @@ + + + + diff --git a/mobile/android/base/resources/layout/bookmark_item_row.xml b/mobile/android/base/resources/layout/bookmark_item_row.xml new file mode 100644 index 000000000000..2708406090f1 --- /dev/null +++ b/mobile/android/base/resources/layout/bookmark_item_row.xml @@ -0,0 +1,10 @@ + + + + diff --git a/mobile/android/base/resources/layout/browser_search.xml b/mobile/android/base/resources/layout/browser_search.xml new file mode 100644 index 000000000000..507e88c41621 --- /dev/null +++ b/mobile/android/base/resources/layout/browser_search.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/mobile/android/base/resources/layout/browser_toolbar.xml b/mobile/android/base/resources/layout/browser_toolbar.xml index 3dd5634d1117..9dcf8a561fd5 100644 --- a/mobile/android/base/resources/layout/browser_toolbar.xml +++ b/mobile/android/base/resources/layout/browser_toolbar.xml @@ -7,18 +7,18 @@ xmlns:gecko="http://schemas.android.com/apk/res-auto"> + style="@style/UrlBar.ImageButton.Back"/> + style="@style/UrlBar.ImageButton.Forward"/> - + android:background="@drawable/url_bar_bg"/> - - + style="@style/UrlBar.ImageButton.Unused"/> - + + + + + + + + - @@ -138,7 +175,7 @@ android:orientation="horizontal"/> @@ -149,7 +186,7 @@ android:layout_width="fill_parent" android:layout_height="2dp" android:layout_alignParentBottom="true" - android:background="@drawable/address_bar_bg_shadow_repeat" + android:background="@drawable/url_bar_bg_shadow_repeat" android:contentDescription="@null" android:visibility="gone"/> diff --git a/mobile/android/base/resources/layout/find_in_page_content.xml b/mobile/android/base/resources/layout/find_in_page_content.xml index ec84b582fcb9..3a1b52169799 100644 --- a/mobile/android/base/resources/layout/find_in_page_content.xml +++ b/mobile/android/base/resources/layout/find_in_page_content.xml @@ -9,7 +9,7 @@ android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:contentDescription="@string/find_text" - android:background="@drawable/address_bar_url" + android:background="@drawable/url_bar_entry" android:singleLine="true" android:textColor="#000000" android:textCursorDrawable="@null" diff --git a/mobile/android/base/resources/layout/gecko_app.xml b/mobile/android/base/resources/layout/gecko_app.xml index f8bf07a7d38a..1a7f32a9f7e4 100644 --- a/mobile/android/base/resources/layout/gecko_app.xml +++ b/mobile/android/base/resources/layout/gecko_app.xml @@ -4,6 +4,7 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> @@ -26,6 +27,14 @@ + + + + + + + + + + + diff --git a/mobile/android/base/resources/layout/home_empty_page.xml b/mobile/android/base/resources/layout/home_empty_page.xml new file mode 100644 index 000000000000..438e84c45d17 --- /dev/null +++ b/mobile/android/base/resources/layout/home_empty_page.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/mobile/android/base/resources/layout/home_empty_reading_page.xml b/mobile/android/base/resources/layout/home_empty_reading_page.xml new file mode 100644 index 000000000000..8ef395798e56 --- /dev/null +++ b/mobile/android/base/resources/layout/home_empty_reading_page.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/base/resources/layout/home_header_row.xml b/mobile/android/base/resources/layout/home_header_row.xml new file mode 100644 index 000000000000..f3ca9322b2df --- /dev/null +++ b/mobile/android/base/resources/layout/home_header_row.xml @@ -0,0 +1,7 @@ + + + + diff --git a/mobile/android/base/resources/layout/home_history_list.xml b/mobile/android/base/resources/layout/home_history_list.xml new file mode 100644 index 000000000000..17bf72e9972c --- /dev/null +++ b/mobile/android/base/resources/layout/home_history_list.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/mobile/android/base/resources/layout/home_history_page.xml b/mobile/android/base/resources/layout/home_history_page.xml new file mode 100644 index 000000000000..70e0589ad9f6 --- /dev/null +++ b/mobile/android/base/resources/layout/home_history_page.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/mobile/android/base/resources/layout/home_history_tabs_indicator.xml b/mobile/android/base/resources/layout/home_history_tabs_indicator.xml new file mode 100644 index 000000000000..e6d68422886b --- /dev/null +++ b/mobile/android/base/resources/layout/home_history_tabs_indicator.xml @@ -0,0 +1,10 @@ + + + + diff --git a/mobile/android/base/resources/layout/home_item_row.xml b/mobile/android/base/resources/layout/home_item_row.xml new file mode 100644 index 000000000000..97894cbc82e8 --- /dev/null +++ b/mobile/android/base/resources/layout/home_item_row.xml @@ -0,0 +1,9 @@ + + + + diff --git a/mobile/android/base/resources/layout/home_last_tabs_page.xml b/mobile/android/base/resources/layout/home_last_tabs_page.xml new file mode 100644 index 000000000000..8cc06427a87c --- /dev/null +++ b/mobile/android/base/resources/layout/home_last_tabs_page.xml @@ -0,0 +1,25 @@ + + + + + + + + + +