/* -*- 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.db; import org.mozilla.gecko.db.BrowserContract.Bookmarks; import org.mozilla.gecko.db.BrowserContract.History; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.Telemetry; import org.json.JSONException; import org.json.JSONObject; import android.content.ContentValues; import android.content.ContentResolver; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.support.v4.util.LruCache; import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; // Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data. public class URLMetadata { private static final String LOGTAG = "GeckoURLMetadata"; // This returns a list of columns in the table. It's used to simplify some loops for reading/writing data. @SuppressWarnings("serial") private static final Set getModel() { return new HashSet() {{ add(URLMetadataTable.URL_COLUMN); add(URLMetadataTable.TILE_IMAGE_URL_COLUMN); add(URLMetadataTable.TILE_COLOR_COLUMN); }}; } // Store a cache of recent results. This number is chosen to match the max number of tiles on about:home private static final int CACHE_SIZE = 9; // Note: Members of this cache are unmodifiable. private static final LruCache> cache = new LruCache>(CACHE_SIZE); /** * Converts a JSON object into a unmodifiable Map of known metadata properties. * Will throw away any properties that aren't stored in the database. */ public static Map fromJSON(JSONObject obj) { Map data = new HashMap(); Set model = getModel(); for (String key : model) { if (obj.has(key)) { data.put(key, obj.optString(key)); } } return Collections.unmodifiableMap(data); } /** * Converts a Cursor into a unmodifiable Map of known metadata properties. * Will throw away any properties that aren't stored in the database. * Will also not iterate through multiple rows in the cursor. */ private static Map fromCursor(Cursor c) { Map data = new HashMap(); Set model = getModel(); String[] columns = c.getColumnNames(); for (String column : columns) { if (model.contains(column)) { try { data.put(column, c.getString(c.getColumnIndexOrThrow(column))); } catch (Exception ex) { Log.i(LOGTAG, "Error getting data for " + column, ex); } } } return Collections.unmodifiableMap(data); } /** * Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls. * Must not be called from UI or Gecko threads. */ public static Map> getForUrls(final ContentResolver cr, final List urls, final List columns) { ThreadUtils.assertNotOnUiThread(); ThreadUtils.assertNotOnGeckoThread(); final Map> data = new HashMap>(); // Nothing to query for if (urls.isEmpty() || columns.isEmpty()) { Log.e(LOGTAG, "Queried metadata for nothing"); return data; } // Search the cache for any of these urls List urlsToQuery = new ArrayList(); for (String url : urls) { final Map hit = cache.get(url); if (hit != null) { // Cache hit! data.put(url, hit); } else { urlsToQuery.add(url); } } Telemetry.HistogramAdd("FENNEC_TILES_CACHE_HIT", data.size()); // If everything was in the cache, we're done! if (urlsToQuery.size() == 0) { return Collections.unmodifiableMap(data); } final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN); // We need the url to build our final HashMap, so we force it to be included in the query. if (!columns.contains(URLMetadataTable.URL_COLUMN)) { columns.add(URLMetadataTable.URL_COLUMN); } final Cursor cursor = cr.query(URLMetadataTable.CONTENT_URI, columns.toArray(new String[columns.size()]), // columns, selection, // selection urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs null); try { if (!cursor.moveToFirst()) { return Collections.unmodifiableMap(data); } do { final Map metadata = fromCursor(cursor); final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN)); data.put(url, metadata); cache.put(url, metadata); } while(cursor.moveToNext()); } finally { cursor.close(); } return Collections.unmodifiableMap(data); } /** * Saves a HashMap of metadata into the database. Will iterate through columns * in the Database and only save rows with matching keys in the HashMap. * Must not be called from UI or Gecko threads. */ public static void save(final ContentResolver cr, final String url, final Map data) { ThreadUtils.assertNotOnUiThread(); ThreadUtils.assertNotOnGeckoThread(); try { ContentValues values = new ContentValues(); Set model = getModel(); for (String key : model) { if (data.containsKey(key)) { values.put(key, (String) data.get(key)); } } if (values.size() == 0) { return; } Uri uri = URLMetadataTable.CONTENT_URI.buildUpon() .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true") .build(); cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] { (String) data.get(URLMetadataTable.URL_COLUMN) }); } catch (Exception ex) { Log.e(LOGTAG, "error saving", ex); } } public static int deleteUnused(final ContentResolver cr, final String profile) { final String selection = URLMetadataTable.URL_COLUMN + " NOT IN " + "(SELECT " + History.URL + " FROM " + History.TABLE_NAME + " WHERE " + History.IS_DELETED + " = 0" + " UNION " + " SELECT " + Bookmarks.URL + " FROM " + Bookmarks.TABLE_NAME + " WHERE " + Bookmarks.IS_DELETED + " = 0 " + " AND " + Bookmarks.URL + " IS NOT NULL)"; Uri uri = URLMetadataTable.CONTENT_URI; if (!TextUtils.isEmpty(profile)) { uri = uri.buildUpon() .appendQueryParameter(BrowserContract.PARAM_PROFILE, profile) .build(); } return cr.delete(uri, selection, null); } }