michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.db.BrowserContract; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.util.UiAsyncTask; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: michael@0: import android.content.ContentResolver; michael@0: import android.content.ContentValues; michael@0: import android.content.Context; michael@0: import android.database.Cursor; michael@0: import android.net.Uri; michael@0: import android.util.Log; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.Collections; michael@0: import java.util.List; michael@0: michael@0: public final class TabsAccessor { michael@0: private static final String LOGTAG = "GeckoTabsAccessor"; michael@0: michael@0: private static final String[] CLIENTS_AVAILABILITY_PROJECTION = new String[] { michael@0: BrowserContract.Clients.GUID michael@0: }; michael@0: michael@0: private static final String[] TABS_PROJECTION_COLUMNS = new String[] { michael@0: BrowserContract.Tabs.TITLE, michael@0: BrowserContract.Tabs.URL, michael@0: BrowserContract.Clients.GUID, michael@0: BrowserContract.Clients.NAME michael@0: }; michael@0: michael@0: // Projection column numbers michael@0: public static enum TABS_COLUMN { michael@0: TITLE, michael@0: URL, michael@0: GUID, michael@0: NAME michael@0: }; michael@0: michael@0: private static final String CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL"; michael@0: private static final String TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL"; michael@0: michael@0: private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL"; michael@0: private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL"; michael@0: michael@0: public static class RemoteTab { michael@0: public String title; michael@0: public String url; michael@0: public String guid; michael@0: public String name; michael@0: } michael@0: michael@0: public interface OnQueryTabsCompleteListener { michael@0: public void onQueryTabsComplete(List tabs); michael@0: } michael@0: michael@0: // This method returns all tabs from all remote clients, michael@0: // ordered by most recent client first, most recent tab first michael@0: public static void getTabs(final Context context, final OnQueryTabsCompleteListener listener) { michael@0: getTabs(context, 0, listener); michael@0: } michael@0: michael@0: // This method returns limited number of tabs from all remote clients, michael@0: // ordered by most recent client first, most recent tab first michael@0: public static void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) { michael@0: // If there is no listener, no point in doing work. michael@0: if (listener == null) michael@0: return; michael@0: michael@0: (new UiAsyncTask>(ThreadUtils.getBackgroundHandler()) { michael@0: @Override michael@0: protected List doInBackground(Void... unused) { michael@0: Uri uri = BrowserContract.Tabs.CONTENT_URI; michael@0: michael@0: if (limit > 0) { michael@0: uri = uri.buildUpon() michael@0: .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)) michael@0: .build(); michael@0: } michael@0: michael@0: Cursor cursor = context.getContentResolver().query(uri, michael@0: TABS_PROJECTION_COLUMNS, michael@0: TABS_SELECTION, michael@0: null, michael@0: null); michael@0: michael@0: if (cursor == null) michael@0: return null; michael@0: michael@0: RemoteTab tab; michael@0: final ArrayList tabs = new ArrayList (); michael@0: try { michael@0: while (cursor.moveToNext()) { michael@0: tab = new RemoteTab(); michael@0: tab.title = cursor.getString(TABS_COLUMN.TITLE.ordinal()); michael@0: tab.url = cursor.getString(TABS_COLUMN.URL.ordinal()); michael@0: tab.guid = cursor.getString(TABS_COLUMN.GUID.ordinal()); michael@0: tab.name = cursor.getString(TABS_COLUMN.NAME.ordinal()); michael@0: michael@0: tabs.add(tab); michael@0: } michael@0: } finally { michael@0: cursor.close(); michael@0: } michael@0: michael@0: return Collections.unmodifiableList(tabs); michael@0: } michael@0: michael@0: @Override michael@0: protected void onPostExecute(List tabs) { michael@0: listener.onQueryTabsComplete(tabs); michael@0: } michael@0: }).execute(); michael@0: } michael@0: michael@0: // Updates the modified time of the local client with the current time. michael@0: private static void updateLocalClient(final ContentResolver cr) { michael@0: ContentValues values = new ContentValues(); michael@0: values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis()); michael@0: cr.update(BrowserContract.Clients.CONTENT_URI, values, LOCAL_CLIENT_SELECTION, null); michael@0: } michael@0: michael@0: // Deletes all local tabs. michael@0: private static void deleteLocalTabs(final ContentResolver cr) { michael@0: cr.delete(BrowserContract.Tabs.CONTENT_URI, LOCAL_TABS_SELECTION, null); michael@0: } michael@0: michael@0: /** michael@0: * Tabs are positioned in the DB in the same order that they appear in the tabs param. michael@0: * - URL should never empty or null. Skip this tab if there's no URL. michael@0: * - TITLE should always a string, either a page title or empty. michael@0: * - LAST_USED should always be numeric. michael@0: * - FAVICON should be a URL or null. michael@0: * - HISTORY should be serialized JSON array of URLs. michael@0: * - POSITION should always be numeric. michael@0: * - CLIENT_GUID should always be null to represent the local client. michael@0: */ michael@0: private static void insertLocalTabs(final ContentResolver cr, final Iterable tabs) { michael@0: // Reuse this for serializing individual history URLs as JSON. michael@0: JSONArray history = new JSONArray(); michael@0: ArrayList valuesToInsert = new ArrayList(); michael@0: michael@0: int position = 0; michael@0: for (Tab tab : tabs) { michael@0: // Skip this tab if it has a null URL or is in private browsing mode michael@0: String url = tab.getURL(); michael@0: if (url == null || tab.isPrivate()) michael@0: continue; michael@0: michael@0: ContentValues values = new ContentValues(); michael@0: values.put(BrowserContract.Tabs.URL, url); michael@0: values.put(BrowserContract.Tabs.TITLE, tab.getTitle()); michael@0: values.put(BrowserContract.Tabs.LAST_USED, tab.getLastUsed()); michael@0: michael@0: String favicon = tab.getFaviconURL(); michael@0: if (favicon != null) michael@0: values.put(BrowserContract.Tabs.FAVICON, favicon); michael@0: else michael@0: values.putNull(BrowserContract.Tabs.FAVICON); michael@0: michael@0: // We don't have access to session history in Java, so for now, we'll michael@0: // just use a JSONArray that holds most recent history item. michael@0: try { michael@0: history.put(0, tab.getURL()); michael@0: values.put(BrowserContract.Tabs.HISTORY, history.toString()); michael@0: } catch (JSONException e) { michael@0: Log.w(LOGTAG, "JSONException adding URL to tab history array.", e); michael@0: } michael@0: michael@0: values.put(BrowserContract.Tabs.POSITION, position++); michael@0: michael@0: // A null client guid corresponds to the local client. michael@0: values.putNull(BrowserContract.Tabs.CLIENT_GUID); michael@0: michael@0: valuesToInsert.add(values); michael@0: } michael@0: michael@0: ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]); michael@0: cr.bulkInsert(BrowserContract.Tabs.CONTENT_URI, valuesToInsertArray); michael@0: } michael@0: michael@0: // Deletes all local tabs and replaces them with a new list of tabs. michael@0: public static synchronized void persistLocalTabs(final ContentResolver cr, final Iterable tabs) { michael@0: deleteLocalTabs(cr); michael@0: insertLocalTabs(cr, tabs); michael@0: updateLocalClient(cr); michael@0: } michael@0: }