1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.quicksearchbox;
18
19
20import com.android.quicksearchbox.util.BarrierConsumer;
21
22import android.content.Context;
23
24import java.util.ArrayList;
25import java.util.Collection;
26import java.util.List;
27import java.util.concurrent.Executor;
28
29/**
30 * Base class for corpora backed by multiple sources.
31 */
32public abstract class MultiSourceCorpus extends AbstractCorpus {
33
34    private final Executor mExecutor;
35
36    private final ArrayList<Source> mSources;
37
38    // calculated values based on properties of sources:
39    private boolean mSourcePropertiesValid;
40    private int mQueryThreshold;
41    private boolean mQueryAfterZeroResults;
42    private boolean mVoiceSearchEnabled;
43    private boolean mIncludeInAll;
44
45    public MultiSourceCorpus(Context context, Config config,
46            Executor executor, Source... sources) {
47        super(context, config);
48        mExecutor = executor;
49
50        mSources = new ArrayList<Source>();
51        for (Source source : sources) {
52            addSource(source);
53        }
54
55    }
56
57    protected void addSource(Source source) {
58        if (source != null) {
59            mSources.add(source);
60            // invalidate calculated values:
61            mSourcePropertiesValid = false;
62        }
63    }
64
65    public Collection<Source> getSources() {
66        return mSources;
67    }
68
69    /**
70     * Creates a corpus result object for a set of source results.
71     * This method should not call {@link Result#fill}.
72     *
73     * @param query The query text.
74     * @param results The results of the queries.
75     * @param latency Latency in milliseconds of the suggestion queries.
76     * @return An instance of {@link Result} or a subclass of it.
77     */
78    protected Result createResult(String query, ArrayList<SourceResult> results, int latency) {
79        return new Result(query, results, latency);
80    }
81
82    /**
83     * Gets the sources to query for suggestions for the given input.
84     *
85     * @param query The current input.
86     * @param onlyCorpus If true, this is the only corpus being queried.
87     * @return The sources to query.
88     */
89    protected List<Source> getSourcesToQuery(String query, boolean onlyCorpus) {
90        List<Source> sources = new ArrayList<Source>();
91        for (Source candidate : getSources()) {
92            if (candidate.getQueryThreshold() <= query.length()) {
93                sources.add(candidate);
94            }
95        }
96        return sources;
97    }
98
99    private void updateSourceProperties() {
100        if (mSourcePropertiesValid) return;
101        mQueryThreshold = Integer.MAX_VALUE;
102        mQueryAfterZeroResults = false;
103        mVoiceSearchEnabled = false;
104        mIncludeInAll = false;
105        for (Source s : getSources()) {
106            mQueryThreshold = Math.min(mQueryThreshold, s.getQueryThreshold());
107            mQueryAfterZeroResults |= s.queryAfterZeroResults();
108            mVoiceSearchEnabled |= s.voiceSearchEnabled();
109            mIncludeInAll |= s.includeInAll();
110        }
111        if (mQueryThreshold == Integer.MAX_VALUE) {
112            mQueryThreshold = 0;
113        }
114        mSourcePropertiesValid = true;
115    }
116
117    public int getQueryThreshold() {
118        updateSourceProperties();
119        return mQueryThreshold;
120    }
121
122    public boolean queryAfterZeroResults() {
123        updateSourceProperties();
124        return mQueryAfterZeroResults;
125    }
126
127    public boolean voiceSearchEnabled() {
128        updateSourceProperties();
129        return mVoiceSearchEnabled;
130    }
131
132    public boolean includeInAll() {
133        updateSourceProperties();
134        return mIncludeInAll;
135    }
136
137    public CorpusResult getSuggestions(String query, int queryLimit, boolean onlyCorpus) {
138        LatencyTracker latencyTracker = new LatencyTracker();
139        List<Source> sources = getSourcesToQuery(query, onlyCorpus);
140        BarrierConsumer<SourceResult> consumer =
141                new BarrierConsumer<SourceResult>(sources.size());
142        boolean onlySource = sources.size() == 1;
143        for (Source source : sources) {
144            QueryTask<SourceResult> task = new QueryTask<SourceResult>(query, queryLimit,
145                    source, null, consumer, onlySource);
146            mExecutor.execute(task);
147        }
148        ArrayList<SourceResult> results = consumer.getValues();
149        int latency = latencyTracker.getLatency();
150        Result result = createResult(query, results, latency);
151        result.fill();
152        return result;
153    }
154
155    /**
156     * Base class for results returned by {@link MultiSourceCorpus#getSuggestions}.
157     * Subclasses of {@link MultiSourceCorpus} should override
158     * {@link MultiSourceCorpus#createResult} and return an instance of this class or a
159     * subclass.
160     */
161    protected class Result extends ListSuggestionCursor implements CorpusResult {
162
163        private final ArrayList<SourceResult> mResults;
164
165        private final int mLatency;
166
167        public Result(String userQuery, ArrayList<SourceResult> results, int latency) {
168            super(userQuery);
169            mResults = results;
170            mLatency = latency;
171        }
172
173        protected ArrayList<SourceResult> getResults() {
174            return mResults;
175        }
176
177        /**
178         * Fills the list of suggestions using the list of results.
179         * The default implementation concatenates the results.
180         */
181        public void fill() {
182            for (SourceResult result : getResults()) {
183                int count = result.getCount();
184                for (int i = 0; i < count; i++) {
185                    result.moveTo(i);
186                    add(new SuggestionPosition(result));
187                }
188            }
189        }
190
191        public Corpus getCorpus() {
192            return MultiSourceCorpus.this;
193        }
194
195        public int getLatency() {
196            return mLatency;
197        }
198
199        @Override
200        public void close() {
201            super.close();
202            for (SourceResult result : mResults) {
203                result.close();
204            }
205        }
206
207        @Override
208        public String toString() {
209            return "{" + getCorpus() + "[" + getUserQuery() + "]" + ";n=" + getCount() + "}";
210        }
211    }
212
213}
214