1 /*
2  * Copyright (C) 2015 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 
17 package com.android.tv.search;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.database.Cursor;
23 import android.media.tv.TvContentRating;
24 import android.media.tv.TvContract;
25 import android.media.tv.TvContract.Channels;
26 import android.media.tv.TvContract.Programs;
27 import android.media.tv.TvContract.WatchedPrograms;
28 import android.media.tv.TvInputInfo;
29 import android.media.tv.TvInputManager;
30 import android.net.Uri;
31 import android.os.SystemClock;
32 import android.support.annotation.WorkerThread;
33 import android.text.TextUtils;
34 import android.util.Log;
35 import com.android.tv.common.TvContentRatingCache;
36 import com.android.tv.common.util.PermissionUtils;
37 import com.android.tv.search.LocalSearchProvider.SearchResult;
38 import com.android.tv.util.Utils;
39 import com.google.common.collect.ImmutableList;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.Comparator;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.Set;
50 import java.util.concurrent.TimeUnit;
51 
52 /** An implementation of {@link SearchInterface} to search query from TvProvider directly. */
53 public class TvProviderSearch implements SearchInterface {
54     private static final String TAG = "TvProviderSearch";
55     private static final boolean DEBUG = false;
56 
57     private static final long SEARCH_TIME_FRAME_MS = TimeUnit.DAYS.toMillis(14);
58 
59     private static final int NO_LIMIT = 0;
60 
61     private final Context mContext;
62     private final ContentResolver mContentResolver;
63     private final TvInputManager mTvInputManager;
64     private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
65 
TvProviderSearch(Context context)66     TvProviderSearch(Context context) {
67         mContext = context;
68         mContentResolver = context.getContentResolver();
69         mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
70     }
71 
72     /**
73      * Search channels, inputs, or programs from TvProvider. This assumes that parental control
74      * settings will not be change while searching.
75      *
76      * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT},
77      *     or {@link #ACTION_TYPE_AMBIGUOUS},
78      */
79     @Override
80     @WorkerThread
search(String query, int limit, int action)81     public List<SearchResult> search(String query, int limit, int action) {
82         // TODO(b/72499463): add a test.
83         List<SearchResult> results = new ArrayList<>();
84         if (!PermissionUtils.hasAccessAllEpg(mContext)) {
85             // TODO: support this feature for non-system LC app. b/23939816
86             return results;
87         }
88         Set<Long> channelsFound = new HashSet<>();
89         if (action == ACTION_TYPE_SWITCH_CHANNEL) {
90             results.addAll(searchChannels(query, channelsFound, limit));
91         } else if (action == ACTION_TYPE_SWITCH_INPUT) {
92             results.addAll(searchInputs(query, limit));
93         } else {
94             // Search channels first.
95             results.addAll(searchChannels(query, channelsFound, limit));
96             if (results.size() >= limit) {
97                 return results;
98             }
99 
100             // In case the user wanted to perform the action "switch to XXX", which is indicated by
101             // setting the limit to 1, search inputs.
102             if (limit == 1) {
103                 results.addAll(searchInputs(query, limit));
104                 if (!results.isEmpty()) {
105                     return results;
106                 }
107             }
108 
109             // Lastly, search programs.
110             limit -= results.size();
111             results.addAll(
112                     searchPrograms(
113                             query,
114                             null,
115                             new String[] {Programs.COLUMN_TITLE, Programs.COLUMN_SHORT_DESCRIPTION},
116                             channelsFound,
117                             limit));
118         }
119         return results;
120     }
121 
appendSelectionString( StringBuilder sb, String[] columnForExactMatching, String[] columnForPartialMatching)122     private void appendSelectionString(
123             StringBuilder sb, String[] columnForExactMatching, String[] columnForPartialMatching) {
124         boolean firstColumn = true;
125         if (columnForExactMatching != null) {
126             for (String column : columnForExactMatching) {
127                 if (!firstColumn) {
128                     sb.append(" OR ");
129                 } else {
130                     firstColumn = false;
131                 }
132                 sb.append(column).append("=?");
133             }
134         }
135         if (columnForPartialMatching != null) {
136             for (String column : columnForPartialMatching) {
137                 if (!firstColumn) {
138                     sb.append(" OR ");
139                 } else {
140                     firstColumn = false;
141                 }
142                 sb.append(column).append(" LIKE ?");
143             }
144         }
145     }
146 
insertSelectionArgumentStrings( String[] selectionArgs, int pos, String query, String[] columnForExactMatching, String[] columnForPartialMatching)147     private void insertSelectionArgumentStrings(
148             String[] selectionArgs,
149             int pos,
150             String query,
151             String[] columnForExactMatching,
152             String[] columnForPartialMatching) {
153         if (columnForExactMatching != null) {
154             int until = pos + columnForExactMatching.length;
155             for (; pos < until; ++pos) {
156                 selectionArgs[pos] = query;
157             }
158         }
159         String selectionArg = "%" + query + "%";
160         if (columnForPartialMatching != null) {
161             int until = pos + columnForPartialMatching.length;
162             for (; pos < until; ++pos) {
163                 selectionArgs[pos] = selectionArg;
164             }
165         }
166     }
167 
168     @WorkerThread
searchChannels(String query, Set<Long> channels, int limit)169     private List<SearchResult> searchChannels(String query, Set<Long> channels, int limit) {
170         if (DEBUG) Log.d(TAG, "Searching channels: '" + query + "'");
171         long time = SystemClock.elapsedRealtime();
172         List<SearchResult> results = new ArrayList<>();
173         if (TextUtils.isDigitsOnly(query)) {
174             results.addAll(
175                     searchChannels(
176                             query,
177                             new String[] {Channels.COLUMN_DISPLAY_NUMBER},
178                             null,
179                             channels,
180                             NO_LIMIT));
181             if (results.size() > 1) {
182                 Collections.sort(results, new ChannelComparatorWithSameDisplayNumber());
183             }
184         }
185         if (results.size() < limit) {
186             results.addAll(
187                     searchChannels(
188                             query,
189                             null,
190                             new String[] {
191                                 Channels.COLUMN_DISPLAY_NAME, Channels.COLUMN_DESCRIPTION
192                             },
193                             channels,
194                             limit - results.size()));
195         }
196         if (results.size() > limit) {
197             results = results.subList(0, limit);
198         }
199         for (int i = 0; i < results.size(); i++) {
200             results.set(i, fillProgramInfo(results.get(i)));
201         }
202         if (DEBUG) {
203             Log.d(
204                     TAG,
205                     "Found "
206                             + results.size()
207                             + " channels. Elapsed time for searching"
208                             + " channels: "
209                             + (SystemClock.elapsedRealtime() - time)
210                             + "(msec)");
211         }
212         return results;
213     }
214 
215     @WorkerThread
searchChannels( String query, String[] columnForExactMatching, String[] columnForPartialMatching, Set<Long> channelsFound, int limit)216     private List<SearchResult> searchChannels(
217             String query,
218             String[] columnForExactMatching,
219             String[] columnForPartialMatching,
220             Set<Long> channelsFound,
221             int limit) {
222         String[] projection = {
223             Channels._ID,
224             Channels.COLUMN_DISPLAY_NUMBER,
225             Channels.COLUMN_DISPLAY_NAME,
226             Channels.COLUMN_DESCRIPTION
227         };
228 
229         StringBuilder sb = new StringBuilder();
230         sb.append(Channels.COLUMN_BROWSABLE)
231                 .append("=1 AND ")
232                 .append(Channels.COLUMN_SEARCHABLE)
233                 .append("=1");
234         if (mTvInputManager.isParentalControlsEnabled()) {
235             sb.append(" AND ").append(Channels.COLUMN_LOCKED).append("=0");
236         }
237         sb.append(" AND (");
238         appendSelectionString(sb, columnForExactMatching, columnForPartialMatching);
239         sb.append(")");
240         String selection = sb.toString();
241 
242         int len =
243                 (columnForExactMatching == null ? 0 : columnForExactMatching.length)
244                         + (columnForPartialMatching == null ? 0 : columnForPartialMatching.length);
245         String[] selectionArgs = new String[len];
246         insertSelectionArgumentStrings(
247                 selectionArgs, 0, query, columnForExactMatching, columnForPartialMatching);
248 
249         List<SearchResult> searchResults = new ArrayList<>();
250 
251         try (Cursor c =
252                 mContentResolver.query(
253                         Channels.CONTENT_URI, projection, selection, selectionArgs, null)) {
254             if (c != null) {
255                 int count = 0;
256                 while (c.moveToNext()) {
257                     long id = c.getLong(0);
258                     // Filter out the channel which has been already searched.
259                     if (channelsFound.contains(id)) {
260                         continue;
261                     }
262                     channelsFound.add(id);
263 
264                     SearchResult.Builder result = SearchResult.builder();
265                     result.setChannelId(id);
266                     result.setChannelNumber(c.getString(1));
267                     result.setTitle(c.getString(2));
268                     result.setDescription(c.getString(3));
269                     result.setImageUri(TvContract.buildChannelLogoUri(id).toString());
270                     result.setIntentAction(Intent.ACTION_VIEW);
271                     result.setIntentData(buildIntentData(id));
272                     result.setContentType(Programs.CONTENT_ITEM_TYPE);
273                     result.setIsLive(true);
274                     result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE);
275 
276                     searchResults.add(result.build());
277 
278                     if (limit != NO_LIMIT && ++count >= limit) {
279                         break;
280                     }
281                 }
282             }
283         }
284         return searchResults;
285     }
286 
287     /**
288      * Replaces the channel information - title, description, channel logo - with the current
289      * program information of the channel if the current program information exists and it is not
290      * blocked.
291      */
292     @WorkerThread
fillProgramInfo(SearchResult result)293     private SearchResult fillProgramInfo(SearchResult result) {
294         long now = System.currentTimeMillis();
295         Uri uri = TvContract.buildProgramsUriForChannel(result.getChannelId(), now, now);
296         String[] projection =
297                 new String[] {
298                     Programs.COLUMN_TITLE,
299                     Programs.COLUMN_POSTER_ART_URI,
300                     Programs.COLUMN_CONTENT_RATING,
301                     Programs.COLUMN_VIDEO_WIDTH,
302                     Programs.COLUMN_VIDEO_HEIGHT,
303                     Programs.COLUMN_START_TIME_UTC_MILLIS,
304                     Programs.COLUMN_END_TIME_UTC_MILLIS
305                 };
306 
307         try (Cursor c = mContentResolver.query(uri, projection, null, null, null)) {
308             if (c != null && c.moveToNext() && !isRatingBlocked(c.getString(2))) {
309                 String channelName = result.getTitle();
310                 String channelNumber = result.getChannelNumber();
311                 SearchResult.Builder builder = result.toBuilder();
312                 long startUtcMillis = c.getLong(5);
313                 long endUtcMillis = c.getLong(6);
314                 builder.setTitle(c.getString(0));
315                 builder.setDescription(
316                         buildProgramDescription(
317                                 channelNumber, channelName, startUtcMillis, endUtcMillis));
318                 String imageUri = c.getString(1);
319                 if (imageUri != null) {
320                     builder.setImageUri(imageUri);
321                 }
322                 builder.setVideoWidth(c.getInt(3));
323                 builder.setVideoHeight(c.getInt(4));
324                 builder.setDuration(endUtcMillis - startUtcMillis);
325                 builder.setProgressPercentage(getProgressPercentage(startUtcMillis, endUtcMillis));
326                 return builder.build();
327             }
328         }
329         return result;
330     }
331 
buildProgramDescription( String channelNumber, String channelName, long programStartUtcMillis, long programEndUtcMillis)332     private String buildProgramDescription(
333             String channelNumber,
334             String channelName,
335             long programStartUtcMillis,
336             long programEndUtcMillis) {
337         return Utils.getDurationString(mContext, programStartUtcMillis, programEndUtcMillis, false)
338                 + System.lineSeparator()
339                 + channelNumber
340                 + " "
341                 + channelName;
342     }
343 
getProgressPercentage(long startUtcMillis, long endUtcMillis)344     private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
345         long current = System.currentTimeMillis();
346         if (startUtcMillis > current || endUtcMillis <= current) {
347             return SearchInterface.PROGRESS_PERCENTAGE_HIDE;
348         }
349         return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
350     }
351 
352     @WorkerThread
searchPrograms( String query, String[] columnForExactMatching, String[] columnForPartialMatching, Set<Long> channelsFound, int limit)353     private List<SearchResult> searchPrograms(
354             String query,
355             String[] columnForExactMatching,
356             String[] columnForPartialMatching,
357             Set<Long> channelsFound,
358             int limit) {
359         if (DEBUG) Log.d(TAG, "Searching programs: '" + query + "'");
360         long time = SystemClock.elapsedRealtime();
361         String[] projection = {
362             Programs.COLUMN_CHANNEL_ID,
363             Programs.COLUMN_TITLE,
364             Programs.COLUMN_POSTER_ART_URI,
365             Programs.COLUMN_CONTENT_RATING,
366             Programs.COLUMN_VIDEO_WIDTH,
367             Programs.COLUMN_VIDEO_HEIGHT,
368             Programs.COLUMN_START_TIME_UTC_MILLIS,
369             Programs.COLUMN_END_TIME_UTC_MILLIS,
370             Programs._ID
371         };
372 
373         StringBuilder sb = new StringBuilder();
374         // Search among the programs which are now being on the air.
375         sb.append(Programs.COLUMN_START_TIME_UTC_MILLIS).append("<=? AND ");
376         sb.append(Programs.COLUMN_END_TIME_UTC_MILLIS).append(">=? AND (");
377         appendSelectionString(sb, columnForExactMatching, columnForPartialMatching);
378         sb.append(")");
379         String selection = sb.toString();
380 
381         int len =
382                 (columnForExactMatching == null ? 0 : columnForExactMatching.length)
383                         + (columnForPartialMatching == null ? 0 : columnForPartialMatching.length);
384         String[] selectionArgs = new String[len + 2];
385         long now = System.currentTimeMillis();
386         selectionArgs[0] = String.valueOf(now + SEARCH_TIME_FRAME_MS);
387         selectionArgs[1] = String.valueOf(now);
388         insertSelectionArgumentStrings(
389                 selectionArgs, 2, query, columnForExactMatching, columnForPartialMatching);
390 
391         List<SearchResult> searchResults = new ArrayList<>();
392 
393         try (Cursor c =
394                 mContentResolver.query(
395                         Programs.CONTENT_URI, projection, selection, selectionArgs, null)) {
396             if (c != null) {
397                 int count = 0;
398                 while (c.moveToNext()) {
399                     long id = c.getLong(0);
400                     // Filter out the program whose channel is already searched.
401                     if (channelsFound.contains(id)) {
402                         continue;
403                     }
404                     channelsFound.add(id);
405 
406                     // Don't know whether the channel is searchable or not.
407                     String[] channelProjection = {
408                         Channels._ID, Channels.COLUMN_DISPLAY_NUMBER, Channels.COLUMN_DISPLAY_NAME
409                     };
410                     sb = new StringBuilder();
411                     sb.append(Channels._ID)
412                             .append("=? AND ")
413                             .append(Channels.COLUMN_BROWSABLE)
414                             .append("=1 AND ")
415                             .append(Channels.COLUMN_SEARCHABLE)
416                             .append("=1");
417                     if (mTvInputManager.isParentalControlsEnabled()) {
418                         sb.append(" AND ").append(Channels.COLUMN_LOCKED).append("=0");
419                     }
420                     String selectionChannel = sb.toString();
421                     try (Cursor cChannel =
422                             mContentResolver.query(
423                                     Channels.CONTENT_URI,
424                                     channelProjection,
425                                     selectionChannel,
426                                     new String[] {String.valueOf(id)},
427                                     null)) {
428                         if (cChannel != null
429                                 && cChannel.moveToNext()
430                                 && !isRatingBlocked(c.getString(3))) {
431                             long startUtcMillis = c.getLong(6);
432                             long endUtcMillis = c.getLong(7);
433                             SearchResult.Builder result = SearchResult.builder();
434                             result.setChannelId(c.getLong(0));
435                             result.setTitle(c.getString(1));
436                             result.setDescription(
437                                     buildProgramDescription(
438                                             cChannel.getString(1),
439                                             cChannel.getString(2),
440                                             startUtcMillis,
441                                             endUtcMillis));
442                             result.setImageUri(c.getString(2));
443                             result.setIntentAction(Intent.ACTION_VIEW);
444                             result.setIntentData(buildIntentData(id));
445                             result.setIntentExtraData(
446                                     TvContract.buildProgramUri(c.getLong(8)).toString());
447                             result.setContentType(Programs.CONTENT_ITEM_TYPE);
448                             result.setIsLive(true);
449                             result.setVideoWidth(c.getInt(4));
450                             result.setVideoHeight(c.getInt(5));
451                             result.setDuration(endUtcMillis - startUtcMillis);
452                             result.setProgressPercentage(
453                                     getProgressPercentage(startUtcMillis, endUtcMillis));
454                             searchResults.add(result.build());
455 
456                             if (limit != NO_LIMIT && ++count >= limit) {
457                                 break;
458                             }
459                         }
460                     }
461                 }
462             }
463         }
464         if (DEBUG) {
465             Log.d(
466                     TAG,
467                     "Found "
468                             + searchResults.size()
469                             + " programs. Elapsed time for searching"
470                             + " programs: "
471                             + (SystemClock.elapsedRealtime() - time)
472                             + "(msec)");
473         }
474         return searchResults;
475     }
476 
buildIntentData(long channelId)477     private String buildIntentData(long channelId) {
478         return TvContract.buildChannelUri(channelId).toString();
479     }
480 
isRatingBlocked(String ratings)481     private boolean isRatingBlocked(String ratings) {
482         if (TextUtils.isEmpty(ratings) || !mTvInputManager.isParentalControlsEnabled()) {
483             return false;
484         }
485         ImmutableList<TvContentRating> ratingArray = mTvContentRatingCache.getRatings(ratings);
486         if (ratingArray != null) {
487             for (TvContentRating r : ratingArray) {
488                 if (mTvInputManager.isRatingBlocked(r)) {
489                     return true;
490                 }
491             }
492         }
493         return false;
494     }
495 
searchInputs(String query, int limit)496     private List<SearchResult> searchInputs(String query, int limit) {
497         if (DEBUG) Log.d(TAG, "Searching inputs: '" + query + "'");
498         long time = SystemClock.elapsedRealtime();
499 
500         query = canonicalizeLabel(query);
501         List<TvInputInfo> inputList = mTvInputManager.getTvInputList();
502         List<SearchResult> results = new ArrayList<>();
503 
504         // Find exact matches first.
505         for (TvInputInfo input : inputList) {
506             if (input.getType() == TvInputInfo.TYPE_TUNER) {
507                 continue;
508             }
509             String label = canonicalizeLabel(input.loadLabel(mContext));
510             String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));
511             if (TextUtils.equals(query, label) || TextUtils.equals(query, customLabel)) {
512                 results.add(buildSearchResultForInput(input.getId()));
513                 if (results.size() >= limit) {
514                     if (DEBUG) {
515                         Log.d(
516                                 TAG,
517                                 "Found "
518                                         + results.size()
519                                         + " inputs. Elapsed time for"
520                                         + " searching inputs: "
521                                         + (SystemClock.elapsedRealtime() - time)
522                                         + "(msec)");
523                     }
524                     return results;
525                 }
526             }
527         }
528 
529         // Then look for partial matches.
530         for (TvInputInfo input : inputList) {
531             if (input.getType() == TvInputInfo.TYPE_TUNER) {
532                 continue;
533             }
534             String label = canonicalizeLabel(input.loadLabel(mContext));
535             String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));
536             if ((label != null && label.contains(query))
537                     || (customLabel != null && customLabel.contains(query))) {
538                 results.add(buildSearchResultForInput(input.getId()));
539                 if (results.size() >= limit) {
540                     if (DEBUG) {
541                         Log.d(
542                                 TAG,
543                                 "Found "
544                                         + results.size()
545                                         + " inputs. Elapsed time for"
546                                         + " searching inputs: "
547                                         + (SystemClock.elapsedRealtime() - time)
548                                         + "(msec)");
549                     }
550                     return results;
551                 }
552             }
553         }
554         if (DEBUG) {
555             Log.d(
556                     TAG,
557                     "Found "
558                             + results.size()
559                             + " inputs. Elapsed time for searching"
560                             + " inputs: "
561                             + (SystemClock.elapsedRealtime() - time)
562                             + "(msec)");
563         }
564         return results;
565     }
566 
canonicalizeLabel(CharSequence cs)567     private String canonicalizeLabel(CharSequence cs) {
568         Locale locale = mContext.getResources().getConfiguration().locale;
569         return cs != null ? cs.toString().replaceAll("[ -]", "").toLowerCase(locale) : null;
570     }
571 
buildSearchResultForInput(String inputId)572     private SearchResult buildSearchResultForInput(String inputId) {
573         SearchResult.Builder result = SearchResult.builder();
574         result.setIntentAction(Intent.ACTION_VIEW);
575         result.setIntentData(TvContract.buildChannelUriForPassthroughInput(inputId).toString());
576         return result.build();
577     }
578 
579     @WorkerThread
580     private class ChannelComparatorWithSameDisplayNumber implements Comparator<SearchResult> {
581         private final Map<Long, Long> mMaxWatchStartTimeMap = new HashMap<>();
582 
583         @Override
compare(SearchResult lhs, SearchResult rhs)584         public int compare(SearchResult lhs, SearchResult rhs) {
585             // Show recently watched channel first
586             Long lhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(lhs.getChannelId());
587             if (lhsMaxWatchStartTime == null) {
588                 lhsMaxWatchStartTime = getMaxWatchStartTime(lhs.getChannelId());
589                 mMaxWatchStartTimeMap.put(lhs.getChannelId(), lhsMaxWatchStartTime);
590             }
591             Long rhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(rhs.getChannelId());
592             if (rhsMaxWatchStartTime == null) {
593                 rhsMaxWatchStartTime = getMaxWatchStartTime(rhs.getChannelId());
594                 mMaxWatchStartTimeMap.put(rhs.getChannelId(), rhsMaxWatchStartTime);
595             }
596             if (!Objects.equals(lhsMaxWatchStartTime, rhsMaxWatchStartTime)) {
597                 return Long.compare(rhsMaxWatchStartTime, lhsMaxWatchStartTime);
598             }
599             // Show recently added channel first if there's no watch history.
600             return Long.compare(rhs.getChannelId(), lhs.getChannelId());
601         }
602 
getMaxWatchStartTime(long channelId)603         private long getMaxWatchStartTime(long channelId) {
604             Uri uri = WatchedPrograms.CONTENT_URI;
605             String[] projections =
606                     new String[] {
607                         "MAX("
608                                 + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS
609                                 + ") AS max_watch_start_time"
610                     };
611             String selection = WatchedPrograms.COLUMN_CHANNEL_ID + "=?";
612             String[] selectionArgs = new String[] {Long.toString(channelId)};
613             try (Cursor c =
614                     mContentResolver.query(uri, projections, selection, selectionArgs, null)) {
615                 if (c != null && c.moveToNext()) {
616                     return c.getLong(0);
617                 }
618             }
619             return -1;
620         }
621     }
622 }
623