mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-07 16:28:38 +09:00
pt 1 in reviving the android build (copied from pm 28a1, wish me luck)
This commit is contained in:
parent
efa9662725
commit
d7788a6d6d
4249 changed files with 468189 additions and 0 deletions
|
|
@ -0,0 +1,147 @@
|
|||
/* -*- 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.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
import org.mozilla.gecko.util.UIAsyncTask;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class BookmarkFolderView extends LinearLayout {
|
||||
private static final Set<Integer> FOLDERS_WITH_COUNT;
|
||||
|
||||
static {
|
||||
final Set<Integer> folders = new TreeSet<>();
|
||||
folders.add(BrowserContract.Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID);
|
||||
|
||||
FOLDERS_WITH_COUNT = Collections.unmodifiableSet(folders);
|
||||
}
|
||||
|
||||
public enum FolderState {
|
||||
/**
|
||||
* A standard folder, i.e. a folder in a list of bookmarks and folders.
|
||||
*/
|
||||
FOLDER(R.drawable.folder_closed),
|
||||
|
||||
/**
|
||||
* The parent folder: this indicates that you are able to return to the previous
|
||||
* folder ("Back to {name}").
|
||||
*/
|
||||
PARENT(R.drawable.bookmark_folder_arrow_up),
|
||||
|
||||
/**
|
||||
* The reading list smartfolder: this displays a reading list icon instead of the
|
||||
* normal folder icon.
|
||||
*/
|
||||
READING_LIST(R.drawable.reading_list_folder);
|
||||
|
||||
public final int image;
|
||||
|
||||
FolderState(final int image) { this.image = image; }
|
||||
}
|
||||
|
||||
private final TextView mTitle;
|
||||
private final TextView mSubtitle;
|
||||
|
||||
private final ImageView mIcon;
|
||||
|
||||
public BookmarkFolderView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public BookmarkFolderView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.two_line_folder_row, this);
|
||||
|
||||
mTitle = (TextView) findViewById(R.id.title);
|
||||
mSubtitle = (TextView) findViewById(R.id.subtitle);
|
||||
mIcon = (ImageView) findViewById(R.id.icon);
|
||||
}
|
||||
|
||||
public void update(String title, int folderID) {
|
||||
setTitle(title);
|
||||
setID(folderID);
|
||||
}
|
||||
|
||||
private void setTitle(String title) {
|
||||
mTitle.setText(title);
|
||||
}
|
||||
|
||||
private static class ItemCountUpdateTask extends UIAsyncTask.WithoutParams<Integer> {
|
||||
private final WeakReference<TextView> mTextViewReference;
|
||||
private final int mFolderID;
|
||||
|
||||
public ItemCountUpdateTask(final WeakReference<TextView> textViewReference,
|
||||
final int folderID) {
|
||||
super(ThreadUtils.getBackgroundHandler());
|
||||
|
||||
mTextViewReference = textViewReference;
|
||||
mFolderID = folderID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground() {
|
||||
final TextView textView = mTextViewReference.get();
|
||||
|
||||
if (textView == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final BrowserDB db = BrowserDB.from(textView.getContext());
|
||||
return db.getBookmarkCountForFolder(textView.getContext().getContentResolver(), mFolderID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer count) {
|
||||
final TextView textView = mTextViewReference.get();
|
||||
|
||||
if (textView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String text;
|
||||
if (count == 1) {
|
||||
text = textView.getContext().getResources().getString(R.string.bookmark_folder_one_item);
|
||||
} else {
|
||||
text = textView.getContext().getResources().getString(R.string.bookmark_folder_items, count);
|
||||
}
|
||||
|
||||
textView.setText(text);
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setID(final int folderID) {
|
||||
if (FOLDERS_WITH_COUNT.contains(folderID)) {
|
||||
final WeakReference<TextView> subTitleReference = new WeakReference<TextView>(mSubtitle);
|
||||
|
||||
new ItemCountUpdateTask(subTitleReference, folderID).execute();
|
||||
} else {
|
||||
mSubtitle.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setState(@NonNull FolderState state) {
|
||||
mIcon.setImageResource(state.image);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/* 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.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserContract.UrlAnnotations;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.FieldPosition;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An entry of the screenshot list in the bookmarks panel.
|
||||
*/
|
||||
class BookmarkScreenshotRow extends LinearLayout {
|
||||
private TextView titleView;
|
||||
private TextView dateView;
|
||||
|
||||
// This DateFormat uses the current locale at instantiation time, which won't get updated if the locale is changed.
|
||||
// Since it's just a date, it's probably not worth the code complexity to fix that.
|
||||
private static final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG);
|
||||
private static final DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
|
||||
|
||||
// This parameter to DateFormat.format has no impact on the result but rather gets mutated by the method to
|
||||
// identify where a certain field starts and ends (by index). This is useful if you want to later modify the String;
|
||||
// I'm not sure why this argument isn't optional.
|
||||
private static final FieldPosition dummyFieldPosition = new FieldPosition(-1);
|
||||
|
||||
public BookmarkScreenshotRow(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
titleView = (TextView) findViewById(R.id.title);
|
||||
dateView = (TextView) findViewById(R.id.date);
|
||||
}
|
||||
|
||||
public void updateFromCursor(final Cursor c) {
|
||||
titleView.setText(getTitleFromCursor(c));
|
||||
dateView.setText(getDateFromCursor(c));
|
||||
}
|
||||
|
||||
private static String getTitleFromCursor(final Cursor c) {
|
||||
final int index = c.getColumnIndexOrThrow(UrlAnnotations.URL);
|
||||
return c.getString(index);
|
||||
}
|
||||
|
||||
private static String getDateFromCursor(final Cursor c) {
|
||||
final long timestamp = c.getLong(c.getColumnIndexOrThrow(UrlAnnotations.DATE_CREATED));
|
||||
final Date date = new Date(timestamp);
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
dateFormat.format(date, sb, dummyFieldPosition)
|
||||
.append(" - ");
|
||||
timeFormat.format(date, sb, dummyFieldPosition);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
/* -*- 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 java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
||||
import org.mozilla.gecko.home.BookmarkFolderView.FolderState;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Adapter to back the BookmarksListView with a list of bookmarks.
|
||||
*/
|
||||
class BookmarksListAdapter extends MultiTypeCursorAdapter {
|
||||
private static final int VIEW_TYPE_BOOKMARK_ITEM = 0;
|
||||
private static final int VIEW_TYPE_FOLDER = 1;
|
||||
private static final int VIEW_TYPE_SCREENSHOT = 2;
|
||||
|
||||
private static final int[] VIEW_TYPES = new int[] { VIEW_TYPE_BOOKMARK_ITEM, VIEW_TYPE_FOLDER, VIEW_TYPE_SCREENSHOT };
|
||||
private static final int[] LAYOUT_TYPES =
|
||||
new int[] { R.layout.bookmark_item_row, R.layout.bookmark_folder_row, R.layout.bookmark_screenshot_row };
|
||||
|
||||
public enum RefreshType implements Parcelable {
|
||||
PARENT,
|
||||
CHILD;
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(ordinal());
|
||||
}
|
||||
|
||||
public static final Creator<RefreshType> CREATOR = new Creator<RefreshType>() {
|
||||
@Override
|
||||
public RefreshType createFromParcel(final Parcel source) {
|
||||
return RefreshType.values()[source.readInt()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefreshType[] newArray(final int size) {
|
||||
return new RefreshType[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static class FolderInfo implements Parcelable {
|
||||
public final int id;
|
||||
public final String title;
|
||||
|
||||
public FolderInfo(int id) {
|
||||
this(id, "");
|
||||
}
|
||||
|
||||
public FolderInfo(Parcel in) {
|
||||
this(in.readInt(), in.readString());
|
||||
}
|
||||
|
||||
public FolderInfo(int id, String title) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(id);
|
||||
dest.writeString(title);
|
||||
}
|
||||
|
||||
public static final Creator<FolderInfo> CREATOR = new Creator<FolderInfo>() {
|
||||
@Override
|
||||
public FolderInfo createFromParcel(Parcel in) {
|
||||
return new FolderInfo(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FolderInfo[] newArray(int size) {
|
||||
return new FolderInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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(FolderInfo folderInfo, RefreshType refreshType);
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of data a bookmarks folder can display. This can be used to
|
||||
* distinguish bookmark folders from "smart folders" that contain non-bookmark
|
||||
* entries but still appear in the Bookmarks panel.
|
||||
*/
|
||||
public enum FolderType {
|
||||
BOOKMARKS,
|
||||
SCREENSHOTS,
|
||||
}
|
||||
|
||||
// mParentStack holds folder info instances (id + title) that allow
|
||||
// us to navigate back up the folder hierarchy.
|
||||
private LinkedList<FolderInfo> mParentStack;
|
||||
|
||||
// Refresh folder listener.
|
||||
private OnRefreshFolderListener mListener;
|
||||
|
||||
private FolderType openFolderType = FolderType.BOOKMARKS;
|
||||
|
||||
public BookmarksListAdapter(Context context, Cursor cursor, List<FolderInfo> parentStack) {
|
||||
// Initializing with a null cursor.
|
||||
super(context, cursor, VIEW_TYPES, LAYOUT_TYPES);
|
||||
|
||||
if (parentStack == null) {
|
||||
mParentStack = new LinkedList<FolderInfo>();
|
||||
} else {
|
||||
mParentStack = new LinkedList<FolderInfo>(parentStack);
|
||||
}
|
||||
}
|
||||
|
||||
public void restoreData(List<FolderInfo> parentStack) {
|
||||
mParentStack = new LinkedList<FolderInfo>(parentStack);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<FolderInfo> getParentStack() {
|
||||
return Collections.unmodifiableList(mParentStack);
|
||||
}
|
||||
|
||||
public FolderType getOpenFolderType() {
|
||||
return openFolderType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to parent folder, if one exists.
|
||||
*
|
||||
* @return Whether the adapter successfully moved to a parent folder.
|
||||
*/
|
||||
public boolean moveToParentFolder() {
|
||||
// If we're already at the root, we can't move to a parent folder.
|
||||
// An empty parent stack here means we're still waiting for the
|
||||
// initial list of bookmarks and can't go to a parent folder.
|
||||
if (mParentStack.size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mListener != null) {
|
||||
// We pick the second folder in the stack as it represents
|
||||
// the parent folder.
|
||||
mListener.onRefreshFolder(mParentStack.get(1), RefreshType.PARENT);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
FolderInfo folderInfo = new FolderInfo(folderId, folderTitle);
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onRefreshFolder(folderInfo, RefreshType.CHILD);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener that can refresh this adapter.
|
||||
*
|
||||
* @param listener The listener that can refresh the adapter.
|
||||
*/
|
||||
public void setOnRefreshFolderListener(OnRefreshFolderListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
private boolean isCurrentFolder(FolderInfo folderInfo) {
|
||||
return (mParentStack.size() > 0 &&
|
||||
mParentStack.peek().id == folderInfo.id);
|
||||
}
|
||||
|
||||
public void swapCursor(Cursor c, FolderInfo folderInfo, RefreshType refreshType) {
|
||||
updateOpenFolderType(folderInfo);
|
||||
switch (refreshType) {
|
||||
case PARENT:
|
||||
if (!isCurrentFolder(folderInfo)) {
|
||||
mParentStack.removeFirst();
|
||||
}
|
||||
break;
|
||||
|
||||
case CHILD:
|
||||
if (!isCurrentFolder(folderInfo)) {
|
||||
mParentStack.addFirst(folderInfo);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing;
|
||||
}
|
||||
|
||||
swapCursor(c);
|
||||
}
|
||||
|
||||
private void updateOpenFolderType(final FolderInfo folderInfo) {
|
||||
if (folderInfo.id == Bookmarks.FIXED_SCREENSHOT_FOLDER_ID) {
|
||||
openFolderType = FolderType.SCREENSHOTS;
|
||||
} else {
|
||||
openFolderType = FolderType.BOOKMARKS;
|
||||
}
|
||||
}
|
||||
|
||||
@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--;
|
||||
}
|
||||
|
||||
if (openFolderType == FolderType.SCREENSHOTS) {
|
||||
return VIEW_TYPE_SCREENSHOT;
|
||||
}
|
||||
|
||||
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_BOOKMARK_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);
|
||||
} else if (guid.equals(Bookmarks.SCREENSHOT_FOLDER_GUID)) {
|
||||
return res.getString(R.string.screenshot_folder_label_in_bookmarks);
|
||||
} else if (guid.equals(Bookmarks.FAKE_READINGLIST_SMARTFOLDER_GUID)) {
|
||||
return res.getString(R.string.readinglist_smartfolder_label_in_bookmarks);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if (mParentStack.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (mParentStack.peek().id != 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_SCREENSHOT) {
|
||||
((BookmarkScreenshotRow) view).updateFromCursor(cursor);
|
||||
} else if (viewType == VIEW_TYPE_BOOKMARK_ITEM) {
|
||||
final TwoLinePageRow row = (TwoLinePageRow) view;
|
||||
row.updateFromCursor(cursor);
|
||||
} else {
|
||||
final BookmarkFolderView row = (BookmarkFolderView) view;
|
||||
if (cursor == null) {
|
||||
final Resources res = context.getResources();
|
||||
row.update(res.getString(R.string.home_move_back_to_filter, mParentStack.get(1).title), -1);
|
||||
row.setState(FolderState.PARENT);
|
||||
} else {
|
||||
int id = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
|
||||
|
||||
row.update(getFolderTitle(context, cursor), id);
|
||||
|
||||
if (id == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) {
|
||||
row.setState(FolderState.READING_LIST);
|
||||
} else {
|
||||
row.setState(FolderState.FOLDER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
/* -*- 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 java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import android.util.Log;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.HeaderViewListAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
|
||||
import org.mozilla.gecko.reader.SavedReaderViewHelper;
|
||||
import org.mozilla.gecko.util.NetworkUtils;
|
||||
|
||||
/**
|
||||
* A ListView of bookmarks.
|
||||
*/
|
||||
public class BookmarksListView extends HomeListView
|
||||
implements AdapterView.OnItemClickListener {
|
||||
public static final String LOGTAG = "GeckoBookmarksListView";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
setOnItemClickListener(this);
|
||||
|
||||
setOnKeyListener(new View.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
final int action = event.getAction();
|
||||
|
||||
// If the user hit the BACK key, try to move to the parent folder.
|
||||
if (action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
return getBookmarksListAdapter().moveToParentFolder();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate telemetry extra for a given folder.
|
||||
*
|
||||
* baseFolderID is the ID of the first-level folder in the parent stack, i.e. the first folder
|
||||
* that was selected from the root hierarchy (e.g. Desktop, Reading List, or any mobile first-level
|
||||
* subfolder). If the current folder is a first-level folder, then the fixed root ID may be used
|
||||
* instead.
|
||||
*
|
||||
* We use baseFolderID only to distinguish whether or not we're currently in a desktop subfolder.
|
||||
* If it isn't equal to FAKE_DESKTOP_FOLDER_ID we know we're in a mobile subfolder, or one
|
||||
* of the smartfolders.
|
||||
*/
|
||||
private String getTelemetryExtraForFolder(int folderID, int baseFolderID) {
|
||||
if (folderID == Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
|
||||
return "folder_desktop";
|
||||
} else if (folderID == Bookmarks.FIXED_SCREENSHOT_FOLDER_ID) {
|
||||
return "folder_screenshots";
|
||||
} else if (folderID == Bookmarks.FAKE_READINGLIST_SMARTFOLDER_ID) {
|
||||
return "folder_reading_list";
|
||||
} else {
|
||||
// The stack depth is 2 for either the fake desktop folder, or any subfolder of mobile
|
||||
// bookmarks, we subtract these offsets so that any direct subfolder of mobile
|
||||
// has a level equal to 1. (Desktop folders will be one level deeper due to the
|
||||
// fake desktop folder, hence subtract 2.)
|
||||
if (baseFolderID == Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
|
||||
return "folder_desktop_subfolder";
|
||||
} else {
|
||||
return "folder_mobile_subfolder";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final BookmarksListAdapter adapter = getBookmarksListAdapter();
|
||||
if (adapter.isShowingChildFolder()) {
|
||||
if (position == 0) {
|
||||
// If we tap on an opened folder, move back to parent folder.
|
||||
|
||||
final List<BookmarksListAdapter.FolderInfo> parentStack = ((BookmarksListAdapter) getAdapter()).getParentStack();
|
||||
if (parentStack.size() < 2) {
|
||||
throw new IllegalStateException("Cannot move to parent folder if we are already in the root folder");
|
||||
}
|
||||
|
||||
// The first item (top of stack) is the current folder, we're returning to the next one
|
||||
BookmarksListAdapter.FolderInfo folder = parentStack.get(1);
|
||||
final int parentID = folder.id;
|
||||
final int baseFolderID;
|
||||
if (parentStack.size() > 2) {
|
||||
baseFolderID = parentStack.get(parentStack.size() - 2).id;
|
||||
} else {
|
||||
baseFolderID = Bookmarks.FIXED_ROOT_ID;
|
||||
}
|
||||
|
||||
final String extra = getTelemetryExtraForFolder(parentID, baseFolderID);
|
||||
|
||||
// Move to parent _after_ retrieving stack information
|
||||
adapter.moveToParentFolder();
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.LIST_ITEM, extra);
|
||||
return;
|
||||
}
|
||||
|
||||
// Accounting for the folder view.
|
||||
position--;
|
||||
}
|
||||
|
||||
final Cursor cursor = adapter.getCursor();
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.moveToPosition(position);
|
||||
|
||||
if (adapter.getOpenFolderType() == BookmarksListAdapter.FolderType.SCREENSHOTS) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "bookmarks-screenshot");
|
||||
|
||||
final String fileUrl = "file://" + cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.VALUE));
|
||||
getOnUrlOpenListener().onUrlOpen(fileUrl, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
final List<BookmarksListAdapter.FolderInfo> parentStack = ((BookmarksListAdapter) getAdapter()).getParentStack();
|
||||
|
||||
final int baseFolderID;
|
||||
if (parentStack.size() > 2) {
|
||||
baseFolderID = parentStack.get(parentStack.size() - 2).id;
|
||||
} else {
|
||||
baseFolderID = Bookmarks.FIXED_ROOT_ID;
|
||||
}
|
||||
|
||||
final String extra = getTelemetryExtraForFolder(folderId, baseFolderID);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.LIST_ITEM, extra);
|
||||
} else {
|
||||
// Otherwise, just open the URL
|
||||
final String url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL));
|
||||
|
||||
final SavedReaderViewHelper rvh = SavedReaderViewHelper.getSavedReaderViewHelper(getContext());
|
||||
|
||||
final String extra;
|
||||
if (rvh.isURLCached(url)) {
|
||||
extra = "bookmarks-reader";
|
||||
} else {
|
||||
extra = "bookmarks";
|
||||
}
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, extra);
|
||||
Telemetry.addToHistogram("FENNEC_LOAD_SAVED_PAGE", NetworkUtils.isConnected(getContext()) ? 2 : 3);
|
||||
|
||||
// This item is a TwoLinePageRow, so we allow switch-to-tab.
|
||||
getOnUrlOpenListener().onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
// Adjust the item position to account for the parent folder row that is inserted
|
||||
// at the top of the list when viewing the contents of a folder.
|
||||
final BookmarksListAdapter adapter = getBookmarksListAdapter();
|
||||
if (adapter.isShowingChildFolder()) {
|
||||
position--;
|
||||
}
|
||||
|
||||
// Temporarily prevent crashes until we figure out what we actually want to do here (bug 1252316).
|
||||
if (adapter.getOpenFolderType() == BookmarksListAdapter.FolderType.SCREENSHOTS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.onItemLongClick(parent, view, position, id);
|
||||
}
|
||||
|
||||
private BookmarksListAdapter getBookmarksListAdapter() {
|
||||
BookmarksListAdapter adapter;
|
||||
ListAdapter listAdapter = getAdapter();
|
||||
if (listAdapter instanceof HeaderViewListAdapter) {
|
||||
adapter = (BookmarksListAdapter) ((HeaderViewListAdapter) listAdapter).getWrappedAdapter();
|
||||
} else {
|
||||
adapter = (BookmarksListAdapter) listAdapter;
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
/* -*- 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 java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
|
||||
import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
|
||||
import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
|
||||
import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
|
||||
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.Cursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* A page in about:home that displays a ListView of bookmarks.
|
||||
*/
|
||||
public class BookmarksPanel extends HomeFragment {
|
||||
public static final String LOGTAG = "GeckoBookmarksPanel";
|
||||
|
||||
// Cursor loader ID for list of bookmarks.
|
||||
private static final int LOADER_ID_BOOKMARKS_LIST = 0;
|
||||
|
||||
// Information about the target bookmarks folder.
|
||||
private static final String BOOKMARKS_FOLDER_INFO = "folder_info";
|
||||
|
||||
// Refresh type for folder refreshing loader.
|
||||
private static final String BOOKMARKS_REFRESH_TYPE = "refresh_type";
|
||||
|
||||
// List of bookmarks.
|
||||
private BookmarksListView mList;
|
||||
|
||||
// Adapter for list of bookmarks.
|
||||
private BookmarksListAdapter mListAdapter;
|
||||
|
||||
// Adapter's parent stack.
|
||||
private List<FolderInfo> mSavedParentStack;
|
||||
|
||||
// Reference to the View to display when there are no results.
|
||||
private View mEmptyView;
|
||||
|
||||
// Callback for cursor loaders.
|
||||
private CursorLoaderCallbacks mLoaderCallbacks;
|
||||
|
||||
@Override
|
||||
public void restoreData(@NonNull Bundle data) {
|
||||
final ArrayList<FolderInfo> stack = data.getParcelableArrayList("parentStack");
|
||||
if (stack == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mListAdapter == null) {
|
||||
mSavedParentStack = new LinkedList<FolderInfo>(stack);
|
||||
} else {
|
||||
mListAdapter.restoreData(stack);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.home_bookmarks_panel, container, false);
|
||||
|
||||
mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list);
|
||||
|
||||
mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
|
||||
@Override
|
||||
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
|
||||
final int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE));
|
||||
if (type == Bookmarks.TYPE_FOLDER) {
|
||||
// We don't show a context menu for folders
|
||||
return null;
|
||||
}
|
||||
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
|
||||
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL));
|
||||
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE));
|
||||
info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
|
||||
info.itemType = RemoveItemType.BOOKMARKS;
|
||||
return info;
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
mList.setTag(HomePager.LIST_TAG_BOOKMARKS);
|
||||
mList.setOnUrlOpenListener(listener);
|
||||
|
||||
registerForContextMenu(mList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
|
||||
// Setup the list adapter.
|
||||
mListAdapter = new BookmarksListAdapter(activity, null, mSavedParentStack);
|
||||
mListAdapter.setOnRefreshFolderListener(new OnRefreshFolderListener() {
|
||||
@Override
|
||||
public void onRefreshFolder(FolderInfo folderInfo, RefreshType refreshType) {
|
||||
// Restart the loader with folder as the argument.
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(BOOKMARKS_FOLDER_INFO, folderInfo);
|
||||
bundle.putParcelable(BOOKMARKS_REFRESH_TYPE, refreshType);
|
||||
getLoaderManager().restartLoader(LOADER_ID_BOOKMARKS_LIST, bundle, mLoaderCallbacks);
|
||||
}
|
||||
});
|
||||
mList.setAdapter(mListAdapter);
|
||||
|
||||
// Create callbacks before the initial loader is started.
|
||||
mLoaderCallbacks = new CursorLoaderCallbacks();
|
||||
loadIfVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
mList = null;
|
||||
mListAdapter = null;
|
||||
mEmptyView = null;
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (isVisible()) {
|
||||
// The parent stack is saved just so that the folder state can be
|
||||
// restored on rotation.
|
||||
mSavedParentStack = mListAdapter.getParentStack();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
final Bundle bundle;
|
||||
if (mSavedParentStack != null && mSavedParentStack.size() > 1) {
|
||||
bundle = new Bundle();
|
||||
bundle.putParcelable(BOOKMARKS_FOLDER_INFO, mSavedParentStack.get(0));
|
||||
bundle.putParcelable(BOOKMARKS_REFRESH_TYPE, RefreshType.CHILD);
|
||||
} else {
|
||||
bundle = null;
|
||||
}
|
||||
|
||||
getLoaderManager().initLoader(LOADER_ID_BOOKMARKS_LIST, bundle, mLoaderCallbacks);
|
||||
}
|
||||
|
||||
private void updateUiFromCursor(Cursor c) {
|
||||
if ((c == null || c.getCount() == 0) && 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_bookmarks_empty);
|
||||
|
||||
final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
|
||||
emptyText.setText(R.string.home_bookmarks_empty);
|
||||
|
||||
mList.setEmptyView(mEmptyView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader for the list for bookmarks.
|
||||
*/
|
||||
private static class BookmarksLoader extends SimpleCursorLoader {
|
||||
private final FolderInfo mFolderInfo;
|
||||
private final RefreshType mRefreshType;
|
||||
private final BrowserDB mDB;
|
||||
|
||||
public BookmarksLoader(Context context) {
|
||||
this(context,
|
||||
new FolderInfo(Bookmarks.FIXED_ROOT_ID, context.getResources().getString(R.string.bookmarks_title)),
|
||||
RefreshType.CHILD);
|
||||
}
|
||||
|
||||
public BookmarksLoader(Context context, FolderInfo folderInfo, RefreshType refreshType) {
|
||||
super(context);
|
||||
mFolderInfo = folderInfo;
|
||||
mRefreshType = refreshType;
|
||||
mDB = BrowserDB.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor loadCursor() {
|
||||
final boolean isRootFolder = mFolderInfo.id == BrowserContract.Bookmarks.FIXED_ROOT_ID;
|
||||
|
||||
final ContentResolver contentResolver = getContext().getContentResolver();
|
||||
|
||||
Cursor partnerCursor = null;
|
||||
Cursor userCursor = null;
|
||||
|
||||
if (GeckoSharedPrefs.forProfile(getContext()).getBoolean(GeckoPreferences.PREFS_READ_PARTNER_BOOKMARKS_PROVIDER, false)
|
||||
&& (isRootFolder || mFolderInfo.id <= Bookmarks.FAKE_PARTNER_BOOKMARKS_START)) {
|
||||
partnerCursor = contentResolver.query(PartnerBookmarksProviderProxy.getUriForBookmarks(getContext(), mFolderInfo.id), null, null, null, null, null);
|
||||
}
|
||||
|
||||
if (isRootFolder || mFolderInfo.id > Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
|
||||
userCursor = mDB.getBookmarksInFolder(contentResolver, mFolderInfo.id);
|
||||
}
|
||||
|
||||
|
||||
if (partnerCursor == null && userCursor == null) {
|
||||
return null;
|
||||
} else if (partnerCursor == null) {
|
||||
return userCursor;
|
||||
} else if (userCursor == null) {
|
||||
return partnerCursor;
|
||||
} else {
|
||||
return new MergeCursor(new Cursor[] { partnerCursor, userCursor });
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentChanged() {
|
||||
// Invalidate the cached value that keeps track of whether or
|
||||
// not desktop bookmarks exist.
|
||||
mDB.invalidate();
|
||||
super.onContentChanged();
|
||||
}
|
||||
|
||||
public FolderInfo getFolderInfo() {
|
||||
return mFolderInfo;
|
||||
}
|
||||
|
||||
public RefreshType getRefreshType() {
|
||||
return mRefreshType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader callbacks for the LoaderManager of this fragment.
|
||||
*/
|
||||
private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
if (args == null) {
|
||||
return new BookmarksLoader(getActivity());
|
||||
} else {
|
||||
FolderInfo folderInfo = (FolderInfo) args.getParcelable(BOOKMARKS_FOLDER_INFO);
|
||||
RefreshType refreshType = (RefreshType) args.getParcelable(BOOKMARKS_REFRESH_TYPE);
|
||||
return new BookmarksLoader(getActivity(), folderInfo, refreshType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
||||
BookmarksLoader bl = (BookmarksLoader) loader;
|
||||
mListAdapter.swapCursor(c, bl.getFolderInfo(), bl.getRefreshType());
|
||||
|
||||
if (mPanelStateChangeListener != null) {
|
||||
final List<FolderInfo> parentStack = mListAdapter.getParentStack();
|
||||
final Bundle bundle = new Bundle();
|
||||
|
||||
// Bundle likes to store ArrayLists or Arrays, but we've got a generic List (which
|
||||
// is actually an unmodifiable wrapper around a LinkedList). We'll need to do a
|
||||
// LinkedList conversion at the other end, when saving we need to use this awkward
|
||||
// syntax to create an Array.
|
||||
bundle.putParcelableArrayList("parentStack", new ArrayList<FolderInfo>(parentStack));
|
||||
|
||||
mPanelStateChangeListener.onStateChanged(bundle);
|
||||
}
|
||||
updateUiFromCursor(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
if (mList != null) {
|
||||
mListAdapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1316
mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
Normal file
1316
mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,373 @@
|
|||
/* -*- 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.support.annotation.UiThread;
|
||||
import android.support.v4.util.Pair;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.db.RemoteTab;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType.*;
|
||||
|
||||
public class ClientsAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
|
||||
public static final String LOGTAG = "GeckoClientsAdapter";
|
||||
|
||||
/**
|
||||
* If a device claims to have synced before this date, we will assume it has never synced.
|
||||
*/
|
||||
public static final Date EARLIEST_VALID_SYNCED_DATE;
|
||||
static {
|
||||
final Calendar c = GregorianCalendar.getInstance();
|
||||
c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
|
||||
EARLIEST_VALID_SYNCED_DATE = c.getTime();
|
||||
}
|
||||
|
||||
List<Pair<String, Integer>> adapterList = new LinkedList<>();
|
||||
|
||||
// List of hidden remote clients.
|
||||
// Only accessed from the UI thread.
|
||||
protected final List<RemoteClient> hiddenClients = new ArrayList<>();
|
||||
private Map<String, RemoteClient> visibleClients = new HashMap<>();
|
||||
|
||||
// Maintain group collapsed and hidden state. Only accessed from the UI thread.
|
||||
protected static RemoteTabsExpandableListState sState;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public ClientsAdapter(Context context) {
|
||||
this.context = context;
|
||||
|
||||
// This races when multiple Fragments are created. That's okay: one
|
||||
// will win, and thereafter, all will be okay. If we create and then
|
||||
// drop an instance the shared SharedPreferences backing all the
|
||||
// instances will maintain the state for us. Since everything happens on
|
||||
// the UI thread, this doesn't even need to be volatile.
|
||||
if (sState == null) {
|
||||
sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
|
||||
}
|
||||
|
||||
this.setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final View view;
|
||||
|
||||
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
|
||||
|
||||
switch (itemType) {
|
||||
case NAVIGATION_BACK:
|
||||
view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
|
||||
case CLIENT:
|
||||
view = inflater.inflate(R.layout.home_remote_tabs_group, parent, false);
|
||||
return new CombinedHistoryItem.ClientItem(view);
|
||||
|
||||
case CHILD:
|
||||
view = inflater.inflate(R.layout.home_item_row, parent, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, parent, false);
|
||||
return new CombinedHistoryItem.BasicItem(view);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
|
||||
switch (itemType) {
|
||||
case CLIENT:
|
||||
final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) holder;
|
||||
final String clientGuid = adapterList.get(position).first;
|
||||
final RemoteClient client = visibleClients.get(clientGuid);
|
||||
clientItem.bind(context, client, sState.isClientCollapsed(clientGuid));
|
||||
break;
|
||||
|
||||
case CHILD:
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
RemoteTab remoteTab = visibleClients.get(pair.first).tabs.get(pair.second);
|
||||
((CombinedHistoryItem.HistoryItem) holder).bind(remoteTab);
|
||||
break;
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size());
|
||||
((TextView) holder.itemView).setText(hiddenDevicesLabel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount () {
|
||||
return adapterList.size();
|
||||
}
|
||||
|
||||
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
|
||||
if (position == 0) {
|
||||
return NAVIGATION_BACK;
|
||||
}
|
||||
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
if (pair == null) {
|
||||
return HIDDEN_DEVICES;
|
||||
} else if (pair.second == -1) {
|
||||
return CLIENT;
|
||||
} else {
|
||||
return CHILD;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
// RecyclerView.NO_ID is -1, so start our hard-coded IDs at -2.
|
||||
final int NAVIGATION_BACK_ID = -2;
|
||||
final int HIDDEN_DEVICES_ID = -3;
|
||||
|
||||
final String clientGuid;
|
||||
// adapterList is a list of tuples (clientGuid, tabId).
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
|
||||
switch (getItemTypeForPosition(position)) {
|
||||
case NAVIGATION_BACK:
|
||||
return NAVIGATION_BACK_ID;
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
return HIDDEN_DEVICES_ID;
|
||||
|
||||
// For Clients, return hashCode of their GUIDs.
|
||||
case CLIENT:
|
||||
clientGuid = pair.first;
|
||||
return clientGuid.hashCode();
|
||||
|
||||
// For Tabs, return hashCode of their URLs.
|
||||
case CHILD:
|
||||
clientGuid = pair.first;
|
||||
final Integer tabId = pair.second;
|
||||
|
||||
final RemoteClient remoteClient = visibleClients.get(clientGuid);
|
||||
if (remoteClient == null) {
|
||||
return RecyclerView.NO_ID;
|
||||
}
|
||||
|
||||
final RemoteTab remoteTab = remoteClient.tabs.get(tabId);
|
||||
if (remoteTab == null) {
|
||||
return RecyclerView.NO_ID;
|
||||
}
|
||||
|
||||
return remoteTab.url.hashCode();
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected Home Panel item type");
|
||||
}
|
||||
}
|
||||
|
||||
public int getClientsCount() {
|
||||
return hiddenClients.size() + visibleClients.size();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void setClients(List<RemoteClient> clients) {
|
||||
adapterList.clear();
|
||||
adapterList.add(null);
|
||||
|
||||
hiddenClients.clear();
|
||||
visibleClients.clear();
|
||||
|
||||
for (RemoteClient client : clients) {
|
||||
final String guid = client.guid;
|
||||
if (sState.isClientHidden(guid)) {
|
||||
hiddenClients.add(client);
|
||||
} else {
|
||||
visibleClients.put(guid, client);
|
||||
adapterList.addAll(getVisibleItems(client));
|
||||
}
|
||||
}
|
||||
|
||||
// Add item for unhiding clients.
|
||||
if (!hiddenClients.isEmpty()) {
|
||||
adapterList.add(null);
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static List<Pair<String, Integer>> getVisibleItems(RemoteClient client) {
|
||||
List<Pair<String, Integer>> list = new LinkedList<>();
|
||||
final String guid = client.guid;
|
||||
list.add(new Pair<>(guid, -1));
|
||||
if (!sState.isClientCollapsed(client.guid)) {
|
||||
for (int i = 0; i < client.tabs.size(); i++) {
|
||||
list.add(new Pair<>(guid, i));
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<RemoteClient> getHiddenClients() {
|
||||
return hiddenClients;
|
||||
}
|
||||
|
||||
public void toggleClient(int position) {
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
if (pair.second != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String clientGuid = pair.first;
|
||||
final RemoteClient client = visibleClients.get(clientGuid);
|
||||
|
||||
final boolean isCollapsed = sState.isClientCollapsed(clientGuid);
|
||||
|
||||
sState.setClientCollapsed(clientGuid, !isCollapsed);
|
||||
notifyItemChanged(position);
|
||||
|
||||
if (isCollapsed) {
|
||||
for (int i = client.tabs.size() - 1; i > -1; i--) {
|
||||
// Insert child tabs at the index right after the client item that was clicked.
|
||||
adapterList.add(position + 1, new Pair<>(clientGuid, i));
|
||||
}
|
||||
notifyItemRangeInserted(position + 1, client.tabs.size());
|
||||
} else {
|
||||
int i = client.tabs.size();
|
||||
while (i > 0) {
|
||||
adapterList.remove(position + 1);
|
||||
i--;
|
||||
}
|
||||
notifyItemRangeRemoved(position + 1, client.tabs.size());
|
||||
}
|
||||
}
|
||||
|
||||
public void unhideClients(List<RemoteClient> selectedClients) {
|
||||
final int numClients = selectedClients.size();
|
||||
if (numClients == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int insertionIndex = adapterList.size() - 1;
|
||||
int itemCount = numClients;
|
||||
|
||||
for (RemoteClient client : selectedClients) {
|
||||
final String clientGuid = client.guid;
|
||||
|
||||
sState.setClientHidden(clientGuid, false);
|
||||
hiddenClients.remove(client);
|
||||
|
||||
visibleClients.put(clientGuid, client);
|
||||
sState.setClientCollapsed(clientGuid, false);
|
||||
adapterList.addAll(adapterList.size() - 1, getVisibleItems(client));
|
||||
|
||||
itemCount += client.tabs.size();
|
||||
}
|
||||
|
||||
notifyItemRangeInserted(insertionIndex, itemCount);
|
||||
|
||||
final int hiddenDevicesIndex = adapterList.size() - 1;
|
||||
if (hiddenClients.isEmpty()) {
|
||||
// No more hidden clients, remove "unhide" item.
|
||||
adapterList.remove(hiddenDevicesIndex);
|
||||
notifyItemRemoved(hiddenDevicesIndex);
|
||||
} else {
|
||||
// Update "hidden clients" item because number of hidden clients changed.
|
||||
notifyItemChanged(hiddenDevicesIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeItem(int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
switch (itemType) {
|
||||
case CLIENT:
|
||||
final String clientGuid = adapterList.get(position).first;
|
||||
final RemoteClient client = visibleClients.remove(clientGuid);
|
||||
final boolean hadHiddenClients = !hiddenClients.isEmpty();
|
||||
|
||||
int removeCount = sState.isClientCollapsed(clientGuid) ? 1 : client.tabs.size() + 1;
|
||||
int c = removeCount;
|
||||
while (c > 0) {
|
||||
adapterList.remove(position);
|
||||
c--;
|
||||
}
|
||||
notifyItemRangeRemoved(position, removeCount);
|
||||
|
||||
sState.setClientHidden(clientGuid, true);
|
||||
hiddenClients.add(client);
|
||||
|
||||
if (!hadHiddenClients) {
|
||||
// Add item for unhiding clients;
|
||||
adapterList.add(null);
|
||||
notifyItemInserted(adapterList.size() - 1);
|
||||
} else {
|
||||
// Update "hidden clients" item because number of hidden clients changed.
|
||||
notifyItemChanged(adapterList.size() - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
HomeContextMenuInfo info;
|
||||
final Pair<String, Integer> pair = adapterList.get(position);
|
||||
switch (itemType) {
|
||||
case CHILD:
|
||||
info = new HomeContextMenuInfo(view, position, -1);
|
||||
return populateChildInfoFromTab(info, visibleClients.get(pair.first).tabs.get(pair.second));
|
||||
|
||||
case CLIENT:
|
||||
info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position, -1, visibleClients.get(pair.first));
|
||||
return info;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab) {
|
||||
info.url = tab.url;
|
||||
info.title = tab.title;
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a relative "Last synced" time span for the given tab record.
|
||||
*
|
||||
* @param now local time.
|
||||
* @param time to format string for.
|
||||
* @return string describing time span
|
||||
*/
|
||||
public static String getLastSyncedString(Context context, long now, long time) {
|
||||
if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) {
|
||||
return context.getString(R.string.remote_tabs_never_synced);
|
||||
}
|
||||
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
|
||||
return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,433 @@
|
|||
/* -*- 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.res.Resources;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
|
||||
private static final int RECENT_TABS_SMARTFOLDER_INDEX = 0;
|
||||
|
||||
// Array for the time ranges in milliseconds covered by each section.
|
||||
static final HistorySectionsHelper.SectionDateRange[] sectionDateRangeArray = new HistorySectionsHelper.SectionDateRange[SectionHeader.values().length];
|
||||
|
||||
// Semantic names for the time covered by each section
|
||||
public enum SectionHeader {
|
||||
TODAY,
|
||||
YESTERDAY,
|
||||
WEEK,
|
||||
THIS_MONTH,
|
||||
MONTH_AGO,
|
||||
TWO_MONTHS_AGO,
|
||||
THREE_MONTHS_AGO,
|
||||
FOUR_MONTHS_AGO,
|
||||
FIVE_MONTHS_AGO,
|
||||
OLDER_THAN_SIX_MONTHS
|
||||
}
|
||||
|
||||
private HomeFragment.PanelStateChangeListener panelStateChangeListener;
|
||||
|
||||
private Cursor historyCursor;
|
||||
private DevicesUpdateHandler devicesUpdateHandler;
|
||||
private int deviceCount = 0;
|
||||
private RecentTabsUpdateHandler recentTabsUpdateHandler;
|
||||
private int recentTabsCount = 0;
|
||||
|
||||
private LinearLayoutManager linearLayoutManager; // Only used on the UI thread, so no need to be volatile.
|
||||
|
||||
// We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
|
||||
private final SparseArray<SectionHeader> sectionHeaders;
|
||||
|
||||
public CombinedHistoryAdapter(Resources resources, int cachedRecentTabsCount) {
|
||||
super();
|
||||
recentTabsCount = cachedRecentTabsCount;
|
||||
sectionHeaders = new SparseArray<>();
|
||||
HistorySectionsHelper.updateRecentSectionOffset(resources, sectionDateRangeArray);
|
||||
this.setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void setPanelStateChangeListener(
|
||||
HomeFragment.PanelStateChangeListener panelStateChangeListener) {
|
||||
this.panelStateChangeListener = panelStateChangeListener;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void setLinearLayoutManager(LinearLayoutManager linearLayoutManager) {
|
||||
this.linearLayoutManager = linearLayoutManager;
|
||||
}
|
||||
|
||||
public void setHistory(Cursor history) {
|
||||
historyCursor = history;
|
||||
populateSectionHeaders(historyCursor, sectionHeaders);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public interface DevicesUpdateHandler {
|
||||
void onDeviceCountUpdated(int count);
|
||||
}
|
||||
|
||||
public DevicesUpdateHandler getDeviceUpdateHandler() {
|
||||
if (devicesUpdateHandler == null) {
|
||||
devicesUpdateHandler = new DevicesUpdateHandler() {
|
||||
@Override
|
||||
public void onDeviceCountUpdated(int count) {
|
||||
deviceCount = count;
|
||||
notifyItemChanged(getSyncedDevicesSmartFolderIndex());
|
||||
}
|
||||
};
|
||||
}
|
||||
return devicesUpdateHandler;
|
||||
}
|
||||
|
||||
public interface RecentTabsUpdateHandler {
|
||||
void onRecentTabsCountUpdated(int count, boolean countReliable);
|
||||
}
|
||||
|
||||
public RecentTabsUpdateHandler getRecentTabsUpdateHandler() {
|
||||
if (recentTabsUpdateHandler != null) {
|
||||
return recentTabsUpdateHandler;
|
||||
}
|
||||
|
||||
recentTabsUpdateHandler = new RecentTabsUpdateHandler() {
|
||||
@Override
|
||||
public void onRecentTabsCountUpdated(final int count, final boolean countReliable) {
|
||||
// Now that other items can move around depending on the visibility of the
|
||||
// Recent Tabs folder, only update the recentTabsCount on the UI thread.
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@UiThread
|
||||
@Override
|
||||
public void run() {
|
||||
if (!countReliable && count <= recentTabsCount) {
|
||||
// The final tab count (where countReliable = true) is normally >= than
|
||||
// previous values with countReliable = false. Hence we only want to
|
||||
// update the displayed tab count with a preliminary value if it's larger
|
||||
// than the previous count, so as to avoid the displayed count jumping
|
||||
// downwards and then back up, as well as unnecessary folder animations.
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean prevFolderVisibility = isRecentTabsFolderVisible();
|
||||
recentTabsCount = count;
|
||||
final boolean folderVisible = isRecentTabsFolderVisible();
|
||||
|
||||
if (prevFolderVisibility == folderVisible) {
|
||||
if (prevFolderVisibility) {
|
||||
notifyItemChanged(RECENT_TABS_SMARTFOLDER_INDEX);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the Recent Tabs smart folder has become hidden/unhidden,
|
||||
// we need to recalculate the history section header positions.
|
||||
populateSectionHeaders(historyCursor, sectionHeaders);
|
||||
|
||||
if (folderVisible) {
|
||||
int scrollPos = -1;
|
||||
if (linearLayoutManager != null) {
|
||||
scrollPos = linearLayoutManager.findFirstCompletelyVisibleItemPosition();
|
||||
}
|
||||
|
||||
notifyItemInserted(RECENT_TABS_SMARTFOLDER_INDEX);
|
||||
// If the list exceeds the display height and we want to show the new
|
||||
// item inserted at position 0, we need to scroll up manually
|
||||
// (see https://code.google.com/p/android/issues/detail?id=174227#c2).
|
||||
// However we only do this if our current scroll position is at the
|
||||
// top of the list.
|
||||
if (linearLayoutManager != null && scrollPos == 0) {
|
||||
linearLayoutManager.scrollToPosition(0);
|
||||
}
|
||||
} else {
|
||||
notifyItemRemoved(RECENT_TABS_SMARTFOLDER_INDEX);
|
||||
}
|
||||
|
||||
if (countReliable && panelStateChangeListener != null) {
|
||||
panelStateChangeListener.setCachedRecentTabsCount(recentTabsCount);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return recentTabsUpdateHandler;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private boolean isRecentTabsFolderVisible() {
|
||||
return recentTabsCount > 0;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
// Number of smart folders for determining practical empty state.
|
||||
public int getNumVisibleSmartFolders() {
|
||||
int visibleFolders = 1; // Synced devices folder is always visible.
|
||||
|
||||
if (isRecentTabsFolderVisible()) {
|
||||
visibleFolders += 1;
|
||||
}
|
||||
|
||||
return visibleFolders;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private int getSyncedDevicesSmartFolderIndex() {
|
||||
return isRecentTabsFolderVisible() ?
|
||||
RECENT_TABS_SMARTFOLDER_INDEX + 1 :
|
||||
RECENT_TABS_SMARTFOLDER_INDEX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
|
||||
final View view;
|
||||
|
||||
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
|
||||
|
||||
switch (itemType) {
|
||||
case RECENT_TABS:
|
||||
case SYNCED_DEVICES:
|
||||
view = inflater.inflate(R.layout.home_smartfolder, viewGroup, false);
|
||||
return new CombinedHistoryItem.SmartFolder(view);
|
||||
|
||||
case SECTION_HEADER:
|
||||
view = inflater.inflate(R.layout.home_header_row, viewGroup, false);
|
||||
return new CombinedHistoryItem.BasicItem(view);
|
||||
|
||||
case HISTORY:
|
||||
view = inflater.inflate(R.layout.home_item_row, viewGroup, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected Home Panel item type");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
|
||||
|
||||
switch (itemType) {
|
||||
case RECENT_TABS:
|
||||
((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.icon_recent, R.string.home_closed_tabs_title2, R.string.home_closed_tabs_one, R.string.home_closed_tabs_number, recentTabsCount);
|
||||
break;
|
||||
|
||||
case SYNCED_DEVICES:
|
||||
((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.cloud, R.string.home_synced_devices_smartfolder, R.string.home_synced_devices_one, R.string.home_synced_devices_number, deviceCount);
|
||||
break;
|
||||
|
||||
case SECTION_HEADER:
|
||||
((TextView) viewHolder.itemView).setText(getSectionHeaderTitle(sectionHeaders.get(localPosition)));
|
||||
break;
|
||||
|
||||
case HISTORY:
|
||||
if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) {
|
||||
throw new IllegalStateException("Couldn't move cursor to position " + localPosition);
|
||||
}
|
||||
((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an adapter position to the position for the data structure backing the item type.
|
||||
*
|
||||
* The type is not strictly necessary and could be fetched from <code>getItemTypeForPosition</code>,
|
||||
* but is used for explicitness.
|
||||
*
|
||||
* @param type ItemType of the item
|
||||
* @param position position in the adapter
|
||||
* @return position of the item in the data structure
|
||||
*/
|
||||
@UiThread
|
||||
private int transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType type, int position) {
|
||||
if (type == CombinedHistoryItem.ItemType.SECTION_HEADER) {
|
||||
return position;
|
||||
} else if (type == CombinedHistoryItem.ItemType.HISTORY) {
|
||||
return position - getHeadersBefore(position) - getNumVisibleSmartFolders();
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
|
||||
if (position == RECENT_TABS_SMARTFOLDER_INDEX && isRecentTabsFolderVisible()) {
|
||||
return CombinedHistoryItem.ItemType.RECENT_TABS;
|
||||
}
|
||||
if (position == getSyncedDevicesSmartFolderIndex()) {
|
||||
return CombinedHistoryItem.ItemType.SYNCED_DEVICES;
|
||||
}
|
||||
final int sectionPosition = transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.SECTION_HEADER, position);
|
||||
if (sectionHeaders.get(sectionPosition) != null) {
|
||||
return CombinedHistoryItem.ItemType.SECTION_HEADER;
|
||||
}
|
||||
return CombinedHistoryItem.ItemType.HISTORY;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
|
||||
return historySize + sectionHeaders.size() + getNumVisibleSmartFolders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stable ID for each position. Data behind historyCursor is a sorted Combined view.
|
||||
*
|
||||
* @param position view item position for which to generate a stable ID
|
||||
* @return stable ID for given position
|
||||
*/
|
||||
@UiThread
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
// Two randomly selected large primes used to generate non-clashing IDs.
|
||||
final long PRIME_BOOKMARKS = 32416189867L;
|
||||
final long PRIME_SECTION_HEADERS = 32416187737L;
|
||||
|
||||
// RecyclerView.NO_ID is -1, so let's start from -2 for our hard-coded IDs.
|
||||
final int RECENT_TABS_ID = -2;
|
||||
final int SYNCED_DEVICES_ID = -3;
|
||||
|
||||
switch (getItemTypeForPosition(position)) {
|
||||
case RECENT_TABS:
|
||||
return RECENT_TABS_ID;
|
||||
case SYNCED_DEVICES:
|
||||
return SYNCED_DEVICES_ID;
|
||||
case SECTION_HEADER:
|
||||
// We might have multiple section headers, so we try get unique IDs for them.
|
||||
return position * PRIME_SECTION_HEADERS;
|
||||
case HISTORY:
|
||||
final int historyPosition = transformAdapterPositionForDataStructure(
|
||||
CombinedHistoryItem.ItemType.HISTORY, position);
|
||||
if (!historyCursor.moveToPosition(historyPosition)) {
|
||||
return RecyclerView.NO_ID;
|
||||
}
|
||||
|
||||
final int historyIdCol = historyCursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID);
|
||||
final long historyId = historyCursor.getLong(historyIdCol);
|
||||
|
||||
if (historyId != -1) {
|
||||
return historyId;
|
||||
}
|
||||
|
||||
final int bookmarkIdCol = historyCursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
|
||||
final long bookmarkId = historyCursor.getLong(bookmarkIdCol);
|
||||
|
||||
// Avoid clashing with historyId.
|
||||
return bookmarkId * PRIME_BOOKMARKS;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected Home Panel item type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add only the SectionHeaders that have history items within their range to a SparseArray, where the
|
||||
* array index is the position of the header in the history-only (no clients) ordering.
|
||||
* @param c data Cursor
|
||||
* @param sparseArray SparseArray to populate
|
||||
*/
|
||||
@UiThread
|
||||
private void populateSectionHeaders(Cursor c, SparseArray<SectionHeader> sparseArray) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
sparseArray.clear();
|
||||
|
||||
if (c == null || !c.moveToFirst()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SectionHeader section = null;
|
||||
|
||||
do {
|
||||
final int historyPosition = c.getPosition();
|
||||
final long visitTime = c.getLong(c.getColumnIndexOrThrow(BrowserContract.History.DATE_LAST_VISITED));
|
||||
final SectionHeader itemSection = getSectionFromTime(visitTime);
|
||||
|
||||
if (section != itemSection) {
|
||||
section = itemSection;
|
||||
sparseArray.append(historyPosition + sparseArray.size() + getNumVisibleSmartFolders(), section);
|
||||
}
|
||||
|
||||
if (section == SectionHeader.OLDER_THAN_SIX_MONTHS) {
|
||||
break;
|
||||
}
|
||||
} while (c.moveToNext());
|
||||
}
|
||||
|
||||
private static String getSectionHeaderTitle(SectionHeader section) {
|
||||
return sectionDateRangeArray[section.ordinal()].displayName;
|
||||
}
|
||||
|
||||
private static SectionHeader getSectionFromTime(long time) {
|
||||
for (int i = 0; i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) {
|
||||
if (time > sectionDateRangeArray[i].start) {
|
||||
return SectionHeader.values()[i];
|
||||
}
|
||||
}
|
||||
|
||||
return SectionHeader.OLDER_THAN_SIX_MONTHS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of section headers before the given history item at the adapter position.
|
||||
* @param position position in the adapter
|
||||
*/
|
||||
private int getHeadersBefore(int position) {
|
||||
// Skip the first header case because there will always be a header.
|
||||
for (int i = 1; i < sectionHeaders.size(); i++) {
|
||||
// If the position of the header is greater than the history position,
|
||||
// return the number of headers tested.
|
||||
if (sectionHeaders.keyAt(i) > position) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return sectionHeaders.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
if (itemType == CombinedHistoryItem.ItemType.HISTORY) {
|
||||
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, -1);
|
||||
|
||||
historyCursor.moveToPosition(transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.HISTORY, position));
|
||||
return populateHistoryInfoFromCursor(info, historyCursor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static HomeContextMenuInfo populateHistoryInfoFromCursor(HomeContextMenuInfo info, Cursor cursor) {
|
||||
info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
|
||||
info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
|
||||
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
|
||||
info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY;
|
||||
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
|
||||
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.
|
||||
info.bookmarkId = -1;
|
||||
} else {
|
||||
info.bookmarkId = cursor.getInt(bookmarkIdCol);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/* -*- 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.content.ContextCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.db.RemoteTab;
|
||||
import org.mozilla.gecko.home.RecentTabsAdapter.ClosedTab;
|
||||
|
||||
public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
|
||||
private static final String LOGTAG = "CombinedHistoryItem";
|
||||
|
||||
public CombinedHistoryItem(View view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
public enum ItemType {
|
||||
CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES,
|
||||
RECENT_TABS, CLOSED_TAB;
|
||||
|
||||
public static ItemType viewTypeToItemType(int viewType) {
|
||||
if (viewType >= ItemType.values().length) {
|
||||
Log.e(LOGTAG, "No corresponding ItemType!");
|
||||
}
|
||||
return ItemType.values()[viewType];
|
||||
}
|
||||
|
||||
public static int itemTypeToViewType(ItemType itemType) {
|
||||
return itemType.ordinal();
|
||||
}
|
||||
}
|
||||
|
||||
public static class BasicItem extends CombinedHistoryItem {
|
||||
public BasicItem(View view) {
|
||||
super(view);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SmartFolder extends CombinedHistoryItem {
|
||||
final Context context;
|
||||
final ImageView icon;
|
||||
final TextView title;
|
||||
final TextView subtext;
|
||||
|
||||
public SmartFolder(View view) {
|
||||
super(view);
|
||||
context = view.getContext();
|
||||
|
||||
icon = (ImageView) view.findViewById(R.id.device_type);
|
||||
title = (TextView) view.findViewById(R.id.title);
|
||||
subtext = (TextView) view.findViewById(R.id.subtext);
|
||||
}
|
||||
|
||||
public void bind(int drawableRes, int titleRes, int singleDeviceRes, int multiDeviceRes, int numDevices) {
|
||||
icon.setImageResource(drawableRes);
|
||||
title.setText(titleRes);
|
||||
final String subtitle = numDevices == 1 ? context.getString(singleDeviceRes) : context.getString(multiDeviceRes, numDevices);
|
||||
subtext.setText(subtitle);
|
||||
}
|
||||
}
|
||||
|
||||
public static class HistoryItem extends CombinedHistoryItem {
|
||||
public HistoryItem(View view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
public void bind(Cursor historyCursor) {
|
||||
final TwoLinePageRow pageRow = (TwoLinePageRow) this.itemView;
|
||||
pageRow.setShowIcons(true);
|
||||
pageRow.updateFromCursor(historyCursor);
|
||||
}
|
||||
|
||||
public void bind(RemoteTab remoteTab) {
|
||||
final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
|
||||
childPageRow.setShowIcons(true);
|
||||
childPageRow.update(remoteTab.title, remoteTab.url);
|
||||
}
|
||||
|
||||
public void bind(ClosedTab closedTab) {
|
||||
final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
|
||||
childPageRow.setShowIcons(false);
|
||||
childPageRow.update(closedTab.title, closedTab.url);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ClientItem extends CombinedHistoryItem {
|
||||
final TextView nameView;
|
||||
final ImageView deviceTypeView;
|
||||
final TextView lastModifiedView;
|
||||
final ImageView deviceExpanded;
|
||||
|
||||
public ClientItem(View view) {
|
||||
super(view);
|
||||
nameView = (TextView) view.findViewById(R.id.client);
|
||||
deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
|
||||
lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
|
||||
deviceExpanded = (ImageView) view.findViewById(R.id.device_expanded);
|
||||
}
|
||||
|
||||
public void bind(Context context, RemoteClient client, boolean isCollapsed) {
|
||||
this.nameView.setText(client.name);
|
||||
final long now = System.currentTimeMillis();
|
||||
this.lastModifiedView.setText(ClientsAdapter.getLastSyncedString(context, now, client.lastModified));
|
||||
|
||||
if (client.isDesktop()) {
|
||||
deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_desktop_inactive : R.drawable.sync_desktop);
|
||||
} else {
|
||||
deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_mobile_inactive : R.drawable.sync_mobile);
|
||||
}
|
||||
|
||||
nameView.setTextColor(ContextCompat.getColor(context, isCollapsed ? R.color.tabs_tray_icon_grey : R.color.placeholder_active_grey));
|
||||
if (client.tabs.size() > 0) {
|
||||
deviceExpanded.setImageResource(isCollapsed ? R.drawable.home_group_collapsed : R.drawable.arrow_down);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,697 @@
|
|||
/* -*- 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.accounts.Account;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DefaultItemAnimator;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextPaint;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.RemoteClientsDialogFragment;
|
||||
import org.mozilla.gecko.fxa.FirefoxAccounts;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.fxa.SyncStatusListener;
|
||||
import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel;
|
||||
import org.mozilla.gecko.restrictions.Restrictions;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.restrictions.Restrictable;
|
||||
import org.mozilla.gecko.widget.HistoryDividerItemDecoration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT;
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_SYNC;
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS;
|
||||
|
||||
public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsDialogFragment.RemoteClientsListener {
|
||||
private static final String LOGTAG = "GeckoCombinedHistoryPnl";
|
||||
|
||||
private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "clients", "tabs" };
|
||||
private final int LOADER_ID_HISTORY = 0;
|
||||
private final int LOADER_ID_REMOTE = 1;
|
||||
|
||||
// String placeholders to mark formatting.
|
||||
private final static String FORMAT_S1 = "%1$s";
|
||||
private final static String FORMAT_S2 = "%2$s";
|
||||
|
||||
private CombinedHistoryRecyclerView mRecyclerView;
|
||||
private CombinedHistoryAdapter mHistoryAdapter;
|
||||
private ClientsAdapter mClientsAdapter;
|
||||
private RecentTabsAdapter mRecentTabsAdapter;
|
||||
private CursorLoaderCallbacks mCursorLoaderCallbacks;
|
||||
|
||||
private Bundle mSavedRestoreBundle;
|
||||
|
||||
private PanelLevel mPanelLevel;
|
||||
private Button mPanelFooterButton;
|
||||
|
||||
private PanelStateUpdateHandler mPanelStateUpdateHandler;
|
||||
|
||||
// Child refresh layout view.
|
||||
protected SwipeRefreshLayout mRefreshLayout;
|
||||
|
||||
// Sync listener that stops refreshing when a sync is completed.
|
||||
protected RemoteTabsSyncListener mSyncStatusListener;
|
||||
|
||||
// Reference to the View to display when there are no results.
|
||||
private View mHistoryEmptyView;
|
||||
private View mClientsEmptyView;
|
||||
private View mRecentTabsEmptyView;
|
||||
|
||||
public interface OnPanelLevelChangeListener {
|
||||
enum PanelLevel {
|
||||
PARENT, CHILD_SYNC, CHILD_RECENT_TABS
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagates level changes.
|
||||
* @param level
|
||||
* @return true if level changed, false otherwise.
|
||||
*/
|
||||
boolean changeLevel(PanelLevel level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstance) {
|
||||
super.onCreate(savedInstance);
|
||||
|
||||
int cachedRecentTabsCount = 0;
|
||||
if (mPanelStateChangeListener != null ) {
|
||||
cachedRecentTabsCount = mPanelStateChangeListener.getCachedRecentTabsCount();
|
||||
}
|
||||
mHistoryAdapter = new CombinedHistoryAdapter(getResources(), cachedRecentTabsCount);
|
||||
if (mPanelStateChangeListener != null) {
|
||||
mHistoryAdapter.setPanelStateChangeListener(mPanelStateChangeListener);
|
||||
}
|
||||
|
||||
mClientsAdapter = new ClientsAdapter(getContext());
|
||||
// The RecentTabsAdapter doesn't use a cursor and therefore can't use the CursorLoader's
|
||||
// onLoadFinished() callback for updating the panel state when the closed tab count changes.
|
||||
// Instead, we provide it with independent callbacks as necessary.
|
||||
mRecentTabsAdapter = new RecentTabsAdapter(getContext(),
|
||||
mHistoryAdapter.getRecentTabsUpdateHandler(), getPanelStateUpdateHandler());
|
||||
|
||||
mSyncStatusListener = new RemoteTabsSyncListener();
|
||||
FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.home_combined_history_panel, container, false);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view);
|
||||
setUpRecyclerView();
|
||||
|
||||
mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
|
||||
setUpRefreshLayout();
|
||||
|
||||
mClientsEmptyView = view.findViewById(R.id.home_clients_empty_view);
|
||||
mHistoryEmptyView = view.findViewById(R.id.home_history_empty_view);
|
||||
mRecentTabsEmptyView = view.findViewById(R.id.home_recent_tabs_empty_view);
|
||||
setUpEmptyViews();
|
||||
|
||||
mPanelFooterButton = (Button) view.findViewById(R.id.history_panel_footer_button);
|
||||
mPanelFooterButton.setText(R.string.home_clear_history_button);
|
||||
mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
|
||||
|
||||
mRecentTabsAdapter.startListeningForClosedTabs();
|
||||
mRecentTabsAdapter.startListeningForHistorySanitize();
|
||||
|
||||
if (mSavedRestoreBundle != null) {
|
||||
setPanelStateFromBundle(mSavedRestoreBundle);
|
||||
mSavedRestoreBundle = null;
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void setUpRecyclerView() {
|
||||
if (mPanelLevel == null) {
|
||||
mPanelLevel = PARENT;
|
||||
}
|
||||
|
||||
mRecyclerView.setAdapter(mPanelLevel == PARENT ? mHistoryAdapter :
|
||||
mPanelLevel == CHILD_SYNC ? mClientsAdapter : mRecentTabsAdapter);
|
||||
|
||||
final RecyclerView.ItemAnimator animator = new DefaultItemAnimator();
|
||||
animator.setAddDuration(100);
|
||||
animator.setChangeDuration(100);
|
||||
animator.setMoveDuration(100);
|
||||
animator.setRemoveDuration(100);
|
||||
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
mHistoryAdapter.setLinearLayoutManager((LinearLayoutManager) mRecyclerView.getLayoutManager());
|
||||
mRecyclerView.setItemAnimator(animator);
|
||||
mRecyclerView.addItemDecoration(new HistoryDividerItemDecoration(getContext()));
|
||||
mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener);
|
||||
mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener());
|
||||
mRecyclerView.setHiddenClientsDialogBuilder(new HiddenClientsHelper());
|
||||
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
final LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager();
|
||||
if ((mPanelLevel == PARENT) && (llm.findLastCompletelyVisibleItemPosition() == HistoryCursorLoader.HISTORY_LIMIT)) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.LIST, "history_scroll_max");
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
registerForContextMenu(mRecyclerView);
|
||||
}
|
||||
|
||||
private void setUpRefreshLayout() {
|
||||
mRefreshLayout.setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange);
|
||||
mRefreshLayout.setOnRefreshListener(new RemoteTabsRefreshListener());
|
||||
mRefreshLayout.setEnabled(false);
|
||||
}
|
||||
|
||||
private void setUpEmptyViews() {
|
||||
// Set up history empty view.
|
||||
final ImageView historyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image);
|
||||
historyIcon.setVisibility(View.GONE);
|
||||
|
||||
final TextView historyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text);
|
||||
historyText.setText(R.string.home_most_recent_empty);
|
||||
|
||||
final TextView historyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint);
|
||||
|
||||
if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
|
||||
historyHint.setVisibility(View.GONE);
|
||||
} else {
|
||||
final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
|
||||
final SpannableStringBuilder hintBuilder = formatHintText(hintText);
|
||||
if (hintBuilder != null) {
|
||||
historyHint.setText(hintBuilder);
|
||||
historyHint.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
historyHint.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up Clients empty view.
|
||||
final Button syncSetupButton = (Button) mClientsEmptyView.findViewById(R.id.sync_setup_button);
|
||||
syncSetupButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "history_syncsetup");
|
||||
// This Activity will redirect to the correct Activity as needed.
|
||||
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up Recent Tabs empty view.
|
||||
final ImageView recentTabsIcon = (ImageView) mRecentTabsEmptyView.findViewById(R.id.home_empty_image);
|
||||
recentTabsIcon.setImageResource(R.drawable.icon_remote_tabs_empty);
|
||||
|
||||
final TextView recentTabsText = (TextView) mRecentTabsEmptyView.findViewById(R.id.home_empty_text);
|
||||
recentTabsText.setText(R.string.home_last_tabs_empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPanelStateChangeListener(
|
||||
PanelStateChangeListener panelStateChangeListener) {
|
||||
super.setPanelStateChangeListener(panelStateChangeListener);
|
||||
if (mHistoryAdapter != null) {
|
||||
mHistoryAdapter.setPanelStateChangeListener(panelStateChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreData(Bundle data) {
|
||||
if (mRecyclerView != null) {
|
||||
setPanelStateFromBundle(data);
|
||||
} else {
|
||||
mSavedRestoreBundle = data;
|
||||
}
|
||||
}
|
||||
|
||||
private void setPanelStateFromBundle(Bundle data) {
|
||||
if (data != null && data.getBoolean("goToRecentTabs", false) && mPanelLevel != CHILD_RECENT_TABS) {
|
||||
mPanelLevel = CHILD_RECENT_TABS;
|
||||
mRecyclerView.swapAdapter(mRecentTabsAdapter, true);
|
||||
updateEmptyView(CHILD_RECENT_TABS);
|
||||
updateButtonFromLevel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
|
||||
getLoaderManager().initLoader(LOADER_ID_REMOTE, null, mCursorLoaderCallbacks);
|
||||
}
|
||||
|
||||
private static class RemoteTabsCursorLoader extends SimpleCursorLoader {
|
||||
private final GeckoProfile mProfile;
|
||||
|
||||
public RemoteTabsCursorLoader(Context context) {
|
||||
super(context);
|
||||
mProfile = GeckoProfile.get(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor loadCursor() {
|
||||
return BrowserDB.from(mProfile).getTabsAccessor().getRemoteTabsCursor(getContext());
|
||||
}
|
||||
}
|
||||
|
||||
private static class HistoryCursorLoader extends SimpleCursorLoader {
|
||||
// Max number of history results
|
||||
public static final int HISTORY_LIMIT = 100;
|
||||
private final BrowserDB mDB;
|
||||
|
||||
public HistoryCursorLoader(Context context) {
|
||||
super(context);
|
||||
mDB = BrowserDB.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor loadCursor() {
|
||||
final ContentResolver cr = getContext().getContentResolver();
|
||||
return mDB.getRecentHistory(cr, HISTORY_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
private BrowserDB mDB; // Pseudo-final: set in onCreateLoader.
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
if (mDB == null) {
|
||||
mDB = BrowserDB.from(getActivity());
|
||||
}
|
||||
|
||||
switch (id) {
|
||||
case LOADER_ID_HISTORY:
|
||||
return new HistoryCursorLoader(getContext());
|
||||
case LOADER_ID_REMOTE:
|
||||
return new RemoteTabsCursorLoader(getContext());
|
||||
default:
|
||||
Log.e(LOGTAG, "Unknown loader id!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
||||
final int loaderId = loader.getId();
|
||||
switch (loaderId) {
|
||||
case LOADER_ID_HISTORY:
|
||||
mHistoryAdapter.setHistory(c);
|
||||
updateEmptyView(PARENT);
|
||||
break;
|
||||
|
||||
case LOADER_ID_REMOTE:
|
||||
final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
|
||||
mHistoryAdapter.getDeviceUpdateHandler().onDeviceCountUpdated(clients.size());
|
||||
mClientsAdapter.setClients(clients);
|
||||
updateEmptyView(CHILD_SYNC);
|
||||
break;
|
||||
}
|
||||
|
||||
updateButtonFromLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
mClientsAdapter.setClients(Collections.<RemoteClient>emptyList());
|
||||
mHistoryAdapter.setHistory(null);
|
||||
}
|
||||
}
|
||||
|
||||
public interface PanelStateUpdateHandler {
|
||||
void onPanelStateUpdated(PanelLevel level);
|
||||
}
|
||||
|
||||
public PanelStateUpdateHandler getPanelStateUpdateHandler() {
|
||||
if (mPanelStateUpdateHandler == null) {
|
||||
mPanelStateUpdateHandler = new PanelStateUpdateHandler() {
|
||||
@Override
|
||||
public void onPanelStateUpdated(PanelLevel level) {
|
||||
updateEmptyView(level);
|
||||
updateButtonFromLevel();
|
||||
}
|
||||
};
|
||||
}
|
||||
return mPanelStateUpdateHandler;
|
||||
}
|
||||
|
||||
protected class OnLevelChangeListener implements OnPanelLevelChangeListener {
|
||||
@Override
|
||||
public boolean changeLevel(PanelLevel level) {
|
||||
if (level == mPanelLevel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mPanelLevel = level;
|
||||
switch (level) {
|
||||
case PARENT:
|
||||
mRecyclerView.swapAdapter(mHistoryAdapter, true);
|
||||
mRefreshLayout.setEnabled(false);
|
||||
break;
|
||||
case CHILD_SYNC:
|
||||
mRecyclerView.swapAdapter(mClientsAdapter, true);
|
||||
mRefreshLayout.setEnabled(mClientsAdapter.getClientsCount() > 0);
|
||||
break;
|
||||
case CHILD_RECENT_TABS:
|
||||
mRecyclerView.swapAdapter(mRecentTabsAdapter, true);
|
||||
break;
|
||||
}
|
||||
|
||||
updateEmptyView(level);
|
||||
updateButtonFromLevel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateButtonFromLevel() {
|
||||
switch (mPanelLevel) {
|
||||
case PARENT:
|
||||
final boolean historyRestricted = !Restrictions.isAllowed(getActivity(), Restrictable.CLEAR_HISTORY);
|
||||
if (historyRestricted || mHistoryAdapter.getItemCount() == mHistoryAdapter.getNumVisibleSmartFolders()) {
|
||||
mPanelFooterButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
mPanelFooterButton.setText(R.string.home_clear_history_button);
|
||||
mPanelFooterButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
break;
|
||||
case CHILD_RECENT_TABS:
|
||||
if (mRecentTabsAdapter.getClosedTabsCount() > 1) {
|
||||
mPanelFooterButton.setText(R.string.home_restore_all);
|
||||
mPanelFooterButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mPanelFooterButton.setVisibility(View.GONE);
|
||||
}
|
||||
break;
|
||||
case CHILD_SYNC:
|
||||
mPanelFooterButton.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class OnFooterButtonClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
switch (mPanelLevel) {
|
||||
case PARENT:
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
|
||||
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
|
||||
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final DialogInterface dialog, final int which) {
|
||||
dialog.dismiss();
|
||||
|
||||
// Send message to Java to clear history.
|
||||
final JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("history", true);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "JSON error", e);
|
||||
}
|
||||
|
||||
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
|
||||
}
|
||||
});
|
||||
|
||||
dialogBuilder.show();
|
||||
break;
|
||||
case CHILD_RECENT_TABS:
|
||||
final String telemetryExtra = mRecentTabsAdapter.restoreAllTabs();
|
||||
if (telemetryExtra != null) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON, telemetryExtra);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEmptyView(PanelLevel level) {
|
||||
boolean showEmptyHistoryView = (mPanelLevel == PARENT && mHistoryEmptyView.isShown());
|
||||
boolean showEmptyClientsView = (mPanelLevel == CHILD_SYNC && mClientsEmptyView.isShown());
|
||||
boolean showEmptyRecentTabsView = (mPanelLevel == CHILD_RECENT_TABS && mRecentTabsEmptyView.isShown());
|
||||
|
||||
if (mPanelLevel == level) {
|
||||
switch (mPanelLevel) {
|
||||
case PARENT:
|
||||
showEmptyHistoryView = mHistoryAdapter.getItemCount() == mHistoryAdapter.getNumVisibleSmartFolders();
|
||||
break;
|
||||
|
||||
case CHILD_SYNC:
|
||||
showEmptyClientsView = mClientsAdapter.getItemCount() == 1;
|
||||
break;
|
||||
|
||||
case CHILD_RECENT_TABS:
|
||||
showEmptyRecentTabsView = mRecentTabsAdapter.getClosedTabsCount() == 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView || showEmptyRecentTabsView;
|
||||
mRecyclerView.setOverScrollMode(showEmptyView ? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS);
|
||||
|
||||
mHistoryEmptyView.setVisibility(showEmptyHistoryView ? View.VISIBLE : View.GONE);
|
||||
mClientsEmptyView.setVisibility(showEmptyClientsView ? View.VISIBLE : View.GONE);
|
||||
mRecentTabsEmptyView.setVisibility(showEmptyRecentTabsView ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make Span that is clickable, and underlined
|
||||
* between the string markers <code>FORMAT_S1</code> and
|
||||
* <code>FORMAT_S2</code>.
|
||||
*
|
||||
* @param text String to format
|
||||
* @return formatted SpannableStringBuilder, or null if there
|
||||
* is not any text to format.
|
||||
*/
|
||||
private SpannableStringBuilder formatHintText(String text) {
|
||||
// Set formatting as marked by string placeholders.
|
||||
final int underlineStart = text.indexOf(FORMAT_S1);
|
||||
final int underlineEnd = text.indexOf(FORMAT_S2);
|
||||
|
||||
// Check that there is text to be formatted.
|
||||
if (underlineStart >= underlineEnd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SpannableStringBuilder ssb = new SpannableStringBuilder(text);
|
||||
|
||||
// Set clickable text.
|
||||
final ClickableSpan clickableSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "hint_private_browsing");
|
||||
try {
|
||||
final JSONObject json = new JSONObject();
|
||||
json.put("type", "Menu:Open");
|
||||
GeckoApp.getEventDispatcher().dispatchEvent(json, null);
|
||||
EventDispatcher.getInstance().dispatchEvent(json, null);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error forming JSON for Private Browsing contextual hint", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ssb.setSpan(clickableSpan, 0, text.length(), 0);
|
||||
|
||||
// Remove underlining set by ClickableSpan.
|
||||
final UnderlineSpan noUnderlineSpan = new UnderlineSpan() {
|
||||
@Override
|
||||
public void updateDrawState(TextPaint textPaint) {
|
||||
textPaint.setUnderlineText(false);
|
||||
}
|
||||
};
|
||||
|
||||
ssb.setSpan(noUnderlineSpan, 0, text.length(), 0);
|
||||
|
||||
// Add underlining for "Private Browsing".
|
||||
ssb.setSpan(new UnderlineSpan(), underlineStart, underlineEnd, 0);
|
||||
|
||||
ssb.delete(underlineEnd, underlineEnd + FORMAT_S2.length());
|
||||
ssb.delete(underlineStart, underlineStart + FORMAT_S1.length());
|
||||
|
||||
return ssb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
if (!(menuInfo instanceof RemoteTabsClientContextMenuInfo)) {
|
||||
// Long pressed item was not a RemoteTabsGroup item. Superclass
|
||||
// can handle this.
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
// Long pressed item was a remote client; provide the appropriate menu.
|
||||
final MenuInflater inflater = new MenuInflater(view.getContext());
|
||||
inflater.inflate(R.menu.home_remote_tabs_client_contextmenu, menu);
|
||||
|
||||
final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo;
|
||||
menu.setHeaderTitle(info.client.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
if (super.onContextItemSelected(item)) {
|
||||
// HomeFragment was able to handle to selected item.
|
||||
return true;
|
||||
}
|
||||
|
||||
final ContextMenu.ContextMenuInfo menuInfo = item.getMenuInfo();
|
||||
if (!(menuInfo instanceof RemoteTabsClientContextMenuInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo;
|
||||
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.home_remote_tabs_hide_client) {
|
||||
mClientsAdapter.removeItem(info.position);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
interface DialogBuilder<E> {
|
||||
void createAndShowDialog(List<E> items);
|
||||
}
|
||||
|
||||
protected class HiddenClientsHelper implements DialogBuilder<RemoteClient> {
|
||||
@Override
|
||||
public void createAndShowDialog(List<RemoteClient> clientsList) {
|
||||
final RemoteClientsDialogFragment dialog = RemoteClientsDialogFragment.newInstance(
|
||||
getResources().getString(R.string.home_remote_tabs_hidden_devices_title),
|
||||
getResources().getString(R.string.home_remote_tabs_unhide_selected_devices),
|
||||
RemoteClientsDialogFragment.ChoiceMode.MULTIPLE, new ArrayList<>(clientsList));
|
||||
dialog.setTargetFragment(CombinedHistoryPanel.this, 0);
|
||||
dialog.show(getActivity().getSupportFragmentManager(), "show-clients");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClients(List<RemoteClient> clients) {
|
||||
mClientsAdapter.unhideClients(clients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores information regarding the creation of the context menu for a remote client.
|
||||
*/
|
||||
protected static class RemoteTabsClientContextMenuInfo extends HomeContextMenuInfo {
|
||||
protected final RemoteClient client;
|
||||
|
||||
public RemoteTabsClientContextMenuInfo(View targetView, int position, long id, RemoteClient client) {
|
||||
super(targetView, position, id);
|
||||
this.client = client;
|
||||
}
|
||||
}
|
||||
|
||||
protected class RemoteTabsRefreshListener implements SwipeRefreshLayout.OnRefreshListener {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (FirefoxAccounts.firefoxAccountsExist(getActivity())) {
|
||||
final Account account = FirefoxAccounts.getFirefoxAccount(getActivity());
|
||||
FirefoxAccounts.requestImmediateSync(account, STAGES_TO_SYNC_ON_REFRESH, null);
|
||||
} else {
|
||||
Log.wtf(LOGTAG, "No Firefox Account found; this should never happen. Ignoring.");
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class RemoteTabsSyncListener implements SyncStatusListener {
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getAccount() {
|
||||
return FirefoxAccounts.getFirefoxAccount(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSyncStarted() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSyncFinished() {
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
mRecentTabsAdapter.stopListeningForClosedTabs();
|
||||
mRecentTabsAdapter.stopListeningForHistorySanitize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (mSyncStatusListener != null) {
|
||||
FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener);
|
||||
mSyncStatusListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/* -*- 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.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import org.mozilla.gecko.db.RemoteClient;
|
||||
import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS;
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_SYNC;
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT;
|
||||
|
||||
public class CombinedHistoryRecyclerView extends RecyclerView
|
||||
implements RecyclerViewClickSupport.OnItemClickListener, RecyclerViewClickSupport.OnItemLongClickListener {
|
||||
public static String LOGTAG = "CombinedHistoryRecycView";
|
||||
|
||||
protected interface AdapterContextMenuBuilder {
|
||||
HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position);
|
||||
}
|
||||
|
||||
protected HomePager.OnUrlOpenListener mOnUrlOpenListener;
|
||||
protected OnPanelLevelChangeListener mOnPanelLevelChangeListener;
|
||||
protected CombinedHistoryPanel.DialogBuilder<RemoteClient> mDialogBuilder;
|
||||
protected HomeContextMenuInfo mContextMenuInfo;
|
||||
|
||||
public CombinedHistoryRecyclerView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public CombinedHistoryRecyclerView(Context context, AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public CombinedHistoryRecyclerView(Context context, AttributeSet attributeSet, int defStyle) {
|
||||
super(context, attributeSet, defStyle);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
|
||||
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
setLayoutManager(layoutManager);
|
||||
|
||||
RecyclerViewClickSupport.addTo(this)
|
||||
.setOnItemClickListener(this)
|
||||
.setOnItemLongClickListener(this);
|
||||
|
||||
setOnKeyListener(new View.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
final int action = event.getAction();
|
||||
|
||||
// If the user hit the BACK key, try to move to the parent folder.
|
||||
if (action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
return mOnPanelLevelChangeListener.changeLevel(PARENT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setOnHistoryClickedListener(HomePager.OnUrlOpenListener listener) {
|
||||
this.mOnUrlOpenListener = listener;
|
||||
}
|
||||
|
||||
public void setOnPanelLevelChangeListener(OnPanelLevelChangeListener listener) {
|
||||
this.mOnPanelLevelChangeListener = listener;
|
||||
}
|
||||
|
||||
public void setHiddenClientsDialogBuilder(CombinedHistoryPanel.DialogBuilder<RemoteClient> builder) {
|
||||
mDialogBuilder = builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||
final int viewType = getAdapter().getItemViewType(position);
|
||||
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
|
||||
final String telemetryExtra;
|
||||
|
||||
switch (itemType) {
|
||||
case RECENT_TABS:
|
||||
mOnPanelLevelChangeListener.changeLevel(CHILD_RECENT_TABS);
|
||||
break;
|
||||
|
||||
case SYNCED_DEVICES:
|
||||
mOnPanelLevelChangeListener.changeLevel(CHILD_SYNC);
|
||||
break;
|
||||
|
||||
case CLIENT:
|
||||
((ClientsAdapter) getAdapter()).toggleClient(position);
|
||||
break;
|
||||
|
||||
case HIDDEN_DEVICES:
|
||||
if (mDialogBuilder != null) {
|
||||
mDialogBuilder.createAndShowDialog(((ClientsAdapter) getAdapter()).getHiddenClients());
|
||||
}
|
||||
break;
|
||||
|
||||
case NAVIGATION_BACK:
|
||||
mOnPanelLevelChangeListener.changeLevel(PARENT);
|
||||
break;
|
||||
|
||||
case CHILD:
|
||||
case HISTORY:
|
||||
if (mOnUrlOpenListener != null) {
|
||||
final TwoLinePageRow historyItem = (TwoLinePageRow) v;
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "history");
|
||||
mOnUrlOpenListener.onUrlOpen(historyItem.getUrl(), EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
|
||||
}
|
||||
break;
|
||||
|
||||
case CLOSED_TAB:
|
||||
telemetryExtra = ((RecentTabsAdapter) getAdapter()).restoreTabFromPosition(position);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, telemetryExtra);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
|
||||
mContextMenuInfo = ((AdapterContextMenuBuilder) getAdapter()).makeContextMenuInfoFromPosition(v, position);
|
||||
return showContextMenuForChild(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeContextMenuInfo getContextMenuInfo() {
|
||||
return mContextMenuInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
/* -*- 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.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserContract.HomeItems;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
import org.mozilla.gecko.home.PanelLayout.ContextMenuRegistry;
|
||||
import org.mozilla.gecko.home.PanelLayout.DatasetHandler;
|
||||
import org.mozilla.gecko.home.PanelLayout.DatasetRequest;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
import org.mozilla.gecko.util.UIAsyncTask;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
/**
|
||||
* Fragment that displays dynamic content specified by a {@code PanelConfig}.
|
||||
* The {@code DynamicPanel} UI is built based on the given {@code LayoutType}
|
||||
* and its associated list of {@code ViewConfig}.
|
||||
*
|
||||
* {@code DynamicPanel} manages all necessary Loaders to load panel datasets
|
||||
* from their respective content providers. Each panel dataset has its own
|
||||
* associated Loader. This is enforced by defining the Loader IDs based on
|
||||
* their associated dataset IDs.
|
||||
*
|
||||
* The {@code PanelLayout} can make load and reset requests on datasets via
|
||||
* the provided {@code DatasetHandler}. This way it doesn't need to know the
|
||||
* details of how datasets are loaded and reset. Each time a dataset is
|
||||
* requested, {@code DynamicPanel} restarts a Loader with the respective ID (see
|
||||
* {@code PanelDatasetHandler}).
|
||||
*
|
||||
* See {@code PanelLayout} for more details on how {@code DynamicPanel}
|
||||
* receives dataset requests and delivers them back to the {@code PanelLayout}.
|
||||
*/
|
||||
public class DynamicPanel extends HomeFragment {
|
||||
private static final String LOGTAG = "GeckoDynamicPanel";
|
||||
|
||||
// Dataset ID to be used by the loader
|
||||
private static final String DATASET_REQUEST = "dataset_request";
|
||||
|
||||
// Max number of items to display in the panel
|
||||
private static final int RESULT_LIMIT = 100;
|
||||
|
||||
// The main view for this fragment. This contains the PanelLayout and PanelAuthLayout.
|
||||
private FrameLayout mView;
|
||||
|
||||
// The panel layout associated with this panel
|
||||
private PanelLayout mPanelLayout;
|
||||
|
||||
// The layout used to show authentication UI for this panel
|
||||
private PanelAuthLayout mPanelAuthLayout;
|
||||
|
||||
// Cache used to keep track of whether or not the user has been authenticated.
|
||||
private PanelAuthCache mPanelAuthCache;
|
||||
|
||||
// Hold a reference to the UiAsyncTask we use to check the state of the
|
||||
// PanelAuthCache, so that we can cancel it if necessary.
|
||||
private UIAsyncTask.WithoutParams<Boolean> mAuthStateTask;
|
||||
|
||||
// The configuration associated with this panel
|
||||
private PanelConfig mPanelConfig;
|
||||
|
||||
// Callbacks used for the loader
|
||||
private PanelLoaderCallbacks mLoaderCallbacks;
|
||||
|
||||
// The current UI mode in the fragment
|
||||
private UIMode mUIMode;
|
||||
|
||||
/*
|
||||
* Different UI modes to display depending on the authentication state.
|
||||
*
|
||||
* PANEL: Layout to display panel data.
|
||||
* AUTH: Authentication UI.
|
||||
*/
|
||||
private enum UIMode {
|
||||
PANEL,
|
||||
AUTH
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final Bundle args = getArguments();
|
||||
if (args != null) {
|
||||
mPanelConfig = (PanelConfig) args.getParcelable(HomePager.PANEL_CONFIG_ARG);
|
||||
}
|
||||
|
||||
if (mPanelConfig == null) {
|
||||
throw new IllegalStateException("Can't create a DynamicPanel without a PanelConfig");
|
||||
}
|
||||
|
||||
mPanelAuthCache = new PanelAuthCache(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
mView = new FrameLayout(getActivity());
|
||||
return mView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
// Restore whatever the UI mode the fragment had before
|
||||
// a device rotation.
|
||||
if (mUIMode != null) {
|
||||
setUIMode(mUIMode);
|
||||
}
|
||||
|
||||
mPanelAuthCache.setOnChangeListener(new PanelAuthChangeListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
mView = null;
|
||||
mPanelLayout = null;
|
||||
mPanelAuthLayout = null;
|
||||
|
||||
mPanelAuthCache.setOnChangeListener(null);
|
||||
|
||||
if (mAuthStateTask != null) {
|
||||
mAuthStateTask.cancel();
|
||||
mAuthStateTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
// Create callbacks before the initial loader is started.
|
||||
mLoaderCallbacks = new PanelLoaderCallbacks();
|
||||
loadIfVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
Log.d(LOGTAG, "Loading layout");
|
||||
|
||||
if (requiresAuth()) {
|
||||
mAuthStateTask = new UIAsyncTask.WithoutParams<Boolean>(ThreadUtils.getBackgroundHandler()) {
|
||||
@Override
|
||||
public synchronized Boolean doInBackground() {
|
||||
return mPanelAuthCache.isAuthenticated(mPanelConfig.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Boolean isAuthenticated) {
|
||||
mAuthStateTask = null;
|
||||
setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH);
|
||||
}
|
||||
};
|
||||
mAuthStateTask.execute();
|
||||
} else {
|
||||
setUIMode(UIMode.PANEL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this panel requires authentication.
|
||||
*/
|
||||
private boolean requiresAuth() {
|
||||
return mPanelConfig.getAuthConfig() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily creates layout for panel data.
|
||||
*/
|
||||
private void createPanelLayout() {
|
||||
final ContextMenuRegistry contextMenuRegistry = new ContextMenuRegistry() {
|
||||
@Override
|
||||
public void register(View view) {
|
||||
registerForContextMenu(view);
|
||||
}
|
||||
};
|
||||
|
||||
switch (mPanelConfig.getLayoutType()) {
|
||||
case FRAME:
|
||||
final PanelDatasetHandler datasetHandler = new PanelDatasetHandler();
|
||||
mPanelLayout = new FramePanelLayout(getActivity(), mPanelConfig, datasetHandler,
|
||||
mUrlOpenListener, contextMenuRegistry);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized layout type in DynamicPanel");
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "Created layout of type: " + mPanelConfig.getLayoutType());
|
||||
mView.addView(mPanelLayout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily creates layout for authentication UI.
|
||||
*/
|
||||
private void createPanelAuthLayout() {
|
||||
mPanelAuthLayout = new PanelAuthLayout(getActivity(), mPanelConfig);
|
||||
mView.addView(mPanelAuthLayout, 0);
|
||||
}
|
||||
|
||||
private void setUIMode(UIMode mode) {
|
||||
switch (mode) {
|
||||
case PANEL:
|
||||
if (mPanelAuthLayout != null) {
|
||||
mPanelAuthLayout.setVisibility(View.GONE);
|
||||
}
|
||||
if (mPanelLayout == null) {
|
||||
createPanelLayout();
|
||||
}
|
||||
mPanelLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
// Only trigger a reload if the UI mode has changed
|
||||
// (e.g. auth cache changes) and the fragment is allowed
|
||||
// to load its contents. Any loaders associated with the
|
||||
// panel layout will be automatically re-bound after a
|
||||
// device rotation, no need to explicitly load it again.
|
||||
if (mUIMode != mode && canLoad()) {
|
||||
mPanelLayout.load();
|
||||
}
|
||||
break;
|
||||
|
||||
case AUTH:
|
||||
if (mPanelLayout != null) {
|
||||
mPanelLayout.setVisibility(View.GONE);
|
||||
}
|
||||
if (mPanelAuthLayout == null) {
|
||||
createPanelAuthLayout();
|
||||
}
|
||||
mPanelAuthLayout.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized UIMode in DynamicPanel");
|
||||
}
|
||||
|
||||
mUIMode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the PanelLayout to make load and reset requests to
|
||||
* the holding fragment.
|
||||
*/
|
||||
private class PanelDatasetHandler implements DatasetHandler {
|
||||
@Override
|
||||
public void requestDataset(DatasetRequest request) {
|
||||
Log.d(LOGTAG, "Requesting request: " + request);
|
||||
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(DATASET_REQUEST, request);
|
||||
|
||||
getLoaderManager().restartLoader(request.getViewIndex(),
|
||||
bundle, mLoaderCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetDataset(int viewIndex) {
|
||||
Log.d(LOGTAG, "Resetting dataset: " + viewIndex);
|
||||
|
||||
final LoaderManager lm = getLoaderManager();
|
||||
|
||||
// Release any resources associated with the dataset if
|
||||
// it's currently loaded in memory.
|
||||
final Loader<?> datasetLoader = lm.getLoader(viewIndex);
|
||||
if (datasetLoader != null) {
|
||||
datasetLoader.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor loader for the panel datasets.
|
||||
*/
|
||||
private static class PanelDatasetLoader extends SimpleCursorLoader {
|
||||
private DatasetRequest mRequest;
|
||||
|
||||
public PanelDatasetLoader(Context context, DatasetRequest request) {
|
||||
super(context);
|
||||
mRequest = request;
|
||||
}
|
||||
|
||||
public DatasetRequest getRequest() {
|
||||
return mRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentChanged() {
|
||||
// Ensure the refresh request doesn't affect the view's filter
|
||||
// stack (i.e. use DATASET_LOAD type) but keep the current
|
||||
// dataset ID and filter.
|
||||
final DatasetRequest newRequest =
|
||||
new DatasetRequest(mRequest.getViewIndex(),
|
||||
DatasetRequest.Type.DATASET_LOAD,
|
||||
mRequest.getDatasetId(),
|
||||
mRequest.getFilterDetail());
|
||||
|
||||
mRequest = newRequest;
|
||||
super.onContentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor loadCursor() {
|
||||
final ContentResolver cr = getContext().getContentResolver();
|
||||
|
||||
final String selection;
|
||||
final String[] selectionArgs;
|
||||
|
||||
// Null represents the root filter
|
||||
if (mRequest.getFilter() == null) {
|
||||
selection = HomeItems.FILTER + " IS NULL";
|
||||
selectionArgs = null;
|
||||
} else {
|
||||
selection = HomeItems.FILTER + " = ?";
|
||||
selectionArgs = new String[] { mRequest.getFilter() };
|
||||
}
|
||||
|
||||
final Uri queryUri = HomeItems.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(BrowserContract.PARAM_DATASET_ID,
|
||||
mRequest.getDatasetId())
|
||||
.appendQueryParameter(BrowserContract.PARAM_LIMIT,
|
||||
String.valueOf(RESULT_LIMIT))
|
||||
.build();
|
||||
|
||||
// XXX: You can use HomeItems.CONTENT_FAKE_URI for development
|
||||
// to pull items from fake_home_items.json.
|
||||
return cr.query(queryUri, null, selection, selectionArgs, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LoaderCallbacks implementation that interacts with the LoaderManager.
|
||||
*/
|
||||
private class PanelLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST);
|
||||
|
||||
Log.d(LOGTAG, "Creating loader for request: " + request);
|
||||
return new PanelDatasetLoader(getActivity(), request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
final DatasetRequest request = getRequestFromLoader(loader);
|
||||
Log.d(LOGTAG, "Finished loader for request: " + request);
|
||||
|
||||
if (mPanelLayout != null) {
|
||||
mPanelLayout.deliverDataset(request, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
final DatasetRequest request = getRequestFromLoader(loader);
|
||||
Log.d(LOGTAG, "Resetting loader for request: " + request);
|
||||
|
||||
if (mPanelLayout != null) {
|
||||
mPanelLayout.releaseDataset(request.getViewIndex());
|
||||
}
|
||||
}
|
||||
|
||||
private DatasetRequest getRequestFromLoader(Loader<Cursor> loader) {
|
||||
final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader;
|
||||
return datasetLoader.getRequest();
|
||||
}
|
||||
}
|
||||
|
||||
private class PanelAuthChangeListener implements PanelAuthCache.OnChangeListener {
|
||||
@Override
|
||||
public void onChange(String panelId, boolean isAuthenticated) {
|
||||
if (!mPanelConfig.getId().equals(panelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUIMode(isAuthenticated ? UIMode.PANEL : UIMode.AUTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/* -*- 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.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
import org.mozilla.gecko.home.HomeConfig.ViewConfig;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
class FramePanelLayout extends PanelLayout {
|
||||
private static final String LOGTAG = "GeckoFramePanelLayout";
|
||||
|
||||
private final View mChildView;
|
||||
private final ViewConfig mChildConfig;
|
||||
|
||||
public FramePanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler,
|
||||
OnUrlOpenListener urlOpenListener, ContextMenuRegistry contextMenuRegistry) {
|
||||
super(context, panelConfig, datasetHandler, urlOpenListener, contextMenuRegistry);
|
||||
|
||||
// This layout can only hold one view so we simply
|
||||
// take the first defined view from PanelConfig.
|
||||
mChildConfig = panelConfig.getViewAt(0);
|
||||
if (mChildConfig == null) {
|
||||
throw new IllegalStateException("FramePanelLayout requires a view in PanelConfig");
|
||||
}
|
||||
|
||||
mChildView = createPanelView(mChildConfig);
|
||||
addView(mChildView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
Log.d(LOGTAG, "Loading");
|
||||
|
||||
if (mChildView instanceof DatasetBacked) {
|
||||
final FilterDetail filter = new FilterDetail(mChildConfig.getFilter(), null);
|
||||
|
||||
final DatasetRequest request = new DatasetRequest(mChildConfig.getIndex(),
|
||||
mChildConfig.getDatasetId(),
|
||||
filter);
|
||||
|
||||
Log.d(LOGTAG, "Requesting child request: " + request);
|
||||
requestDataset(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/* -*- 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.res.Resources;
|
||||
|
||||
import org.mozilla.gecko.home.CombinedHistoryAdapter.SectionHeader;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
public class HistorySectionsHelper {
|
||||
|
||||
// Constants for different time sections.
|
||||
private static final long MS_PER_DAY = 86400000;
|
||||
private static final long MS_PER_WEEK = MS_PER_DAY * 7;
|
||||
|
||||
public static class SectionDateRange {
|
||||
public final long start;
|
||||
public final long end;
|
||||
public final String displayName;
|
||||
|
||||
private SectionDateRange(long start, long end, String displayName) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the time range in milliseconds covered by each section header and sets the title.
|
||||
* @param res Resources for fetching string names
|
||||
* @param sectionsArray Array of section bookkeeping objects
|
||||
*/
|
||||
public static void updateRecentSectionOffset(final Resources res, SectionDateRange[] sectionsArray) {
|
||||
final long now = System.currentTimeMillis();
|
||||
final Calendar cal = Calendar.getInstance();
|
||||
|
||||
// Update calendar to this day.
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0);
|
||||
cal.set(Calendar.MILLISECOND, 1);
|
||||
final long currentDayMS = cal.getTimeInMillis();
|
||||
|
||||
// Calculate the start and end time for each section header and set its display text.
|
||||
sectionsArray[SectionHeader.TODAY.ordinal()] =
|
||||
new SectionDateRange(currentDayMS, now, res.getString(R.string.history_today_section));
|
||||
|
||||
sectionsArray[SectionHeader.YESTERDAY.ordinal()] =
|
||||
new SectionDateRange(currentDayMS - MS_PER_DAY, currentDayMS, res.getString(R.string.history_yesterday_section));
|
||||
|
||||
sectionsArray[SectionHeader.WEEK.ordinal()] =
|
||||
new SectionDateRange(currentDayMS - MS_PER_WEEK, now, res.getString(R.string.history_week_section));
|
||||
|
||||
// Update the calendar to beginning of next month to avoid problems calculating the last day of this month.
|
||||
cal.add(Calendar.MONTH, 1);
|
||||
cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH));
|
||||
|
||||
// Iterate over the remaining history sections, moving backwards in time.
|
||||
for (int i = SectionHeader.THIS_MONTH.ordinal(); i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) {
|
||||
final long end = cal.getTimeInMillis();
|
||||
|
||||
cal.add(Calendar.MONTH, -1);
|
||||
final long start = cal.getTimeInMillis();
|
||||
|
||||
final String displayName = cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault());
|
||||
|
||||
sectionsArray[i] = new SectionDateRange(start, end, displayName);
|
||||
}
|
||||
|
||||
sectionsArray[SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal()] =
|
||||
new SectionDateRange(0L, cal.getTimeInMillis(), res.getString(R.string.history_older_section));
|
||||
}
|
||||
}
|
||||
224
mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
Normal file
224
mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/* -*- 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.activitystream.ActivityStream;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelType;
|
||||
import org.mozilla.gecko.home.activitystream.ActivityStreamHomeFragment;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HomeAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final Context mContext;
|
||||
private final ArrayList<PanelInfo> mPanelInfos;
|
||||
private final Map<String, HomeFragment> mPanels;
|
||||
private final Map<String, Bundle> mRestoreBundles;
|
||||
|
||||
private boolean mCanLoadHint;
|
||||
|
||||
private OnAddPanelListener mAddPanelListener;
|
||||
|
||||
private HomeFragment.PanelStateChangeListener mPanelStateChangeListener = null;
|
||||
|
||||
public interface OnAddPanelListener {
|
||||
void onAddPanel(String title);
|
||||
}
|
||||
|
||||
public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
|
||||
mPanelStateChangeListener = listener;
|
||||
|
||||
for (Fragment fragment : mPanels.values()) {
|
||||
((HomeFragment) fragment).setPanelStateChangeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public HomeAdapter(Context context, FragmentManager fm) {
|
||||
super(fm);
|
||||
|
||||
mContext = context;
|
||||
mCanLoadHint = HomeFragment.DEFAULT_CAN_LOAD_HINT;
|
||||
|
||||
mPanelInfos = new ArrayList<>();
|
||||
mPanels = new HashMap<>();
|
||||
mRestoreBundles = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mPanelInfos.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
PanelInfo info = mPanelInfos.get(position);
|
||||
return Fragment.instantiate(mContext, info.getClassName(mContext), info.getArgs());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
if (mPanelInfos.size() > 0) {
|
||||
PanelInfo info = mPanelInfos.get(position);
|
||||
return info.getTitle().toUpperCase();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
final HomeFragment fragment = (HomeFragment) super.instantiateItem(container, position);
|
||||
fragment.setPanelStateChangeListener(mPanelStateChangeListener);
|
||||
|
||||
final String id = mPanelInfos.get(position).getId();
|
||||
mPanels.put(id, fragment);
|
||||
|
||||
if (mRestoreBundles.containsKey(id)) {
|
||||
fragment.restoreData(mRestoreBundles.get(id));
|
||||
mRestoreBundles.remove(id);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public void setRestoreData(int position, Bundle data) {
|
||||
final String id = mPanelInfos.get(position).getId();
|
||||
final HomeFragment fragment = mPanels.get(id);
|
||||
|
||||
// We have no guarantees as to whether our desired fragment is instantiated yet: therefore
|
||||
// we might need to either pass data to the fragment, or store it for later.
|
||||
if (fragment != null) {
|
||||
fragment.restoreData(data);
|
||||
} else {
|
||||
mRestoreBundles.put(id, data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
final String id = mPanelInfos.get(position).getId();
|
||||
|
||||
super.destroyItem(container, position, object);
|
||||
mPanels.remove(id);
|
||||
}
|
||||
|
||||
public void setOnAddPanelListener(OnAddPanelListener listener) {
|
||||
mAddPanelListener = listener;
|
||||
}
|
||||
|
||||
public int getItemPosition(String panelId) {
|
||||
for (int i = 0; i < mPanelInfos.size(); i++) {
|
||||
final String id = mPanelInfos.get(i).getId();
|
||||
if (id.equals(panelId)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public String getPanelIdAtPosition(int position) {
|
||||
// getPanelIdAtPosition() might be called before HomeAdapter
|
||||
// has got its initial list of PanelConfigs. Just bail.
|
||||
if (mPanelInfos.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mPanelInfos.get(position).getId();
|
||||
}
|
||||
|
||||
private void addPanel(PanelInfo info) {
|
||||
mPanelInfos.add(info);
|
||||
|
||||
if (mAddPanelListener != null) {
|
||||
mAddPanelListener.onAddPanel(info.getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
public void update(List<PanelConfig> panelConfigs) {
|
||||
mPanels.clear();
|
||||
mPanelInfos.clear();
|
||||
|
||||
if (panelConfigs != null) {
|
||||
for (PanelConfig panelConfig : panelConfigs) {
|
||||
final PanelInfo info = new PanelInfo(panelConfig);
|
||||
addPanel(info);
|
||||
}
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public boolean getCanLoadHint() {
|
||||
return mCanLoadHint;
|
||||
}
|
||||
|
||||
public void setCanLoadHint(boolean canLoadHint) {
|
||||
// We cache the last hint value so that we can use it when
|
||||
// creating new panels. See PanelInfo.getArgs().
|
||||
mCanLoadHint = canLoadHint;
|
||||
|
||||
// Enable/disable loading on all existing panels
|
||||
for (Fragment panelFragment : mPanels.values()) {
|
||||
final HomeFragment panel = (HomeFragment) panelFragment;
|
||||
panel.setCanLoadHint(canLoadHint);
|
||||
}
|
||||
}
|
||||
|
||||
private final class PanelInfo {
|
||||
private final PanelConfig mPanelConfig;
|
||||
|
||||
PanelInfo(PanelConfig panelConfig) {
|
||||
mPanelConfig = panelConfig;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return mPanelConfig.getId();
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mPanelConfig.getTitle();
|
||||
}
|
||||
|
||||
public String getClassName(Context context) {
|
||||
final PanelType type = mPanelConfig.getType();
|
||||
|
||||
// Override top_sites with ActivityStream panel when enabled
|
||||
// PanelType.toString() returns the panel id
|
||||
if (type.toString() == "top_sites" &&
|
||||
ActivityStream.isEnabled(context) &&
|
||||
ActivityStream.isHomePanel()) {
|
||||
return ActivityStreamHomeFragment.class.getName();
|
||||
}
|
||||
return type.getPanelClass().getName();
|
||||
}
|
||||
|
||||
public Bundle getArgs() {
|
||||
final Bundle args = new Bundle();
|
||||
|
||||
args.putBoolean(HomePager.CAN_LOAD_ARG, mCanLoadHint);
|
||||
|
||||
// Only DynamicPanels need the PanelConfig argument
|
||||
if (mPanelConfig.isDynamic()) {
|
||||
args.putParcelable(HomePager.PANEL_CONFIG_ARG, mPanelConfig);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
}
|
||||
315
mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java
Normal file
315
mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
/* -*- 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.json.JSONObject;
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.animation.PropertyAnimator;
|
||||
import org.mozilla.gecko.animation.PropertyAnimator.Property;
|
||||
import org.mozilla.gecko.animation.ViewHelper;
|
||||
import org.mozilla.gecko.util.ResourceDrawableUtils;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
import org.mozilla.gecko.widget.EllipsisTextView;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
public class HomeBanner extends LinearLayout
|
||||
implements GeckoEventListener {
|
||||
private static final String LOGTAG = "GeckoHomeBanner";
|
||||
|
||||
// Used for tracking scroll length
|
||||
private float mTouchY = -1;
|
||||
|
||||
// Used to detect for upwards scroll to push banner all the way up
|
||||
private boolean mSnapBannerToTop;
|
||||
|
||||
// Tracks whether or not the banner should be shown on the current panel.
|
||||
private boolean mActive;
|
||||
|
||||
// The user is currently swiping between HomePager pages
|
||||
private boolean mScrollingPages;
|
||||
|
||||
// Tracks whether the user swiped the banner down, preventing us from autoshowing when the user
|
||||
// switches back to the default page.
|
||||
private boolean mUserSwipedDown;
|
||||
|
||||
// We must use this custom TextView to address an issue on 2.3 and lower where ellipsized text
|
||||
// will not wrap more than 2 lines.
|
||||
private final EllipsisTextView mTextView;
|
||||
private final ImageView mIconView;
|
||||
|
||||
// The height of the banner view.
|
||||
private final float mHeight;
|
||||
|
||||
// Listener that gets called when the banner is dismissed from the close button.
|
||||
private OnDismissListener mOnDismissListener;
|
||||
|
||||
public interface OnDismissListener {
|
||||
public void onDismiss();
|
||||
}
|
||||
|
||||
public HomeBanner(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public HomeBanner(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.home_banner_content, this);
|
||||
|
||||
mTextView = (EllipsisTextView) findViewById(R.id.text);
|
||||
mIconView = (ImageView) findViewById(R.id.icon);
|
||||
|
||||
mHeight = getResources().getDimensionPixelSize(R.dimen.home_banner_height);
|
||||
|
||||
// Disable the banner until a message is set.
|
||||
setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
// Tapping on the close button will ensure that the banner is never
|
||||
// showed again on this session.
|
||||
final ImageButton closeButton = (ImageButton) findViewById(R.id.close);
|
||||
|
||||
// The drawable should have 50% opacity.
|
||||
closeButton.getDrawable().setAlpha(127);
|
||||
|
||||
closeButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
HomeBanner.this.dismiss();
|
||||
|
||||
// Send the current message id back to JS.
|
||||
GeckoAppShell.notifyObservers("HomeBanner:Dismiss", (String) getTag());
|
||||
}
|
||||
});
|
||||
|
||||
setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
HomeBanner.this.dismiss();
|
||||
|
||||
// Send the current message id back to JS.
|
||||
GeckoAppShell.notifyObservers("HomeBanner:Click", (String) getTag());
|
||||
}
|
||||
});
|
||||
|
||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "HomeBanner:Data");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "HomeBanner:Data");
|
||||
}
|
||||
|
||||
public void setScrollingPages(boolean scrollingPages) {
|
||||
mScrollingPages = scrollingPages;
|
||||
}
|
||||
|
||||
public void setOnDismissListener(OnDismissListener listener) {
|
||||
mOnDismissListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides and disables the banner.
|
||||
*/
|
||||
private void dismiss() {
|
||||
setVisibility(View.GONE);
|
||||
setEnabled(false);
|
||||
|
||||
if (mOnDismissListener != null) {
|
||||
mOnDismissListener.onDismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to gecko to request a new banner message. UI is updated in handleMessage.
|
||||
*/
|
||||
public void update() {
|
||||
GeckoAppShell.notifyObservers("HomeBanner:Get", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
final String id = message.optString("id");
|
||||
final String text = message.optString("text");
|
||||
final String iconURI = message.optString("iconURI");
|
||||
|
||||
// Don't update the banner if the message doesn't have valid id and text.
|
||||
if (TextUtils.isEmpty(id) || TextUtils.isEmpty(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the banner message on the UI thread.
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Store the current message id to pass back to JS in the view's OnClickListener.
|
||||
setTag(id);
|
||||
mTextView.setOriginalText(Html.fromHtml(text));
|
||||
|
||||
ResourceDrawableUtils.getDrawable(getContext(), iconURI, new ResourceDrawableUtils.BitmapLoader() {
|
||||
@Override
|
||||
public void onBitmapFound(final Drawable d) {
|
||||
// Hide the image view if we don't have an icon to show.
|
||||
if (d == null) {
|
||||
mIconView.setVisibility(View.GONE);
|
||||
} else {
|
||||
mIconView.setImageDrawable(d);
|
||||
mIconView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
GeckoAppShell.notifyObservers("HomeBanner:Shown", id);
|
||||
|
||||
// Enable the banner after a message is set.
|
||||
setEnabled(true);
|
||||
|
||||
// Animate the banner if it is currently active.
|
||||
if (mActive) {
|
||||
animateUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
// No need to animate if not changing
|
||||
if (mActive == active) {
|
||||
return;
|
||||
}
|
||||
|
||||
mActive = active;
|
||||
|
||||
// Don't animate if the banner isn't enabled.
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
animateUp();
|
||||
} else {
|
||||
animateDown();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureVisible() {
|
||||
// The banner visibility is set to GONE after it is animated off screen,
|
||||
// so we need to make it visible again.
|
||||
if (getVisibility() == View.GONE) {
|
||||
// Translate the banner off screen before setting it to VISIBLE.
|
||||
ViewHelper.setTranslationY(this, mHeight);
|
||||
setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void animateUp() {
|
||||
// Don't try to animate if the user swiped the banner down previously to hide it.
|
||||
if (mUserSwipedDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureVisible();
|
||||
|
||||
final PropertyAnimator animator = new PropertyAnimator(100);
|
||||
animator.attach(this, Property.TRANSLATION_Y, 0);
|
||||
animator.start();
|
||||
}
|
||||
|
||||
private void animateDown() {
|
||||
if (ViewHelper.getTranslationY(this) == mHeight) {
|
||||
// Hide the banner to avoid intercepting clicks on pre-honeycomb devices.
|
||||
setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
final PropertyAnimator animator = new PropertyAnimator(100);
|
||||
animator.attach(this, Property.TRANSLATION_Y, mHeight);
|
||||
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
|
||||
@Override
|
||||
public void onPropertyAnimationStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPropertyAnimationEnd() {
|
||||
// Hide the banner to avoid intercepting clicks on pre-honeycomb devices.
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
animator.start();
|
||||
}
|
||||
|
||||
public void handleHomeTouch(MotionEvent event) {
|
||||
if (!mActive || !isEnabled() || mScrollingPages) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureVisible();
|
||||
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
// Track the beginning of the touch
|
||||
mTouchY = event.getRawY();
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
final float curY = event.getRawY();
|
||||
final float delta = mTouchY - curY;
|
||||
mSnapBannerToTop = delta <= 0.0f;
|
||||
|
||||
float newTranslationY = ViewHelper.getTranslationY(this) + delta;
|
||||
|
||||
// Clamp the values to be between 0 and height.
|
||||
if (newTranslationY < 0.0f) {
|
||||
newTranslationY = 0.0f;
|
||||
} else if (newTranslationY > mHeight) {
|
||||
newTranslationY = mHeight;
|
||||
}
|
||||
|
||||
// Don't change this value if it wasn't a significant movement
|
||||
if (delta >= 10 || delta <= -10) {
|
||||
mUserSwipedDown = (newTranslationY == mHeight);
|
||||
}
|
||||
|
||||
ViewHelper.setTranslationY(this, newTranslationY);
|
||||
mTouchY = curY;
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL: {
|
||||
mTouchY = -1;
|
||||
if (mSnapBannerToTop) {
|
||||
animateUp();
|
||||
} else {
|
||||
animateDown();
|
||||
mUserSwipedDown = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1694
mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
Normal file
1694
mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,83 @@
|
|||
/* -*- 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.home.HomeConfig.OnReloadListener;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
|
||||
public class HomeConfigLoader extends AsyncTaskLoader<HomeConfig.State> {
|
||||
private final HomeConfig mConfig;
|
||||
private HomeConfig.State mConfigState;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public HomeConfigLoader(Context context, HomeConfig homeConfig) {
|
||||
super(context);
|
||||
mContext = context;
|
||||
mConfig = homeConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeConfig.State loadInBackground() {
|
||||
return mConfig.load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(HomeConfig.State configState) {
|
||||
if (isReset()) {
|
||||
mConfigState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
mConfigState = configState;
|
||||
mConfig.setOnReloadListener(new ForceReloadListener());
|
||||
|
||||
if (isStarted()) {
|
||||
super.deliverResult(configState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
if (mConfigState != null) {
|
||||
deliverResult(mConfigState);
|
||||
}
|
||||
|
||||
if (takeContentChanged() || mConfigState == null) {
|
||||
forceLoad();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled(HomeConfig.State configState) {
|
||||
mConfigState = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
super.onReset();
|
||||
|
||||
// Ensure the loader is stopped.
|
||||
onStopLoading();
|
||||
|
||||
mConfigState = null;
|
||||
mConfig.setOnReloadListener(null);
|
||||
}
|
||||
|
||||
private class ForceReloadListener implements OnReloadListener {
|
||||
@Override
|
||||
public void onReload() {
|
||||
onContentChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,663 @@
|
|||
/* -*- 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 static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.home.HomeConfig.HomeConfigBackend;
|
||||
import org.mozilla.gecko.home.HomeConfig.OnReloadListener;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelType;
|
||||
import org.mozilla.gecko.home.HomeConfig.State;
|
||||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.CheckResult;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
public class HomeConfigPrefsBackend implements HomeConfigBackend {
|
||||
private static final String LOGTAG = "GeckoHomeConfigBackend";
|
||||
|
||||
// Increment this to trigger a migration.
|
||||
@VisibleForTesting
|
||||
static final int VERSION = 8;
|
||||
|
||||
// This key was originally used to store only an array of panel configs.
|
||||
public static final String PREFS_CONFIG_KEY_OLD = "home_panels";
|
||||
|
||||
// This key is now used to store a version number with the array of panel configs.
|
||||
public static final String PREFS_CONFIG_KEY = "home_panels_with_version";
|
||||
|
||||
// Keys used with JSON object stored in prefs.
|
||||
private static final String JSON_KEY_PANELS = "panels";
|
||||
private static final String JSON_KEY_VERSION = "version";
|
||||
|
||||
private static final String PREFS_LOCALE_KEY = "home_locale";
|
||||
|
||||
private static final String RELOAD_BROADCAST = "HomeConfigPrefsBackend:Reload";
|
||||
|
||||
private final Context mContext;
|
||||
private ReloadBroadcastReceiver mReloadBroadcastReceiver;
|
||||
private OnReloadListener mReloadListener;
|
||||
|
||||
private static boolean sMigrationDone;
|
||||
|
||||
public HomeConfigPrefsBackend(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
private SharedPreferences getSharedPreferences() {
|
||||
return GeckoSharedPrefs.forProfile(mContext);
|
||||
}
|
||||
|
||||
private State loadDefaultConfig() {
|
||||
final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();
|
||||
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES,
|
||||
EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
|
||||
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
|
||||
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY));
|
||||
|
||||
|
||||
return new State(panelConfigs, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through the panels to check if they are all disabled.
|
||||
*/
|
||||
private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
|
||||
final int count = jsonPanels.length();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
|
||||
|
||||
if (!jsonPanelConfig.optBoolean(PanelConfig.JSON_KEY_DISABLED, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected enum Position {
|
||||
NONE, // Not present.
|
||||
FRONT, // At the front of the list of panels.
|
||||
BACK, // At the back of the list of panels.
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and insert a built-in panel configuration.
|
||||
*
|
||||
* @param context Android context.
|
||||
* @param jsonPanels array of JSON panels to update in place.
|
||||
* @param panelType to add.
|
||||
* @param positionOnPhones where to place the new panel on phones.
|
||||
* @param positionOnTablets where to place the new panel on tablets.
|
||||
* @throws JSONException
|
||||
*/
|
||||
protected static void addBuiltinPanelConfig(Context context, JSONArray jsonPanels,
|
||||
PanelType panelType, Position positionOnPhones, Position positionOnTablets) throws JSONException {
|
||||
// Add the new panel.
|
||||
final JSONObject jsonPanelConfig =
|
||||
createBuiltinPanelConfig(context, panelType).toJSON();
|
||||
|
||||
// If any panel is enabled, then we should make the new panel enabled.
|
||||
jsonPanelConfig.put(PanelConfig.JSON_KEY_DISABLED,
|
||||
allPanelsAreDisabled(jsonPanels));
|
||||
|
||||
final boolean isTablet = HardwareUtils.isTablet();
|
||||
final boolean isPhone = !isTablet;
|
||||
|
||||
// Maybe add the new panel to the front of the array.
|
||||
if ((isPhone && positionOnPhones == Position.FRONT) ||
|
||||
(isTablet && positionOnTablets == Position.FRONT)) {
|
||||
// This is an inefficient way to stretch [a, b, c] to [a, a, b, c].
|
||||
for (int i = jsonPanels.length(); i >= 1; i--) {
|
||||
jsonPanels.put(i, jsonPanels.get(i - 1));
|
||||
}
|
||||
// And this inserts [d, a, b, c].
|
||||
jsonPanels.put(0, jsonPanelConfig);
|
||||
}
|
||||
|
||||
// Maybe add the new panel to the back of the array.
|
||||
if ((isPhone && positionOnPhones == Position.BACK) ||
|
||||
(isTablet && positionOnTablets == Position.BACK)) {
|
||||
jsonPanels.put(jsonPanelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the panels to combine the History and Sync panels into the (Combined) History panel.
|
||||
*
|
||||
* Tries to replace the History panel with the Combined History panel if visible, or falls back to
|
||||
* replacing the Sync panel if it's visible. That way, we minimize panel reordering during a migration.
|
||||
* @param context Android context
|
||||
* @param jsonPanels array of original JSON panels
|
||||
* @return new array of updated JSON panels
|
||||
* @throws JSONException
|
||||
*/
|
||||
private static JSONArray combineHistoryAndSyncPanels(Context context, JSONArray jsonPanels) throws JSONException {
|
||||
EnumSet<PanelConfig.Flags> historyFlags = null;
|
||||
EnumSet<PanelConfig.Flags> syncFlags = null;
|
||||
|
||||
int historyIndex = -1;
|
||||
int syncIndex = -1;
|
||||
|
||||
// Determine state and location of History and Sync panels.
|
||||
for (int i = 0; i < jsonPanels.length(); i++) {
|
||||
JSONObject panelObj = jsonPanels.getJSONObject(i);
|
||||
final PanelConfig panelConfig = new PanelConfig(panelObj);
|
||||
final PanelType type = panelConfig.getType();
|
||||
if (type == PanelType.DEPRECATED_HISTORY) {
|
||||
historyIndex = i;
|
||||
historyFlags = panelConfig.getFlags();
|
||||
} else if (type == PanelType.DEPRECATED_REMOTE_TABS) {
|
||||
syncIndex = i;
|
||||
syncFlags = panelConfig.getFlags();
|
||||
} else if (type == PanelType.COMBINED_HISTORY) {
|
||||
// Partial landing of bug 1220928 combined the History and Sync panels of users who didn't
|
||||
// have home panel customizations (including new users), thus they don't this migration.
|
||||
return jsonPanels;
|
||||
}
|
||||
}
|
||||
|
||||
if (historyIndex == -1 || syncIndex == -1) {
|
||||
throw new IllegalArgumentException("Expected both History and Sync panels to be present prior to Combined History.");
|
||||
}
|
||||
|
||||
PanelConfig newPanel;
|
||||
int replaceIndex;
|
||||
int removeIndex;
|
||||
|
||||
if (historyFlags.contains(PanelConfig.Flags.DISABLED_PANEL) && !syncFlags.contains(PanelConfig.Flags.DISABLED_PANEL)) {
|
||||
// Replace the Sync panel if it's visible and the History panel is disabled.
|
||||
replaceIndex = syncIndex;
|
||||
removeIndex = historyIndex;
|
||||
newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, syncFlags);
|
||||
} else {
|
||||
// Otherwise, just replace the History panel.
|
||||
replaceIndex = historyIndex;
|
||||
removeIndex = syncIndex;
|
||||
newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, historyFlags);
|
||||
}
|
||||
|
||||
// Copy the array with updated panel and removed panel.
|
||||
final JSONArray newArray = new JSONArray();
|
||||
for (int i = 0; i < jsonPanels.length(); i++) {
|
||||
if (i == replaceIndex) {
|
||||
newArray.put(newPanel.toJSON());
|
||||
} else if (i == removeIndex) {
|
||||
continue;
|
||||
} else {
|
||||
newArray.put(jsonPanels.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
return newArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over all homepanels to verify that there is at least one default panel. If there is
|
||||
* no default panel, set History as the default panel. (This is only relevant for two botched
|
||||
* migrations where the history panel should have been made the default panel, but wasn't.)
|
||||
*/
|
||||
private static void ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels) throws JSONException {
|
||||
int historyIndex = -1;
|
||||
|
||||
// If all panels are disabled, there is no default panel - this is the only valid state
|
||||
// that has no default. We can use this flag to track whether any visible panels have been
|
||||
// found.
|
||||
boolean enabledPanelsFound = false;
|
||||
|
||||
for (int i = 0; i < jsonPanels.length(); i++) {
|
||||
final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i));
|
||||
if (panelConfig.isDefault()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!panelConfig.isDisabled()) {
|
||||
enabledPanelsFound = true;
|
||||
}
|
||||
|
||||
if (panelConfig.getType() == PanelType.COMBINED_HISTORY) {
|
||||
historyIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabledPanelsFound) {
|
||||
// No panels are enabled, hence there can be no default (see noEnabledPanelsFound declaration
|
||||
// for more information).
|
||||
return;
|
||||
}
|
||||
|
||||
// Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
|
||||
final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
|
||||
jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a panel from the home panel config.
|
||||
* If the removed panel was set as the default home panel, we provide a replacement for it.
|
||||
*
|
||||
* @param context Android context
|
||||
* @param jsonPanels array of original JSON panels
|
||||
* @param panelToRemove The home panel to be removed.
|
||||
* @param replacementPanel The panel which will replace it if the removed panel
|
||||
* was the default home panel.
|
||||
* @param alwaysUnhide If true, the replacement panel will always be unhidden,
|
||||
* otherwise only if we turn it into the new default panel.
|
||||
* @return new array of updated JSON panels
|
||||
* @throws JSONException
|
||||
*/
|
||||
private static JSONArray removePanel(Context context, JSONArray jsonPanels,
|
||||
PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
|
||||
boolean wasDefault = false;
|
||||
boolean wasDisabled = false;
|
||||
int replacementPanelIndex = -1;
|
||||
boolean replacementWasDefault = false;
|
||||
|
||||
// JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
|
||||
// the items we don't want deleted into a new array.
|
||||
final JSONArray newJSONPanels = new JSONArray();
|
||||
|
||||
for (int i = 0; i < jsonPanels.length(); i++) {
|
||||
final JSONObject panelJSON = jsonPanels.getJSONObject(i);
|
||||
final PanelConfig panelConfig = new PanelConfig(panelJSON);
|
||||
|
||||
if (panelConfig.getType() == panelToRemove) {
|
||||
// If this panel was the default we'll need to assign a new default:
|
||||
wasDefault = panelConfig.isDefault();
|
||||
wasDisabled = panelConfig.isDisabled();
|
||||
} else {
|
||||
if (panelConfig.getType() == replacementPanel) {
|
||||
replacementPanelIndex = newJSONPanels.length();
|
||||
if (panelConfig.isDefault()) {
|
||||
replacementWasDefault = true;
|
||||
}
|
||||
}
|
||||
|
||||
newJSONPanels.put(panelJSON);
|
||||
}
|
||||
}
|
||||
|
||||
// Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
|
||||
// to be the new default panel, since a hidden default panel doesn't make sense.
|
||||
// This is to allow preserving the behaviour of the original reading list migration function.
|
||||
if ((wasDefault || alwaysUnhide) && !wasDisabled) {
|
||||
final JSONObject replacementPanelConfig;
|
||||
if (wasDefault) {
|
||||
// If the removed panel was the default, the replacement has to be made the new default
|
||||
replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
|
||||
} else {
|
||||
final EnumSet<HomeConfig.PanelConfig.Flags> flags;
|
||||
if (replacementWasDefault) {
|
||||
// However if the replacement panel was already default, we need to preserve it's default status
|
||||
// (By rewriting the PanelConfig, we lose all existing flags, so we need to make sure desired
|
||||
// flags are retained - in this case there's only DEFAULT_PANEL, which is mutually
|
||||
// exclusive with the DISABLE_PANEL case).
|
||||
flags = EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL);
|
||||
} else {
|
||||
flags = EnumSet.noneOf(PanelConfig.Flags.class);
|
||||
}
|
||||
|
||||
// The panel is visible since we don't set Flags.DISABLED_PANEL.
|
||||
replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, flags).toJSON();
|
||||
}
|
||||
|
||||
if (replacementPanelIndex != -1) {
|
||||
newJSONPanels.put(replacementPanelIndex, replacementPanelConfig);
|
||||
} else {
|
||||
newJSONPanels.put(replacementPanelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
return newJSONPanels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the reading list panel already exists.
|
||||
*
|
||||
* @param jsonPanels JSONArray array representing the curent set of panel configs.
|
||||
*
|
||||
* @return boolean Whether or not the reading list panel exists.
|
||||
*/
|
||||
private static boolean readingListPanelExists(JSONArray jsonPanels) {
|
||||
final int count = jsonPanels.length();
|
||||
for (int i = 0; i < count; i++) {
|
||||
try {
|
||||
final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
|
||||
final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
|
||||
if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// It's okay to ignore this exception, since an invalid reading list
|
||||
// panel config is equivalent to no reading list panel.
|
||||
Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
static synchronized JSONArray migratePrefsFromVersionToVersion(final Context context, final int currentVersion, final int newVersion,
|
||||
final JSONArray jsonPanelsIn, final SharedPreferences.Editor prefsEditor) throws JSONException {
|
||||
|
||||
JSONArray jsonPanels = jsonPanelsIn;
|
||||
|
||||
for (int v = currentVersion + 1; v <= newVersion; v++) {
|
||||
Log.d(LOGTAG, "Migrating to version = " + v);
|
||||
|
||||
switch (v) {
|
||||
case 1:
|
||||
// Add "Recent Tabs" panel.
|
||||
addBuiltinPanelConfig(context, jsonPanels,
|
||||
PanelType.DEPRECATED_RECENT_TABS, Position.FRONT, Position.BACK);
|
||||
|
||||
// Remove the old pref key.
|
||||
prefsEditor.remove(PREFS_CONFIG_KEY_OLD);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Add "Remote Tabs"/"Synced Tabs" panel.
|
||||
addBuiltinPanelConfig(context, jsonPanels,
|
||||
PanelType.DEPRECATED_REMOTE_TABS, Position.FRONT, Position.BACK);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Add the "Reading List" panel if it does not exist. At one time,
|
||||
// the Reading List panel was shown only to devices that were not
|
||||
// considered "low memory". Now, we expose the panel to all devices.
|
||||
// This migration should only occur for "low memory" devices.
|
||||
// Note: This will not agree with the default configuration, which
|
||||
// has DEPRECATED_REMOTE_TABS after DEPRECATED_READING_LIST on some devices.
|
||||
if (!readingListPanelExists(jsonPanels)) {
|
||||
addBuiltinPanelConfig(context, jsonPanels,
|
||||
PanelType.DEPRECATED_READING_LIST, Position.BACK, Position.BACK);
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
// Combine the History and Sync panels. In order to minimize an unexpected reordering
|
||||
// of panels, we try to replace the History panel if it's visible, and fall back to
|
||||
// the Sync panel if that's visible.
|
||||
jsonPanels = combineHistoryAndSyncPanels(context, jsonPanels);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
// This is the fix for bug 1264136 where we lost track of the default panel during some migrations.
|
||||
ensureDefaultPanelForV5orV8(context, jsonPanels);
|
||||
break;
|
||||
|
||||
case 6:
|
||||
jsonPanels = removePanel(context, jsonPanels,
|
||||
PanelType.DEPRECATED_READING_LIST, PanelType.BOOKMARKS, false);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
jsonPanels = removePanel(context, jsonPanels,
|
||||
PanelType.DEPRECATED_RECENT_TABS, PanelType.COMBINED_HISTORY, true);
|
||||
break;
|
||||
|
||||
case 8:
|
||||
// Similar to "case 5" above, this time 1304777 - once again we lost track
|
||||
// of the history panel
|
||||
ensureDefaultPanelForV5orV8(context, jsonPanels);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return jsonPanels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates JSON config data storage.
|
||||
*
|
||||
* @param context Context used to get shared preferences and create built-in panel.
|
||||
* @param jsonString String currently stored in preferences.
|
||||
*
|
||||
* @return JSONArray array representing new set of panel configs.
|
||||
*/
|
||||
private static synchronized JSONArray maybePerformMigration(Context context, String jsonString) throws JSONException {
|
||||
// If the migration is already done, we're at the current version.
|
||||
if (sMigrationDone) {
|
||||
final JSONObject json = new JSONObject(jsonString);
|
||||
return json.getJSONArray(JSON_KEY_PANELS);
|
||||
}
|
||||
|
||||
// Make sure we only do this version check once.
|
||||
sMigrationDone = true;
|
||||
|
||||
JSONArray jsonPanels;
|
||||
final int version;
|
||||
|
||||
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
|
||||
if (prefs.contains(PREFS_CONFIG_KEY_OLD)) {
|
||||
// Our original implementation did not contain versioning, so this is implicitly version 0.
|
||||
jsonPanels = new JSONArray(jsonString);
|
||||
version = 0;
|
||||
} else {
|
||||
final JSONObject json = new JSONObject(jsonString);
|
||||
jsonPanels = json.getJSONArray(JSON_KEY_PANELS);
|
||||
version = json.getInt(JSON_KEY_VERSION);
|
||||
}
|
||||
|
||||
if (version == VERSION) {
|
||||
return jsonPanels;
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "Performing migration");
|
||||
|
||||
final SharedPreferences.Editor prefsEditor = prefs.edit();
|
||||
|
||||
jsonPanels = migratePrefsFromVersionToVersion(context, version, VERSION, jsonPanels, prefsEditor);
|
||||
|
||||
// Save the new panel config and the new version number.
|
||||
final JSONObject newJson = new JSONObject();
|
||||
newJson.put(JSON_KEY_PANELS, jsonPanels);
|
||||
newJson.put(JSON_KEY_VERSION, VERSION);
|
||||
|
||||
prefsEditor.putString(PREFS_CONFIG_KEY, newJson.toString());
|
||||
prefsEditor.apply();
|
||||
|
||||
return jsonPanels;
|
||||
}
|
||||
|
||||
private State loadConfigFromString(String jsonString) {
|
||||
final JSONArray jsonPanelConfigs;
|
||||
try {
|
||||
jsonPanelConfigs = maybePerformMigration(mContext, jsonString);
|
||||
updatePrefsFromConfig(jsonPanelConfigs);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error loading the list of home panels from JSON prefs", e);
|
||||
|
||||
// Fallback to default config
|
||||
return loadDefaultConfig();
|
||||
}
|
||||
|
||||
final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();
|
||||
|
||||
final int count = jsonPanelConfigs.length();
|
||||
for (int i = 0; i < count; i++) {
|
||||
try {
|
||||
final JSONObject jsonPanelConfig = jsonPanelConfigs.getJSONObject(i);
|
||||
final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
|
||||
panelConfigs.add(panelConfig);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
return new State(panelConfigs, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State load() {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
|
||||
final String key = (prefs.contains(PREFS_CONFIG_KEY_OLD) ? PREFS_CONFIG_KEY_OLD : PREFS_CONFIG_KEY);
|
||||
final String jsonString = prefs.getString(key, null);
|
||||
|
||||
final State configState;
|
||||
if (TextUtils.isEmpty(jsonString)) {
|
||||
configState = loadDefaultConfig();
|
||||
} else {
|
||||
configState = loadConfigFromString(jsonString);
|
||||
}
|
||||
|
||||
return configState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(State configState) {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
|
||||
// No need to save the state to disk if it represents the default
|
||||
// HomeConfig configuration. Simply force all existing HomeConfigLoader
|
||||
// instances to refresh their contents.
|
||||
if (!configState.isDefault()) {
|
||||
final JSONArray jsonPanelConfigs = new JSONArray();
|
||||
|
||||
for (PanelConfig panelConfig : configState) {
|
||||
try {
|
||||
final JSONObject jsonPanelConfig = panelConfig.toJSON();
|
||||
jsonPanelConfigs.put(jsonPanelConfig);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Exception converting PanelConfig to JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final JSONObject json = new JSONObject();
|
||||
json.put(JSON_KEY_PANELS, jsonPanelConfigs);
|
||||
json.put(JSON_KEY_VERSION, VERSION);
|
||||
|
||||
editor.putString(PREFS_CONFIG_KEY, json.toString());
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Exception saving PanelConfig state", e);
|
||||
}
|
||||
}
|
||||
|
||||
editor.putString(PREFS_LOCALE_KEY, Locale.getDefault().toString());
|
||||
editor.apply();
|
||||
|
||||
// Trigger reload listeners on all live backend instances
|
||||
sendReloadBroadcast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocale() {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
|
||||
String locale = prefs.getString(PREFS_LOCALE_KEY, null);
|
||||
if (locale == null) {
|
||||
// Initialize config with the current locale
|
||||
final String currentLocale = Locale.getDefault().toString();
|
||||
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString(PREFS_LOCALE_KEY, currentLocale);
|
||||
editor.apply();
|
||||
|
||||
// If the user has saved HomeConfig before, return null this
|
||||
// one time to trigger a refresh and ensure we use the
|
||||
// correct locale for the saved state. For more context,
|
||||
// see HomePanelsManager.onLocaleReady().
|
||||
if (!prefs.contains(PREFS_CONFIG_KEY)) {
|
||||
locale = currentLocale;
|
||||
}
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnReloadListener(OnReloadListener listener) {
|
||||
if (mReloadListener != null) {
|
||||
unregisterReloadReceiver();
|
||||
mReloadBroadcastReceiver = null;
|
||||
}
|
||||
|
||||
mReloadListener = listener;
|
||||
|
||||
if (mReloadListener != null) {
|
||||
mReloadBroadcastReceiver = new ReloadBroadcastReceiver();
|
||||
registerReloadReceiver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update prefs that depend on home panels state.
|
||||
*
|
||||
* This includes the prefs that keep track of whether bookmarks or history are enabled, which are
|
||||
* used to control the visibility of the corresponding menu items.
|
||||
*/
|
||||
private void updatePrefsFromConfig(JSONArray panelsArray) {
|
||||
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mContext);
|
||||
if (!prefs.contains(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED)
|
||||
|| !prefs.contains(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED)) {
|
||||
|
||||
final String bookmarkType = PanelType.BOOKMARKS.toString();
|
||||
final String historyType = PanelType.COMBINED_HISTORY.toString();
|
||||
try {
|
||||
for (int i = 0; i < panelsArray.length(); i++) {
|
||||
final JSONObject panelObj = panelsArray.getJSONObject(i);
|
||||
final String panelType = panelObj.optString(PanelConfig.JSON_KEY_TYPE, null);
|
||||
if (panelType == null) {
|
||||
break;
|
||||
}
|
||||
final boolean isDisabled = panelObj.optBoolean(PanelConfig.JSON_KEY_DISABLED, false);
|
||||
if (bookmarkType.equals(panelType)) {
|
||||
prefs.edit().putBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, !isDisabled).apply();
|
||||
} else if (historyType.equals(panelType)) {
|
||||
prefs.edit().putBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, !isDisabled).apply();
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Error fetching panel from config to update prefs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void sendReloadBroadcast() {
|
||||
final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
|
||||
final Intent reloadIntent = new Intent(RELOAD_BROADCAST);
|
||||
lbm.sendBroadcast(reloadIntent);
|
||||
}
|
||||
|
||||
private void registerReloadReceiver() {
|
||||
final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
|
||||
lbm.registerReceiver(mReloadBroadcastReceiver, new IntentFilter(RELOAD_BROADCAST));
|
||||
}
|
||||
|
||||
private void unregisterReloadReceiver() {
|
||||
final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
|
||||
lbm.unregisterReceiver(mReloadBroadcastReceiver);
|
||||
}
|
||||
|
||||
private class ReloadBroadcastReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
mReloadListener.onReload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/* -*- 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.BrowserContract;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
import android.widget.ExpandableListAdapter;
|
||||
import android.widget.ListAdapter;
|
||||
|
||||
/**
|
||||
* A ContextMenuInfo for HomeListView
|
||||
*/
|
||||
public class HomeContextMenuInfo extends AdapterContextMenuInfo {
|
||||
|
||||
public String url;
|
||||
public String title;
|
||||
public boolean isFolder;
|
||||
public int historyId = -1;
|
||||
public int bookmarkId = -1;
|
||||
public RemoveItemType itemType = null;
|
||||
|
||||
// Item type to be handled with "Remove" selection.
|
||||
public static enum RemoveItemType {
|
||||
BOOKMARKS, COMBINED, HISTORY
|
||||
}
|
||||
|
||||
public HomeContextMenuInfo(View targetView, int position, long id) {
|
||||
super(targetView, position, id);
|
||||
}
|
||||
|
||||
public boolean hasBookmarkId() {
|
||||
return bookmarkId > -1;
|
||||
}
|
||||
|
||||
public boolean hasHistoryId() {
|
||||
return historyId > -1;
|
||||
}
|
||||
|
||||
public boolean hasPartnerBookmarkId() {
|
||||
return bookmarkId <= BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START;
|
||||
}
|
||||
|
||||
public boolean canRemove() {
|
||||
return hasBookmarkId() || hasHistoryId() || hasPartnerBookmarkId();
|
||||
}
|
||||
|
||||
public String getDisplayTitle() {
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
return title;
|
||||
}
|
||||
return StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for creating ContextMenuInfo instances from cursors.
|
||||
*/
|
||||
public interface Factory {
|
||||
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for creating ContextMenuInfo instances from ListAdapters.
|
||||
*/
|
||||
public interface ListFactory extends Factory {
|
||||
public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ListAdapter adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for creating ContextMenuInfo instances from ExpandableListAdapters.
|
||||
*/
|
||||
public interface ExpandableFactory {
|
||||
public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ExpandableListAdapter adapter);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/* -*- 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.util.AttributeSet;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemLongClickListener;
|
||||
import android.widget.ExpandableListView;
|
||||
|
||||
/**
|
||||
* <code>HomeExpandableListView</code> is a custom extension of
|
||||
* <code>ExpandableListView<code>, that packs a <code>HomeContextMenuInfo</code>
|
||||
* when any of its rows is long pressed.
|
||||
* <p>
|
||||
* This is the <code>ExpandableListView</code> equivalent of
|
||||
* <code>HomeListView</code>.
|
||||
*/
|
||||
public class HomeExpandableListView extends ExpandableListView
|
||||
implements OnItemLongClickListener {
|
||||
|
||||
// ContextMenuInfo associated with the currently long pressed list item.
|
||||
private HomeContextMenuInfo mContextMenuInfo;
|
||||
|
||||
// ContextMenuInfo factory.
|
||||
private HomeContextMenuInfo.ExpandableFactory mContextMenuInfoFactory;
|
||||
|
||||
public HomeExpandableListView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public HomeExpandableListView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public HomeExpandableListView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
setOnItemLongClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (mContextMenuInfoFactory == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// HomeExpandableListView items can correspond to groups and children.
|
||||
// The factory can determine whether to add context menu for either,
|
||||
// both, or none by unpacking the given position.
|
||||
mContextMenuInfo = mContextMenuInfoFactory.makeInfoForAdapter(view, position, id, getExpandableListAdapter());
|
||||
return showContextMenuForChild(HomeExpandableListView.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContextMenuInfo getContextMenuInfo() {
|
||||
return mContextMenuInfo;
|
||||
}
|
||||
|
||||
public void setContextMenuInfoFactory(final HomeContextMenuInfo.ExpandableFactory factory) {
|
||||
mContextMenuInfoFactory = factory;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,498 @@
|
|||
/* -*- 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 java.util.EnumSet;
|
||||
|
||||
import org.mozilla.gecko.EditBookmarkDialog;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.IntentHelper;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.SnackbarBuilder;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
|
||||
import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
|
||||
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
|
||||
import org.mozilla.gecko.reader.SavedReaderViewHelper;
|
||||
import org.mozilla.gecko.reader.ReadingListHelper;
|
||||
import org.mozilla.gecko.restrictions.Restrictable;
|
||||
import org.mozilla.gecko.restrictions.Restrictions;
|
||||
import org.mozilla.gecko.util.Clipboard;
|
||||
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.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.Snackbar;
|
||||
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;
|
||||
|
||||
/**
|
||||
* HomeFragment is an empty fragment that can be added to the HomePager.
|
||||
* Subclasses can add their own views.
|
||||
* <p>
|
||||
* The containing activity <b>must</b> implement {@link OnUrlOpenListener}.
|
||||
*/
|
||||
public abstract class HomeFragment extends Fragment {
|
||||
// Log Tag.
|
||||
private static final String LOGTAG = "GeckoHomeFragment";
|
||||
|
||||
// Share MIME type.
|
||||
protected static final String SHARE_MIME_TYPE = "text/plain";
|
||||
|
||||
// Default value for "can load" hint
|
||||
static final boolean DEFAULT_CAN_LOAD_HINT = false;
|
||||
|
||||
// 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;
|
||||
|
||||
// On URL open listener
|
||||
protected OnUrlOpenListener mUrlOpenListener;
|
||||
|
||||
// Helper for opening a tab in the background.
|
||||
protected OnUrlOpenInBackgroundListener mUrlOpenInBackgroundListener;
|
||||
|
||||
protected PanelStateChangeListener mPanelStateChangeListener = null;
|
||||
|
||||
/**
|
||||
* Listener to notify when a home panels' state has changed in a way that needs to be stored
|
||||
* for history/restoration. E.g. when a folder is opened/closed in bookmarks.
|
||||
*/
|
||||
public interface PanelStateChangeListener {
|
||||
|
||||
/**
|
||||
* @param bundle Data that should be persisted, and passed to this panel if restored at a later
|
||||
* stage.
|
||||
*/
|
||||
void onStateChanged(Bundle bundle);
|
||||
|
||||
void setCachedRecentTabsCount(int count);
|
||||
|
||||
int getCachedRecentTabsCount();
|
||||
}
|
||||
|
||||
public void restoreData(Bundle data) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public void setPanelStateChangeListener(
|
||||
PanelStateChangeListener mPanelStateChangeListener) {
|
||||
this.mPanelStateChangeListener = mPanelStateChangeListener;
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
try {
|
||||
mUrlOpenInBackgroundListener = (OnUrlOpenInBackgroundListener) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(activity.toString()
|
||||
+ " must implement HomePager.OnUrlOpenInBackgroundListener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
mUrlOpenListener = null;
|
||||
mUrlOpenInBackgroundListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final Bundle args = getArguments();
|
||||
if (args != null) {
|
||||
mCanLoadHint = args.getBoolean(HomePager.CAN_LOAD_ARG, DEFAULT_CAN_LOAD_HINT);
|
||||
} else {
|
||||
mCanLoadHint = DEFAULT_CAN_LOAD_HINT;
|
||||
}
|
||||
|
||||
mIsLoaded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
GeckoApplication.watchReference(getActivity(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
|
||||
if (!(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.getDisplayTitle());
|
||||
|
||||
// Hide unused menu items.
|
||||
menu.findItem(R.id.top_sites_edit).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_pin).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_unpin).setVisible(false);
|
||||
|
||||
// Hide the "Edit" menuitem if this item isn't a bookmark,
|
||||
// or if this is a reading list item.
|
||||
if (!info.hasBookmarkId()) {
|
||||
menu.findItem(R.id.home_edit_bookmark).setVisible(false);
|
||||
}
|
||||
|
||||
// Hide the "Remove" menuitem if this item not removable.
|
||||
if (!info.canRemove()) {
|
||||
menu.findItem(R.id.home_remove).setVisible(false);
|
||||
}
|
||||
|
||||
if (!StringUtils.isShareableUrl(info.url) || GeckoProfile.get(getActivity()).inGuestMode()) {
|
||||
menu.findItem(R.id.home_share).setVisible(false);
|
||||
}
|
||||
|
||||
if (!Restrictions.isAllowed(view.getContext(), Restrictable.PRIVATE_BROWSING)) {
|
||||
menu.findItem(R.id.home_open_private_tab).setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@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 instanceof HomeContextMenuInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
|
||||
final Context context = getActivity();
|
||||
|
||||
final int itemId = item.getItemId();
|
||||
|
||||
// Track the menu action. We don't know much about the context, but we can use this to determine
|
||||
// the frequency of use for various actions.
|
||||
String extras = getResources().getResourceEntryName(itemId);
|
||||
if (TextUtils.equals(extras, "home_open_private_tab")) {
|
||||
// Mask private browsing
|
||||
extras = "home_open_new_tab";
|
||||
}
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, extras);
|
||||
|
||||
if (itemId == R.id.home_copyurl) {
|
||||
if (info.url == null) {
|
||||
Log.e(LOGTAG, "Can't copy address because URL is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
Clipboard.setText(info.url);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.home_share) {
|
||||
if (info.url == null) {
|
||||
Log.e(LOGTAG, "Can't share because URL is null");
|
||||
return false;
|
||||
} else {
|
||||
IntentHelper.openUriExternal(info.url, SHARE_MIME_TYPE, "", "",
|
||||
Intent.ACTION_SEND, info.getDisplayTitle(), false);
|
||||
|
||||
// Context: Sharing via chrome homepage contextmenu list (home session should be active)
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "home_contextmenu");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Fetch an icon big enough for use as a home screen icon.
|
||||
final String displayTitle = info.getDisplayTitle();
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GeckoAppShell.createShortcut(displayTitle, info.url);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Some pinned site items have "user-entered" urls. URLs entered in
|
||||
// the PinSiteDialog are wrapped in a special URI until we can get a
|
||||
// valid URL. If the url is a user-entered url, decode the URL
|
||||
// before loading it.
|
||||
final String url = StringUtils.decodeUserEnteredUrl(info.url);
|
||||
|
||||
final EnumSet<OnUrlOpenInBackgroundListener.Flags> flags = EnumSet.noneOf(OnUrlOpenInBackgroundListener.Flags.class);
|
||||
if (item.getItemId() == R.id.home_open_private_tab) {
|
||||
flags.add(OnUrlOpenInBackgroundListener.Flags.PRIVATE);
|
||||
}
|
||||
|
||||
mUrlOpenInBackgroundListener.onUrlOpenInBackground(url, flags);
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.home_edit_bookmark) {
|
||||
// UI Dialog associates to the activity context, not the applications'.
|
||||
new EditBookmarkDialog(context).show(info.url);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.home_remove) {
|
||||
// For Top Sites grid items, position is required in case item is Pinned.
|
||||
final int position = info instanceof TopSitesGridContextMenuInfo ? info.position : -1;
|
||||
|
||||
if (info.hasPartnerBookmarkId()) {
|
||||
new RemovePartnerBookmarkTask(context, info.bookmarkId).execute();
|
||||
} else {
|
||||
new RemoveItemByUrlTask(context, info.url, info.itemType, position).execute();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint (boolean isVisibleToUser) {
|
||||
if (isVisibleToUser == getUserVisibleHint()) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
loadIfVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a configuration change by detaching and re-attaching.
|
||||
* <p>
|
||||
* A HomeFragment only needs to handle onConfiguration change (i.e.,
|
||||
* re-attach) if its UI needs to change (i.e., re-inflate layouts, use
|
||||
* different styles, etc) for different device orientations. Handling
|
||||
* configuration changes in all HomeFragments will simply cause some
|
||||
* redundant re-inflations on device rotation. This slight inefficiency
|
||||
* avoids potentially not handling a needed onConfigurationChanged in a
|
||||
* subclass.
|
||||
*/
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
// Reattach the fragment, forcing a re-inflation 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();
|
||||
}
|
||||
}
|
||||
|
||||
void setCanLoadHint(boolean canLoadHint) {
|
||||
if (mCanLoadHint == canLoadHint) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCanLoadHint = canLoadHint;
|
||||
loadIfVisible();
|
||||
}
|
||||
|
||||
boolean getCanLoadHint() {
|
||||
return mCanLoadHint;
|
||||
}
|
||||
|
||||
protected abstract void load();
|
||||
|
||||
protected boolean canLoad() {
|
||||
return (mCanLoadHint && isVisible() && getUserVisibleHint());
|
||||
}
|
||||
|
||||
protected void loadIfVisible() {
|
||||
if (!canLoad() || mIsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
load();
|
||||
mIsLoaded = true;
|
||||
}
|
||||
|
||||
protected static class RemoveItemByUrlTask extends UIAsyncTask.WithoutParams<Void> {
|
||||
private final Context mContext;
|
||||
private final String mUrl;
|
||||
private final RemoveItemType mType;
|
||||
private final int mPosition;
|
||||
private final BrowserDB mDB;
|
||||
|
||||
/**
|
||||
* Remove bookmark/history/reading list type item by url, and also unpin the
|
||||
* Top Sites grid item at index <code>position</code>.
|
||||
*/
|
||||
public RemoveItemByUrlTask(Context context, String url, RemoveItemType type, int position) {
|
||||
super(ThreadUtils.getBackgroundHandler());
|
||||
|
||||
mContext = context;
|
||||
mUrl = url;
|
||||
mType = type;
|
||||
mPosition = position;
|
||||
mDB = BrowserDB.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void doInBackground() {
|
||||
ContentResolver cr = mContext.getContentResolver();
|
||||
|
||||
if (mPosition > -1) {
|
||||
mDB.unpinSite(cr, mPosition);
|
||||
if (mDB.hideSuggestedSite(mUrl)) {
|
||||
cr.notifyChange(SuggestedSites.CONTENT_URI, null);
|
||||
}
|
||||
}
|
||||
|
||||
switch (mType) {
|
||||
case BOOKMARKS:
|
||||
removeBookmark(cr);
|
||||
break;
|
||||
|
||||
case HISTORY:
|
||||
removeHistory(cr);
|
||||
break;
|
||||
|
||||
case COMBINED:
|
||||
removeBookmark(cr);
|
||||
removeHistory(cr);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(LOGTAG, "Can't remove item type " + mType.toString());
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Void result) {
|
||||
SnackbarBuilder.builder((Activity) mContext)
|
||||
.message(R.string.page_removed)
|
||||
.duration(Snackbar.LENGTH_LONG)
|
||||
.buildAndShow();
|
||||
}
|
||||
|
||||
private void removeBookmark(ContentResolver cr) {
|
||||
SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(mContext);
|
||||
final boolean isReaderViewPage = rch.isURLCached(mUrl);
|
||||
|
||||
final String extra;
|
||||
if (isReaderViewPage) {
|
||||
extra = "bookmark_reader";
|
||||
} else {
|
||||
extra = "bookmark";
|
||||
}
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, extra);
|
||||
mDB.removeBookmarksWithURL(cr, mUrl);
|
||||
|
||||
if (isReaderViewPage) {
|
||||
ReadingListHelper.removeCachedReaderItem(mUrl, mContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeHistory(ContentResolver cr) {
|
||||
mDB.removeHistoryEntry(cr, mUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private static class RemovePartnerBookmarkTask extends UIAsyncTask.WithoutParams<Void> {
|
||||
private Context context;
|
||||
private long bookmarkId;
|
||||
|
||||
public RemovePartnerBookmarkTask(Context context, long bookmarkId) {
|
||||
super(ThreadUtils.getBackgroundHandler());
|
||||
|
||||
this.context = context;
|
||||
this.bookmarkId = bookmarkId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
context.getContentResolver().delete(
|
||||
PartnerBookmarksProviderProxy.getUriForBookmark(context, bookmarkId),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
SnackbarBuilder.builder((Activity) context)
|
||||
.message(R.string.page_removed)
|
||||
.duration(Snackbar.LENGTH_LONG)
|
||||
.buildAndShow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
/* -*- 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.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.View;
|
||||
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
|
||||
protected OnUrlOpenListener mUrlOpenListener;
|
||||
|
||||
// Top divider
|
||||
private final boolean mShowTopDivider;
|
||||
|
||||
// ContextMenuInfo maker
|
||||
private HomeContextMenuInfo.Factory mContextMenuInfoFactory;
|
||||
|
||||
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.MATCH_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;
|
||||
if (cursor == null || mContextMenuInfoFactory == null) {
|
||||
mContextMenuInfo = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
mContextMenuInfo = mContextMenuInfoFactory.makeInfoForCursor(view, position, id, cursor);
|
||||
return showContextMenuForChild(HomeListView.this);
|
||||
|
||||
} else if (mContextMenuInfoFactory instanceof HomeContextMenuInfo.ListFactory) {
|
||||
mContextMenuInfo = ((HomeContextMenuInfo.ListFactory) mContextMenuInfoFactory).makeInfoForAdapter(view, position, id, getAdapter());
|
||||
return showContextMenuForChild(HomeListView.this);
|
||||
} else {
|
||||
mContextMenuInfo = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContextMenuInfo getContextMenuInfo() {
|
||||
return mContextMenuInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnItemClickListener(final AdapterView.OnItemClickListener listener) {
|
||||
if (listener == null) {
|
||||
super.setOnItemClickListener(null);
|
||||
return;
|
||||
}
|
||||
|
||||
super.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (mShowTopDivider) {
|
||||
position--;
|
||||
}
|
||||
|
||||
listener.onItemClick(parent, view, position, id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setContextMenuInfoFactory(final HomeContextMenuInfo.Factory factory) {
|
||||
mContextMenuInfoFactory = factory;
|
||||
}
|
||||
|
||||
public OnUrlOpenListener getOnUrlOpenListener() {
|
||||
return mUrlOpenListener;
|
||||
}
|
||||
|
||||
public void setOnUrlOpenListener(OnUrlOpenListener listener) {
|
||||
mUrlOpenListener = listener;
|
||||
}
|
||||
}
|
||||
564
mobile/android/base/java/org/mozilla/gecko/home/HomePager.java
Normal file
564
mobile/android/base/java/org/mozilla/gecko/home/HomePager.java
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
/* -*- 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 java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import org.mozilla.gecko.AppConstants.Versions;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.animation.PropertyAnimator;
|
||||
import org.mozilla.gecko.animation.ViewHelper;
|
||||
import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
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.Loader;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
public class HomePager extends ViewPager implements HomeScreen {
|
||||
|
||||
@Override
|
||||
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
|
||||
return super.requestFocus(direction, previouslyFocusedRect);
|
||||
}
|
||||
|
||||
private static final int LOADER_ID_CONFIG = 0;
|
||||
|
||||
private final Context mContext;
|
||||
private volatile boolean mVisible;
|
||||
private Decor mDecor;
|
||||
private View mTabStrip;
|
||||
private HomeBanner mHomeBanner;
|
||||
private int mDefaultPageIndex = -1;
|
||||
|
||||
private final OnAddPanelListener mAddPanelListener;
|
||||
|
||||
private final HomeConfig mConfig;
|
||||
private final ConfigLoaderCallbacks mConfigLoaderCallbacks;
|
||||
|
||||
private String mInitialPanelId;
|
||||
private Bundle mRestoreData;
|
||||
|
||||
// Cached original ViewPager background.
|
||||
private final Drawable mOriginalBackground;
|
||||
|
||||
// Telemetry session for current panel.
|
||||
private TelemetryContract.Session mCurrentPanelSession;
|
||||
private String mCurrentPanelSessionSuffix;
|
||||
|
||||
// Current load state of HomePager.
|
||||
private LoadState mLoadState;
|
||||
|
||||
// Listens for when the current panel changes.
|
||||
private OnPanelChangeListener mPanelChangedListener;
|
||||
|
||||
private HomeFragment.PanelStateChangeListener mPanelStateChangeListener;
|
||||
|
||||
// This is mostly used by UI tests to easily fetch
|
||||
// specific list views at runtime.
|
||||
public static final String LIST_TAG_HISTORY = "history";
|
||||
public static final String LIST_TAG_BOOKMARKS = "bookmarks";
|
||||
public static final String LIST_TAG_TOP_SITES = "top_sites";
|
||||
public static final String LIST_TAG_RECENT_TABS = "recent_tabs";
|
||||
public static final String LIST_TAG_BROWSER_SEARCH = "browser_search";
|
||||
public static final String LIST_TAG_REMOTE_TABS = "remote_tabs";
|
||||
|
||||
public interface OnUrlOpenListener {
|
||||
public enum Flags {
|
||||
ALLOW_SWITCH_TO_TAB,
|
||||
OPEN_WITH_INTENT,
|
||||
/**
|
||||
* Ensure that the raw URL is opened. If not set, then the reader view version of the page
|
||||
* might be opened if the URL is stored as an offline reader-view bookmark.
|
||||
*/
|
||||
NO_READER_VIEW
|
||||
}
|
||||
|
||||
public void onUrlOpen(String url, EnumSet<Flags> flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for requesting a new tab be opened in the background.
|
||||
* <p>
|
||||
* This is the <code>HomeFragment</code> equivalent of opening a new tab by
|
||||
* long clicking a link and selecting the "Open new [private] tab" context
|
||||
* menu option.
|
||||
*/
|
||||
public interface OnUrlOpenInBackgroundListener {
|
||||
public enum Flags {
|
||||
PRIVATE,
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new tab with the given URL
|
||||
*
|
||||
* @param url to open.
|
||||
* @param flags to open new tab with.
|
||||
*/
|
||||
public void onUrlOpenInBackground(String url, EnumSet<Flags> flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Special type of child views that could be added as pager decorations by default.
|
||||
*/
|
||||
public interface Decor {
|
||||
void onAddPagerView(String title);
|
||||
void removeAllPagerViews();
|
||||
void onPageSelected(int position);
|
||||
void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
|
||||
void setOnTitleClickListener(TabMenuStrip.OnTitleClickListener onTitleClickListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* State of HomePager with respect to loading its configuration.
|
||||
*/
|
||||
private enum LoadState {
|
||||
UNLOADED,
|
||||
LOADING,
|
||||
LOADED
|
||||
}
|
||||
|
||||
public static final String CAN_LOAD_ARG = "canLoad";
|
||||
public static final String PANEL_CONFIG_ARG = "panelConfig";
|
||||
|
||||
public HomePager(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public HomePager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mContext = context;
|
||||
|
||||
mConfig = HomeConfig.getDefault(mContext);
|
||||
mConfigLoaderCallbacks = new ConfigLoaderCallbacks();
|
||||
|
||||
mAddPanelListener = new OnAddPanelListener() {
|
||||
@Override
|
||||
public void onAddPanel(String title) {
|
||||
if (mDecor != null) {
|
||||
mDecor.onAddPagerView(title);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This is to keep all 4 panels in memory after they are
|
||||
// selected in the pager.
|
||||
setOffscreenPageLimit(3);
|
||||
|
||||
// We can call HomePager.requestFocus to steal focus from the URL bar and drop the soft
|
||||
// keyboard. However, if there are no focusable views (e.g. an empty reading list), the
|
||||
// URL bar will be refocused. Therefore, we make the HomePager container focusable to
|
||||
// ensure there is always a focusable view. This would ordinarily be done via an XML
|
||||
// attribute, but it is not working properly.
|
||||
setFocusableInTouchMode(true);
|
||||
|
||||
mOriginalBackground = getBackground();
|
||||
setOnPageChangeListener(new PageChangeListener());
|
||||
|
||||
mLoadState = LoadState.UNLOADED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index, ViewGroup.LayoutParams params) {
|
||||
if (child instanceof Decor) {
|
||||
((ViewPager.LayoutParams) params).isDecor = true;
|
||||
mDecor = (Decor) child;
|
||||
mTabStrip = child;
|
||||
|
||||
mDecor.setOnTitleClickListener(new TabMenuStrip.OnTitleClickListener() {
|
||||
@Override
|
||||
public void onTitleClicked(int index) {
|
||||
setCurrentItem(index, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
super.addView(child, index, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and initializes the pager.
|
||||
*
|
||||
* @param fm FragmentManager for the adapter
|
||||
*/
|
||||
@Override
|
||||
public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator) {
|
||||
mLoadState = LoadState.LOADING;
|
||||
|
||||
mVisible = true;
|
||||
mInitialPanelId = panelId;
|
||||
mRestoreData = restoreData;
|
||||
|
||||
// Update the home banner message each time the HomePager is loaded.
|
||||
if (mHomeBanner != null) {
|
||||
mHomeBanner.update();
|
||||
}
|
||||
|
||||
// Only animate on post-HC devices, when a non-null animator is given
|
||||
final boolean shouldAnimate = animator != null;
|
||||
|
||||
final HomeAdapter adapter = new HomeAdapter(mContext, fm);
|
||||
adapter.setOnAddPanelListener(mAddPanelListener);
|
||||
adapter.setPanelStateChangeListener(mPanelStateChangeListener);
|
||||
adapter.setCanLoadHint(true);
|
||||
setAdapter(adapter);
|
||||
|
||||
// Don't show the tabs strip until we have the
|
||||
// list of panels in place.
|
||||
mTabStrip.setVisibility(View.INVISIBLE);
|
||||
|
||||
// Load list of panels from configuration
|
||||
lm.initLoader(LOADER_ID_CONFIG, null, mConfigLoaderCallbacks);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
ViewHelper.setAlpha(this, 0.0f);
|
||||
|
||||
animator.attach(this,
|
||||
PropertyAnimator.Property.ALPHA,
|
||||
1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all child fragments to free memory.
|
||||
*/
|
||||
@Override
|
||||
public void unload() {
|
||||
mVisible = false;
|
||||
setAdapter(null);
|
||||
mLoadState = LoadState.UNLOADED;
|
||||
|
||||
// Stop UI Telemetry sessions.
|
||||
stopCurrentPanelTelemetrySession();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 loaded
|
||||
*/
|
||||
public boolean isVisible() {
|
||||
return mVisible;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentItem(int item, boolean smoothScroll) {
|
||||
super.setCurrentItem(item, smoothScroll);
|
||||
|
||||
if (mDecor != null) {
|
||||
mDecor.onPageSelected(item);
|
||||
}
|
||||
|
||||
if (mHomeBanner != null) {
|
||||
mHomeBanner.setActive(item == mDefaultPageIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void restorePanelData(int item, Bundle data) {
|
||||
((HomeAdapter) getAdapter()).setRestoreData(item, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a home panel. If the given panelId is null,
|
||||
* the default panel will be shown. No action will be taken if:
|
||||
* * HomePager has not loaded yet
|
||||
* * Panel with the given panelId cannot be found
|
||||
*
|
||||
* If you're trying to open a built-in panel, consider loading the panel url directly with
|
||||
* {@link org.mozilla.gecko.AboutPages#getURLForBuiltinPanelType(HomeConfig.PanelType)}.
|
||||
*
|
||||
* @param panelId of the home panel to be shown.
|
||||
*/
|
||||
@Override
|
||||
public void showPanel(String panelId, Bundle restoreData) {
|
||||
if (!mVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mLoadState) {
|
||||
case LOADING:
|
||||
mInitialPanelId = panelId;
|
||||
mRestoreData = restoreData;
|
||||
break;
|
||||
|
||||
case LOADED:
|
||||
int position = mDefaultPageIndex;
|
||||
if (panelId != null) {
|
||||
position = ((HomeAdapter) getAdapter()).getItemPosition(panelId);
|
||||
}
|
||||
|
||||
if (position > -1) {
|
||||
setCurrentItem(position);
|
||||
if (restoreData != null) {
|
||||
restorePanelData(position, restoreData);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
// Drop the soft keyboard by stealing focus from the URL bar.
|
||||
requestFocus();
|
||||
}
|
||||
|
||||
return super.onInterceptTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setBanner(HomeBanner banner) {
|
||||
mHomeBanner = banner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent event) {
|
||||
if (mHomeBanner != null) {
|
||||
mHomeBanner.handleHomeTouch(event);
|
||||
}
|
||||
|
||||
return super.dispatchTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarFocusChange(boolean hasFocus) {
|
||||
if (mHomeBanner == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We should only make the banner active if the toolbar is not focused and we are on the default page
|
||||
final boolean active = !hasFocus && getCurrentItem() == mDefaultPageIndex;
|
||||
mHomeBanner.setActive(active);
|
||||
}
|
||||
|
||||
private void updateUiFromConfigState(HomeConfig.State configState) {
|
||||
// We only care about the adapter if HomePager is currently
|
||||
// loaded, which means it's visible in the activity.
|
||||
if (!mVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDecor != null) {
|
||||
mDecor.removeAllPagerViews();
|
||||
}
|
||||
|
||||
final HomeAdapter adapter = (HomeAdapter) getAdapter();
|
||||
|
||||
// Disable any fragment loading until we have the initial
|
||||
// panel selection done.
|
||||
adapter.setCanLoadHint(false);
|
||||
|
||||
// Destroy any existing panels currently loaded
|
||||
// in the pager.
|
||||
setAdapter(null);
|
||||
|
||||
// Only keep enabled panels.
|
||||
final List<PanelConfig> enabledPanels = new ArrayList<PanelConfig>();
|
||||
|
||||
for (PanelConfig panelConfig : configState) {
|
||||
if (!panelConfig.isDisabled()) {
|
||||
enabledPanels.add(panelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the adapter with the new panel configs
|
||||
adapter.update(enabledPanels);
|
||||
|
||||
final int count = enabledPanels.size();
|
||||
if (count == 0) {
|
||||
// Set firefox watermark as background.
|
||||
setBackgroundResource(R.drawable.home_pager_empty_state);
|
||||
// Hide the tab strip as there are no panels.
|
||||
mTabStrip.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
mTabStrip.setVisibility(View.VISIBLE);
|
||||
// Restore original background.
|
||||
setBackgroundDrawable(mOriginalBackground);
|
||||
}
|
||||
|
||||
// Re-install the adapter with the final state
|
||||
// in the pager.
|
||||
setAdapter(adapter);
|
||||
|
||||
if (count == 0) {
|
||||
mDefaultPageIndex = -1;
|
||||
|
||||
// Hide the banner if there are no enabled panels.
|
||||
if (mHomeBanner != null) {
|
||||
mHomeBanner.setActive(false);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (enabledPanels.get(i).isDefault()) {
|
||||
mDefaultPageIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the default panel if the initial panel wasn't explicitly set by the
|
||||
// load() caller, or if the initial panel is not found in the adapter.
|
||||
final int itemPosition = (mInitialPanelId == null) ? -1 : adapter.getItemPosition(mInitialPanelId);
|
||||
if (itemPosition > -1) {
|
||||
setCurrentItem(itemPosition, false);
|
||||
if (mRestoreData != null) {
|
||||
restorePanelData(itemPosition, mRestoreData);
|
||||
mRestoreData = null; // Release data since it's no longer needed
|
||||
}
|
||||
mInitialPanelId = null;
|
||||
} else {
|
||||
setCurrentItem(mDefaultPageIndex, false);
|
||||
}
|
||||
}
|
||||
|
||||
// The selection is updated asynchronously so we need to post to
|
||||
// UI thread to give the pager time to commit the new page selection
|
||||
// internally and load the right initial panel.
|
||||
ThreadUtils.getUiHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapter.setCanLoadHint(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnPanelChangeListener(OnPanelChangeListener listener) {
|
||||
mPanelChangedListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
|
||||
mPanelStateChangeListener = listener;
|
||||
|
||||
HomeAdapter adapter = (HomeAdapter) getAdapter();
|
||||
if (adapter != null) {
|
||||
adapter.setPanelStateChangeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify listeners of newly selected panel.
|
||||
*
|
||||
* @param position of the newly selected panel
|
||||
*/
|
||||
private void notifyPanelSelected(int position) {
|
||||
if (mDecor != null) {
|
||||
mDecor.onPageSelected(position);
|
||||
}
|
||||
|
||||
if (mPanelChangedListener != null) {
|
||||
final String panelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position);
|
||||
mPanelChangedListener.onPanelSelected(panelId);
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigLoaderCallbacks implements LoaderCallbacks<HomeConfig.State> {
|
||||
@Override
|
||||
public Loader<HomeConfig.State> onCreateLoader(int id, Bundle args) {
|
||||
return new HomeConfigLoader(mContext, mConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<HomeConfig.State> loader, HomeConfig.State configState) {
|
||||
mLoadState = LoadState.LOADED;
|
||||
updateUiFromConfigState(configState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<HomeConfig.State> loader) {
|
||||
mLoadState = LoadState.UNLOADED;
|
||||
}
|
||||
}
|
||||
|
||||
private class PageChangeListener implements ViewPager.OnPageChangeListener {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
notifyPanelSelected(position);
|
||||
|
||||
if (mHomeBanner != null) {
|
||||
mHomeBanner.setActive(position == mDefaultPageIndex);
|
||||
}
|
||||
|
||||
// Start a UI telemetry session for the newly selected panel.
|
||||
final String newPanelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position);
|
||||
startNewPanelTelemetrySession(newPanelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
if (mDecor != null) {
|
||||
mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels);
|
||||
}
|
||||
|
||||
if (mHomeBanner != null) {
|
||||
mHomeBanner.setScrollingPages(positionOffsetPixels != 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Start UI telemetry session for the a panel.
|
||||
* If there is currently a session open for a panel,
|
||||
* it will be stopped before a new one is started.
|
||||
*
|
||||
* @param panelId of panel to start a session for
|
||||
*/
|
||||
private void startNewPanelTelemetrySession(String panelId) {
|
||||
// Stop the current panel's session if we have one.
|
||||
stopCurrentPanelTelemetrySession();
|
||||
|
||||
mCurrentPanelSession = TelemetryContract.Session.HOME_PANEL;
|
||||
mCurrentPanelSessionSuffix = panelId;
|
||||
Telemetry.startUISession(mCurrentPanelSession, mCurrentPanelSessionSuffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current panel telemetry session if one exists.
|
||||
*/
|
||||
private void stopCurrentPanelTelemetrySession() {
|
||||
if (mCurrentPanelSession != null) {
|
||||
Telemetry.stopUISession(mCurrentPanelSession, mCurrentPanelSessionSuffix);
|
||||
mCurrentPanelSession = null;
|
||||
mCurrentPanelSessionSuffix = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,368 @@
|
|||
/* -*- 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 static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.db.HomeProvider;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
import org.mozilla.gecko.home.PanelInfoManager.PanelInfo;
|
||||
import org.mozilla.gecko.home.PanelInfoManager.RequestCallback;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
public class HomePanelsManager implements GeckoEventListener {
|
||||
public static final String LOGTAG = "HomePanelsManager";
|
||||
|
||||
private static final HomePanelsManager sInstance = new HomePanelsManager();
|
||||
|
||||
private static final int INVALIDATION_DELAY_MSEC = 500;
|
||||
private static final int PANEL_INFO_TIMEOUT_MSEC = 1000;
|
||||
|
||||
private static final String EVENT_HOMEPANELS_INSTALL = "HomePanels:Install";
|
||||
private static final String EVENT_HOMEPANELS_UNINSTALL = "HomePanels:Uninstall";
|
||||
private static final String EVENT_HOMEPANELS_UPDATE = "HomePanels:Update";
|
||||
private static final String EVENT_HOMEPANELS_REFRESH = "HomePanels:RefreshDataset";
|
||||
|
||||
private static final String JSON_KEY_PANEL = "panel";
|
||||
private static final String JSON_KEY_PANEL_ID = "id";
|
||||
|
||||
private enum ChangeType {
|
||||
UNINSTALL,
|
||||
INSTALL,
|
||||
UPDATE,
|
||||
REFRESH
|
||||
}
|
||||
|
||||
private enum InvalidationMode {
|
||||
DELAYED,
|
||||
IMMEDIATE
|
||||
}
|
||||
|
||||
private static class ConfigChange {
|
||||
private final ChangeType type;
|
||||
private final Object target;
|
||||
|
||||
public ConfigChange(ChangeType type) {
|
||||
this(type, null);
|
||||
}
|
||||
|
||||
public ConfigChange(ChangeType type, Object target) {
|
||||
this.type = type;
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private HomeConfig mHomeConfig;
|
||||
private boolean mInitialized;
|
||||
|
||||
private final Queue<ConfigChange> mPendingChanges = new ConcurrentLinkedQueue<ConfigChange>();
|
||||
private final Runnable mInvalidationRunnable = new InvalidationRunnable();
|
||||
|
||||
public static HomePanelsManager getInstance() {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void init(Context context) {
|
||||
if (mInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
mContext = context;
|
||||
mHomeConfig = HomeConfig.getDefault(context);
|
||||
|
||||
EventDispatcher.getInstance().registerGeckoThreadListener(this,
|
||||
EVENT_HOMEPANELS_INSTALL,
|
||||
EVENT_HOMEPANELS_UNINSTALL,
|
||||
EVENT_HOMEPANELS_UPDATE,
|
||||
EVENT_HOMEPANELS_REFRESH);
|
||||
|
||||
mInitialized = true;
|
||||
}
|
||||
|
||||
public void onLocaleReady(final String locale) {
|
||||
ThreadUtils.getBackgroundHandler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final String configLocale = mHomeConfig.getLocale();
|
||||
if (configLocale == null || !configLocale.equals(locale)) {
|
||||
handleLocaleChange();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
try {
|
||||
if (event.equals(EVENT_HOMEPANELS_INSTALL)) {
|
||||
Log.d(LOGTAG, EVENT_HOMEPANELS_INSTALL);
|
||||
handlePanelInstall(createPanelConfigFromMessage(message), InvalidationMode.DELAYED);
|
||||
} else if (event.equals(EVENT_HOMEPANELS_UNINSTALL)) {
|
||||
Log.d(LOGTAG, EVENT_HOMEPANELS_UNINSTALL);
|
||||
final String panelId = message.getString(JSON_KEY_PANEL_ID);
|
||||
handlePanelUninstall(panelId);
|
||||
} else if (event.equals(EVENT_HOMEPANELS_UPDATE)) {
|
||||
Log.d(LOGTAG, EVENT_HOMEPANELS_UPDATE);
|
||||
handlePanelUpdate(createPanelConfigFromMessage(message));
|
||||
} else if (event.equals(EVENT_HOMEPANELS_REFRESH)) {
|
||||
Log.d(LOGTAG, EVENT_HOMEPANELS_REFRESH);
|
||||
handleDatasetRefresh(message);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Failed to handle event " + event, e);
|
||||
}
|
||||
}
|
||||
|
||||
private PanelConfig createPanelConfigFromMessage(JSONObject message) throws JSONException {
|
||||
final JSONObject json = message.getJSONObject(JSON_KEY_PANEL);
|
||||
return new PanelConfig(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new PanelConfig to the HomeConfig.
|
||||
*
|
||||
* This posts the invalidation of HomeConfig immediately.
|
||||
*
|
||||
* @param panelConfig panel to add
|
||||
*/
|
||||
public void installPanel(PanelConfig panelConfig) {
|
||||
Log.d(LOGTAG, "installPanel: " + panelConfig.getTitle());
|
||||
handlePanelInstall(panelConfig, InvalidationMode.IMMEDIATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the gecko thread.
|
||||
*/
|
||||
private void handlePanelInstall(PanelConfig panelConfig, InvalidationMode mode) {
|
||||
mPendingChanges.offer(new ConfigChange(ChangeType.INSTALL, panelConfig));
|
||||
Log.d(LOGTAG, "handlePanelInstall: " + mPendingChanges.size());
|
||||
|
||||
scheduleInvalidation(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the gecko thread.
|
||||
*/
|
||||
private void handlePanelUninstall(String panelId) {
|
||||
mPendingChanges.offer(new ConfigChange(ChangeType.UNINSTALL, panelId));
|
||||
Log.d(LOGTAG, "handlePanelUninstall: " + mPendingChanges.size());
|
||||
|
||||
scheduleInvalidation(InvalidationMode.DELAYED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the gecko thread.
|
||||
*/
|
||||
private void handlePanelUpdate(PanelConfig panelConfig) {
|
||||
mPendingChanges.offer(new ConfigChange(ChangeType.UPDATE, panelConfig));
|
||||
Log.d(LOGTAG, "handlePanelUpdate: " + mPendingChanges.size());
|
||||
|
||||
scheduleInvalidation(InvalidationMode.DELAYED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private void handleLocaleChange() {
|
||||
mPendingChanges.offer(new ConfigChange(ChangeType.REFRESH));
|
||||
Log.d(LOGTAG, "handleLocaleChange: " + mPendingChanges.size());
|
||||
|
||||
scheduleInvalidation(InvalidationMode.IMMEDIATE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles a dataset refresh request from Gecko. This is usually
|
||||
* triggered by a HomeStorage.save() call in an add-on.
|
||||
*
|
||||
* Runs in the gecko thread.
|
||||
*/
|
||||
private void handleDatasetRefresh(JSONObject message) {
|
||||
final String datasetId;
|
||||
try {
|
||||
datasetId = message.getString("datasetId");
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Failed to handle dataset refresh", e);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "Refresh request for dataset: " + datasetId);
|
||||
|
||||
final ContentResolver cr = mContext.getContentResolver();
|
||||
cr.notifyChange(HomeProvider.getDatasetNotificationUri(datasetId), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the gecko or main thread.
|
||||
*/
|
||||
private void scheduleInvalidation(InvalidationMode mode) {
|
||||
final Handler handler = ThreadUtils.getBackgroundHandler();
|
||||
|
||||
handler.removeCallbacks(mInvalidationRunnable);
|
||||
|
||||
if (mode == InvalidationMode.IMMEDIATE) {
|
||||
handler.post(mInvalidationRunnable);
|
||||
} else {
|
||||
handler.postDelayed(mInvalidationRunnable, INVALIDATION_DELAY_MSEC);
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "scheduleInvalidation: scheduled new invalidation: " + mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private void executePendingChanges(HomeConfig.Editor editor) {
|
||||
boolean shouldRefresh = false;
|
||||
|
||||
while (!mPendingChanges.isEmpty()) {
|
||||
final ConfigChange pendingChange = mPendingChanges.poll();
|
||||
|
||||
switch (pendingChange.type) {
|
||||
case UNINSTALL: {
|
||||
final String panelId = (String) pendingChange.target;
|
||||
if (editor.uninstall(panelId)) {
|
||||
Log.d(LOGTAG, "executePendingChanges: uninstalled panel " + panelId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case INSTALL: {
|
||||
final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
|
||||
if (editor.install(panelConfig)) {
|
||||
Log.d(LOGTAG, "executePendingChanges: added panel " + panelConfig.getId());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UPDATE: {
|
||||
final PanelConfig panelConfig = (PanelConfig) pendingChange.target;
|
||||
if (editor.update(panelConfig)) {
|
||||
Log.w(LOGTAG, "executePendingChanges: updated panel " + panelConfig.getId());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case REFRESH: {
|
||||
shouldRefresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The editor still represents the default HomeConfig
|
||||
// configuration and hasn't been changed by any operation
|
||||
// above. No need to refresh as the HomeConfig backend will
|
||||
// take of forcing all existing HomeConfigLoader instances to
|
||||
// refresh their contents.
|
||||
if (shouldRefresh && !editor.isDefault()) {
|
||||
executeRefresh(editor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private void refreshFromPanelInfos(HomeConfig.Editor editor, List<PanelInfo> panelInfos) {
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos");
|
||||
|
||||
for (PanelConfig panelConfig : editor) {
|
||||
PanelConfig refreshedPanelConfig = null;
|
||||
|
||||
if (panelConfig.isDynamic()) {
|
||||
for (PanelInfo panelInfo : panelInfos) {
|
||||
if (panelInfo.getId().equals(panelConfig.getId())) {
|
||||
refreshedPanelConfig = panelInfo.toPanelConfig();
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos: refreshing from panel info: " + panelInfo.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
refreshedPanelConfig = createBuiltinPanelConfig(mContext, panelConfig.getType());
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos: refreshing built-in panel: " + panelConfig.getId());
|
||||
}
|
||||
|
||||
if (refreshedPanelConfig == null) {
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos: no refreshed panel, falling back: " + panelConfig.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "refreshFromPanelInfos: refreshed panel " + refreshedPanelConfig.getId());
|
||||
editor.update(refreshedPanelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private void executeRefresh(HomeConfig.Editor editor) {
|
||||
if (editor.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "executeRefresh");
|
||||
|
||||
final Set<String> ids = new HashSet<String>();
|
||||
for (PanelConfig panelConfig : editor) {
|
||||
ids.add(panelConfig.getId());
|
||||
}
|
||||
|
||||
final Object panelRequestLock = new Object();
|
||||
final List<PanelInfo> latestPanelInfos = new ArrayList<PanelInfo>();
|
||||
|
||||
final PanelInfoManager pm = new PanelInfoManager();
|
||||
pm.requestPanelsById(ids, new RequestCallback() {
|
||||
@Override
|
||||
public void onComplete(List<PanelInfo> panelInfos) {
|
||||
synchronized (panelRequestLock) {
|
||||
latestPanelInfos.addAll(panelInfos);
|
||||
Log.d(LOGTAG, "executeRefresh: fetched panel infos: " + panelInfos.size());
|
||||
|
||||
panelRequestLock.notifyAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
synchronized (panelRequestLock) {
|
||||
panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC);
|
||||
|
||||
Log.d(LOGTAG, "executeRefresh: done fetching panel infos");
|
||||
refreshFromPanelInfos(editor, latestPanelInfos);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(LOGTAG, "Failed to fetch panels from gecko", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs in the background thread.
|
||||
*/
|
||||
private class InvalidationRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
final HomeConfig.Editor editor = mHomeConfig.load().edit();
|
||||
executePendingChanges(editor);
|
||||
editor.apply();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package org.mozilla.gecko.home;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.view.View;
|
||||
|
||||
import org.mozilla.gecko.animation.PropertyAnimator;
|
||||
|
||||
/**
|
||||
* Generic interface for any View that can be used as the homescreen.
|
||||
*
|
||||
* In the past we had the HomePager, which contained the usual homepanels (multiple panels: TopSites,
|
||||
* bookmarks, history, etc.), which could be swiped between.
|
||||
*
|
||||
* This interface allows easily switching between different homepanel implementations. For example
|
||||
* the prototype activity-stream panel (which will be a single panel combining the functionality
|
||||
* of the previous panels).
|
||||
*/
|
||||
public interface HomeScreen {
|
||||
/**
|
||||
* Interface for listening into ViewPager panel changes
|
||||
*/
|
||||
public interface OnPanelChangeListener {
|
||||
/**
|
||||
* Called when a new panel is selected.
|
||||
*
|
||||
* @param panelId of the newly selected panel
|
||||
*/
|
||||
public void onPanelSelected(String panelId);
|
||||
}
|
||||
|
||||
// The following two methods are actually methods of View. Since there is no View interface
|
||||
// we're forced to do this instead of "extending" View. Any class implementing HomeScreen
|
||||
// will have to implement these and pass them through to the underlying View.
|
||||
boolean isVisible();
|
||||
boolean requestFocus();
|
||||
|
||||
void onToolbarFocusChange(boolean hasFocus);
|
||||
|
||||
// The following three methods are HomePager specific. The persistence framework might need
|
||||
// refactoring/generalising at some point, but it isn't entirely clear what other panels
|
||||
// might need so we can leave these as is for now.
|
||||
void showPanel(String panelId, Bundle restoreData);
|
||||
void setOnPanelChangeListener(OnPanelChangeListener listener);
|
||||
void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener);
|
||||
|
||||
/**
|
||||
* Set a banner that may be displayed at the bottom of the HomeScreen. This can be used
|
||||
* e.g. to show snippets.
|
||||
*/
|
||||
void setBanner(HomeBanner banner);
|
||||
|
||||
void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData, PropertyAnimator animator);
|
||||
|
||||
void unload();
|
||||
}
|
||||
164
mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java
Normal file
164
mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/* -*- 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.net.Uri;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import com.squareup.picasso.LruCache;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Downloader.Response;
|
||||
import com.squareup.picasso.UrlConnectionDownloader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.mozilla.gecko.distribution.Distribution;
|
||||
|
||||
public class ImageLoader {
|
||||
private static final String LOGTAG = "GeckoImageLoader";
|
||||
|
||||
private static final String DISTRIBUTION_SCHEME = "gecko.distribution";
|
||||
private static final String SUGGESTED_SITES_AUTHORITY = "suggestedsites";
|
||||
|
||||
// The order of density factors to try when looking for an image resource
|
||||
// in the distribution directory. It looks for an exact match first (1.0) then
|
||||
// tries to find images with higher density (2.0 and 1.5). If no image is found,
|
||||
// try a lower density (0.5). See loadDistributionImage().
|
||||
private static final float[] densityFactors = new float[] { 1.0f, 2.0f, 1.5f, 0.5f };
|
||||
|
||||
private static enum Density {
|
||||
MDPI,
|
||||
HDPI,
|
||||
XHDPI,
|
||||
XXHDPI;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Picasso instance and LruCache lrucache are protected by synchronization.
|
||||
private static Picasso instance;
|
||||
private static LruCache lrucache;
|
||||
|
||||
public static synchronized Picasso with(Context context) {
|
||||
if (instance == null) {
|
||||
lrucache = new LruCache(context);
|
||||
Picasso.Builder builder = new Picasso.Builder(context).memoryCache(lrucache);
|
||||
|
||||
final Distribution distribution = Distribution.getInstance(context.getApplicationContext());
|
||||
builder.downloader(new ImageDownloader(context, distribution));
|
||||
instance = builder.build();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static synchronized void clearLruCache() {
|
||||
if (lrucache != null) {
|
||||
lrucache.evictAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Downloader built on top of Picasso's UrlConnectionDownloader
|
||||
* that supports loading images from custom URIs.
|
||||
*/
|
||||
public static class ImageDownloader extends UrlConnectionDownloader {
|
||||
private final Context context;
|
||||
private final Distribution distribution;
|
||||
|
||||
public ImageDownloader(Context context, Distribution distribution) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
this.distribution = distribution;
|
||||
}
|
||||
|
||||
private Density getDensity(float factor) {
|
||||
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||
final float densityDpi = dm.densityDpi * factor;
|
||||
|
||||
if (densityDpi >= DisplayMetrics.DENSITY_XXHIGH) {
|
||||
return Density.XXHDPI;
|
||||
} else if (densityDpi >= DisplayMetrics.DENSITY_XHIGH) {
|
||||
return Density.XHDPI;
|
||||
} else if (densityDpi >= DisplayMetrics.DENSITY_HIGH) {
|
||||
return Density.HDPI;
|
||||
}
|
||||
|
||||
// Fallback to mdpi, no need to handle ldpi.
|
||||
return Density.MDPI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response load(Uri uri, boolean localCacheOnly) throws IOException {
|
||||
final String scheme = uri.getScheme();
|
||||
if (DISTRIBUTION_SCHEME.equals(scheme)) {
|
||||
return loadDistributionImage(uri);
|
||||
}
|
||||
|
||||
return super.load(uri, localCacheOnly);
|
||||
}
|
||||
|
||||
private static String getPathForDensity(String basePath, Density density,
|
||||
String filename) {
|
||||
final File dir = new File(basePath, density.toString());
|
||||
return String.format("%s/%s.png", dir.toString(), filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle distribution URIs in Picasso. The expected format is:
|
||||
*
|
||||
* gecko.distribution://<basepath>/<imagename>
|
||||
*
|
||||
* Which will look for the following file in the distribution:
|
||||
*
|
||||
* <distribution-root-dir>/<basepath>/<device-density>/<imagename>.png
|
||||
*/
|
||||
private Response loadDistributionImage(Uri uri) throws IOException {
|
||||
// Eliminate the leading '//'
|
||||
final String ssp = uri.getSchemeSpecificPart().substring(2);
|
||||
|
||||
final String filename;
|
||||
final String basePath;
|
||||
|
||||
final int slashIndex = ssp.lastIndexOf('/');
|
||||
if (slashIndex == -1) {
|
||||
filename = ssp;
|
||||
basePath = "";
|
||||
} else {
|
||||
filename = ssp.substring(slashIndex + 1);
|
||||
basePath = ssp.substring(0, slashIndex);
|
||||
}
|
||||
|
||||
Set<Density> triedDensities = EnumSet.noneOf(Density.class);
|
||||
|
||||
for (int i = 0; i < densityFactors.length; i++) {
|
||||
final Density density = getDensity(densityFactors[i]);
|
||||
if (!triedDensities.add(density)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String path = getPathForDensity(basePath, density, filename);
|
||||
Log.d(LOGTAG, "Trying to load image from distribution " + path);
|
||||
|
||||
final File f = distribution.getDistributionFile(path);
|
||||
if (f != null) {
|
||||
return new Response(new FileInputStream(f), true);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ResponseException("Couldn't find suggested site image in distribution");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/* -*- 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, 0);
|
||||
|
||||
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 boolean hasStableIds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@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,82 @@
|
|||
/* -*- 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.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
|
||||
/**
|
||||
* Cache used to store authentication state of dynamic panels. The values
|
||||
* in this cache are set in JS through the Home.panels API.
|
||||
*
|
||||
* {@code DynamicPanel} uses this cache to determine whether or not to
|
||||
* show authentication UI for dynamic panels, including listening for
|
||||
* changes in authentication state.
|
||||
*/
|
||||
class PanelAuthCache {
|
||||
private static final String LOGTAG = "GeckoPanelAuthCache";
|
||||
|
||||
// Keep this in sync with the constant defined in Home.jsm
|
||||
private static final String PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_";
|
||||
|
||||
private final Context mContext;
|
||||
private SharedPrefsListener mSharedPrefsListener;
|
||||
private OnChangeListener mChangeListener;
|
||||
|
||||
public interface OnChangeListener {
|
||||
public void onChange(String panelId, boolean isAuthenticated);
|
||||
}
|
||||
|
||||
public PanelAuthCache(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
private SharedPreferences getSharedPreferences() {
|
||||
return GeckoSharedPrefs.forProfile(mContext);
|
||||
}
|
||||
|
||||
private String getPanelAuthKey(String panelId) {
|
||||
return PREFS_PANEL_AUTH_PREFIX + panelId;
|
||||
}
|
||||
|
||||
public boolean isAuthenticated(String panelId) {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
return prefs.getBoolean(getPanelAuthKey(panelId), false);
|
||||
}
|
||||
|
||||
public void setOnChangeListener(OnChangeListener listener) {
|
||||
final SharedPreferences prefs = getSharedPreferences();
|
||||
|
||||
if (mChangeListener != null) {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(mSharedPrefsListener);
|
||||
mSharedPrefsListener = null;
|
||||
}
|
||||
|
||||
mChangeListener = listener;
|
||||
|
||||
if (mChangeListener != null) {
|
||||
mSharedPrefsListener = new SharedPrefsListener();
|
||||
prefs.registerOnSharedPreferenceChangeListener(mSharedPrefsListener);
|
||||
}
|
||||
}
|
||||
|
||||
private class SharedPrefsListener implements OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
|
||||
if (key.startsWith(PREFS_PANEL_AUTH_PREFIX)) {
|
||||
final String panelId = key.substring(PREFS_PANEL_AUTH_PREFIX.length());
|
||||
final boolean isAuthenticated = prefs.getBoolean(key, false);
|
||||
|
||||
Log.d(LOGTAG, "Auth state changed: panelId=" + panelId + ", isAuthenticated=" + isAuthenticated);
|
||||
mChangeListener.onChange(panelId, isAuthenticated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/* -*- 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.GeckoAppShell;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.home.HomeConfig.AuthConfig;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
class PanelAuthLayout extends LinearLayout {
|
||||
|
||||
public PanelAuthLayout(Context context, PanelConfig panelConfig) {
|
||||
super(context);
|
||||
|
||||
final AuthConfig authConfig = panelConfig.getAuthConfig();
|
||||
if (authConfig == null) {
|
||||
throw new IllegalStateException("Can't create PanelAuthLayout without a valid AuthConfig");
|
||||
}
|
||||
|
||||
setOrientation(LinearLayout.VERTICAL);
|
||||
LayoutInflater.from(context).inflate(R.layout.panel_auth_layout, this);
|
||||
|
||||
final TextView messageView = (TextView) findViewById(R.id.message);
|
||||
messageView.setText(authConfig.getMessageText());
|
||||
|
||||
final Button buttonView = (Button) findViewById(R.id.button);
|
||||
buttonView.setText(authConfig.getButtonText());
|
||||
|
||||
final String panelId = panelConfig.getId();
|
||||
buttonView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GeckoAppShell.notifyObservers("HomePanels:Authenticate", panelId);
|
||||
}
|
||||
});
|
||||
|
||||
final ImageView imageView = (ImageView) findViewById(R.id.image);
|
||||
final String imageUrl = authConfig.getImageUrl();
|
||||
|
||||
if (TextUtils.isEmpty(imageUrl)) {
|
||||
// Use a default image if an image URL isn't specified.
|
||||
imageView.setImageResource(R.drawable.icon_home_empty_firefox);
|
||||
} else {
|
||||
ImageLoader.with(getContext())
|
||||
.load(imageUrl)
|
||||
.into(imageView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/* -*- 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.home.PanelLayout.FilterDetail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
class PanelBackItemView extends LinearLayout {
|
||||
private final TextView title;
|
||||
|
||||
public PanelBackItemView(Context context, String backImageUrl) {
|
||||
super(context);
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.panel_back_item, this);
|
||||
setOrientation(HORIZONTAL);
|
||||
|
||||
title = (TextView) findViewById(R.id.title);
|
||||
|
||||
final ImageView image = (ImageView) findViewById(R.id.image);
|
||||
|
||||
if (TextUtils.isEmpty(backImageUrl)) {
|
||||
image.setImageResource(R.drawable.arrow_up);
|
||||
} else {
|
||||
ImageLoader.with(getContext())
|
||||
.load(backImageUrl)
|
||||
.placeholder(R.drawable.arrow_up)
|
||||
.into(image);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFromFilter(FilterDetail filter) {
|
||||
final String backText = getResources()
|
||||
.getString(R.string.home_move_back_to_filter, filter.title);
|
||||
title.setText(backText);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package org.mozilla.gecko.home;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.widget.ImageView;
|
||||
|
||||
@SuppressLint("ViewConstructor") // View is only created from code
|
||||
public class PanelHeaderView extends ImageView {
|
||||
public PanelHeaderView(Context context, HomeConfig.HeaderConfig config) {
|
||||
super(context);
|
||||
|
||||
setAdjustViewBounds(true);
|
||||
|
||||
ImageLoader.with(context)
|
||||
.load(config.getImageUrl())
|
||||
.into(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
|
||||
// Always span the whole width and adjust height as needed.
|
||||
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
|
||||
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/* -*- 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
public class PanelInfoManager implements GeckoEventListener {
|
||||
private static final String LOGTAG = "GeckoPanelInfoManager";
|
||||
|
||||
public class PanelInfo {
|
||||
private final String mId;
|
||||
private final String mTitle;
|
||||
private final JSONObject mJSONData;
|
||||
|
||||
public PanelInfo(String id, String title, JSONObject jsonData) {
|
||||
mId = id;
|
||||
mTitle = title;
|
||||
mJSONData = jsonData;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
public PanelConfig toPanelConfig() {
|
||||
try {
|
||||
return new PanelConfig(mJSONData);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Failed to convert PanelInfo to PanelConfig", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface RequestCallback {
|
||||
public void onComplete(List<PanelInfo> panelInfos);
|
||||
}
|
||||
|
||||
private static final AtomicInteger sRequestId = new AtomicInteger(0);
|
||||
|
||||
// Stores set of pending request callbacks.
|
||||
private static final SparseArray<RequestCallback> sCallbacks = new SparseArray<RequestCallback>();
|
||||
|
||||
/**
|
||||
* Asynchronously fetches list of available panels from Gecko
|
||||
* for the given IDs.
|
||||
*
|
||||
* @param ids list of panel ids to be fetched. A null value will fetch all
|
||||
* available panels.
|
||||
* @param callback onComplete will be called on the UI thread.
|
||||
*/
|
||||
public void requestPanelsById(Set<String> ids, RequestCallback callback) {
|
||||
final int requestId = sRequestId.getAndIncrement();
|
||||
|
||||
synchronized (sCallbacks) {
|
||||
// If there are no pending callbacks, register the event listener.
|
||||
if (sCallbacks.size() == 0) {
|
||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
|
||||
"HomePanels:Data");
|
||||
}
|
||||
sCallbacks.put(requestId, callback);
|
||||
}
|
||||
|
||||
final JSONObject message = new JSONObject();
|
||||
try {
|
||||
message.put("requestId", requestId);
|
||||
|
||||
if (ids != null && ids.size() > 0) {
|
||||
JSONArray idsArray = new JSONArray();
|
||||
for (String id : ids) {
|
||||
idsArray.put(id);
|
||||
}
|
||||
|
||||
message.put("ids", idsArray);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Failed to build event to request panels by id", e);
|
||||
return;
|
||||
}
|
||||
|
||||
GeckoAppShell.notifyObservers("HomePanels:Get", message.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously fetches list of available panels from Gecko.
|
||||
*
|
||||
* @param callback onComplete will be called on the UI thread.
|
||||
*/
|
||||
public void requestAvailablePanels(RequestCallback callback) {
|
||||
requestPanelsById(null, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "HomePanels:Data" events.
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
final ArrayList<PanelInfo> panelInfos = new ArrayList<PanelInfo>();
|
||||
|
||||
try {
|
||||
final JSONArray panels = message.getJSONArray("panels");
|
||||
final int count = panels.length();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final PanelInfo panelInfo = getPanelInfoFromJSON(panels.getJSONObject(i));
|
||||
panelInfos.add(panelInfo);
|
||||
}
|
||||
|
||||
final RequestCallback callback;
|
||||
final int requestId = message.getInt("requestId");
|
||||
|
||||
synchronized (sCallbacks) {
|
||||
callback = sCallbacks.get(requestId);
|
||||
sCallbacks.delete(requestId);
|
||||
|
||||
// Unregister the event listener if there are no more pending callbacks.
|
||||
if (sCallbacks.size() == 0) {
|
||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
|
||||
"HomePanels:Data");
|
||||
}
|
||||
}
|
||||
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onComplete(panelInfos);
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Exception handling " + event + " message", e);
|
||||
}
|
||||
}
|
||||
|
||||
private PanelInfo getPanelInfoFromJSON(JSONObject jsonPanelInfo) throws JSONException {
|
||||
final String id = jsonPanelInfo.getString("id");
|
||||
final String title = jsonPanelInfo.getString("title");
|
||||
|
||||
return new PanelInfo(id, title, jsonPanelInfo);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
/* -*- 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.HomeItems;
|
||||
import org.mozilla.gecko.home.HomeConfig.ItemType;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
class PanelItemView extends LinearLayout {
|
||||
private final TextView titleView;
|
||||
private final TextView descriptionView;
|
||||
private final ImageView imageView;
|
||||
private final LinearLayout titleDescContainerView;
|
||||
private final ImageView backgroundView;
|
||||
|
||||
private PanelItemView(Context context, int layoutId) {
|
||||
super(context);
|
||||
|
||||
LayoutInflater.from(context).inflate(layoutId, this);
|
||||
titleView = (TextView) findViewById(R.id.title);
|
||||
descriptionView = (TextView) findViewById(R.id.description);
|
||||
imageView = (ImageView) findViewById(R.id.image);
|
||||
backgroundView = (ImageView) findViewById(R.id.background);
|
||||
titleDescContainerView = (LinearLayout) findViewById(R.id.title_desc_container);
|
||||
}
|
||||
|
||||
public void updateFromCursor(Cursor cursor) {
|
||||
int titleIndex = cursor.getColumnIndexOrThrow(HomeItems.TITLE);
|
||||
final String titleText = cursor.getString(titleIndex);
|
||||
|
||||
// Only show title if the item has one
|
||||
final boolean hasTitle = !TextUtils.isEmpty(titleText);
|
||||
titleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE);
|
||||
if (hasTitle) {
|
||||
titleView.setText(titleText);
|
||||
}
|
||||
|
||||
int descriptionIndex = cursor.getColumnIndexOrThrow(HomeItems.DESCRIPTION);
|
||||
final String descriptionText = cursor.getString(descriptionIndex);
|
||||
|
||||
// Only show description if the item has one
|
||||
// Descriptions are not supported for IconItemView objects (Bug 1157539)
|
||||
final boolean hasDescription = !TextUtils.isEmpty(descriptionText);
|
||||
if (descriptionView != null) {
|
||||
descriptionView.setVisibility(hasDescription ? View.VISIBLE : View.GONE);
|
||||
if (hasDescription) {
|
||||
descriptionView.setText(descriptionText);
|
||||
}
|
||||
}
|
||||
if (titleDescContainerView != null) {
|
||||
titleDescContainerView.setVisibility(hasTitle || hasDescription ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
int imageIndex = cursor.getColumnIndexOrThrow(HomeItems.IMAGE_URL);
|
||||
final String imageUrl = cursor.getString(imageIndex);
|
||||
|
||||
// Only try to load the image if the item has define image URL
|
||||
final boolean hasImageUrl = !TextUtils.isEmpty(imageUrl);
|
||||
imageView.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (hasImageUrl) {
|
||||
ImageLoader.with(getContext())
|
||||
.load(imageUrl)
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
final int columnIndexBackgroundColor = cursor.getColumnIndex(HomeItems.BACKGROUND_COLOR);
|
||||
if (columnIndexBackgroundColor != -1) {
|
||||
final String color = cursor.getString(columnIndexBackgroundColor);
|
||||
if (!TextUtils.isEmpty(color)) {
|
||||
setBackgroundColor(Color.parseColor(color));
|
||||
}
|
||||
}
|
||||
|
||||
// Backgrounds are only supported for IconItemView objects (Bug 1157539)
|
||||
final int columnIndexBackgroundUrl = cursor.getColumnIndex(HomeItems.BACKGROUND_URL);
|
||||
if (columnIndexBackgroundUrl != -1) {
|
||||
final String backgroundUrl = cursor.getString(columnIndexBackgroundUrl);
|
||||
if (backgroundView != null && !TextUtils.isEmpty(backgroundUrl)) {
|
||||
ImageLoader.with(getContext())
|
||||
.load(backgroundUrl)
|
||||
.fit()
|
||||
.into(backgroundView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ArticleItemView extends PanelItemView {
|
||||
private ArticleItemView(Context context) {
|
||||
super(context, R.layout.panel_article_item);
|
||||
setOrientation(LinearLayout.HORIZONTAL);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ImageItemView extends PanelItemView {
|
||||
private ImageItemView(Context context) {
|
||||
super(context, R.layout.panel_image_item);
|
||||
setOrientation(LinearLayout.VERTICAL);
|
||||
}
|
||||
}
|
||||
|
||||
private static class IconItemView extends PanelItemView {
|
||||
private IconItemView(Context context) {
|
||||
super(context, R.layout.panel_icon_item);
|
||||
}
|
||||
}
|
||||
|
||||
public static PanelItemView create(Context context, ItemType itemType) {
|
||||
switch (itemType) {
|
||||
case ARTICLE:
|
||||
return new ArticleItemView(context);
|
||||
|
||||
case IMAGE:
|
||||
return new ImageItemView(context);
|
||||
|
||||
case ICON:
|
||||
return new IconItemView(context);
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not create panel item view from " + itemType);
|
||||
}
|
||||
}
|
||||
}
|
||||
747
mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java
Normal file
747
mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java
Normal file
|
|
@ -0,0 +1,747 @@
|
|||
/* -*- 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.HomeItems;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.home.HomeConfig.EmptyViewConfig;
|
||||
import org.mozilla.gecko.home.HomeConfig.ItemHandler;
|
||||
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
|
||||
import org.mozilla.gecko.home.HomeConfig.ViewConfig;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
/**
|
||||
* {@code PanelLayout} is the base class for custom layouts to be
|
||||
* used in {@code DynamicPanel}. It provides the basic framework
|
||||
* that enables custom layouts to request and reset datasets and
|
||||
* create panel views. Furthermore, it automates most of the process
|
||||
* of binding panel views with their respective datasets.
|
||||
*
|
||||
* {@code PanelLayout} abstracts the implemention details of how
|
||||
* datasets are actually loaded through the {@DatasetHandler} interface.
|
||||
* {@code DatasetHandler} provides two operations: request and reset.
|
||||
* The results of the dataset requests done via the {@code DatasetHandler}
|
||||
* are delivered to the {@code PanelLayout} with the {@code deliverDataset()}
|
||||
* method.
|
||||
*
|
||||
* Subclasses of {@code PanelLayout} should simply use the utilities
|
||||
* provided by {@code PanelLayout}. Namely:
|
||||
*
|
||||
* {@code requestDataset()} - To fetch datasets and auto-bind them to
|
||||
* the existing panel views backed by them.
|
||||
*
|
||||
* {@code resetDataset()} - To release any resources associated with a
|
||||
* previously loaded dataset.
|
||||
*
|
||||
* {@code createPanelView()} - To create a panel view for a ViewConfig
|
||||
* associated with the panel.
|
||||
*
|
||||
* {@code disposePanelView()} - To dispose any dataset references associated
|
||||
* with the given view.
|
||||
*
|
||||
* {@code PanelLayout} subclasses should always use {@code createPanelView()}
|
||||
* to create the views dynamically created based on {@code ViewConfig}. This
|
||||
* allows {@code PanelLayout} to auto-bind datasets with panel views.
|
||||
* {@code PanelLayout} subclasses are free to have any type of views to arrange
|
||||
* the panel views in different ways.
|
||||
*/
|
||||
abstract class PanelLayout extends FrameLayout {
|
||||
private static final String LOGTAG = "GeckoPanelLayout";
|
||||
|
||||
protected final SparseArray<ViewState> mViewStates;
|
||||
private final PanelConfig mPanelConfig;
|
||||
private final DatasetHandler mDatasetHandler;
|
||||
private final OnUrlOpenListener mUrlOpenListener;
|
||||
private final ContextMenuRegistry mContextMenuRegistry;
|
||||
|
||||
/**
|
||||
* To be used by panel views to express that they are
|
||||
* backed by datasets.
|
||||
*/
|
||||
public interface DatasetBacked {
|
||||
public void setDataset(Cursor cursor);
|
||||
public void setFilterManager(FilterManager manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used by requests made to {@code DatasetHandler}s to couple dataset ID with current
|
||||
* filter for queries on the database.
|
||||
*/
|
||||
public static class DatasetRequest implements Parcelable {
|
||||
public enum Type implements Parcelable {
|
||||
DATASET_LOAD,
|
||||
FILTER_PUSH,
|
||||
FILTER_POP;
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(ordinal());
|
||||
}
|
||||
|
||||
public static final Creator<Type> CREATOR = new Creator<Type>() {
|
||||
@Override
|
||||
public Type createFromParcel(final Parcel source) {
|
||||
return Type.values()[source.readInt()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type[] newArray(final int size) {
|
||||
return new Type[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private final int mViewIndex;
|
||||
private final Type mType;
|
||||
private final String mDatasetId;
|
||||
private final FilterDetail mFilterDetail;
|
||||
|
||||
private DatasetRequest(Parcel in) {
|
||||
this.mViewIndex = in.readInt();
|
||||
this.mType = (Type) in.readParcelable(getClass().getClassLoader());
|
||||
this.mDatasetId = in.readString();
|
||||
this.mFilterDetail = (FilterDetail) in.readParcelable(getClass().getClassLoader());
|
||||
}
|
||||
|
||||
public DatasetRequest(int index, String datasetId, FilterDetail filterDetail) {
|
||||
this(index, Type.DATASET_LOAD, datasetId, filterDetail);
|
||||
}
|
||||
|
||||
public DatasetRequest(int index, Type type, String datasetId, FilterDetail filterDetail) {
|
||||
this.mViewIndex = index;
|
||||
this.mType = type;
|
||||
this.mDatasetId = datasetId;
|
||||
this.mFilterDetail = filterDetail;
|
||||
}
|
||||
|
||||
public int getViewIndex() {
|
||||
return mViewIndex;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public String getDatasetId() {
|
||||
return mDatasetId;
|
||||
}
|
||||
|
||||
public String getFilter() {
|
||||
return (mFilterDetail != null ? mFilterDetail.filter : null);
|
||||
}
|
||||
|
||||
public FilterDetail getFilterDetail() {
|
||||
return mFilterDetail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(mViewIndex);
|
||||
dest.writeParcelable(mType, 0);
|
||||
dest.writeString(mDatasetId);
|
||||
dest.writeParcelable(mFilterDetail, 0);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "{ index: " + mViewIndex +
|
||||
", type: " + mType +
|
||||
", dataset: " + mDatasetId +
|
||||
", filter: " + mFilterDetail +
|
||||
" }";
|
||||
}
|
||||
|
||||
public static final Creator<DatasetRequest> CREATOR = new Creator<DatasetRequest>() {
|
||||
@Override
|
||||
public DatasetRequest createFromParcel(Parcel in) {
|
||||
return new DatasetRequest(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatasetRequest[] newArray(int size) {
|
||||
return new DatasetRequest[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the contract with the component that is responsible
|
||||
* for handling datasets requests.
|
||||
*/
|
||||
public interface DatasetHandler {
|
||||
/**
|
||||
* Requests a dataset to be fetched and auto-bound to the
|
||||
* panel views backed by it.
|
||||
*/
|
||||
public void requestDataset(DatasetRequest request);
|
||||
|
||||
/**
|
||||
* Releases any resources associated with a panel view. It will
|
||||
* do nothing if the view with the given index been created
|
||||
* before.
|
||||
*/
|
||||
public void resetDataset(int viewIndex);
|
||||
}
|
||||
|
||||
public interface PanelView {
|
||||
public void setOnItemOpenListener(OnItemOpenListener listener);
|
||||
public void setOnKeyListener(OnKeyListener listener);
|
||||
public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory);
|
||||
}
|
||||
|
||||
public interface FilterManager {
|
||||
public FilterDetail getPreviousFilter();
|
||||
public boolean canGoBack();
|
||||
public void goBack();
|
||||
}
|
||||
|
||||
public interface ContextMenuRegistry {
|
||||
public void register(View view);
|
||||
}
|
||||
|
||||
public PanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler,
|
||||
OnUrlOpenListener urlOpenListener, ContextMenuRegistry contextMenuRegistry) {
|
||||
super(context);
|
||||
mViewStates = new SparseArray<ViewState>();
|
||||
mPanelConfig = panelConfig;
|
||||
mDatasetHandler = datasetHandler;
|
||||
mUrlOpenListener = urlOpenListener;
|
||||
mContextMenuRegistry = contextMenuRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
final int count = mViewStates.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final ViewState viewState = mViewStates.valueAt(i);
|
||||
|
||||
final View view = viewState.getView();
|
||||
if (view != null) {
|
||||
maybeSetDataset(view, null);
|
||||
}
|
||||
}
|
||||
mViewStates.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers the dataset as a {@code Cursor} to be bound to the
|
||||
* panel view backed by it. This is used by the {@code DatasetHandler}
|
||||
* in response to a dataset request.
|
||||
*/
|
||||
public final void deliverDataset(DatasetRequest request, Cursor cursor) {
|
||||
Log.d(LOGTAG, "Delivering request: " + request);
|
||||
final ViewState viewState = mViewStates.get(request.getViewIndex());
|
||||
if (viewState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (request.getType()) {
|
||||
case FILTER_PUSH:
|
||||
viewState.pushFilter(request.getFilterDetail());
|
||||
break;
|
||||
case FILTER_POP:
|
||||
viewState.popFilter();
|
||||
break;
|
||||
}
|
||||
|
||||
final View activeView = viewState.getActiveView();
|
||||
if (activeView == null) {
|
||||
throw new IllegalStateException("No active view for view state: " + viewState.getIndex());
|
||||
}
|
||||
|
||||
final ViewConfig viewConfig = viewState.getViewConfig();
|
||||
|
||||
final View newView;
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
newView = createEmptyView(viewConfig);
|
||||
maybeSetDataset(activeView, null);
|
||||
} else {
|
||||
newView = createPanelView(viewConfig);
|
||||
maybeSetDataset(newView, cursor);
|
||||
}
|
||||
|
||||
if (activeView != newView) {
|
||||
replacePanelView(activeView, newView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases any references to the given dataset from all
|
||||
* existing panel views.
|
||||
*/
|
||||
public final void releaseDataset(int viewIndex) {
|
||||
Log.d(LOGTAG, "Releasing dataset: " + viewIndex);
|
||||
final ViewState viewState = mViewStates.get(viewIndex);
|
||||
if (viewState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final View view = viewState.getView();
|
||||
if (view != null) {
|
||||
maybeSetDataset(view, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a dataset to be loaded and bound to any existing
|
||||
* panel view backed by it.
|
||||
*/
|
||||
protected final void requestDataset(DatasetRequest request) {
|
||||
Log.d(LOGTAG, "Requesting request: " + request);
|
||||
if (mViewStates.get(request.getViewIndex()) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDatasetHandler.requestDataset(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases any resources associated with a panel view.
|
||||
* e.g. close any associated {@code Cursor}.
|
||||
*/
|
||||
protected final void resetDataset(int viewIndex) {
|
||||
Log.d(LOGTAG, "Resetting view with index: " + viewIndex);
|
||||
if (mViewStates.get(viewIndex) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDatasetHandler.resetDataset(viewIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create instance of panels from a given
|
||||
* {@code ViewConfig}. All panel views defined in {@code PanelConfig}
|
||||
* should be created using this method so that {@PanelLayout} can
|
||||
* keep track of panel views and their associated datasets.
|
||||
*/
|
||||
protected final View createPanelView(ViewConfig viewConfig) {
|
||||
Log.d(LOGTAG, "Creating panel view: " + viewConfig.getType());
|
||||
|
||||
ViewState viewState = mViewStates.get(viewConfig.getIndex());
|
||||
if (viewState == null) {
|
||||
viewState = new ViewState(viewConfig);
|
||||
mViewStates.put(viewConfig.getIndex(), viewState);
|
||||
}
|
||||
|
||||
View view = viewState.getView();
|
||||
if (view == null) {
|
||||
switch (viewConfig.getType()) {
|
||||
case LIST:
|
||||
view = new PanelListView(getContext(), viewConfig);
|
||||
break;
|
||||
|
||||
case GRID:
|
||||
view = new PanelRecyclerView(getContext(), viewConfig);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized view type in " + getClass().getSimpleName());
|
||||
}
|
||||
|
||||
PanelView panelView = (PanelView) view;
|
||||
panelView.setOnItemOpenListener(new PanelOnItemOpenListener(viewState));
|
||||
panelView.setOnKeyListener(new PanelKeyListener(viewState));
|
||||
panelView.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
|
||||
@Override
|
||||
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
|
||||
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
|
||||
info.url = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.URL));
|
||||
info.title = cursor.getString(cursor.getColumnIndexOrThrow(HomeItems.TITLE));
|
||||
return info;
|
||||
}
|
||||
});
|
||||
|
||||
mContextMenuRegistry.register(view);
|
||||
|
||||
if (view instanceof DatasetBacked) {
|
||||
DatasetBacked datasetBacked = (DatasetBacked) view;
|
||||
datasetBacked.setFilterManager(new PanelFilterManager(viewState));
|
||||
|
||||
if (viewConfig.isRefreshEnabled()) {
|
||||
view = new PanelRefreshLayout(getContext(), view,
|
||||
mPanelConfig.getId(), viewConfig.getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
viewState.setView(view);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose any dataset references associated with the
|
||||
* given view.
|
||||
*/
|
||||
protected final void disposePanelView(View view) {
|
||||
Log.d(LOGTAG, "Disposing panel view");
|
||||
final int count = mViewStates.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final ViewState viewState = mViewStates.valueAt(i);
|
||||
|
||||
if (viewState.getView() == view) {
|
||||
maybeSetDataset(view, null);
|
||||
mViewStates.remove(viewState.getIndex());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSetDataset(View view, Cursor cursor) {
|
||||
if (view instanceof DatasetBacked) {
|
||||
final DatasetBacked dsb = (DatasetBacked) view;
|
||||
dsb.setDataset(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
private View createEmptyView(ViewConfig viewConfig) {
|
||||
Log.d(LOGTAG, "Creating empty view: " + viewConfig.getType());
|
||||
|
||||
ViewState viewState = mViewStates.get(viewConfig.getIndex());
|
||||
if (viewState == null) {
|
||||
throw new IllegalStateException("No view state found for view index: " + viewConfig.getIndex());
|
||||
}
|
||||
|
||||
View view = viewState.getEmptyView();
|
||||
if (view == null) {
|
||||
view = LayoutInflater.from(getContext()).inflate(R.layout.home_empty_panel, null);
|
||||
|
||||
final EmptyViewConfig emptyViewConfig = viewConfig.getEmptyViewConfig();
|
||||
|
||||
// XXX: Refactor this into a custom view (bug 985134)
|
||||
final String text = (emptyViewConfig == null) ? null : emptyViewConfig.getText();
|
||||
final TextView textView = (TextView) view.findViewById(R.id.home_empty_text);
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
textView.setText(R.string.home_default_empty);
|
||||
} else {
|
||||
textView.setText(text);
|
||||
}
|
||||
|
||||
final String imageUrl = (emptyViewConfig == null) ? null : emptyViewConfig.getImageUrl();
|
||||
final ImageView imageView = (ImageView) view.findViewById(R.id.home_empty_image);
|
||||
|
||||
if (TextUtils.isEmpty(imageUrl)) {
|
||||
imageView.setImageResource(R.drawable.icon_home_empty_firefox);
|
||||
} else {
|
||||
ImageLoader.with(getContext())
|
||||
.load(imageUrl)
|
||||
.error(R.drawable.icon_home_empty_firefox)
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
viewState.setEmptyView(view);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void replacePanelView(View currentView, View newView) {
|
||||
final ViewGroup parent = (ViewGroup) currentView.getParent();
|
||||
parent.addView(newView, parent.indexOfChild(currentView), currentView.getLayoutParams());
|
||||
parent.removeView(currentView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be implemented by {@code PanelLayout} subclasses to define
|
||||
* what happens then the layout is first loaded. Should set initial
|
||||
* UI state and request any necessary datasets.
|
||||
*/
|
||||
public abstract void load();
|
||||
|
||||
/**
|
||||
* Represents a 'live' instance of a panel view associated with
|
||||
* the {@code PanelLayout}. Is responsible for tracking the history stack of filters.
|
||||
*/
|
||||
protected class ViewState {
|
||||
private final ViewConfig mViewConfig;
|
||||
private SoftReference<View> mView;
|
||||
private SoftReference<View> mEmptyView;
|
||||
private LinkedList<FilterDetail> mFilterStack;
|
||||
|
||||
public ViewState(ViewConfig viewConfig) {
|
||||
mViewConfig = viewConfig;
|
||||
mView = new SoftReference<View>(null);
|
||||
mEmptyView = new SoftReference<View>(null);
|
||||
}
|
||||
|
||||
public ViewConfig getViewConfig() {
|
||||
return mViewConfig;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return mViewConfig.getIndex();
|
||||
}
|
||||
|
||||
public View getView() {
|
||||
return mView.get();
|
||||
}
|
||||
|
||||
public void setView(View view) {
|
||||
mView = new SoftReference<View>(view);
|
||||
}
|
||||
|
||||
public View getEmptyView() {
|
||||
return mEmptyView.get();
|
||||
}
|
||||
|
||||
public void setEmptyView(View view) {
|
||||
mEmptyView = new SoftReference<View>(view);
|
||||
}
|
||||
|
||||
public View getActiveView() {
|
||||
final View view = getView();
|
||||
if (view != null && view.getParent() != null) {
|
||||
return view;
|
||||
}
|
||||
|
||||
final View emptyView = getEmptyView();
|
||||
if (emptyView != null && emptyView.getParent() != null) {
|
||||
return emptyView;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getDatasetId() {
|
||||
return mViewConfig.getDatasetId();
|
||||
}
|
||||
|
||||
public ItemHandler getItemHandler() {
|
||||
return mViewConfig.getItemHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current filter that this view is displaying, or null if none.
|
||||
*/
|
||||
public FilterDetail getCurrentFilter() {
|
||||
if (mFilterStack == null) {
|
||||
return null;
|
||||
} else {
|
||||
return mFilterStack.peek();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous filter that this view was displaying, or null if none.
|
||||
*/
|
||||
public FilterDetail getPreviousFilter() {
|
||||
if (!canPopFilter()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mFilterStack.get(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a filter to the history stack for this view.
|
||||
*/
|
||||
public void pushFilter(FilterDetail filter) {
|
||||
if (mFilterStack == null) {
|
||||
mFilterStack = new LinkedList<FilterDetail>();
|
||||
|
||||
// Initialize with the initial filter.
|
||||
mFilterStack.push(new FilterDetail(mViewConfig.getFilter(),
|
||||
mPanelConfig.getTitle()));
|
||||
}
|
||||
|
||||
mFilterStack.push(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the most recent filter from the stack.
|
||||
*
|
||||
* @return whether the filter was popped
|
||||
*/
|
||||
public boolean popFilter() {
|
||||
if (!canPopFilter()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mFilterStack.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canPopFilter() {
|
||||
return (mFilterStack != null && mFilterStack.size() > 1);
|
||||
}
|
||||
}
|
||||
|
||||
static class FilterDetail implements Parcelable {
|
||||
final String filter;
|
||||
final String title;
|
||||
|
||||
private FilterDetail(Parcel in) {
|
||||
this.filter = in.readString();
|
||||
this.title = in.readString();
|
||||
}
|
||||
|
||||
public FilterDetail(String filter, String title) {
|
||||
this.filter = filter;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(filter);
|
||||
dest.writeString(title);
|
||||
}
|
||||
|
||||
public static final Creator<FilterDetail> CREATOR = new Creator<FilterDetail>() {
|
||||
@Override
|
||||
public FilterDetail createFromParcel(Parcel in) {
|
||||
return new FilterDetail(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterDetail[] newArray(int size) {
|
||||
return new FilterDetail[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes filter to {@code ViewState}'s stack and makes request for new filter value.
|
||||
*/
|
||||
private void pushFilterOnView(ViewState viewState, FilterDetail filterDetail) {
|
||||
final int index = viewState.getIndex();
|
||||
final String datasetId = viewState.getDatasetId();
|
||||
|
||||
mDatasetHandler.requestDataset(new DatasetRequest(index,
|
||||
DatasetRequest.Type.FILTER_PUSH,
|
||||
datasetId,
|
||||
filterDetail));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops filter from {@code ViewState}'s stack and makes request for previous filter value.
|
||||
*
|
||||
* @return whether the filter has changed
|
||||
*/
|
||||
private boolean popFilterOnView(ViewState viewState) {
|
||||
if (viewState.canPopFilter()) {
|
||||
final int index = viewState.getIndex();
|
||||
final String datasetId = viewState.getDatasetId();
|
||||
final FilterDetail filterDetail = viewState.getPreviousFilter();
|
||||
|
||||
mDatasetHandler.requestDataset(new DatasetRequest(index,
|
||||
DatasetRequest.Type.FILTER_POP,
|
||||
datasetId,
|
||||
filterDetail));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnItemOpenListener {
|
||||
public void onItemOpen(String url, String title);
|
||||
}
|
||||
|
||||
private class PanelOnItemOpenListener implements OnItemOpenListener {
|
||||
private final ViewState mViewState;
|
||||
|
||||
public PanelOnItemOpenListener(ViewState viewState) {
|
||||
mViewState = viewState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemOpen(String url, String title) {
|
||||
if (StringUtils.isFilterUrl(url)) {
|
||||
FilterDetail filterDetail = new FilterDetail(StringUtils.getFilterFromUrl(url), title);
|
||||
pushFilterOnView(mViewState, filterDetail);
|
||||
} else {
|
||||
EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
|
||||
if (mViewState.getItemHandler() == ItemHandler.INTENT) {
|
||||
flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
|
||||
}
|
||||
|
||||
mUrlOpenListener.onUrlOpen(url, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PanelKeyListener implements View.OnKeyListener {
|
||||
private final ViewState mViewState;
|
||||
|
||||
public PanelKeyListener(ViewState viewState) {
|
||||
mViewState = viewState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
return popFilterOnView(mViewState);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class PanelFilterManager implements FilterManager {
|
||||
private final ViewState mViewState;
|
||||
|
||||
public PanelFilterManager(ViewState viewState) {
|
||||
mViewState = viewState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterDetail getPreviousFilter() {
|
||||
return mViewState.getPreviousFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canGoBack() {
|
||||
return mViewState.canPopFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goBack() {
|
||||
popFilterOnView(mViewState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/* -*- 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 java.util.EnumSet;
|
||||
|
||||
import org.mozilla.gecko.db.BrowserContract.HomeItems;
|
||||
import org.mozilla.gecko.home.HomeConfig.ItemHandler;
|
||||
import org.mozilla.gecko.home.HomeConfig.ViewConfig;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
|
||||
import org.mozilla.gecko.home.PanelLayout.FilterManager;
|
||||
import org.mozilla.gecko.home.PanelLayout.OnItemOpenListener;
|
||||
import org.mozilla.gecko.home.PanelLayout.PanelView;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
public class PanelListView extends HomeListView
|
||||
implements DatasetBacked, PanelView {
|
||||
|
||||
private static final String LOGTAG = "GeckoPanelListView";
|
||||
|
||||
private final ViewConfig viewConfig;
|
||||
private final PanelViewAdapter adapter;
|
||||
private final PanelViewItemHandler itemHandler;
|
||||
private OnItemOpenListener itemOpenListener;
|
||||
|
||||
public PanelListView(Context context, ViewConfig viewConfig) {
|
||||
super(context);
|
||||
|
||||
this.viewConfig = viewConfig;
|
||||
itemHandler = new PanelViewItemHandler();
|
||||
|
||||
adapter = new PanelViewAdapter(context, viewConfig);
|
||||
setAdapter(adapter);
|
||||
|
||||
setOnItemClickListener(new PanelListItemClickListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
itemHandler.setOnItemOpenListener(itemOpenListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
itemHandler.setOnItemOpenListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDataset(Cursor cursor) {
|
||||
Log.d(LOGTAG, "Setting dataset: " + viewConfig.getDatasetId());
|
||||
adapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnItemOpenListener(OnItemOpenListener listener) {
|
||||
itemHandler.setOnItemOpenListener(listener);
|
||||
itemOpenListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilterManager(FilterManager filterManager) {
|
||||
adapter.setFilterManager(filterManager);
|
||||
itemHandler.setFilterManager(filterManager);
|
||||
}
|
||||
|
||||
private class PanelListItemClickListener implements AdapterView.OnItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
itemHandler.openItemAtPosition(adapter.getCursor(), position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
/* -*- 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.home.PanelLayout.DatasetBacked;
|
||||
import org.mozilla.gecko.home.PanelLayout.PanelView;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemClickListener;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport.OnItemLongClickListener;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* RecyclerView implementation for grid home panels.
|
||||
*/
|
||||
@SuppressLint("ViewConstructor") // View is only created from code
|
||||
public class PanelRecyclerView extends RecyclerView
|
||||
implements DatasetBacked, PanelView, OnItemClickListener, OnItemLongClickListener {
|
||||
private final PanelRecyclerViewAdapter adapter;
|
||||
private final GridLayoutManager layoutManager;
|
||||
private final PanelViewItemHandler itemHandler;
|
||||
private final float columnWidth;
|
||||
private final boolean autoFit;
|
||||
private final HomeConfig.ViewConfig viewConfig;
|
||||
|
||||
private PanelLayout.OnItemOpenListener itemOpenListener;
|
||||
private HomeContextMenuInfo contextMenuInfo;
|
||||
private HomeContextMenuInfo.Factory contextMenuInfoFactory;
|
||||
|
||||
public PanelRecyclerView(Context context, HomeConfig.ViewConfig viewConfig) {
|
||||
super(context);
|
||||
|
||||
this.viewConfig = viewConfig;
|
||||
|
||||
final Resources resources = context.getResources();
|
||||
|
||||
int spanCount;
|
||||
if (viewConfig.getItemType() == HomeConfig.ItemType.ICON) {
|
||||
autoFit = false;
|
||||
spanCount = getResources().getInteger(R.integer.panel_icon_grid_view_columns);
|
||||
} else {
|
||||
autoFit = true;
|
||||
spanCount = 1;
|
||||
}
|
||||
|
||||
columnWidth = resources.getDimension(R.dimen.panel_grid_view_column_width);
|
||||
layoutManager = new GridLayoutManager(context, spanCount);
|
||||
adapter = new PanelRecyclerViewAdapter(context, viewConfig);
|
||||
itemHandler = new PanelViewItemHandler();
|
||||
|
||||
layoutManager.setSpanSizeLookup(new PanelSpanSizeLookup());
|
||||
|
||||
setLayoutManager(layoutManager);
|
||||
setAdapter(adapter);
|
||||
|
||||
int horizontalSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_horizontal_spacing);
|
||||
int verticalSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_vertical_spacing);
|
||||
int outerSpacing = (int) resources.getDimension(R.dimen.panel_grid_view_outer_spacing);
|
||||
|
||||
addItemDecoration(new SpacingDecoration(horizontalSpacing, verticalSpacing));
|
||||
|
||||
setPadding(outerSpacing, outerSpacing, outerSpacing, outerSpacing);
|
||||
setClipToPadding(false);
|
||||
|
||||
RecyclerViewClickSupport.addTo(this)
|
||||
.setOnItemClickListener(this)
|
||||
.setOnItemLongClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec) {
|
||||
super.onMeasure(widthSpec, heightSpec);
|
||||
|
||||
if (autoFit) {
|
||||
// Adjust span based on space available (What GridView does when you say numColumns="auto_fit")
|
||||
final int spanCount = (int) Math.max(1, getMeasuredWidth() / columnWidth);
|
||||
layoutManager.setSpanCount(spanCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
itemHandler.setOnItemOpenListener(itemOpenListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
itemHandler.setOnItemOpenListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDataset(Cursor cursor) {
|
||||
adapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilterManager(PanelLayout.FilterManager manager) {
|
||||
adapter.setFilterManager(manager);
|
||||
itemHandler.setFilterManager(manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnItemOpenListener(PanelLayout.OnItemOpenListener listener) {
|
||||
itemOpenListener = listener;
|
||||
itemHandler.setOnItemOpenListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeContextMenuInfo getContextMenuInfo() {
|
||||
return contextMenuInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContextMenuInfoFactory(HomeContextMenuInfo.Factory factory) {
|
||||
contextMenuInfoFactory = factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||
if (viewConfig.hasHeaderConfig()) {
|
||||
if (position == 0) {
|
||||
itemOpenListener.onItemOpen(viewConfig.getHeaderConfig().getUrl(), null);
|
||||
return;
|
||||
}
|
||||
|
||||
position--;
|
||||
}
|
||||
|
||||
itemHandler.openItemAtPosition(adapter.getCursor(), position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
|
||||
if (viewConfig.hasHeaderConfig()) {
|
||||
if (position == 0) {
|
||||
final HomeConfig.HeaderConfig headerConfig = viewConfig.getHeaderConfig();
|
||||
|
||||
final HomeContextMenuInfo info = new HomeContextMenuInfo(v, position, -1);
|
||||
info.url = headerConfig.getUrl();
|
||||
info.title = headerConfig.getUrl();
|
||||
|
||||
contextMenuInfo = info;
|
||||
return showContextMenuForChild(this);
|
||||
}
|
||||
|
||||
position--;
|
||||
}
|
||||
|
||||
Cursor cursor = adapter.getCursor();
|
||||
cursor.moveToPosition(position);
|
||||
|
||||
contextMenuInfo = contextMenuInfoFactory.makeInfoForCursor(recyclerView, position, -1, cursor);
|
||||
return showContextMenuForChild(this);
|
||||
}
|
||||
|
||||
private class PanelSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
if (position == 0 && viewConfig.hasHeaderConfig()) {
|
||||
return layoutManager.getSpanCount();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/* -*- 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.database.Cursor;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
public class PanelRecyclerViewAdapter extends RecyclerView.Adapter<PanelRecyclerViewAdapter.PanelViewHolder> {
|
||||
private static final int VIEW_TYPE_ITEM = 0;
|
||||
private static final int VIEW_TYPE_BACK = 1;
|
||||
private static final int VIEW_TYPE_HEADER = 2;
|
||||
|
||||
public static class PanelViewHolder extends RecyclerView.ViewHolder {
|
||||
public static PanelViewHolder create(View itemView) {
|
||||
|
||||
// Wrap in a FrameLayout that will handle the highlight on touch
|
||||
FrameLayout frameLayout = (FrameLayout) LayoutInflater.from(itemView.getContext())
|
||||
.inflate(R.layout.panel_item_container, null);
|
||||
|
||||
frameLayout.addView(itemView, 0, new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
return new PanelViewHolder(frameLayout);
|
||||
}
|
||||
|
||||
private PanelViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final HomeConfig.ViewConfig viewConfig;
|
||||
private PanelLayout.FilterManager filterManager;
|
||||
private Cursor cursor;
|
||||
|
||||
public PanelRecyclerViewAdapter(Context context, HomeConfig.ViewConfig viewConfig) {
|
||||
this.context = context;
|
||||
this.viewConfig = viewConfig;
|
||||
}
|
||||
|
||||
public void setFilterManager(PanelLayout.FilterManager filterManager) {
|
||||
this.filterManager = filterManager;
|
||||
}
|
||||
|
||||
private boolean isShowingBack() {
|
||||
return filterManager != null && filterManager.canGoBack();
|
||||
}
|
||||
|
||||
public void swapCursor(Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public Cursor getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (viewConfig.hasHeaderConfig() && position == 0) {
|
||||
return VIEW_TYPE_HEADER;
|
||||
} else if (isShowingBack() && position == getBackPosition()) {
|
||||
return VIEW_TYPE_BACK;
|
||||
} else {
|
||||
return VIEW_TYPE_ITEM;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PanelViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_HEADER:
|
||||
return PanelViewHolder.create(new PanelHeaderView(context, viewConfig.getHeaderConfig()));
|
||||
case VIEW_TYPE_BACK:
|
||||
return PanelViewHolder.create(new PanelBackItemView(context, viewConfig.getBackImageUrl()));
|
||||
case VIEW_TYPE_ITEM:
|
||||
return PanelViewHolder.create(PanelItemView.create(context, viewConfig.getItemType()));
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown view type: " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PanelViewHolder panelViewHolder, int position) {
|
||||
final View view = ((FrameLayout) panelViewHolder.itemView).getChildAt(0);
|
||||
|
||||
if (viewConfig.hasHeaderConfig()) {
|
||||
if (position == 0) {
|
||||
// Nothing to do here, the header is static
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isShowingBack()) {
|
||||
if (position == getBackPosition()) {
|
||||
final PanelBackItemView item = (PanelBackItemView) view;
|
||||
item.updateFromFilter(filterManager.getPreviousFilter());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int actualPosition = position
|
||||
- (isShowingBack() ? 1 : 0)
|
||||
- (viewConfig.hasHeaderConfig() ? 1 : 0);
|
||||
|
||||
cursor.moveToPosition(actualPosition);
|
||||
|
||||
final PanelItemView panelItemView = (PanelItemView) view;
|
||||
panelItemView.updateFromCursor(cursor);
|
||||
}
|
||||
|
||||
private int getBackPosition() {
|
||||
return viewConfig.hasHeaderConfig() ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (cursor == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return cursor.getCount()
|
||||
+ (isShowingBack() ? 1 : 0)
|
||||
+ (viewConfig.hasHeaderConfig() ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/* -*- 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.GeckoAppShell;
|
||||
import org.mozilla.gecko.home.PanelLayout.DatasetBacked;
|
||||
import org.mozilla.gecko.home.PanelLayout.FilterManager;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Used to wrap a {@code DatasetBacked} ListView or GridView to give the child view swipe-to-refresh
|
||||
* capabilities.
|
||||
*
|
||||
* This view acts as a decorator to forward the {@code DatasetBacked} methods to the child view
|
||||
* while providing the refresh gesture support on top of it.
|
||||
*/
|
||||
class PanelRefreshLayout extends SwipeRefreshLayout implements DatasetBacked {
|
||||
private static final String LOGTAG = "GeckoPanelRefreshLayout";
|
||||
|
||||
private static final String JSON_KEY_PANEL_ID = "panelId";
|
||||
private static final String JSON_KEY_VIEW_INDEX = "viewIndex";
|
||||
|
||||
private final String panelId;
|
||||
private final int viewIndex;
|
||||
private final DatasetBacked datasetBacked;
|
||||
|
||||
/**
|
||||
* @param context Android context.
|
||||
* @param childView ListView or GridView. Must implement {@code DatasetBacked}.
|
||||
* @param panelId The ID from the {@code PanelConfig}.
|
||||
* @param viewIndex The index from the {@code ViewConfig}.
|
||||
*/
|
||||
public PanelRefreshLayout(Context context, View childView, String panelId, int viewIndex) {
|
||||
super(context);
|
||||
|
||||
if (!(childView instanceof DatasetBacked)) {
|
||||
throw new IllegalArgumentException("View must implement DatasetBacked to be refreshed");
|
||||
}
|
||||
|
||||
this.panelId = panelId;
|
||||
this.viewIndex = viewIndex;
|
||||
this.datasetBacked = (DatasetBacked) childView;
|
||||
|
||||
setOnRefreshListener(new RefreshListener());
|
||||
addView(childView);
|
||||
|
||||
// Must be called after the child view has been added.
|
||||
setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDataset(Cursor cursor) {
|
||||
datasetBacked.setDataset(cursor);
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilterManager(FilterManager manager) {
|
||||
datasetBacked.setFilterManager(manager);
|
||||
}
|
||||
|
||||
private class RefreshListener implements OnRefreshListener {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
final JSONObject response = new JSONObject();
|
||||
try {
|
||||
response.put(JSON_KEY_PANEL_ID, panelId);
|
||||
response.put(JSON_KEY_VIEW_INDEX, viewIndex);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "Could not create refresh message", e);
|
||||
return;
|
||||
}
|
||||
|
||||
GeckoAppShell.notifyObservers("HomePanels:RefreshView", response.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.home.HomeConfig.ItemType;
|
||||
import org.mozilla.gecko.home.HomeConfig.ViewConfig;
|
||||
import org.mozilla.gecko.home.PanelLayout.FilterManager;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
class PanelViewAdapter extends CursorAdapter {
|
||||
private static final int VIEW_TYPE_ITEM = 0;
|
||||
private static final int VIEW_TYPE_BACK = 1;
|
||||
|
||||
private final ViewConfig viewConfig;
|
||||
private FilterManager filterManager;
|
||||
private final Context context;
|
||||
|
||||
public PanelViewAdapter(Context context, ViewConfig viewConfig) {
|
||||
super(context, null, 0);
|
||||
this.context = context;
|
||||
this.viewConfig = viewConfig;
|
||||
}
|
||||
|
||||
public void setFilterManager(FilterManager manager) {
|
||||
this.filterManager = manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getViewTypeCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return super.getCount() + (isShowingBack() ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (isShowingBack() && position == 0) {
|
||||
return VIEW_TYPE_BACK;
|
||||
} else {
|
||||
return VIEW_TYPE_ITEM;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = newView(parent.getContext(), position, parent);
|
||||
}
|
||||
|
||||
bindView(convertView, position);
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private View newView(Context context, int position, ViewGroup parent) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_BACK) {
|
||||
return new PanelBackItemView(context, viewConfig.getBackImageUrl());
|
||||
} else {
|
||||
return PanelItemView.create(context, viewConfig.getItemType());
|
||||
}
|
||||
}
|
||||
|
||||
private void bindView(View view, int position) {
|
||||
if (isShowingBack()) {
|
||||
if (position == 0) {
|
||||
final PanelBackItemView item = (PanelBackItemView) view;
|
||||
item.updateFromFilter(filterManager.getPreviousFilter());
|
||||
return;
|
||||
}
|
||||
|
||||
position--;
|
||||
}
|
||||
|
||||
final Cursor cursor = getCursor(position);
|
||||
final PanelItemView item = (PanelItemView) view;
|
||||
item.updateFromCursor(cursor);
|
||||
}
|
||||
|
||||
private boolean isShowingBack() {
|
||||
return filterManager != null && filterManager.canGoBack();
|
||||
}
|
||||
|
||||
private 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 void bindView(View view, Context context, Cursor cursor) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public final View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/* -*- 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.BrowserContract.HomeItems;
|
||||
import org.mozilla.gecko.home.HomeConfig.ViewConfig;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.home.PanelLayout.FilterManager;
|
||||
import org.mozilla.gecko.home.PanelLayout.OnItemOpenListener;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
class PanelViewItemHandler {
|
||||
private OnItemOpenListener mItemOpenListener;
|
||||
private FilterManager mFilterManager;
|
||||
|
||||
public void setOnItemOpenListener(OnItemOpenListener listener) {
|
||||
mItemOpenListener = listener;
|
||||
}
|
||||
|
||||
public void setFilterManager(FilterManager manager) {
|
||||
mFilterManager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* If item at this position is a back item, perform the go back action via the
|
||||
* {@code FilterManager}. Otherwise, prepare the url to be opened by the
|
||||
* {@code OnUrlOpenListener}.
|
||||
*/
|
||||
public void openItemAtPosition(Cursor cursor, int position) {
|
||||
if (mFilterManager != null && mFilterManager.canGoBack()) {
|
||||
if (position == 0) {
|
||||
mFilterManager.goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
position--;
|
||||
}
|
||||
|
||||
if (cursor == null || !cursor.moveToPosition(position)) {
|
||||
throw new IllegalStateException("Couldn't move cursor to position " + position);
|
||||
}
|
||||
|
||||
int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
|
||||
final String url = cursor.getString(urlIndex);
|
||||
|
||||
int titleIndex = cursor.getColumnIndexOrThrow(HomeItems.TITLE);
|
||||
final String title = cursor.getString(titleIndex);
|
||||
|
||||
if (mItemOpenListener != null) {
|
||||
mItemOpenListener.onItemOpen(url, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
/* -*- 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 java.util.EnumSet;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserContract.URLColumns;
|
||||
import org.mozilla.gecko.db.BrowserDB.FilterFlags;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
import android.app.Dialog;
|
||||
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.app.LoaderManager.LoaderCallbacks;
|
||||
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.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
|
||||
/**
|
||||
* Dialog fragment that displays frecency search results, for pinning a site, in a GridView.
|
||||
*/
|
||||
class PinSiteDialog extends DialogFragment {
|
||||
// Listener for url selection
|
||||
public static interface OnSiteSelectedListener {
|
||||
public void onSiteSelected(String url, String title);
|
||||
}
|
||||
|
||||
// Cursor loader ID for search query
|
||||
private static final int LOADER_ID_SEARCH = 0;
|
||||
|
||||
// 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 loader
|
||||
private CursorLoaderCallbacks mLoaderCallbacks;
|
||||
|
||||
// Bookmark selected listener
|
||||
private OnSiteSelectedListener mOnSiteSelectedListener;
|
||||
|
||||
public static PinSiteDialog newInstance() {
|
||||
return new PinSiteDialog();
|
||||
}
|
||||
|
||||
private PinSiteDialog() {
|
||||
}
|
||||
|
||||
@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_site_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) {
|
||||
setSearchTerm(mSearch.getText().toString());
|
||||
filter(mSearchTerm);
|
||||
}
|
||||
});
|
||||
|
||||
mSearch.setOnKeyListener(new View.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (keyCode != KeyEvent.KEYCODE_ENTER || mOnSiteSelectedListener == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the user manually entered a search term or URL, wrap the value in
|
||||
// a special URI until we can get a valid URL for this bookmark.
|
||||
final String text = mSearch.getText().toString().trim();
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
final String url = StringUtils.encodeUserEnteredUrl(text);
|
||||
mOnSiteSelectedListener.onSiteSelected(url, text);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
mSearch.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (hasFocus) {
|
||||
// On rotation, the view gets destroyed and we could be in a race to get the dialog
|
||||
// and window (see bug 1072959).
|
||||
Dialog dialog = getDialog();
|
||||
if (dialog == null) {
|
||||
return;
|
||||
}
|
||||
Window window = dialog.getWindow();
|
||||
if (window == null) {
|
||||
return;
|
||||
}
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 (mOnSiteSelectedListener != 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));
|
||||
mOnSiteSelectedListener.onSiteSelected(url, title);
|
||||
}
|
||||
|
||||
// Dismiss the fragment and the dialog.
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
final LoaderManager manager = getLoaderManager();
|
||||
|
||||
// Initialize the search adapter
|
||||
mAdapter = new SearchAdapter(getActivity());
|
||||
mList.setAdapter(mAdapter);
|
||||
|
||||
// Create callbacks before the initial loader is started
|
||||
mLoaderCallbacks = new CursorLoaderCallbacks();
|
||||
|
||||
// Reconnect to the loader only if present
|
||||
manager.initLoader(LOADER_ID_SEARCH, null, mLoaderCallbacks);
|
||||
|
||||
// If there is a search term, put it in the text field
|
||||
if (!TextUtils.isEmpty(mSearchTerm)) {
|
||||
mSearch.setText(mSearchTerm);
|
||||
mSearch.selectAll();
|
||||
}
|
||||
|
||||
// Always start with an empty filter
|
||||
filter("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
// Discard any additional site selection as the dialog
|
||||
// is getting destroyed (see bug 935542).
|
||||
setOnSiteSelectedListener(null);
|
||||
}
|
||||
|
||||
public void setSearchTerm(String searchTerm) {
|
||||
mSearchTerm = searchTerm;
|
||||
}
|
||||
|
||||
private void filter(String searchTerm) {
|
||||
// Restart loaders with the new search term
|
||||
SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH,
|
||||
mLoaderCallbacks, searchTerm,
|
||||
EnumSet.of(FilterFlags.EXCLUDE_PINNED_SITES));
|
||||
}
|
||||
|
||||
public void setOnSiteSelectedListener(OnSiteSelectedListener listener) {
|
||||
mOnSiteSelectedListener = listener;
|
||||
}
|
||||
|
||||
private static class SearchAdapter extends CursorAdapter {
|
||||
private final LayoutInflater mInflater;
|
||||
|
||||
public SearchAdapter(Context context) {
|
||||
super(context, null, 0);
|
||||
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 implements LoaderCallbacks<Cursor> {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return SearchLoader.createInstance(getActivity(), args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
||||
mAdapter.swapCursor(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
mAdapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,454 @@
|
|||
/* -*- 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.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.AboutPages;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.SessionParser;
|
||||
import org.mozilla.gecko.home.CombinedHistoryAdapter.RecentTabsUpdateHandler;
|
||||
import org.mozilla.gecko.home.CombinedHistoryPanel.PanelStateUpdateHandler;
|
||||
import org.mozilla.gecko.util.EventCallback;
|
||||
import org.mozilla.gecko.util.NativeEventListener;
|
||||
import org.mozilla.gecko.util.NativeJSObject;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType;
|
||||
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS;
|
||||
|
||||
public class RecentTabsAdapter extends RecyclerView.Adapter<CombinedHistoryItem>
|
||||
implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder, NativeEventListener {
|
||||
private static final String LOGTAG = "GeckoRecentTabsAdapter";
|
||||
|
||||
private static final int NAVIGATION_BACK_BUTTON_INDEX = 0;
|
||||
|
||||
private static final String TELEMETRY_EXTRA_LAST_TIME = "recent_tabs_last_time";
|
||||
private static final String TELEMETRY_EXTRA_RECENTLY_CLOSED = "recent_closed_tabs";
|
||||
private static final String TELEMETRY_EXTRA_MIXED = "recent_tabs_mixed";
|
||||
|
||||
// Recently closed tabs from Gecko.
|
||||
private ClosedTab[] recentlyClosedTabs;
|
||||
private boolean recentlyClosedTabsReceived = false;
|
||||
|
||||
// "Tabs from last time".
|
||||
private ClosedTab[] lastSessionTabs;
|
||||
|
||||
public static final class ClosedTab {
|
||||
public final String url;
|
||||
public final String title;
|
||||
public final String data;
|
||||
|
||||
public ClosedTab(String url, String title, String data) {
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final RecentTabsUpdateHandler recentTabsUpdateHandler;
|
||||
private final PanelStateUpdateHandler panelStateUpdateHandler;
|
||||
|
||||
public RecentTabsAdapter(Context context,
|
||||
RecentTabsUpdateHandler recentTabsUpdateHandler,
|
||||
PanelStateUpdateHandler panelStateUpdateHandler) {
|
||||
this.context = context;
|
||||
this.recentTabsUpdateHandler = recentTabsUpdateHandler;
|
||||
this.panelStateUpdateHandler = panelStateUpdateHandler;
|
||||
recentlyClosedTabs = new ClosedTab[0];
|
||||
lastSessionTabs = new ClosedTab[0];
|
||||
|
||||
readPreviousSessionData();
|
||||
}
|
||||
|
||||
public void startListeningForClosedTabs() {
|
||||
EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
|
||||
GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null);
|
||||
}
|
||||
|
||||
public void stopListeningForClosedTabs() {
|
||||
GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null);
|
||||
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
|
||||
recentlyClosedTabsReceived = false;
|
||||
}
|
||||
|
||||
public void startListeningForHistorySanitize() {
|
||||
EventDispatcher.getInstance().registerGeckoThreadListener(this, "Sanitize:Finished");
|
||||
}
|
||||
|
||||
public void stopListeningForHistorySanitize() {
|
||||
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Sanitize:Finished");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
|
||||
switch (event) {
|
||||
case "ClosedTabs:Data":
|
||||
updateRecentlyClosedTabs(message);
|
||||
break;
|
||||
case "Sanitize:Finished":
|
||||
clearLastSessionData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRecentlyClosedTabs(NativeJSObject message) {
|
||||
final NativeJSObject[] tabs = message.getObjectArray("tabs");
|
||||
final int length = tabs.length;
|
||||
|
||||
final ClosedTab[] closedTabs = new ClosedTab[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
final NativeJSObject tab = tabs[i];
|
||||
closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"), tab.getObject("data").toString());
|
||||
}
|
||||
|
||||
// Only modify recentlyClosedTabs on the UI thread.
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Save some data about the old panel state, so we can be
|
||||
// smarter about notifying the recycler view which bits changed.
|
||||
int prevClosedTabsCount = recentlyClosedTabs.length;
|
||||
boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
|
||||
int prevSectionHeaderIndex = getSectionHeaderIndex();
|
||||
|
||||
recentlyClosedTabs = closedTabs;
|
||||
recentlyClosedTabsReceived = true;
|
||||
recentTabsUpdateHandler.onRecentTabsCountUpdated(
|
||||
getClosedTabsCount(), recentlyClosedTabsReceived);
|
||||
panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS);
|
||||
|
||||
// Handle the section header hiding/unhiding.
|
||||
updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
|
||||
|
||||
// Update the "Recently closed" part of the tab list.
|
||||
updateTabsList(prevClosedTabsCount, recentlyClosedTabs.length, getFirstRecentTabIndex(), getLastRecentTabIndex());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void readPreviousSessionData() {
|
||||
// If we happen to initialise before GeckoApp, waiting on either the main or the background
|
||||
// thread can lead to a deadlock, so we have to run on a separate thread instead.
|
||||
final Thread parseThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Make sure that the start up code has had a chance to update sessionstore.old as necessary.
|
||||
GeckoProfile.get(context).waitForOldSessionDataProcessing();
|
||||
|
||||
final String jsonString = GeckoProfile.get(context).readPreviousSessionFile();
|
||||
if (jsonString == null) {
|
||||
// No previous session data.
|
||||
return;
|
||||
}
|
||||
|
||||
final List<ClosedTab> parsedTabs = new ArrayList<>();
|
||||
|
||||
new SessionParser() {
|
||||
@Override
|
||||
public void onTabRead(SessionTab tab) {
|
||||
final String url = tab.getUrl();
|
||||
|
||||
// Don't show last tabs for about:home
|
||||
if (AboutPages.isAboutHome(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
parsedTabs.add(new ClosedTab(url, tab.getTitle(), tab.getTabObject().toString()));
|
||||
}
|
||||
}.parse(jsonString);
|
||||
|
||||
final ClosedTab[] closedTabs = parsedTabs.toArray(new ClosedTab[parsedTabs.size()]);
|
||||
|
||||
// Only modify lastSessionTabs on the UI thread.
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Save some data about the old panel state, so we can be
|
||||
// smarter about notifying the recycler view which bits changed.
|
||||
int prevClosedTabsCount = lastSessionTabs.length;
|
||||
boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
|
||||
int prevSectionHeaderIndex = getSectionHeaderIndex();
|
||||
|
||||
lastSessionTabs = closedTabs;
|
||||
recentTabsUpdateHandler.onRecentTabsCountUpdated(
|
||||
getClosedTabsCount(), recentlyClosedTabsReceived);
|
||||
panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS);
|
||||
|
||||
// Handle the section header hiding/unhiding.
|
||||
updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
|
||||
|
||||
// Update the "Tabs from last time" part of the tab list.
|
||||
updateTabsList(prevClosedTabsCount, lastSessionTabs.length, getFirstLastSessionTabIndex(), getLastLastSessionTabIndex());
|
||||
}
|
||||
});
|
||||
}
|
||||
}, "LastSessionTabsThread");
|
||||
|
||||
parseThread.start();
|
||||
}
|
||||
|
||||
private void clearLastSessionData() {
|
||||
final ClosedTab[] emptyLastSessionTabs = new ClosedTab[0];
|
||||
|
||||
// Only modify mLastSessionTabs on the UI thread.
|
||||
ThreadUtils.postToUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Save some data about the old panel state, so we can be
|
||||
// smarter about notifying the recycler view which bits changed.
|
||||
int prevClosedTabsCount = lastSessionTabs.length;
|
||||
boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
|
||||
int prevSectionHeaderIndex = getSectionHeaderIndex();
|
||||
|
||||
lastSessionTabs = emptyLastSessionTabs;
|
||||
recentTabsUpdateHandler.onRecentTabsCountUpdated(
|
||||
getClosedTabsCount(), recentlyClosedTabsReceived);
|
||||
panelStateUpdateHandler.onPanelStateUpdated(CHILD_RECENT_TABS);
|
||||
|
||||
// Handle the section header hiding.
|
||||
updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
|
||||
|
||||
// Handle the "tabs from last time" being cleared.
|
||||
if (prevClosedTabsCount > 0) {
|
||||
notifyItemRangeRemoved(getFirstLastSessionTabIndex(), prevClosedTabsCount);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateHeaderVisibility(boolean prevSectionHeaderVisibility, int prevSectionHeaderIndex) {
|
||||
if (prevSectionHeaderVisibility && !isSectionHeaderVisible()) {
|
||||
notifyItemRemoved(prevSectionHeaderIndex);
|
||||
} else if (!prevSectionHeaderVisibility && isSectionHeaderVisible()) {
|
||||
notifyItemInserted(getSectionHeaderIndex());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tab list as necessary to account for any changes in tab count in a particular data source.
|
||||
*
|
||||
* Since the session store only sends out full updates, we don't know for sure what has changed compared
|
||||
* to the last data set, so we can only animate if the tab count actually changes.
|
||||
*
|
||||
* @param prevClosedTabsCount The previous number of closed tabs from that data source.
|
||||
* @param closedTabsCount The current number of closed tabs contained in that data source.
|
||||
* @param firstTabListIndex The current position of that data source's first item in the RecyclerView.
|
||||
* @param lastTabListIndex The current position of that data source's last item in the RecyclerView.
|
||||
*/
|
||||
private void updateTabsList(int prevClosedTabsCount, int closedTabsCount, int firstTabListIndex, int lastTabListIndex) {
|
||||
final int closedTabsCountChange = closedTabsCount - prevClosedTabsCount;
|
||||
|
||||
if (closedTabsCountChange <= 0) {
|
||||
notifyItemRangeRemoved(lastTabListIndex + 1, -closedTabsCountChange); // Remove tabs from the bottom of the list.
|
||||
notifyItemRangeChanged(firstTabListIndex, closedTabsCount); // Update the contents of the remaining items.
|
||||
} else { // closedTabsCountChange > 0
|
||||
notifyItemRangeInserted(firstTabListIndex, closedTabsCountChange); // Add additional tabs at the top of the list.
|
||||
notifyItemRangeChanged(firstTabListIndex + closedTabsCountChange, prevClosedTabsCount); // Update any previous list items.
|
||||
}
|
||||
}
|
||||
|
||||
public String restoreTabFromPosition(int position) {
|
||||
final List<String> dataList = new ArrayList<>(1);
|
||||
dataList.add(getClosedTabForPosition(position).data);
|
||||
|
||||
final String telemetryExtra =
|
||||
position > getLastRecentTabIndex() ? TELEMETRY_EXTRA_LAST_TIME : TELEMETRY_EXTRA_RECENTLY_CLOSED;
|
||||
|
||||
restoreSessionWithHistory(dataList);
|
||||
|
||||
return telemetryExtra;
|
||||
}
|
||||
|
||||
public String restoreAllTabs() {
|
||||
if (recentlyClosedTabs.length == 0 && lastSessionTabs.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<String> dataList = new ArrayList<>(getClosedTabsCount());
|
||||
addTabDataToList(dataList, recentlyClosedTabs);
|
||||
addTabDataToList(dataList, lastSessionTabs);
|
||||
|
||||
final String telemetryExtra = recentlyClosedTabs.length > 0 && lastSessionTabs.length > 0 ? TELEMETRY_EXTRA_MIXED :
|
||||
recentlyClosedTabs.length > 0 ? TELEMETRY_EXTRA_RECENTLY_CLOSED : TELEMETRY_EXTRA_LAST_TIME;
|
||||
|
||||
restoreSessionWithHistory(dataList);
|
||||
|
||||
return telemetryExtra;
|
||||
}
|
||||
|
||||
private void addTabDataToList(List<String> dataList, ClosedTab[] closedTabs) {
|
||||
for (ClosedTab closedTab : closedTabs) {
|
||||
dataList.add(closedTab.data);
|
||||
}
|
||||
}
|
||||
|
||||
private static void restoreSessionWithHistory(List<String> dataList) {
|
||||
final JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("tabs", new JSONArray(dataList));
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOGTAG, "JSON error", e);
|
||||
}
|
||||
|
||||
GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final View view;
|
||||
|
||||
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
|
||||
|
||||
switch (itemType) {
|
||||
case NAVIGATION_BACK:
|
||||
view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
|
||||
case SECTION_HEADER:
|
||||
view = inflater.inflate(R.layout.home_header_row, parent, false);
|
||||
return new CombinedHistoryItem.BasicItem(view);
|
||||
|
||||
case CLOSED_TAB:
|
||||
view = inflater.inflate(R.layout.home_item_row, parent, false);
|
||||
return new CombinedHistoryItem.HistoryItem(view);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
|
||||
switch (itemType) {
|
||||
case SECTION_HEADER:
|
||||
((TextView) holder.itemView).setText(context.getString(R.string.home_closed_tabs_title2));
|
||||
break;
|
||||
|
||||
case CLOSED_TAB:
|
||||
final ClosedTab closedTab = getClosedTabForPosition(position);
|
||||
((CombinedHistoryItem.HistoryItem) holder).bind(closedTab);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int itemCount = 1; // NAVIGATION_BACK button is always visible.
|
||||
|
||||
if (isSectionHeaderVisible()) {
|
||||
itemCount += 1;
|
||||
}
|
||||
|
||||
itemCount += getClosedTabsCount();
|
||||
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
|
||||
if (position == NAVIGATION_BACK_BUTTON_INDEX) {
|
||||
return ItemType.NAVIGATION_BACK;
|
||||
}
|
||||
|
||||
if (position == getSectionHeaderIndex() && isSectionHeaderVisible()) {
|
||||
return ItemType.SECTION_HEADER;
|
||||
}
|
||||
|
||||
return ItemType.CLOSED_TAB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
|
||||
}
|
||||
|
||||
public int getClosedTabsCount() {
|
||||
return recentlyClosedTabs.length + lastSessionTabs.length;
|
||||
}
|
||||
|
||||
private boolean isSectionHeaderVisible() {
|
||||
return recentlyClosedTabs.length > 0 || lastSessionTabs.length > 0;
|
||||
}
|
||||
|
||||
private int getSectionHeaderIndex() {
|
||||
return isSectionHeaderVisible() ?
|
||||
NAVIGATION_BACK_BUTTON_INDEX + 1 :
|
||||
NAVIGATION_BACK_BUTTON_INDEX;
|
||||
}
|
||||
|
||||
private int getFirstRecentTabIndex() {
|
||||
return getSectionHeaderIndex() + 1;
|
||||
}
|
||||
|
||||
private int getLastRecentTabIndex() {
|
||||
return getSectionHeaderIndex() + recentlyClosedTabs.length;
|
||||
}
|
||||
|
||||
private int getFirstLastSessionTabIndex() {
|
||||
return getLastRecentTabIndex() + 1;
|
||||
}
|
||||
|
||||
private int getLastLastSessionTabIndex() {
|
||||
return getLastRecentTabIndex() + lastSessionTabs.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the closed tab corresponding to a RecyclerView list item.
|
||||
*
|
||||
* The Recent Tab folder combines two data sources, so if we want to get the ClosedTab object
|
||||
* behind a certain list item, we need to route this request to the corresponding data source
|
||||
* and also transform the global list position into a local array index.
|
||||
*/
|
||||
private ClosedTab getClosedTabForPosition(int position) {
|
||||
final ClosedTab closedTab;
|
||||
if (position <= getLastRecentTabIndex()) { // Upper part of the list is "Recently closed tabs".
|
||||
closedTab = recentlyClosedTabs[position - getFirstRecentTabIndex()];
|
||||
} else { // Lower part is "Tabs from last time".
|
||||
closedTab = lastSessionTabs[position - getFirstLastSessionTabIndex()];
|
||||
}
|
||||
|
||||
return closedTab;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
|
||||
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
|
||||
final HomeContextMenuInfo info;
|
||||
|
||||
switch (itemType) {
|
||||
case CLOSED_TAB:
|
||||
info = new HomeContextMenuInfo(view, position, -1);
|
||||
ClosedTab closedTab = getClosedTabForPosition(position);
|
||||
return populateChildInfoFromTab(info, closedTab);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, ClosedTab tab) {
|
||||
info.url = tab.url;
|
||||
info.title = tab.title;
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/* -*- 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 java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.mozilla.gecko.util.PrefUtils;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
|
||||
/**
|
||||
* Encapsulate visual state maintained by the Remote Tabs home panel.
|
||||
* <p>
|
||||
* This state should persist across database updates by Sync and the like. This
|
||||
* state could be stored in a separate "clients_metadata" table and served by
|
||||
* the Tabs provider, but that is heavy-weight for what we want to achieve. Such
|
||||
* a scheme would require either an expensive table join, or a tricky
|
||||
* co-ordination between multiple cursors. In contrast, this is easy and cheap
|
||||
* enough to do on the main thread.
|
||||
* <p>
|
||||
* This state is "per SharedPreferences" object. In practice, there should exist
|
||||
* one state object per Gecko Profile; since we can't change profiles without
|
||||
* killing our process, this can be a static singleton.
|
||||
*/
|
||||
public class RemoteTabsExpandableListState {
|
||||
private static final String PREF_COLLAPSED_CLIENT_GUIDS = "remote_tabs_collapsed_client_guids";
|
||||
private static final String PREF_HIDDEN_CLIENT_GUIDS = "remote_tabs_hidden_client_guids";
|
||||
private static final String PREF_SELECTED_CLIENT_GUID = "remote_tabs_selected_client_guid";
|
||||
|
||||
protected final SharedPreferences sharedPrefs;
|
||||
|
||||
// Synchronized by the state instance. The default is to expand a clients
|
||||
// tabs, so "not present" means "expanded".
|
||||
// Only accessed from the UI thread.
|
||||
protected final Set<String> collapsedClients;
|
||||
|
||||
// Synchronized by the state instance. The default is to show a client, so
|
||||
// "not present" means "shown".
|
||||
// Only accessed from the UI thread.
|
||||
protected final Set<String> hiddenClients;
|
||||
|
||||
// Synchronized by the state instance. The last user selected client guid.
|
||||
// The selectedClient may be invalid or null.
|
||||
protected String selectedClient;
|
||||
|
||||
public RemoteTabsExpandableListState(SharedPreferences sharedPrefs) {
|
||||
if (null == sharedPrefs) {
|
||||
throw new IllegalArgumentException("sharedPrefs must not be null");
|
||||
}
|
||||
this.sharedPrefs = sharedPrefs;
|
||||
|
||||
this.collapsedClients = getStringSet(PREF_COLLAPSED_CLIENT_GUIDS);
|
||||
this.hiddenClients = getStringSet(PREF_HIDDEN_CLIENT_GUIDS);
|
||||
this.selectedClient = sharedPrefs.getString(PREF_SELECTED_CLIENT_GUID, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a string set from shared preferences.
|
||||
* <p>
|
||||
* Nota bene: it is not OK to modify the set returned by {@link SharedPreferences#getStringSet(String, Set)}.
|
||||
*
|
||||
* @param pref to read from.
|
||||
* @returns string set; never null.
|
||||
*/
|
||||
protected Set<String> getStringSet(String pref) {
|
||||
final Set<String> loaded = PrefUtils.getStringSet(sharedPrefs, pref, null);
|
||||
if (loaded != null) {
|
||||
return new HashSet<String>(loaded);
|
||||
} else {
|
||||
return new HashSet<String>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update client membership in a set.
|
||||
*
|
||||
* @param pref
|
||||
* to write updated set to.
|
||||
* @param clients
|
||||
* set to update membership in.
|
||||
* @param clientGuid
|
||||
* to update membership of.
|
||||
* @param isMember
|
||||
* whether the client is a member of the set.
|
||||
* @return true if the set of clients was modified.
|
||||
*/
|
||||
protected boolean updateClientMembership(String pref, Set<String> clients, String clientGuid, boolean isMember) {
|
||||
final boolean modified;
|
||||
if (isMember) {
|
||||
modified = clients.add(clientGuid);
|
||||
} else {
|
||||
modified = clients.remove(clientGuid);
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
// This starts an asynchronous write. We don't care if we drop the
|
||||
// write, and we don't really care if we race between writes, since
|
||||
// we will return results from our in-memory cache.
|
||||
final Editor editor = sharedPrefs.edit();
|
||||
PrefUtils.putStringSet(editor, pref, clients);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a client as collapsed.
|
||||
*
|
||||
* @param clientGuid
|
||||
* to update.
|
||||
* @param collapsed
|
||||
* whether the client is collapsed.
|
||||
* @return true if the set of collapsed clients was modified.
|
||||
*/
|
||||
protected synchronized boolean setClientCollapsed(String clientGuid, boolean collapsed) {
|
||||
return updateClientMembership(PREF_COLLAPSED_CLIENT_GUIDS, collapsedClients, clientGuid, collapsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a client as the selected.
|
||||
*
|
||||
* @param clientGuid
|
||||
* to update.
|
||||
*/
|
||||
protected synchronized void setClientAsSelected(String clientGuid) {
|
||||
if (hiddenClients.contains(clientGuid)) {
|
||||
selectedClient = null;
|
||||
} else {
|
||||
selectedClient = clientGuid;
|
||||
}
|
||||
|
||||
final Editor editor = sharedPrefs.edit();
|
||||
editor.putString(PREF_SELECTED_CLIENT_GUID, selectedClient);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public synchronized boolean isClientCollapsed(String clientGuid) {
|
||||
return collapsedClients.contains(clientGuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a client as hidden.
|
||||
*
|
||||
* @param clientGuid
|
||||
* to update.
|
||||
* @param hidden
|
||||
* whether the client is hidden.
|
||||
* @return true if the set of hidden clients was modified.
|
||||
*/
|
||||
protected synchronized boolean setClientHidden(String clientGuid, boolean hidden) {
|
||||
return updateClientMembership(PREF_HIDDEN_CLIENT_GUIDS, hiddenClients, clientGuid, hidden);
|
||||
}
|
||||
|
||||
public synchronized boolean isClientHidden(String clientGuid) {
|
||||
return hiddenClients.contains(clientGuid);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 android.support.annotation.NonNull;
|
||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SearchEngine {
|
||||
public static final String LOG_TAG = "GeckoSearchEngine";
|
||||
|
||||
public final String name; // Never null.
|
||||
public final String identifier; // Can be null.
|
||||
|
||||
private final Bitmap icon;
|
||||
private volatile List<String> suggestions = new ArrayList<String>(); // Never null.
|
||||
|
||||
public SearchEngine(final Context context, final JSONObject engineJSON) throws JSONException {
|
||||
if (engineJSON == null) {
|
||||
throw new IllegalArgumentException("Can't instantiate SearchEngine from null JSON.");
|
||||
}
|
||||
|
||||
this.name = getString(engineJSON, "name");
|
||||
if (this.name == null) {
|
||||
throw new IllegalArgumentException("Cannot have an unnamed search engine.");
|
||||
}
|
||||
|
||||
this.identifier = getString(engineJSON, "identifier");
|
||||
|
||||
final String iconURI = getString(engineJSON, "iconURI");
|
||||
if (iconURI == null) {
|
||||
Log.w(LOG_TAG, "iconURI is null for search engine " + this.name);
|
||||
}
|
||||
final Bitmap tempIcon = BitmapUtils.getBitmapFromDataURI(iconURI);
|
||||
|
||||
this.icon = (tempIcon != null) ? tempIcon : getDefaultFavicon(context);
|
||||
}
|
||||
|
||||
private Bitmap getDefaultFavicon(final Context context) {
|
||||
return BitmapFactory.decodeResource(context.getResources(), R.drawable.search_icon_inactive);
|
||||
}
|
||||
|
||||
private static String getString(JSONObject data, String key) throws JSONException {
|
||||
if (data.isNull(key)) {
|
||||
return null;
|
||||
}
|
||||
return data.getString(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a non-null string suitable for use by FHR.
|
||||
*/
|
||||
@NonNull
|
||||
public String getEngineIdentifier() {
|
||||
if (this.identifier != null) {
|
||||
return this.identifier;
|
||||
}
|
||||
if (this.name != null) {
|
||||
return "other-" + this.name;
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
public boolean hasSuggestions() {
|
||||
return !this.suggestions.isEmpty();
|
||||
}
|
||||
|
||||
public int getSuggestionsCount() {
|
||||
return this.suggestions.size();
|
||||
}
|
||||
|
||||
public Iterable<String> getSuggestions() {
|
||||
return this.suggestions;
|
||||
}
|
||||
|
||||
public void setSuggestions(List<String> suggestions) {
|
||||
if (suggestions == null) {
|
||||
this.suggestions = new ArrayList<String>();
|
||||
return;
|
||||
}
|
||||
this.suggestions = suggestions;
|
||||
}
|
||||
|
||||
public Bitmap getIcon() {
|
||||
return this.icon;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package org.mozilla.gecko.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.graphics.drawable.DrawableCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class SearchEngineAdapter
|
||||
extends RecyclerView.Adapter<SearchEngineAdapter.SearchEngineViewHolder> {
|
||||
|
||||
private static final String LOGTAG = SearchEngineAdapter.class.getSimpleName();
|
||||
|
||||
private static final int VIEW_TYPE_SEARCH_ENGINE = 0;
|
||||
private static final int VIEW_TYPE_LABEL = 1;
|
||||
private final Context mContext;
|
||||
|
||||
private int mContainerWidth;
|
||||
private List<SearchEngine> mSearchEngines = Collections.emptyList();
|
||||
|
||||
public void setSearchEngines(List<SearchEngine> searchEngines) {
|
||||
mSearchEngines = searchEngines;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* The container width is used for setting the appropriate calculated amount of width that
|
||||
* a search engine icon can have. This varies depending on the space available in the
|
||||
* {@link SearchEngineBar}. The setter exists for this attribute, in creating the view in the
|
||||
* adapter after said calculation is done when the search bar is created.
|
||||
* @param iconContainerWidth Width of each search icon.
|
||||
*/
|
||||
void setIconContainerWidth(int iconContainerWidth) {
|
||||
mContainerWidth = iconContainerWidth;
|
||||
}
|
||||
|
||||
public static class SearchEngineViewHolder extends RecyclerView.ViewHolder {
|
||||
final private ImageView faviconView;
|
||||
|
||||
public void bindItem(SearchEngine searchEngine) {
|
||||
faviconView.setImageBitmap(searchEngine.getIcon());
|
||||
final String desc = itemView.getResources().getString(R.string.search_bar_item_desc,
|
||||
searchEngine.getEngineIdentifier());
|
||||
itemView.setContentDescription(desc);
|
||||
}
|
||||
|
||||
public SearchEngineViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
faviconView = (ImageView) itemView.findViewById(R.id.search_engine_icon);
|
||||
}
|
||||
}
|
||||
|
||||
public SearchEngineAdapter(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position == 0 ? VIEW_TYPE_LABEL : VIEW_TYPE_SEARCH_ENGINE;
|
||||
}
|
||||
|
||||
public SearchEngine getItem(int position) {
|
||||
// We omit the first position which is where the label currently is.
|
||||
return position == 0 ? null : mSearchEngines.get(position - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchEngineViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_LABEL:
|
||||
return new SearchEngineViewHolder(createLabelView(parent));
|
||||
case VIEW_TYPE_SEARCH_ENGINE:
|
||||
return new SearchEngineViewHolder(createSearchEngineView(parent));
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown view type: " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SearchEngineViewHolder holder, int position) {
|
||||
if (position != 0) {
|
||||
holder.bindItem(getItem(position));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mSearchEngines.size() + 1;
|
||||
}
|
||||
|
||||
private View createLabelView(ViewGroup parent) {
|
||||
View view = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.search_engine_bar_label, parent, false);
|
||||
final Drawable icon = DrawableCompat.wrap(
|
||||
ContextCompat.getDrawable(mContext, R.drawable.search_icon_active).mutate());
|
||||
DrawableCompat.setTint(icon, ContextCompat.getColor(mContext, R.color.disabled_grey));
|
||||
|
||||
final ImageView iconView = (ImageView) view.findViewById(R.id.search_engine_label);
|
||||
iconView.setImageDrawable(icon);
|
||||
return view;
|
||||
}
|
||||
|
||||
private View createSearchEngineView(ViewGroup parent) {
|
||||
View view = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.search_engine_bar_item, parent, false);
|
||||
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
params.width = mContainerWidth;
|
||||
view.setLayoutParams(params);
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/* -*- 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.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
import org.mozilla.gecko.annotation.RobocopTarget;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SearchEngineBar extends RecyclerView
|
||||
implements RecyclerViewClickSupport.OnItemClickListener {
|
||||
private static final String LOGTAG = SearchEngineBar.class.getSimpleName();
|
||||
|
||||
private static final float ICON_CONTAINER_MIN_WIDTH_DP = 72;
|
||||
private static final float LABEL_CONTAINER_WIDTH_DP = 48;
|
||||
|
||||
public interface OnSearchBarClickListener {
|
||||
void onSearchBarClickListener(SearchEngine searchEngine);
|
||||
}
|
||||
|
||||
private final SearchEngineAdapter mAdapter;
|
||||
private final LinearLayoutManager mLayoutManager;
|
||||
private final Paint mDividerPaint;
|
||||
private final float mMinIconContainerWidth;
|
||||
private final float mDividerHeight;
|
||||
private final int mLabelContainerWidth;
|
||||
|
||||
private int mIconContainerWidth;
|
||||
private OnSearchBarClickListener mOnSearchBarClickListener;
|
||||
|
||||
public SearchEngineBar(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
mDividerPaint = new Paint();
|
||||
mDividerPaint.setColor(ContextCompat.getColor(context, R.color.toolbar_divider_grey));
|
||||
mDividerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
|
||||
final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
mMinIconContainerWidth = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, ICON_CONTAINER_MIN_WIDTH_DP, displayMetrics);
|
||||
mDividerHeight = context.getResources().getDimension(R.dimen.page_row_divider_height);
|
||||
mLabelContainerWidth = Math.round(TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, LABEL_CONTAINER_WIDTH_DP, displayMetrics));
|
||||
|
||||
mIconContainerWidth = Math.round(mMinIconContainerWidth);
|
||||
|
||||
mAdapter = new SearchEngineAdapter(context);
|
||||
mAdapter.setIconContainerWidth(mIconContainerWidth);
|
||||
mLayoutManager = new LinearLayoutManager(context);
|
||||
mLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
|
||||
|
||||
setAdapter(mAdapter);
|
||||
setLayoutManager(mLayoutManager);
|
||||
|
||||
RecyclerViewClickSupport.addTo(this)
|
||||
.setOnItemClickListener(this);
|
||||
}
|
||||
|
||||
public void setSearchEngines(List<SearchEngine> searchEngines) {
|
||||
mAdapter.setSearchEngines(searchEngines);
|
||||
}
|
||||
|
||||
public void setOnSearchBarClickListener(OnSearchBarClickListener listener) {
|
||||
mOnSearchBarClickListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
final int searchEngineCount = mAdapter.getItemCount() - 1;
|
||||
|
||||
if (searchEngineCount > 0) {
|
||||
final int availableWidth = getMeasuredWidth() - mLabelContainerWidth;
|
||||
|
||||
if (searchEngineCount * mMinIconContainerWidth <= availableWidth) {
|
||||
// All search engines fit int: So let's just display all.
|
||||
mIconContainerWidth = (int) mMinIconContainerWidth;
|
||||
} else {
|
||||
// If only (n) search engines fit into the available space then display only (x)
|
||||
// search engines with (x) picked so that the last search engine will be cut-off
|
||||
// (we only display half of it) to show the ability to scroll this view.
|
||||
|
||||
final double searchEnginesToDisplay = Math.floor((availableWidth / mMinIconContainerWidth) - 0.5) + 0.5;
|
||||
// Use all available width and spread search engine icons
|
||||
mIconContainerWidth = (int) (availableWidth / searchEnginesToDisplay);
|
||||
}
|
||||
|
||||
mAdapter.setIconContainerWidth(mIconContainerWidth);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
canvas.drawRect(0, 0, getWidth(), mDividerHeight, mDividerPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||
if (mOnSearchBarClickListener == null) {
|
||||
throw new IllegalStateException(
|
||||
OnSearchBarClickListener.class.getSimpleName() + " is not initializer."
|
||||
);
|
||||
}
|
||||
|
||||
if (position == 0) {
|
||||
final Intent settingsIntent = new Intent(getContext(), GeckoPreferences.class);
|
||||
GeckoPreferences.setResourceToOpen(settingsIntent, "preferences_search");
|
||||
getContext().startActivity(settingsIntent);
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "searchenginebar-settings");
|
||||
return;
|
||||
}
|
||||
|
||||
final SearchEngine searchEngine = mAdapter.getItem(position);
|
||||
mOnSearchBarClickListener.onSearchBarClickListener(searchEngine);
|
||||
}
|
||||
|
||||
/**
|
||||
* We manually add the override for getAdapter because we see this method getting stripped
|
||||
* out during compile time by aggressive proguard rules.
|
||||
*/
|
||||
@RobocopTarget
|
||||
@Override
|
||||
public SearchEngineAdapter getAdapter() {
|
||||
return mAdapter;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,494 @@
|
|||
/* -*- 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.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
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.icons.IconResponse;
|
||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
import org.mozilla.gecko.widget.AnimatedHeightLayout;
|
||||
import org.mozilla.gecko.widget.FaviconView;
|
||||
import org.mozilla.gecko.widget.FlowLayout;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
class SearchEngineRow extends AnimatedHeightLayout {
|
||||
// Duration for fade-in animation
|
||||
private static final int ANIMATION_DURATION = 250;
|
||||
|
||||
// Inner views
|
||||
private final FlowLayout mSuggestionView;
|
||||
private final FaviconView mIconView;
|
||||
private final LinearLayout mUserEnteredView;
|
||||
private final TextView mUserEnteredTextView;
|
||||
|
||||
// Inflater used when updating from suggestions
|
||||
private final LayoutInflater mInflater;
|
||||
|
||||
// Search engine associated with this view
|
||||
private SearchEngine mSearchEngine;
|
||||
|
||||
// Event listeners for suggestion views
|
||||
private final OnClickListener mClickListener;
|
||||
private final OnLongClickListener mLongClickListener;
|
||||
|
||||
// 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;
|
||||
|
||||
// android:backgroundTint only works in Android 21 and higher so we can't do this statically in the xml
|
||||
private Drawable mSearchHistorySuggestionIcon;
|
||||
|
||||
// Maximums for suggestions
|
||||
private int mMaxSavedSuggestions;
|
||||
private int mMaxSearchSuggestions;
|
||||
|
||||
private final List<Integer> mOccurrences = new ArrayList<Integer>();
|
||||
|
||||
public SearchEngineRow(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SearchEngineRow(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public SearchEngineRow(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
mClickListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final String suggestion = getSuggestionTextFromView(v);
|
||||
|
||||
// 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 (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, true)) {
|
||||
if (mUrlOpenListener != null) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "url");
|
||||
|
||||
mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class));
|
||||
}
|
||||
} else if (mSearchListener != null) {
|
||||
if (v == mUserEnteredView) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user");
|
||||
} else {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, (String) v.getTag());
|
||||
}
|
||||
mSearchListener.onSearch(mSearchEngine, suggestion, TelemetryContract.Method.SUGGESTION);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mLongClickListener = new OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (mEditSuggestionListener != null) {
|
||||
final String suggestion = getSuggestionTextFromView(v);
|
||||
mEditSuggestionListener.onEditSuggestion(suggestion);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
mInflater = LayoutInflater.from(context);
|
||||
mInflater.inflate(R.layout.search_engine_row, this);
|
||||
|
||||
mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout);
|
||||
mIconView = (FaviconView) findViewById(R.id.suggestion_icon);
|
||||
|
||||
// User-entered search term is first suggestion
|
||||
mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered);
|
||||
mUserEnteredView.setOnClickListener(mClickListener);
|
||||
|
||||
mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text);
|
||||
mSearchHistorySuggestionIcon = DrawableUtil.tintDrawableWithColorRes(getContext(), R.drawable.icon_most_recent_empty, R.color.tabs_tray_icon_grey);
|
||||
|
||||
// Suggestion limits
|
||||
mMaxSavedSuggestions = getResources().getInteger(R.integer.max_saved_suggestions);
|
||||
mMaxSearchSuggestions = getResources().getInteger(R.integer.max_search_suggestions);
|
||||
}
|
||||
|
||||
private void setDescriptionOnSuggestion(View v, String suggestion) {
|
||||
v.setContentDescription(getResources().getString(R.string.suggestion_for_engine,
|
||||
mSearchEngine.name, suggestion));
|
||||
}
|
||||
|
||||
private String getSuggestionTextFromView(View v) {
|
||||
final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text);
|
||||
return suggestionText.getText().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all occurrences of pattern in string and returns a list of the starting indices
|
||||
* of each occurrence.
|
||||
*
|
||||
* @param pattern The pattern that is searched for
|
||||
* @param string The string where we search for the pattern
|
||||
*/
|
||||
private void refreshOccurrencesWith(String pattern, String string) {
|
||||
mOccurrences.clear();
|
||||
|
||||
// Don't try to search for an empty string - String.indexOf will return 0, which would result
|
||||
// in us iterating with lastIndexOfMatch = 0, which eventually results in an OOM.
|
||||
if (TextUtils.isEmpty(pattern)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int patternLength = pattern.length();
|
||||
|
||||
int indexOfMatch = 0;
|
||||
int lastIndexOfMatch = 0;
|
||||
while (indexOfMatch != -1) {
|
||||
indexOfMatch = string.indexOf(pattern, lastIndexOfMatch);
|
||||
lastIndexOfMatch = indexOfMatch + patternLength;
|
||||
if (indexOfMatch != -1) {
|
||||
mOccurrences.add(indexOfMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content for the suggestion view.
|
||||
*
|
||||
* If the suggestion doesn't contain mUserSearchTerm, nothing is made bold.
|
||||
* All instances of mUserSearchTerm in the suggestion are not bold.
|
||||
*
|
||||
* @param v The View that needs to be populated
|
||||
* @param suggestion The suggestion text that will be placed in the view
|
||||
* @param isUserSavedSearch whether the suggestion is from history or not
|
||||
*/
|
||||
private void setSuggestionOnView(View v, String suggestion, boolean isUserSavedSearch) {
|
||||
final ImageView historyIcon = (ImageView) v.findViewById(R.id.suggestion_item_icon);
|
||||
if (isUserSavedSearch) {
|
||||
historyIcon.setImageDrawable(mSearchHistorySuggestionIcon);
|
||||
historyIcon.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
historyIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text);
|
||||
final String searchTerm = getSuggestionTextFromView(mUserEnteredView);
|
||||
final int searchTermLength = searchTerm.length();
|
||||
refreshOccurrencesWith(searchTerm, suggestion);
|
||||
if (mOccurrences.size() > 0) {
|
||||
final SpannableStringBuilder sb = new SpannableStringBuilder(suggestion);
|
||||
int nextStartSpanIndex = 0;
|
||||
// Done to make sure that the stretch of text after the last occurrence, till the end of the suggestion, is made bold
|
||||
mOccurrences.add(suggestion.length());
|
||||
for (int occurrence : mOccurrences) {
|
||||
// Even though they're the same style, SpannableStringBuilder will interpret there as being only one Span present if we re-use a StyleSpan
|
||||
StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||
sb.setSpan(boldSpan, nextStartSpanIndex, occurrence, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
nextStartSpanIndex = occurrence + searchTermLength;
|
||||
}
|
||||
mOccurrences.clear();
|
||||
suggestionText.setText(sb);
|
||||
} else {
|
||||
suggestionText.setText(suggestion);
|
||||
}
|
||||
|
||||
setDescriptionOnSuggestion(suggestionText, suggestion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a search for the user-entered term.
|
||||
*/
|
||||
public void performUserEnteredSearch() {
|
||||
String searchTerm = getSuggestionTextFromView(mUserEnteredView);
|
||||
if (mSearchListener != null) {
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user");
|
||||
mSearchListener.onSearch(mSearchEngine, searchTerm, TelemetryContract.Method.SUGGESTION);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSearchTerm(String searchTerm) {
|
||||
mUserEnteredTextView.setText(searchTerm);
|
||||
|
||||
// mSearchEngine is not set in the first call to this method; the content description
|
||||
// is instead initially set in updateSuggestions().
|
||||
if (mSearchEngine != null) {
|
||||
setDescriptionOnSuggestion(mUserEnteredTextView, searchTerm);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnUrlOpenListener(OnUrlOpenListener listener) {
|
||||
mUrlOpenListener = listener;
|
||||
}
|
||||
|
||||
public void setOnSearchListener(OnSearchListener listener) {
|
||||
mSearchListener = listener;
|
||||
}
|
||||
|
||||
public void setOnEditSuggestionListener(OnEditSuggestionListener listener) {
|
||||
mEditSuggestionListener = listener;
|
||||
}
|
||||
|
||||
private void bindSuggestionView(String suggestion, boolean animate, int recycledSuggestionCount, Integer previousSuggestionChildIndex, boolean isUserSavedSearch, String telemetryTag) {
|
||||
final View suggestionItem;
|
||||
|
||||
// Reuse suggestion views from recycled view, if possible.
|
||||
if (previousSuggestionChildIndex + 1 < recycledSuggestionCount) {
|
||||
suggestionItem = mSuggestionView.getChildAt(previousSuggestionChildIndex + 1);
|
||||
suggestionItem.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
suggestionItem = mInflater.inflate(R.layout.suggestion_item, null);
|
||||
|
||||
suggestionItem.setOnClickListener(mClickListener);
|
||||
suggestionItem.setOnLongClickListener(mLongClickListener);
|
||||
|
||||
suggestionItem.setTag(telemetryTag);
|
||||
|
||||
mSuggestionView.addView(suggestionItem);
|
||||
}
|
||||
|
||||
setSuggestionOnView(suggestionItem, suggestion, isUserSavedSearch);
|
||||
|
||||
if (animate) {
|
||||
AlphaAnimation anim = new AlphaAnimation(0, 1);
|
||||
anim.setDuration(ANIMATION_DURATION);
|
||||
anim.setStartOffset(previousSuggestionChildIndex * ANIMATION_DURATION);
|
||||
suggestionItem.startAnimation(anim);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideRecycledSuggestions(int lastVisibleChildIndex, int recycledSuggestionCount) {
|
||||
// Hide extra suggestions that have been recycled.
|
||||
for (int i = lastVisibleChildIndex + 1; i < recycledSuggestionCount; ++i) {
|
||||
mSuggestionView.getChildAt(i).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays search suggestions from previous searches.
|
||||
*
|
||||
* @param savedSuggestions The List to iterate over for saved search suggestions to display. This function does not
|
||||
* enforce a ui maximum or filter. It will show all the suggestions in this list.
|
||||
* @param suggestionStartIndex global index of where to start adding suggestion "buttons" in the search engine row. Also
|
||||
* acts as a counter for total number of suggestions visible.
|
||||
* @param animate whether or not to animate suggestions for visual polish
|
||||
* @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls
|
||||
*/
|
||||
private void updateFromSavedSearches(List<String> savedSuggestions, boolean animate, int suggestionStartIndex, int recycledSuggestionCount) {
|
||||
if (savedSuggestions == null || savedSuggestions.isEmpty()) {
|
||||
hideRecycledSuggestions(suggestionStartIndex, recycledSuggestionCount);
|
||||
return;
|
||||
}
|
||||
|
||||
final int numSavedSearches = savedSuggestions.size();
|
||||
int indexOfPreviousSuggestion = 0;
|
||||
for (int i = 0; i < numSavedSearches; i++) {
|
||||
String telemetryTag = "history." + i;
|
||||
final String suggestion = savedSuggestions.get(i);
|
||||
indexOfPreviousSuggestion = suggestionStartIndex + i;
|
||||
bindSuggestionView(suggestion, animate, recycledSuggestionCount, indexOfPreviousSuggestion, true, telemetryTag);
|
||||
}
|
||||
|
||||
hideRecycledSuggestions(indexOfPreviousSuggestion + 1, recycledSuggestionCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays suggestions supplied by the search engine, relative to number of suggestions from search history.
|
||||
*
|
||||
* @param animate whether or not to animate suggestions for visual polish
|
||||
* @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls
|
||||
* @param savedSuggestionCount how many saved searches this searchTerm has
|
||||
* @return the global count of how many suggestions have been bound/shown in the search engine row
|
||||
*/
|
||||
private int updateFromSearchEngine(boolean animate, List<String> searchEngineSuggestions, int recycledSuggestionCount, int savedSuggestionCount) {
|
||||
int maxSuggestions = mMaxSearchSuggestions;
|
||||
// If there are less than max saved searches on phones, fill the space with more search engine suggestions
|
||||
if (!HardwareUtils.isTablet() && savedSuggestionCount < mMaxSavedSuggestions) {
|
||||
maxSuggestions += mMaxSavedSuggestions - savedSuggestionCount;
|
||||
}
|
||||
|
||||
final int numSearchEngineSuggestions = searchEngineSuggestions.size();
|
||||
int relativeIndex;
|
||||
for (relativeIndex = 0; relativeIndex < numSearchEngineSuggestions; relativeIndex++) {
|
||||
if (relativeIndex == maxSuggestions) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Since the search engine suggestions are listed first, their relative index is their global index
|
||||
String telemetryTag = "engine." + relativeIndex;
|
||||
final String suggestion = searchEngineSuggestions.get(relativeIndex);
|
||||
bindSuggestionView(suggestion, animate, recycledSuggestionCount, relativeIndex, false, telemetryTag);
|
||||
}
|
||||
|
||||
hideRecycledSuggestions(relativeIndex + 1, recycledSuggestionCount);
|
||||
|
||||
// Make sure mSelectedView is still valid.
|
||||
if (mSelectedView >= mSuggestionView.getChildCount()) {
|
||||
mSelectedView = mSuggestionView.getChildCount() - 1;
|
||||
}
|
||||
|
||||
return relativeIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the whole suggestions UI, the search engine UI, suggestions from the default search engine,
|
||||
* and suggestions from search history.
|
||||
*
|
||||
* This can be called before the opt-in permission prompt is shown or set.
|
||||
* Even if both suggestion types are disabled, we need to update the search engine, its image, and the content description.
|
||||
*
|
||||
* @param searchSuggestionsEnabled whether or not suggestions from the default search engine are enabled
|
||||
* @param searchEngine the search engine to use throughout the SearchEngineRow class
|
||||
* @param rawSearchHistorySuggestions search history suggestions
|
||||
* @param animate whether or not to use animations
|
||||
**/
|
||||
public void updateSuggestions(boolean searchSuggestionsEnabled, SearchEngine searchEngine, @Nullable List<String> rawSearchHistorySuggestions, boolean animate) {
|
||||
mSearchEngine = searchEngine;
|
||||
// Set the search engine icon (e.g., Google) for the row.
|
||||
|
||||
mIconView.updateAndScaleImage(IconResponse.create(mSearchEngine.getIcon()));
|
||||
// Set the initial content description.
|
||||
setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString());
|
||||
|
||||
final int recycledSuggestionCount = mSuggestionView.getChildCount();
|
||||
final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext());
|
||||
final boolean savedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true);
|
||||
|
||||
// Remove duplicates of search engine suggestions from saved searches.
|
||||
List<String> searchHistorySuggestions = (rawSearchHistorySuggestions != null) ? rawSearchHistorySuggestions : new ArrayList<String>();
|
||||
|
||||
// Filter out URLs and long search suggestions
|
||||
Iterator<String> searchHistoryIterator = searchHistorySuggestions.iterator();
|
||||
while (searchHistoryIterator.hasNext()) {
|
||||
final String currentSearchHistory = searchHistoryIterator.next();
|
||||
|
||||
if (currentSearchHistory.length() > 50 || Pattern.matches("^(https?|ftp|file)://.*", currentSearchHistory)) {
|
||||
searchHistoryIterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<String> searchEngineSuggestions = new ArrayList<String>();
|
||||
for (String suggestion : searchEngine.getSuggestions()) {
|
||||
searchHistorySuggestions.remove(suggestion);
|
||||
searchEngineSuggestions.add(suggestion);
|
||||
}
|
||||
// Make sure the search term itself isn't duplicated. This is more important on phones than tablets where screen
|
||||
// space is more precious.
|
||||
searchHistorySuggestions.remove(getSuggestionTextFromView(mUserEnteredView));
|
||||
|
||||
// Trim the history suggestions down to the maximum allowed.
|
||||
if (searchHistorySuggestions.size() >= mMaxSavedSuggestions) {
|
||||
// The second index to subList() is exclusive, so this looks like an off by one error but it is not.
|
||||
searchHistorySuggestions = searchHistorySuggestions.subList(0, mMaxSavedSuggestions);
|
||||
}
|
||||
final int searchHistoryCount = searchHistorySuggestions.size();
|
||||
|
||||
if (searchSuggestionsEnabled && savedSearchesEnabled) {
|
||||
final int suggestionViewCount = updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, searchHistoryCount);
|
||||
updateFromSavedSearches(searchHistorySuggestions, animate, suggestionViewCount, recycledSuggestionCount);
|
||||
} else if (savedSearchesEnabled) {
|
||||
updateFromSavedSearches(searchHistorySuggestions, animate, 0, recycledSuggestionCount);
|
||||
} else if (searchSuggestionsEnabled) {
|
||||
updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, 0);
|
||||
} else {
|
||||
// The current search term is treated separately from the suggestions list, hence we can
|
||||
// recycle ALL suggestion items here. (We always show the current search term, i.e. 1 item,
|
||||
// in front of the search engine suggestions and/or the search history.)
|
||||
hideRecycledSuggestions(0, recycledSuggestionCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, android.view.KeyEvent event) {
|
||||
final View suggestion = mSuggestionView.getChildAt(mSelectedView);
|
||||
|
||||
if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
final View nextSuggestion = mSuggestionView.getChildAt(mSelectedView + 1);
|
||||
if (nextSuggestion != null) {
|
||||
changeSelectedSuggestion(suggestion, nextSuggestion);
|
||||
mSelectedView++;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
final View prevSuggestion = mSuggestionView.getChildAt(mSelectedView - 1);
|
||||
if (prevSuggestion != null) {
|
||||
changeSelectedSuggestion(suggestion, prevSuggestion);
|
||||
mSelectedView--;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
// TODO: handle long pressing for editing suggestions
|
||||
return suggestion.performClick();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void changeSelectedSuggestion(View oldSuggestion, View newSuggestion) {
|
||||
oldSuggestion.setDuplicateParentStateEnabled(false);
|
||||
newSuggestion.setDuplicateParentStateEnabled(true);
|
||||
oldSuggestion.refreshDrawableState();
|
||||
newSuggestion.refreshDrawableState();
|
||||
}
|
||||
|
||||
public void onSelected() {
|
||||
mSelectedView = 0;
|
||||
mUserEnteredView.setDuplicateParentStateEnabled(true);
|
||||
mUserEnteredView.refreshDrawableState();
|
||||
}
|
||||
|
||||
public void onDeselected() {
|
||||
final View suggestion = mSuggestionView.getChildAt(mSelectedView);
|
||||
suggestion.setDuplicateParentStateEnabled(false);
|
||||
suggestion.refreshDrawableState();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/* -*- 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 java.util.EnumSet;
|
||||
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.BrowserDB.FilterFlags;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.Loader;
|
||||
|
||||
/**
|
||||
* Encapsulates the implementation of the search cursor loader.
|
||||
*/
|
||||
class SearchLoader {
|
||||
public static final String LOGTAG = "GeckoSearchLoader";
|
||||
|
||||
private static final String KEY_SEARCH_TERM = "search_term";
|
||||
private static final String KEY_FILTER_FLAGS = "flags";
|
||||
|
||||
private SearchLoader() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Loader<Cursor> createInstance(Context context, Bundle args) {
|
||||
if (args != null) {
|
||||
final String searchTerm = args.getString(KEY_SEARCH_TERM);
|
||||
final EnumSet<FilterFlags> flags =
|
||||
(EnumSet<FilterFlags>) args.getSerializable(KEY_FILTER_FLAGS);
|
||||
return new SearchCursorLoader(context, searchTerm, flags);
|
||||
} else {
|
||||
return new SearchCursorLoader(context, "", EnumSet.noneOf(FilterFlags.class));
|
||||
}
|
||||
}
|
||||
|
||||
private static Bundle createArgs(String searchTerm, EnumSet<FilterFlags> flags) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(SearchLoader.KEY_SEARCH_TERM, searchTerm);
|
||||
args.putSerializable(SearchLoader.KEY_FILTER_FLAGS, flags);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
public static void init(LoaderManager manager, int loaderId,
|
||||
LoaderCallbacks<Cursor> callbacks, String searchTerm) {
|
||||
init(manager, loaderId, callbacks, searchTerm, EnumSet.noneOf(FilterFlags.class));
|
||||
}
|
||||
|
||||
public static void init(LoaderManager manager, int loaderId,
|
||||
LoaderCallbacks<Cursor> callbacks, String searchTerm,
|
||||
EnumSet<FilterFlags> flags) {
|
||||
final Bundle args = createArgs(searchTerm, flags);
|
||||
manager.initLoader(loaderId, args, callbacks);
|
||||
}
|
||||
|
||||
public static void restart(LoaderManager manager, int loaderId,
|
||||
LoaderCallbacks<Cursor> callbacks, String searchTerm) {
|
||||
restart(manager, loaderId, callbacks, searchTerm, EnumSet.noneOf(FilterFlags.class));
|
||||
}
|
||||
|
||||
public static void restart(LoaderManager manager, int loaderId,
|
||||
LoaderCallbacks<Cursor> callbacks, String searchTerm,
|
||||
EnumSet<FilterFlags> flags) {
|
||||
final Bundle args = createArgs(searchTerm, flags);
|
||||
manager.restartLoader(loaderId, args, callbacks);
|
||||
}
|
||||
|
||||
public static class SearchCursorLoader extends SimpleCursorLoader {
|
||||
private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_SEARCH_LOADER_TIME_MS";
|
||||
|
||||
// Max number of search results.
|
||||
private static final int SEARCH_LIMIT = 100;
|
||||
|
||||
// The target search term associated with the loader.
|
||||
private final String mSearchTerm;
|
||||
|
||||
// The filter flags associated with the loader.
|
||||
private final EnumSet<FilterFlags> mFlags;
|
||||
private final GeckoProfile mProfile;
|
||||
|
||||
public SearchCursorLoader(Context context, String searchTerm, EnumSet<FilterFlags> flags) {
|
||||
super(context);
|
||||
mSearchTerm = searchTerm;
|
||||
mFlags = flags;
|
||||
mProfile = GeckoProfile.get(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor loadCursor() {
|
||||
final long start = SystemClock.uptimeMillis();
|
||||
final Cursor cursor = BrowserDB.from(mProfile).filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT, mFlags);
|
||||
final long end = SystemClock.uptimeMillis();
|
||||
final long took = end - start;
|
||||
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public String getSearchTerm() {
|
||||
return mSearchTerm;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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.Cursor;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
|
||||
import org.mozilla.gecko.GeckoApplication;
|
||||
|
||||
/**
|
||||
* A copy of the framework's {@link android.content.CursorLoader} that
|
||||
* instead allows the caller to load the Cursor themselves via the abstract
|
||||
* {@link #loadCursor()} method, rather than calling out to a ContentProvider via
|
||||
* class methods.
|
||||
*
|
||||
* For new code, prefer {@link android.content.CursorLoader} (see @deprecated).
|
||||
*
|
||||
* This was originally created to re-use existing code which loaded Cursors manually.
|
||||
*
|
||||
* @deprecated since the framework provides an implementation, we'd like to eventually remove
|
||||
* this class to reduce maintenance burden. Originally planned for bug 1239491, but
|
||||
* it'd be more efficient to do this over time, rather than all at once.
|
||||
*/
|
||||
@Deprecated
|
||||
public 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();
|
||||
|
||||
// Trying to read from the closed cursor will cause crashes, hence we should make
|
||||
// sure that no adapters/LoaderCallbacks are holding onto this cursor.
|
||||
GeckoApplication.getRefWatcher(getContext()).watch(oldCursor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package org.mozilla.gecko.home;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
public class SpacingDecoration extends RecyclerView.ItemDecoration {
|
||||
private final int horizontalSpacing;
|
||||
private final int verticalSpacing;
|
||||
|
||||
public SpacingDecoration(int horizontalSpacing, int verticalSpacing) {
|
||||
this.horizontalSpacing = horizontalSpacing;
|
||||
this.verticalSpacing = verticalSpacing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
|
||||
outRect.set(horizontalSpacing, verticalSpacing, horizontalSpacing, verticalSpacing);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/* -*- 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.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* {@code TabMenuStrip} is the view used to display {@code HomePager} tabs
|
||||
* on tablets. See {@code TabMenuStripLayout} for details about how the
|
||||
* tabs are created and updated.
|
||||
*/
|
||||
public class TabMenuStrip extends HorizontalScrollView
|
||||
implements HomePager.Decor {
|
||||
|
||||
// Offset between the selected tab title and the edge of the screen,
|
||||
// except for the first and last tab in the tab strip.
|
||||
private static final int TITLE_OFFSET_DIPS = 24;
|
||||
|
||||
private final int titleOffset;
|
||||
private final TabMenuStripLayout layout;
|
||||
|
||||
private final Paint shadowPaint;
|
||||
private final int shadowSize;
|
||||
|
||||
public interface OnTitleClickListener {
|
||||
void onTitleClicked(int index);
|
||||
}
|
||||
|
||||
public TabMenuStrip(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
// Disable the scroll bar.
|
||||
setHorizontalScrollBarEnabled(false);
|
||||
setFillViewport(true);
|
||||
|
||||
final Resources res = getResources();
|
||||
|
||||
titleOffset = (int) (TITLE_OFFSET_DIPS * res.getDisplayMetrics().density);
|
||||
|
||||
layout = new TabMenuStripLayout(context, attrs);
|
||||
addView(layout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
|
||||
shadowSize = res.getDimensionPixelSize(R.dimen.tabs_strip_shadow_size);
|
||||
|
||||
shadowPaint = new Paint();
|
||||
shadowPaint.setColor(ContextCompat.getColor(context, R.color.url_bar_shadow));
|
||||
shadowPaint.setStrokeWidth(0.0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
|
||||
final int height = getHeight();
|
||||
canvas.drawRect(0, height - shadowSize, layout.getWidth(), height, shadowPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddPagerView(String title) {
|
||||
layout.onAddPagerView(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAllPagerViews() {
|
||||
layout.removeAllViews();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(final int position) {
|
||||
layout.onPageSelected(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
layout.onPageScrolled(position, positionOffset, positionOffsetPixels);
|
||||
|
||||
final View selectedTitle = layout.getChildAt(position);
|
||||
if (selectedTitle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int selectedTitleOffset = (int) (positionOffset * selectedTitle.getWidth());
|
||||
|
||||
int titleLeft = selectedTitle.getLeft() + selectedTitleOffset;
|
||||
if (position > 0) {
|
||||
titleLeft -= titleOffset;
|
||||
}
|
||||
|
||||
int titleRight = selectedTitle.getRight() + selectedTitleOffset;
|
||||
if (position < layout.getChildCount() - 1) {
|
||||
titleRight += titleOffset;
|
||||
}
|
||||
|
||||
final int scrollX = getScrollX();
|
||||
if (titleLeft < scrollX) {
|
||||
// Tab strip overflows to the left.
|
||||
scrollTo(titleLeft, 0);
|
||||
} else if (titleRight > scrollX + getWidth()) {
|
||||
// Tab strip overflows to the right.
|
||||
scrollTo(titleRight - getWidth(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnTitleClickListener(OnTitleClickListener onTitleClickListener) {
|
||||
layout.setOnTitleClickListener(onTitleClickListener);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
/* -*- 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.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* {@code TabMenuStripLayout} is the view that draws the {@code HomePager}
|
||||
* tabs that are displayed in {@code TabMenuStrip}.
|
||||
*/
|
||||
class TabMenuStripLayout extends LinearLayout
|
||||
implements View.OnFocusChangeListener {
|
||||
|
||||
private TabMenuStrip.OnTitleClickListener onTitleClickListener;
|
||||
private Drawable strip;
|
||||
private TextView selectedView;
|
||||
|
||||
// Data associated with the scrolling of the strip drawable.
|
||||
private View toTab;
|
||||
private View fromTab;
|
||||
private int fromPosition;
|
||||
private int toPosition;
|
||||
private float progress;
|
||||
|
||||
// This variable is used to predict the direction of scroll.
|
||||
private float prevProgress;
|
||||
private int tabContentStart;
|
||||
private boolean titlebarFill;
|
||||
private int activeTextColor;
|
||||
private ColorStateList inactiveTextColor;
|
||||
|
||||
TabMenuStripLayout(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);
|
||||
|
||||
titlebarFill = a.getBoolean(R.styleable.TabMenuStrip_titlebarFill, false);
|
||||
tabContentStart = a.getDimensionPixelSize(R.styleable.TabMenuStrip_tabsMarginLeft, 0);
|
||||
activeTextColor = a.getColor(R.styleable.TabMenuStrip_activeTextColor, R.color.text_and_tabs_tray_grey);
|
||||
inactiveTextColor = a.getColorStateList(R.styleable.TabMenuStrip_inactiveTextColor);
|
||||
a.recycle();
|
||||
|
||||
if (stripResId != -1) {
|
||||
strip = getResources().getDrawable(stripResId);
|
||||
}
|
||||
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
void onAddPagerView(String title) {
|
||||
final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false);
|
||||
button.setText(title.toUpperCase());
|
||||
button.setTextColor(inactiveTextColor);
|
||||
|
||||
// Set titles width to weight, or wrap text width.
|
||||
if (titlebarFill) {
|
||||
button.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f));
|
||||
} else {
|
||||
button.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
if (getChildCount() == 0) {
|
||||
button.setPadding(button.getPaddingLeft() + tabContentStart,
|
||||
button.getPaddingTop(),
|
||||
button.getPaddingRight(),
|
||||
button.getPaddingBottom());
|
||||
}
|
||||
|
||||
addView(button);
|
||||
button.setOnClickListener(new ViewClickListener(getChildCount() - 1));
|
||||
button.setOnFocusChangeListener(this);
|
||||
}
|
||||
|
||||
void onPageSelected(final int position) {
|
||||
if (selectedView != null) {
|
||||
selectedView.setTextColor(inactiveTextColor);
|
||||
}
|
||||
|
||||
selectedView = (TextView) getChildAt(position);
|
||||
selectedView.setTextColor(activeTextColor);
|
||||
|
||||
// Callback to measure and draw the strip after the view is visible.
|
||||
ViewTreeObserver vto = selectedView.getViewTreeObserver();
|
||||
if (vto.isAlive()) {
|
||||
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
selectedView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
|
||||
if (strip != null) {
|
||||
strip.setBounds(selectedView.getLeft() + (position == 0 ? tabContentStart : 0),
|
||||
selectedView.getTop(),
|
||||
selectedView.getRight(),
|
||||
selectedView.getBottom());
|
||||
}
|
||||
|
||||
prevProgress = position;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Page scroll animates the drawable and its bounds from the previous to next child view.
|
||||
void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
if (strip == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setScrollingData(position, positionOffset);
|
||||
|
||||
if (fromTab == null || toTab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int fromTabLeft = fromTab.getLeft();
|
||||
final int fromTabRight = fromTab.getRight();
|
||||
|
||||
final int toTabLeft = toTab.getLeft();
|
||||
final int toTabRight = toTab.getRight();
|
||||
|
||||
// The first tab has a padding applied (tabContentStart). We don't want the 'strip' to jump around so we remove
|
||||
// this padding slowly (modifier) when scrolling to or from the first tab.
|
||||
final int modifier;
|
||||
|
||||
if (fromPosition == 0 && toPosition == 1) {
|
||||
// Slowly remove extra padding (tabContentStart) based on scroll progress
|
||||
modifier = (int) (tabContentStart * (1 - progress));
|
||||
} else if (fromPosition == 1 && toPosition == 0) {
|
||||
// Slowly add extra padding (tabContentStart) based on scroll progress
|
||||
modifier = (int) (tabContentStart * progress);
|
||||
} else {
|
||||
// We are not scrolling tab 0 in any way, no modifier needed
|
||||
modifier = 0;
|
||||
}
|
||||
|
||||
strip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)) + modifier,
|
||||
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.
|
||||
*/
|
||||
void setScrollingData(int position, float positionOffset) {
|
||||
if (position >= getChildCount() - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final float currProgress = position + positionOffset;
|
||||
|
||||
if (prevProgress > currProgress) {
|
||||
toPosition = position;
|
||||
fromPosition = position + 1;
|
||||
progress = 1 - positionOffset;
|
||||
} else {
|
||||
toPosition = position + 1;
|
||||
fromPosition = position;
|
||||
progress = positionOffset;
|
||||
}
|
||||
|
||||
toTab = getChildAt(toPosition);
|
||||
fromTab = getChildAt(fromPosition);
|
||||
|
||||
prevProgress = currProgress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (strip != null) {
|
||||
strip.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (v == this && hasFocus && getChildCount() > 0) {
|
||||
selectedView.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++;
|
||||
}
|
||||
}
|
||||
|
||||
void setOnTitleClickListener(TabMenuStrip.OnTitleClickListener onTitleClickListener) {
|
||||
this.onTitleClickListener = onTitleClickListener;
|
||||
}
|
||||
|
||||
private class ViewClickListener implements OnClickListener {
|
||||
private final int mIndex;
|
||||
|
||||
public ViewClickListener(int index) {
|
||||
mIndex = index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (onTitleClickListener != null) {
|
||||
onTitleClickListener.onTitleClicked(mIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
/* -*- 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.graphics.Bitmap;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.ImageView.ScaleType;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserContract.TopSites;
|
||||
import org.mozilla.gecko.icons.IconCallback;
|
||||
import org.mozilla.gecko.icons.IconResponse;
|
||||
import org.mozilla.gecko.icons.Icons;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* A view that displays the thumbnail and the title/url for a top/pinned site.
|
||||
* 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 TopSitesGridItemView extends RelativeLayout implements IconCallback {
|
||||
private static final String LOGTAG = "GeckoTopSitesGridItemView";
|
||||
|
||||
// Empty state, to denote there is no valid url.
|
||||
private static final int[] STATE_EMPTY = { android.R.attr.state_empty };
|
||||
|
||||
private static final ScaleType SCALE_TYPE_FAVICON = ScaleType.CENTER;
|
||||
private static final ScaleType SCALE_TYPE_RESOURCE = ScaleType.CENTER;
|
||||
private static final ScaleType SCALE_TYPE_THUMBNAIL = ScaleType.CENTER_CROP;
|
||||
private static final ScaleType SCALE_TYPE_URL = ScaleType.CENTER_INSIDE;
|
||||
|
||||
// Child views.
|
||||
private final TextView mTitleView;
|
||||
private final TopSitesThumbnailView mThumbnailView;
|
||||
|
||||
// Data backing this view.
|
||||
private String mTitle;
|
||||
private String mUrl;
|
||||
|
||||
private boolean mThumbnailSet;
|
||||
|
||||
// Matches BrowserContract.TopSites row types
|
||||
private int mType = -1;
|
||||
|
||||
// Dirty state.
|
||||
private boolean mIsDirty;
|
||||
|
||||
private Future<IconResponse> mOngoingIconRequest;
|
||||
|
||||
public TopSitesGridItemView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TopSitesGridItemView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.topSitesGridItemViewStyle);
|
||||
}
|
||||
|
||||
public TopSitesGridItemView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.top_sites_grid_item_view, this);
|
||||
|
||||
mTitleView = (TextView) findViewById(R.id.title);
|
||||
mThumbnailView = (TopSitesThumbnailView) findViewById(R.id.thumbnail);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
if (mType == TopSites.TYPE_BLANK) {
|
||||
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 The site type associated with this view.
|
||||
*/
|
||||
public int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
|
||||
public void blankOut() {
|
||||
mUrl = "";
|
||||
mTitle = "";
|
||||
updateType(TopSites.TYPE_BLANK);
|
||||
updateTitleView();
|
||||
cancelIconLoading();
|
||||
ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
|
||||
displayThumbnail(R.drawable.top_site_add);
|
||||
|
||||
}
|
||||
|
||||
public void markAsDirty() {
|
||||
mIsDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title, URL, and pinned state of this view.
|
||||
*
|
||||
* Also resets our loadId to NOT_LOADING.
|
||||
*
|
||||
* Returns true if any fields changed.
|
||||
*/
|
||||
public boolean updateState(final String title, final String url, final int type, final TopSitesPanel.ThumbnailInfo thumbnail) {
|
||||
boolean changed = false;
|
||||
if (mUrl == null || !mUrl.equals(url)) {
|
||||
mUrl = url;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (mTitle == null || !mTitle.equals(title)) {
|
||||
mTitle = title;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (thumbnail != null) {
|
||||
if (thumbnail.imageUrl != null) {
|
||||
displayThumbnail(thumbnail.imageUrl, thumbnail.bgColor);
|
||||
} else if (thumbnail.bitmap != null) {
|
||||
displayThumbnail(thumbnail.bitmap);
|
||||
}
|
||||
} else if (changed) {
|
||||
// Because we'll have a new favicon or thumbnail arriving shortly, and
|
||||
// we need to not reject it because we already had a thumbnail.
|
||||
mThumbnailSet = false;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
updateTitleView();
|
||||
cancelIconLoading();
|
||||
ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
|
||||
}
|
||||
|
||||
if (updateType(type)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// The dirty state forces the state update to return true
|
||||
// so that the adapter loads favicons once the thumbnails
|
||||
// are loaded in TopSitesPanel/TopSitesGridAdapter.
|
||||
changed = (changed || mIsDirty);
|
||||
mIsDirty = false;
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to load an icon for the given page URL.
|
||||
*/
|
||||
public void loadFavicon(String pageUrl) {
|
||||
mOngoingIconRequest = Icons.with(getContext())
|
||||
.pageUrl(pageUrl)
|
||||
.skipNetwork()
|
||||
.build()
|
||||
.execute(this);
|
||||
}
|
||||
|
||||
private void cancelIconLoading() {
|
||||
if (mOngoingIconRequest != null) {
|
||||
mOngoingIconRequest.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the thumbnail from a resource.
|
||||
*
|
||||
* @param resId Resource ID of the drawable to show.
|
||||
*/
|
||||
public void displayThumbnail(int resId) {
|
||||
mThumbnailView.setScaleType(SCALE_TYPE_RESOURCE);
|
||||
mThumbnailView.setImageResource(resId);
|
||||
mThumbnailView.setBackgroundColor(0x0);
|
||||
mThumbnailSet = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the thumbnail from a bitmap.
|
||||
*
|
||||
* @param thumbnail The bitmap to show as thumbnail.
|
||||
*/
|
||||
public void displayThumbnail(Bitmap thumbnail) {
|
||||
if (thumbnail == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mThumbnailSet = true;
|
||||
|
||||
cancelIconLoading();
|
||||
ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
|
||||
|
||||
mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL);
|
||||
mThumbnailView.setImageBitmap(thumbnail, true);
|
||||
mThumbnailView.setBackgroundDrawable(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the thumbnail from a URL.
|
||||
*
|
||||
* @param imageUrl URL of the image to show.
|
||||
* @param bgColor background color to use in the view.
|
||||
*/
|
||||
public void displayThumbnail(final String imageUrl, final int bgColor) {
|
||||
mThumbnailView.setScaleType(SCALE_TYPE_URL);
|
||||
mThumbnailView.setBackgroundColor(bgColor);
|
||||
mThumbnailSet = true;
|
||||
|
||||
ImageLoader.with(getContext())
|
||||
.load(imageUrl)
|
||||
.noFade()
|
||||
.into(mThumbnailView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the item type associated with this view. Returns true if
|
||||
* the type has changed, false otherwise.
|
||||
*/
|
||||
private boolean updateType(int type) {
|
||||
if (mType == type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mType = type;
|
||||
refreshDrawableState();
|
||||
|
||||
int pinResourceId = (type == TopSites.TYPE_PINNED ? R.drawable.pin : 0);
|
||||
mTitleView.setCompoundDrawablesWithIntrinsicBounds(pinResourceId, 0, 0, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
} else {
|
||||
mTitleView.setText(R.string.home_top_sites_add);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the loaded icon (if no thumbnail is set).
|
||||
*/
|
||||
@Override
|
||||
public void onIconResponse(IconResponse response) {
|
||||
if (mThumbnailSet) {
|
||||
// Already showing a thumbnail; do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
mThumbnailView.setScaleType(SCALE_TYPE_FAVICON);
|
||||
mThumbnailView.setImageBitmap(response.getBitmap(), false);
|
||||
mThumbnailView.setBackgroundColorWithOpacityFilter(response.getColor());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/* -*- 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.content.res.TypedArray;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.View;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.GridView;
|
||||
|
||||
/**
|
||||
* A grid view of top and pinned sites.
|
||||
* Each cell in the grid is a TopSitesGridItemView.
|
||||
*/
|
||||
public class TopSitesGridView extends GridView {
|
||||
private static final String LOGTAG = "GeckoTopSitesGridView";
|
||||
|
||||
// Listener for editing pinned sites.
|
||||
public static interface OnEditPinnedSiteListener {
|
||||
public void onEditPinnedSite(int position, String searchTerm);
|
||||
}
|
||||
|
||||
// Max number of top sites that needs to be shown.
|
||||
private final int mMaxSites;
|
||||
|
||||
// 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;
|
||||
|
||||
// Measured width of this view.
|
||||
private int mMeasuredWidth;
|
||||
|
||||
// Measured height of this view.
|
||||
private int mMeasuredHeight;
|
||||
|
||||
// A dummy View used to measure the required size of the child Views.
|
||||
private final TopSitesGridItemView dummyChildView;
|
||||
|
||||
// Context menu info.
|
||||
private TopSitesGridContextMenuInfo mContextMenuInfo;
|
||||
|
||||
// Whether we're handling focus changes or not. This is used
|
||||
// to avoid infinite re-layouts when using this GridView as
|
||||
// a ListView header view (see bug 918044).
|
||||
private boolean mIsHandlingFocusChange;
|
||||
|
||||
public TopSitesGridView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TopSitesGridView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.topSitesGridViewStyle);
|
||||
}
|
||||
|
||||
public TopSitesGridView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
mMaxSites = 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.TopSitesGridView, defStyle, 0);
|
||||
mHorizontalSpacing = a.getDimensionPixelOffset(R.styleable.TopSitesGridView_android_horizontalSpacing, 0x00);
|
||||
mVerticalSpacing = a.getDimensionPixelOffset(R.styleable.TopSitesGridView_android_verticalSpacing, 0x00);
|
||||
a.recycle();
|
||||
|
||||
dummyChildView = new TopSitesGridItemView(context);
|
||||
// Set a default LayoutParams on the child, if it doesn't have one on its own.
|
||||
AbsListView.LayoutParams params = (AbsListView.LayoutParams) dummyChildView.getLayoutParams();
|
||||
if (params == null) {
|
||||
params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT,
|
||||
AbsListView.LayoutParams.WRAP_CONTENT);
|
||||
dummyChildView.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
||||
mIsHandlingFocusChange = true;
|
||||
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||
mIsHandlingFocusChange = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestLayout() {
|
||||
if (!mIsHandlingFocusChange) {
|
||||
super.requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
// Sets the padding for this view.
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
final int measuredWidth = getMeasuredWidth();
|
||||
if (measuredWidth == mMeasuredWidth) {
|
||||
// Return the cached values as the width is the same.
|
||||
setMeasuredDimension(mMeasuredWidth, mMeasuredHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
final int columnWidth = getColumnWidth();
|
||||
|
||||
// Measure the exact width of the child, and the height based on the width.
|
||||
// Note: the child (and TopSitesThumbnailView) takes care of calculating its height.
|
||||
int childWidthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
|
||||
int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
|
||||
dummyChildView.measure(childWidthSpec, childHeightSpec);
|
||||
final int childHeight = dummyChildView.getMeasuredHeight();
|
||||
|
||||
// This is the maximum width of the contents of each child in the grid.
|
||||
// Use this as the target width for thumbnails.
|
||||
final int thumbnailWidth = dummyChildView.getMeasuredWidth() - dummyChildView.getPaddingLeft() - dummyChildView.getPaddingRight();
|
||||
ThumbnailHelper.getInstance().setThumbnailWidth(thumbnailWidth);
|
||||
|
||||
// Number of rows required to show these top sites.
|
||||
final int rows = (int) Math.ceil((double) mMaxSites / 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);
|
||||
mMeasuredWidth = measuredWidth;
|
||||
mMeasuredHeight = measuredHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContextMenuInfo getContextMenuInfo() {
|
||||
return mContextMenuInfo;
|
||||
}
|
||||
|
||||
public void setContextMenuInfo(TopSitesGridContextMenuInfo contextMenuInfo) {
|
||||
mContextMenuInfo = contextMenuInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores information regarding the creation of the context menu for a GridView item.
|
||||
*/
|
||||
public static class TopSitesGridContextMenuInfo extends HomeContextMenuInfo {
|
||||
public int type = -1;
|
||||
|
||||
public TopSitesGridContextMenuInfo(View targetView, int position, long id) {
|
||||
super(targetView, position, id);
|
||||
this.itemType = RemoveItemType.HISTORY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,968 @@
|
|||
/* -*- 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 static org.mozilla.gecko.db.URLMetadataTable.TILE_COLOR_COLUMN;
|
||||
import static org.mozilla.gecko.db.URLMetadataTable.TILE_IMAGE_URL_COLUMN;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
|
||||
import org.mozilla.gecko.db.BrowserContract.TopSites;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.gfx.BitmapUtils;
|
||||
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
|
||||
import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;
|
||||
import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
|
||||
import org.mozilla.gecko.icons.IconCallback;
|
||||
import org.mozilla.gecko.icons.IconResponse;
|
||||
import org.mozilla.gecko.icons.Icons;
|
||||
import org.mozilla.gecko.restrictions.Restrictable;
|
||||
import org.mozilla.gecko.restrictions.Restrictions;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.AsyncTaskLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
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.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
/**
|
||||
* Fragment that displays frecency search results in a ListView.
|
||||
*/
|
||||
public class TopSitesPanel extends HomeFragment {
|
||||
// Logging tag name
|
||||
private static final String LOGTAG = "GeckoTopSitesPanel";
|
||||
|
||||
// Cursor loader ID for the top sites
|
||||
private static final int LOADER_ID_TOP_SITES = 0;
|
||||
|
||||
// Loader ID for thumbnails
|
||||
private static final int LOADER_ID_THUMBNAILS = 1;
|
||||
|
||||
// Key for thumbnail urls
|
||||
private static final String THUMBNAILS_URLS_KEY = "urls";
|
||||
|
||||
// Adapter for the list of top sites
|
||||
private VisitedAdapter mListAdapter;
|
||||
|
||||
// Adapter for the grid of top sites
|
||||
private TopSitesGridAdapter mGridAdapter;
|
||||
|
||||
// List of top sites
|
||||
private HomeListView mList;
|
||||
|
||||
// Grid of top sites
|
||||
private TopSitesGridView mGrid;
|
||||
|
||||
// Callbacks used for the search and favicon cursor loaders
|
||||
private CursorLoaderCallbacks mCursorLoaderCallbacks;
|
||||
|
||||
// Callback for thumbnail loader
|
||||
private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks;
|
||||
|
||||
// Listener for editing pinned sites.
|
||||
private EditPinnedSiteListener mEditPinnedSiteListener;
|
||||
|
||||
// Max number of entries shown in the grid from the cursor.
|
||||
private int mMaxGridEntries;
|
||||
|
||||
// Time in ms until the Gecko thread is reset to normal priority.
|
||||
private static final long PRIORITY_RESET_TIMEOUT = 10000;
|
||||
|
||||
public static TopSitesPanel newInstance() {
|
||||
return new TopSitesPanel();
|
||||
}
|
||||
|
||||
private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
|
||||
private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
|
||||
|
||||
private static void debug(final String message) {
|
||||
if (logDebug) {
|
||||
Log.d(LOGTAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(final String message) {
|
||||
if (logVerbose) {
|
||||
Log.v(LOGTAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
|
||||
mMaxGridEntries = activity.getResources().getInteger(R.integer.number_of_top_sites);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.home_top_sites_panel, container, false);
|
||||
|
||||
mList = (HomeListView) view.findViewById(R.id.list);
|
||||
|
||||
mGrid = new TopSitesGridView(getActivity());
|
||||
mList.addHeaderView(mGrid);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
mEditPinnedSiteListener = new EditPinnedSiteListener();
|
||||
|
||||
mList.setTag(HomePager.LIST_TAG_TOP_SITES);
|
||||
mList.setHeaderDividersEnabled(false);
|
||||
|
||||
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@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 += (mGridAdapter.getCount() - headerCount);
|
||||
|
||||
final Cursor c = mListAdapter.getCursor();
|
||||
if (c == null || !c.moveToPosition(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String url = c.getString(c.getColumnIndexOrThrow(TopSites.URL));
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "top_sites");
|
||||
|
||||
// This item is a TwoLinePageRow, so we allow switch-to-tab.
|
||||
mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
|
||||
}
|
||||
});
|
||||
|
||||
mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
|
||||
@Override
|
||||
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
|
||||
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
|
||||
info.url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
|
||||
info.title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE));
|
||||
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.HISTORY_ID));
|
||||
info.itemType = RemoveItemType.HISTORY;
|
||||
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(TopSites.BOOKMARK_ID);
|
||||
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.
|
||||
info.bookmarkId = -1;
|
||||
} else {
|
||||
info.bookmarkId = cursor.getInt(bookmarkIdCol);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
});
|
||||
|
||||
mGrid.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
TopSitesGridItemView item = (TopSitesGridItemView) view;
|
||||
|
||||
// Decode "user-entered" URLs before loading them.
|
||||
String url = StringUtils.decodeUserEnteredUrl(item.getUrl());
|
||||
int type = item.getType();
|
||||
|
||||
// If the url is empty, the user can pin a site.
|
||||
// If not, navigate to the page given by the url.
|
||||
if (type != TopSites.TYPE_BLANK) {
|
||||
if (mUrlOpenListener != null) {
|
||||
final TelemetryContract.Method method;
|
||||
if (type == TopSites.TYPE_SUGGESTED) {
|
||||
method = TelemetryContract.Method.SUGGESTION;
|
||||
} else {
|
||||
method = TelemetryContract.Method.GRID_ITEM;
|
||||
}
|
||||
|
||||
String extra = Integer.toString(position);
|
||||
if (type == TopSites.TYPE_PINNED) {
|
||||
extra += "-pinned";
|
||||
}
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method, extra);
|
||||
|
||||
mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.NO_READER_VIEW));
|
||||
}
|
||||
} else {
|
||||
if (mEditPinnedSiteListener != null) {
|
||||
mEditPinnedSiteListener.onEditPinnedSite(position, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mGrid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
|
||||
Cursor cursor = (Cursor) parent.getItemAtPosition(position);
|
||||
|
||||
TopSitesGridItemView item = (TopSitesGridItemView) view;
|
||||
if (cursor == null || item.getType() == TopSites.TYPE_BLANK) {
|
||||
mGrid.setContextMenuInfo(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
TopSitesGridContextMenuInfo contextMenuInfo = new TopSitesGridContextMenuInfo(view, position, id);
|
||||
updateContextMenuFromCursor(contextMenuInfo, cursor);
|
||||
mGrid.setContextMenuInfo(contextMenuInfo);
|
||||
return mGrid.showContextMenuForChild(mGrid);
|
||||
}
|
||||
|
||||
/*
|
||||
* Update the fields of a TopSitesGridContextMenuInfo object
|
||||
* from a cursor.
|
||||
*
|
||||
* @param info context menu info object to be updated
|
||||
* @param cursor used to update the context menu info object
|
||||
*/
|
||||
private void updateContextMenuFromCursor(TopSitesGridContextMenuInfo info, Cursor cursor) {
|
||||
info.url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
|
||||
info.title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE));
|
||||
info.type = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.TYPE));
|
||||
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.HISTORY_ID));
|
||||
}
|
||||
});
|
||||
|
||||
registerForContextMenu(mList);
|
||||
registerForContextMenu(mGrid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
// Discard any additional item clicks on the list as the
|
||||
// panel is getting destroyed (see bugs 930160 & 1096958).
|
||||
mList.setOnItemClickListener(null);
|
||||
mGrid.setOnItemClickListener(null);
|
||||
|
||||
mList = null;
|
||||
mGrid = null;
|
||||
mListAdapter = null;
|
||||
mGridAdapter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
final Activity activity = getActivity();
|
||||
|
||||
// Setup the top sites grid adapter.
|
||||
mGridAdapter = new TopSitesGridAdapter(activity, null);
|
||||
mGrid.setAdapter(mGridAdapter);
|
||||
|
||||
// Setup the top sites list adapter.
|
||||
mListAdapter = new VisitedAdapter(activity, null);
|
||||
mList.setAdapter(mListAdapter);
|
||||
|
||||
// Create callbacks before the initial loader is started
|
||||
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
|
||||
mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks();
|
||||
loadIfVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
|
||||
if (menuInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) {
|
||||
// Long pressed item was not a Top Sites GridView item. Superclass
|
||||
// can handle this.
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
|
||||
if (!Restrictions.isAllowed(view.getContext(), Restrictable.CLEAR_HISTORY)) {
|
||||
menu.findItem(R.id.home_remove).setVisible(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final Context context = view.getContext();
|
||||
|
||||
// Long pressed item was a Top Sites GridView item, handle it.
|
||||
MenuInflater inflater = new MenuInflater(context);
|
||||
inflater.inflate(R.menu.home_contextmenu, menu);
|
||||
|
||||
// Hide unused menu items.
|
||||
menu.findItem(R.id.home_edit_bookmark).setVisible(false);
|
||||
|
||||
menu.findItem(R.id.home_remove).setVisible(Restrictions.isAllowed(context, Restrictable.CLEAR_HISTORY));
|
||||
|
||||
TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
|
||||
menu.setHeaderTitle(info.getDisplayTitle());
|
||||
|
||||
if (info.type != TopSites.TYPE_BLANK) {
|
||||
if (info.type == TopSites.TYPE_PINNED) {
|
||||
menu.findItem(R.id.top_sites_pin).setVisible(false);
|
||||
} else {
|
||||
menu.findItem(R.id.top_sites_unpin).setVisible(false);
|
||||
}
|
||||
} else {
|
||||
menu.findItem(R.id.home_open_new_tab).setVisible(false);
|
||||
menu.findItem(R.id.home_open_private_tab).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_pin).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_unpin).setVisible(false);
|
||||
}
|
||||
|
||||
if (!StringUtils.isShareableUrl(info.url) || GeckoProfile.get(getActivity()).inGuestMode()) {
|
||||
menu.findItem(R.id.home_share).setVisible(false);
|
||||
}
|
||||
|
||||
if (!Restrictions.isAllowed(context, Restrictable.PRIVATE_BROWSING)) {
|
||||
menu.findItem(R.id.home_open_private_tab).setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
if (super.onContextItemSelected(item)) {
|
||||
// HomeFragment was able to handle to selected item.
|
||||
return true;
|
||||
}
|
||||
|
||||
ContextMenuInfo menuInfo = item.getMenuInfo();
|
||||
|
||||
if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
|
||||
|
||||
final int itemId = item.getItemId();
|
||||
final BrowserDB db = BrowserDB.from(getActivity());
|
||||
|
||||
if (itemId == R.id.top_sites_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() {
|
||||
db.pinSite(context.getContentResolver(), url, title, position);
|
||||
}
|
||||
});
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.PIN);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.top_sites_unpin) {
|
||||
final int position = info.position;
|
||||
final Context context = getActivity().getApplicationContext();
|
||||
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
db.unpinSite(context.getContentResolver(), position);
|
||||
}
|
||||
});
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.UNPIN);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.top_sites_edit) {
|
||||
// Decode "user-entered" URLs before showing them.
|
||||
mEditPinnedSiteListener.onEditPinnedSite(info.position,
|
||||
StringUtils.decodeUserEnteredUrl(info.url));
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.EDIT);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
getLoaderManager().initLoader(LOADER_ID_TOP_SITES, null, mCursorLoaderCallbacks);
|
||||
|
||||
// Since this is the primary fragment that loads whenever about:home is
|
||||
// visited, we want to load it as quickly as possible. Heavy load on
|
||||
// the Gecko thread can slow down the time it takes for thumbnails to
|
||||
// appear, especially during startup (bug 897162). By minimizing the
|
||||
// Gecko thread priority, we ensure that the UI appears quickly. The
|
||||
// priority is reset to normal once thumbnails are loaded.
|
||||
ThreadUtils.reduceGeckoPriority(PRIORITY_RESET_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for editing pinned sites.
|
||||
*/
|
||||
private class EditPinnedSiteListener implements OnEditPinnedSiteListener,
|
||||
OnSiteSelectedListener {
|
||||
// Tag for the PinSiteDialog fragment.
|
||||
private static final String TAG_PIN_SITE = "pin_site";
|
||||
|
||||
// Position of the pin.
|
||||
private int mPosition;
|
||||
|
||||
@Override
|
||||
public void onEditPinnedSite(int position, String searchTerm) {
|
||||
final FragmentManager manager = getChildFragmentManager();
|
||||
PinSiteDialog dialog = (PinSiteDialog) manager.findFragmentByTag(TAG_PIN_SITE);
|
||||
if (dialog == null) {
|
||||
mPosition = position;
|
||||
|
||||
dialog = PinSiteDialog.newInstance();
|
||||
dialog.setOnSiteSelectedListener(this);
|
||||
dialog.setSearchTerm(searchTerm);
|
||||
dialog.show(manager, TAG_PIN_SITE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSiteSelected(final String url, final String title) {
|
||||
final int position = mPosition;
|
||||
final Context context = getActivity().getApplicationContext();
|
||||
final BrowserDB db = BrowserDB.from(getActivity());
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
db.pinSite(context.getContentResolver(), url, title, position);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUiFromCursor(Cursor c) {
|
||||
mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries);
|
||||
}
|
||||
|
||||
private void updateUiWithThumbnails(Map<String, ThumbnailInfo> thumbnails) {
|
||||
if (mGridAdapter != null) {
|
||||
mGridAdapter.updateThumbnails(thumbnails);
|
||||
}
|
||||
|
||||
// Once thumbnails have finished loading, the UI is ready. Reset
|
||||
// Gecko to normal priority.
|
||||
ThreadUtils.resetGeckoPriority();
|
||||
}
|
||||
|
||||
private static class TopSitesLoader extends SimpleCursorLoader {
|
||||
// Max number of search results.
|
||||
private static final int SEARCH_LIMIT = 30;
|
||||
private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_TOPSITES_LOADER_TIME_MS";
|
||||
private final BrowserDB mDB;
|
||||
private final int mMaxGridEntries;
|
||||
|
||||
public TopSitesLoader(Context context) {
|
||||
super(context);
|
||||
mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites);
|
||||
mDB = BrowserDB.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor loadCursor() {
|
||||
final long start = SystemClock.uptimeMillis();
|
||||
final Cursor cursor = mDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
|
||||
final long end = SystemClock.uptimeMillis();
|
||||
final long took = end - start;
|
||||
Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
|
||||
return cursor;
|
||||
}
|
||||
}
|
||||
|
||||
private class VisitedAdapter extends CursorAdapter {
|
||||
public VisitedAdapter(Context context, Cursor cursor) {
|
||||
super(context, cursor, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return Math.max(0, super.getCount() - mMaxGridEntries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return super.getItem(position + mMaxGridEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* We have to override default getItemId implementation, since for a given position, it returns
|
||||
* value of the _id column. In our case _id is always 0 (see Combined view).
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
final int adjustedPosition = position + mMaxGridEntries;
|
||||
final Cursor cursor = getCursor();
|
||||
|
||||
cursor.moveToPosition(adjustedPosition);
|
||||
return getItemIdForTopSitesCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
final int position = cursor.getPosition();
|
||||
cursor.moveToPosition(position + mMaxGridEntries);
|
||||
|
||||
final TwoLinePageRow row = (TwoLinePageRow) view;
|
||||
row.updateFromCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return LayoutInflater.from(context).inflate(R.layout.bookmark_item_row, parent, false);
|
||||
}
|
||||
}
|
||||
|
||||
public class TopSitesGridAdapter extends CursorAdapter {
|
||||
private final BrowserDB mDB;
|
||||
// Cache to store the thumbnails.
|
||||
// Ensure that this is only accessed from the UI thread.
|
||||
private Map<String, ThumbnailInfo> mThumbnailInfos;
|
||||
|
||||
public TopSitesGridAdapter(Context context, Cursor cursor) {
|
||||
super(context, cursor, 0);
|
||||
mDB = BrowserDB.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return Math.min(mMaxGridEntries, super.getCount());
|
||||
}
|
||||
|
||||
@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, ThumbnailInfo> thumbnails) {
|
||||
mThumbnailInfos = thumbnails;
|
||||
|
||||
final int count = mGrid.getChildCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
TopSitesGridItemView gridItem = (TopSitesGridItemView) mGrid.getChildAt(i);
|
||||
|
||||
// All the views have already got their initial state at this point.
|
||||
// This will force each view to load favicons for the missing
|
||||
// thumbnails if necessary.
|
||||
gridItem.markAsDirty();
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* We have to override default getItemId implementation, since for a given position, it returns
|
||||
* value of the _id column. In our case _id is always 0 (see Combined view).
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
final Cursor cursor = getCursor();
|
||||
cursor.moveToPosition(position);
|
||||
|
||||
return getItemIdForTopSitesCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View bindView, Context context, Cursor cursor) {
|
||||
final String url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
|
||||
final String title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE));
|
||||
final int type = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.TYPE));
|
||||
|
||||
final TopSitesGridItemView view = (TopSitesGridItemView) bindView;
|
||||
|
||||
// If there is no url, then show "add bookmark".
|
||||
if (type == TopSites.TYPE_BLANK) {
|
||||
view.blankOut();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the thumbnail, if any.
|
||||
ThumbnailInfo thumbnail = (mThumbnailInfos != null ? mThumbnailInfos.get(url) : null);
|
||||
|
||||
// Debounce bindView calls to avoid redundant redraws and favicon
|
||||
// fetches.
|
||||
final boolean updated = view.updateState(title, url, type, thumbnail);
|
||||
|
||||
// Thumbnails are delivered late, so we can't short-circuit any
|
||||
// sooner than this. But we can avoid a duplicate favicon
|
||||
// fetch...
|
||||
if (!updated) {
|
||||
debug("bindView called twice for same values; short-circuiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we query suggested images without the user-entered wrapper.
|
||||
final String decodedUrl = StringUtils.decodeUserEnteredUrl(url);
|
||||
|
||||
// Suggested images have precedence over thumbnails, no need to wait
|
||||
// for them to be loaded. See: CursorLoaderCallbacks.onLoadFinished()
|
||||
final String imageUrl = mDB.getSuggestedImageUrlForUrl(decodedUrl);
|
||||
if (!TextUtils.isEmpty(imageUrl)) {
|
||||
final int bgColor = mDB.getSuggestedBackgroundColorForUrl(decodedUrl);
|
||||
view.displayThumbnail(imageUrl, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// If thumbnails are still being loaded, don't try to load favicons
|
||||
// just yet. If we sent in a thumbnail, we're done now.
|
||||
if (mThumbnailInfos == null || thumbnail != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.loadFavicon(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return new TopSitesGridItemView(context);
|
||||
}
|
||||
}
|
||||
|
||||
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
trace("Creating TopSitesLoader: " + id);
|
||||
return new TopSitesLoader(getActivity());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called *twice* in some circumstances.
|
||||
*
|
||||
* If you try to avoid that through some kind of boolean flag,
|
||||
* sometimes (e.g., returning to the activity) you'll *not* be called
|
||||
* twice, and thus you'll never draw thumbnails.
|
||||
*
|
||||
* The root cause is TopSitesLoader.loadCursor being called twice.
|
||||
* Why that is... dunno.
|
||||
*/
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
||||
debug("onLoadFinished: " + c.getCount() + " rows.");
|
||||
|
||||
mListAdapter.swapCursor(c);
|
||||
mGridAdapter.swapCursor(c);
|
||||
updateUiFromCursor(c);
|
||||
|
||||
final int col = c.getColumnIndexOrThrow(TopSites.URL);
|
||||
|
||||
// Load the thumbnails.
|
||||
// Even though the cursor we're given is supposed to be fresh,
|
||||
// we getIcon a bad first value unless we reset its position.
|
||||
// Using move(-1) and moveToNext() doesn't work correctly under
|
||||
// rotation, so we use moveToFirst.
|
||||
if (!c.moveToFirst()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ArrayList<String> urls = new ArrayList<String>();
|
||||
int i = 1;
|
||||
do {
|
||||
final String url = c.getString(col);
|
||||
|
||||
// Only try to fetch thumbnails for non-empty URLs that
|
||||
// don't have an associated suggested image URL.
|
||||
final GeckoProfile profile = GeckoProfile.get(getActivity());
|
||||
if (TextUtils.isEmpty(url) || BrowserDB.from(profile).hasSuggestedImageUrl(url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
urls.add(url);
|
||||
} while (i++ < mMaxGridEntries && c.moveToNext());
|
||||
|
||||
if (urls.isEmpty()) {
|
||||
// Short-circuit empty results to the UI.
|
||||
updateUiWithThumbnails(new HashMap<String, ThumbnailInfo>());
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putStringArrayList(THUMBNAILS_URLS_KEY, urls);
|
||||
getLoaderManager().restartLoader(LOADER_ID_THUMBNAILS, bundle, mThumbnailsLoaderCallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
if (mListAdapter != null) {
|
||||
mListAdapter.swapCursor(null);
|
||||
}
|
||||
|
||||
if (mGridAdapter != null) {
|
||||
mGridAdapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ThumbnailInfo {
|
||||
public final Bitmap bitmap;
|
||||
public final String imageUrl;
|
||||
public final int bgColor;
|
||||
|
||||
public ThumbnailInfo(final Bitmap bitmap) {
|
||||
this.bitmap = bitmap;
|
||||
this.imageUrl = null;
|
||||
this.bgColor = Color.TRANSPARENT;
|
||||
}
|
||||
|
||||
public ThumbnailInfo(final String imageUrl, final int bgColor) {
|
||||
this.bitmap = null;
|
||||
this.imageUrl = imageUrl;
|
||||
this.bgColor = bgColor;
|
||||
}
|
||||
|
||||
public static ThumbnailInfo fromMetadata(final Map<String, Object> data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String imageUrl = (String) data.get(TILE_IMAGE_URL_COLUMN);
|
||||
if (imageUrl == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int bgColor = Color.WHITE;
|
||||
final String colorString = (String) data.get(TILE_COLOR_COLUMN);
|
||||
try {
|
||||
bgColor = Color.parseColor(colorString);
|
||||
} catch (Exception ex) {
|
||||
}
|
||||
|
||||
return new ThumbnailInfo(imageUrl, bgColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An AsyncTaskLoader to load the thumbnails from a cursor.
|
||||
*/
|
||||
static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, ThumbnailInfo>> {
|
||||
private final BrowserDB mDB;
|
||||
private Map<String, ThumbnailInfo> mThumbnailInfos;
|
||||
private final ArrayList<String> mUrls;
|
||||
|
||||
private static final List<String> COLUMNS;
|
||||
static {
|
||||
final ArrayList<String> tempColumns = new ArrayList<>(2);
|
||||
tempColumns.add(TILE_IMAGE_URL_COLUMN);
|
||||
tempColumns.add(TILE_COLOR_COLUMN);
|
||||
COLUMNS = Collections.unmodifiableList(tempColumns);
|
||||
}
|
||||
|
||||
public ThumbnailsLoader(Context context, ArrayList<String> urls) {
|
||||
super(context);
|
||||
mUrls = urls;
|
||||
mDB = BrowserDB.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ThumbnailInfo> loadInBackground() {
|
||||
final Map<String, ThumbnailInfo> thumbnails = new HashMap<String, ThumbnailInfo>();
|
||||
if (mUrls == null || mUrls.size() == 0) {
|
||||
return thumbnails;
|
||||
}
|
||||
|
||||
// We need to query metadata based on the URL without any refs, hence we create a new
|
||||
// mapping and list of these URLs (we need to preserve the original URL for display purposes)
|
||||
final Map<String, String> queryURLs = new HashMap<>();
|
||||
for (final String pageURL : mUrls) {
|
||||
queryURLs.put(pageURL, StringUtils.stripRef(pageURL));
|
||||
}
|
||||
|
||||
// Query the DB for tile images.
|
||||
final ContentResolver cr = getContext().getContentResolver();
|
||||
// Use the stripped URLs for querying the DB
|
||||
final Map<String, Map<String, Object>> metadata = mDB.getURLMetadata().getForURLs(cr, queryURLs.values(), COLUMNS);
|
||||
|
||||
// Keep a list of urls that don't have tiles images. We'll use thumbnails for them instead.
|
||||
final List<String> thumbnailUrls = new ArrayList<String>();
|
||||
for (final String pageURL : mUrls) {
|
||||
final String queryURL = queryURLs.get(pageURL);
|
||||
|
||||
ThumbnailInfo info = ThumbnailInfo.fromMetadata(metadata.get(queryURL));
|
||||
if (info == null) {
|
||||
// If we didn't find metadata, we'll look for a thumbnail for this url.
|
||||
thumbnailUrls.add(pageURL);
|
||||
continue;
|
||||
}
|
||||
|
||||
thumbnails.put(pageURL, info);
|
||||
}
|
||||
|
||||
if (thumbnailUrls.size() == 0) {
|
||||
return thumbnails;
|
||||
}
|
||||
|
||||
// Query the DB for tile thumbnails.
|
||||
final Cursor cursor = mDB.getThumbnailsForUrls(cr, thumbnailUrls);
|
||||
if (cursor == null) {
|
||||
return thumbnails;
|
||||
}
|
||||
|
||||
try {
|
||||
final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
|
||||
final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
String url = cursor.getString(urlIndex);
|
||||
|
||||
// This should never be null, but if it is...
|
||||
final byte[] b = cursor.getBlob(dataIndex);
|
||||
if (b == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final Bitmap bitmap = BitmapUtils.decodeByteArray(b);
|
||||
|
||||
// Our thumbnails are never null, so if we getIcon a null decoded
|
||||
// bitmap, it's because we hit an OOM or some other disaster.
|
||||
// Give up immediately rather than hammering on.
|
||||
if (bitmap == null) {
|
||||
Log.w(LOGTAG, "Aborting thumbnail load; decode failed.");
|
||||
break;
|
||||
}
|
||||
|
||||
thumbnails.put(url, new ThumbnailInfo(bitmap));
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return thumbnails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(Map<String, ThumbnailInfo> thumbnails) {
|
||||
if (isReset()) {
|
||||
mThumbnailInfos = null;
|
||||
return;
|
||||
}
|
||||
|
||||
mThumbnailInfos = thumbnails;
|
||||
|
||||
if (isStarted()) {
|
||||
super.deliverResult(thumbnails);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
if (mThumbnailInfos != null) {
|
||||
deliverResult(mThumbnailInfos);
|
||||
}
|
||||
|
||||
if (takeContentChanged() || mThumbnailInfos == null) {
|
||||
forceLoad();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled(Map<String, ThumbnailInfo> thumbnails) {
|
||||
mThumbnailInfos = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
super.onReset();
|
||||
|
||||
// Ensure the loader is stopped.
|
||||
onStopLoading();
|
||||
|
||||
mThumbnailInfos = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader callbacks for the thumbnails on TopSitesGridView.
|
||||
*/
|
||||
private class ThumbnailsLoaderCallbacks implements LoaderCallbacks<Map<String, ThumbnailInfo>> {
|
||||
@Override
|
||||
public Loader<Map<String, ThumbnailInfo>> onCreateLoader(int id, Bundle args) {
|
||||
return new ThumbnailsLoader(getActivity(), args.getStringArrayList(THUMBNAILS_URLS_KEY));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Map<String, ThumbnailInfo>> loader, Map<String, ThumbnailInfo> thumbnails) {
|
||||
updateUiWithThumbnails(thumbnails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Map<String, ThumbnailInfo>> loader) {
|
||||
if (mGridAdapter != null) {
|
||||
mGridAdapter.updateThumbnails(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We are trying to return stable IDs so that Android can recycle views appropriately:
|
||||
* - If we have a history ID then we return it
|
||||
* - If we only have a bookmark ID then we negate it and return it. We negate it in order
|
||||
* to avoid clashing/conflicting with history IDs.
|
||||
*
|
||||
* @param cursorInPosition Cursor already moved to position for which we're getting a stable ID
|
||||
* @return Stable ID for a given cursor
|
||||
*/
|
||||
private static long getItemIdForTopSitesCursor(final Cursor cursorInPosition) {
|
||||
final int historyIdCol = cursorInPosition.getColumnIndexOrThrow(TopSites.HISTORY_ID);
|
||||
final long historyId = cursorInPosition.getLong(historyIdCol);
|
||||
if (historyId != 0) {
|
||||
return historyId;
|
||||
}
|
||||
|
||||
final int bookmarkIdCol = cursorInPosition.getColumnIndexOrThrow(TopSites.BOOKMARK_ID);
|
||||
final long bookmarkId = cursorInPosition.getLong(bookmarkIdCol);
|
||||
return -1 * bookmarkId;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 android.support.v4.content.ContextCompat;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.ThumbnailHelper;
|
||||
import org.mozilla.gecko.widget.CropImageView;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff.Mode;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* A width constrained ImageView to show thumbnails of top and pinned sites.
|
||||
*/
|
||||
public class TopSitesThumbnailView extends CropImageView {
|
||||
private static final String LOGTAG = "GeckoTopSitesThumbnailView";
|
||||
|
||||
// 27.34% opacity filter for the dominant color.
|
||||
private static final int COLOR_FILTER = 0x46FFFFFF;
|
||||
|
||||
// Default filter color for "Add a bookmark" views.
|
||||
private final int mDefaultColor = ContextCompat.getColor(getContext(), R.color.top_site_default);
|
||||
|
||||
// Stroke width for the border.
|
||||
private final float mStrokeWidth = getResources().getDisplayMetrics().density * 2;
|
||||
|
||||
// Paint for drawing the border.
|
||||
private final Paint mBorderPaint;
|
||||
|
||||
public TopSitesThumbnailView(Context context) {
|
||||
this(context, null);
|
||||
|
||||
// A border will be drawn if needed.
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
public TopSitesThumbnailView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.topSitesThumbnailViewStyle);
|
||||
}
|
||||
|
||||
public TopSitesThumbnailView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
// Initialize the border paint.
|
||||
final Resources res = getResources();
|
||||
mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mBorderPaint.setColor(ContextCompat.getColor(context, R.color.top_site_border));
|
||||
mBorderPaint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getAspectRatio() {
|
||||
return ThumbnailHelper.TOP_SITES_THUMBNAIL_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (getBackground() == null) {
|
||||
mBorderPaint.setStrokeWidth(mStrokeWidth);
|
||||
canvas.drawRect(0, 0, getWidth(), getHeight(), mBorderPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the background color with a filter to reduce the color opacity.
|
||||
*
|
||||
* @param color the color filter to apply over the drawable.
|
||||
*/
|
||||
public void setBackgroundColorWithOpacityFilter(int color) {
|
||||
setBackgroundColor(color & COLOR_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (color == 0) {
|
||||
color = mDefaultColor;
|
||||
}
|
||||
|
||||
Drawable drawable = getResources().getDrawable(R.drawable.top_sites_thumbnail_bg);
|
||||
drawable.setColorFilter(color, Mode.SRC_ATOP);
|
||||
setBackgroundDrawable(drawable);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
/* -*- 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 java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.mozilla.gecko.AboutPages;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tab;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserContract.Combined;
|
||||
import org.mozilla.gecko.db.BrowserContract.URLColumns;
|
||||
import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
|
||||
import org.mozilla.gecko.icons.IconDescriptor;
|
||||
import org.mozilla.gecko.icons.IconResponse;
|
||||
import org.mozilla.gecko.icons.Icons;
|
||||
import org.mozilla.gecko.reader.ReaderModeUtils;
|
||||
import org.mozilla.gecko.reader.SavedReaderViewHelper;
|
||||
import org.mozilla.gecko.widget.FaviconView;
|
||||
|
||||
public class TwoLinePageRow extends LinearLayout
|
||||
implements Tabs.OnTabsChangedListener {
|
||||
|
||||
protected static final int NO_ICON = 0;
|
||||
|
||||
private final TextView mTitle;
|
||||
private final TextView mUrl;
|
||||
private final ImageView mStatusIcon;
|
||||
|
||||
private int mSwitchToTabIconId;
|
||||
|
||||
private final FaviconView mFavicon;
|
||||
private Future<IconResponse> mOngoingIconLoad;
|
||||
|
||||
private boolean mShowIcons;
|
||||
|
||||
// The URL for the page corresponding to this view.
|
||||
private String mPageUrl;
|
||||
|
||||
private boolean mHasReaderCacheItem;
|
||||
|
||||
public TwoLinePageRow(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TwoLinePageRow(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setGravity(Gravity.CENTER_VERTICAL);
|
||||
|
||||
LayoutInflater.from(context).inflate(R.layout.two_line_page_row, this);
|
||||
// Merge layouts lose their padding, so set it dynamically.
|
||||
setPadding(0, 0, (int) getResources().getDimension(R.dimen.page_row_edge_padding), 0);
|
||||
|
||||
mTitle = (TextView) findViewById(R.id.title);
|
||||
mUrl = (TextView) findViewById(R.id.url);
|
||||
mStatusIcon = (ImageView) findViewById(R.id.status_icon_bookmark);
|
||||
|
||||
mSwitchToTabIconId = NO_ICON;
|
||||
mShowIcons = true;
|
||||
|
||||
mFavicon = (FaviconView) findViewById(R.id.icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
Tabs.registerOnTabsChangedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
// Tabs' listener array is safe to modify during use: its
|
||||
// iteration pattern is based on snapshots.
|
||||
Tabs.unregisterOnTabsChangedListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the row in response to a tab change event.
|
||||
* <p>
|
||||
* This method is always invoked on the UI thread.
|
||||
*/
|
||||
@Override
|
||||
public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final String data) {
|
||||
// Carefully check if this tab event is relevant to this row.
|
||||
final String pageUrl = mPageUrl;
|
||||
if (pageUrl == null) {
|
||||
return;
|
||||
}
|
||||
if (tab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return early if the page URL doesn't match the current tab URL,
|
||||
// or the old tab URL.
|
||||
// data is an empty String for ADDED/CLOSED, and contains the previous/old URL during
|
||||
// LOCATION_CHANGE (the new URL is retrieved using tab.getURL()).
|
||||
// tabURL and data may be about:reader URLs if the current or old tab page was a reader view
|
||||
// page, however pageUrl will always be a plain URL (i.e. we only add about:reader when opening
|
||||
// a reader view bookmark, at all other times it's a normal bookmark with normal URL).
|
||||
final String tabUrl = tab.getURL();
|
||||
if (!pageUrl.equals(ReaderModeUtils.stripAboutReaderUrl(tabUrl)) &&
|
||||
!pageUrl.equals(ReaderModeUtils.stripAboutReaderUrl(data))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: we *might* need to update the display status (i.e. switch-to-tab icon/label) if
|
||||
// a matching tab has been opened/closed/switched to a different page. updateDisplayedUrl() will
|
||||
// determine the changes (if any) that actually need to be made. A tab change with a matching URL
|
||||
// does not imply that any changes are needed - e.g. if a given URL is already open in one tab, and
|
||||
// is also opened in a second tab, the switch-to-tab status doesn't change, closing 1 of 2 tabs with a URL
|
||||
// similarly doesn't change the switch-to-tab display, etc. (However closing the last tab for
|
||||
// a given URL does require a status change, as does opening the first tab with that URL.)
|
||||
switch (msg) {
|
||||
case ADDED:
|
||||
case CLOSED:
|
||||
case LOCATION_CHANGE:
|
||||
updateDisplayedUrl();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void setTitle(String text) {
|
||||
mTitle.setText(text);
|
||||
}
|
||||
|
||||
protected void setUrl(String text) {
|
||||
mUrl.setText(text);
|
||||
}
|
||||
|
||||
protected void setUrl(int stringId) {
|
||||
mUrl.setText(stringId);
|
||||
}
|
||||
|
||||
protected String getUrl() {
|
||||
return mPageUrl;
|
||||
}
|
||||
|
||||
protected void setSwitchToTabIcon(int iconId) {
|
||||
if (mSwitchToTabIconId == iconId) {
|
||||
return;
|
||||
}
|
||||
|
||||
mSwitchToTabIconId = iconId;
|
||||
mUrl.setCompoundDrawablesWithIntrinsicBounds(mSwitchToTabIconId, 0, 0, 0);
|
||||
}
|
||||
|
||||
private void updateStatusIcon(boolean isBookmark, boolean isReaderItem) {
|
||||
if (isReaderItem) {
|
||||
mStatusIcon.setImageResource(R.drawable.status_icon_readercache);
|
||||
} else if (isBookmark) {
|
||||
mStatusIcon.setImageResource(R.drawable.star_blue);
|
||||
}
|
||||
|
||||
if (mShowIcons && (isBookmark || isReaderItem)) {
|
||||
mStatusIcon.setVisibility(View.VISIBLE);
|
||||
} else if (mShowIcons) {
|
||||
// We use INVISIBLE to have consistent padding for our items. This means text/URLs
|
||||
// fade consistently in the same location, regardless of them being bookmarked.
|
||||
mStatusIcon.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
mStatusIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, boolean hasReaderCacheItem) {
|
||||
mPageUrl = url;
|
||||
mHasReaderCacheItem = hasReaderCacheItem;
|
||||
updateDisplayedUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the page URL with "Switch to tab" if there is already a tab open with that URL.
|
||||
* Only looks for tabs that are either private or non-private, depending on the current
|
||||
* selected tab.
|
||||
*/
|
||||
protected void updateDisplayedUrl() {
|
||||
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
|
||||
final boolean isPrivate = (selectedTab != null) && (selectedTab.isPrivate());
|
||||
|
||||
// We always want to display the underlying page url, however for readermode pages
|
||||
// we navigate to the about:reader equivalent, hence we need to use that url when finding
|
||||
// existing tabs
|
||||
final String navigationUrl = mHasReaderCacheItem ? ReaderModeUtils.getAboutReaderForUrl(mPageUrl) : mPageUrl;
|
||||
Tab tab = Tabs.getInstance().getFirstTabForUrl(navigationUrl, isPrivate);
|
||||
|
||||
|
||||
if (!mShowIcons || tab == null) {
|
||||
setUrl(mPageUrl);
|
||||
setSwitchToTabIcon(NO_ICON);
|
||||
} else {
|
||||
setUrl(R.string.switch_to_tab);
|
||||
setSwitchToTabIcon(R.drawable.ic_url_bar_tab);
|
||||
}
|
||||
}
|
||||
|
||||
public void setShowIcons(boolean showIcons) {
|
||||
mShowIcons = showIcons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the data displayed by this row.
|
||||
* <p>
|
||||
* This method must be invoked on the UI thread.
|
||||
*
|
||||
* @param title to display.
|
||||
* @param url to display.
|
||||
*/
|
||||
public void update(String title, String url) {
|
||||
update(title, url, 0, false);
|
||||
}
|
||||
|
||||
protected void update(String title, String url, long bookmarkId, boolean hasReaderCacheItem) {
|
||||
if (mShowIcons) {
|
||||
// The bookmark id will be 0 (null in database) when the url
|
||||
// is not a bookmark and negative for 'fake' bookmarks.
|
||||
final boolean isBookmark = bookmarkId > 0;
|
||||
|
||||
updateStatusIcon(isBookmark, hasReaderCacheItem);
|
||||
} else {
|
||||
updateStatusIcon(false, false);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// No point updating the below things if URL has not changed. Prevents evil Favicon flicker.
|
||||
if (url.equals(mPageUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Blank the Favicon, so we don't show the wrong Favicon if we scroll and miss DB.
|
||||
mFavicon.clearImage();
|
||||
|
||||
if (mOngoingIconLoad != null) {
|
||||
mOngoingIconLoad.cancel(true);
|
||||
}
|
||||
|
||||
// Displayed RecentTabsPanel URLs may refer to pages opened in reader mode, so we
|
||||
// remove the about:reader prefix to ensure the Favicon loads properly.
|
||||
final String pageURL = ReaderModeUtils.stripAboutReaderUrl(url);
|
||||
|
||||
if (TextUtils.isEmpty(pageURL)) {
|
||||
// If url is empty, display the item as-is but do not load an icon if we do not have a page URL (bug 1310622)
|
||||
} else if (bookmarkId < BrowserContract.Bookmarks.FAKE_PARTNER_BOOKMARKS_START) {
|
||||
mOngoingIconLoad = Icons.with(getContext())
|
||||
.pageUrl(pageURL)
|
||||
.skipNetwork()
|
||||
.privileged(true)
|
||||
.icon(IconDescriptor.createGenericIcon(
|
||||
PartnerBookmarksProviderProxy.getUriForIcon(getContext(), bookmarkId).toString()))
|
||||
.build()
|
||||
.execute(mFavicon.createIconCallback());
|
||||
} else {
|
||||
mOngoingIconLoad = Icons.with(getContext())
|
||||
.pageUrl(pageURL)
|
||||
.skipNetwork()
|
||||
.build()
|
||||
.execute(mFavicon.createIconCallback());
|
||||
|
||||
}
|
||||
|
||||
updateDisplayedUrl(url, hasReaderCacheItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the data displayed by this row.
|
||||
* <p>
|
||||
* This method must be invoked on the UI thread.
|
||||
*
|
||||
* @param cursor to extract data from.
|
||||
*/
|
||||
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);
|
||||
|
||||
final long bookmarkId;
|
||||
final int bookmarkIdIndex = cursor.getColumnIndex(Combined.BOOKMARK_ID);
|
||||
if (bookmarkIdIndex != -1) {
|
||||
bookmarkId = cursor.getLong(bookmarkIdIndex);
|
||||
} else {
|
||||
bookmarkId = 0;
|
||||
}
|
||||
|
||||
SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(getContext());
|
||||
final boolean hasReaderCacheItem = rch.isURLCached(url);
|
||||
|
||||
update(title, url, bookmarkId, hasReaderCacheItem);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/* -*- 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.activitystream;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.graphics.ColorUtils;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter;
|
||||
import org.mozilla.gecko.util.ContextUtils;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
|
||||
|
||||
public class ActivityStream extends FrameLayout {
|
||||
private final StreamRecyclerAdapter adapter;
|
||||
|
||||
private static final int LOADER_ID_HIGHLIGHTS = 0;
|
||||
private static final int LOADER_ID_TOPSITES = 1;
|
||||
|
||||
private static final int MINIMUM_TILES = 4;
|
||||
private static final int MAXIMUM_TILES = 6;
|
||||
|
||||
private int desiredTileWidth;
|
||||
private int desiredTilesHeight;
|
||||
private int tileMargin;
|
||||
|
||||
public ActivityStream(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setBackgroundColor(ContextCompat.getColor(context, R.color.about_page_header_grey));
|
||||
|
||||
inflate(context, R.layout.as_content, this);
|
||||
|
||||
adapter = new StreamRecyclerAdapter();
|
||||
|
||||
RecyclerView rv = (RecyclerView) findViewById(R.id.activity_stream_main_recyclerview);
|
||||
|
||||
rv.setAdapter(adapter);
|
||||
rv.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
rv.setHasFixedSize(true);
|
||||
|
||||
RecyclerViewClickSupport.addTo(rv)
|
||||
.setOnItemClickListener(adapter);
|
||||
|
||||
final Resources resources = getResources();
|
||||
desiredTileWidth = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_width);
|
||||
desiredTilesHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_height);
|
||||
tileMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin);
|
||||
}
|
||||
|
||||
void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
adapter.setOnUrlOpenListeners(onUrlOpenListener, onUrlOpenInBackgroundListener);
|
||||
}
|
||||
|
||||
public void load(LoaderManager lm) {
|
||||
CursorLoaderCallbacks callbacks = new CursorLoaderCallbacks();
|
||||
|
||||
lm.initLoader(LOADER_ID_HIGHLIGHTS, null, callbacks);
|
||||
lm.initLoader(LOADER_ID_TOPSITES, null, callbacks);
|
||||
}
|
||||
|
||||
public void unload() {
|
||||
adapter.swapHighlightsCursor(null);
|
||||
adapter.swapTopSitesCursor(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
int tiles = (w - tileMargin) / (desiredTileWidth + tileMargin);
|
||||
|
||||
if (tiles < MINIMUM_TILES) {
|
||||
tiles = MINIMUM_TILES;
|
||||
|
||||
setPadding(0, 0, 0, 0);
|
||||
} else if (tiles > MAXIMUM_TILES) {
|
||||
tiles = MAXIMUM_TILES;
|
||||
|
||||
// Use the remaining space as padding
|
||||
int needed = tiles * (desiredTileWidth + tileMargin) + tileMargin;
|
||||
int padding = (w - needed) / 2;
|
||||
w = needed;
|
||||
|
||||
setPadding(padding, 0, padding, 0);
|
||||
} else {
|
||||
setPadding(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
final float ratio = (float) desiredTilesHeight / (float) desiredTileWidth;
|
||||
final int tilesWidth = (w - (tiles * tileMargin) - tileMargin) / tiles;
|
||||
final int tilesHeight = (int) (ratio * tilesWidth);
|
||||
|
||||
adapter.setTileSize(tiles, tilesWidth, tilesHeight);
|
||||
}
|
||||
|
||||
private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
final Context context = getContext();
|
||||
if (id == LOADER_ID_HIGHLIGHTS) {
|
||||
return BrowserDB.from(context).getHighlights(context, 10);
|
||||
} else if (id == LOADER_ID_TOPSITES) {
|
||||
return BrowserDB.from(context).getActivityStreamTopSites(
|
||||
context, TopSitesPagerAdapter.PAGES * MAXIMUM_TILES);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can't handle loader id " + id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
if (loader.getId() == LOADER_ID_HIGHLIGHTS) {
|
||||
adapter.swapHighlightsCursor(data);
|
||||
} else if (loader.getId() == LOADER_ID_TOPSITES) {
|
||||
adapter.swapTopSitesCursor(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
if (loader.getId() == LOADER_ID_HIGHLIGHTS) {
|
||||
adapter.swapHighlightsCursor(null);
|
||||
} else if (loader.getId() == LOADER_ID_TOPSITES) {
|
||||
adapter.swapTopSitesCursor(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/* -*- 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.activitystream;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.home.HomeFragment;
|
||||
|
||||
/**
|
||||
* Simple wrapper around the ActivityStream view that allows embedding as a HomePager panel.
|
||||
*/
|
||||
public class ActivityStreamHomeFragment
|
||||
extends HomeFragment {
|
||||
private ActivityStream activityStream;
|
||||
|
||||
@Override
|
||||
protected void load() {
|
||||
activityStream.load(getLoaderManager());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
if (activityStream == null) {
|
||||
activityStream = (ActivityStream) inflater.inflate(R.layout.activity_stream, container, false);
|
||||
activityStream.setOnUrlOpenListeners(mUrlOpenListener, mUrlOpenInBackgroundListener);
|
||||
}
|
||||
|
||||
return activityStream;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/* -*- 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.activitystream;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.mozilla.gecko.animation.PropertyAnimator;
|
||||
import org.mozilla.gecko.home.HomeBanner;
|
||||
import org.mozilla.gecko.home.HomeFragment;
|
||||
import org.mozilla.gecko.home.HomeScreen;
|
||||
|
||||
/**
|
||||
* HomeScreen implementation that displays ActivityStream.
|
||||
*/
|
||||
public class ActivityStreamHomeScreen
|
||||
extends ActivityStream
|
||||
implements HomeScreen {
|
||||
|
||||
private boolean visible = false;
|
||||
|
||||
public ActivityStreamHomeScreen(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarFocusChange(boolean hasFocus) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showPanel(String panelId, Bundle restoreData) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnPanelChangeListener(OnPanelChangeListener listener) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBanner(HomeBanner banner) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData,
|
||||
PropertyAnimator animator) {
|
||||
super.load(lm);
|
||||
visible = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() {
|
||||
super.unload();
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
/* -*- 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.activitystream;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.activitystream.ActivityStream.LabelCallback;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
|
||||
import org.mozilla.gecko.home.activitystream.topsites.CirclePageIndicator;
|
||||
import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter;
|
||||
import org.mozilla.gecko.icons.IconCallback;
|
||||
import org.mozilla.gecko.icons.IconResponse;
|
||||
import org.mozilla.gecko.icons.Icons;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
import org.mozilla.gecko.util.ViewUtil;
|
||||
import org.mozilla.gecko.util.TouchTargetUtil;
|
||||
import org.mozilla.gecko.widget.FaviconView;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel;
|
||||
|
||||
public abstract class StreamItem extends RecyclerView.ViewHolder {
|
||||
public StreamItem(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public static class HighlightsTitle extends StreamItem {
|
||||
public static final int LAYOUT_ID = R.layout.activity_stream_main_highlightstitle;
|
||||
|
||||
public HighlightsTitle(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
public static class TopPanel extends StreamItem {
|
||||
public static final int LAYOUT_ID = R.layout.activity_stream_main_toppanel;
|
||||
|
||||
private final ViewPager topSitesPager;
|
||||
|
||||
public TopPanel(View itemView, HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
super(itemView);
|
||||
|
||||
topSitesPager = (ViewPager) itemView.findViewById(R.id.topsites_pager);
|
||||
topSitesPager.setAdapter(new TopSitesPagerAdapter(itemView.getContext(), onUrlOpenListener, onUrlOpenInBackgroundListener));
|
||||
|
||||
CirclePageIndicator indicator = (CirclePageIndicator) itemView.findViewById(R.id.topsites_indicator);
|
||||
indicator.setViewPager(topSitesPager);
|
||||
}
|
||||
|
||||
public void bind(Cursor cursor, int tiles, int tilesWidth, int tilesHeight) {
|
||||
final TopSitesPagerAdapter adapter = (TopSitesPagerAdapter) topSitesPager.getAdapter();
|
||||
adapter.setTilesSize(tiles, tilesWidth, tilesHeight);
|
||||
adapter.swapCursor(cursor);
|
||||
|
||||
final Resources resources = itemView.getResources();
|
||||
final int tilesMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin);
|
||||
final int textHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_top_sites_text_height);
|
||||
|
||||
ViewGroup.LayoutParams layoutParams = topSitesPager.getLayoutParams();
|
||||
layoutParams.height = tilesHeight + tilesMargin + textHeight;
|
||||
topSitesPager.setLayoutParams(layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
public static class HighlightItem extends StreamItem implements IconCallback {
|
||||
public static final int LAYOUT_ID = R.layout.activity_stream_card_history_item;
|
||||
|
||||
String title;
|
||||
String url;
|
||||
|
||||
final FaviconView vIconView;
|
||||
final TextView vLabel;
|
||||
final TextView vTimeSince;
|
||||
final TextView vSourceView;
|
||||
final TextView vPageView;
|
||||
final ImageView vSourceIconView;
|
||||
|
||||
private Future<IconResponse> ongoingIconLoad;
|
||||
private int tilesMargin;
|
||||
|
||||
public HighlightItem(final View itemView,
|
||||
final HomePager.OnUrlOpenListener onUrlOpenListener,
|
||||
final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
super(itemView);
|
||||
|
||||
tilesMargin = itemView.getResources().getDimensionPixelSize(R.dimen.activity_stream_base_margin);
|
||||
|
||||
vLabel = (TextView) itemView.findViewById(R.id.card_history_label);
|
||||
vTimeSince = (TextView) itemView.findViewById(R.id.card_history_time_since);
|
||||
vIconView = (FaviconView) itemView.findViewById(R.id.icon);
|
||||
vSourceView = (TextView) itemView.findViewById(R.id.card_history_source);
|
||||
vPageView = (TextView) itemView.findViewById(R.id.page);
|
||||
vSourceIconView = (ImageView) itemView.findViewById(R.id.source_icon);
|
||||
|
||||
final ImageView menuButton = (ImageView) itemView.findViewById(R.id.menu);
|
||||
|
||||
menuButton.setImageDrawable(
|
||||
DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, Color.LTGRAY));
|
||||
|
||||
TouchTargetUtil.ensureTargetHitArea(menuButton, itemView);
|
||||
|
||||
menuButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ActivityStreamContextMenu.show(v.getContext(),
|
||||
menuButton,
|
||||
ActivityStreamContextMenu.MenuMode.HIGHLIGHT,
|
||||
title, url, onUrlOpenListener, onUrlOpenInBackgroundListener,
|
||||
vIconView.getWidth(), vIconView.getHeight());
|
||||
}
|
||||
});
|
||||
|
||||
ViewUtil.enableTouchRipple(menuButton);
|
||||
}
|
||||
|
||||
public void bind(Cursor cursor, int tilesWidth, int tilesHeight) {
|
||||
|
||||
final long time = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Highlights.DATE));
|
||||
final String ago = DateUtils.getRelativeTimeSpanString(time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0).toString();
|
||||
|
||||
title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.History.TITLE));
|
||||
url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
|
||||
|
||||
vLabel.setText(title);
|
||||
vTimeSince.setText(ago);
|
||||
|
||||
ViewGroup.LayoutParams layoutParams = vIconView.getLayoutParams();
|
||||
layoutParams.width = tilesWidth - tilesMargin;
|
||||
layoutParams.height = tilesHeight;
|
||||
vIconView.setLayoutParams(layoutParams);
|
||||
|
||||
updateSource(cursor);
|
||||
updatePage(url);
|
||||
|
||||
if (ongoingIconLoad != null) {
|
||||
ongoingIconLoad.cancel(true);
|
||||
}
|
||||
|
||||
ongoingIconLoad = Icons.with(itemView.getContext())
|
||||
.pageUrl(url)
|
||||
.skipNetwork()
|
||||
.build()
|
||||
.execute(this);
|
||||
}
|
||||
|
||||
private void updateSource(final Cursor cursor) {
|
||||
final boolean isBookmark = -1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID));
|
||||
final boolean isHistory = -1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
|
||||
|
||||
if (isBookmark) {
|
||||
vSourceView.setText(R.string.activity_stream_highlight_label_bookmarked);
|
||||
vSourceView.setVisibility(View.VISIBLE);
|
||||
vSourceIconView.setImageResource(R.drawable.ic_as_bookmarked);
|
||||
} else if (isHistory) {
|
||||
vSourceView.setText(R.string.activity_stream_highlight_label_visited);
|
||||
vSourceView.setVisibility(View.VISIBLE);
|
||||
vSourceIconView.setImageResource(R.drawable.ic_as_visited);
|
||||
} else {
|
||||
vSourceView.setVisibility(View.INVISIBLE);
|
||||
vSourceIconView.setImageResource(0);
|
||||
}
|
||||
|
||||
vSourceView.setText(vSourceView.getText());
|
||||
}
|
||||
|
||||
private void updatePage(final String url) {
|
||||
extractLabel(itemView.getContext(), url, false, new LabelCallback() {
|
||||
@Override
|
||||
public void onLabelExtracted(String label) {
|
||||
vPageView.setText(TextUtils.isEmpty(label) ? url : label);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIconResponse(IconResponse response) {
|
||||
vIconView.updateImage(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/* -*- 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.activitystream;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
import org.mozilla.gecko.home.activitystream.StreamItem.HighlightItem;
|
||||
import org.mozilla.gecko.home.activitystream.StreamItem.TopPanel;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class StreamRecyclerAdapter extends RecyclerView.Adapter<StreamItem> implements RecyclerViewClickSupport.OnItemClickListener {
|
||||
private Cursor highlightsCursor;
|
||||
private Cursor topSitesCursor;
|
||||
|
||||
private HomePager.OnUrlOpenListener onUrlOpenListener;
|
||||
private HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
|
||||
|
||||
private int tiles;
|
||||
private int tilesWidth;
|
||||
private int tilesHeight;
|
||||
|
||||
void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
this.onUrlOpenListener = onUrlOpenListener;
|
||||
this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
|
||||
}
|
||||
|
||||
public void setTileSize(int tiles, int tilesWidth, int tilesHeight) {
|
||||
this.tilesWidth = tilesWidth;
|
||||
this.tilesHeight = tilesHeight;
|
||||
this.tiles = tiles;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == 0) {
|
||||
return TopPanel.LAYOUT_ID;
|
||||
} else if (position == 1) {
|
||||
return StreamItem.HighlightsTitle.LAYOUT_ID;
|
||||
} else {
|
||||
return HighlightItem.LAYOUT_ID;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamItem onCreateViewHolder(ViewGroup parent, final int type) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
|
||||
if (type == TopPanel.LAYOUT_ID) {
|
||||
return new TopPanel(inflater.inflate(type, parent, false), onUrlOpenListener, onUrlOpenInBackgroundListener);
|
||||
} else if (type == StreamItem.HighlightsTitle.LAYOUT_ID) {
|
||||
return new StreamItem.HighlightsTitle(inflater.inflate(type, parent, false));
|
||||
} else if (type == HighlightItem.LAYOUT_ID) {
|
||||
return new HighlightItem(inflater.inflate(type, parent, false), onUrlOpenListener, onUrlOpenInBackgroundListener);
|
||||
} else {
|
||||
throw new IllegalStateException("Missing inflation for ViewType " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private int translatePositionToCursor(int position) {
|
||||
if (position == 0) {
|
||||
throw new IllegalArgumentException("Requested cursor position for invalid item");
|
||||
}
|
||||
|
||||
// We have two blank panels at the top, hence remove that to obtain the cursor position
|
||||
return position - 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(StreamItem holder, int position) {
|
||||
int type = getItemViewType(position);
|
||||
|
||||
if (type == HighlightItem.LAYOUT_ID) {
|
||||
final int cursorPosition = translatePositionToCursor(position);
|
||||
|
||||
highlightsCursor.moveToPosition(cursorPosition);
|
||||
((HighlightItem) holder).bind(highlightsCursor, tilesWidth, tilesHeight);
|
||||
} else if (type == TopPanel.LAYOUT_ID) {
|
||||
((TopPanel) holder).bind(topSitesCursor, tiles, tilesWidth, tilesHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||
if (position < 1) {
|
||||
// The header contains top sites and has its own click handling.
|
||||
return;
|
||||
}
|
||||
|
||||
highlightsCursor.moveToPosition(
|
||||
translatePositionToCursor(position));
|
||||
|
||||
final String url = highlightsCursor.getString(
|
||||
highlightsCursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
|
||||
|
||||
onUrlOpenListener.onUrlOpen(url, EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
final int highlightsCount;
|
||||
|
||||
if (highlightsCursor != null) {
|
||||
highlightsCount = highlightsCursor.getCount();
|
||||
} else {
|
||||
highlightsCount = 0;
|
||||
}
|
||||
|
||||
return highlightsCount + 2;
|
||||
}
|
||||
|
||||
public void swapHighlightsCursor(Cursor cursor) {
|
||||
highlightsCursor = cursor;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void swapTopSitesCursor(Cursor cursor) {
|
||||
this.topSitesCursor = cursor;
|
||||
|
||||
notifyItemChanged(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
/* -*- 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.activitystream.menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.IntentHelper;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.annotation.RobocopTarget;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
import org.mozilla.gecko.util.Clipboard;
|
||||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
import org.mozilla.gecko.util.UIAsyncTask;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
@RobocopTarget
|
||||
public abstract class ActivityStreamContextMenu
|
||||
implements NavigationView.OnNavigationItemSelectedListener {
|
||||
|
||||
public enum MenuMode {
|
||||
HIGHLIGHT,
|
||||
TOPSITE
|
||||
}
|
||||
|
||||
final Context context;
|
||||
|
||||
final String title;
|
||||
final String url;
|
||||
|
||||
final HomePager.OnUrlOpenListener onUrlOpenListener;
|
||||
final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
|
||||
|
||||
boolean isAlreadyBookmarked; // default false;
|
||||
|
||||
public abstract MenuItem getItemByID(int id);
|
||||
|
||||
public abstract void show();
|
||||
|
||||
public abstract void dismiss();
|
||||
|
||||
final MenuMode mode;
|
||||
|
||||
/* package-private */ ActivityStreamContextMenu(final Context context,
|
||||
final MenuMode mode,
|
||||
final String title, @NonNull final String url,
|
||||
HomePager.OnUrlOpenListener onUrlOpenListener,
|
||||
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
this.context = context;
|
||||
|
||||
this.mode = mode;
|
||||
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
this.onUrlOpenListener = onUrlOpenListener;
|
||||
this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called before the menu is shown.
|
||||
* <p/>
|
||||
* Your implementation must be ready to return items from getItemByID() before postInit() is
|
||||
* called, i.e. you should probably inflate your menu items before this call.
|
||||
*/
|
||||
protected void postInit() {
|
||||
// Disable "dismiss" for topsites until we have decided on its behaviour for topsites
|
||||
// (currently "dismiss" adds the URL to a highlights-specific blocklist, which the topsites
|
||||
// query has no knowledge of).
|
||||
if (mode == MenuMode.TOPSITE) {
|
||||
final MenuItem dismissItem = getItemByID(R.id.dismiss);
|
||||
dismissItem.setVisible(false);
|
||||
}
|
||||
|
||||
// Disable the bookmark item until we know its bookmark state
|
||||
final MenuItem bookmarkItem = getItemByID(R.id.bookmark);
|
||||
bookmarkItem.setEnabled(false);
|
||||
|
||||
(new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
isAlreadyBookmarked = BrowserDB.from(context).isBookmark(context.getContentResolver(), url);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
if (isAlreadyBookmarked) {
|
||||
bookmarkItem.setTitle(R.string.bookmark_remove);
|
||||
}
|
||||
|
||||
bookmarkItem.setEnabled(true);
|
||||
}
|
||||
}).execute();
|
||||
|
||||
// Only show the "remove from history" item if a page actually has history
|
||||
final MenuItem deleteHistoryItem = getItemByID(R.id.delete);
|
||||
deleteHistoryItem.setVisible(false);
|
||||
|
||||
(new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
|
||||
boolean hasHistory;
|
||||
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
final Cursor cursor = BrowserDB.from(context).getHistoryForURL(context.getContentResolver(), url);
|
||||
try {
|
||||
if (cursor != null &&
|
||||
cursor.getCount() == 1) {
|
||||
hasHistory = true;
|
||||
} else {
|
||||
hasHistory = false;
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
if (hasHistory) {
|
||||
deleteHistoryItem.setVisible(true);
|
||||
}
|
||||
}
|
||||
}).execute();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.share:
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu");
|
||||
IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title, false);
|
||||
break;
|
||||
|
||||
case R.id.bookmark:
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final BrowserDB db = BrowserDB.from(context);
|
||||
|
||||
if (isAlreadyBookmarked) {
|
||||
db.removeBookmarksWithURL(context.getContentResolver(), url);
|
||||
} else {
|
||||
db.addBookmark(context.getContentResolver(), title, url);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case R.id.copy_url:
|
||||
Clipboard.setText(url);
|
||||
break;
|
||||
|
||||
case R.id.add_homescreen:
|
||||
GeckoAppShell.createShortcut(title, url);
|
||||
break;
|
||||
|
||||
case R.id.open_new_tab:
|
||||
onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class));
|
||||
break;
|
||||
|
||||
case R.id.open_new_private_tab:
|
||||
onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.of(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE));
|
||||
break;
|
||||
|
||||
case R.id.dismiss:
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BrowserDB.from(context)
|
||||
.blockActivityStreamSite(context.getContentResolver(),
|
||||
url);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case R.id.delete:
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BrowserDB.from(context)
|
||||
.removeHistoryEntry(context.getContentResolver(),
|
||||
url);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Menu item with ID=" + item.getItemId() + " not handled");
|
||||
}
|
||||
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@RobocopTarget
|
||||
public static ActivityStreamContextMenu show(Context context,
|
||||
View anchor,
|
||||
final MenuMode menuMode,
|
||||
final String title, @NonNull final String url,
|
||||
HomePager.OnUrlOpenListener onUrlOpenListener,
|
||||
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
|
||||
final int tilesWidth, final int tilesHeight) {
|
||||
final ActivityStreamContextMenu menu;
|
||||
|
||||
if (!HardwareUtils.isTablet()) {
|
||||
menu = new BottomSheetContextMenu(context,
|
||||
menuMode,
|
||||
title, url,
|
||||
onUrlOpenListener, onUrlOpenInBackgroundListener,
|
||||
tilesWidth, tilesHeight);
|
||||
} else {
|
||||
menu = new PopupContextMenu(context,
|
||||
anchor,
|
||||
menuMode,
|
||||
title, url,
|
||||
onUrlOpenListener, onUrlOpenInBackgroundListener);
|
||||
}
|
||||
|
||||
menu.show();
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.activitystream.menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.BottomSheetBehavior;
|
||||
import android.support.design.widget.BottomSheetDialog;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.activitystream.ActivityStream;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
import org.mozilla.gecko.icons.IconCallback;
|
||||
import org.mozilla.gecko.icons.IconResponse;
|
||||
import org.mozilla.gecko.icons.Icons;
|
||||
import org.mozilla.gecko.widget.FaviconView;
|
||||
|
||||
import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel;
|
||||
|
||||
/* package-private */ class BottomSheetContextMenu
|
||||
extends ActivityStreamContextMenu {
|
||||
|
||||
|
||||
private final BottomSheetDialog bottomSheetDialog;
|
||||
|
||||
private final NavigationView navigationView;
|
||||
|
||||
public BottomSheetContextMenu(final Context context,
|
||||
final MenuMode mode,
|
||||
final String title, @NonNull final String url,
|
||||
HomePager.OnUrlOpenListener onUrlOpenListener,
|
||||
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
|
||||
final int tilesWidth, final int tilesHeight) {
|
||||
|
||||
super(context,
|
||||
mode,
|
||||
title,
|
||||
url,
|
||||
onUrlOpenListener,
|
||||
onUrlOpenInBackgroundListener);
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
final View content = inflater.inflate(R.layout.activity_stream_contextmenu_bottomsheet, null);
|
||||
|
||||
bottomSheetDialog = new BottomSheetDialog(context);
|
||||
bottomSheetDialog.setContentView(content);
|
||||
|
||||
((TextView) content.findViewById(R.id.title)).setText(title);
|
||||
|
||||
extractLabel(context, url, false, new ActivityStream.LabelCallback() {
|
||||
public void onLabelExtracted(String label) {
|
||||
((TextView) content.findViewById(R.id.url)).setText(label);
|
||||
}
|
||||
});
|
||||
|
||||
// Copy layouted parameters from the Highlights / TopSites items to ensure consistency
|
||||
final FaviconView faviconView = (FaviconView) content.findViewById(R.id.icon);
|
||||
ViewGroup.LayoutParams layoutParams = faviconView.getLayoutParams();
|
||||
layoutParams.width = tilesWidth;
|
||||
layoutParams.height = tilesHeight;
|
||||
faviconView.setLayoutParams(layoutParams);
|
||||
|
||||
Icons.with(context)
|
||||
.pageUrl(url)
|
||||
.skipNetwork()
|
||||
.build()
|
||||
.execute(new IconCallback() {
|
||||
@Override
|
||||
public void onIconResponse(IconResponse response) {
|
||||
faviconView.updateImage(response);
|
||||
}
|
||||
});
|
||||
|
||||
navigationView = (NavigationView) content.findViewById(R.id.menu);
|
||||
navigationView.setNavigationItemSelectedListener(this);
|
||||
|
||||
super.postInit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MenuItem getItemByID(int id) {
|
||||
return navigationView.getMenu().findItem(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
bottomSheetDialog.show();
|
||||
}
|
||||
|
||||
public void dismiss() {
|
||||
bottomSheetDialog.dismiss();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/* -*- 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.activitystream.menu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
|
||||
/* package-private */ class PopupContextMenu
|
||||
extends ActivityStreamContextMenu {
|
||||
|
||||
private final PopupWindow popupWindow;
|
||||
private final NavigationView navigationView;
|
||||
|
||||
private final View anchor;
|
||||
|
||||
public PopupContextMenu(final Context context,
|
||||
View anchor,
|
||||
final MenuMode mode,
|
||||
final String title,
|
||||
@NonNull final String url,
|
||||
HomePager.OnUrlOpenListener onUrlOpenListener,
|
||||
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
super(context,
|
||||
mode,
|
||||
title,
|
||||
url,
|
||||
onUrlOpenListener,
|
||||
onUrlOpenInBackgroundListener);
|
||||
|
||||
this.anchor = anchor;
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
View card = inflater.inflate(R.layout.activity_stream_contextmenu_popupmenu, null);
|
||||
navigationView = (NavigationView) card.findViewById(R.id.menu);
|
||||
navigationView.setNavigationItemSelectedListener(this);
|
||||
|
||||
popupWindow = new PopupWindow(context);
|
||||
popupWindow.setContentView(card);
|
||||
popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||
popupWindow.setFocusable(true);
|
||||
|
||||
super.postInit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MenuItem getItemByID(int id) {
|
||||
return navigationView.getMenu().findItem(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
// By default popupWindow follows the pre-material convention of displaying the popup
|
||||
// below a View. We need to shift it over the view:
|
||||
popupWindow.showAsDropDown(anchor,
|
||||
0,
|
||||
-(anchor.getHeight() + anchor.getPaddingBottom()));
|
||||
}
|
||||
|
||||
public void dismiss() {
|
||||
popupWindow.dismiss();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,568 @@
|
|||
/*
|
||||
* Copyright (C) 2011 Patrik Akerfeldt
|
||||
* Copyright (C) 2011 Jake Wharton
|
||||
*
|
||||
* 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.activitystream.topsites;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.view.MotionEventCompat;
|
||||
import android.support.v4.view.ViewConfigurationCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
|
||||
import static android.graphics.Paint.ANTI_ALIAS_FLAG;
|
||||
import static android.widget.LinearLayout.HORIZONTAL;
|
||||
import static android.widget.LinearLayout.VERTICAL;
|
||||
|
||||
/**
|
||||
* Draws circles (one for each view). The current view position is filled and
|
||||
* others are only stroked.
|
||||
*
|
||||
* This file was imported from Jake Wharton's ViewPagerIndicator library:
|
||||
* https://github.com/JakeWharton/ViewPagerIndicator
|
||||
* It was modified to not extend the PageIndicator interface (as we only use one single Indicator)
|
||||
* implementation, and has had some minor appearance related modifications added alter.
|
||||
*/
|
||||
public class CirclePageIndicator
|
||||
extends View
|
||||
implements ViewPager.OnPageChangeListener {
|
||||
|
||||
/**
|
||||
* Separation between circles, as a factor of the circle radius. By default CirclePageIndicator
|
||||
* shipped with a separation factor of 3, however we want to be able to tweak this for
|
||||
* ActivityStream.
|
||||
*
|
||||
* If/when we reuse this indicator elsewhere, this should probably become a configurable property.
|
||||
*/
|
||||
private static final int SEPARATION_FACTOR = 7;
|
||||
|
||||
private static final int INVALID_POINTER = -1;
|
||||
|
||||
private float mRadius;
|
||||
private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG);
|
||||
private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG);
|
||||
private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG);
|
||||
private ViewPager mViewPager;
|
||||
private ViewPager.OnPageChangeListener mListener;
|
||||
private int mCurrentPage;
|
||||
private int mSnapPage;
|
||||
private float mPageOffset;
|
||||
private int mScrollState;
|
||||
private int mOrientation;
|
||||
private boolean mCentered;
|
||||
private boolean mSnap;
|
||||
|
||||
private int mTouchSlop;
|
||||
private float mLastMotionX = -1;
|
||||
private int mActivePointerId = INVALID_POINTER;
|
||||
private boolean mIsDragging;
|
||||
|
||||
|
||||
public CirclePageIndicator(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CirclePageIndicator(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.vpiCirclePageIndicatorStyle);
|
||||
}
|
||||
|
||||
public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
if (isInEditMode()) return;
|
||||
|
||||
//Load defaults from resources
|
||||
final Resources res = getResources();
|
||||
final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color);
|
||||
final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color);
|
||||
final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation);
|
||||
final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color);
|
||||
final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width);
|
||||
final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius);
|
||||
final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered);
|
||||
final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap);
|
||||
|
||||
//Retrieve styles attributes
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0);
|
||||
|
||||
mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered);
|
||||
mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation);
|
||||
mPaintPageFill.setStyle(Style.FILL);
|
||||
mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor));
|
||||
mPaintStroke.setStyle(Style.STROKE);
|
||||
mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor));
|
||||
mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth));
|
||||
mPaintFill.setStyle(Style.FILL);
|
||||
mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor));
|
||||
mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius);
|
||||
mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap);
|
||||
|
||||
Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background);
|
||||
if (background != null) {
|
||||
setBackgroundDrawable(background);
|
||||
}
|
||||
|
||||
a.recycle();
|
||||
|
||||
final ViewConfiguration configuration = ViewConfiguration.get(context);
|
||||
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
|
||||
}
|
||||
|
||||
|
||||
public void setCentered(boolean centered) {
|
||||
mCentered = centered;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public boolean isCentered() {
|
||||
return mCentered;
|
||||
}
|
||||
|
||||
public void setPageColor(int pageColor) {
|
||||
mPaintPageFill.setColor(pageColor);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getPageColor() {
|
||||
return mPaintPageFill.getColor();
|
||||
}
|
||||
|
||||
public void setFillColor(int fillColor) {
|
||||
mPaintFill.setColor(fillColor);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getFillColor() {
|
||||
return mPaintFill.getColor();
|
||||
}
|
||||
|
||||
public void setOrientation(int orientation) {
|
||||
switch (orientation) {
|
||||
case HORIZONTAL:
|
||||
case VERTICAL:
|
||||
mOrientation = orientation;
|
||||
requestLayout();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL.");
|
||||
}
|
||||
}
|
||||
|
||||
public int getOrientation() {
|
||||
return mOrientation;
|
||||
}
|
||||
|
||||
public void setStrokeColor(int strokeColor) {
|
||||
mPaintStroke.setColor(strokeColor);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getStrokeColor() {
|
||||
return mPaintStroke.getColor();
|
||||
}
|
||||
|
||||
public void setStrokeWidth(float strokeWidth) {
|
||||
mPaintStroke.setStrokeWidth(strokeWidth);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public float getStrokeWidth() {
|
||||
return mPaintStroke.getStrokeWidth();
|
||||
}
|
||||
|
||||
public void setRadius(float radius) {
|
||||
mRadius = radius;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public float getRadius() {
|
||||
return mRadius;
|
||||
}
|
||||
|
||||
public void setSnap(boolean snap) {
|
||||
mSnap = snap;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public boolean isSnap() {
|
||||
return mSnap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (mViewPager == null) {
|
||||
return;
|
||||
}
|
||||
final int count = mViewPager.getAdapter().getCount();
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCurrentPage >= count) {
|
||||
setCurrentItem(count - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
int longSize;
|
||||
int longPaddingBefore;
|
||||
int longPaddingAfter;
|
||||
int shortPaddingBefore;
|
||||
if (mOrientation == HORIZONTAL) {
|
||||
longSize = getWidth();
|
||||
longPaddingBefore = getPaddingLeft();
|
||||
longPaddingAfter = getPaddingRight();
|
||||
shortPaddingBefore = getPaddingTop();
|
||||
} else {
|
||||
longSize = getHeight();
|
||||
longPaddingBefore = getPaddingTop();
|
||||
longPaddingAfter = getPaddingBottom();
|
||||
shortPaddingBefore = getPaddingLeft();
|
||||
}
|
||||
|
||||
final float threeRadius = mRadius * SEPARATION_FACTOR;
|
||||
final float shortOffset = shortPaddingBefore + mRadius;
|
||||
float longOffset = longPaddingBefore + mRadius;
|
||||
if (mCentered) {
|
||||
longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f);
|
||||
}
|
||||
|
||||
float dX;
|
||||
float dY;
|
||||
|
||||
float pageFillRadius = mRadius;
|
||||
if (mPaintStroke.getStrokeWidth() > 0) {
|
||||
pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f;
|
||||
}
|
||||
|
||||
//Draw stroked circles
|
||||
for (int iLoop = 0; iLoop < count; iLoop++) {
|
||||
float drawLong = longOffset + (iLoop * threeRadius);
|
||||
if (mOrientation == HORIZONTAL) {
|
||||
dX = drawLong;
|
||||
dY = shortOffset;
|
||||
} else {
|
||||
dX = shortOffset;
|
||||
dY = drawLong;
|
||||
}
|
||||
// Only paint fill if not completely transparent
|
||||
if (mPaintPageFill.getAlpha() > 0) {
|
||||
canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill);
|
||||
}
|
||||
|
||||
// Only paint stroke if a stroke width was non-zero
|
||||
if (pageFillRadius != mRadius) {
|
||||
canvas.drawCircle(dX, dY, mRadius, mPaintStroke);
|
||||
}
|
||||
}
|
||||
|
||||
//Draw the filled circle according to the current scroll
|
||||
float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius;
|
||||
if (!mSnap) {
|
||||
cx += mPageOffset * threeRadius;
|
||||
}
|
||||
if (mOrientation == HORIZONTAL) {
|
||||
dX = longOffset + cx;
|
||||
dY = shortOffset;
|
||||
} else {
|
||||
dX = shortOffset;
|
||||
dY = longOffset + cx;
|
||||
}
|
||||
canvas.drawCircle(dX, dY, mRadius, mPaintFill);
|
||||
}
|
||||
|
||||
public boolean onTouchEvent(android.view.MotionEvent ev) {
|
||||
if (super.onTouchEvent(ev)) {
|
||||
return true;
|
||||
}
|
||||
if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
|
||||
mLastMotionX = ev.getX();
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
|
||||
final float x = MotionEventCompat.getX(ev, activePointerIndex);
|
||||
final float deltaX = x - mLastMotionX;
|
||||
|
||||
if (!mIsDragging) {
|
||||
if (Math.abs(deltaX) > mTouchSlop) {
|
||||
mIsDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (mIsDragging) {
|
||||
mLastMotionX = x;
|
||||
if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) {
|
||||
mViewPager.fakeDragBy(deltaX);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (!mIsDragging) {
|
||||
final int count = mViewPager.getAdapter().getCount();
|
||||
final int width = getWidth();
|
||||
final float halfWidth = width / 2f;
|
||||
final float sixthWidth = width / 6f;
|
||||
|
||||
if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) {
|
||||
if (action != MotionEvent.ACTION_CANCEL) {
|
||||
mViewPager.setCurrentItem(mCurrentPage - 1);
|
||||
}
|
||||
return true;
|
||||
} else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) {
|
||||
if (action != MotionEvent.ACTION_CANCEL) {
|
||||
mViewPager.setCurrentItem(mCurrentPage + 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
mIsDragging = false;
|
||||
mActivePointerId = INVALID_POINTER;
|
||||
if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag();
|
||||
break;
|
||||
|
||||
case MotionEventCompat.ACTION_POINTER_DOWN: {
|
||||
final int index = MotionEventCompat.getActionIndex(ev);
|
||||
mLastMotionX = MotionEventCompat.getX(ev, index);
|
||||
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEventCompat.ACTION_POINTER_UP:
|
||||
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
|
||||
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
|
||||
if (pointerId == mActivePointerId) {
|
||||
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
|
||||
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
|
||||
}
|
||||
mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setViewPager(ViewPager view) {
|
||||
if (mViewPager == view) {
|
||||
return;
|
||||
}
|
||||
if (mViewPager != null) {
|
||||
mViewPager.setOnPageChangeListener(null);
|
||||
}
|
||||
if (view.getAdapter() == null) {
|
||||
throw new IllegalStateException("ViewPager does not have adapter instance.");
|
||||
}
|
||||
mViewPager = view;
|
||||
mViewPager.setOnPageChangeListener(this);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setViewPager(ViewPager view, int initialPosition) {
|
||||
setViewPager(view);
|
||||
setCurrentItem(initialPosition);
|
||||
}
|
||||
|
||||
public void setCurrentItem(int item) {
|
||||
if (mViewPager == null) {
|
||||
throw new IllegalStateException("ViewPager has not been bound.");
|
||||
}
|
||||
mViewPager.setCurrentItem(item);
|
||||
mCurrentPage = item;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void notifyDataSetChanged() {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
mScrollState = state;
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onPageScrollStateChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
mCurrentPage = position;
|
||||
mPageOffset = positionOffset;
|
||||
invalidate();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) {
|
||||
mCurrentPage = position;
|
||||
mSnapPage = position;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onPageSelected(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see android.view.View#onMeasure(int, int)
|
||||
*/
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
if (mOrientation == HORIZONTAL) {
|
||||
setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec));
|
||||
} else {
|
||||
setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the width of this view
|
||||
*
|
||||
* @param measureSpec
|
||||
* A measureSpec packed into an int
|
||||
* @return The width of the view, honoring constraints from measureSpec
|
||||
*/
|
||||
private int measureLong(int measureSpec) {
|
||||
int result;
|
||||
int specMode = MeasureSpec.getMode(measureSpec);
|
||||
int specSize = MeasureSpec.getSize(measureSpec);
|
||||
|
||||
if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {
|
||||
//We were told how big to be
|
||||
result = specSize;
|
||||
} else {
|
||||
//Calculate the width according the views count
|
||||
final int count = mViewPager.getAdapter().getCount();
|
||||
result = (int)(getPaddingLeft() + getPaddingRight()
|
||||
+ (count * 2 * mRadius) + (count - 1) * mRadius + 1);
|
||||
//Respect AT_MOST value if that was what is called for by measureSpec
|
||||
if (specMode == MeasureSpec.AT_MOST) {
|
||||
result = Math.min(result, specSize);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the height of this view
|
||||
*
|
||||
* @param measureSpec
|
||||
* A measureSpec packed into an int
|
||||
* @return The height of the view, honoring constraints from measureSpec
|
||||
*/
|
||||
private int measureShort(int measureSpec) {
|
||||
int result;
|
||||
int specMode = MeasureSpec.getMode(measureSpec);
|
||||
int specSize = MeasureSpec.getSize(measureSpec);
|
||||
|
||||
if (specMode == MeasureSpec.EXACTLY) {
|
||||
//We were told how big to be
|
||||
result = specSize;
|
||||
} else {
|
||||
//Measure the height
|
||||
result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);
|
||||
//Respect AT_MOST value if that was what is called for by measureSpec
|
||||
if (specMode == MeasureSpec.AT_MOST) {
|
||||
result = Math.min(result, specSize);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
SavedState savedState = (SavedState)state;
|
||||
super.onRestoreInstanceState(savedState.getSuperState());
|
||||
mCurrentPage = savedState.currentPage;
|
||||
mSnapPage = savedState.currentPage;
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
SavedState savedState = new SavedState(superState);
|
||||
savedState.currentPage = mCurrentPage;
|
||||
return savedState;
|
||||
}
|
||||
|
||||
static class SavedState extends BaseSavedState {
|
||||
int currentPage;
|
||||
|
||||
public SavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
private SavedState(Parcel in) {
|
||||
super(in);
|
||||
currentPage = in.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(currentPage);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/* -*- 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.activitystream.topsites;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.activitystream.ActivityStream;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
|
||||
import org.mozilla.gecko.icons.IconCallback;
|
||||
import org.mozilla.gecko.icons.IconResponse;
|
||||
import org.mozilla.gecko.icons.Icons;
|
||||
import org.mozilla.gecko.util.DrawableUtil;
|
||||
import org.mozilla.gecko.util.ViewUtil;
|
||||
import org.mozilla.gecko.util.TouchTargetUtil;
|
||||
import org.mozilla.gecko.widget.FaviconView;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
class TopSitesCard extends RecyclerView.ViewHolder
|
||||
implements IconCallback, View.OnClickListener {
|
||||
private final FaviconView faviconView;
|
||||
|
||||
private final TextView title;
|
||||
private final ImageView menuButton;
|
||||
private Future<IconResponse> ongoingIconLoad;
|
||||
|
||||
private String url;
|
||||
|
||||
private final HomePager.OnUrlOpenListener onUrlOpenListener;
|
||||
private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
|
||||
|
||||
public TopSitesCard(FrameLayout card, final HomePager.OnUrlOpenListener onUrlOpenListener, final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
super(card);
|
||||
|
||||
faviconView = (FaviconView) card.findViewById(R.id.favicon);
|
||||
|
||||
title = (TextView) card.findViewById(R.id.title);
|
||||
menuButton = (ImageView) card.findViewById(R.id.menu);
|
||||
|
||||
this.onUrlOpenListener = onUrlOpenListener;
|
||||
this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
|
||||
|
||||
card.setOnClickListener(this);
|
||||
|
||||
TouchTargetUtil.ensureTargetHitArea(menuButton, card);
|
||||
menuButton.setOnClickListener(this);
|
||||
|
||||
ViewUtil.enableTouchRipple(menuButton);
|
||||
}
|
||||
|
||||
void bind(final TopSitesPageAdapter.TopSite topSite) {
|
||||
ActivityStream.extractLabel(itemView.getContext(), topSite.url, true, new ActivityStream.LabelCallback() {
|
||||
@Override
|
||||
public void onLabelExtracted(String label) {
|
||||
title.setText(label);
|
||||
}
|
||||
});
|
||||
|
||||
this.url = topSite.url;
|
||||
|
||||
if (ongoingIconLoad != null) {
|
||||
ongoingIconLoad.cancel(true);
|
||||
}
|
||||
|
||||
ongoingIconLoad = Icons.with(itemView.getContext())
|
||||
.pageUrl(topSite.url)
|
||||
.skipNetwork()
|
||||
.build()
|
||||
.execute(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIconResponse(IconResponse response) {
|
||||
faviconView.updateImage(response);
|
||||
|
||||
final int tintColor = !response.hasColor() || response.getColor() == Color.WHITE ? Color.LTGRAY : Color.WHITE;
|
||||
|
||||
menuButton.setImageDrawable(
|
||||
DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, tintColor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View clickedView) {
|
||||
if (clickedView == itemView) {
|
||||
onUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
|
||||
} else if (clickedView == menuButton) {
|
||||
ActivityStreamContextMenu.show(clickedView.getContext(),
|
||||
menuButton,
|
||||
ActivityStreamContextMenu.MenuMode.TOPSITE,
|
||||
title.getText().toString(), url,
|
||||
onUrlOpenListener, onUrlOpenInBackgroundListener,
|
||||
faviconView.getWidth(), faviconView.getHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/* -*- 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.activitystream.topsites;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class TopSitesPage
|
||||
extends RecyclerView {
|
||||
public TopSitesPage(Context context,
|
||||
@Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setLayoutManager(new GridLayoutManager(context, 1));
|
||||
}
|
||||
|
||||
public void setTiles(int tiles) {
|
||||
setLayoutManager(new GridLayoutManager(getContext(), tiles));
|
||||
}
|
||||
|
||||
private HomePager.OnUrlOpenListener onUrlOpenListener;
|
||||
private HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
|
||||
|
||||
public TopSitesPageAdapter getAdapter() {
|
||||
return (TopSitesPageAdapter) super.getAdapter();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/* -*- 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.activitystream.topsites;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TopSitesPageAdapter extends RecyclerView.Adapter<TopSitesCard> {
|
||||
static final class TopSite {
|
||||
public final long id;
|
||||
public final String url;
|
||||
public final String title;
|
||||
|
||||
TopSite(long id, String url, String title) {
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
private List<TopSite> topSites;
|
||||
private int tiles;
|
||||
private int tilesWidth;
|
||||
private int tilesHeight;
|
||||
private int textHeight;
|
||||
|
||||
private final HomePager.OnUrlOpenListener onUrlOpenListener;
|
||||
private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
|
||||
|
||||
public TopSitesPageAdapter(Context context, int tiles, int tilesWidth, int tilesHeight,
|
||||
HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
setHasStableIds(true);
|
||||
|
||||
this.topSites = new ArrayList<>();
|
||||
this.tiles = tiles;
|
||||
this.tilesWidth = tilesWidth;
|
||||
this.tilesHeight = tilesHeight;
|
||||
this.textHeight = context.getResources().getDimensionPixelSize(R.dimen.activity_stream_top_sites_text_height);
|
||||
|
||||
this.onUrlOpenListener = onUrlOpenListener;
|
||||
this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param cursor
|
||||
* @param startIndex The first item that this topsites group should show. This item, and the following
|
||||
* 3 items will be displayed by this adapter.
|
||||
*/
|
||||
public void swapCursor(Cursor cursor, int startIndex) {
|
||||
topSites.clear();
|
||||
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < tiles && startIndex + i < cursor.getCount(); i++) {
|
||||
cursor.moveToPosition(startIndex + i);
|
||||
|
||||
// The Combined View only contains pages that have been visited at least once, i.e. any
|
||||
// page in the TopSites query will contain a HISTORY_ID. _ID however will be 0 for all rows.
|
||||
final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
|
||||
final String url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
|
||||
final String title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
|
||||
|
||||
topSites.add(new TopSite(id, url, title));
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TopSitesCard holder, int position) {
|
||||
holder.bind(topSites.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TopSitesCard onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
|
||||
final FrameLayout card = (FrameLayout) inflater.inflate(R.layout.activity_stream_topsites_card, parent, false);
|
||||
final View content = card.findViewById(R.id.content);
|
||||
|
||||
ViewGroup.LayoutParams layoutParams = content.getLayoutParams();
|
||||
layoutParams.width = tilesWidth;
|
||||
layoutParams.height = tilesHeight + textHeight;
|
||||
content.setLayoutParams(layoutParams);
|
||||
|
||||
return new TopSitesCard(card, onUrlOpenListener, onUrlOpenInBackgroundListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return topSites.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@UiThread
|
||||
public long getItemId(int position) {
|
||||
return topSites.get(position).id;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/* -*- 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.activitystream.topsites;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.home.HomePager;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* The primary / top-level TopSites adapter: it handles the ViewPager, and also handles
|
||||
* all lower-level Adapters that populate the individual topsite items.
|
||||
*/
|
||||
public class TopSitesPagerAdapter extends PagerAdapter {
|
||||
public static final int PAGES = 4;
|
||||
|
||||
private int tiles;
|
||||
private int tilesWidth;
|
||||
private int tilesHeight;
|
||||
|
||||
private LinkedList<TopSitesPage> pages = new LinkedList<>();
|
||||
|
||||
private final Context context;
|
||||
private final HomePager.OnUrlOpenListener onUrlOpenListener;
|
||||
private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
|
||||
|
||||
private int count = 0;
|
||||
|
||||
public TopSitesPagerAdapter(Context context,
|
||||
HomePager.OnUrlOpenListener onUrlOpenListener,
|
||||
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
|
||||
this.context = context;
|
||||
this.onUrlOpenListener = onUrlOpenListener;
|
||||
this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
|
||||
}
|
||||
|
||||
public void setTilesSize(int tiles, int tilesWidth, int tilesHeight) {
|
||||
this.tilesWidth = tilesWidth;
|
||||
this.tilesHeight = tilesHeight;
|
||||
this.tiles = tiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return Math.min(count, 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(View view, Object object) {
|
||||
return view == object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
TopSitesPage page = pages.get(position);
|
||||
|
||||
container.addView(page);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(Object object) {
|
||||
return PagerAdapter.POSITION_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
container.removeView((View) object);
|
||||
}
|
||||
|
||||
public void swapCursor(Cursor cursor) {
|
||||
// Divide while rounding up: 0 items = 0 pages, 1-ITEMS_PER_PAGE items = 1 page, etc.
|
||||
if (cursor != null) {
|
||||
count = (cursor.getCount() - 1) / tiles + 1;
|
||||
} else {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
pages.clear();
|
||||
final int pageDelta = count;
|
||||
|
||||
if (pageDelta > 0) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
for (int i = 0; i < pageDelta; i++) {
|
||||
final TopSitesPage page = (TopSitesPage) inflater.inflate(R.layout.activity_stream_topsites_page, null, false);
|
||||
|
||||
page.setTiles(tiles);
|
||||
final TopSitesPageAdapter adapter = new TopSitesPageAdapter(context, tiles, tilesWidth, tilesHeight,
|
||||
onUrlOpenListener, onUrlOpenInBackgroundListener);
|
||||
page.setAdapter(adapter);
|
||||
pages.add(page);
|
||||
}
|
||||
} else if (pageDelta < 0) {
|
||||
for (int i = 0; i > pageDelta; i--) {
|
||||
final TopSitesPage page = pages.getLast();
|
||||
|
||||
// Ensure the page doesn't use the old/invalid cursor anymore
|
||||
page.getAdapter().swapCursor(null, 0);
|
||||
|
||||
pages.removeLast();
|
||||
}
|
||||
} else {
|
||||
// do nothing: we will be updating all the pages below
|
||||
}
|
||||
|
||||
int startIndex = 0;
|
||||
for (TopSitesPage page : pages) {
|
||||
page.getAdapter().swapCursor(cursor, startIndex);
|
||||
startIndex += tiles;
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue