aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/search/TvProviderSearch.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/search/TvProviderSearch.java')
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java535
1 files changed, 415 insertions, 120 deletions
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index 4d6f53f6..2548d34a 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,192 +16,487 @@
package com.android.tv.search;
-import android.content.ContentUris;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvContract.Programs;
+import android.media.tv.TvContract.WatchedPrograms;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
-import com.android.internal.util.Preconditions;
+import com.android.tv.search.LocalSearchProvider.SearchResult;
+import com.android.tv.util.Utils;
+
+import junit.framework.Assert;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
public class TvProviderSearch {
- public static List<SearchResult> search(Context context, String query, int limit) {
- List<SearchResult> results = new ArrayList<SearchResult>();
- results.addAll(searchChannels(context, query, new String[] {
- Channels.COLUMN_DISPLAY_NAME,
- Channels.COLUMN_DESCRIPTION
- }, limit));
- if (results.size() >= limit) {
- return results;
- }
+ private static final boolean DEBUG = false;
+ private static final String TAG = "TvProviderSearch";
- Set<Long> previousResults = getChannelIdSet(results);
- limit -= results.size();
- results.addAll(searchPrograms(context, query, new String[] {
- Programs.COLUMN_TITLE,
- Programs.COLUMN_SHORT_DESCRIPTION
- }, previousResults, limit));
+ private static final int NO_LIMIT = 0;
+
+ static final int ACTION_TYPE_AMBIGUOUS = 1;
+ static final int ACTION_TYPE_SWITCH_CHANNEL = 2;
+ static final int ACTION_TYPE_SWITCH_INPUT = 3;
+
+ private static final String SOURCE_TV_SEARCH = "TvSearch";
+
+ private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private final TvInputManager mTvInputManager;
+
+ TvProviderSearch(Context context) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ }
+
+ /**
+ * Search channels, inputs, or programs from TvProvider.
+ * This assumes that parental control settings will not be change while searching.
+ *
+ * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT},
+ * or {@link #ACTION_TYPE_AMBIGUOUS},
+ */
+ public List<SearchResult> search(String query, int limit, int action) {
+ List<SearchResult> results = new ArrayList<>();
+ Set<Long> channelsFound = new HashSet<>();
+ if (action == ACTION_TYPE_SWITCH_CHANNEL) {
+ results.addAll(searchChannels(query, channelsFound, limit));
+ } else if (action == ACTION_TYPE_SWITCH_INPUT) {
+ results.addAll(searchInputs(query, limit));
+ } else {
+ // Search channels first.
+ results.addAll(searchChannels(query, channelsFound, limit));
+ if (results.size() >= limit) {
+ return results;
+ }
+
+ // In case the user wanted to perform the action "switch to XXX", which is indicated by
+ // setting the limit to 1, search inputs.
+ if (limit == 1) {
+ results.addAll(searchInputs(query, limit));
+ if (!results.isEmpty()) {
+ return results;
+ }
+ }
+
+ // Lastly, search programs.
+ limit -= results.size();
+ results.addAll(searchPrograms(query, null, new String[] {
+ Programs.COLUMN_TITLE, Programs.COLUMN_SHORT_DESCRIPTION },
+ channelsFound, limit));
+ }
return results;
}
- private static Set<Long> getChannelIdSet(List<SearchResult> results) {
- Set<Long> channelIdSet = new HashSet<Long>();
- for (SearchResult sr : results) {
- channelIdSet.add(sr.getChannelId());
+ private StringBuilder appendSelectionString(StringBuilder sb,
+ String[] columnForExactMatching, String[] columnForPartialMatching) {
+ boolean firstColumn = true;
+ if (columnForExactMatching != null) {
+ for (String column : columnForExactMatching) {
+ if (!firstColumn) {
+ sb.append(" OR ");
+ } else {
+ firstColumn = false;
+ }
+ sb.append(column).append("=?");
+ }
+ }
+ if (columnForPartialMatching != null) {
+ for (String column : columnForPartialMatching) {
+ if (!firstColumn) {
+ sb.append(" OR ");
+ } else {
+ firstColumn = false;
+ }
+ sb.append(column).append(" LIKE ?");
+ }
+ }
+ return sb;
+ }
+
+ private void insertSelectionArgumentStrings(String[] selectionArgs, int pos,
+ String query, String[] columnForExactMatching, String[] columnForPartialMatching) {
+ if (columnForExactMatching != null) {
+ int until = pos + columnForExactMatching.length;
+ for (; pos < until; ++pos) {
+ selectionArgs[pos] = query;
+ }
+ }
+ String selectionArg = "%" + query + "%";
+ if (columnForPartialMatching != null) {
+ int until = pos + columnForPartialMatching.length;
+ for (; pos < until; ++pos) {
+ selectionArgs[pos] = selectionArg;
+ }
+ }
+ }
+
+ private List<SearchResult> searchChannels(String query, Set<Long> channels, int limit) {
+ List<SearchResult> results = new ArrayList<>();
+ if (TextUtils.isDigitsOnly(query)) {
+ results.addAll(searchChannels(query, new String[] { Channels.COLUMN_DISPLAY_NUMBER },
+ null, channels, NO_LIMIT));
+ if (results.size() > 1) {
+ Collections.sort(results, new ChannelComparatorWithSameDisplayNumber());
+ }
+ }
+ if (results.size() < limit) {
+ results.addAll(searchChannels(query, null,
+ new String[] { Channels.COLUMN_DISPLAY_NAME, Channels.COLUMN_DESCRIPTION },
+ channels, limit - results.size()));
}
- return channelIdSet;
+ if (results.size() > limit) {
+ results = results.subList(0, limit);
+ }
+ for (SearchResult result : results) {
+ fillProgramInfo(result);
+ }
+ return results;
}
- private static List<SearchResult> searchChannels(Context context, String query,
- String[] columnNames, int limit) {
- Preconditions.checkState(columnNames != null && columnNames.length > 0);
+ private List<SearchResult> searchChannels(String query, String[] columnForExactMatching,
+ String[] columnForPartialMatching, Set<Long> channelsFound, int limit) {
+ Assert.assertTrue(
+ (columnForExactMatching != null && columnForExactMatching.length > 0) ||
+ (columnForPartialMatching != null && columnForPartialMatching.length > 0));
String[] projection = {
Channels._ID,
+ Channels.COLUMN_DISPLAY_NUMBER,
Channels.COLUMN_DISPLAY_NAME,
- Channels.COLUMN_DESCRIPTION,
+ Channels.COLUMN_DESCRIPTION
};
StringBuilder sb = new StringBuilder();
- sb.append(Channels.COLUMN_BROWSABLE).append("=1 AND ");
- sb.append(Channels.COLUMN_SEARCHABLE).append("=1 AND (");
- sb.append(columnNames[0]).append(" like ?");
- for (int i = 1; i < columnNames.length; ++i) {
- sb.append(" OR ").append(columnNames[i]).append(" like ?");
+ sb.append(Channels.COLUMN_BROWSABLE).append("=1 AND ")
+ .append(Channels.COLUMN_SEARCHABLE).append("=1");
+ if (mTvInputManager.isParentalControlsEnabled()) {
+ sb.append(" AND ").append(Channels.COLUMN_LOCKED).append("=0");
}
+ sb.append(" AND (");
+ appendSelectionString(sb, columnForExactMatching, columnForPartialMatching);
sb.append(")");
String selection = sb.toString();
- String selectionArg = "%" + query + "%";
- String[] selectionArgs = new String[columnNames.length];
- for (int i = 0; i < selectionArgs.length; ++i) {
- selectionArgs[i] = selectionArg;
+ int len = (columnForExactMatching == null ? 0 : columnForExactMatching.length) +
+ (columnForPartialMatching == null ? 0 : columnForPartialMatching.length);
+ String[] selectionArgs = new String[len];
+ insertSelectionArgumentStrings(selectionArgs, 0, query, columnForExactMatching,
+ columnForPartialMatching);
+
+ List<SearchResult> searchResults = new ArrayList<>();
+
+ try (Cursor c = mContentResolver.query(Channels.CONTENT_URI, projection, selection,
+ selectionArgs, null)) {
+ if (c != null) {
+ int count = 0;
+ while (c.moveToNext()) {
+ long id = c.getLong(0);
+ // Filter out the channel which has been already searched.
+ if (channelsFound.contains(id)) {
+ continue;
+ }
+ channelsFound.add(id);
+
+ SearchResult result = new SearchResult();
+ result.channelId = id;
+ result.channelNumber = c.getString(1);
+ result.title = c.getString(2);
+ result.description = c.getString(3);
+ result.imageUri = TvContract.buildChannelLogoUri(result.channelId).toString();
+ result.intentAction = Intent.ACTION_VIEW;
+ result.intentData = buildIntentData(result.channelId);
+ result.contentType = Programs.CONTENT_ITEM_TYPE;
+ result.isLive = true;
+ result.progressPercentage = LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+
+ searchResults.add(result);
+
+ if (limit != NO_LIMIT && ++count >= limit) {
+ break;
+ }
+ }
+ }
+ }
+ return searchResults;
+ }
+
+ /**
+ * Replaces the channel information - title, description, channel logo - with the current
+ * program information of the channel if the current program information exists and it is not
+ * blocked.
+ */
+ private void fillProgramInfo(SearchResult result) {
+ long now = System.currentTimeMillis();
+ Uri uri = TvContract.buildProgramsUriForChannel(result.channelId, now, now);
+ String[] projection = new String[] {
+ Programs.COLUMN_TITLE,
+ Programs.COLUMN_POSTER_ART_URI,
+ Programs.COLUMN_CONTENT_RATING,
+ Programs.COLUMN_VIDEO_WIDTH,
+ Programs.COLUMN_VIDEO_HEIGHT,
+ Programs.COLUMN_START_TIME_UTC_MILLIS,
+ Programs.COLUMN_END_TIME_UTC_MILLIS
+ };
+
+ try (Cursor c = mContentResolver.query(uri, projection, null, null, null)) {
+ if (c != null && c.moveToNext() && !isRatingBlocked(c.getString(2))) {
+ String channelName = result.title;
+ long startUtcMillis = c.getLong(5);
+ long endUtcMillis = c.getLong(6);
+ result.title = c.getString(0);
+ result.description = buildProgramDescription(result.channelNumber, channelName,
+ startUtcMillis, endUtcMillis);
+ String imageUri = c.getString(1);
+ if (imageUri != null) {
+ result.imageUri = imageUri;
+ }
+ result.videoWidth = c.getInt(3);
+ result.videoHeight = c.getInt(4);
+ result.duration = endUtcMillis - startUtcMillis;
+ result.progressPercentage = getProgressPercentage(startUtcMillis, endUtcMillis);
+ }
}
+ }
+
+ private String buildProgramDescription(String channelNumber, String channelName,
+ long programStartUtcMillis, long programEndUtcMillis) {
+ return Utils.getDurationString(mContext, programStartUtcMillis, programEndUtcMillis, false)
+ + System.lineSeparator() + channelNumber + " " + channelName;
+ }
- return search(context, Channels.CONTENT_URI, projection, selection, selectionArgs, limit,
- null);
+ private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
+ long current = System.currentTimeMillis();
+ if (startUtcMillis > current || endUtcMillis <= current) {
+ return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+ }
+ return (int)(100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
}
- private static List<SearchResult> searchPrograms(final Context context, String query,
- String[] columnNames, final Set<Long> previousResults, int limit) {
- Preconditions.checkState(columnNames != null && columnNames.length > 0);
+ private List<SearchResult> searchPrograms(String query, String[] columnForExactMatching,
+ String[] columnForPartialMatching, Set<Long> channelsFound, int limit) {
+ Assert.assertTrue(
+ (columnForExactMatching != null && columnForExactMatching.length > 0) ||
+ (columnForPartialMatching != null && columnForPartialMatching.length > 0));
String[] projection = {
Programs.COLUMN_CHANNEL_ID,
Programs.COLUMN_TITLE,
- Programs.COLUMN_SHORT_DESCRIPTION,
+ Programs.COLUMN_POSTER_ART_URI,
+ Programs.COLUMN_CONTENT_RATING,
+ Programs.COLUMN_VIDEO_WIDTH,
+ Programs.COLUMN_VIDEO_HEIGHT,
+ Programs.COLUMN_START_TIME_UTC_MILLIS,
+ Programs.COLUMN_END_TIME_UTC_MILLIS
};
StringBuilder sb = new StringBuilder();
// Search among the programs which are now being on the air.
sb.append(Programs.COLUMN_START_TIME_UTC_MILLIS).append("<=? AND ");
sb.append(Programs.COLUMN_END_TIME_UTC_MILLIS).append(">=? AND (");
- sb.append(columnNames[0]).append(" like ?");
- for (int i = 1; i < columnNames.length; ++i) {
- sb.append(" OR ").append(columnNames[0]).append(" like ?");
- }
+ appendSelectionString(sb, columnForExactMatching, columnForPartialMatching);
sb.append(")");
String selection = sb.toString();
- String selectionArg = "%" + query + "%";
- String[] selectionArgs = new String[columnNames.length+2];
+
+ int len = (columnForExactMatching == null ? 0 : columnForExactMatching.length) +
+ (columnForPartialMatching == null ? 0 : columnForPartialMatching.length);
+ String[] selectionArgs = new String[len + 2];
selectionArgs[0] = selectionArgs[1] = String.valueOf(System.currentTimeMillis());
- for (int i = 2; i < selectionArgs.length; ++i) {
- selectionArgs[i] = selectionArg;
- }
+ insertSelectionArgumentStrings(selectionArgs, 2, query, columnForExactMatching,
+ columnForPartialMatching);
- return search(context, Programs.CONTENT_URI, projection, selection, selectionArgs, limit,
- new ResultFilter() {
- private Map<Long, Boolean> searchableMap = new HashMap<Long, Boolean>();
+ List<SearchResult> searchResults = new ArrayList<>();
- @Override
- public boolean filter(Cursor c) {
- long id = c.getLong(0);
- // Filter out the program whose channel is already searched.
- if (previousResults.contains(id)) {
- return false;
- }
- // The channel is cached.
- Boolean isSearchable = searchableMap.get(id);
- if (isSearchable != null) {
- return isSearchable;
- }
+ try (Cursor c = mContentResolver.query(Programs.CONTENT_URI, projection, selection,
+ selectionArgs, null)) {
+ if (c != null) {
+ int count = 0;
+ while (c.moveToNext()) {
+ long id = c.getLong(0);
+ // Filter out the program whose channel is already searched.
+ if (channelsFound.contains(id)) {
+ continue;
+ }
+ channelsFound.add(id);
- // Don't know whether the channel is searchable or not.
- String selection = Channels._ID + "=? AND "
- + Channels.COLUMN_BROWSABLE + "=1 AND "
- + Channels.COLUMN_SEARCHABLE + "=1";
- Cursor cursor = null;
- try {
- // Don't need to fetch all the columns.
- cursor = context.getContentResolver().query(Channels.CONTENT_URI,
- new String[] { Channels._ID }, selection,
- new String[] { String.valueOf(id) }, null);
- boolean isSearchableChannel = cursor != null && cursor.getCount() > 0;
- searchableMap.put(id, isSearchableChannel);
- return isSearchableChannel;
- } finally {
- if (cursor != null) {
- cursor.close();
+ // Don't know whether the channel is searchable or not.
+ String[] channelProjection = {
+ Channels._ID,
+ Channels.COLUMN_DISPLAY_NUMBER,
+ Channels.COLUMN_DISPLAY_NAME
+ };
+ sb = new StringBuilder();
+ sb.append(Channels._ID).append("=? AND ")
+ .append(Channels.COLUMN_BROWSABLE).append("=1 AND ")
+ .append(Channels.COLUMN_SEARCHABLE).append("=1");
+ if (mTvInputManager.isParentalControlsEnabled()) {
+ sb.append(" AND ").append(Channels.COLUMN_LOCKED).append("=0");
+ }
+ String selectionChannel = sb.toString();
+ try (Cursor cChannel = mContentResolver.query(Channels.CONTENT_URI,
+ channelProjection, selectionChannel,
+ new String[] { String.valueOf(id) }, null)) {
+ if (cChannel != null && cChannel.moveToNext()
+ && !isRatingBlocked(c.getString(3))) {
+ long startUtcMillis = c.getLong(6);
+ long endUtcMillis = c.getLong(7);
+ SearchResult result = new SearchResult();
+ result.channelId = c.getLong(0);
+ result.title = c.getString(1);
+ result.description = buildProgramDescription(cChannel.getString(1),
+ cChannel.getString(2), startUtcMillis, endUtcMillis);
+ result.imageUri = c.getString(2);
+ result.intentAction = Intent.ACTION_VIEW;
+ result.intentData = buildIntentData(id);
+ result.contentType = Programs.CONTENT_ITEM_TYPE;
+ result.isLive = true;
+ result.videoWidth = c.getInt(4);
+ result.videoHeight = c.getInt(5);
+ result.duration = endUtcMillis - startUtcMillis;
+ result.progressPercentage = getProgressPercentage(startUtcMillis,
+ endUtcMillis);
+ searchResults.add(result);
+
+ if (limit != NO_LIMIT && ++count >= limit) {
+ break;
}
}
}
- });
+ }
+ }
+ }
+ return searchResults;
}
- private static List<SearchResult> search(Context context, Uri uri, String[] projection,
- String selection, String[] selectionArgs, int limit, ResultFilter resultFilter) {
- List<SearchResult> results = new ArrayList<SearchResult>();
-
- Cursor cursor = null;
- try {
- cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
- null);
- if (cursor != null) {
- // TODO: Need to add image when available.
- int count = 0;
- while (cursor.moveToNext()) {
- if (resultFilter != null && !resultFilter.filter(cursor)) {
- continue;
- }
+ private String buildIntentData(long channelId) {
+ return TvContract.buildChannelUri(channelId).buildUpon()
+ .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH)
+ .build().toString();
+ }
- long id = cursor.getLong(0);
- String title = cursor.getString(1);
- String description = cursor.getString(2);
-
- SearchResult result = SearchResult.builder()
- .setChannelId(id)
- .setTitle(title)
- .setDescription(description)
- .setIntentAction(Intent.ACTION_VIEW)
- .setIntentData(ContentUris.withAppendedId(Channels.CONTENT_URI, id)
- .toString())
- .build();
- results.add(result);
-
- if (++count >= limit) {
- break;
- }
+ private boolean isRatingBlocked(String ratings) {
+ if (ratings == null) {
+ return false;
+ }
+ for (String rating : ratings.split("\\s*,\\s*")) {
+ try {
+ if (mTvInputManager.isParentalControlsEnabled() && mTvInputManager.isRatingBlocked(
+ TvContentRating.unflattenFromString(rating))) {
+ return true;
}
+ } catch (IllegalArgumentException e) {
+ // Do nothing.
}
- } finally {
- if (cursor != null) {
- cursor.close();
+ }
+ return false;
+ }
+
+ private List<SearchResult> searchInputs(String query, int limit) {
+ if (DEBUG) {
+ Log.d(TAG, "searchInputs(" + query + ", limit=" + limit + ")");
+ }
+
+ query = canonicalizeLabel(query);
+ List<TvInputInfo> inputList = mTvInputManager.getTvInputList();
+ List<SearchResult> results = new ArrayList<>();
+
+ // Find exact matches first.
+ for (TvInputInfo input : inputList) {
+ String label = canonicalizeLabel(input.loadLabel(mContext));
+ String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));
+ if (TextUtils.equals(query, label) || TextUtils.equals(query, customLabel)) {
+ results.add(buildSearchResultForInput(input.getId()));
+ if (results.size() >= limit) {
+ return results;
+ }
}
}
+ // Then look for partial matches.
+ for (TvInputInfo input : inputList) {
+ String label = canonicalizeLabel(input.loadLabel(mContext));
+ String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));
+ if ((label != null && label.contains(query)) ||
+ (customLabel != null && customLabel.contains(query))) {
+ results.add(buildSearchResultForInput(input.getId()));
+ if (results.size() >= limit) {
+ return results;
+ }
+ }
+ }
return results;
}
- private interface ResultFilter {
- boolean filter(Cursor c);
+ private String canonicalizeLabel(CharSequence cs) {
+ Locale locale = mContext.getResources().getConfiguration().locale;
+ return cs != null ? cs.toString().replaceAll("[ -]", "").toLowerCase(locale) : null;
+ }
+
+ private SearchResult buildSearchResultForInput(String inputId) {
+ SearchResult result = new SearchResult();
+ result.intentAction = Intent.ACTION_VIEW;
+ result.intentData = TvContract.buildChannelUriForPassthroughInput(inputId).toString();
+ return result;
+ }
+
+ private class ChannelComparatorWithSameDisplayNumber implements Comparator<SearchResult> {
+ private final Map<Long, Long> mMaxWatchStartTimeMap = new HashMap<>();
+
+ @Override
+ public int compare(SearchResult lhs, SearchResult rhs) {
+ // Show recently watched channel first
+ Long lhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(lhs.channelId);
+ if (lhsMaxWatchStartTime == null) {
+ lhsMaxWatchStartTime = getMaxWatchStartTime(lhs.channelId);
+ mMaxWatchStartTimeMap.put(lhs.channelId, lhsMaxWatchStartTime);
+ }
+ Long rhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(rhs.channelId);
+ if (rhsMaxWatchStartTime == null) {
+ rhsMaxWatchStartTime = getMaxWatchStartTime(rhs.channelId);
+ mMaxWatchStartTimeMap.put(rhs.channelId, rhsMaxWatchStartTime);
+ }
+ if (!Objects.equals(lhsMaxWatchStartTime, rhsMaxWatchStartTime)) {
+ return Long.compare(rhsMaxWatchStartTime, lhsMaxWatchStartTime);
+ }
+ // Show recently added channel first if there's no watch history.
+ return Long.compare(rhs.channelId, lhs.channelId);
+ }
+
+ private long getMaxWatchStartTime(long channelId) {
+ Uri uri = WatchedPrograms.CONTENT_URI;
+ String[] projections = new String[] {
+ "MAX(" + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS
+ + ") AS max_watch_start_time"
+ };
+ String selection = WatchedPrograms.COLUMN_CHANNEL_ID + "=?";
+ String[] selectionArgs = new String[] { Long.toString(channelId) };
+ try (Cursor c = mContentResolver.query(uri, projections, selection, selectionArgs,
+ null)) {
+ if (c != null && c.moveToNext()) {
+ return c.getLong(0);
+ }
+ }
+ return -1;
+ }
}
}