/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; import org.mozilla.gecko.AwesomeBar.ContextMenuSubject; import org.mozilla.gecko.db.BrowserContract.Bookmarks; import org.mozilla.gecko.db.BrowserContract.Combined; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.BrowserDB.URLColumns; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.widget.FaviconView; import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.os.AsyncTask; import android.util.Log; import android.util.Pair; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; import java.util.LinkedList; public class BookmarksTab extends AwesomeBarTab { public static final String LOGTAG = "BOOKMARKS_TAB"; public static final String TAG = "bookmarks"; private int mFolderId; private String mFolderTitle; private BookmarksListAdapter mCursorAdapter = null; private BookmarksQueryTask mQueryTask = null; private boolean mShowReadingList = false; @Override public int getTitleStringId() { return R.string.awesomebar_bookmarks_title; } @Override public String getTag() { return TAG; } public BookmarksTab(Context context) { super(context); } @Override public View getView() { if (mView == null) { mView = new ListView(mContext, null); ((Activity)mContext).registerForContextMenu(mView); mView.setTag(TAG); mView.setOnTouchListener(mListListener); // We need to add the header before we set the adapter, hence make it null ListView list = (ListView)mView; list.setAdapter(null); list.setAdapter(getCursorAdapter()); list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { handleItemClick(parent, view, position, id); } }); if (mShowReadingList) { String title = getResources().getString(R.string.bookmarks_folder_reading_list); getCursorAdapter().moveToChildFolder(Bookmarks.FIXED_READING_LIST_ID, title); } else { BookmarksQueryTask task = getQueryTask(); task.execute(); } } return (ListView)mView; } public void setShowReadingList(boolean showReadingList) { mShowReadingList = showReadingList; } @Override public void destroy() { super.destroy(); // Can't use getters for adapter. It will create one if null. if (mCursorAdapter != null && mView != null) { ListView list = (ListView)mView; list.setAdapter(null); final Cursor cursor = mCursorAdapter.getCursor(); // Gingerbread locks the DB when closing a cursor, so do it in the // background. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { if (cursor != null && !cursor.isClosed()) cursor.close(); } }); } } @Override public boolean onBackPressed() { // If the soft keyboard is visible in the bookmarks or history tab, the user // must have explictly brought it up, so we should try hiding it instead of // exiting the activity or going up a bookmarks folder level. if (hideSoftInput(getView())) return true; return moveToParentFolder(); } protected BookmarksListAdapter getCursorAdapter() { return getCursorAdapter(null); } protected BookmarksListAdapter getCursorAdapter(Cursor c) { if (mCursorAdapter == null) { mCursorAdapter = new BookmarksListAdapter(mContext, c); } else if (c != null) { mCursorAdapter.changeCursor(c); } else { // do a quick return if just asking for the cached adapter return mCursorAdapter; } TextView headerView = mCursorAdapter.getHeaderView(); if (headerView == null) { headerView = (TextView) getInflater().inflate(R.layout.awesomebar_header_row, null); mCursorAdapter.setHeaderView(headerView); } // Add/Remove header based on the root folder if (mView != null) { ListView list = (ListView)mView; if (mFolderId == Bookmarks.FIXED_ROOT_ID) { if (list.getHeaderViewsCount() == 1) { list.removeHeaderView(headerView); } } else { if (list.getHeaderViewsCount() == 0) { list.addHeaderView(headerView, null, true); } headerView.setText(mFolderTitle); } } return mCursorAdapter; } protected BookmarksQueryTask getQueryTask() { if (mQueryTask == null) { mQueryTask = new BookmarksQueryTask(); } return mQueryTask; } public void handleItemClick(AdapterView parent, View view, int position, long id) { ListView list = (ListView)getView(); if (list == null) return; int headerCount = list.getHeaderViewsCount(); // If we tap on the header view, there's nothing to do if (headerCount == 1 && position == 0) return; BookmarksListAdapter adapter = getCursorAdapter(); if (adapter == null) return; Cursor cursor = adapter.getCursor(); if (cursor == null) return; // The header view takes up a spot in the list if (headerCount == 1) position--; cursor.moveToPosition(position); int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE)); if (type == Bookmarks.TYPE_FOLDER) { // If we're clicking on a folder, update adapter to move to that folder int folderId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); String folderTitle = adapter.getFolderTitle(position); adapter.moveToChildFolder(folderId, folderTitle); return; } // Otherwise, just open the URL String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); String title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); long parentId = cursor.getLong(cursor.getColumnIndexOrThrow(Bookmarks.PARENT)); if (parentId == Bookmarks.FIXED_READING_LIST_ID) { url = ReaderModeUtils.getAboutReaderForUrl(url, true); } sendToListener(url, title); } private class BookmarksListAdapter extends SimpleCursorAdapter { private static final int VIEW_TYPE_ITEM = 0; private static final int VIEW_TYPE_FOLDER = 1; private static final int VIEW_TYPE_COUNT = 2; private LinkedList> mParentStack; private TextView mBookmarksTitleView; public BookmarksListAdapter(Context context, Cursor c) { super(context, -1, c, new String[] {}, new int[] {}); // mParentStack holds folder id/title pairs that allow us to navigate // back up the folder heirarchy mParentStack = new LinkedList>(); // Add the root folder to the stack Pair rootFolder = new Pair(Bookmarks.FIXED_ROOT_ID, ""); mParentStack.addFirst(rootFolder); } public void refreshCurrentFolder() { // Cancel any pre-existing async refresh tasks if (mQueryTask != null) mQueryTask.cancel(false); Pair folderPair = mParentStack.getFirst(); mQueryTask = new BookmarksQueryTask(folderPair.first, folderPair.second); mQueryTask.execute(); } // Returns false if there is no parent folder to move to public boolean moveToParentFolder() { // If we're already at the root, we can't move to a parent folder if (mParentStack.size() == 1) return false; mParentStack.removeFirst(); refreshCurrentFolder(); return true; } public void moveToChildFolder(int folderId, String folderTitle) { Pair folderPair = new Pair(folderId, folderTitle); mParentStack.addFirst(folderPair); refreshCurrentFolder(); } public boolean isInReadingList() { Pair folderPair = mParentStack.getFirst(); return (folderPair.first == Bookmarks.FIXED_READING_LIST_ID); } @Override public int getItemViewType(int position) { Cursor c = getCursor(); if (c.moveToPosition(position) && c.getInt(c.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER) return VIEW_TYPE_FOLDER; // Default to retuning normal item type return VIEW_TYPE_ITEM; } @Override public int getViewTypeCount() { return VIEW_TYPE_COUNT; } public String getFolderTitle(int position) { Cursor c = getCursor(); if (!c.moveToPosition(position)) return ""; String guid = c.getString(c.getColumnIndexOrThrow(Bookmarks.GUID)); // If we don't have a special GUID, just return the folder title from the DB. if (guid == null || guid.length() == 12) return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE)); // Use localized strings for special folder names. if (guid.equals(Bookmarks.FAKE_DESKTOP_FOLDER_GUID)) return getResources().getString(R.string.bookmarks_folder_desktop); else if (guid.equals(Bookmarks.MENU_FOLDER_GUID)) return getResources().getString(R.string.bookmarks_folder_menu); else if (guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID)) return getResources().getString(R.string.bookmarks_folder_toolbar); else if (guid.equals(Bookmarks.UNFILED_FOLDER_GUID)) return getResources().getString(R.string.bookmarks_folder_unfiled); else if (guid.equals(Bookmarks.READING_LIST_FOLDER_GUID)) return getResources().getString(R.string.bookmarks_folder_reading_list); // If for some reason we have a folder with a special GUID, but it's not one of // the special folders we expect in the UI, just return the title from the DB. return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE)); } @Override public View getView(int position, View convertView, ViewGroup parent) { int viewType = getItemViewType(position); AwesomeEntryViewHolder viewHolder = null; if (convertView == null) { if (viewType == VIEW_TYPE_ITEM) convertView = getInflater().inflate(R.layout.awesomebar_row, null); else convertView = getInflater().inflate(R.layout.awesomebar_folder_row, null); viewHolder = new AwesomeEntryViewHolder(); viewHolder.titleView = (TextView) convertView.findViewById(R.id.title); viewHolder.faviconView = (FaviconView) convertView.findViewById(R.id.favicon); if (viewType == VIEW_TYPE_ITEM) viewHolder.urlView = (TextView) convertView.findViewById(R.id.url); convertView.setTag(viewHolder); } else { viewHolder = (AwesomeEntryViewHolder) convertView.getTag(); } Cursor cursor = getCursor(); if (!cursor.moveToPosition(position)) throw new IllegalStateException("Couldn't move cursor to position " + position); if (viewType == VIEW_TYPE_ITEM) { updateTitle(viewHolder.titleView, cursor); updateUrl(viewHolder, cursor); byte[] b = cursor.getBlob(cursor.getColumnIndexOrThrow(URLColumns.FAVICON)); Bitmap favicon = null; if (b != null && b.length > 0) { Bitmap bitmap = BitmapUtils.decodeByteArray(b); if (bitmap != null) { favicon = Favicons.getInstance().scaleImage(bitmap); } } String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); updateFavicon(viewHolder.faviconView, favicon, url); } else { viewHolder.titleView.setText(getFolderTitle(position)); } return convertView; } public TextView getHeaderView() { return mBookmarksTitleView; } public void setHeaderView(TextView titleView) { mBookmarksTitleView = titleView; } } private class BookmarksQueryTask extends AsyncTask { public BookmarksQueryTask() { mFolderId = Bookmarks.FIXED_ROOT_ID; mFolderTitle = ""; } public BookmarksQueryTask(int folderId, String folderTitle) { mFolderId = folderId; mFolderTitle = folderTitle; } @Override protected Cursor doInBackground(Void... arg0) { return BrowserDB.getBookmarksInFolder(getContentResolver(), mFolderId); } @Override protected void onPostExecute(final Cursor cursor) { // Hack: force this to the main thread, even though it should already be on it ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { // this will update the cursorAdapter to use the new one if it already exists // We need to add the header before we set the adapter, hence make it null ListView list = (ListView)mView; list.setAdapter(null); list.setAdapter(getCursorAdapter(cursor)); } }); mQueryTask = null; } } public boolean moveToParentFolder() { // If we're not in the bookmarks tab, we have nothing to do. We should // also return false if mBookmarksAdapter hasn't been initialized yet. BookmarksListAdapter adapter = getCursorAdapter(); if (adapter == null) return false; return adapter.moveToParentFolder(); } /** * Whether the user is in the Reading List bookmarks directory in the * AwesomeScreen UI. */ public boolean isInReadingList() { if (mCursorAdapter == null) return false; return mCursorAdapter.isInReadingList(); } @Override public ContextMenuSubject getSubject(ContextMenu menu, View view, ContextMenuInfo menuInfo) { ContextMenuSubject subject = null; if (!(menuInfo instanceof AdapterView.AdapterContextMenuInfo)) { Log.e(LOGTAG, "menuInfo is not AdapterContextMenuInfo"); return subject; } ListView list = (ListView)view; AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; Object selectedItem = list.getItemAtPosition(info.position); if (!(selectedItem instanceof Cursor)) { Log.e(LOGTAG, "item at " + info.position + " is not a Cursor"); return subject; } Cursor cursor = (Cursor) selectedItem; // Don't show the context menu for folders if (!(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER)) { String keyword = null; int keywordCol = cursor.getColumnIndex(URLColumns.KEYWORD); if (keywordCol != -1) keyword = cursor.getString(keywordCol); int id = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)); subject = new ContextMenuSubject(id, cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)), cursor.getBlob(cursor.getColumnIndexOrThrow(URLColumns.FAVICON)), cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)), keyword, isInReadingList() ? Combined.DISPLAY_READER : Combined.DISPLAY_NORMAL); } if (subject == null) return subject; setupMenu(menu, subject); menu.findItem(R.id.remove_history).setVisible(false); menu.findItem(R.id.open_in_reader).setVisible(false); return subject; } }