From 68b37c80c5bc09206e4997279723d532776ffa21 Mon Sep 17 00:00:00 2001 From: Sriram Ramasubramanian Date: Mon, 25 Mar 2013 15:45:33 -0700 Subject: [PATCH] Bug 852312: Split views in about:home [r=bnicholson] --HG-- rename : mobile/android/base/AboutHomeContent.java => mobile/android/base/widget/AboutHomeContent.java rename : mobile/android/base/AboutHomeSection.java => mobile/android/base/widget/AboutHomeSection.java rename : mobile/android/base/AboutHomePromoBox.java => mobile/android/base/widget/PromoBox.java extra : rebase_source : 0db7d60aa0c57eb9aeb9cad3e1f297b0ec0ddd0f --- mobile/android/base/BrowserApp.java | 5 +- mobile/android/base/GeckoViewsFactory.java | 17 +- mobile/android/base/Makefile.in | 6 +- .../abouthome_content.xml | 70 +- .../resources/layout/abouthome_content.xml | 71 +- .../android/base/widget/AboutHomeContent.java | 976 +----------------- .../android/base/widget/AboutHomeSection.java | 19 +- mobile/android/base/widget/AddonsSection.java | 222 ++++ .../android/base/widget/LastTabsSection.java | 108 ++ .../{AboutHomePromoBox.java => PromoBox.java} | 10 +- .../base/widget/RemoteTabsSection.java | 100 ++ mobile/android/base/widget/TopSitesView.java | 628 +++++++++++ 12 files changed, 1209 insertions(+), 1023 deletions(-) create mode 100644 mobile/android/base/widget/AddonsSection.java create mode 100644 mobile/android/base/widget/LastTabsSection.java rename mobile/android/base/widget/{AboutHomePromoBox.java => PromoBox.java} (94%) create mode 100644 mobile/android/base/widget/RemoteTabsSection.java create mode 100644 mobile/android/base/widget/TopSitesView.java diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 229abd85d044..02d18a03f5e5 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -15,6 +15,7 @@ import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.UiAsyncTask; import org.mozilla.gecko.widget.AboutHomeContent; +import org.mozilla.gecko.widget.TopSitesView; import org.json.JSONArray; import org.json.JSONException; @@ -1513,7 +1514,7 @@ abstract public class BrowserApp extends GeckoApp return true; case R.id.abouthome_topsites_unpin: - mAboutHomeContent.unpinSite(info, AboutHomeContent.UnpinFlags.REMOVE_PIN); + mAboutHomeContent.unpinSite(info, TopSitesView.UnpinFlags.REMOVE_PIN); return true; case R.id.abouthome_topsites_pin: @@ -1521,7 +1522,7 @@ abstract public class BrowserApp extends GeckoApp return true; case R.id.abouthome_topsites_remove: - mAboutHomeContent.unpinSite(info, AboutHomeContent.UnpinFlags.REMOVE_HISTORY); + mAboutHomeContent.unpinSite(info, TopSitesView.UnpinFlags.REMOVE_HISTORY); return true; } diff --git a/mobile/android/base/GeckoViewsFactory.java b/mobile/android/base/GeckoViewsFactory.java index b6430c9b8771..c1e631406124 100644 --- a/mobile/android/base/GeckoViewsFactory.java +++ b/mobile/android/base/GeckoViewsFactory.java @@ -6,12 +6,15 @@ package org.mozilla.gecko; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.widget.AboutHomeContent; -import org.mozilla.gecko.widget.AboutHomePromoBox; -import org.mozilla.gecko.widget.AboutHomeSection; +import org.mozilla.gecko.widget.AddonsSection; import org.mozilla.gecko.widget.IconTabWidget; +import org.mozilla.gecko.widget.LastTabsSection; import org.mozilla.gecko.widget.LinkTextView; +import org.mozilla.gecko.widget.PromoBox; +import org.mozilla.gecko.widget.RemoteTabsSection; import org.mozilla.gecko.widget.TabRow; import org.mozilla.gecko.widget.ThumbnailView; +import org.mozilla.gecko.widget.TopSitesView; import android.content.Context; import android.text.TextUtils; @@ -21,8 +24,8 @@ import android.view.LayoutInflater; import android.view.View; import java.lang.reflect.Constructor; -import java.util.Map; import java.util.HashMap; +import java.util.Map; public final class GeckoViewsFactory implements LayoutInflater.Factory { private static final String LOGTAG = "GeckoViewsFactory"; @@ -43,10 +46,12 @@ public final class GeckoViewsFactory implements LayoutInflater.Factory { Class arg1Class = Context.class; Class arg2Class = AttributeSet.class; try { - mFactoryMap.put("AboutHomePromoBox", AboutHomePromoBox.class.getConstructor(arg1Class, arg2Class)); mFactoryMap.put("AboutHomeContent", AboutHomeContent.class.getConstructor(arg1Class, arg2Class)); - mFactoryMap.put("AboutHomeContent$TopSitesGridView", AboutHomeContent.TopSitesGridView.class.getConstructor(arg1Class, arg2Class)); - mFactoryMap.put("AboutHomeSection", AboutHomeSection.class.getConstructor(arg1Class, arg2Class)); + mFactoryMap.put("AddonsSection", AddonsSection.class.getConstructor(arg1Class, arg2Class)); + mFactoryMap.put("LastTabsSection", LastTabsSection.class.getConstructor(arg1Class, arg2Class)); + mFactoryMap.put("PromoBox", PromoBox.class.getConstructor(arg1Class, arg2Class)); + mFactoryMap.put("RemoteTabsSection", RemoteTabsSection.class.getConstructor(arg1Class, arg2Class)); + mFactoryMap.put("TopSitesView", TopSitesView.class.getConstructor(arg1Class, arg2Class)); mFactoryMap.put("AwesomeBarTabs", AwesomeBarTabs.class.getConstructor(arg1Class, arg2Class)); mFactoryMap.put("AwesomeBarTabs$BackgroundLayout", AwesomeBarTabs.BackgroundLayout.class.getConstructor(arg1Class, arg2Class)); mFactoryMap.put("BackButton", BackButton.class.getConstructor(arg1Class, arg2Class)); diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index a68a952af4c2..27d90a23ac66 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -198,11 +198,15 @@ FENNEC_JAVA_FILES = \ gfx/ViewTransform.java \ gfx/VirtualLayer.java \ widget/AboutHomeContent.java \ - widget/AboutHomePromoBox.java \ widget/AboutHomeSection.java \ + widget/AddonsSection.java \ widget/DateTimePicker.java \ widget/IconTabWidget.java \ + widget/LastTabsSection.java \ widget/LinkTextView.java \ + widget/PromoBox.java \ + widget/RemoteTabsSection.java \ + widget/TopSitesView.java \ widget/TabRow.java \ widget/ThumbnailView.java \ widget/TwoWayView.java \ diff --git a/mobile/android/base/resources/layout-xlarge-land-v11/abouthome_content.xml b/mobile/android/base/resources/layout-xlarge-land-v11/abouthome_content.xml index bf1b4b3ea334..86caec980a8f 100644 --- a/mobile/android/base/resources/layout-xlarge-land-v11/abouthome_content.xml +++ b/mobile/android/base/resources/layout-xlarge-land-v11/abouthome_content.xml @@ -36,24 +36,24 @@ android:textStyle="bold" android:gravity="left|center_vertical"/> - + - + @@ -78,26 +78,26 @@ android:layout_alignParentLeft="true" android:layout_marginTop="50dip"> - + - + - + diff --git a/mobile/android/base/resources/layout/abouthome_content.xml b/mobile/android/base/resources/layout/abouthome_content.xml index 4a891afdf586..698efad15b91 100644 --- a/mobile/android/base/resources/layout/abouthome_content.xml +++ b/mobile/android/base/resources/layout/abouthome_content.xml @@ -46,46 +46,45 @@ android:layout_height="fill_parent" android:layout_below="@id/top_sites_title"> - + - + - + - + - + diff --git a/mobile/android/base/widget/AboutHomeContent.java b/mobile/android/base/widget/AboutHomeContent.java index d7e25ba8af4c..e113fc63d128 100644 --- a/mobile/android/base/widget/AboutHomeContent.java +++ b/mobile/android/base/widget/AboutHomeContent.java @@ -5,68 +5,21 @@ package org.mozilla.gecko.widget; -import org.mozilla.gecko.AwesomeBar; import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.Favicons; -import org.mozilla.gecko.GeckoApp; -import org.mozilla.gecko.GeckoAppShell; -import org.mozilla.gecko.GeckoLinearLayout; import org.mozilla.gecko.LightweightTheme; import org.mozilla.gecko.LightweightThemeDrawable; import org.mozilla.gecko.R; -import org.mozilla.gecko.SessionParser; -import org.mozilla.gecko.Tabs; -import org.mozilla.gecko.TabsAccessor; -import org.mozilla.gecko.ThumbnailHelper; import org.mozilla.gecko.db.BrowserContract; -import org.mozilla.gecko.db.BrowserContract.Thumbnails; -import org.mozilla.gecko.db.BrowserDB; -import org.mozilla.gecko.db.BrowserDB.URLColumns; -import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper; -import org.mozilla.gecko.sync.setup.SyncAccounts; -import org.mozilla.gecko.util.ActivityResultHandler; -import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.gecko.util.UiAsyncTask; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.app.Activity; -import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.database.ContentObserver; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.PathShape; -import android.graphics.Path; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; import android.util.AttributeSet; -import android.util.Log; -import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; 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.AdapterView.AdapterContextMenuInfo; -import android.widget.AbsListView; -import android.widget.GridView; -import android.widget.ImageView; import android.widget.ScrollView; -import android.widget.SimpleCursorAdapter; -import android.widget.TextView; import android.widget.Toast; import java.io.File; @@ -77,26 +30,9 @@ import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; public class AboutHomeContent extends ScrollView - implements TabsAccessor.OnQueryTabsCompleteListener, - LightweightTheme.OnChangeListener { - private static final String LOGTAG = "GeckoAboutHome"; - - private static final int NUMBER_OF_REMOTE_TABS = 5; - - private static int mNumberOfTopSites; - private static int mNumberOfCols; - - public static enum UnpinFlags { - REMOVE_PIN, - REMOVE_HISTORY - } - + implements LightweightTheme.OnChangeListener { public static enum UpdateFlags { TOP_SITES, PREVIOUS_TABS, @@ -108,26 +44,15 @@ public class AboutHomeContent extends ScrollView private Context mContext; private BrowserApp mActivity; - UriLoadCallback mUriLoadCallback = null; - VoidCallback mLoadCompleteCallback = null; - private LayoutInflater mInflater; + private UriLoadCallback mUriLoadCallback = null; private ContentObserver mTabsContentObserver = null; - protected TopSitesCursorAdapter mTopSitesAdapter; - protected TopSitesGridView mTopSitesGrid; - - private AboutHomePromoBox mPromoBox; - protected AboutHomeSection mAddons; - protected AboutHomeSection mLastTabs; - protected AboutHomeSection mRemoteTabs; - - private View.OnClickListener mRemoteTabClickListener; - - private static Rect sIconBounds; - private static TextAppearanceSpan sSubTitleSpan; - private static Drawable sPinDrawable = null; - private int mThumbnailBackground; + protected TopSitesView mTopSites; + protected AddonsSection mAddons; + protected LastTabsSection mLastTabs; + protected RemoteTabsSection mRemoteTabs; + private PromoBox mPromoBox; public interface UriLoadCallback { public void callback(String uriSpec); @@ -150,11 +75,6 @@ public class AboutHomeContent extends ScrollView } public void init() { - int iconSize = mContext.getResources().getDimensionPixelSize(R.dimen.abouthome_addon_icon_size); - sIconBounds = new Rect(0, 0, iconSize, iconSize); - sSubTitleSpan = new TextAppearanceSpan(mContext, R.style.AboutHome_TextAppearance_SubTitle); - mThumbnailBackground = mContext.getResources().getColor(R.color.abouthome_thumbnail_bg); - inflate(); // Reload the mobile homepage on inbound tab syncs @@ -169,88 +89,22 @@ public class AboutHomeContent extends ScrollView }; mActivity.getContentResolver().registerContentObserver(BrowserContract.Tabs.CONTENT_URI, false, mTabsContentObserver); - - mRemoteTabClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - int flags = Tabs.LOADURL_NEW_TAB; - if (Tabs.getInstance().getSelectedTab().isPrivate()) - flags |= Tabs.LOADURL_PRIVATE; - Tabs.getInstance().loadUrl((String) v.getTag(), flags); - } - }; } private void inflate() { - mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mInflater.inflate(R.layout.abouthome_content, this); + LayoutInflater.from(mContext).inflate(R.layout.abouthome_content, this); - mTopSitesGrid = (TopSitesGridView)findViewById(R.id.top_sites_grid); - mTopSitesGrid.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View v, int position, long id) { - TopSitesViewHolder holder = (TopSitesViewHolder) v.getTag(); - String spec = holder.getUrl(); + mTopSites = (TopSitesView) findViewById(R.id.top_sites_grid); + mPromoBox = (PromoBox) findViewById(R.id.promo_box); + mAddons = (AddonsSection) findViewById(R.id.recommended_addons); + mLastTabs = (LastTabsSection) findViewById(R.id.last_tabs); + mRemoteTabs = (RemoteTabsSection) findViewById(R.id.remote_tabs); + + // Inform the new views about the UriLoadCallback + if (mUriLoadCallback != null) + setUriLoadCallback(mUriLoadCallback); - // If we don't have a url, this must be an empty row. Show the edit dialog box - if (TextUtils.isEmpty(spec)) { - editSite(spec, position); - return; - } - - if (mUriLoadCallback != null) - mUriLoadCallback.callback(spec); - } - }); - - mTopSitesGrid.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - - MenuInflater inflater = mActivity.getMenuInflater(); - inflater.inflate(R.menu.abouthome_topsites_contextmenu, menu); - - // If nothing is pinned at all, hide both clear items - // We can assume that the adapter count and view count are the same in this case because our grid view - // force all items to be visible all the time - View view = mTopSitesGrid.getChildAt(info.position); - TopSitesViewHolder holder = (TopSitesViewHolder) view.getTag(); - if (TextUtils.isEmpty(holder.getUrl())) { - menu.findItem(R.id.abouthome_open_new_tab).setVisible(false); - menu.findItem(R.id.abouthome_open_private_tab).setVisible(false); - menu.findItem(R.id.abouthome_topsites_pin).setVisible(false); - menu.findItem(R.id.abouthome_topsites_unpin).setVisible(false); - menu.findItem(R.id.abouthome_topsites_remove).setVisible(false); - } else if (holder.isPinned()) { - menu.findItem(R.id.abouthome_topsites_pin).setVisible(false); - } else { - menu.findItem(R.id.abouthome_topsites_unpin).setVisible(false); - } - } - }); - - mPromoBox = (AboutHomePromoBox) findViewById(R.id.promo_box); - mAddons = (AboutHomeSection) findViewById(R.id.recommended_addons); - mLastTabs = (AboutHomeSection) findViewById(R.id.last_tabs); - mRemoteTabs = (AboutHomeSection) findViewById(R.id.remote_tabs); - - mAddons.setOnMoreTextClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mUriLoadCallback != null) - mUriLoadCallback.callback("https://addons.mozilla.org/android"); - } - }); - - mRemoteTabs.setOnMoreTextClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mActivity.showRemoteTabs(); - } - }); - - setTopSitesConstants(); + mPromoBox.showRandomPromo(); } @Override @@ -266,10 +120,8 @@ public class AboutHomeContent extends ScrollView } public void onDestroy() { - if (mTopSitesAdapter != null) { - Cursor cursor = mTopSitesAdapter.getCursor(); - if (cursor != null && !cursor.isClosed()) - cursor.close(); + if (mTopSites != null) { + mTopSites.onDestroy(); } if (mTabsContentObserver != null) { @@ -278,184 +130,28 @@ public class AboutHomeContent extends ScrollView } } - public void setLastTabsVisibility(boolean visible) { - if (visible) - mLastTabs.show(); - else - mLastTabs.hide(); - } - - private void setTopSitesVisibility(boolean hasTopSites) { - int visibility = hasTopSites ? View.VISIBLE : View.GONE; - - findViewById(R.id.top_sites_title).setVisibility(visibility); - findViewById(R.id.top_sites_grid).setVisibility(visibility); - } - - private void updateLayout() { - boolean hasTopSites = mTopSitesAdapter.getCount() > 0; - setTopSitesVisibility(hasTopSites); - mPromoBox.showRandomPromo(); - } - - private void updateLayoutForSync() { - final GeckoApp.StartupMode startupMode = mActivity.getStartupMode(); - - post(new Runnable() { - @Override - public void run() { - // The listener might run before the UI is initially updated. - // In this case, we should simply wait for the initial setup - // to happen. - if (mTopSitesAdapter != null) - updateLayout(); - } - }); - } - private void loadTopSites() { - final ContentResolver resolver = mActivity.getContentResolver(); - Cursor old = null; - if (mTopSitesAdapter != null) { - old = mTopSitesAdapter.getCursor(); - } - // Swap in the new cursor. - final Cursor oldCursor = old; - final Cursor newCursor = BrowserDB.getTopSites(resolver, mNumberOfTopSites); - - post(new Runnable() { - @Override - public void run() { - if (mTopSitesAdapter == null) { - mTopSitesAdapter = new TopSitesCursorAdapter(mActivity, - R.layout.abouthome_topsite_item, - newCursor, - new String[] { URLColumns.TITLE }, - new int[] { R.id.title }); - - mTopSitesGrid.setAdapter(mTopSitesAdapter); - } else { - mTopSitesAdapter.changeCursor(newCursor); - } - - if (mTopSitesAdapter.getCount() > 0) - loadTopSitesThumbnails(resolver); - - updateLayout(); - - // Free the old Cursor in the right thread now. - if (oldCursor != null && !oldCursor.isClosed()) - oldCursor.close(); - - // Even if AboutHome isn't necessarily entirely loaded if we - // get here, for phones this is the part the user initially sees, - // so it's the one we will care about for now. - if (mLoadCompleteCallback != null) - mLoadCompleteCallback.callback(); - } - }); + mTopSites.loadTopSites(); } - private List getTopSitesUrls() { - List urls = new ArrayList(); - - Cursor c = mTopSitesAdapter.getCursor(); - if (c == null || !c.moveToFirst()) - return urls; - - do { - final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); - urls.add(url); - } while (c.moveToNext()); - - return urls; + public void openNewTab(ContextMenuInfo info) { + mTopSites.openNewTab(info); } - private void displayThumbnail(View view, Bitmap thumbnail) { - ImageView thumbnailView = (ImageView) view.findViewById(R.id.thumbnail); - - if (thumbnail == null) { - thumbnailView.setImageResource(R.drawable.abouthome_thumbnail_bg); - thumbnailView.setBackgroundColor(mThumbnailBackground); - thumbnailView.setScaleType(ImageView.ScaleType.FIT_CENTER); - } else { - try { - thumbnailView.setImageBitmap(thumbnail); - thumbnailView.setBackgroundColor(0x0); - thumbnailView.setScaleType(ImageView.ScaleType.CENTER_CROP); - } catch (OutOfMemoryError oom) { - Log.e(LOGTAG, "Unable to load thumbnail bitmap", oom); - thumbnailView.setImageResource(R.drawable.abouthome_thumbnail_bg); - thumbnailView.setScaleType(ImageView.ScaleType.FIT_CENTER); - } - } + public void openNewPrivateTab(ContextMenuInfo info) { + mTopSites.openNewPrivateTab(info); } - private void updateTopSitesThumbnails(Map thumbnails) { - for (int i = 0; i < mTopSitesAdapter.getCount(); i++) { - final View view = mTopSitesGrid.getChildAt(i); - - // The grid view might get temporarily out of sync with the - // adapter refreshes (e.g. on device rotation) - if (view == null) - continue; - - TopSitesViewHolder holder = (TopSitesViewHolder)view.getTag(); - final String url = holder.getUrl(); - if (TextUtils.isEmpty(url)) { - holder.thumbnailView.setImageResource(R.drawable.abouthome_thumbnail_add); - holder.thumbnailView.setScaleType(ImageView.ScaleType.FIT_CENTER); - } else { - displayThumbnail(view, thumbnails.get(url)); - } - } - - mTopSitesGrid.invalidate(); + public void pinSite(ContextMenuInfo info) { + mTopSites.pinSite(info); } - public Map getThumbnailsFromCursor(Cursor c) { - Map thumbnails = new HashMap(); - - try { - if (c == null || !c.moveToFirst()) - return thumbnails; - - do { - final String url = c.getString(c.getColumnIndexOrThrow(Thumbnails.URL)); - final byte[] b = c.getBlob(c.getColumnIndexOrThrow(Thumbnails.DATA)); - if (b == null) - continue; - - Bitmap thumbnail = BitmapFactory.decodeByteArray(b, 0, b.length); - if (thumbnail == null) - continue; - - thumbnails.put(url, thumbnail); - } while (c.moveToNext()); - } finally { - if (c != null) - c.close(); - } - - return thumbnails; + public void unpinSite(ContextMenuInfo info, final TopSitesView.UnpinFlags flags) { + mTopSites.unpinSite(info, flags); } - private void loadTopSitesThumbnails(final ContentResolver cr) { - final List urls = getTopSitesUrls(); - if (urls.size() == 0) - return; - - (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { - @Override - public Cursor doInBackground(Void... params) { - return BrowserDB.getThumbnailsForUrls(cr, urls); - } - - @Override - public void onPostExecute(Cursor c) { - updateTopSitesThumbnails(getThumbnailsFromCursor(c)); - } - }).execute(); + public void editSite(ContextMenuInfo info) { + mTopSites.editSite(info); } public void update(final EnumSet flags) { @@ -479,304 +175,50 @@ public class AboutHomeContent extends ScrollView public void setUriLoadCallback(UriLoadCallback uriLoadCallback) { mUriLoadCallback = uriLoadCallback; + mTopSites.setUriLoadCallback(uriLoadCallback); + mAddons.setUriLoadCallback(uriLoadCallback); } public void setLoadCompleteCallback(VoidCallback callback) { - mLoadCompleteCallback = callback; + if (mTopSites != null) + mTopSites.setLoadCompleteCallback(callback); } public void onActivityContentChanged() { update(EnumSet.of(UpdateFlags.TOP_SITES)); } - private void setTopSitesConstants() { - mNumberOfTopSites = getResources().getInteger(R.integer.number_of_top_sites); - mNumberOfCols = getResources().getInteger(R.integer.number_of_top_sites_cols); - } - /** * Reinflates and updates all components of this view. */ public void refresh() { - if (mTopSitesAdapter != null) - mTopSitesAdapter.notifyDataSetChanged(); + // We must remove the currently inflated view to allow for reinflation. + removeAllViews(); - removeAllViews(); // We must remove the currently inflated view to allow for reinflation. inflate(); - mTopSitesGrid.setAdapter(mTopSitesAdapter); // mTopSitesGrid is a new instance (from loadTopSites()). - update(AboutHomeContent.UpdateFlags.ALL); // Refresh all elements. + mTopSites.refresh(); + + // Refresh all elements. + update(AboutHomeContent.UpdateFlags.ALL); } - private String readFromZipFile(String filename) { - ZipFile zip = null; - String str = null; - try { - InputStream fileStream = null; - File applicationPackage = new File(mActivity.getApplication().getPackageResourcePath()); - zip = new ZipFile(applicationPackage); - if (zip == null) - return null; - ZipEntry fileEntry = zip.getEntry(filename); - if (fileEntry == null) - return null; - fileStream = zip.getInputStream(fileEntry); - str = readStringFromStream(fileStream); - } catch (IOException ioe) { - Log.e(LOGTAG, "error reading zip file: " + filename, ioe); - } finally { - try { - if (zip != null) - zip.close(); - } catch (IOException ioe) { - // catch this here because we can continue even if the - // close failed - Log.e(LOGTAG, "error closing zip filestream", ioe); - } - } - return str; - } - - private String readStringFromStream(InputStream fileStream) { - String str = null; - try { - byte[] buf = new byte[32768]; - StringBuffer jsonString = new StringBuffer(); - int read = 0; - while ((read = fileStream.read(buf, 0, 32768)) != -1) - jsonString.append(new String(buf, 0, read)); - str = jsonString.toString(); - } catch (IOException ioe) { - Log.i(LOGTAG, "error reading filestream", ioe); - } finally { - try { - if (fileStream != null) - fileStream.close(); - } catch (IOException ioe) { - // catch this here because we can continue even if the - // close failed - Log.e(LOGTAG, "error closing filestream", ioe); - } - } - return str; - } - - private String getPageUrlFromIconUrl(String iconUrl) { - // Addon icon URLs come with a query argument that is usually - // used for expiration purposes. We want the "page URL" here to be - // stable enough to avoid unnecessary duplicate records of the - // same addon. - String pageUrl = iconUrl; - - try { - URL urlForIcon = new URL(iconUrl); - URL urlForPage = new URL(urlForIcon.getProtocol(), urlForIcon.getAuthority(), urlForIcon.getPath()); - pageUrl = urlForPage.toString(); - } catch (MalformedURLException e) { - // Defaults to pageUrl = iconUrl in case of error - } - - return pageUrl; - } - - private void readRecommendedAddons() { - final String addonsFilename = "recommended-addons.json"; - String jsonString; - try { - jsonString = mActivity.getProfile().readFile(addonsFilename); - } catch (IOException ioe) { - Log.i(LOGTAG, "filestream is null"); - jsonString = readFromZipFile(addonsFilename); - } - - JSONArray addonsArray = null; - if (jsonString != null) { - try { - addonsArray = new JSONObject(jsonString).getJSONArray("addons"); - } catch (JSONException e) { - Log.i(LOGTAG, "error reading json file", e); - } - } - - final JSONArray array = addonsArray; - post(new Runnable() { - @Override - public void run() { - try { - if (array == null || array.length() == 0) { - mAddons.hide(); - return; - } - - for (int i = 0; i < array.length(); i++) { - JSONObject jsonobj = array.getJSONObject(i); - String name = jsonobj.getString("name"); - String version = jsonobj.getString("version"); - String text = name + " " + version; - - SpannableString spannable = new SpannableString(text); - spannable.setSpan(sSubTitleSpan, name.length() + 1, text.length(), 0); - - final TextView row = (TextView) mInflater.inflate(R.layout.abouthome_addon_row, mAddons.getItemsContainer(), false); - row.setText(spannable, TextView.BufferType.SPANNABLE); - - Drawable drawable = mContext.getResources().getDrawable(R.drawable.ic_addons_empty); - drawable.setBounds(sIconBounds); - row.setCompoundDrawables(drawable, null, null, null); - - String iconUrl = jsonobj.getString("iconURL"); - String pageUrl = getPageUrlFromIconUrl(iconUrl); - - final String homepageUrl = jsonobj.getString("homepageURL"); - row.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mUriLoadCallback != null) - mUriLoadCallback.callback(homepageUrl); - } - }); - row.setOnKeyListener(GamepadUtils.getClickDispatcher()); - - Favicons favicons = Favicons.getInstance(); - favicons.loadFavicon(pageUrl, iconUrl, true, - new Favicons.OnFaviconLoadedListener() { - @Override - public void onFaviconLoaded(String url, Bitmap favicon) { - if (favicon != null) { - Drawable drawable = new BitmapDrawable(favicon); - drawable.setBounds(sIconBounds); - row.setCompoundDrawables(drawable, null, null, null); - } - } - }); - - mAddons.addItem(row); - } - - mAddons.show(); - } catch (JSONException e) { - Log.i(LOGTAG, "error reading json file", e); - } - } - }); + public void setLastTabsVisibility(boolean visible) { + if (visible) + mLastTabs.show(); + else + mLastTabs.hide(); } private void readLastTabs() { - String jsonString = mActivity.getProfile().readSessionFile(true); - if (jsonString == null) { - // no previous session data - return; - } - - final ArrayList lastTabUrlsList = new ArrayList(); - new SessionParser() { - @Override - public void onTabRead(final SessionTab tab) { - final String url = tab.getSelectedUrl(); - // don't show last tabs for about:home - if (url.equals("about:home")) { - return; - } - - ContentResolver resolver = mActivity.getContentResolver(); - final Bitmap favicon = BrowserDB.getFaviconForUrl(resolver, url); - lastTabUrlsList.add(url); - - AboutHomeContent.this.post(new Runnable() { - @Override - public void run() { - View container = mInflater.inflate(R.layout.abouthome_last_tabs_row, mLastTabs.getItemsContainer(), false); - ((TextView) container.findViewById(R.id.last_tab_title)).setText(tab.getSelectedTitle()); - ((TextView) container.findViewById(R.id.last_tab_url)).setText(tab.getSelectedUrl()); - if (favicon != null) { - ((ImageView) container.findViewById(R.id.last_tab_favicon)).setImageBitmap(favicon); - } - - container.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - int flags = Tabs.LOADURL_NEW_TAB; - if (Tabs.getInstance().getSelectedTab().isPrivate()) - flags |= Tabs.LOADURL_PRIVATE; - Tabs.getInstance().loadUrl(url, flags); - } - }); - container.setOnKeyListener(GamepadUtils.getClickDispatcher()); - - mLastTabs.addItem(container); - } - }); - } - }.parse(jsonString); - - final int numLastTabs = lastTabUrlsList.size(); - if (numLastTabs >= 1) { - post(new Runnable() { - @Override - public void run() { - if (numLastTabs > 1) { - mLastTabs.showMoreText(); - mLastTabs.setOnMoreTextClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - int flags = Tabs.LOADURL_NEW_TAB; - if (Tabs.getInstance().getSelectedTab().isPrivate()) - flags |= Tabs.LOADURL_PRIVATE; - for (String url : lastTabUrlsList) { - Tabs.getInstance().loadUrl(url, flags); - } - } - }); - } else if (numLastTabs == 1) { - mLastTabs.hideMoreText(); - } - mLastTabs.show(); - } - }); - } + mLastTabs.readLastTabs(mActivity.getProfile()); } private void loadRemoteTabs() { - if (!SyncAccounts.syncAccountsExist(mActivity)) { - post(new Runnable() { - @Override - public void run() { - mRemoteTabs.hide(); - } - }); - return; - } - - TabsAccessor.getTabs(getContext(), NUMBER_OF_REMOTE_TABS, this); + mRemoteTabs.loadRemoteTabs(); } - @Override - public void onQueryTabsComplete(List tabsList) { - ArrayList tabs = new ArrayList (tabsList); - if (tabs == null || tabs.size() == 0) { - mRemoteTabs.hide(); - return; - } - - mRemoteTabs.clear(); - - String client = null; - - for (TabsAccessor.RemoteTab tab : tabs) { - if (client == null) - client = tab.name; - else if (!TextUtils.equals(client, tab.name)) - break; - - final TextView row = (TextView) mInflater.inflate(R.layout.abouthome_remote_tab_row, mRemoteTabs.getItemsContainer(), false); - row.setText(TextUtils.isEmpty(tab.title) ? tab.url : tab.title); - row.setTag(tab.url); - mRemoteTabs.addItem(row); - row.setOnClickListener(mRemoteTabClickListener); - row.setOnKeyListener(GamepadUtils.getClickDispatcher()); - } - - mRemoteTabs.setSubtitle(client); - mRemoteTabs.show(); + private void readRecommendedAddons() { + mAddons.readRecommendedAddons(); } @Override @@ -799,320 +241,4 @@ public class AboutHomeContent extends ScrollView super.onLayout(changed, left, top, right, bottom); onLightweightThemeChanged(); } - - public static class TopSitesGridView extends GridView { - public TopSitesGridView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public int getColumnWidth() { - return getColumnWidth(getWidth()); - } - - public int getColumnWidth(int width) { - // super.getColumnWidth() doesn't always return the correct value. - return (width - getPaddingLeft() - getPaddingRight()) / mNumberOfCols; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int measuredWidth = View.MeasureSpec.getSize(widthMeasureSpec); - int numRows; - - SimpleCursorAdapter adapter = (SimpleCursorAdapter) getAdapter(); - int nSites = Integer.MAX_VALUE; - - if (adapter != null) { - Cursor c = adapter.getCursor(); - if (c != null) - nSites = c.getCount(); - } - - nSites = Math.min(nSites, mNumberOfTopSites); - numRows = (int) Math.round((double) nSites / mNumberOfCols); - setNumColumns(mNumberOfCols); - - // Just using getWidth() will use incorrect values during onMeasure when rotating the device - // Instead we pass in the measuredWidth, which is correct - int w = getColumnWidth(measuredWidth); - ThumbnailHelper.getInstance().setThumbnailWidth(w); - heightMeasureSpec = MeasureSpec.makeMeasureSpec((int)(w*ThumbnailHelper.THUMBNAIL_ASPECT_RATIO*numRows) + getPaddingTop() + getPaddingBottom(), - MeasureSpec.EXACTLY); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - } - - private class TopSitesViewHolder { - public TextView titleView = null; - public ImageView thumbnailView = null; - public ImageView pinnedView = null; - private String mTitle = null; - private String mUrl = null; - private boolean mIsPinned = false; - - public TopSitesViewHolder(View v) { - titleView = (TextView) v.findViewById(R.id.title); - thumbnailView = (ImageView) v.findViewById(R.id.thumbnail); - pinnedView = (ImageView) v.findViewById(R.id.pinned); - } - - public void setTitle(String title) { - if (mTitle != null && mTitle.equals(title)) - return; - mTitle = title; - updateTitleView(); - } - - public String getTitle() { - return (!TextUtils.isEmpty(mTitle) ? mTitle : mUrl); - } - - public void setUrl(String url) { - if (mUrl != null && mUrl.equals(url)) - return; - mUrl = url; - updateTitleView(); - } - - public String getUrl() { - return mUrl; - } - - public void updateTitleView() { - String title = getTitle(); - if (!TextUtils.isEmpty(title)) { - titleView.setText(title); - titleView.setVisibility(View.VISIBLE); - } else { - titleView.setVisibility(View.INVISIBLE); - } - } - - private Drawable getPinDrawable() { - if (sPinDrawable == null) { - int size = mContext.getResources().getDimensionPixelSize(R.dimen.abouthome_topsite_pinsize); - - // Draw a little triangle in the upper right corner - Path path = new Path(); - path.moveTo(0, 0); - path.lineTo(size, 0); - path.lineTo(size, size); - path.close(); - - sPinDrawable = new ShapeDrawable(new PathShape(path, size, size)); - Paint p = ((ShapeDrawable) sPinDrawable).getPaint(); - p.setColor(mContext.getResources().getColor(R.color.abouthome_topsite_pin)); - } - return sPinDrawable; - } - - public void setPinned(boolean aPinned) { - mIsPinned = aPinned; - pinnedView.setBackgroundDrawable(aPinned ? getPinDrawable() : null); - } - - public boolean isPinned() { - return mIsPinned; - } - } - - public class TopSitesCursorAdapter extends SimpleCursorAdapter { - public TopSitesCursorAdapter(Context context, int layout, Cursor c, - String[] from, int[] to) { - super(context, layout, c, from, to); - } - - @Override - public int getCount() { - return Math.min(super.getCount(), mNumberOfTopSites); - } - - @Override - protected void onContentChanged () { - // Don't do anything. We don't want to regenerate every time - // our history database is updated. - return; - } - - private View buildView(String url, String title, boolean pinned, View convertView) { - TopSitesViewHolder viewHolder; - if (convertView == null) { - convertView = mInflater.inflate(R.layout.abouthome_topsite_item, null); - - viewHolder = new TopSitesViewHolder(convertView); - convertView.setTag(viewHolder); - } else { - viewHolder = (TopSitesViewHolder) convertView.getTag(); - } - - viewHolder.setTitle(title); - viewHolder.setUrl(url); - viewHolder.setPinned(pinned); - - // Force the view to fit inside this slot in the grid - convertView.setLayoutParams(new AbsListView.LayoutParams(mTopSitesGrid.getColumnWidth(), - Math.round(mTopSitesGrid.getColumnWidth()*ThumbnailHelper.THUMBNAIL_ASPECT_RATIO))); - - return convertView; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - String url = ""; - String title = ""; - boolean pinned = false; - - Cursor c = getCursor(); - c.moveToPosition(position); - if (!c.isAfterLast()) { - url = c.getString(c.getColumnIndex(URLColumns.URL)); - title = c.getString(c.getColumnIndex(URLColumns.TITLE)); - pinned = ((TopSitesCursorWrapper)c).isPinned(); - } - - return buildView(url, title, pinned, convertView); - } - } - - private void clearThumbnailsWithUrl(final String url) { - for (int i = 0; i < mTopSitesAdapter.getCount(); i++) { - final View view = mTopSitesGrid.getChildAt(i); - final TopSitesViewHolder holder = (TopSitesViewHolder) view.getTag(); - - if (holder.getUrl().equals(url)) { - clearThumbnail(holder); - } - } - } - - private void clearThumbnail(TopSitesViewHolder holder) { - holder.setTitle(""); - holder.setUrl(""); - holder.thumbnailView.setImageResource(R.drawable.abouthome_thumbnail_add); - holder.thumbnailView.setBackgroundColor(mThumbnailBackground); - holder.thumbnailView.setScaleType(ImageView.ScaleType.FIT_CENTER); - holder.setPinned(false); - } - - private void openTab(ContextMenuInfo menuInfo, int flags) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - final TopSitesViewHolder holder = (TopSitesViewHolder) info.targetView.getTag(); - final String url = holder.getUrl(); - - Tabs.getInstance().loadUrl(url, flags); - Toast.makeText(mActivity, R.string.new_tab_opened, Toast.LENGTH_SHORT).show(); - } - - public void openNewTab(ContextMenuInfo menuInfo) { - openTab(menuInfo, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND); - } - - public void openNewPrivateTab(ContextMenuInfo menuInfo) { - openTab(menuInfo, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE | Tabs.LOADURL_BACKGROUND); - } - - public void unpinSite(ContextMenuInfo menuInfo, final UnpinFlags flags) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - final int position = info.position; - final TopSitesViewHolder holder = (TopSitesViewHolder) info.targetView.getTag(); - final String url = holder.getUrl(); - // Quickly update the view so that there isn't as much lag between the request and response - clearThumbnail(holder); - (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { - @Override - public Void doInBackground(Void... params) { - final ContentResolver resolver = mActivity.getContentResolver(); - BrowserDB.unpinSite(resolver, position); - if (flags == UnpinFlags.REMOVE_HISTORY) { - BrowserDB.removeHistoryEntry(resolver, url); - } - return null; - } - }).execute(); - } - - public void pinSite(ContextMenuInfo menuInfo) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - final int position = info.position; - - final TopSitesViewHolder holder = (TopSitesViewHolder) info.targetView.getTag(); - holder.setPinned(true); - - // update the database on a background thread - (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { - @Override - public Void doInBackground(Void... params) { - final ContentResolver resolver = mActivity.getContentResolver(); - BrowserDB.pinSite(resolver, holder.getUrl(), holder.getTitle(), position); - return null; - } - }).execute(); - } - - public void editSite(ContextMenuInfo menuInfo) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - TopSitesViewHolder holder = (TopSitesViewHolder) info.targetView.getTag(); - editSite(holder.getUrl(), info.position); - } - - // Edit the site at position. Provide a url to start editing with - private void editSite(String url, final int position) { - Intent intent = new Intent(mContext, AwesomeBar.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.putExtra(AwesomeBar.TARGET_KEY, AwesomeBar.Target.PICK_SITE.toString()); - if (url != null && !TextUtils.isEmpty(url)) { - intent.putExtra(AwesomeBar.CURRENT_URL_KEY, url); - } - - int requestCode = GeckoAppShell.sActivityHelper.makeRequestCode(new ActivityResultHandler() { - @Override - public void onActivityResult(int resultCode, Intent data) { - if (resultCode == Activity.RESULT_CANCELED || data == null) - return; - - final View v = mTopSitesGrid.getChildAt(position); - final TopSitesViewHolder holder = (TopSitesViewHolder) v.getTag(); - - final String title = data.getStringExtra(AwesomeBar.TITLE_KEY); - final String url = data.getStringExtra(AwesomeBar.URL_KEY); - clearThumbnailsWithUrl(url); - - holder.setUrl(url); - holder.setTitle(title); - holder.setPinned(true); - - // update the database on a background thread - (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { - @Override - public Bitmap doInBackground(Void... params) { - final ContentResolver resolver = mActivity.getContentResolver(); - BrowserDB.pinSite(resolver, holder.getUrl(), holder.getTitle(), position); - - List urls = new ArrayList(); - urls.add(holder.getUrl()); - - Cursor c = BrowserDB.getThumbnailsForUrls(resolver, urls); - if (c == null || !c.moveToFirst()) { - return null; - } - - final byte[] b = c.getBlob(c.getColumnIndexOrThrow(Thumbnails.DATA)); - if (b != null) { - return BitmapFactory.decodeByteArray(b, 0, b.length); - } - - return null; - } - - @Override - public void onPostExecute(Bitmap b) { - displayThumbnail(v, b); - } - }).execute(); - } - }); - - mActivity.startActivityForResult(intent, requestCode); - } } diff --git a/mobile/android/base/widget/AboutHomeSection.java b/mobile/android/base/widget/AboutHomeSection.java index 4f346ff4200f..a75159364d05 100644 --- a/mobile/android/base/widget/AboutHomeSection.java +++ b/mobile/android/base/widget/AboutHomeSection.java @@ -4,6 +4,7 @@ package org.mozilla.gecko.widget; +import org.mozilla.gecko.Divider; import org.mozilla.gecko.GeckoLinearLayout; import org.mozilla.gecko.R; import org.mozilla.gecko.util.GamepadUtils; @@ -18,8 +19,6 @@ import android.widget.LinearLayout; import android.widget.TextView; public class AboutHomeSection extends GeckoLinearLayout { - private static final String LOGTAG = "GeckoAboutHomeSection"; - private TextView mTitle; private TextView mSubtitle; private LinearLayout mItemsContainer; @@ -82,7 +81,11 @@ public class AboutHomeSection extends GeckoLinearLayout { public void addItem(View item) { mItemsContainer.addView(item); - mItemsContainer.addView(new Divider(getContext(), null)); + + Divider divider = new Divider(getContext(), null); + divider.setBackgroundColor(0x3460666E); + + mItemsContainer.addView(divider); } public void clear() { @@ -104,14 +107,4 @@ public class AboutHomeSection extends GeckoLinearLayout { public void hideMoreText() { mMoreText.setVisibility(View.GONE); } - - private class Divider extends View { - public Divider(Context context, AttributeSet attrs) { - super(context, attrs); - - setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, - (int) context.getResources().getDisplayMetrics().density)); - setBackgroundColor(0x3460666E); - } - } } diff --git a/mobile/android/base/widget/AddonsSection.java b/mobile/android/base/widget/AddonsSection.java new file mode 100644 index 000000000000..5f89b441ebd0 --- /dev/null +++ b/mobile/android/base/widget/AddonsSection.java @@ -0,0 +1,222 @@ +/* -*- 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.widget; + +import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.Favicons; +import org.mozilla.gecko.R; +import org.mozilla.gecko.util.GamepadUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.SpannableString; +import android.text.style.TextAppearanceSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class AddonsSection extends AboutHomeSection { + private static final String LOGTAG = "GeckoAboutHomeAddons"; + + private Context mContext; + private BrowserApp mActivity; + private AboutHomeContent.UriLoadCallback mUriLoadCallback = null; + + private static Rect sIconBounds; + private static TextAppearanceSpan sSubTitleSpan; + + public AddonsSection(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mActivity = (BrowserApp) context; + + int iconSize = mContext.getResources().getDimensionPixelSize(R.dimen.abouthome_addon_icon_size); + sIconBounds = new Rect(0, 0, iconSize, iconSize); + sSubTitleSpan = new TextAppearanceSpan(mContext, R.style.AboutHome_TextAppearance_SubTitle); + + setOnMoreTextClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mUriLoadCallback != null) + mUriLoadCallback.callback("https://addons.mozilla.org/android"); + } + }); + } + + public void setUriLoadCallback(AboutHomeContent.UriLoadCallback uriLoadCallback) { + mUriLoadCallback = uriLoadCallback; + } + + private String readFromZipFile(String filename) { + ZipFile zip = null; + String str = null; + try { + InputStream fileStream = null; + File applicationPackage = new File(mActivity.getApplication().getPackageResourcePath()); + zip = new ZipFile(applicationPackage); + if (zip == null) + return null; + ZipEntry fileEntry = zip.getEntry(filename); + if (fileEntry == null) + return null; + fileStream = zip.getInputStream(fileEntry); + str = readStringFromStream(fileStream); + } catch (IOException ioe) { + Log.e(LOGTAG, "error reading zip file: " + filename, ioe); + } finally { + try { + if (zip != null) + zip.close(); + } catch (IOException ioe) { + // catch this here because we can continue even if the + // close failed + Log.e(LOGTAG, "error closing zip filestream", ioe); + } + } + return str; + } + + private String readStringFromStream(InputStream fileStream) { + String str = null; + try { + byte[] buf = new byte[32768]; + StringBuffer jsonString = new StringBuffer(); + int read = 0; + while ((read = fileStream.read(buf, 0, 32768)) != -1) + jsonString.append(new String(buf, 0, read)); + str = jsonString.toString(); + } catch (IOException ioe) { + Log.i(LOGTAG, "error reading filestream", ioe); + } finally { + try { + if (fileStream != null) + fileStream.close(); + } catch (IOException ioe) { + // catch this here because we can continue even if the + // close failed + Log.e(LOGTAG, "error closing filestream", ioe); + } + } + return str; + } + + private String getPageUrlFromIconUrl(String iconUrl) { + // Addon icon URLs come with a query argument that is usually + // used for expiration purposes. We want the "page URL" here to be + // stable enough to avoid unnecessary duplicate records of the + // same addon. + String pageUrl = iconUrl; + + try { + URL urlForIcon = new URL(iconUrl); + URL urlForPage = new URL(urlForIcon.getProtocol(), urlForIcon.getAuthority(), urlForIcon.getPath()); + pageUrl = urlForPage.toString(); + } catch (MalformedURLException e) { + // Defaults to pageUrl = iconUrl in case of error + } + + return pageUrl; + } + + public void readRecommendedAddons() { + final String addonsFilename = "recommended-addons.json"; + String jsonString; + try { + jsonString = mActivity.getProfile().readFile(addonsFilename); + } catch (IOException ioe) { + Log.i(LOGTAG, "filestream is null"); + jsonString = readFromZipFile(addonsFilename); + } + + JSONArray addonsArray = null; + if (jsonString != null) { + try { + addonsArray = new JSONObject(jsonString).getJSONArray("addons"); + } catch (JSONException e) { + Log.i(LOGTAG, "error reading json file", e); + } + } + + final JSONArray array = addonsArray; + post(new Runnable() { + @Override + public void run() { + try { + if (array == null || array.length() == 0) { + hide(); + return; + } + + for (int i = 0; i < array.length(); i++) { + JSONObject jsonobj = array.getJSONObject(i); + String name = jsonobj.getString("name"); + String version = jsonobj.getString("version"); + String text = name + " " + version; + + SpannableString spannable = new SpannableString(text); + spannable.setSpan(sSubTitleSpan, name.length() + 1, text.length(), 0); + + final TextView row = (TextView) LayoutInflater.from(mContext).inflate(R.layout.abouthome_addon_row, getItemsContainer(), false); + row.setText(spannable, TextView.BufferType.SPANNABLE); + + Drawable drawable = mContext.getResources().getDrawable(R.drawable.ic_addons_empty); + drawable.setBounds(sIconBounds); + row.setCompoundDrawables(drawable, null, null, null); + + String iconUrl = jsonobj.getString("iconURL"); + String pageUrl = getPageUrlFromIconUrl(iconUrl); + + final String homepageUrl = jsonobj.getString("homepageURL"); + row.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mUriLoadCallback != null) + mUriLoadCallback.callback(homepageUrl); + } + }); + row.setOnKeyListener(GamepadUtils.getClickDispatcher()); + + Favicons favicons = Favicons.getInstance(); + favicons.loadFavicon(pageUrl, iconUrl, true, + new Favicons.OnFaviconLoadedListener() { + @Override + public void onFaviconLoaded(String url, Bitmap favicon) { + if (favicon != null) { + Drawable drawable = new BitmapDrawable(favicon); + drawable.setBounds(sIconBounds); + row.setCompoundDrawables(drawable, null, null, null); + } + } + }); + + addItem(row); + } + + show(); + } catch (JSONException e) { + Log.i(LOGTAG, "error reading json file", e); + } + } + }); + } +} diff --git a/mobile/android/base/widget/LastTabsSection.java b/mobile/android/base/widget/LastTabsSection.java new file mode 100644 index 000000000000..dd3b2a772a0d --- /dev/null +++ b/mobile/android/base/widget/LastTabsSection.java @@ -0,0 +1,108 @@ +/* -*- 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.widget; + +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.R; +import org.mozilla.gecko.SessionParser; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.util.GamepadUtils; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Bitmap; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; + +public class LastTabsSection extends AboutHomeSection { + private Context mContext; + + public LastTabsSection(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + } + + public void readLastTabs(GeckoProfile profile) { + String jsonString = profile.readSessionFile(true); + if (jsonString == null) { + // no previous session data + return; + } + + final ArrayList lastTabUrlsList = new ArrayList(); + new SessionParser() { + @Override + public void onTabRead(final SessionTab tab) { + final String url = tab.getSelectedUrl(); + // don't show last tabs for about:home + if (url.equals("about:home")) { + return; + } + + ContentResolver resolver = mContext.getContentResolver(); + final Bitmap favicon = BrowserDB.getFaviconForUrl(resolver, url); + lastTabUrlsList.add(url); + + LastTabsSection.this.post(new Runnable() { + @Override + public void run() { + View container = LayoutInflater.from(mContext).inflate(R.layout.abouthome_last_tabs_row, getItemsContainer(), false); + ((TextView) container.findViewById(R.id.last_tab_title)).setText(tab.getSelectedTitle()); + ((TextView) container.findViewById(R.id.last_tab_url)).setText(tab.getSelectedUrl()); + if (favicon != null) { + ((ImageView) container.findViewById(R.id.last_tab_favicon)).setImageBitmap(favicon); + } + + container.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int flags = Tabs.LOADURL_NEW_TAB; + if (Tabs.getInstance().getSelectedTab().isPrivate()) + flags |= Tabs.LOADURL_PRIVATE; + Tabs.getInstance().loadUrl(url, flags); + } + }); + container.setOnKeyListener(GamepadUtils.getClickDispatcher()); + + addItem(container); + } + }); + } + }.parse(jsonString); + + final int nu = lastTabUrlsList.size(); + if (nu >= 1) { + post(new Runnable() { + @Override + public void run() { + if (nu > 1) { + showMoreText(); + setOnMoreTextClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + int flags = Tabs.LOADURL_NEW_TAB; + if (Tabs.getInstance().getSelectedTab().isPrivate()) + flags |= Tabs.LOADURL_PRIVATE; + for (String url : lastTabUrlsList) { + Tabs.getInstance().loadUrl(url, flags); + } + } + }); + } else if (nu == 1) { + hideMoreText(); + } + show(); + } + }); + } + } +} diff --git a/mobile/android/base/widget/AboutHomePromoBox.java b/mobile/android/base/widget/PromoBox.java similarity index 94% rename from mobile/android/base/widget/AboutHomePromoBox.java rename to mobile/android/base/widget/PromoBox.java index 0f86418e610a..e3a80cdc3e17 100644 --- a/mobile/android/base/widget/AboutHomePromoBox.java +++ b/mobile/android/base/widget/PromoBox.java @@ -33,7 +33,7 @@ import java.util.Random; * To do this, add a new Type value and update show() to call setResources() for your values - * including a set[Box Type]Resources() helper method is recommended. */ -public class AboutHomePromoBox extends TextView implements View.OnClickListener { +public class PromoBox extends TextView implements View.OnClickListener { private static final String LOGTAG = "GeckoAboutHomePromoBox"; /* Small class for implementing a new promo box type. Implementors should override canShow and onClick @@ -101,7 +101,7 @@ public class AboutHomePromoBox extends TextView implements View.OnClickListener private final Context mContext; - public AboutHomePromoBox(Context context, AttributeSet attrs) { + public PromoBox(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; @@ -161,10 +161,10 @@ public class AboutHomePromoBox extends TextView implements View.OnClickListener } // Try to maintain a promo type for the lifetime of the application - if (AboutHomePromoBox.sTypeIndex == -1 || AboutHomePromoBox.sTypeIndex >= types.size()) { - AboutHomePromoBox.sTypeIndex = new Random().nextInt(types.size()); + if (PromoBox.sTypeIndex == -1 || PromoBox.sTypeIndex >= types.size()) { + PromoBox.sTypeIndex = new Random().nextInt(types.size()); } - mType = types.get(AboutHomePromoBox.sTypeIndex); + mType = types.get(PromoBox.sTypeIndex); updateViewResources(); setVisibility(View.VISIBLE); diff --git a/mobile/android/base/widget/RemoteTabsSection.java b/mobile/android/base/widget/RemoteTabsSection.java new file mode 100644 index 000000000000..ef2b8734f269 --- /dev/null +++ b/mobile/android/base/widget/RemoteTabsSection.java @@ -0,0 +1,100 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.widget; + +import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.TabsAccessor; +import org.mozilla.gecko.sync.setup.SyncAccounts; +import org.mozilla.gecko.util.GamepadUtils; + +import android.content.Context; +import android.util.AttributeSet; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +public class RemoteTabsSection extends AboutHomeSection + implements TabsAccessor.OnQueryTabsCompleteListener { + private static final int NUMBER_OF_REMOTE_TABS = 5; + + private Context mContext; + private BrowserApp mActivity; + + private View.OnClickListener mRemoteTabClickListener; + + public RemoteTabsSection(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mActivity = (BrowserApp) context; + + setOnMoreTextClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mActivity.showRemoteTabs(); + } + }); + + mRemoteTabClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + int flags = Tabs.LOADURL_NEW_TAB; + if (Tabs.getInstance().getSelectedTab().isPrivate()) + flags |= Tabs.LOADURL_PRIVATE; + Tabs.getInstance().loadUrl((String) v.getTag(), flags); + } + }; + } + + public void loadRemoteTabs() { + if (!SyncAccounts.syncAccountsExist(mActivity)) { + post(new Runnable() { + @Override + public void run() { + hide(); + } + }); + return; + } + + TabsAccessor.getTabs(getContext(), NUMBER_OF_REMOTE_TABS, this); + } + + @Override + public void onQueryTabsComplete(List tabsList) { + ArrayList tabs = new ArrayList (tabsList); + if (tabs == null || tabs.size() == 0) { + hide(); + return; + } + + clear(); + + String client = null; + + for (TabsAccessor.RemoteTab tab : tabs) { + if (client == null) + client = tab.name; + else if (!TextUtils.equals(client, tab.name)) + break; + + final TextView row = (TextView) LayoutInflater.from(mContext).inflate(R.layout.abouthome_remote_tab_row, getItemsContainer(), false); + row.setText(TextUtils.isEmpty(tab.title) ? tab.url : tab.title); + row.setTag(tab.url); + addItem(row); + row.setOnClickListener(mRemoteTabClickListener); + row.setOnKeyListener(GamepadUtils.getClickDispatcher()); + } + + setSubtitle(client); + show(); + } +} diff --git a/mobile/android/base/widget/TopSitesView.java b/mobile/android/base/widget/TopSitesView.java new file mode 100644 index 000000000000..42a1052b12f4 --- /dev/null +++ b/mobile/android/base/widget/TopSitesView.java @@ -0,0 +1,628 @@ +/* -*- 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.widget; + +import org.mozilla.gecko.AwesomeBar; +import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.ThumbnailHelper; +import org.mozilla.gecko.db.BrowserContract.Thumbnails; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper; +import org.mozilla.gecko.db.BrowserDB.URLColumns; +import org.mozilla.gecko.util.ActivityResultHandler; +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.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TopSitesView extends GridView { + private static final String LOGTAG = "GeckoAboutHomeTopSites"; + + private static int mNumberOfTopSites; + private static int mNumberOfCols; + + public static enum UnpinFlags { + REMOVE_PIN, + REMOVE_HISTORY + } + + private Context mContext; + private BrowserApp mActivity; + private AboutHomeContent.UriLoadCallback mUriLoadCallback = null; + private AboutHomeContent.VoidCallback mLoadCompleteCallback = null; + + protected TopSitesCursorAdapter mTopSitesAdapter; + + private static Drawable sPinDrawable = null; + private int mThumbnailBackground; + + public TopSitesView(Context context) { + super(context); + mContext = context; + mActivity = (BrowserApp) context; + + init(); + } + + public TopSitesView(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mActivity = (BrowserApp) context; + + init(); + } + + private void init() { + mThumbnailBackground = mContext.getResources().getColor(R.color.abouthome_thumbnail_bg); + setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + TopSitesViewHolder holder = (TopSitesViewHolder) v.getTag(); + String spec = holder.getUrl(); + + // If we don't have a url, this must be an empty row. Show the edit dialog box + if (TextUtils.isEmpty(spec)) { + editSite(spec, position); + return; + } + + if (mUriLoadCallback != null) + mUriLoadCallback.callback(spec); + } + }); + + setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; + + MenuInflater inflater = mActivity.getMenuInflater(); + inflater.inflate(R.menu.abouthome_topsites_contextmenu, menu); + + // If nothing is pinned at all, hide both clear items + // We can assume that the adapter count and view count are the same in this case because our grid view + // force all items to be visible all the time + View view = getChildAt(info.position); + TopSitesViewHolder holder = (TopSitesViewHolder) view.getTag(); + if (TextUtils.isEmpty(holder.getUrl())) { + menu.findItem(R.id.abouthome_open_new_tab).setVisible(false); + menu.findItem(R.id.abouthome_open_private_tab).setVisible(false); + menu.findItem(R.id.abouthome_topsites_pin).setVisible(false); + menu.findItem(R.id.abouthome_topsites_unpin).setVisible(false); + menu.findItem(R.id.abouthome_topsites_remove).setVisible(false); + } else if (holder.isPinned()) { + menu.findItem(R.id.abouthome_topsites_pin).setVisible(false); + } else { + menu.findItem(R.id.abouthome_topsites_unpin).setVisible(false); + } + } + }); + + setTopSitesConstants(); + } + + public void onDestroy() { + if (mTopSitesAdapter != null) { + Cursor cursor = mTopSitesAdapter.getCursor(); + if (cursor != null && !cursor.isClosed()) + cursor.close(); + } + } + + @Override + public int getColumnWidth() { + return getColumnWidth(getWidth()); + } + + public int getColumnWidth(int width) { + // super.getColumnWidth() doesn't always return the correct value. + return (width - getPaddingLeft() - getPaddingRight()) / mNumberOfCols; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = View.MeasureSpec.getSize(widthMeasureSpec); + int numRows; + + SimpleCursorAdapter adapter = (SimpleCursorAdapter) getAdapter(); + int nSites = Integer.MAX_VALUE; + + if (adapter != null) { + Cursor c = adapter.getCursor(); + if (c != null) + nSites = c.getCount(); + } + + nSites = Math.min(nSites, mNumberOfTopSites); + numRows = (int) Math.round((double) nSites / mNumberOfCols); + setNumColumns(mNumberOfCols); + + // Just using getWidth() will use incorrect values during onMeasure when rotating the device + // Instead we pass in the measuredWidth, which is correct + int w = getColumnWidth(measuredWidth); + ThumbnailHelper.getInstance().setThumbnailWidth(w); + heightMeasureSpec = MeasureSpec.makeMeasureSpec((int)(w*ThumbnailHelper.THUMBNAIL_ASPECT_RATIO*numRows) + getPaddingTop() + getPaddingBottom(), + MeasureSpec.EXACTLY); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public void loadTopSites() { + final ContentResolver resolver = mContext.getContentResolver(); + Cursor old = null; + if (mTopSitesAdapter != null) { + old = mTopSitesAdapter.getCursor(); + } + // Swap in the new cursor. + final Cursor oldCursor = old; + final Cursor newCursor = BrowserDB.getTopSites(resolver, mNumberOfTopSites); + + post(new Runnable() { + @Override + public void run() { + if (mTopSitesAdapter == null) { + mTopSitesAdapter = new TopSitesCursorAdapter(mContext, + R.layout.abouthome_topsite_item, + newCursor, + new String[] { URLColumns.TITLE }, + new int[] { R.id.title }); + + setAdapter(mTopSitesAdapter); + } else { + mTopSitesAdapter.changeCursor(newCursor); + } + + if (mTopSitesAdapter.getCount() > 0) + loadTopSitesThumbnails(resolver); + + // Free the old Cursor in the right thread now. + if (oldCursor != null && !oldCursor.isClosed()) + oldCursor.close(); + + // Even if AboutHome isn't necessarily entirely loaded if we + // get here, for phones this is the part the user initially sees, + // so it's the one we will care about for now. + if (mLoadCompleteCallback != null) + mLoadCompleteCallback.callback(); + } + }); + } + + private List getTopSitesUrls() { + List urls = new ArrayList(); + + Cursor c = mTopSitesAdapter.getCursor(); + if (c == null || !c.moveToFirst()) + return urls; + + do { + final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL)); + urls.add(url); + } while (c.moveToNext()); + + return urls; + } + + private void displayThumbnail(View view, Bitmap thumbnail) { + ImageView thumbnailView = (ImageView) view.findViewById(R.id.thumbnail); + + if (thumbnail == null) { + thumbnailView.setImageResource(R.drawable.abouthome_thumbnail_bg); + thumbnailView.setBackgroundColor(mThumbnailBackground); + thumbnailView.setScaleType(ImageView.ScaleType.FIT_CENTER); + } else { + try { + thumbnailView.setImageBitmap(thumbnail); + thumbnailView.setBackgroundColor(0x0); + thumbnailView.setScaleType(ImageView.ScaleType.CENTER_CROP); + } catch (OutOfMemoryError oom) { + Log.e(LOGTAG, "Unable to load thumbnail bitmap", oom); + thumbnailView.setImageResource(R.drawable.abouthome_thumbnail_bg); + thumbnailView.setScaleType(ImageView.ScaleType.FIT_CENTER); + } + } + } + + private void updateTopSitesThumbnails(Map thumbnails) { + for (int i = 0; i < mTopSitesAdapter.getCount(); i++) { + final View view = getChildAt(i); + + // The grid view might get temporarily out of sync with the + // adapter refreshes (e.g. on device rotation) + if (view == null) + continue; + + TopSitesViewHolder holder = (TopSitesViewHolder)view.getTag(); + final String url = holder.getUrl(); + if (TextUtils.isEmpty(url)) { + holder.thumbnailView.setImageResource(R.drawable.abouthome_thumbnail_add); + holder.thumbnailView.setScaleType(ImageView.ScaleType.FIT_CENTER); + } else { + displayThumbnail(view, thumbnails.get(url)); + } + } + + invalidate(); + } + + public Map getThumbnailsFromCursor(Cursor c) { + Map thumbnails = new HashMap(); + + try { + if (c == null || !c.moveToFirst()) + return thumbnails; + + do { + final String url = c.getString(c.getColumnIndexOrThrow(Thumbnails.URL)); + final byte[] b = c.getBlob(c.getColumnIndexOrThrow(Thumbnails.DATA)); + if (b == null) + continue; + + Bitmap thumbnail = BitmapFactory.decodeByteArray(b, 0, b.length); + if (thumbnail == null) + continue; + + thumbnails.put(url, thumbnail); + } while (c.moveToNext()); + } finally { + if (c != null) + c.close(); + } + + return thumbnails; + } + + private void loadTopSitesThumbnails(final ContentResolver cr) { + final List urls = getTopSitesUrls(); + if (urls.size() == 0) + return; + + (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { + @Override + public Cursor doInBackground(Void... params) { + return BrowserDB.getThumbnailsForUrls(cr, urls); + } + + @Override + public void onPostExecute(Cursor c) { + updateTopSitesThumbnails(getThumbnailsFromCursor(c)); + } + }).execute(); + } + + private void setTopSitesConstants() { + mNumberOfTopSites = getResources().getInteger(R.integer.number_of_top_sites); + mNumberOfCols = getResources().getInteger(R.integer.number_of_top_sites_cols); + } + + public void setUriLoadCallback(AboutHomeContent.UriLoadCallback uriLoadCallback) { + mUriLoadCallback = uriLoadCallback; + } + + public void setLoadCompleteCallback(AboutHomeContent.VoidCallback callback) { + mLoadCompleteCallback = callback; + } + + public void refresh() { + if (mTopSitesAdapter != null) + mTopSitesAdapter.notifyDataSetChanged(); + + setAdapter(mTopSitesAdapter); + } + + private class TopSitesViewHolder { + public TextView titleView = null; + public ImageView thumbnailView = null; + public ImageView pinnedView = null; + private String mTitle = null; + private String mUrl = null; + private boolean mIsPinned = false; + + public TopSitesViewHolder(View v) { + titleView = (TextView) v.findViewById(R.id.title); + thumbnailView = (ImageView) v.findViewById(R.id.thumbnail); + pinnedView = (ImageView) v.findViewById(R.id.pinned); + } + + public void setTitle(String title) { + if (mTitle != null && mTitle.equals(title)) + return; + mTitle = title; + updateTitleView(); + } + + public String getTitle() { + return (!TextUtils.isEmpty(mTitle) ? mTitle : mUrl); + } + + public void setUrl(String url) { + if (mUrl != null && mUrl.equals(url)) + return; + mUrl = url; + updateTitleView(); + } + + public String getUrl() { + return mUrl; + } + + public void updateTitleView() { + String title = getTitle(); + if (!TextUtils.isEmpty(title)) { + titleView.setText(title); + titleView.setVisibility(View.VISIBLE); + } else { + titleView.setVisibility(View.INVISIBLE); + } + } + + private Drawable getPinDrawable() { + if (sPinDrawable == null) { + int size = mContext.getResources().getDimensionPixelSize(R.dimen.abouthome_topsite_pinsize); + + // Draw a little triangle in the upper right corner + Path path = new Path(); + path.moveTo(0, 0); + path.lineTo(size, 0); + path.lineTo(size, size); + path.close(); + + sPinDrawable = new ShapeDrawable(new PathShape(path, size, size)); + Paint p = ((ShapeDrawable) sPinDrawable).getPaint(); + p.setColor(mContext.getResources().getColor(R.color.abouthome_topsite_pin)); + } + return sPinDrawable; + } + + public void setPinned(boolean aPinned) { + mIsPinned = aPinned; + pinnedView.setBackgroundDrawable(aPinned ? getPinDrawable() : null); + } + + public boolean isPinned() { + return mIsPinned; + } + } + + public class TopSitesCursorAdapter extends SimpleCursorAdapter { + public TopSitesCursorAdapter(Context context, int layout, Cursor c, + String[] from, int[] to) { + super(context, layout, c, from, to); + } + + @Override + public int getCount() { + return Math.min(super.getCount(), mNumberOfTopSites); + } + + @Override + protected void onContentChanged () { + // Don't do anything. We don't want to regenerate every time + // our history database is updated. + return; + } + + private View buildView(String url, String title, boolean pinned, View convertView) { + TopSitesViewHolder viewHolder; + if (convertView == null) { + convertView = LayoutInflater.from(mContext).inflate(R.layout.abouthome_topsite_item, null); + + viewHolder = new TopSitesViewHolder(convertView); + convertView.setTag(viewHolder); + } else { + viewHolder = (TopSitesViewHolder) convertView.getTag(); + } + + viewHolder.setTitle(title); + viewHolder.setUrl(url); + viewHolder.setPinned(pinned); + + // Force the view to fit inside this slot in the grid + convertView.setLayoutParams(new AbsListView.LayoutParams(getColumnWidth(), + Math.round(getColumnWidth()*ThumbnailHelper.THUMBNAIL_ASPECT_RATIO))); + + return convertView; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + String url = ""; + String title = ""; + boolean pinned = false; + + Cursor c = getCursor(); + c.moveToPosition(position); + if (!c.isAfterLast()) { + url = c.getString(c.getColumnIndex(URLColumns.URL)); + title = c.getString(c.getColumnIndex(URLColumns.TITLE)); + pinned = ((TopSitesCursorWrapper)c).isPinned(); + } + + return buildView(url, title, pinned, convertView); + } + } + + private void clearThumbnailsWithUrl(final String url) { + for (int i = 0; i < mTopSitesAdapter.getCount(); i++) { + final View view = getChildAt(i); + final TopSitesViewHolder holder = (TopSitesViewHolder) view.getTag(); + + if (holder.getUrl().equals(url)) { + clearThumbnail(holder); + } + } + } + + private void clearThumbnail(TopSitesViewHolder holder) { + holder.setTitle(""); + holder.setUrl(""); + holder.thumbnailView.setImageResource(R.drawable.abouthome_thumbnail_add); + holder.thumbnailView.setBackgroundColor(mThumbnailBackground); + holder.thumbnailView.setScaleType(ImageView.ScaleType.FIT_CENTER); + holder.setPinned(false); + } + + private void openTab(ContextMenuInfo menuInfo, int flags) { + AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; + final TopSitesViewHolder holder = (TopSitesViewHolder) info.targetView.getTag(); + final String url = holder.getUrl(); + + Tabs.getInstance().loadUrl(url, flags); + Toast.makeText(mActivity, R.string.new_tab_opened, Toast.LENGTH_SHORT).show(); + } + + public void openNewTab(ContextMenuInfo menuInfo) { + openTab(menuInfo, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND); + } + + public void openNewPrivateTab(ContextMenuInfo menuInfo) { + openTab(menuInfo, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE | Tabs.LOADURL_BACKGROUND); + } + + public void unpinSite(ContextMenuInfo menuInfo, final UnpinFlags flags) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + final int position = info.position; + + final View v = getChildAt(position); + final TopSitesViewHolder holder = (TopSitesViewHolder) v.getTag(); + final String url = holder.getUrl(); + // Quickly update the view so that there isn't as much lag between the request and response + clearThumbnail(holder); + (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { + @Override + public Void doInBackground(Void... params) { + final ContentResolver resolver = mContext.getContentResolver(); + BrowserDB.unpinSite(resolver, position); + if (flags == UnpinFlags.REMOVE_HISTORY) { + BrowserDB.removeHistoryEntry(resolver, url); + } + return null; + } + }).execute(); + } + + public void pinSite(ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + final int position = info.position; + View v = getChildAt(position); + + final TopSitesViewHolder holder = (TopSitesViewHolder) v.getTag(); + holder.setPinned(true); + + // update the database on a background thread + (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { + @Override + public Void doInBackground(Void... params) { + final ContentResolver resolver = mContext.getContentResolver(); + BrowserDB.pinSite(resolver, holder.getUrl(), holder.getTitle(), position); + return null; + } + }).execute(); + } + + public void editSite(ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + int position = info.position; + View v = getChildAt(position); + + TopSitesViewHolder holder = (TopSitesViewHolder) v.getTag(); + editSite(holder.getUrl(), position); + } + + // Edit the site at position. Provide a url to start editing with + public void editSite(String url, final int position) { + Intent intent = new Intent(mContext, AwesomeBar.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.putExtra(AwesomeBar.TARGET_KEY, AwesomeBar.Target.PICK_SITE.toString()); + if (url != null && !TextUtils.isEmpty(url)) { + intent.putExtra(AwesomeBar.CURRENT_URL_KEY, url); + } + + int requestCode = GeckoAppShell.sActivityHelper.makeRequestCode(new ActivityResultHandler() { + @Override + public void onActivityResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_CANCELED || data == null) + return; + + final View v = getChildAt(position); + final TopSitesViewHolder holder = (TopSitesViewHolder) v.getTag(); + + final String title = data.getStringExtra(AwesomeBar.TITLE_KEY); + final String url = data.getStringExtra(AwesomeBar.URL_KEY); + clearThumbnailsWithUrl(url); + + holder.setUrl(url); + holder.setTitle(title); + holder.setPinned(true); + + // update the database on a background thread + (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { + @Override + public Bitmap doInBackground(Void... params) { + final ContentResolver resolver = mContext.getContentResolver(); + BrowserDB.pinSite(resolver, holder.getUrl(), holder.getTitle(), position); + + List urls = new ArrayList(); + urls.add(holder.getUrl()); + + Cursor c = BrowserDB.getThumbnailsForUrls(resolver, urls); + if (c == null || !c.moveToFirst()) { + return null; + } + + final byte[] b = c.getBlob(c.getColumnIndexOrThrow(Thumbnails.DATA)); + if (b != null) { + return BitmapFactory.decodeByteArray(b, 0, b.length); + } + + return null; + } + + @Override + public void onPostExecute(Bitmap b) { + displayThumbnail(v, b); + } + }).execute(); + } + }); + + mActivity.startActivityForResult(intent, requestCode); + } +}