1a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney/* 2a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * Copyright (C) 2010 The Android Open Source Project 3a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * 4a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * Licensed under the Apache License, Version 2.0 (the "License"); 5a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * you may not use this file except in compliance with the License. 6a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * You may obtain a copy of the License at 7a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * 8a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * http://www.apache.org/licenses/LICENSE-2.0 9a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * 10a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * Unless required by applicable law or agreed to in writing, software 11a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * distributed under the License is distributed on an "AS IS" BASIS, 12a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * See the License for the specific language governing permissions and 14a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * limitations under the License. 15a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney */ 16a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 17a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinneypackage com.android.quicksearchbox; 18a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 19b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringertimport com.google.common.annotations.VisibleForTesting; 20b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert 2187e947cbd9f279a83337900ff8bbd5ab0a8dc455Bjorn Bringertimport android.util.Log; 2287e947cbd9f279a83337900ff8bbd5ab0a8dc455Bjorn Bringert 2387e947cbd9f279a83337900ff8bbd5ab0a8dc455Bjorn Bringertimport java.util.Iterator; 2487e947cbd9f279a83337900ff8bbd5ab0a8dc455Bjorn Bringertimport java.util.LinkedList; 25a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 26a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney/** 27a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * A promoter that gives preference to suggestions from higher ranking corpora. 28a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney */ 29af1ca2cc65a2c2fdf6f396126e235d64e4da0936Mathew Inwoodpublic class RankAwarePromoter extends AbstractPromoter { 30a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 31b5fc08b7f16a32d3865f44b7f26d8aaa5304a2adBjorn Bringert private static final boolean DBG = false; 32bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert private static final String TAG = "QSB.RankAwarePromoter"; 33bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert 3427691bfcdcf3d2918b45bfadd57b08547c317ce5Mathew Inwood public RankAwarePromoter(Config config, SuggestionFilter filter, Promoter next) { 3527691bfcdcf3d2918b45bfadd57b08547c317ce5Mathew Inwood super(filter, next, config); 36b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert } 37b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert 38af1ca2cc65a2c2fdf6f396126e235d64e4da0936Mathew Inwood @Override 3927691bfcdcf3d2918b45bfadd57b08547c317ce5Mathew Inwood public void doPickPromoted(Suggestions suggestions, 4039bbcdc1a485ded93059de4a3f70bfda85e9f304Bryan Mawhinney int maxPromoted, ListSuggestionCursor promoted) { 41b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert promoteSuggestions(suggestions.getCorpusResults(), maxPromoted, promoted); 42b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert } 43a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 44b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert @VisibleForTesting 45b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert void promoteSuggestions(Iterable<CorpusResult> suggestions, int maxPromoted, 46b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert ListSuggestionCursor promoted) { 47bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert if (DBG) Log.d(TAG, "Available results: " + suggestions); 48bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert 499a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // Split non-empty results into important suggestions and not-so-important 509a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // suggestions, each corpus's cursor positioned at the first suggestion. 519a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay LinkedList<CorpusResult> highRankingSuggestions = new LinkedList<CorpusResult>(); 529a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay LinkedList<CorpusResult> lowRankingSuggestions = new LinkedList<CorpusResult>(); 539a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay partitionSuggestionsByRank(suggestions, highRankingSuggestions, lowRankingSuggestions); 549a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 559a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // Top results, evenly distributed between each high-ranking corpus. 569a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay promoteTopSuggestions(highRankingSuggestions, promoted, maxPromoted); 579a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 589a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // Then try to fill promoted list with the remaining high-ranking suggestions, 599a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // and then use the low-ranking suggestions if the list isn't full yet. 609a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay promoteEquallyFromEachCorpus(highRankingSuggestions, promoted, maxPromoted); 619a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay promoteEquallyFromEachCorpus(lowRankingSuggestions, promoted, maxPromoted); 629a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 639a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay if (DBG) Log.d(TAG, "Returning " + promoted.toString()); 649a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 65a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 669a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay /** 679a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * Shares the top slots evenly among each of the high-ranking (default) corpora. 689a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * 699a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * The corpora will appear in the promoted list in the order they are listed 709a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * among the incoming suggestions (this method doesn't change their order). 719a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay */ 729a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay private void promoteTopSuggestions(LinkedList<CorpusResult> highRankingSuggestions, 739a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay ListSuggestionCursor promoted, int maxPromoted) { 74fdb80c2962c88ac62dcd7ee7f2fab1857b61506bMathew Inwood 759a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay int slotsLeft = getSlotsLeft(promoted, maxPromoted); 769a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay if (slotsLeft > 0 && !highRankingSuggestions.isEmpty()) { 77fdb80c2962c88ac62dcd7ee7f2fab1857b61506bMathew Inwood int slotsToFill = Math.min(getSlotsAboveKeyboard() - promoted.getCount(), slotsLeft); 789a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 7939bbcdc1a485ded93059de4a3f70bfda85e9f304Bryan Mawhinney if (slotsToFill > 0) { 809a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay int stripeSize = Math.max(1, slotsToFill / highRankingSuggestions.size()); 819a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay roundRobin(highRankingSuggestions, slotsToFill, stripeSize, promoted); 8239bbcdc1a485ded93059de4a3f70bfda85e9f304Bryan Mawhinney } 8339bbcdc1a485ded93059de4a3f70bfda85e9f304Bryan Mawhinney } 849a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 859a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 869a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay /** 879a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * Tries to promote the same number of elements from each corpus. 889a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * 899a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * The corpora will appear in the promoted list in the order they are listed 909a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * among the incoming suggestions (this method doesn't change their order). 919a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay */ 929a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay private void promoteEquallyFromEachCorpus(LinkedList<CorpusResult> suggestions, 939a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay ListSuggestionCursor promoted, int maxPromoted) { 94a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 959a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay int slotsLeft = getSlotsLeft(promoted, maxPromoted); 969a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay if (slotsLeft == 0) { 979a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // No more items to add. 989a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay return; 99a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney } 100a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 1019a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay if (suggestions.isEmpty()) { 1029a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay return; 103a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney } 104bf61e445cbe423cc2554b722b6dd38675015c36dBjorn Bringert 1059a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay int stripeSize = Math.max(1, slotsLeft / suggestions.size()); 1069a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay roundRobin(suggestions, slotsLeft, stripeSize, promoted); 1079a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 1089a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // We may still have a few slots left 1099a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay slotsLeft = getSlotsLeft(promoted, maxPromoted); 1109a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay roundRobin(suggestions, slotsLeft, slotsLeft, promoted); 1119a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 1129a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 1139a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay /** 1149a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * Partitions the suggestions into "important" (high-ranking) 1159a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * and "not-so-important" (low-ranking) suggestions, dependent on the 1169a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * rank of the corpus the result is part of. 1179a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * 1189a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * @param suggestions 1199a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * @param highRankingSuggestions These should be displayed first to the 1209a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * user. 1219a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * @param lowRankingSuggestions These should be displayed if the 1229a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * high-ranking suggestions don't fill all the available space in the 1239a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay * result view. 1249a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay */ 1259a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay private void partitionSuggestionsByRank(Iterable<CorpusResult> suggestions, 1269a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay LinkedList<CorpusResult> highRankingSuggestions, 1279a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay LinkedList<CorpusResult> lowRankingSuggestions) { 1289a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 1299a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay for (CorpusResult result : suggestions) { 1309a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay if (result.getCount() > 0) { 1319a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay result.moveTo(0); 1329a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay Corpus corpus = result.getCorpus(); 1339a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay if (isCorpusHighlyRanked(corpus)) { 1349a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay highRankingSuggestions.add(result); 1359a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } else { 1369a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay lowRankingSuggestions.add(result); 1379a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 1389a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 1399a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 1409a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 1419a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 1429a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay private boolean isCorpusHighlyRanked(Corpus corpus) { 1439a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // The default corpora shipped with QSB (apps, etc.) are 1449a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // more important than ones that were registered later. 1459a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay return corpus == null || corpus.isCorpusDefaultEnabled(); 1469a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 1479a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay 1489a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay private int getSlotsLeft(ListSuggestionCursor promoted, int maxPromoted) { 1499a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // It's best to calculate this after each addition because duplicates 1509a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // may get filtered out automatically in the list of promoted items. 1519a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay return Math.max(0, maxPromoted - promoted.getCount()); 152a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney } 153a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 15439bbcdc1a485ded93059de4a3f70bfda85e9f304Bryan Mawhinney private int getSlotsAboveKeyboard() { 155af1ca2cc65a2c2fdf6f396126e235d64e4da0936Mathew Inwood return getConfig().getNumSuggestionsAboveKeyboard(); 15639bbcdc1a485ded93059de4a3f70bfda85e9f304Bryan Mawhinney } 15739bbcdc1a485ded93059de4a3f70bfda85e9f304Bryan Mawhinney 158a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney /** 159a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * Promotes "stripes" of suggestions from each corpus. 160a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * 161a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @param results the list of CorpusResults from which to promote. 162a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * Exhausted CorpusResults are removed from the list. 163a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @param maxPromoted maximum number of suggestions to promote. 164a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @param stripeSize number of suggestions to take from each corpus. 165a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @param promoted the list to which promoted suggestions are added. 166a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @return the number of suggestions actually promoted. 167a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney */ 168a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney private int roundRobin(LinkedList<CorpusResult> results, int maxPromoted, int stripeSize, 169a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney ListSuggestionCursor promoted) { 170a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney int count = 0; 171a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney if (maxPromoted > 0 && !results.isEmpty()) { 172a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney for (Iterator<CorpusResult> iter = results.iterator(); 173a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney count < maxPromoted && iter.hasNext();) { 174a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney CorpusResult result = iter.next(); 175a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney count += promote(result, stripeSize, promoted); 176a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney if (result.getPosition() == result.getCount()) { 177a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney iter.remove(); 178a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney } 179a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney } 180a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney } 181a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney return count; 182a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney } 183a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney 184a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney /** 185a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * Copies suggestions from a SuggestionCursor to the list of promoted suggestions. 186a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * 187a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @param cursor from which to copy the suggestions 188a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @param count maximum number of suggestions to copy 189a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @param promoted the list to which to add the suggestions 190a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney * @return the number of suggestions actually copied. 191a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney */ 192a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney private int promote(SuggestionCursor cursor, int count, ListSuggestionCursor promoted) { 19387e947cbd9f279a83337900ff8bbd5ab0a8dc455Bjorn Bringert if (count < 1 || cursor.getPosition() >= cursor.getCount()) { 19487e947cbd9f279a83337900ff8bbd5ab0a8dc455Bjorn Bringert return 0; 19587e947cbd9f279a83337900ff8bbd5ab0a8dc455Bjorn Bringert } 196b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert int addedCount = 0; 19787e947cbd9f279a83337900ff8bbd5ab0a8dc455Bjorn Bringert do { 198b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert if (accept(cursor)) { 1999a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay if (promoted.add(new SuggestionPosition(cursor))) { 2009a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay // Added successfully (wasn't already promoted). 2019a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay addedCount++; 2029a8c6b416ac7ab00998d3496226712f1d442838fPeter Visontay } 203b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert } 204b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert } while (cursor.moveToNext() && addedCount < count); 205b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert return addedCount; 206a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney } 207b83882b9efa37ec0f20a0f1c85cf5ccc93194aeeBjorn Bringert 208a4cd9e7cdd5bdc6198ce73eed55696554a146514Bryan Mawhinney} 209