Merge latest green fx-team changeset and mozilla-central

This commit is contained in:
Ed Morley 2013-08-21 12:56:05 +01:00
Родитель 15fa4e65e7 dca467e0ab
Коммит d58a607c99
313 изменённых файлов: 9607 добавлений и 8396 удалений

Просмотреть файл

@ -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
*/

Просмотреть файл

@ -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);

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -230,11 +230,6 @@
</intent-filter>
</activity>
<activity android:name="org.mozilla.gecko.AwesomeBar"
android:theme="@style/Gecko.AwesomeBar"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="stateUnspecified|adjustResize"/>
<activity android:name="org.mozilla.gecko.GeckoPreferences"
android:theme="@style/Gecko.Preferences"
android:label="@string/settings_title"

Просмотреть файл

@ -17,8 +17,12 @@ public class AnimatedHeightLayout extends RelativeLayout {
private static final int ANIMATION_DURATION = 100;
private boolean mAnimating = false;
public AnimatedHeightLayout(Context context) {
super(context, null);
}
public AnimatedHeightLayout(Context context, AttributeSet attrs) {
super(context, attrs);
super(context, attrs, 0);
}
public AnimatedHeightLayout(Context context, AttributeSet attrs, int defStyle) {

Просмотреть файл

@ -0,0 +1,10 @@
/* -*- 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;
public interface AutocompleteHandler {
void onAutocomplete(String res);
}

Просмотреть файл

@ -1,730 +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.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.health.BrowserHealthRecorder;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UiAsyncTask;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
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.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MenuInflater;
import android.view.View;
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.TabWidget;
import android.widget.Toast;
import org.json.JSONObject;
import java.net.URLEncoder;
interface AutocompleteHandler {
void onAutocomplete(String res);
}
public class AwesomeBar extends GeckoActivity
implements AutocompleteHandler,
TextWatcher {
private static final String LOGTAG = "GeckoAwesomeBar";
public static final String URL_KEY = "url";
public static final String TAB_KEY = "tab";
public static final String CURRENT_URL_KEY = "currenturl";
public static final String TARGET_KEY = "target";
public static final String SEARCH_KEY = "search";
public static final String TITLE_KEY = "title";
public static final String USER_ENTERED_KEY = "user_entered";
public static final String READING_LIST_KEY = "reading_list";
public static enum Target { NEW_TAB, CURRENT_TAB, PICK_SITE };
private String mTarget;
private AwesomeBarTabs mAwesomeTabs;
private CustomEditText mText;
private ImageButton mGoButton;
private ContextMenuSubject mContextMenuSubject;
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;
@Override
public void onCreate(Bundle savedInstanceState) {
LayoutInflater.from(this).setFactory(this);
super.onCreate(savedInstanceState);
Log.d(LOGTAG, "creating awesomebar");
setContentView(R.layout.awesomebar);
mGoButton = (ImageButton) findViewById(R.id.awesomebar_button);
mText = (CustomEditText) findViewById(R.id.awesomebar_text);
TabWidget tabWidget = (TabWidget) findViewById(android.R.id.tabs);
tabWidget.setDividerDrawable(null);
mAwesomeTabs = (AwesomeBarTabs) findViewById(R.id.awesomebar_tabs);
mAwesomeTabs.setOnUrlOpenListener(new AwesomeBarTabs.OnUrlOpenListener() {
@Override
public void onUrlOpen(String url, String title) {
openUrlAndFinish(url, title, false);
}
@Override
public void onSearch(SearchEngine engine, String text) {
Intent resultIntent = new Intent();
resultIntent.putExtra(URL_KEY, text);
resultIntent.putExtra(TARGET_KEY, mTarget);
resultIntent.putExtra(SEARCH_KEY, engine.name);
recordSearch(engine.identifier, "barsuggest");
finishWithResult(resultIntent);
}
@Override
public void onEditSuggestion(final String text) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
mText.setText(text);
mText.setSelection(mText.getText().length());
mText.requestFocus();
}
});
}
@Override
public void onSwitchToTab(final int tabId) {
Intent resultIntent = new Intent();
resultIntent.putExtra(TAB_KEY, Integer.toString(tabId));
finishWithResult(resultIntent);
}
});
mGoButton.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
openUserEnteredAndFinish(mText.getText().toString());
}
});
Intent intent = getIntent();
String currentUrl = intent.getStringExtra(CURRENT_URL_KEY);
if (currentUrl != null) {
mText.setText(currentUrl);
mText.selectAll();
}
mTarget = intent.getStringExtra(TARGET_KEY);
if (mTarget.equals(Target.CURRENT_TAB.name())) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null && tab.isPrivate()) {
BrowserToolbarBackground mAddressBarBg = (BrowserToolbarBackground) findViewById(R.id.address_bar_bg);
mAddressBarBg.setPrivateMode(true);
ShapedButton mTabs = (ShapedButton) findViewById(R.id.dummy_tab);
if (mTabs != null)
mTabs.setPrivateMode(true);
mText.setPrivateMode(true);
}
}
mAwesomeTabs.setTarget(mTarget);
mText.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 AwesomeBar has a composition string, don't submit the text yet.
// ENTER is needed to commit the composition string.
Editable content = mText.getText();
if (!hasCompositionString(content)) {
openUserEnteredAndFinish(content.toString());
return true;
}
}
// If input method is in fullscreen mode, we want to dismiss
// it instead of closing awesomebar straight away.
InputMethodManager imm =
(InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (keyCode == KeyEvent.KEYCODE_BACK && !imm.isFullscreenMode()) {
return handleBackKey();
}
return false;
}
});
mText.addTextChangedListener(this);
mText.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
if (event.getAction() != KeyEvent.ACTION_DOWN)
return true;
openUserEnteredAndFinish(mText.getText().toString());
return true;
} else if (GamepadUtils.isBackKey(event)) {
return handleBackKey();
} else {
return false;
}
}
});
mText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (v == null || hasFocus) {
return;
}
InputMethodManager imm = (InputMethodManager)
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);
}
}
});
boolean showReadingList = intent.getBooleanExtra(READING_LIST_KEY, false);
if (showReadingList) {
BookmarksTab bookmarksTab = mAwesomeTabs.getBookmarksTab();
bookmarksTab.setShowReadingList(true);
mAwesomeTabs.setCurrentItemByTag(bookmarksTab.getTag());
}
}
private boolean handleBackKey() {
// Let mAwesomeTabs try to handle the back press, since we may be in a
// bookmarks sub-folder.
if (mAwesomeTabs.onBackPressed())
return true;
// If mAwesomeTabs.onBackPressed() returned false, we didn't move up
// a folder level, so just exit the activity.
cancelAndFinish();
return true;
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
}
@Override
public boolean onSearchRequested() {
cancelAndFinish();
return true;
}
private void updateGoButton(String text) {
if (text.length() == 0) {
mGoButton.setVisibility(View.GONE);
return;
}
mGoButton.setVisibility(View.VISIBLE);
int imageResource = R.drawable.ic_awesomebar_go;
String contentDescription = getString(R.string.go);
int imeAction = EditorInfo.IME_ACTION_GO;
int actionBits = mText.getImeOptions() & EditorInfo.IME_MASK_ACTION;
if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) {
imageResource = R.drawable.ic_awesomebar_search;
contentDescription = getString(R.string.search);
imeAction = EditorInfo.IME_ACTION_SEARCH;
}
InputMethodManager imm = InputMethods.getInputMethodManager(mText.getContext());
if (imm == null) {
return;
}
boolean restartInput = false;
if (actionBits != imeAction) {
int optionBits = mText.getImeOptions() & ~EditorInfo.IME_MASK_ACTION;
mText.setImeOptions(optionBits | imeAction);
mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) &&
(InputMethods.shouldDelayAwesomebarUpdate(mText.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(mText);
mGoButton.setImageResource(imageResource);
mGoButton.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 = mText.getText().toString();
int currentInputType = mText.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) {
mText.setRawInputType(newInputType);
}
}
private void cancelAndFinish() {
setResult(Activity.RESULT_CANCELED);
finish();
overridePendingTransition(R.anim.awesomebar_hold_still, R.anim.awesomebar_fade_out);
}
private void finishWithResult(Intent intent) {
setResult(Activity.RESULT_OK, intent);
finish();
overridePendingTransition(R.anim.awesomebar_hold_still, R.anim.awesomebar_fade_out);
}
private void openUrlAndFinish(String url) {
openUrlAndFinish(url, null, false);
}
private void openUrlAndFinish(String url, String title, boolean userEntered) {
Intent resultIntent = new Intent();
resultIntent.putExtra(URL_KEY, url);
if (title != null && !TextUtils.isEmpty(title))
resultIntent.putExtra(TITLE_KEY, title);
if (userEntered)
resultIntent.putExtra(USER_ENTERED_KEY, userEntered);
resultIntent.putExtra(TARGET_KEY, mTarget);
finishWithResult(resultIntent);
}
/**
* Record in Health Report that a search has occurred.
*
* @param identifier
* a search identifier, such as "partnername". Can be null.
* @param where
* where the search was initialized; one of the values in
* {@link BrowserHealthRecorder#SEARCH_LOCATIONS}.
*/
private static void recordSearch(String identifier, String where) {
Log.i(LOGTAG, "Recording search: " + identifier + ", " + where);
try {
JSONObject message = new JSONObject();
message.put("type", BrowserHealthRecorder.EVENT_SEARCH);
message.put("location", where);
message.put("identifier", identifier);
GeckoAppShell.getEventDispatcher().dispatchEvent(message);
} catch (Exception e) {
Log.w(LOGTAG, "Error recording search.", e);
}
}
private void openUserEnteredAndFinish(final String url) {
final int index = url.indexOf(' ');
// Check for a keyword if the URL looks like a search query
if (!StringUtils.isSearchQuery(url, true)) {
openUrlAndFinish(url, "", true);
return;
}
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final String keyword;
final String keywordSearch;
if (index == -1) {
keyword = url;
keywordSearch = "";
} else {
keyword = url.substring(0, index);
keywordSearch = url.substring(index + 1);
}
final String keywordUrl = BrowserDB.getUrlForKeyword(getContentResolver(), keyword);
final String searchUrl = (keywordUrl != null)
? keywordUrl.replace("%s", URLEncoder.encode(keywordSearch))
: url;
if (keywordUrl != null) {
recordSearch(null, "barkeyword");
}
openUrlAndFinish(searchUrl, "", true);
}
});
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 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 ||
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<Void, Void, Integer>(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<Void, Void, Void>(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
}
}

Просмотреть файл

@ -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]);
}
}
}

Просмотреть файл

@ -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);
}
}
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<OnUrlOpenListener.Flags> 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<OnUrlOpenListener.Flags> 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

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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));
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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 <code>ACTION_SEND</code> 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);

Просмотреть файл

@ -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);

Просмотреть файл

@ -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 \

Просмотреть файл

@ -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);

Просмотреть файл

@ -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);

Просмотреть файл

@ -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();

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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<ElementHolder> mElementsList;
private PropertyAnimationListener mListener;
private List<PropertyAnimationListener> mListeners;
private FramePoster mFramePoster;
private boolean mUseHardwareLayer;
@ -64,6 +65,7 @@ public class PropertyAnimator implements Runnable {
mElementsList = new ArrayList<ElementHolder>();
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<PropertyAnimationListener>();
}
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;
}
}

Просмотреть файл

@ -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<SearchEngine> mSearchEngines;
private SuggestClient mSuggestClient;
private boolean mSuggestionsEnabled;
private AsyncTask<String, Void, ArrayList<String>> 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<SearchEngine>();
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<String> 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<String, Void, ArrayList<String>>() {
@Override
protected ArrayList<String> doInBackground(String... query) {
return mSuggestClient.query(query[0]);
}
@Override
protected void onPostExecute(ArrayList<String> 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<String> 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<SearchEngine> searchEngines = new ArrayList<SearchEngine>();
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<String> getUrlsWithoutFavicon() {
List<String> urls = new ArrayList<String>();
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<String> urls = getUrlsWithoutFavicon();
if (urls.size() == 0)
return;
(new UiAsyncTask<Void, Void, Void>(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;
}
}
}
}

Просмотреть файл

@ -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<String, Integer> 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<String, Integer> getOpenTabs() {
if (sOpenTabs == null || sOpenTabs.isEmpty()) {
Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
sOpenTabs = new HashMap<String, Integer>();
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);
}
}

Просмотреть файл

@ -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<Pair<Integer, String>> 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<Pair<Integer, String>>();
// Add the root folder to the stack
Pair<Integer, String> rootFolder = new Pair<Integer, String>(Bookmarks.FIXED_ROOT_ID, "");
mParentStack.addFirst(rootFolder);
}
public void refreshCurrentFolder() {
// Cancel any pre-existing async refresh tasks
if (mQueryTask != null)
mQueryTask.cancel(false);
Pair<Integer, String> 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<Integer, String> folderPair = new Pair<Integer, String>(folderId, folderTitle);
mParentStack.addFirst(folderPair);
refreshCurrentFolder();
}
public boolean isInReadingList() {
Pair<Integer, String> 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<Void, Void, Cursor> {
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;
}
}

Просмотреть файл

@ -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<? extends Map<String, ?>> groupData,
int groupLayout, String[] groupFrom, int[] groupTo,
List<? extends List<? extends Map<String, ?>>> 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<String,Object> historyItem =
(Map<String,Object>) 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<Map<String,String>> {
private static final long serialVersionUID = 0L;
}
private static class ChildrenList extends LinkedList<Map<String,Object>> {
private static final long serialVersionUID = 0L;
}
private class HistoryQueryTask extends AsyncTask<Void, Void, Pair<GroupList,List<ChildrenList>>> {
private static final long MS_PER_DAY = 86400000;
private static final long MS_PER_WEEK = MS_PER_DAY * 7;
@Override
protected Pair<GroupList,List<ChildrenList>> 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<ChildrenList> childrenLists = new LinkedList<ChildrenList>();
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.<GroupList,List<ChildrenList>>create(groups, childrenLists);
}
public Map<String,Object> createHistoryItem(Cursor cursor) {
Map<String,Object> historyItem = new HashMap<String,Object>();
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<String,String> createGroupItem(HistorySection section) {
Map<String,String> groupItem = new HashMap<String,String>();
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<GroupList,List<ChildrenList>> 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<String,Object> historyItem = (Map<String,Object>) 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;
}
}

Просмотреть файл

@ -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<String> 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<String> urls) {
return sDb.getFaviconsForUrls(cr, urls);
}

Просмотреть файл

@ -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<String, Long>();
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<String> 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 "";
}

Просмотреть файл

@ -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();

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<Pair<Integer, String>> 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<Pair<Integer, String>>();
// Add the root folder to the stack
Pair<Integer, String> rootFolder = new Pair<Integer, String>(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<Integer, String> folderPair = new Pair<Integer, String>(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();
}
}
}
}

Просмотреть файл

@ -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));
}
}
}

Просмотреть файл

@ -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<Cursor> 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<Cursor> 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<String> urls = new ArrayList<String>();
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<Cursor> 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<Map<String, Thumbnail>> {
private Map<String, Thumbnail> mThumbnails;
private ArrayList<String> mUrls;
public ThumbnailsLoader(Context context, ArrayList<String> urls) {
super(context);
mUrls = urls;
}
@Override
public Map<String, Thumbnail> loadInBackground() {
if (mUrls == null || mUrls.size() == 0) {
return null;
}
final Map<String, Thumbnail> thumbnails = new HashMap<String, Thumbnail>();
// 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<String, Thumbnail> 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<String, Thumbnail> 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<Map<String, Thumbnail>> {
@Override
public Loader<Map<String, Thumbnail>> onCreateLoader(int id, Bundle args) {
return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY));
}
@Override
public void onLoadFinished(Loader<Map<String, Thumbnail>> loader, Map<String, Thumbnail> thumbnails) {
if (mTopBookmarksAdapter != null) {
mTopBookmarksAdapter.updateThumbnails(thumbnails);
}
}
@Override
public void onLoaderReset(Loader<Map<String, Thumbnail>> loader) {
if (mTopBookmarksAdapter != null) {
mTopBookmarksAdapter.updateThumbnails(null);
}
}
}
}

Просмотреть файл

@ -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<SearchEngine> 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<SearchEngine>();
}
@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<String> 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<String> 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<SearchEngine> searchEngines = new ArrayList<SearchEngine>();
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<ArrayList<String>> {
private final SuggestClient mSuggestClient;
private final String mSearchTerm;
private ArrayList<String> mSuggestions;
public SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) {
super(context);
mSuggestClient = suggestClient;
mSearchTerm = searchTerm;
mSuggestions = null;
}
@Override
public ArrayList<String> loadInBackground() {
return mSuggestClient.query(mSearchTerm);
}
@Override
public void deliverResult(ArrayList<String> 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<Cursor> 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<Cursor> 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<Cursor> 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<ArrayList<String>> {
@Override
public Loader<ArrayList<String>> 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<ArrayList<String>> loader, ArrayList<String> suggestions) {
setSuggestions(suggestions);
}
@Override
public void onLoaderReset(Loader<ArrayList<String>> loader) {
setSuggestions(new ArrayList<String>());
}
}
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;
}
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<String> getUrlsWithoutFavicon(Cursor c) {
ArrayList<String> urls = new ArrayList<String>();
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<Cursor> callbacks, Cursor c) {
// If there urls without in-memory favicons, trigger a new loader
// to load the images from disk to memory.
ArrayList<String> 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<Cursor> createInstance(Context context, Bundle args) {
final ArrayList<String> urls = args.getStringArrayList(FAVICONS_LOADER_URLS_ARG);
return new FaviconsCursorLoader(context, urls);
}
private static class FaviconsCursorLoader extends SimpleCursorLoader {
private final ArrayList<String> mUrls;
public FaviconsCursorLoader(Context context, ArrayList<String> urls) {
super(context);
mUrls = urls;
}
@Override
public Cursor loadCursor() {
final ContentResolver cr = getContext().getContentResolver();
Cursor c = BrowserDB.getFaviconsForUrls(cr, mUrls);
storeFaviconsInMemCache(c);
return c;
}
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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> {
// 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<Cursor> onCreateLoader(int id, Bundle args) {
if (id == LOADER_ID_FAVICONS) {
return FaviconsLoader.createInstance(mContext, args);
}
return null;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
if (loader.getId() == LOADER_ID_FAVICONS) {
onFaviconsLoaded();
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
// Do nothing by default.
}
// Callback for favicons loaded in memory.
public abstract void onFaviconsLoaded();
}

Просмотреть файл

@ -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<Void, Void, Void> {
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<Void, Void, Void> {
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();
}
}
}

Просмотреть файл

@ -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;
}
}
}
}

Просмотреть файл

@ -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<Page, Fragment> mPages = new EnumMap<Page, Fragment>(Page.class);
public interface OnUrlOpenListener {
public enum Flags {
ALLOW_SWITCH_TO_TAB
}
public void onUrlOpen(String url, EnumSet<Flags> 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<TabInfo> mTabs = new ArrayList<TabInfo>();
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);
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<Cursor> 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<Cursor> 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<Cursor> loader) {
if (loader.getId() == LOADER_ID_LAST_TABS) {
mAdapter.swapCursor(null);
} else {
super.onLoaderReset(loader);
}
}
@Override
public void onFaviconsLoaded() {
mAdapter.notifyDataSetChanged();
}
}
}

Просмотреть файл

@ -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<MostRecentSection> mMostRecentSections;
public MostRecentAdapter(Context context) {
super(context, null, VIEW_TYPES, LAYOUT_TYPES);
mContext = context;
// Initialize map of history sections
mMostRecentSections = new SparseArray<MostRecentSection>();
}
@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<Cursor> 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<Cursor> 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<Cursor> loader) {
if (loader.getId() == LOADER_ID_HISTORY) {
mAdapter.swapCursor(null);
} else {
super.onLoaderReset(loader);
}
}
@Override
public void onFaviconsLoaded() {
mAdapter.notifyDataSetChanged();
}
}
}

Просмотреть файл

@ -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<Cursor> 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<Cursor> 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<Cursor> loader) {
if (loader.getId() == LOADER_ID_FRECENCY) {
mAdapter.swapCursor(null);
} else {
super.onLoaderReset(loader);
}
}
@Override
public void onFaviconsLoaded() {
mAdapter.notifyDataSetChanged();
}
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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<Cursor> 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<Cursor> loader, Cursor c) {
if (loader.getId() == LOADER_ID_SEARCH) {
mAdapter.swapCursor(c);
loadFavicons(c);
} else {
super.onLoadFinished(loader, c);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (loader.getId() == LOADER_ID_SEARCH) {
mAdapter.swapCursor(null);
} else {
super.onLoaderReset(loader);
}
}
@Override
public void onFaviconsLoaded() {
mAdapter.notifyDataSetChanged();
}
}
}

Просмотреть файл

@ -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<Cursor> {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch(id) {
case LOADER_ID_READING_LIST:
return new ReadingListLoader(getActivity());
}
return null;
}
@Override
public void onLoadFinished(Loader<Cursor> 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<Cursor> loader) {
final int loaderId = loader.getId();
switch(loaderId) {
case LOADER_ID_READING_LIST:
mAdapter.swapCursor(null);
break;
}
}
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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);

Просмотреть файл

@ -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<Cursor> 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<Cursor> callbacks, String searchTerm) {
restart(manager, loaderId, callbacks, searchTerm, true);
}
public static void restart(LoaderManager manager, int loaderId,
LoaderCallbacks<Cursor> 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;
}
}
}

Просмотреть файл

@ -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<Cursor> {
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;
}
}

Просмотреть файл

@ -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();

Просмотреть файл

@ -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);
}
}
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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<String, Thumbnail> 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<String, Thumbnail> 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);
}
}

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -6,10 +6,13 @@
<!ENTITY no_space_to_start_error "There is not enough space available for &brandShortName; to start.">
<!ENTITY error_loading_file "An error occurred when trying to load files required to run &brandShortName;">
<!ENTITY awesomebar_all_pages_title "Top Sites">
<!ENTITY awesomebar_bookmarks_title "Bookmarks">
<!ENTITY awesomebar_history_title "History">
<!ENTITY awesomebar_switch_to_tab "Switch to tab">
<!-- Localization note: These are used as the titles of different pages on the home screen.
They are automatically converted to all caps by the Android platform. -->
<!ENTITY bookmarks_title "Bookmarks">
<!ENTITY history_title "History">
<!ENTITY reading_list_title "Reading List">
<!ENTITY switch_to_tab "Switch to tab">
<!ENTITY crash_reporter_title "&brandShortName; Crash Reporter">
<!ENTITY crash_message2 "&brandShortName; had a problem and crashed. Your tabs should be listed on the &brandShortName; Start page when you restart.">
@ -30,10 +33,7 @@
<!ENTITY choose_file "Choose File">
<!ENTITY awesomebar_default_text "Enter Search or Address">
<!ENTITY remote_tabs "Synced Tabs">
<!ENTITY remote_tabs_show_all "Show all tabs">
<!ENTITY url_bar_default_text "Enter Search or Address">
<!ENTITY bookmark "Bookmark">
<!ENTITY bookmark_added "Bookmark added">
@ -217,8 +217,7 @@ size. -->
<!ENTITY contextmenu_open_new_tab "Open in New Tab">
<!ENTITY contextmenu_open_private_tab "Open in Private Tab">
<!ENTITY contextmenu_open_in_reader "Open in Reader">
<!ENTITY contextmenu_remove_history "Remove">
<!ENTITY contextmenu_remove_bookmark "Remove">
<!ENTITY contextmenu_remove "Remove">
<!ENTITY contextmenu_add_to_launcher "Add to Home Screen">
<!ENTITY contextmenu_share "Share">
<!ENTITY contextmenu_pasteandgo "Paste &amp; Go">
@ -227,6 +226,9 @@ size. -->
<!ENTITY contextmenu_edit_bookmark "Edit">
<!ENTITY contextmenu_subscribe "Subscribe to Page">
<!ENTITY contextmenu_site_settings "Edit Site Settings">
<!ENTITY contextmenu_top_bookmarks_edit "Edit">
<!ENTITY contextmenu_top_bookmarks_pin "Pin Site">
<!ENTITY contextmenu_top_bookmarks_unpin "Unpin Site">
<!ENTITY pref_titlebar_mode "Title bar">
<!ENTITY pref_titlebar_mode_title "Show page title">
@ -234,6 +236,7 @@ size. -->
<!ENTITY history_removed "Page removed">
<!ENTITY bookmark_add "Add a bookmark">
<!ENTITY bookmark_edit_title "Edit Bookmark">
<!ENTITY bookmark_edit_name "Name">
<!ENTITY bookmark_edit_location "Location">
@ -246,7 +249,6 @@ size. -->
<!ENTITY site_settings_clear "Clear">
<!ENTITY site_settings_no_settings "There are no settings to clear.">
<!ENTITY reading_list "Reading List">
<!ENTITY reading_list_added "Page added to your Reading List">
<!ENTITY reading_list_removed "Page removed from your Reading List">
<!ENTITY reading_list_failed "Failed to add page to your Reading List">
@ -269,28 +271,22 @@ size. -->
<!ENTITY button_set "Set">
<!ENTITY button_clear "Clear">
<!ENTITY abouthome_addons_title "Add-ons for your &brandShortName;">
<!ENTITY abouthome_addons_browse "Browse all &brandShortName; add-ons">
<!ENTITY abouthome_last_tabs_title "Your tabs from last time">
<!ENTITY abouthome_last_tabs_open "Open all tabs from last time">
<!ENTITY abouthome_top_sites_title "Top sites">
<!ENTITY abouthome_topsites_edit "Edit">
<!ENTITY abouthome_topsites_pin "Pin Site">
<!ENTITY abouthome_topsites_unpin "Unpin Site">
<!ENTITY home_history_title "History">
<!ENTITY home_last_tabs_title "Tabs from last time">
<!ENTITY home_last_tabs_open "Open all tabs from last time">
<!ENTITY home_last_tabs_empty "Your recent tabs show up here.">
<!ENTITY home_most_recent_title "Most recent">
<!ENTITY home_most_recent_empty "Websites you visited most recently show up here.">
<!ENTITY home_most_visited_title "Most visited">
<!ENTITY home_reading_list_empty "Articles you save for later show up here.">
<!-- Localization note (home_reading_list_hint): The "TIP" string is synonymous to "hint", "clue", etc. This string is displayed
as an advisory message on how to add content to the reading list when the reading list empty.
The "&#37;I" in the string will be replaced by a small image of the icon described, and can be moved to wherever
it is applicable.. -->
<!ENTITY home_reading_list_hint "TIP: Save articles to your reading list by long pressing the &#37;I icon when it appears in the title bar.">
<!-- Localization note (abouthome_about_sync3, abouthome_about_apps2): The
chevron (ex: "»"; unicode= U+00BB) is used as an arrow to show that
clicking this text in the promotions box will perform some action. Note
that a non-breaking space (unicode= U+00A0) should be used between this
character and the remainder of the string to prevent word wrap. -->
<!ENTITY abouthome_about_sync3 "Set up Firefox Sync to access bookmarks, history and tabs from your other devices&#x00A0;»">
<!ENTITY abouthome_about_apps3 "Get apps from the Firefox Marketplace and discover the best the Web has to offer&#x00A0;»">
<!-- Localization note (abouthome_sync_bold_name, abouthome_apps_bold_name):
These strings are accentuated as bold text in the "abouthome_about_..."
strings above. These strings should be a subset of the strings above and
generally be the name of the product the string describes. -->
<!ENTITY abouthome_sync_bold_name "Firefox Sync">
<!ENTITY abouthome_apps_bold_name2 "Firefox Marketplace">
<!ENTITY home_most_visited_empty "Websites you visit most frequently show up here.">
<!ENTITY pin_bookmark_dialog_hint "Enter a search keyword">
<!ENTITY filepicker_title "Choose File">
<!ENTITY filepicker_audio_title "Choose or record a sound">

Просмотреть файл

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="0.0"
android:toAlpha="1.0"
android:duration="100"/>
</set>

Просмотреть файл

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="200"/>
</set>

Просмотреть файл

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto">
<!-- dark theme -->
<item gecko:state_dark="true" android:color="#CCFFFFFF" />
<!-- light theme -->
<item gecko:state_light="true" android:color="#CC222222" />
<!-- default -->
<item android:color="@color/text_color_secondary"/>
</selector>

Просмотреть файл

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto">
<!-- dark theme -->
<item gecko:state_dark="true" android:color="@color/text_color_primary_inverse" />
<!-- light theme -->
<item gecko:state_light="true" android:color="@color/text_color_primary" />
<!-- default -->
<item android:color="@color/text_color_primary"/>
</selector>

Просмотреть файл

@ -4,9 +4,11 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true"
android:drawable="@color/awesomebar_header_row_focused"/>
<item android:drawable="@color/awesomebar_header_row"/>
<!-- empty with no url -->
<item android:state_empty="true" android:color="#80777777" />
<!-- default -->
<item android:color="#FF777777"/>
</selector>

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 55 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.2 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 957 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 932 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 11 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 25 KiB

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 249 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 264 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 506 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.7 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 89 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 3.1 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 372 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 398 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 297 B

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 291 B

После

Ширина:  |  Высота:  |  Размер: 291 B

Двоичные данные
mobile/android/base/resources/drawable-hdpi/ic_url_bar_reader.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 317 B

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 497 B

После

Ширина:  |  Высота:  |  Размер: 497 B

Двоичные данные
mobile/android/base/resources/drawable-hdpi/ic_url_bar_star.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 306 B

Двоичные данные
mobile/android/base/resources/drawable-hdpi/ic_url_bar_tab.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 221 B

Двоичные данные
mobile/android/base/resources/drawable-hdpi/icon_last_tabs.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 446 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.3 KiB

Двоичные данные
mobile/android/base/resources/drawable-hdpi/icon_most_recent.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 841 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.3 KiB

Двоичные данные
mobile/android/base/resources/drawable-hdpi/icon_most_visited.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 421 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.3 KiB

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше