/* 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 java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONException; import org.mozilla.gecko.R; import org.mozilla.gecko.Tab; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.UIAsyncTask; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; public class LocalTabsAccessor implements TabsAccessor { private static final String LOGTAG = "GeckoTabsAccessor"; public static final String[] TABS_PROJECTION_COLUMNS = new String[] { BrowserContract.Tabs.TITLE, BrowserContract.Tabs.URL, BrowserContract.Clients.GUID, BrowserContract.Clients.NAME, BrowserContract.Tabs.LAST_USED, BrowserContract.Clients.LAST_MODIFIED, BrowserContract.Clients.DEVICE_TYPE, }; public static final String[] CLIENTS_PROJECTION_COLUMNS = new String[] { BrowserContract.Clients.GUID, BrowserContract.Clients.NAME, BrowserContract.Clients.LAST_MODIFIED, BrowserContract.Clients.DEVICE_TYPE }; private static final String REMOTE_CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL"; private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL"; private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL"; private static final String REMOTE_TABS_SORT_ORDER = // Most recently synced clients first. BrowserContract.Clients.LAST_MODIFIED + " DESC, " + // If two clients somehow had the same last modified time, this will // group them (arbitrarily). BrowserContract.Clients.GUID + " DESC, " + // Within a single client, most recently used tabs first. BrowserContract.Tabs.LAST_USED + " DESC"; private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL"; private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):"); private final Uri clientsRecencyUriWithProfile; private final Uri tabsUriWithProfile; private final Uri clientsUriWithProfile; public LocalTabsAccessor(String profileName) { tabsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Tabs.CONTENT_URI); clientsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_URI); clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI); } /** * Extracts a List of just RemoteClients from a cursor. * The supplied cursor should be grouped by guid and sorted by most recently used. */ @Override public List getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) { final ArrayList clients = new ArrayList<>(cursor.getCount()); final int originalPosition = cursor.getPosition(); try { if (!cursor.moveToFirst()) { return clients; } final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID); final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME); final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED); final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE); while (!cursor.isAfterLast()) { final String clientGuid = cursor.getString(clientGuidIndex); final String clientName = cursor.getString(clientNameIndex); final String deviceType = cursor.getString(clientDeviceTypeIndex); final long lastModified = cursor.getLong(clientLastModifiedIndex); clients.add(new RemoteClient(clientGuid, clientName, lastModified, deviceType)); cursor.moveToNext(); } } finally { cursor.moveToPosition(originalPosition); } return clients; } /** * Extract client and tab records from a cursor. *

* The position of the cursor is moved to before the first record before * reading. The cursor is advanced until there are no more records to be * read. The position of the cursor is restored before returning. * * @param cursor * to extract records from. The records should already be grouped * by client GUID. * @return list of clients, each containing list of tabs. */ @Override public List getClientsFromCursor(final Cursor cursor) { final ArrayList clients = new ArrayList(); final int originalPosition = cursor.getPosition(); try { if (!cursor.moveToFirst()) { return clients; } final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE); final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL); final int tabLastUsedIndex = cursor.getColumnIndex(BrowserContract.Tabs.LAST_USED); final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID); final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME); final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED); final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE); // A walking partition, chunking by client GUID. We assume the // cursor records are already grouped by client GUID; see the query // sort order. RemoteClient lastClient = null; while (!cursor.isAfterLast()) { final String clientGuid = cursor.getString(clientGuidIndex); if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) { final String clientName = cursor.getString(clientNameIndex); final long lastModified = cursor.getLong(clientLastModifiedIndex); final String deviceType = cursor.getString(clientDeviceTypeIndex); lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType); clients.add(lastClient); } final String tabTitle = cursor.getString(tabTitleIndex); final String tabUrl = cursor.getString(tabUrlIndex); final long tabLastUsed = cursor.getLong(tabLastUsedIndex); lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl, tabLastUsed)); cursor.moveToNext(); } } finally { cursor.moveToPosition(originalPosition); } return clients; } @Override public Cursor getRemoteClientsByRecencyCursor(Context context) { final Uri uri = clientsRecencyUriWithProfile; return context.getContentResolver().query(uri, CLIENTS_PROJECTION_COLUMNS, REMOTE_CLIENTS_SELECTION, null, null); } @Override public Cursor getRemoteTabsCursor(Context context) { return getRemoteTabsCursor(context, -1); } @Override public Cursor getRemoteTabsCursor(Context context, int limit) { Uri uri = tabsUriWithProfile; if (limit > 0) { uri = uri.buildUpon() .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)) .build(); } final Cursor cursor = context.getContentResolver().query(uri, TABS_PROJECTION_COLUMNS, REMOTE_TABS_SELECTION, null, REMOTE_TABS_SORT_ORDER); return cursor; } // This method returns all tabs from all remote clients, // ordered by most recent client first, most recent tab first @Override public void getTabs(final Context context, final OnQueryTabsCompleteListener listener) { getTabs(context, 0, listener); } // This method returns limited number of tabs from all remote clients, // ordered by most recent client first, most recent tab first @Override public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) { // If there is no listener, no point in doing work. if (listener == null) return; (new UIAsyncTask.WithoutParams>(ThreadUtils.getBackgroundHandler()) { @Override protected List doInBackground() { final Cursor cursor = getRemoteTabsCursor(context, limit); if (cursor == null) return null; try { return Collections.unmodifiableList(getClientsFromCursor(cursor)); } finally { cursor.close(); } } @Override protected void onPostExecute(List clients) { listener.onQueryTabsComplete(clients); } }).execute(); } // Updates the modified time of the local client with the current time. private void updateLocalClient(final ContentResolver cr) { ContentValues values = new ContentValues(); values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis()); cr.update(clientsUriWithProfile, values, LOCAL_CLIENT_SELECTION, null); } // Deletes all local tabs. private void deleteLocalTabs(final ContentResolver cr) { cr.delete(tabsUriWithProfile, LOCAL_TABS_SELECTION, null); } /** * Tabs are positioned in the DB in the same order that they appear in the tabs param. * - URL should never empty or null. Skip this tab if there's no URL. * - TITLE should always a string, either a page title or empty. * - LAST_USED should always be numeric. * - FAVICON should be a URL or null. * - HISTORY should be serialized JSON array of URLs. * - POSITION should always be numeric. * - CLIENT_GUID should always be null to represent the local client. */ private void insertLocalTabs(final ContentResolver cr, final Iterable tabs) { // Reuse this for serializing individual history URLs as JSON. JSONArray history = new JSONArray(); ArrayList valuesToInsert = new ArrayList(); int position = 0; for (Tab tab : tabs) { // Skip this tab if it has a null URL or is in private browsing mode, or is a filtered URL. String url = tab.getURL(); if (url == null || tab.isPrivate() || isFilteredURL(url)) continue; ContentValues values = new ContentValues(); values.put(BrowserContract.Tabs.URL, url); values.put(BrowserContract.Tabs.TITLE, tab.getTitle()); values.put(BrowserContract.Tabs.LAST_USED, tab.getLastUsed()); String favicon = tab.getFaviconURL(); if (favicon != null) values.put(BrowserContract.Tabs.FAVICON, favicon); else values.putNull(BrowserContract.Tabs.FAVICON); // We don't have access to session history in Java, so for now, we'll // just use a JSONArray that holds most recent history item. try { history.put(0, tab.getURL()); values.put(BrowserContract.Tabs.HISTORY, history.toString()); } catch (JSONException e) { Log.w(LOGTAG, "JSONException adding URL to tab history array.", e); } values.put(BrowserContract.Tabs.POSITION, position++); // A null client guid corresponds to the local client. values.putNull(BrowserContract.Tabs.CLIENT_GUID); valuesToInsert.add(values); } ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]); cr.bulkInsert(tabsUriWithProfile, valuesToInsertArray); } // Deletes all local tabs and replaces them with a new list of tabs. @Override public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable tabs) { deleteLocalTabs(cr); insertLocalTabs(cr, tabs); updateLocalClient(cr); } /** * Matches the supplied URL string against the set of URLs to filter. * * @return true if the supplied URL should be skipped; false otherwise. */ private boolean isFilteredURL(String url) { return FILTERED_URL_PATTERN.matcher(url).lookingAt(); } }