All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.lite.LiveQuery Maven / Gradle / Ivy

//
// Copyright (c) 2020, 2017 Couchbase, Inc All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package com.couchbase.lite;

import android.support.annotation.GuardedBy;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;

import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;

import com.couchbase.lite.internal.support.Log;
import com.couchbase.lite.internal.utils.ClassUtils;
import com.couchbase.lite.internal.utils.Preconditions;


/**
 * A Query subclass that automatically refreshes the result rows every time the database changes.
 * 

* Be careful with the state machine here: * A query that has been STOPPED can be STARTED again! * In particular, a query that is stopping when it receives a request to restart * should suspend the restart request, finish stopping, and then restart. */ final class LiveQuery implements DatabaseChangeListener { //--------------------------------------------- // static variables //--------------------------------------------- private static final LogDomain DOMAIN = LogDomain.QUERY; @VisibleForTesting static final long LIVE_QUERY_UPDATE_INTERVAL_MS = 200; // 0.2sec (200ms) @VisibleForTesting enum State {STOPPED, STARTED, SCHEDULED} //--------------------------------------------- // member variables //--------------------------------------------- private final ChangeNotifier changeNotifier = new ChangeNotifier<>(); private final AtomicReference state = new AtomicReference<>(State.STOPPED); @NonNull private final AbstractQuery query; private final Object lock = new Object(); @GuardedBy("lock") private ListenerToken dbListenerToken; @GuardedBy("lock") private ResultSet previousResults; //--------------------------------------------- // Constructors //--------------------------------------------- LiveQuery(@NonNull AbstractQuery query) { Preconditions.assertNotNull(query, "query"); this.query = query; } //--------------------------------------------- // API - public methods //--------------------------------------------- @NonNull @Override public String toString() { return "LiveQuery{" + ClassUtils.objId(this) + "," + query.toString() + "}"; } //--------------------------------------------- // Implementation of DatabaseChangeListener //--------------------------------------------- @Override public void changed(@NonNull DatabaseChange change) { update(LIVE_QUERY_UPDATE_INTERVAL_MS); } //--------------------------------------------- // package //--------------------------------------------- /** * Adds a change listener. *

* NOTE: this method is synchronized with Query level. */ ListenerToken addChangeListener(Executor executor, QueryChangeListener listener) { final ChangeListenerToken token = changeNotifier.addChangeListener(executor, listener); start(false); return token; } void removeChangeListener(ListenerToken token) { if (changeNotifier.removeChangeListener(token) <= 0) { stop(); } } /** * Starts observing database changes and reports changes in the query result. */ void start(boolean shouldClearResults) { final Database db = Preconditions.assertNotNull(query.getDatabase(), "Live query database"); // can't have the db closing while a query is starting. synchronized (db.getLock()) { db.mustBeOpen(); if (state.compareAndSet(State.STOPPED, State.STARTED)) { synchronized (lock) { dbListenerToken = db.addActiveLiveQuery(this); } } else { // Here if the live query was already running. This can happen in two ways: // 1) when adding another listener // 2) when the query parameters have changed. // In either case we probably want to kick off a new query. // In the latter case the current query results are irrelevant and need to be cleared. if (shouldClearResults) { synchronized (lock) { previousResults = null; } } } } update(0); } void stop() { final Database db = query.getDatabase(); if (db == null) { if (State.STOPPED != state.get()) { Log.w(LogDomain.DATABASE, "Null db when stopping LiveQuery"); } return; } synchronized (db.getLock()) { if (State.STOPPED == state.getAndSet(State.STOPPED)) { return; } synchronized (lock) { previousResults = null; final ListenerToken token = dbListenerToken; dbListenerToken = null; if (token == null) { return; } db.removeActiveLiveQuery(this, token); } } } State getState() { return state.get(); } //--------------------------------------------- // Private (in class only) //--------------------------------------------- private void update(long delay) { if (!state.compareAndSet(State.STARTED, State.SCHEDULED)) { return; } query.getDatabase().scheduleOnQueryExecutor(this::refreshResults, delay); } // Runs on the query.database.queryExecutor // Assumes that call to `previousResults.refresh` is safe, even if previousResults has been freed. @SuppressWarnings("PMD.CloseResource") private void refreshResults() { try { final ResultSet prevResults; synchronized (lock) { if (!state.compareAndSet(State.SCHEDULED, State.STARTED)) { return; } prevResults = previousResults; } final ResultSet newResults = (prevResults == null) ? query.execute() : prevResults.refresh(); Log.i(DOMAIN, "LiveQuery refresh: %s > %s", prevResults, newResults); if (newResults == null) { return; } boolean update = false; synchronized (lock) { if (state.get() != State.STOPPED) { previousResults = newResults; update = true; } } // Listeners may be notified even after the LiveQuery has been stopped. if (update) { changeNotifier.postChange(new QueryChange(query, newResults, null)); } } catch (CouchbaseLiteException err) { changeNotifier.postChange(new QueryChange(query, null, err)); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy