diff options
author | Matt A. Tobin <email@mattatobin.com> | 2019-04-23 15:32:23 -0400 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2019-04-23 15:32:23 -0400 |
commit | abe80cc31d5a40ebed743085011fbcda0c1a9a10 (patch) | |
tree | fb3762f06b84745b182af281abb107b95a9fcf01 /mobile/android/base/java/org/mozilla/gecko/home | |
parent | 63295d0087eb58a6eb34cad324c4c53d1b220491 (diff) | |
download | UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.gz UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.lz UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.xz UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.zip |
Issue #1053 - Drop support Android and remove Fennec - Part 1a: Remove mobile/android
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/home')
71 files changed, 0 insertions, 17885 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java deleted file mode 100644 index 566422faf..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkFolderView.java +++ /dev/null @@ -1,147 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java deleted file mode 100644 index a1efff049..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarkScreenshotRow.java +++ /dev/null @@ -1,67 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java deleted file mode 100644 index b31116693..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java +++ /dev/null @@ -1,352 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java deleted file mode 100644 index 94157be10..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java +++ /dev/null @@ -1,218 +0,0 @@ - /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java deleted file mode 100644 index 4b4781996..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksPanel.java +++ /dev/null @@ -1,316 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java b/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java deleted file mode 100644 index 7732932fe..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java +++ /dev/null @@ -1,1316 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.home; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; - -import android.content.SharedPreferences; -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.GeckoSharedPrefs; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.R; -import org.mozilla.gecko.SuggestClient; -import org.mozilla.gecko.Tab; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.Telemetry; -import org.mozilla.gecko.TelemetryContract; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.History; -import org.mozilla.gecko.db.BrowserContract.URLColumns; -import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.home.SearchLoader.SearchCursorLoader; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.toolbar.AutocompleteHandler; -import org.mozilla.gecko.util.GeckoEventListener; -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.view.ContextMenu.ContextMenuInfo; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.support.v4.app.LoaderManager.LoaderCallbacks; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.ViewStub; -import android.view.WindowManager.LayoutParams; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.Animation; -import android.view.animation.TranslateAnimation; -import android.widget.AdapterView; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TextView; - -/** - * Fragment that displays frecency search results in a ListView. - */ -public class BrowserSearch extends HomeFragment - implements GeckoEventListener, - SearchEngineBar.OnSearchBarClickListener { - - @RobocopTarget - public interface SuggestClientFactory { - public SuggestClient getSuggestClient(Context context, String template, int timeout, int max); - } - - @RobocopTarget - public static class DefaultSuggestClientFactory implements SuggestClientFactory { - @Override - public SuggestClient getSuggestClient(Context context, String template, int timeout, int max) { - return new SuggestClient(context, template, timeout, max, true); - } - } - - /** - * Set this to mock the suggestion mechanism. Public for access from tests. - */ - @RobocopTarget - public static volatile SuggestClientFactory sSuggestClientFactory = new DefaultSuggestClientFactory(); - - // Logging tag name - private static final String LOGTAG = "GeckoBrowserSearch"; - - // Cursor loader ID for search query - private static final int LOADER_ID_SEARCH = 0; - - // AsyncTask loader ID for suggestion query - private static final int LOADER_ID_SUGGESTION = 1; - private static final int LOADER_ID_SAVED_SUGGESTION = 2; - - // Timeout for the suggestion client to respond - private static final int SUGGESTION_TIMEOUT = 3000; - - // Maximum number of suggestions from the search engine's suggestion client. This impacts network traffic and device - // data consumption whereas R.integer.max_saved_suggestions controls how many suggestion to show in the UI. - private static final int NETWORK_SUGGESTION_MAX = 3; - - // The maximum number of rows deep in a search we'll dig - // for an autocomplete result - private static final int MAX_AUTOCOMPLETE_SEARCH = 20; - - // Length of https:// + 1 required to make autocomplete - // fill in the domain, for both http:// and https:// - private static final int HTTPS_PREFIX_LENGTH = 9; - - // Duration for fade-in animation - private static final int ANIMATION_DURATION = 250; - - // Holds the current search term to use in the query - private volatile String mSearchTerm; - - // Adapter for the list of search results - private SearchAdapter mAdapter; - - // The view shown by the fragment - private LinearLayout mView; - - // The list showing search results - private HomeListView mList; - - // The bar on the bottom of the screen displaying search engine options. - private SearchEngineBar mSearchEngineBar; - - // Client that performs search suggestion queries. - // Public for testing. - @RobocopTarget - public volatile SuggestClient mSuggestClient; - - // List of search engines from Gecko. - // Do not mutate this list. - // Access to this member must only occur from the UI thread. - private List<SearchEngine> mSearchEngines; - - // Search history suggestions - private ArrayList<String> mSearchHistorySuggestions; - - // Track the locale that was last in use when we filled mSearchEngines. - // Access to this member must only occur from the UI thread. - private Locale mLastLocale; - - // Whether search suggestions are enabled or not - private boolean mSuggestionsEnabled; - - // Whether history suggestions are enabled or not - private boolean mSavedSearchesEnabled; - - // Callbacks used for the search loader - private CursorLoaderCallbacks mCursorLoaderCallbacks; - - // Callbacks used for the search suggestion loader - private SearchEngineSuggestionLoaderCallbacks mSearchEngineSuggestionLoaderCallbacks; - private SearchHistorySuggestionLoaderCallbacks mSearchHistorySuggestionLoaderCallback; - - // Autocomplete handler used when filtering results - private AutocompleteHandler mAutocompleteHandler; - - // On search listener - private OnSearchListener mSearchListener; - - // On edit suggestion listener - private OnEditSuggestionListener mEditSuggestionListener; - - // Whether the suggestions will fade in when shown - private boolean mAnimateSuggestions; - - // Opt-in prompt view for search suggestions - private View mSuggestionsOptInPrompt; - - public interface OnSearchListener { - void onSearch(SearchEngine engine, String text, TelemetryContract.Method method); - } - - public interface OnEditSuggestionListener { - public void onEditSuggestion(String suggestion); - } - - public static BrowserSearch newInstance() { - BrowserSearch browserSearch = new BrowserSearch(); - - final Bundle args = new Bundle(); - args.putBoolean(HomePager.CAN_LOAD_ARG, true); - browserSearch.setArguments(args); - - return browserSearch; - } - - public BrowserSearch() { - mSearchTerm = ""; - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - try { - mSearchListener = (OnSearchListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement BrowserSearch.OnSearchListener"); - } - - try { - mEditSuggestionListener = (OnEditSuggestionListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement BrowserSearch.OnEditSuggestionListener"); - } - } - - @Override - public void onDetach() { - super.onDetach(); - - mAutocompleteHandler = null; - mSearchListener = null; - mEditSuggestionListener = null; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mSearchEngines = new ArrayList<SearchEngine>(); - mSearchHistorySuggestions = new ArrayList<>(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - mSearchEngines = null; - } - - @Override - public void onHiddenChanged(boolean hidden) { - if (!hidden) { - final Tab tab = Tabs.getInstance().getSelectedTab(); - final boolean isPrivate = (tab != null && tab.isPrivate()); - - // Removes Search Suggestions Loader if in private browsing mode - // Loader may have been inserted when browsing in normal tab - if (isPrivate) { - getLoaderManager().destroyLoader(LOADER_ID_SUGGESTION); - } - - GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null); - } - super.onHiddenChanged(hidden); - } - - @Override - public void onResume() { - super.onResume(); - - final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext()); - mSavedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true); - - // Fetch engines if we need to. - if (mSearchEngines.isEmpty() || !Locale.getDefault().equals(mLastLocale)) { - GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null); - } else { - updateSearchEngineBar(); - } - - Telemetry.startUISession(TelemetryContract.Session.FRECENCY); - } - - @Override - public void onPause() { - super.onPause(); - - Telemetry.stopUISession(TelemetryContract.Session.FRECENCY); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // All list views are styled to look the same with a global activity theme. - // If the style of the list changes, inflate it from an XML. - mView = (LinearLayout) inflater.inflate(R.layout.browser_search, container, false); - mList = (HomeListView) mView.findViewById(R.id.home_list_view); - mSearchEngineBar = (SearchEngineBar) mView.findViewById(R.id.search_engine_bar); - - return mView; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, - "SearchEngines:Data"); - - mSearchEngineBar.setAdapter(null); - mSearchEngineBar = null; - - mList.setAdapter(null); - mList = null; - - mView = null; - mSuggestionsOptInPrompt = null; - mSuggestClient = null; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - mList.setTag(HomePager.LIST_TAG_BROWSER_SEARCH); - - mList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - // Perform the user-entered search if the user clicks on a search engine row. - // This row will be disabled if suggestions (in addition to the user-entered term) are showing. - if (view instanceof SearchEngineRow) { - ((SearchEngineRow) view).performUserEnteredSearch(); - return; - } - - // Account for the search engine rows. - position -= getPrimaryEngineCount(); - final Cursor c = mAdapter.getCursor(position); - final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); - - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "frecency"); - - // This item is a TwoLinePageRow, so we allow switch-to-tab. - mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); - } - }); - - mList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override - public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { - // Don't do anything when the user long-clicks on a search engine row. - if (view instanceof SearchEngineRow) { - return true; - } - - // Account for the search engine rows. - position -= getPrimaryEngineCount(); - return mList.onItemLongClick(parent, view, position, id); - } - }); - - final ListSelectionListener listener = new ListSelectionListener(); - mList.setOnItemSelectedListener(listener); - mList.setOnFocusChangeListener(listener); - - 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(BrowserContract.Combined.URL)); - info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE)); - - int bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID)); - info.bookmarkId = bookmarkId; - - int historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID)); - info.historyId = historyId; - - boolean isBookmark = bookmarkId != -1; - boolean isHistory = historyId != -1; - - if (isBookmark && isHistory) { - info.itemType = HomeContextMenuInfo.RemoveItemType.COMBINED; - } else if (isBookmark) { - info.itemType = HomeContextMenuInfo.RemoveItemType.BOOKMARKS; - } else if (isHistory) { - info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY; - } - - return info; - } - }); - - mList.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, android.view.KeyEvent event) { - final View selected = mList.getSelectedView(); - - if (selected instanceof SearchEngineRow) { - return selected.onKeyDown(keyCode, event); - } - return false; - } - }); - - registerForContextMenu(mList); - GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, - "SearchEngines:Data"); - - mSearchEngineBar.setOnSearchBarClickListener(this); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { - if (!(menuInfo instanceof HomeContextMenuInfo)) { - return; - } - - HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; - - MenuInflater inflater = new MenuInflater(view.getContext()); - inflater.inflate(R.menu.browsersearch_contextmenu, menu); - - menu.setHeaderTitle(info.getDisplayTitle()); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - ContextMenuInfo menuInfo = item.getMenuInfo(); - if (!(menuInfo instanceof HomeContextMenuInfo)) { - return false; - } - - final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; - final Context context = getActivity(); - - final int itemId = item.getItemId(); - - if (itemId == R.id.browsersearch_remove) { - // Position for Top Sites grid items, but will always be -1 since this is only for BrowserSearch result - final int position = -1; - - new RemoveItemByUrlTask(context, info.url, info.itemType, position).execute(); - return true; - } - - return false; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // Initialize the search adapter - mAdapter = new SearchAdapter(getActivity()); - mList.setAdapter(mAdapter); - - // Only create an instance when we need it - mSearchEngineSuggestionLoaderCallbacks = null; - mSearchHistorySuggestionLoaderCallback = null; - - // Create callbacks before the initial loader is started - mCursorLoaderCallbacks = new CursorLoaderCallbacks(); - loadIfVisible(); - } - - @Override - public void handleMessage(String event, final JSONObject message) { - if (event.equals("SearchEngines:Data")) { - ThreadUtils.postToUiThread(new Runnable() { - @Override - public void run() { - setSearchEngines(message); - } - }); - } - } - - @Override - protected void load() { - SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm); - } - - private void handleAutocomplete(String searchTerm, Cursor c) { - if (c == null || - mAutocompleteHandler == null || - TextUtils.isEmpty(searchTerm)) { - return; - } - - // Avoid searching the path if we don't have to. Currently just - // decided by whether there is a '/' character in the string. - final boolean searchPath = searchTerm.indexOf('/') > 0; - final String autocompletion = findAutocompletion(searchTerm, c, searchPath); - - if (autocompletion == null || mAutocompleteHandler == null) { - return; - } - - // Prefetch auto-completed domain since it's a likely target - GeckoAppShell.notifyObservers("Session:Prefetch", "http://" + autocompletion); - - mAutocompleteHandler.onAutocomplete(autocompletion); - mAutocompleteHandler = null; - } - - /** - * Returns the substring of a provided URI, starting at the given offset, - * and extending up to the end of the path segment in which the provided - * index is found. - * - * For example, given - * - * "www.reddit.com/r/boop/abcdef", 0, ? - * - * this method returns - * - * ?=2: "www.reddit.com/" - * ?=17: "www.reddit.com/r/boop/" - * ?=21: "www.reddit.com/r/boop/" - * ?=22: "www.reddit.com/r/boop/abcdef" - * - */ - private static String uriSubstringUpToMatchedPath(final String url, final int offset, final int begin) { - final int afterEnd = url.length(); - - // We want to include the trailing slash, but not other characters. - int chop = url.indexOf('/', begin); - if (chop != -1) { - ++chop; - if (chop < offset) { - // This isn't supposed to happen. Fall back to returning the whole damn thing. - return url; - } - } else { - chop = url.indexOf('?', begin); - if (chop == -1) { - chop = url.indexOf('#', begin); - } - if (chop == -1) { - chop = afterEnd; - } - } - - return url.substring(offset, chop); - } - - LinkedHashSet<String> domains = null; - private LinkedHashSet<String> getDomains() { - if (domains == null) { - domains = new LinkedHashSet<String>(500); - BufferedReader buf = null; - try { - buf = new BufferedReader(new InputStreamReader(getResources().openRawResource(R.raw.topdomains))); - String res = null; - - do { - res = buf.readLine(); - if (res != null) { - domains.add(res); - } - } while (res != null); - } catch (IOException e) { - Log.e(LOGTAG, "Error reading domains", e); - } finally { - if (buf != null) { - try { - buf.close(); - } catch (IOException e) { } - } - } - } - return domains; - } - - private String searchDomains(String search) { - for (String domain : getDomains()) { - if (domain.startsWith(search)) { - return domain; - } - } - return null; - } - - private String findAutocompletion(String searchTerm, Cursor c, boolean searchPath) { - if (!c.moveToFirst()) { - // No cursor probably means no history, so let's try the fallback list. - return searchDomains(searchTerm); - } - - final int searchLength = searchTerm.length(); - final int urlIndex = c.getColumnIndexOrThrow(History.URL); - int searchCount = 0; - - do { - final String url = c.getString(urlIndex); - - if (searchCount == 0) { - // Prefetch the first item in the list since it's weighted the highest - GeckoAppShell.notifyObservers("Session:Prefetch", url); - } - - // Does the completion match against the whole URL? This will match - // about: pages, as well as user input including "http://...". - if (url.startsWith(searchTerm)) { - return uriSubstringUpToMatchedPath(url, 0, - (searchLength > HTTPS_PREFIX_LENGTH) ? searchLength : HTTPS_PREFIX_LENGTH); - } - - final Uri uri = Uri.parse(url); - final String host = uri.getHost(); - - // Host may be null for about pages. - if (host == null) { - continue; - } - - if (host.startsWith(searchTerm)) { - return host + "/"; - } - - final String strippedHost = StringUtils.stripCommonSubdomains(host); - if (strippedHost.startsWith(searchTerm)) { - return strippedHost + "/"; - } - - ++searchCount; - - if (!searchPath) { - continue; - } - - // Otherwise, if we're matching paths, let's compare against the string itself. - final int hostOffset = url.indexOf(strippedHost); - if (hostOffset == -1) { - // This was a URL string that parsed to a different host (normalized?). - // Give up. - continue; - } - - // We already matched the non-stripped host, so now we're - // substring-searching in the part of the URL without the common - // subdomains. - if (url.startsWith(searchTerm, hostOffset)) { - // Great! Return including the rest of the path segment. - return uriSubstringUpToMatchedPath(url, hostOffset, hostOffset + searchLength); - } - } while (searchCount < MAX_AUTOCOMPLETE_SEARCH && c.moveToNext()); - - // If we can't find an autocompletion domain from history, let's try using the fallback list. - return searchDomains(searchTerm); - } - - public void resetScrollState() { - mSearchEngineBar.scrollToPosition(0); - } - - private void filterSuggestions() { - Tab tab = Tabs.getInstance().getSelectedTab(); - final boolean isPrivate = (tab != null && tab.isPrivate()); - - // mSuggestClient may be null if we haven't received our search engine list yet - hence - // we need to exit here in that case. - if (isPrivate || mSuggestClient == null || (!mSuggestionsEnabled && !mSavedSearchesEnabled)) { - mSearchHistorySuggestions.clear(); - return; - } - - // Suggestions from search engine - if (mSearchEngineSuggestionLoaderCallbacks == null) { - mSearchEngineSuggestionLoaderCallbacks = new SearchEngineSuggestionLoaderCallbacks(); - } - getLoaderManager().restartLoader(LOADER_ID_SUGGESTION, null, mSearchEngineSuggestionLoaderCallbacks); - - // Saved suggestions - if (mSearchHistorySuggestionLoaderCallback == null) { - mSearchHistorySuggestionLoaderCallback = new SearchHistorySuggestionLoaderCallbacks(); - } - getLoaderManager().restartLoader(LOADER_ID_SAVED_SUGGESTION, null, mSearchHistorySuggestionLoaderCallback); - } - - private void setSuggestions(ArrayList<String> suggestions) { - ThreadUtils.assertOnUiThread(); - - // mSearchEngines may be null if the setSuggestions calls after onDestroy (bug 1310621). - // So drop the suggestions if search engines are not available - if (mSearchEngines != null && !mSearchEngines.isEmpty()) { - mSearchEngines.get(0).setSuggestions(suggestions); - mAdapter.notifyDataSetChanged(); - } - - } - - private void setSavedSuggestions(ArrayList<String> savedSuggestions) { - ThreadUtils.assertOnUiThread(); - - mSearchHistorySuggestions = savedSuggestions; - mAdapter.notifyDataSetChanged(); - } - - private boolean shouldUpdateSearchEngine(ArrayList<SearchEngine> searchEngines) { - if (searchEngines.size() != mSearchEngines.size()) { - return true; - } - - int size = searchEngines.size(); - - for (int i = 0; i < size; i++) { - if (!mSearchEngines.get(i).name.equals(searchEngines.get(i).name)) { - return true; - } - } - - return false; - } - - private void setSearchEngines(JSONObject data) { - ThreadUtils.assertOnUiThread(); - - // This method is called via a Runnable posted from the Gecko thread, so - // it's possible the fragment and/or its view has been destroyed by the - // time we get here. If so, just abort. - if (mView == null) { - return; - } - - try { - final JSONObject suggest = data.getJSONObject("suggest"); - final String suggestEngine = suggest.optString("engine", null); - final String suggestTemplate = suggest.optString("template", null); - final boolean suggestionsPrompted = suggest.getBoolean("prompted"); - final JSONArray engines = data.getJSONArray("searchEngines"); - - mSuggestionsEnabled = suggest.getBoolean("enabled"); - - ArrayList<SearchEngine> searchEngines = new ArrayList<SearchEngine>(); - for (int i = 0; i < engines.length(); i++) { - final JSONObject engineJSON = engines.getJSONObject(i); - final SearchEngine engine = new SearchEngine((Context) getActivity(), engineJSON); - - if (engine.name.equals(suggestEngine) && suggestTemplate != null) { - // Suggest engine should be at the front of the list. - // We're baking in an assumption here that the suggest engine - // is also the default engine. - searchEngines.add(0, engine); - - ensureSuggestClientIsSet(suggestTemplate); - } else { - searchEngines.add(engine); - } - } - - // checking if the new searchEngine is different from mSearchEngine, will have to re-layout if yes - boolean change = shouldUpdateSearchEngine(searchEngines); - - if (mAdapter != null && change) { - mSearchEngines = Collections.unmodifiableList(searchEngines); - mLastLocale = Locale.getDefault(); - updateSearchEngineBar(); - - mAdapter.notifyDataSetChanged(); - } - - final Tab tab = Tabs.getInstance().getSelectedTab(); - final boolean isPrivate = (tab != null && tab.isPrivate()); - - // Show suggestions opt-in prompt only if suggestions are not enabled yet, - // user hasn't been prompted and we're not on a private browsing tab. - // The prompt might have been inflated already when this view was previously called. - // Remove the opt-in prompt if it has been inflated in the view and dealt with by the user, - // or if we're on a private browsing tab - if (!mSuggestionsEnabled && !suggestionsPrompted && !isPrivate) { - showSuggestionsOptIn(); - } else { - removeSuggestionsOptIn(); - } - - } catch (JSONException e) { - Log.e(LOGTAG, "Error getting search engine JSON", e); - } - - filterSuggestions(); - } - - private void updateSearchEngineBar() { - final int primaryEngineCount = getPrimaryEngineCount(); - - if (primaryEngineCount < mSearchEngines.size()) { - mSearchEngineBar.setSearchEngines( - mSearchEngines.subList(primaryEngineCount, mSearchEngines.size()) - ); - mSearchEngineBar.setVisibility(View.VISIBLE); - } else { - mSearchEngineBar.setVisibility(View.GONE); - } - } - - @Override - public void onSearchBarClickListener(final SearchEngine searchEngine) { - final TelemetryContract.Method method = TelemetryContract.Method.LIST_ITEM; - Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method, "searchenginebar"); - mSearchListener.onSearch(searchEngine, mSearchTerm, method); - } - - private void ensureSuggestClientIsSet(final String suggestTemplate) { - // Don't update the suggestClient if we already have a client with the correct template - if (mSuggestClient != null && suggestTemplate.equals(mSuggestClient.getSuggestTemplate())) { - return; - } - - mSuggestClient = sSuggestClientFactory.getSuggestClient(getActivity(), suggestTemplate, SUGGESTION_TIMEOUT, NETWORK_SUGGESTION_MAX); - } - - private void showSuggestionsOptIn() { - // Only make the ViewStub visible again if it has already previously been shown. - // (An inflated ViewStub is removed from the View hierarchy so a second call to findViewById will return null, - // which also further necessitates handling this separately.) - if (mSuggestionsOptInPrompt != null) { - mSuggestionsOptInPrompt.setVisibility(View.VISIBLE); - return; - } - - mSuggestionsOptInPrompt = ((ViewStub) mView.findViewById(R.id.suggestions_opt_in_prompt)).inflate(); - - TextView promptText = (TextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title); - promptText.setText(getResources().getString(R.string.suggestions_prompt)); - - final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes); - final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no); - - final OnClickListener listener = new OnClickListener() { - @Override - public void onClick(View v) { - // Prevent the buttons from being clicked multiple times (bug 816902) - yesButton.setOnClickListener(null); - noButton.setOnClickListener(null); - - setSuggestionsEnabled(v == yesButton); - } - }; - - yesButton.setOnClickListener(listener); - noButton.setOnClickListener(listener); - - // If the prompt gains focus, automatically pass focus to the - // yes button in the prompt. - final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt); - prompt.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - yesButton.requestFocus(); - } - } - }); - } - - private void removeSuggestionsOptIn() { - if (mSuggestionsOptInPrompt == null) { - return; - } - - mSuggestionsOptInPrompt.setVisibility(View.GONE); - } - - private void setSuggestionsEnabled(final boolean enabled) { - // Clicking the yes/no buttons quickly can cause the click events be - // queued before the listeners are removed above, so it's possible - // setSuggestionsEnabled() can be called twice. mSuggestionsOptInPrompt - // can be null if this happens (bug 828480). - if (mSuggestionsOptInPrompt == null) { - return; - } - - // Make suggestions appear immediately after the user opts in - ThreadUtils.postToBackgroundThread(new Runnable() { - @Override - public void run() { - SuggestClient client = mSuggestClient; - if (client != null) { - client.query(mSearchTerm); - } - } - }); - - PrefsHelper.setPref("browser.search.suggest.prompted", true); - PrefsHelper.setPref("browser.search.suggest.enabled", enabled); - - Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, (enabled ? "suggestions_optin_yes" : "suggestions_optin_no")); - - TranslateAnimation slideAnimation = new TranslateAnimation(0, mSuggestionsOptInPrompt.getWidth(), 0, 0); - slideAnimation.setDuration(ANIMATION_DURATION); - slideAnimation.setInterpolator(new AccelerateInterpolator()); - slideAnimation.setFillAfter(true); - final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt); - - TranslateAnimation shrinkAnimation = new TranslateAnimation(0, 0, 0, -1 * mSuggestionsOptInPrompt.getHeight()); - shrinkAnimation.setDuration(ANIMATION_DURATION); - shrinkAnimation.setFillAfter(true); - shrinkAnimation.setStartOffset(slideAnimation.getDuration()); - shrinkAnimation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation a) { - // Increase the height of the view so a gap isn't shown during animation - mView.getLayoutParams().height = mView.getHeight() + - mSuggestionsOptInPrompt.getHeight(); - mView.requestLayout(); - } - - @Override - public void onAnimationRepeat(Animation a) {} - - @Override - public void onAnimationEnd(Animation a) { - // Removing the view immediately results in a NPE in - // dispatchDraw(), possibly because this callback executes - // before drawing is finished. Posting this as a Runnable fixes - // the issue. - mView.post(new Runnable() { - @Override - public void run() { - mView.removeView(mSuggestionsOptInPrompt); - mList.clearAnimation(); - mSuggestionsOptInPrompt = null; - - // Reset the view height - mView.getLayoutParams().height = LayoutParams.MATCH_PARENT; - - // Show search suggestions and update them - if (enabled) { - mSuggestionsEnabled = enabled; - mAnimateSuggestions = true; - mAdapter.notifyDataSetChanged(); - filterSuggestions(); - } - } - }); - } - }); - - prompt.startAnimation(slideAnimation); - mSuggestionsOptInPrompt.startAnimation(shrinkAnimation); - mList.startAnimation(shrinkAnimation); - } - - private int getPrimaryEngineCount() { - return mSearchEngines.size() > 0 ? 1 : 0; - } - - private void restartSearchLoader() { - SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm); - } - - private void initSearchLoader() { - SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm); - } - - public void filter(String searchTerm, AutocompleteHandler handler) { - if (TextUtils.isEmpty(searchTerm)) { - return; - } - - final boolean isNewFilter = !TextUtils.equals(mSearchTerm, searchTerm); - - mSearchTerm = searchTerm; - mAutocompleteHandler = handler; - - if (mAdapter != null) { - if (isNewFilter) { - // The adapter depends on the search term to determine its number - // of items. Make it we notify the view about it. - mAdapter.notifyDataSetChanged(); - - // Restart loaders with the new search term - restartSearchLoader(); - filterSuggestions(); - } else { - // The search term hasn't changed, simply reuse any existing - // loader for the current search term. This will ensure autocompletion - // is consistently triggered (see bug 933739). - initSearchLoader(); - } - } - } - - abstract private static class SuggestionAsyncLoader extends AsyncTaskLoader<ArrayList<String>> { - protected final String mSearchTerm; - private ArrayList<String> mSuggestions; - - public SuggestionAsyncLoader(Context context, String searchTerm) { - super(context); - mSearchTerm = searchTerm; - } - - @Override - public void deliverResult(ArrayList<String> suggestions) { - mSuggestions = suggestions; - - if (isStarted()) { - super.deliverResult(mSuggestions); - } - } - - @Override - protected void onStartLoading() { - if (mSuggestions != null) { - deliverResult(mSuggestions); - } - - if (takeContentChanged() || mSuggestions == null) { - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - cancelLoad(); - } - - @Override - protected void onReset() { - super.onReset(); - - onStopLoading(); - mSuggestions = null; - } - } - - private static class SearchEngineSuggestionAsyncLoader extends SuggestionAsyncLoader { - private final SuggestClient mSuggestClient; - - public SearchEngineSuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) { - super(context, searchTerm); - mSuggestClient = suggestClient; - } - - @Override - public ArrayList<String> loadInBackground() { - return mSuggestClient.query(mSearchTerm); - } - } - - private static class SearchHistorySuggestionAsyncLoader extends SuggestionAsyncLoader { - public SearchHistorySuggestionAsyncLoader(Context context, String searchTerm) { - super(context, searchTerm); - } - - @Override - public ArrayList<String> loadInBackground() { - final ContentResolver cr = getContext().getContentResolver(); - - String[] columns = new String[] { BrowserContract.SearchHistory.QUERY }; - String actualQuery = BrowserContract.SearchHistory.QUERY + " LIKE ?"; - String[] queryArgs = new String[] { '%' + mSearchTerm + '%' }; - - // For deduplication, the worst case is that all the first NETWORK_SUGGESTION_MAX history suggestions are duplicates - // of search engine suggestions, and the there is a duplicate for the search term itself. A duplicate of the - // search term can occur if the user has previously searched for the same thing. - final int maxSavedSuggestions = NETWORK_SUGGESTION_MAX + 1 + getContext().getResources().getInteger(R.integer.max_saved_suggestions); - - final String sortOrderAndLimit = BrowserContract.SearchHistory.DATE + " DESC LIMIT " + maxSavedSuggestions; - final Cursor result = cr.query(BrowserContract.SearchHistory.CONTENT_URI, columns, actualQuery, queryArgs, sortOrderAndLimit); - - if (result == null) { - return new ArrayList<>(); - } - - final ArrayList<String> savedSuggestions = new ArrayList<>(); - try { - if (result.moveToFirst()) { - final int searchColumn = result.getColumnIndexOrThrow(BrowserContract.SearchHistory.QUERY); - do { - final String savedSearch = result.getString(searchColumn); - savedSuggestions.add(savedSearch); - } while (result.moveToNext()); - } - } finally { - result.close(); - } - - return savedSuggestions; - } - } - - private class SearchAdapter extends MultiTypeCursorAdapter { - private static final int ROW_SEARCH = 0; - private static final int ROW_STANDARD = 1; - private static final int ROW_SUGGEST = 2; - - public SearchAdapter(Context context) { - super(context, null, new int[] { ROW_STANDARD, - ROW_SEARCH, - ROW_SUGGEST }, - new int[] { R.layout.home_item_row, - R.layout.home_search_item_row, - R.layout.home_search_item_row }); - } - - @Override - public int getItemViewType(int position) { - if (position < getPrimaryEngineCount()) { - if (mSuggestionsEnabled && mSearchEngines.get(position).hasSuggestions()) { - // Give suggestion views their own type to prevent them from - // sharing other recycled search result views. Using other - // recycled views for the suggestion row can break animations - // (bug 815937). - - return ROW_SUGGEST; - } else { - return ROW_SEARCH; - } - } - - return ROW_STANDARD; - } - - @Override - public boolean isEnabled(int position) { - // If we're using a gamepad or keyboard, allow the row to be - // focused so it can pass the focus to its child suggestion views. - if (!mList.isInTouchMode()) { - return true; - } - - // If the suggestion row only contains one item (the user-entered - // query), allow the entire row to be clickable; clicking the row - // has the same effect as clicking the single suggestion. If the - // row contains multiple items, clicking the row will do nothing. - - if (position < getPrimaryEngineCount()) { - return !mSearchEngines.get(position).hasSuggestions(); - } - - return true; - } - - // Add the search engines to the number of reported results. - @Override - public int getCount() { - final int resultCount = super.getCount(); - - // Don't show search engines or suggestions if search field is empty - if (TextUtils.isEmpty(mSearchTerm)) { - return resultCount; - } - - return resultCount + getPrimaryEngineCount(); - } - - @Override - public void bindView(View view, Context context, int position) { - final int type = getItemViewType(position); - - if (type == ROW_SEARCH || type == ROW_SUGGEST) { - final SearchEngineRow row = (SearchEngineRow) view; - row.setOnUrlOpenListener(mUrlOpenListener); - row.setOnSearchListener(mSearchListener); - row.setOnEditSuggestionListener(mEditSuggestionListener); - row.setSearchTerm(mSearchTerm); - - final SearchEngine engine = mSearchEngines.get(position); - final boolean haveSuggestions = (engine.hasSuggestions() || !mSearchHistorySuggestions.isEmpty()); - final boolean animate = (mAnimateSuggestions && haveSuggestions); - row.updateSuggestions(mSuggestionsEnabled, engine, mSearchHistorySuggestions, animate); - if (animate) { - // Only animate suggestions the first time they are shown - mAnimateSuggestions = false; - } - } else { - // Account for the search engines - position -= getPrimaryEngineCount(); - - final Cursor c = getCursor(position); - final TwoLinePageRow row = (TwoLinePageRow) view; - row.updateFromCursor(c); - } - } - } - - 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) { - if (mAdapter != null) { - mAdapter.swapCursor(c); - - // We should handle autocompletion based on the search term - // associated with the loader that has just provided - // the results. - SearchCursorLoader searchLoader = (SearchCursorLoader) loader; - handleAutocomplete(searchLoader.getSearchTerm(), c); - } - } - - @Override - public void onLoaderReset(Loader<Cursor> loader) { - if (mAdapter != null) { - mAdapter.swapCursor(null); - } - } - } - - private class SearchEngineSuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> { - @Override - public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) { - // mSuggestClient is set to null in onDestroyView(), so using it - // safely here relies on the fact that onCreateLoader() is called - // synchronously in restartLoader(). - return new SearchEngineSuggestionAsyncLoader(getActivity(), mSuggestClient, mSearchTerm); - } - - @Override - public void onLoadFinished(Loader<ArrayList<String>> loader, ArrayList<String> suggestions) { - setSuggestions(suggestions); - } - - @Override - public void onLoaderReset(Loader<ArrayList<String>> loader) { - setSuggestions(new ArrayList<String>()); - } - } - - private class SearchHistorySuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> { - @Override - public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) { - // mSuggestClient is set to null in onDestroyView(), so using it - // safely here relies on the fact that onCreateLoader() is called - // synchronously in restartLoader(). - return new SearchHistorySuggestionAsyncLoader(getActivity(), mSearchTerm); - } - - @Override - public void onLoadFinished(Loader<ArrayList<String>> loader, ArrayList<String> suggestions) { - setSavedSuggestions(suggestions); - } - - @Override - public void onLoaderReset(Loader<ArrayList<String>> loader) { - setSavedSuggestions(new ArrayList<String>()); - } - } - - private static class ListSelectionListener implements View.OnFocusChangeListener, - AdapterView.OnItemSelectedListener { - private SearchEngineRow mSelectedEngineRow; - - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - View selectedRow = ((ListView) v).getSelectedView(); - if (selectedRow != null) { - selectRow(selectedRow); - } - } else { - deselectRow(); - } - } - - @Override - public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - deselectRow(); - selectRow(view); - } - - @Override - public void onNothingSelected(AdapterView<?> parent) { - deselectRow(); - } - - private void selectRow(View row) { - if (row instanceof SearchEngineRow) { - mSelectedEngineRow = (SearchEngineRow) row; - mSelectedEngineRow.onSelected(); - } - } - - private void deselectRow() { - if (mSelectedEngineRow != null) { - mSelectedEngineRow.onDeselected(); - mSelectedEngineRow = null; - } - } - } - - /** - * HomeSearchListView is a list view for displaying search engine results on the awesome screen. - */ - public static class HomeSearchListView extends HomeListView { - public HomeSearchListView(Context context, AttributeSet attrs) { - this(context, attrs, R.attr.homeListViewStyle); - } - - public HomeSearchListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - // Dismiss the soft keyboard. - requestFocus(); - } - - return super.onTouchEvent(event); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java deleted file mode 100644 index f288a2745..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java +++ /dev/null @@ -1,373 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java deleted file mode 100644 index 402ed26e7..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java +++ /dev/null @@ -1,433 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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; - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java deleted file mode 100644 index a2c1b72c2..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java +++ /dev/null @@ -1,127 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java deleted file mode 100644 index c9afecd63..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java +++ /dev/null @@ -1,697 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java deleted file mode 100644 index e813e4c44..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java +++ /dev/null @@ -1,145 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java deleted file mode 100644 index d2c136219..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java +++ /dev/null @@ -1,393 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java deleted file mode 100644 index 7168c1576..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/FramePanelLayout.java +++ /dev/null @@ -1,52 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java b/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java deleted file mode 100644 index 7a49559f6..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HistorySectionsHelper.java +++ /dev/null @@ -1,80 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java deleted file mode 100644 index 98d1ae6d8..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java +++ /dev/null @@ -1,224 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java deleted file mode 100644 index 10f5db39e..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java +++ /dev/null @@ -1,315 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java deleted file mode 100644 index 08e79be3a..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java +++ /dev/null @@ -1,1694 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.home; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.annotation.RobocopTarget; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.R; -import org.mozilla.gecko.util.ThreadUtils; - -import android.content.Context; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; -import android.util.Pair; - -public final class HomeConfig { - public static final String PREF_KEY_BOOKMARKS_PANEL_ENABLED = "bookmarksPanelEnabled"; - public static final String PREF_KEY_HISTORY_PANEL_ENABLED = "combinedHistoryPanelEnabled"; - - /** - * Used to determine what type of HomeFragment subclass to use when creating - * a given panel. With the exception of DYNAMIC, all of these types correspond - * to a default set of built-in panels. The DYNAMIC panel type is used by - * third-party services to create panels with varying types of content. - */ - @RobocopTarget - public static enum PanelType implements Parcelable { - TOP_SITES("top_sites", TopSitesPanel.class), - BOOKMARKS("bookmarks", BookmarksPanel.class), - COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class), - DYNAMIC("dynamic", DynamicPanel.class), - // Deprecated panels that should no longer exist but are kept around for - // migration code. Class references have been replaced with new version of the panel. - DEPRECATED_REMOTE_TABS("remote_tabs", CombinedHistoryPanel.class), - DEPRECATED_HISTORY("history", CombinedHistoryPanel.class), - DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class), - DEPRECATED_RECENT_TABS("recent_tabs", CombinedHistoryPanel.class); - - private final String mId; - private final Class<?> mPanelClass; - - PanelType(String id, Class<?> panelClass) { - mId = id; - mPanelClass = panelClass; - } - - public static PanelType fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to PanelType"); - } - - for (PanelType panelType : PanelType.values()) { - if (TextUtils.equals(panelType.mId, id.toLowerCase())) { - return panelType; - } - } - - throw new IllegalArgumentException("Could not convert String id to PanelType"); - } - - @Override - public String toString() { - return mId; - } - - public Class<?> getPanelClass() { - return mPanelClass; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator<PanelType> CREATOR = new Creator<PanelType>() { - @Override - public PanelType createFromParcel(final Parcel source) { - return PanelType.values()[source.readInt()]; - } - - @Override - public PanelType[] newArray(final int size) { - return new PanelType[size]; - } - }; - } - - public static class PanelConfig implements Parcelable { - private final PanelType mType; - private final String mTitle; - private final String mId; - private final LayoutType mLayoutType; - private final List<ViewConfig> mViews; - private final AuthConfig mAuthConfig; - private final EnumSet<Flags> mFlags; - private final int mPosition; - - static final String JSON_KEY_TYPE = "type"; - static final String JSON_KEY_TITLE = "title"; - static final String JSON_KEY_ID = "id"; - static final String JSON_KEY_LAYOUT = "layout"; - static final String JSON_KEY_VIEWS = "views"; - static final String JSON_KEY_AUTH_CONFIG = "authConfig"; - static final String JSON_KEY_DEFAULT = "default"; - static final String JSON_KEY_DISABLED = "disabled"; - static final String JSON_KEY_POSITION = "position"; - - public enum Flags { - DEFAULT_PANEL, - DISABLED_PANEL - } - - public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException { - final String panelType = json.optString(JSON_KEY_TYPE, null); - if (TextUtils.isEmpty(panelType)) { - mType = PanelType.DYNAMIC; - } else { - mType = PanelType.fromId(panelType); - } - - mTitle = json.getString(JSON_KEY_TITLE); - mId = json.getString(JSON_KEY_ID); - - final String layoutTypeId = json.optString(JSON_KEY_LAYOUT, null); - if (layoutTypeId != null) { - mLayoutType = LayoutType.fromId(layoutTypeId); - } else { - mLayoutType = null; - } - - final JSONArray jsonViews = json.optJSONArray(JSON_KEY_VIEWS); - if (jsonViews != null) { - mViews = new ArrayList<ViewConfig>(); - - final int viewCount = jsonViews.length(); - for (int i = 0; i < viewCount; i++) { - final JSONObject jsonViewConfig = (JSONObject) jsonViews.get(i); - final ViewConfig viewConfig = new ViewConfig(i, jsonViewConfig); - mViews.add(viewConfig); - } - } else { - mViews = null; - } - - final JSONObject jsonAuthConfig = json.optJSONObject(JSON_KEY_AUTH_CONFIG); - if (jsonAuthConfig != null) { - mAuthConfig = new AuthConfig(jsonAuthConfig); - } else { - mAuthConfig = null; - } - - mFlags = EnumSet.noneOf(Flags.class); - - if (json.optBoolean(JSON_KEY_DEFAULT, false)) { - mFlags.add(Flags.DEFAULT_PANEL); - } - - if (json.optBoolean(JSON_KEY_DISABLED, false)) { - mFlags.add(Flags.DISABLED_PANEL); - } - - mPosition = json.optInt(JSON_KEY_POSITION, -1); - - validate(); - } - - @SuppressWarnings("unchecked") - public PanelConfig(Parcel in) { - mType = (PanelType) in.readParcelable(getClass().getClassLoader()); - mTitle = in.readString(); - mId = in.readString(); - mLayoutType = (LayoutType) in.readParcelable(getClass().getClassLoader()); - - mViews = new ArrayList<ViewConfig>(); - in.readTypedList(mViews, ViewConfig.CREATOR); - - mAuthConfig = (AuthConfig) in.readParcelable(getClass().getClassLoader()); - - mFlags = (EnumSet<Flags>) in.readSerializable(); - mPosition = in.readInt(); - - validate(); - } - - public PanelConfig(PanelConfig panelConfig) { - mType = panelConfig.mType; - mTitle = panelConfig.mTitle; - mId = panelConfig.mId; - mLayoutType = panelConfig.mLayoutType; - - mViews = new ArrayList<ViewConfig>(); - List<ViewConfig> viewConfigs = panelConfig.mViews; - if (viewConfigs != null) { - for (ViewConfig viewConfig : viewConfigs) { - mViews.add(new ViewConfig(viewConfig)); - } - } - - mAuthConfig = panelConfig.mAuthConfig; - mFlags = panelConfig.mFlags.clone(); - mPosition = panelConfig.mPosition; - - validate(); - } - - public PanelConfig(PanelType type, String title, String id) { - this(type, title, id, EnumSet.noneOf(Flags.class)); - } - - public PanelConfig(PanelType type, String title, String id, EnumSet<Flags> flags) { - this(type, title, id, null, null, null, flags, -1); - } - - public PanelConfig(PanelType type, String title, String id, LayoutType layoutType, - List<ViewConfig> views, AuthConfig authConfig, EnumSet<Flags> flags, int position) { - mType = type; - mTitle = title; - mId = id; - mLayoutType = layoutType; - mViews = views; - mAuthConfig = authConfig; - mFlags = flags; - mPosition = position; - - validate(); - } - - private void validate() { - if (mType == null) { - throw new IllegalArgumentException("Can't create PanelConfig with null type"); - } - - if (TextUtils.isEmpty(mTitle)) { - throw new IllegalArgumentException("Can't create PanelConfig with empty title"); - } - - if (TextUtils.isEmpty(mId)) { - throw new IllegalArgumentException("Can't create PanelConfig with empty id"); - } - - if (mLayoutType == null && mType == PanelType.DYNAMIC) { - throw new IllegalArgumentException("Can't create a dynamic PanelConfig with null layout type"); - } - - if ((mViews == null || mViews.size() == 0) && mType == PanelType.DYNAMIC) { - throw new IllegalArgumentException("Can't create a dynamic PanelConfig with no views"); - } - - if (mFlags == null) { - throw new IllegalArgumentException("Can't create PanelConfig with null flags"); - } - } - - public PanelType getType() { - return mType; - } - - public String getTitle() { - return mTitle; - } - - public String getId() { - return mId; - } - - public LayoutType getLayoutType() { - return mLayoutType; - } - - public int getViewCount() { - return (mViews != null ? mViews.size() : 0); - } - - public ViewConfig getViewAt(int index) { - return (mViews != null ? mViews.get(index) : null); - } - - public EnumSet<Flags> getFlags() { - return mFlags.clone(); - } - - public boolean isDynamic() { - return (mType == PanelType.DYNAMIC); - } - - public boolean isDefault() { - return mFlags.contains(Flags.DEFAULT_PANEL); - } - - private void setIsDefault(boolean isDefault) { - if (isDefault) { - mFlags.add(Flags.DEFAULT_PANEL); - } else { - mFlags.remove(Flags.DEFAULT_PANEL); - } - } - - public boolean isDisabled() { - return mFlags.contains(Flags.DISABLED_PANEL); - } - - private void setIsDisabled(boolean isDisabled) { - if (isDisabled) { - mFlags.add(Flags.DISABLED_PANEL); - } else { - mFlags.remove(Flags.DISABLED_PANEL); - } - } - - public AuthConfig getAuthConfig() { - return mAuthConfig; - } - - public int getPosition() { - return mPosition; - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - json.put(JSON_KEY_TYPE, mType.toString()); - json.put(JSON_KEY_TITLE, mTitle); - json.put(JSON_KEY_ID, mId); - - if (mLayoutType != null) { - json.put(JSON_KEY_LAYOUT, mLayoutType.toString()); - } - - if (mViews != null) { - final JSONArray jsonViews = new JSONArray(); - - final int viewCount = mViews.size(); - for (int i = 0; i < viewCount; i++) { - final ViewConfig viewConfig = mViews.get(i); - final JSONObject jsonViewConfig = viewConfig.toJSON(); - jsonViews.put(jsonViewConfig); - } - - json.put(JSON_KEY_VIEWS, jsonViews); - } - - if (mAuthConfig != null) { - json.put(JSON_KEY_AUTH_CONFIG, mAuthConfig.toJSON()); - } - - if (mFlags.contains(Flags.DEFAULT_PANEL)) { - json.put(JSON_KEY_DEFAULT, true); - } - - if (mFlags.contains(Flags.DISABLED_PANEL)) { - json.put(JSON_KEY_DISABLED, true); - } - - json.put(JSON_KEY_POSITION, mPosition); - - return json; - } - - @Override - public boolean equals(Object o) { - if (o == null) { - return false; - } - - if (this == o) { - return true; - } - - if (!(o instanceof PanelConfig)) { - return false; - } - - final PanelConfig other = (PanelConfig) o; - return mId.equals(other.mId); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(mType, 0); - dest.writeString(mTitle); - dest.writeString(mId); - dest.writeParcelable(mLayoutType, 0); - dest.writeTypedList(mViews); - dest.writeParcelable(mAuthConfig, 0); - dest.writeSerializable(mFlags); - dest.writeInt(mPosition); - } - - public static final Creator<PanelConfig> CREATOR = new Creator<PanelConfig>() { - @Override - public PanelConfig createFromParcel(final Parcel in) { - return new PanelConfig(in); - } - - @Override - public PanelConfig[] newArray(final int size) { - return new PanelConfig[size]; - } - }; - } - - public static enum LayoutType implements Parcelable { - FRAME("frame"); - - private final String mId; - - LayoutType(String id) { - mId = id; - } - - public static LayoutType fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to LayoutType"); - } - - for (LayoutType layoutType : LayoutType.values()) { - if (TextUtils.equals(layoutType.mId, id.toLowerCase())) { - return layoutType; - } - } - - throw new IllegalArgumentException("Could not convert String id to LayoutType"); - } - - @Override - public String toString() { - return mId; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator<LayoutType> CREATOR = new Creator<LayoutType>() { - @Override - public LayoutType createFromParcel(final Parcel source) { - return LayoutType.values()[source.readInt()]; - } - - @Override - public LayoutType[] newArray(final int size) { - return new LayoutType[size]; - } - }; - } - - public static enum ViewType implements Parcelable { - LIST("list"), - GRID("grid"); - - private final String mId; - - ViewType(String id) { - mId = id; - } - - public static ViewType fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to ViewType"); - } - - for (ViewType viewType : ViewType.values()) { - if (TextUtils.equals(viewType.mId, id.toLowerCase())) { - return viewType; - } - } - - throw new IllegalArgumentException("Could not convert String id to ViewType"); - } - - @Override - public String toString() { - return mId; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator<ViewType> CREATOR = new Creator<ViewType>() { - @Override - public ViewType createFromParcel(final Parcel source) { - return ViewType.values()[source.readInt()]; - } - - @Override - public ViewType[] newArray(final int size) { - return new ViewType[size]; - } - }; - } - - public static enum ItemType implements Parcelable { - ARTICLE("article"), - IMAGE("image"), - ICON("icon"); - - private final String mId; - - ItemType(String id) { - mId = id; - } - - public static ItemType fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to ItemType"); - } - - for (ItemType itemType : ItemType.values()) { - if (TextUtils.equals(itemType.mId, id.toLowerCase())) { - return itemType; - } - } - - throw new IllegalArgumentException("Could not convert String id to ItemType"); - } - - @Override - public String toString() { - return mId; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator<ItemType> CREATOR = new Creator<ItemType>() { - @Override - public ItemType createFromParcel(final Parcel source) { - return ItemType.values()[source.readInt()]; - } - - @Override - public ItemType[] newArray(final int size) { - return new ItemType[size]; - } - }; - } - - public static enum ItemHandler implements Parcelable { - BROWSER("browser"), - INTENT("intent"); - - private final String mId; - - ItemHandler(String id) { - mId = id; - } - - public static ItemHandler fromId(String id) { - if (id == null) { - throw new IllegalArgumentException("Could not convert null String to ItemHandler"); - } - - for (ItemHandler itemHandler : ItemHandler.values()) { - if (TextUtils.equals(itemHandler.mId, id.toLowerCase())) { - return itemHandler; - } - } - - throw new IllegalArgumentException("Could not convert String id to ItemHandler"); - } - - @Override - public String toString() { - return mId; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator<ItemHandler> CREATOR = new Creator<ItemHandler>() { - @Override - public ItemHandler createFromParcel(final Parcel source) { - return ItemHandler.values()[source.readInt()]; - } - - @Override - public ItemHandler[] newArray(final int size) { - return new ItemHandler[size]; - } - }; - } - - public static class ViewConfig implements Parcelable { - private final int mIndex; - private final ViewType mType; - private final String mDatasetId; - private final ItemType mItemType; - private final ItemHandler mItemHandler; - private final String mBackImageUrl; - private final String mFilter; - private final EmptyViewConfig mEmptyViewConfig; - private final HeaderConfig mHeaderConfig; - private final EnumSet<Flags> mFlags; - - static final String JSON_KEY_TYPE = "type"; - static final String JSON_KEY_DATASET = "dataset"; - static final String JSON_KEY_ITEM_TYPE = "itemType"; - static final String JSON_KEY_ITEM_HANDLER = "itemHandler"; - static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl"; - static final String JSON_KEY_FILTER = "filter"; - static final String JSON_KEY_EMPTY = "empty"; - static final String JSON_KEY_HEADER = "header"; - static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled"; - - public enum Flags { - REFRESH_ENABLED - } - - public ViewConfig(int index, JSONObject json) throws JSONException, IllegalArgumentException { - mIndex = index; - mType = ViewType.fromId(json.getString(JSON_KEY_TYPE)); - mDatasetId = json.getString(JSON_KEY_DATASET); - mItemType = ItemType.fromId(json.getString(JSON_KEY_ITEM_TYPE)); - mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER)); - mBackImageUrl = json.optString(JSON_KEY_BACK_IMAGE_URL, null); - mFilter = json.optString(JSON_KEY_FILTER, null); - - final JSONObject jsonEmptyViewConfig = json.optJSONObject(JSON_KEY_EMPTY); - if (jsonEmptyViewConfig != null) { - mEmptyViewConfig = new EmptyViewConfig(jsonEmptyViewConfig); - } else { - mEmptyViewConfig = null; - } - - final JSONObject jsonHeaderConfig = json.optJSONObject(JSON_KEY_HEADER); - mHeaderConfig = jsonHeaderConfig != null ? new HeaderConfig(jsonHeaderConfig) : null; - - mFlags = EnumSet.noneOf(Flags.class); - if (json.optBoolean(JSON_KEY_REFRESH_ENABLED, false)) { - mFlags.add(Flags.REFRESH_ENABLED); - } - - validate(); - } - - @SuppressWarnings("unchecked") - public ViewConfig(Parcel in) { - mIndex = in.readInt(); - mType = (ViewType) in.readParcelable(getClass().getClassLoader()); - mDatasetId = in.readString(); - mItemType = (ItemType) in.readParcelable(getClass().getClassLoader()); - mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader()); - mBackImageUrl = in.readString(); - mFilter = in.readString(); - mEmptyViewConfig = (EmptyViewConfig) in.readParcelable(getClass().getClassLoader()); - mHeaderConfig = (HeaderConfig) in.readParcelable(getClass().getClassLoader()); - mFlags = (EnumSet<Flags>) in.readSerializable(); - - validate(); - } - - public ViewConfig(ViewConfig viewConfig) { - mIndex = viewConfig.mIndex; - mType = viewConfig.mType; - mDatasetId = viewConfig.mDatasetId; - mItemType = viewConfig.mItemType; - mItemHandler = viewConfig.mItemHandler; - mBackImageUrl = viewConfig.mBackImageUrl; - mFilter = viewConfig.mFilter; - mEmptyViewConfig = viewConfig.mEmptyViewConfig; - mHeaderConfig = viewConfig.mHeaderConfig; - mFlags = viewConfig.mFlags.clone(); - - validate(); - } - - private void validate() { - if (mType == null) { - throw new IllegalArgumentException("Can't create ViewConfig with null type"); - } - - if (TextUtils.isEmpty(mDatasetId)) { - throw new IllegalArgumentException("Can't create ViewConfig with empty dataset ID"); - } - - if (mItemType == null) { - throw new IllegalArgumentException("Can't create ViewConfig with null item type"); - } - - if (mItemHandler == null) { - throw new IllegalArgumentException("Can't create ViewConfig with null item handler"); - } - - if (mFlags == null) { - throw new IllegalArgumentException("Can't create ViewConfig with null flags"); - } - } - - public int getIndex() { - return mIndex; - } - - public ViewType getType() { - return mType; - } - - public String getDatasetId() { - return mDatasetId; - } - - public ItemType getItemType() { - return mItemType; - } - - public ItemHandler getItemHandler() { - return mItemHandler; - } - - public String getBackImageUrl() { - return mBackImageUrl; - } - - public String getFilter() { - return mFilter; - } - - public EmptyViewConfig getEmptyViewConfig() { - return mEmptyViewConfig; - } - - public HeaderConfig getHeaderConfig() { - return mHeaderConfig; - } - - public boolean hasHeaderConfig() { - return mHeaderConfig != null; - } - - public boolean isRefreshEnabled() { - return mFlags.contains(Flags.REFRESH_ENABLED); - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - json.put(JSON_KEY_TYPE, mType.toString()); - json.put(JSON_KEY_DATASET, mDatasetId); - json.put(JSON_KEY_ITEM_TYPE, mItemType.toString()); - json.put(JSON_KEY_ITEM_HANDLER, mItemHandler.toString()); - - if (!TextUtils.isEmpty(mBackImageUrl)) { - json.put(JSON_KEY_BACK_IMAGE_URL, mBackImageUrl); - } - - if (!TextUtils.isEmpty(mFilter)) { - json.put(JSON_KEY_FILTER, mFilter); - } - - if (mEmptyViewConfig != null) { - json.put(JSON_KEY_EMPTY, mEmptyViewConfig.toJSON()); - } - - if (mHeaderConfig != null) { - json.put(JSON_KEY_HEADER, mHeaderConfig.toJSON()); - } - - if (mFlags.contains(Flags.REFRESH_ENABLED)) { - json.put(JSON_KEY_REFRESH_ENABLED, true); - } - - return json; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mIndex); - dest.writeParcelable(mType, 0); - dest.writeString(mDatasetId); - dest.writeParcelable(mItemType, 0); - dest.writeParcelable(mItemHandler, 0); - dest.writeString(mBackImageUrl); - dest.writeString(mFilter); - dest.writeParcelable(mEmptyViewConfig, 0); - dest.writeParcelable(mHeaderConfig, 0); - dest.writeSerializable(mFlags); - } - - public static final Creator<ViewConfig> CREATOR = new Creator<ViewConfig>() { - @Override - public ViewConfig createFromParcel(final Parcel in) { - return new ViewConfig(in); - } - - @Override - public ViewConfig[] newArray(final int size) { - return new ViewConfig[size]; - } - }; - } - - public static class EmptyViewConfig implements Parcelable { - private final String mText; - private final String mImageUrl; - - static final String JSON_KEY_TEXT = "text"; - static final String JSON_KEY_IMAGE_URL = "imageUrl"; - - public EmptyViewConfig(JSONObject json) throws JSONException, IllegalArgumentException { - mText = json.optString(JSON_KEY_TEXT, null); - mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null); - } - - public EmptyViewConfig(Parcel in) { - mText = in.readString(); - mImageUrl = in.readString(); - } - - public EmptyViewConfig(EmptyViewConfig emptyViewConfig) { - mText = emptyViewConfig.mText; - mImageUrl = emptyViewConfig.mImageUrl; - } - - public EmptyViewConfig(String text, String imageUrl) { - mText = text; - mImageUrl = imageUrl; - } - - public String getText() { - return mText; - } - - public String getImageUrl() { - return mImageUrl; - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - json.put(JSON_KEY_TEXT, mText); - json.put(JSON_KEY_IMAGE_URL, mImageUrl); - - return json; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mText); - dest.writeString(mImageUrl); - } - - public static final Creator<EmptyViewConfig> CREATOR = new Creator<EmptyViewConfig>() { - @Override - public EmptyViewConfig createFromParcel(final Parcel in) { - return new EmptyViewConfig(in); - } - - @Override - public EmptyViewConfig[] newArray(final int size) { - return new EmptyViewConfig[size]; - } - }; - } - - public static class HeaderConfig implements Parcelable { - static final String JSON_KEY_IMAGE_URL = "image_url"; - static final String JSON_KEY_URL = "url"; - - private final String mImageUrl; - private final String mUrl; - - public HeaderConfig(JSONObject json) { - mImageUrl = json.optString(JSON_KEY_IMAGE_URL); - mUrl = json.optString(JSON_KEY_URL); - } - - public HeaderConfig(Parcel in) { - mImageUrl = in.readString(); - mUrl = in.readString(); - } - - public String getImageUrl() { - return mImageUrl; - } - - public String getUrl() { - return mUrl; - } - - public JSONObject toJSON() throws JSONException { - JSONObject json = new JSONObject(); - - json.put(JSON_KEY_IMAGE_URL, mImageUrl); - json.put(JSON_KEY_URL, mUrl); - - return json; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mImageUrl); - dest.writeString(mUrl); - } - - public static final Creator<HeaderConfig> CREATOR = new Creator<HeaderConfig>() { - @Override - public HeaderConfig createFromParcel(Parcel source) { - return new HeaderConfig(source); - } - - @Override - public HeaderConfig[] newArray(int size) { - return new HeaderConfig[size]; - } - }; - } - - public static class AuthConfig implements Parcelable { - private final String mMessageText; - private final String mButtonText; - private final String mImageUrl; - - static final String JSON_KEY_MESSAGE_TEXT = "messageText"; - static final String JSON_KEY_BUTTON_TEXT = "buttonText"; - static final String JSON_KEY_IMAGE_URL = "imageUrl"; - - public AuthConfig(JSONObject json) throws JSONException, IllegalArgumentException { - mMessageText = json.optString(JSON_KEY_MESSAGE_TEXT); - mButtonText = json.optString(JSON_KEY_BUTTON_TEXT); - mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null); - } - - public AuthConfig(Parcel in) { - mMessageText = in.readString(); - mButtonText = in.readString(); - mImageUrl = in.readString(); - - validate(); - } - - public AuthConfig(AuthConfig authConfig) { - mMessageText = authConfig.mMessageText; - mButtonText = authConfig.mButtonText; - mImageUrl = authConfig.mImageUrl; - - validate(); - } - - public AuthConfig(String messageText, String buttonText, String imageUrl) { - mMessageText = messageText; - mButtonText = buttonText; - mImageUrl = imageUrl; - - validate(); - } - - private void validate() { - if (mMessageText == null) { - throw new IllegalArgumentException("Can't create AuthConfig with null message text"); - } - - if (mButtonText == null) { - throw new IllegalArgumentException("Can't create AuthConfig with null button text"); - } - } - - public String getMessageText() { - return mMessageText; - } - - public String getButtonText() { - return mButtonText; - } - - public String getImageUrl() { - return mImageUrl; - } - - public JSONObject toJSON() throws JSONException { - final JSONObject json = new JSONObject(); - - json.put(JSON_KEY_MESSAGE_TEXT, mMessageText); - json.put(JSON_KEY_BUTTON_TEXT, mButtonText); - json.put(JSON_KEY_IMAGE_URL, mImageUrl); - - return json; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mMessageText); - dest.writeString(mButtonText); - dest.writeString(mImageUrl); - } - - public static final Creator<AuthConfig> CREATOR = new Creator<AuthConfig>() { - @Override - public AuthConfig createFromParcel(final Parcel in) { - return new AuthConfig(in); - } - - @Override - public AuthConfig[] newArray(final int size) { - return new AuthConfig[size]; - } - }; - } - /** - * Immutable representation of the current state of {@code HomeConfig}. - * This is what HomeConfig returns from a load() call and takes as - * input to save a new state. - * - * Users of {@code State} should use an {@code Iterator} to iterate - * through the contained {@code PanelConfig} instances. - * - * {@code State} is immutable i.e. you can't add, remove, or update - * contained elements directly. You have to use an {@code Editor} to - * change the state, which can be created through the {@code edit()} - * method. - */ - public static class State implements Iterable<PanelConfig> { - private HomeConfig mHomeConfig; - private final List<PanelConfig> mPanelConfigs; - private final boolean mIsDefault; - - State(List<PanelConfig> panelConfigs, boolean isDefault) { - this(null, panelConfigs, isDefault); - } - - private State(HomeConfig homeConfig, List<PanelConfig> panelConfigs, boolean isDefault) { - mHomeConfig = homeConfig; - mPanelConfigs = Collections.unmodifiableList(panelConfigs); - mIsDefault = isDefault; - } - - private void setHomeConfig(HomeConfig homeConfig) { - if (mHomeConfig != null) { - throw new IllegalStateException("Can't set HomeConfig more than once"); - } - - mHomeConfig = homeConfig; - } - - @Override - public Iterator<PanelConfig> iterator() { - return mPanelConfigs.iterator(); - } - - /** - * Returns whether this {@code State} instance represents the default - * {@code HomeConfig} configuration or not. - */ - public boolean isDefault() { - return mIsDefault; - } - - /** - * Creates an {@code Editor} for this state. - */ - public Editor edit() { - return new Editor(mHomeConfig, this); - } - } - - /** - * {@code Editor} allows you to make changes to a {@code State}. You - * can create {@code Editor} by calling {@code edit()} on the target - * {@code State} instance. - * - * {@code Editor} works on a copy of the {@code State} that originated - * it. This means that adding, removing, or updating panels in an - * {@code Editor} will never change the {@code State} which you - * created the {@code Editor} from. Calling {@code commit()} or - * {@code apply()} will cause the new {@code State} instance to be - * created and saved using the {@code HomeConfig} instance that - * created the source {@code State}. - * - * {@code Editor} is *not* thread-safe. You can only make calls on it - * from the thread where it was originally created. It will throw an - * exception if you don't follow this invariant. - */ - public static class Editor implements Iterable<PanelConfig> { - private final HomeConfig mHomeConfig; - private final Map<String, PanelConfig> mConfigMap; - private final List<String> mConfigOrder; - private final Thread mOriginalThread; - - // Each Pair represents parameters to a GeckoAppShell.notifyObservers call; - // the first String is the observer topic and the second string is the notification data. - private List<Pair<String, String>> mNotificationQueue; - private PanelConfig mDefaultPanel; - private int mEnabledCount; - - private boolean mHasChanged; - private final boolean mIsFromDefault; - - private Editor(HomeConfig homeConfig, State configState) { - mHomeConfig = homeConfig; - mOriginalThread = Thread.currentThread(); - mConfigMap = new HashMap<String, PanelConfig>(); - mConfigOrder = new LinkedList<String>(); - mNotificationQueue = new ArrayList<>(); - - mIsFromDefault = configState.isDefault(); - - initFromState(configState); - } - - /** - * Initialize the initial state of the editor from the given - * {@sode State}. A HashMap is used to represent the list of - * panels as it provides fast access, and a LinkedList is used to - * keep track of order. We keep a reference to the default panel - * and the number of enabled panels to avoid iterating through the - * map every time we need those. - * - * @param configState The source State to load the editor from. - */ - private void initFromState(State configState) { - for (PanelConfig panelConfig : configState) { - final PanelConfig panelCopy = new PanelConfig(panelConfig); - - if (!panelCopy.isDisabled()) { - mEnabledCount++; - } - - if (panelCopy.isDefault()) { - if (mDefaultPanel == null) { - mDefaultPanel = panelCopy; - } else { - throw new IllegalStateException("Multiple default panels in HomeConfig state"); - } - } - - final String panelId = panelConfig.getId(); - mConfigOrder.add(panelId); - mConfigMap.put(panelId, panelCopy); - } - - // We should always have a defined default panel if there's - // at least one enabled panel around. - if (mEnabledCount > 0 && mDefaultPanel == null) { - throw new IllegalStateException("Default panel in HomeConfig state is undefined"); - } - } - - private PanelConfig getPanelOrThrow(String panelId) { - final PanelConfig panelConfig = mConfigMap.get(panelId); - if (panelConfig == null) { - throw new IllegalStateException("Tried to access non-existing panel: " + panelId); - } - - return panelConfig; - } - - private boolean isCurrentDefaultPanel(PanelConfig panelConfig) { - if (mDefaultPanel == null) { - return false; - } - - return mDefaultPanel.equals(panelConfig); - } - - private void findNewDefault() { - // Pick the first panel that is neither disabled nor currently - // set as default. - for (PanelConfig panelConfig : makeOrderedCopy(false)) { - if (!panelConfig.isDefault() && !panelConfig.isDisabled()) { - setDefault(panelConfig.getId()); - return; - } - } - - mDefaultPanel = null; - } - - /** - * Makes an ordered list of PanelConfigs that can be references - * or deep copied objects. - * - * @param deepCopy true to make deep-copied objects - * @return ordered List of PanelConfigs - */ - private List<PanelConfig> makeOrderedCopy(boolean deepCopy) { - final List<PanelConfig> copiedList = new ArrayList<PanelConfig>(mConfigOrder.size()); - for (String panelId : mConfigOrder) { - PanelConfig panelConfig = mConfigMap.get(panelId); - if (deepCopy) { - panelConfig = new PanelConfig(panelConfig); - } - copiedList.add(panelConfig); - } - - return copiedList; - } - - private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) { - if (panelConfig.isDisabled() == disabled) { - return; - } - - panelConfig.setIsDisabled(disabled); - mEnabledCount += (disabled ? -1 : 1); - } - - /** - * Gets the ID of the current default panel. - */ - public String getDefaultPanelId() { - ThreadUtils.assertOnThread(mOriginalThread); - - if (mDefaultPanel == null) { - return null; - } - - return mDefaultPanel.getId(); - } - - /** - * Set a new default panel. - * - * @param panelId the ID of the new default panel. - */ - public void setDefault(String panelId) { - ThreadUtils.assertOnThread(mOriginalThread); - - final PanelConfig panelConfig = getPanelOrThrow(panelId); - if (isCurrentDefaultPanel(panelConfig)) { - return; - } - - if (mDefaultPanel != null) { - mDefaultPanel.setIsDefault(false); - } - - panelConfig.setIsDefault(true); - setPanelIsDisabled(panelConfig, false); - - mDefaultPanel = panelConfig; - mHasChanged = true; - } - - /** - * Toggles disabled state for a panel. - * - * @param panelId the ID of the target panel. - * @param disabled true to disable the panel. - */ - public void setDisabled(String panelId, boolean disabled) { - ThreadUtils.assertOnThread(mOriginalThread); - - final PanelConfig panelConfig = getPanelOrThrow(panelId); - if (panelConfig.isDisabled() == disabled) { - return; - } - - setPanelIsDisabled(panelConfig, disabled); - - if (disabled) { - if (isCurrentDefaultPanel(panelConfig)) { - panelConfig.setIsDefault(false); - findNewDefault(); - } - } else if (mEnabledCount == 1) { - setDefault(panelId); - } - - mHasChanged = true; - } - - /** - * Adds a new {@code PanelConfig}. It will do nothing if the - * {@code Editor} already contains a panel with the same ID. - * - * @param panelConfig the {@code PanelConfig} instance to be added. - * @return true if the item has been added. - */ - public boolean install(PanelConfig panelConfig) { - ThreadUtils.assertOnThread(mOriginalThread); - - if (panelConfig == null) { - throw new IllegalStateException("Can't install a null panel"); - } - - if (!panelConfig.isDynamic()) { - throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId()); - } - - if (panelConfig.isDisabled()) { - throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId()); - } - - boolean installed = false; - - final String id = panelConfig.getId(); - if (!mConfigMap.containsKey(id)) { - mConfigMap.put(id, panelConfig); - - final int position = panelConfig.getPosition(); - if (position < 0 || position >= mConfigOrder.size()) { - mConfigOrder.add(id); - } else { - mConfigOrder.add(position, id); - } - - mEnabledCount++; - if (mEnabledCount == 1 || panelConfig.isDefault()) { - setDefault(panelConfig.getId()); - } - - installed = true; - - // Add an event to the queue if a new panel is successfully installed. - mNotificationQueue.add(new Pair<String, String>( - "HomePanels:Installed", panelConfig.getId())); - } - - mHasChanged = true; - return installed; - } - - /** - * Removes an existing panel. - * - * @return true if the item has been removed. - */ - public boolean uninstall(String panelId) { - ThreadUtils.assertOnThread(mOriginalThread); - - final PanelConfig panelConfig = mConfigMap.get(panelId); - if (panelConfig == null) { - return false; - } - - if (!panelConfig.isDynamic()) { - throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId()); - } - - mConfigMap.remove(panelId); - mConfigOrder.remove(panelId); - - if (!panelConfig.isDisabled()) { - mEnabledCount--; - } - - if (isCurrentDefaultPanel(panelConfig)) { - findNewDefault(); - } - - // Add an event to the queue if a panel is successfully uninstalled. - mNotificationQueue.add(new Pair<String, String>("HomePanels:Uninstalled", panelId)); - - mHasChanged = true; - return true; - } - - /** - * Moves panel associated with panelId to the specified position. - * - * @param panelId Id of panel - * @param destIndex Destination position - * @return true if move succeeded - */ - public boolean moveTo(String panelId, int destIndex) { - ThreadUtils.assertOnThread(mOriginalThread); - - if (!mConfigOrder.contains(panelId)) { - return false; - } - - mConfigOrder.remove(panelId); - mConfigOrder.add(destIndex, panelId); - mHasChanged = true; - - return true; - } - - /** - * Replaces an existing panel with a new {@code PanelConfig} instance. - * - * @return true if the item has been updated. - */ - public boolean update(PanelConfig panelConfig) { - ThreadUtils.assertOnThread(mOriginalThread); - - if (panelConfig == null) { - throw new IllegalStateException("Can't update a null panel"); - } - - boolean updated = false; - - final String id = panelConfig.getId(); - if (mConfigMap.containsKey(id)) { - final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig); - - // The disabled and default states can't never be - // changed by an update operation. - panelConfig.setIsDefault(oldPanelConfig.isDefault()); - panelConfig.setIsDisabled(oldPanelConfig.isDisabled()); - - updated = true; - } - - mHasChanged = true; - return updated; - } - - /** - * Saves the current {@code Editor} state asynchronously in the - * background thread. - * - * @return the resulting {@code State} instance. - */ - public State apply() { - ThreadUtils.assertOnThread(mOriginalThread); - - // We're about to save the current state in the background thread - // so we should use a deep copy of the PanelConfig instances to - // avoid saving corrupted state. - final State newConfigState = - new State(mHomeConfig, makeOrderedCopy(true), isDefault()); - - // Copy the event queue to a new list, so that we only modify mNotificationQueue on - // the original thread where it was created. - final List<Pair<String, String>> copiedQueue = mNotificationQueue; - mNotificationQueue = new ArrayList<>(); - - ThreadUtils.getBackgroundHandler().post(new Runnable() { - @Override - public void run() { - mHomeConfig.save(newConfigState); - - // Send pending events after the new config is saved. - sendNotificationsToGecko(copiedQueue); - } - }); - - return newConfigState; - } - - /** - * Saves the current {@code Editor} state synchronously in the - * current thread. - * - * @return the resulting {@code State} instance. - */ - public State commit() { - ThreadUtils.assertOnThread(mOriginalThread); - - final State newConfigState = - new State(mHomeConfig, makeOrderedCopy(false), isDefault()); - - // This is a synchronous blocking operation, hence no - // need to deep copy the current PanelConfig instances. - mHomeConfig.save(newConfigState); - - // Send pending events after the new config is saved. - sendNotificationsToGecko(mNotificationQueue); - mNotificationQueue.clear(); - - return newConfigState; - } - - /** - * Returns whether the {@code Editor} represents the default - * {@code HomeConfig} configuration without any unsaved changes. - */ - public boolean isDefault() { - ThreadUtils.assertOnThread(mOriginalThread); - - return (!mHasChanged && mIsFromDefault); - } - - public boolean isEmpty() { - return mConfigMap.isEmpty(); - } - - private void sendNotificationsToGecko(List<Pair<String, String>> notifications) { - for (Pair<String, String> p : notifications) { - GeckoAppShell.notifyObservers(p.first, p.second); - } - } - - private class EditorIterator implements Iterator<PanelConfig> { - private final Iterator<String> mOrderIterator; - - public EditorIterator() { - mOrderIterator = mConfigOrder.iterator(); - } - - @Override - public boolean hasNext() { - return mOrderIterator.hasNext(); - } - - @Override - public PanelConfig next() { - final String panelId = mOrderIterator.next(); - return mConfigMap.get(panelId); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Can't 'remove' from on Editor iterator."); - } - } - - @Override - public Iterator<PanelConfig> iterator() { - ThreadUtils.assertOnThread(mOriginalThread); - - return new EditorIterator(); - } - } - - public interface OnReloadListener { - public void onReload(); - } - - public interface HomeConfigBackend { - public State load(); - public void save(State configState); - public String getLocale(); - public void setOnReloadListener(OnReloadListener listener); - } - - // UUIDs used to create PanelConfigs for default built-in panels. These are - // public because they can be used in "about:home?panel=UUID" query strings - // to open specific panels without querying the active Home Panel - // configuration. Because they don't consider the active configuration, it - // is only sensible to do this for built-in panels (and not for dynamic - // panels). - private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e"; - private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907"; - private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8"; - private static final String COMBINED_HISTORY_PANEL_ID = "4d716ce2-e063-486d-9e7c-b190d7b04dc6"; - private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8"; - private static final String REMOTE_TABS_PANEL_ID = "72429afd-8d8b-43d8-9189-14b779c563d0"; - private static final String DEPRECATED_READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b"; - - private final HomeConfigBackend mBackend; - - public HomeConfig(HomeConfigBackend backend) { - mBackend = backend; - } - - public State load() { - final State configState = mBackend.load(); - configState.setHomeConfig(this); - - return configState; - } - - public String getLocale() { - return mBackend.getLocale(); - } - - public void save(State configState) { - mBackend.save(configState); - } - - public void setOnReloadListener(OnReloadListener listener) { - mBackend.setOnReloadListener(listener); - } - - public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) { - return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class)); - } - - public static int getTitleResourceIdForBuiltinPanelType(PanelType panelType) { - switch (panelType) { - case TOP_SITES: - return R.string.home_top_sites_title; - - case BOOKMARKS: - case DEPRECATED_READING_LIST: - return R.string.bookmarks_title; - - case DEPRECATED_HISTORY: - case DEPRECATED_REMOTE_TABS: - case DEPRECATED_RECENT_TABS: - case COMBINED_HISTORY: - return R.string.home_history_title; - - default: - throw new IllegalArgumentException("Only for built-in panel types: " + panelType); - } - } - - public static String getIdForBuiltinPanelType(PanelType panelType) { - switch (panelType) { - case TOP_SITES: - return TOP_SITES_PANEL_ID; - - case BOOKMARKS: - return BOOKMARKS_PANEL_ID; - - case DEPRECATED_HISTORY: - return HISTORY_PANEL_ID; - - case COMBINED_HISTORY: - return COMBINED_HISTORY_PANEL_ID; - - case DEPRECATED_REMOTE_TABS: - return REMOTE_TABS_PANEL_ID; - - case DEPRECATED_READING_LIST: - return DEPRECATED_READING_LIST_PANEL_ID; - - case DEPRECATED_RECENT_TABS: - return RECENT_TABS_PANEL_ID; - - default: - throw new IllegalArgumentException("Only for built-in panel types: " + panelType); - } - } - - public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType, EnumSet<PanelConfig.Flags> flags) { - final int titleId = getTitleResourceIdForBuiltinPanelType(panelType); - final String id = getIdForBuiltinPanelType(panelType); - - return new PanelConfig(panelType, context.getString(titleId), id, flags); - } - - public static HomeConfig getDefault(Context context) { - return new HomeConfig(new HomeConfigPrefsBackend(context)); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java deleted file mode 100644 index 914d0fdd1..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigLoader.java +++ /dev/null @@ -1,83 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java deleted file mode 100644 index a2d80788c..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java +++ /dev/null @@ -1,663 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java deleted file mode 100644 index cefa0329d..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeContextMenuInfo.java +++ /dev/null @@ -1,82 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java deleted file mode 100644 index 7badd6929..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeExpandableListView.java +++ /dev/null @@ -1,68 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java deleted file mode 100644 index da6e9b703..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java +++ /dev/null @@ -1,498 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java deleted file mode 100644 index d179a27ce..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeListView.java +++ /dev/null @@ -1,138 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java b/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java deleted file mode 100644 index 4915f0c91..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomePager.java +++ /dev/null @@ -1,564 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java b/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java deleted file mode 100644 index bfd6c5624..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java +++ /dev/null @@ -1,368 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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(); - } - }; -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java b/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java deleted file mode 100644 index 1525969a0..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/HomeScreen.java +++ /dev/null @@ -1,57 +0,0 @@ -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(); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java deleted file mode 100644 index 2bbd82a8d..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java +++ /dev/null @@ -1,164 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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"); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java deleted file mode 100644 index 26edf13ff..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/MultiTypeCursorAdapter.java +++ /dev/null @@ -1,100 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java deleted file mode 100644 index d66919344..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthCache.java +++ /dev/null @@ -1,82 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java deleted file mode 100644 index 1ad91b7ca..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java +++ /dev/null @@ -1,63 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java deleted file mode 100644 index 4772e08ab..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelBackItemView.java +++ /dev/null @@ -1,48 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java deleted file mode 100644 index 50c4dbc07..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelHeaderView.java +++ /dev/null @@ -1,28 +0,0 @@ -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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java deleted file mode 100644 index 089e17837..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java +++ /dev/null @@ -1,162 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java deleted file mode 100644 index 2a97d42bc..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java +++ /dev/null @@ -1,136 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java deleted file mode 100644 index 2c2d89ae0..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java +++ /dev/null @@ -1,747 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java deleted file mode 100644 index 505fb9b0d..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelListView.java +++ /dev/null @@ -1,83 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java deleted file mode 100644 index 9145ab1e1..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerView.java +++ /dev/null @@ -1,178 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java deleted file mode 100644 index fa632bccd..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRecyclerViewAdapter.java +++ /dev/null @@ -1,137 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java deleted file mode 100644 index d43a97f31..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java +++ /dev/null @@ -1,90 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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()); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java deleted file mode 100644 index cf03c50c0..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewAdapter.java +++ /dev/null @@ -1,113 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java b/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java deleted file mode 100644 index a69db0b41..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PanelViewItemHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java b/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java deleted file mode 100644 index 230b1d329..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/PinSiteDialog.java +++ /dev/null @@ -1,256 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java deleted file mode 100755 index 3091f77da..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java +++ /dev/null @@ -1,454 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java b/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java deleted file mode 100644 index 43497ae6c..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsExpandableListState.java +++ /dev/null @@ -1,163 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java deleted file mode 100644 index 9b2d2746a..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java +++ /dev/null @@ -1,102 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} - diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java deleted file mode 100644 index be5b3b461..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineAdapter.java +++ /dev/null @@ -1,122 +0,0 @@ -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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java deleted file mode 100644 index 6a6509bcb..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineBar.java +++ /dev/null @@ -1,148 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java deleted file mode 100644 index 5b97a8f5f..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java +++ /dev/null @@ -1,494 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java deleted file mode 100644 index f7b5b6586..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SearchLoader.java +++ /dev/null @@ -1,114 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java b/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java deleted file mode 100644 index b8889c033..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SimpleCursorLoader.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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; - } -}
\ No newline at end of file diff --git a/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java b/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java deleted file mode 100644 index 039b65e82..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/SpacingDecoration.java +++ /dev/null @@ -1,20 +0,0 @@ -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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java b/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java deleted file mode 100644 index b302d3522..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStrip.java +++ /dev/null @@ -1,127 +0,0 @@ -/* -*- 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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java b/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java deleted file mode 100644 index a09add80b..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TabMenuStripLayout.java +++ /dev/null @@ -1,246 +0,0 @@ -/* -*- 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); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java deleted file mode 100644 index c17aff209..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridItemView.java +++ /dev/null @@ -1,312 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java deleted file mode 100644 index 58a05b198..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesGridView.java +++ /dev/null @@ -1,169 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java deleted file mode 100644 index f39e51ac5..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java +++ /dev/null @@ -1,968 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java deleted file mode 100644 index dd45014b0..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesThumbnailView.java +++ /dev/null @@ -1,102 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java deleted file mode 100644 index 68eb8daa5..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java +++ /dev/null @@ -1,324 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java deleted file mode 100644 index ef0c105d3..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java +++ /dev/null @@ -1,145 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - package org.mozilla.gecko.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); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java deleted file mode 100644 index 09f6705d7..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java +++ /dev/null @@ -1,39 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java deleted file mode 100644 index 4decc8218..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java +++ /dev/null @@ -1,73 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java deleted file mode 100644 index 24348dfe0..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java +++ /dev/null @@ -1,196 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java deleted file mode 100644 index f7cda2e7f..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java +++ /dev/null @@ -1,135 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.gecko.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); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java deleted file mode 100644 index 525d3b426..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java +++ /dev/null @@ -1,239 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java deleted file mode 100644 index e95867c36..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java +++ /dev/null @@ -1,102 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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(); - } - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java deleted file mode 100644 index 56615937b..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java +++ /dev/null @@ -1,76 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java deleted file mode 100644 index 096f0c597..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java +++ /dev/null @@ -1,568 +0,0 @@ -/* - * 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]; - } - }; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java deleted file mode 100644 index b436a466f..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java +++ /dev/null @@ -1,105 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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()); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java deleted file mode 100644 index 45fdc0d1a..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java +++ /dev/null @@ -1,38 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java deleted file mode 100644 index 29e6aca3d..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java +++ /dev/null @@ -1,117 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java deleted file mode 100644 index dc824d902..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java +++ /dev/null @@ -1,124 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.gecko.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(); - } -} |