/* 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.sync.repositories.android; import java.util.ArrayList; import org.json.simple.JSONArray; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserContract.Tabs; import org.mozilla.gecko.sync.Logger; import org.mozilla.gecko.sync.repositories.InactiveSessionException; import org.mozilla.gecko.sync.repositories.NoContentProviderException; import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; import org.mozilla.gecko.sync.repositories.Repository; import org.mozilla.gecko.sync.repositories.RepositorySession; import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; import org.mozilla.gecko.sync.repositories.domain.Record; import org.mozilla.gecko.sync.repositories.domain.TabsRecord; import org.mozilla.gecko.sync.repositories.domain.TabsRecord.Tab; import android.content.ContentProviderClient; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; public class FennecTabsRepository extends Repository { protected final String localClientName; protected final String localClientGuid; public FennecTabsRepository(final String localClientName, final String localClientGuid) { this.localClientName = localClientName; this.localClientGuid = localClientGuid; } /** * Note that — unlike most repositories — this will only fetch Fennec's tabs, * and only store tabs from other clients. * * It will never retrieve tabs from other clients, or store tabs for Fennec, * unless you use {@link #fetch(String[], RepositorySessionFetchRecordsDelegate)} * and specify an explicit GUID. */ public class FennecTabsRepositorySession extends RepositorySession { protected static final String LOG_TAG = "FennecTabsSession"; private final ContentProviderClient tabsProvider; private final ContentProviderClient clientsProvider; protected final RepoUtils.QueryHelper tabsHelper; protected ContentProviderClient getContentProvider(final Context context, final Uri uri) throws NoContentProviderException { ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(uri); if (client == null) { throw new NoContentProviderException(uri); } return client; } protected void releaseProviders() { try { clientsProvider.release(); } catch (Exception e) {} try { tabsProvider.release(); } catch (Exception e) {} } public FennecTabsRepositorySession(Repository repository, Context context) throws NoContentProviderException { super(repository); clientsProvider = getContentProvider(context, BrowserContract.Clients.CONTENT_URI); try { tabsProvider = getContentProvider(context, BrowserContract.Tabs.CONTENT_URI); } catch (NoContentProviderException e) { clientsProvider.release(); throw e; } catch (Exception e) { clientsProvider.release(); // Oh, Java. throw new RuntimeException(e); } tabsHelper = new RepoUtils.QueryHelper(context, BrowserContract.Tabs.CONTENT_URI, LOG_TAG); } @Override public void abort() { releaseProviders(); super.abort(); } @Override public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException { releaseProviders(); super.finish(delegate); } // Default parameters for local data: local client has null GUID. Override // these to test against non-live data. protected String localClientSelection() { return BrowserContract.Tabs.CLIENT_GUID + " IS NULL"; } protected String[] localClientSelectionArgs() { return null; } @Override public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) { // Bug 783692: Now that Bug 730039 has landed, we could implement this, // but it's not a priority since it's not used (yet). Logger.warn(LOG_TAG, "Not returning anything from guidsSince."); delegateQueue.execute(new Runnable() { @Override public void run() { delegate.onGuidsSinceSucceeded(new String[] {}); } }); } @Override public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) { if (tabsProvider == null) { throw new IllegalArgumentException("tabsProvider was null."); } if (tabsHelper == null) { throw new IllegalArgumentException("tabsHelper was null."); } final String positionAscending = BrowserContract.Tabs.POSITION + " ASC"; final String localClientSelection = localClientSelection(); final String[] localClientSelectionArgs = localClientSelectionArgs(); final Runnable command = new Runnable() { @Override public void run() { // We fetch all local tabs (since the record must contain them all) // but only process the record if the timestamp is sufficiently // recent. try { final Cursor cursor = tabsHelper.safeQuery(tabsProvider, ".fetchSince()", null, localClientSelection, localClientSelectionArgs, positionAscending); try { final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, localClientGuid, localClientName); if (tabsRecord.lastModified >= timestamp) { delegate.onFetchedRecord(tabsRecord); } } finally { cursor.close(); } } catch (Exception e) { delegate.onFetchFailed(e, null); return; } delegate.onFetchCompleted(now()); } }; delegateQueue.execute(command); } @Override public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) { // Bug 783692: Now that Bug 730039 has landed, we could implement this, // but it's not a priority since it's not used (yet). Logger.warn(LOG_TAG, "Not returning anything from fetch"); delegateQueue.execute(new Runnable() { @Override public void run() { delegate.onFetchCompleted(now()); } }); } @Override public void fetchAll(final RepositorySessionFetchRecordsDelegate delegate) { fetchSince(0, delegate); } private static final String TABS_CLIENT_GUID_IS = BrowserContract.Tabs.CLIENT_GUID + " = ?"; private static final String CLIENT_GUID_IS = BrowserContract.Clients.GUID + " = ?"; @Override public void store(final Record record) throws NoStoreDelegateException { if (delegate == null) { Logger.warn(LOG_TAG, "No store delegate."); throw new NoStoreDelegateException(); } if (record == null) { Logger.error(LOG_TAG, "Record sent to store was null"); throw new IllegalArgumentException("Null record passed to FennecTabsRepositorySession.store()."); } if (!(record instanceof TabsRecord)) { Logger.error(LOG_TAG, "Can't store anything but a TabsRecord"); throw new IllegalArgumentException("Non-TabsRecord passed to FennecTabsRepositorySession.store()."); } final TabsRecord tabsRecord = (TabsRecord) record; Runnable command = new Runnable() { @Override public void run() { Logger.debug(LOG_TAG, "Storing tabs for client " + tabsRecord.guid); if (!isActive()) { delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid); return; } if (tabsRecord.guid == null) { delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid); return; } try { // This is nice and easy: we *always* store. final String[] selectionArgs = new String[] { tabsRecord.guid }; if (tabsRecord.deleted) { try { Logger.debug(LOG_TAG, "Clearing entry for client " + tabsRecord.guid); clientsProvider.delete(BrowserContract.Clients.CONTENT_URI, CLIENT_GUID_IS, selectionArgs); delegate.onRecordStoreSucceeded(record.guid); } catch (Exception e) { delegate.onRecordStoreFailed(e, record.guid); } return; } // If it exists, update the client record; otherwise insert. final ContentValues clientsCV = tabsRecord.getClientsContentValues(); Logger.debug(LOG_TAG, "Updating clients provider."); final int updated = clientsProvider.update(BrowserContract.Clients.CONTENT_URI, clientsCV, CLIENT_GUID_IS, selectionArgs); if (0 == updated) { clientsProvider.insert(BrowserContract.Clients.CONTENT_URI, clientsCV); } // Now insert tabs. final ContentValues[] tabsArray = tabsRecord.getTabsContentValues(); Logger.debug(LOG_TAG, "Inserting " + tabsArray.length + " tabs for client " + tabsRecord.guid); tabsProvider.delete(BrowserContract.Tabs.CONTENT_URI, TABS_CLIENT_GUID_IS, selectionArgs); final int inserted = tabsProvider.bulkInsert(BrowserContract.Tabs.CONTENT_URI, tabsArray); Logger.trace(LOG_TAG, "Inserted: " + inserted); delegate.onRecordStoreSucceeded(record.guid); } catch (Exception e) { Logger.warn(LOG_TAG, "Error storing tabs.", e); delegate.onRecordStoreFailed(e, record.guid); } } }; storeWorkQueue.execute(command); } @Override public void wipe(RepositorySessionWipeDelegate delegate) { try { tabsProvider.delete(BrowserContract.Tabs.CONTENT_URI, null, null); clientsProvider.delete(BrowserContract.Clients.CONTENT_URI, null, null); } catch (RemoteException e) { Logger.warn(LOG_TAG, "Got RemoteException in wipe.", e); delegate.onWipeFailed(e); return; } delegate.onWipeSucceeded(); } } @Override public void createSession(RepositorySessionCreationDelegate delegate, Context context) { try { final FennecTabsRepositorySession session = new FennecTabsRepositorySession(this, context); delegate.onSessionCreated(session); } catch (Exception e) { delegate.onSessionCreateFailed(e); } } /** * Extract a <code>Tab</code> from a cursor row. * <p> * Caller is responsible for creating, positioning, and closing the cursor. * * @param cursor * to inspect. * @return <code>Tab</code> instance. */ public static Tab tabFromCursor(final Cursor cursor) { final String title = RepoUtils.getStringFromCursor(cursor, Tabs.TITLE); final String icon = RepoUtils.getStringFromCursor(cursor, Tabs.FAVICON); final JSONArray history = RepoUtils.getJSONArrayFromCursor(cursor, Tabs.HISTORY); final long lastUsed = RepoUtils.getLongFromCursor(cursor, Tabs.LAST_USED); return new Tab(title, icon, history, lastUsed); } /** * Extract a <code>TabsRecord</code> from a cursor. * <p> * Caller is responsible for creating and closing cursor. Each row of the * cursor should be an individual tab record. * <p> * The extracted tabs record has the given client GUID and client name. * * @param cursor * to inspect. * @param clientGuid * returned tabs record will have this client GUID. * @param clientName * returned tabs record will have this client name. * @return <code>TabsRecord</code> instance. */ public static TabsRecord tabsRecordFromCursor(final Cursor cursor, final String clientGuid, final String clientName) { final String collection = "tabs"; final TabsRecord record = new TabsRecord(clientGuid, collection, 0, false); record.tabs = new ArrayList<TabsRecord.Tab>(); record.clientName = clientName; record.androidID = -1; record.deleted = false; record.lastModified = 0; int position = cursor.getPosition(); try { cursor.moveToFirst(); while (!cursor.isAfterLast()) { final Tab tab = FennecTabsRepository.tabFromCursor(cursor); record.tabs.add(tab); if (tab.lastUsed > record.lastModified) { record.lastModified = tab.lastUsed; } cursor.moveToNext(); } } finally { cursor.moveToPosition(position); } return record; } }