diff options
Diffstat (limited to 'src/com/android/tv/data')
-rw-r--r-- | src/com/android/tv/data/Channel.java | 97 | ||||
-rw-r--r-- | src/com/android/tv/data/ChannelDataManager.java | 42 | ||||
-rw-r--r-- | src/com/android/tv/data/ChannelLogoFetcher.java | 307 | ||||
-rw-r--r-- | src/com/android/tv/data/ChannelNumber.java | 71 | ||||
-rw-r--r-- | src/com/android/tv/data/InternalDataUtils.java | 2 | ||||
-rw-r--r-- | src/com/android/tv/data/StreamInfo.java | 4 | ||||
-rw-r--r-- | src/com/android/tv/data/epg/EpgFetcher.java | 327 | ||||
-rw-r--r-- | src/com/android/tv/data/epg/EpgReader.java | 29 | ||||
-rw-r--r-- | src/com/android/tv/data/epg/StubEpgReader.java | 17 |
9 files changed, 513 insertions, 383 deletions
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 30f84236..4da56311 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -52,6 +52,16 @@ public final class Channel { public static final int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3; /** + * Compares the channel numbers of channels which belong to the same input. + */ + public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = new Comparator<Channel>() { + @Override + public int compare(Channel lhs, Channel rhs) { + return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); + } + }; + + /** * When a TIS doesn't provide any information about app link, and it doesn't have a leanback * launch intent, there will be no app link card for the TIS. */ @@ -87,9 +97,15 @@ public final class Channel { TvContract.Channels.COLUMN_APP_LINK_ICON_URI, TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI, TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, + TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input }; /** + * Channel number delimiter between major and minor parts. + */ + public static final char CHANNEL_NUMBER_DELIMITER = '-'; + + /** * Creates {@code Channel} object from cursor. * * <p>The query that created the cursor MUST use {@link #PROJECTION} @@ -103,7 +119,7 @@ public final class Channel { channel.mPackageName = Utils.intern(cursor.getString(index++)); channel.mInputId = Utils.intern(cursor.getString(index++)); channel.mType = Utils.intern(cursor.getString(index++)); - channel.mDisplayNumber = cursor.getString(index++); + channel.mDisplayNumber = normalizeDisplayNumber(cursor.getString(index++)); channel.mDisplayName = cursor.getString(index++); channel.mDescription = cursor.getString(index++); channel.mVideoFormat = Utils.intern(cursor.getString(index++)); @@ -114,17 +130,29 @@ public final class Channel { channel.mAppLinkIconUri = cursor.getString(index++); channel.mAppLinkPosterArtUri = cursor.getString(index++); channel.mAppLinkIntentUri = cursor.getString(index++); + if (Utils.isBundledInput(channel.mInputId)) { + channel.mRecordingProhibited = cursor.getInt(index++) != 0; + } return channel; } /** - * Creates a {@link Channel} object from the DVR database. + * Replaces the channel number separator with dash('-'). */ - public static Channel fromDvrCursor(Cursor c) { - Channel channel = new Channel(); - int index = -1; - channel.mDvrId = c.getLong(++index); - return channel; + public static String normalizeDisplayNumber(String string) { + if (!TextUtils.isEmpty(string)) { + int length = string.length(); + for (int i = 0; i < length; i++) { + char c = string.charAt(i); + if (c == '.' || Character.isWhitespace(c) + || Character.getType(c) == Character.DASH_PUNCTUATION) { + StringBuilder sb = new StringBuilder(string); + sb.setCharAt(i, CHANNEL_NUMBER_DELIMITER); + return sb.toString(); + } + } + } + return string; } /** ID of this channel. Matches to BaseColumns._ID. */ @@ -147,8 +175,10 @@ public final class Channel { private String mAppLinkIntentUri; private Intent mAppLinkIntent; private int mAppLinkType; + private String mLogoUri; + private boolean mRecordingProhibited; - private long mDvrId; + private boolean mChannelLogoExist; private Channel() { // Do nothing. @@ -230,10 +260,14 @@ public final class Channel { } /** - * Returns an ID in DVR database. + * Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */ - public long getDvrId() { - return mDvrId; + public String getLogoUri() { + return mLogoUri; + } + + public boolean isRecordingProhibited() { + return mRecordingProhibited; } /** @@ -279,6 +313,13 @@ public final class Channel { } /** + * Sets channel logo uri which is got from cloud. + */ + public void setLogoUri(String logoUri) { + mLogoUri = logoUri; + } + + /** * Check whether {@code other} has same read-only channel info as this. But, it cannot check two * channels have same logos. It also excludes browsable and locked, because two fields are * changed by TV app. @@ -298,7 +339,8 @@ public final class Channel { && mAppLinkColor == other.mAppLinkColor && Objects.equals(mAppLinkIconUri, other.mAppLinkIconUri) && Objects.equals(mAppLinkPosterArtUri, other.mAppLinkPosterArtUri) - && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri); + && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri) + && Objects.equals(mRecordingProhibited, other.mRecordingProhibited); } @Override @@ -315,7 +357,8 @@ public final class Channel { + ", isPassthrough=" + mIsPassthrough + ", browsable=" + mBrowsable + ", locked=" + mLocked - + ", appLinkText=" + mAppLinkText + "}"; + + ", appLinkText=" + mAppLinkText + + ", recordingProhibited=" + mRecordingProhibited + "}"; } void copyFrom(Channel other) { @@ -340,6 +383,8 @@ public final class Channel { mAppLinkIntentUri = other.mAppLinkIntentUri; mAppLinkIntent = other.mAppLinkIntent; mAppLinkType = other.mAppLinkType; + mRecordingProhibited = other.mRecordingProhibited; + mChannelLogoExist = other.mChannelLogoExist; } /** @@ -389,8 +434,6 @@ public final class Channel { mChannel.mDisplayName = "name"; mChannel.mDescription = "description"; mChannel.mBrowsable = true; - mChannel.mLocked = false; - mChannel.mIsPassthrough = false; } public Builder(Channel other) { @@ -422,7 +465,7 @@ public final class Channel { @VisibleForTesting public Builder setDisplayNumber(String displayNumber) { - mChannel.mDisplayNumber = displayNumber; + mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber); return this; } @@ -485,6 +528,11 @@ public final class Channel { return this; } + public Builder setRecordingProhibited(boolean recordingProhibited) { + mChannel.mRecordingProhibited = recordingProhibited; + return this; + } + public Channel build() { Channel channel = new Channel(); channel.copyFrom(mChannel); @@ -524,6 +572,21 @@ public final class Channel { } /** + * Sets if the channel logo exists. This method should be only called from + * {@link ChannelDataManager}. + */ + void setChannelLogoExist(boolean exist) { + mChannelLogoExist = exist; + } + + /** + * Returns if channel logo exists. + */ + public boolean channelLogoExists() { + return mChannelLogoExist; + } + + /** * Returns the type of app link for this channel. * It returns {@link #APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and * a valid app link intent, it returns {@link #APP_LINK_TYPE_APP} if the input service which @@ -655,4 +718,4 @@ public final class Channel { return label; } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index 6f9ea6d7..eb3871fc 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -21,10 +21,14 @@ import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; +import android.database.sqlite.SQLiteException; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.media.tv.TvInputManager.TvInputCallback; +import android.net.Uri; +import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -43,6 +47,8 @@ import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -192,7 +198,6 @@ public class ChannelDataManager { mStarted = false; mDbLoadFinished = false; - ChannelLogoFetcher.stopFetchingChannelLogos(); mInputManager.removeCallback(mTvInputCallback); mContentResolver.unregisterContentObserver(mChannelObserver); mHandler.removeCallbacksAndMessages(null); @@ -590,6 +595,36 @@ public class ChannelDataManager { } } + private class checkChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> { + private final Channel mChannel; + + public checkChannelLogoExistTask(Channel channel) { + mChannel = channel; + } + + @Override + protected Boolean doInBackground(Void... params) { + boolean result = false; + try { + AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor( + TvContract.buildChannelLogoUri(mChannel.getId()), "r"); + result = true; + f.close(); + } catch (SQLiteException | IOException | NullPointerException e) { + // File not found or asset file not found. + } + return result; + } + + @Override + protected void onPostExecute(Boolean result) { + ChannelWrapper wrapper = mChannelWrapperMap.get(mChannel.getId()); + if (wrapper != null) { + wrapper.mChannel.setChannelLogoExist(result); + } + } + } + private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask { public QueryAllChannelsTask(ContentResolver contentResolver) { @@ -625,6 +660,8 @@ public class ChannelDataManager { boolean newlyAdded = !removedChannelIds.remove(channelId); ChannelWrapper channelWrapper; if (newlyAdded) { + new checkChannelLogoExistTask(channel) + .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); channelWrapper = new ChannelWrapper(channel); mChannelWrapperMap.put(channel.getId(), channelWrapper); if (!channelWrapper.mInputRemoved) { @@ -640,9 +677,9 @@ public class ChannelDataManager { // {@link #applyUpdatedValuesToDb} is called. Therefore, the value // between DB and ChannelDataManager could be different for a while. // Therefore, we'll keep the values in ChannelDataManager. - channelWrapper.mChannel.copyFrom(channel); channel.setBrowsable(oldChannel.isBrowsable()); channel.setLocked(oldChannel.isLocked()); + channelWrapper.mChannel.copyFrom(channel); if (!channelWrapper.mInputRemoved) { channelUpdated = true; updatedChannelWrappers.add(channelWrapper); @@ -693,7 +730,6 @@ public class ChannelDataManager { r.run(); } mPostRunnablesAfterChannelUpdate.clear(); - ChannelLogoFetcher.startFetchingChannelLogos(mContext); } } diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java index 5a549f83..256ecdb2 100644 --- a/src/com/android/tv/data/ChannelLogoFetcher.java +++ b/src/com/android/tv/data/ChannelLogoFetcher.java @@ -16,155 +16,68 @@ package com.android.tv.data; +import android.content.ContentProviderOperation; import android.content.Context; -import android.database.Cursor; +import android.content.OperationApplicationException; +import android.content.SharedPreferences; import android.graphics.Bitmap.CompressFormat; import android.media.tv.TvContract; -import android.media.tv.TvContract.Channels; import android.net.Uri; import android.os.AsyncTask; -import android.support.annotation.WorkerThread; +import android.os.RemoteException; +import android.support.annotation.AnyThread; import android.text.TextUtils; import android.util.Log; -import com.android.tv.util.AsyncDbTask; +import com.android.tv.common.SharedPreferencesUtils; import com.android.tv.util.BitmapUtils; import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; import com.android.tv.util.PermissionUtils; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStream; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Set; +import java.util.List; /** - * Utility class for TMS data. - * This class is thread safe. + * Fetches channel logos from the cloud into the database. It's for the channels which have no logos + * or need update logos. This class is thread safe. */ public class ChannelLogoFetcher { private static final String TAG = "ChannelLogoFetcher"; private static final boolean DEBUG = false; - /** - * The name of the file which contains the TMS data. - * The file has multiple records and each of them is a string separated by '|' like - * STATION_NAME|SHORT_NAME|CALL_SIGN|LOGO_URI. - */ - private static final String TMS_US_TABLE_FILE = "tms_us.table"; - private static final String TMS_KR_TABLE_FILE = "tms_kr.table"; - private static final String FIELD_SEPARATOR = "\\|"; - private static final String NAME_SEPARATOR_FOR_TMS = "\\(|\\)|\\{|\\}|\\[|\\]"; - private static final String NAME_SEPARATOR_FOR_DB = "\\W"; - private static final int INDEX_NAME = 0; - private static final int INDEX_SHORT_NAME = 1; - private static final int INDEX_CALL_SIGN = 2; - private static final int INDEX_LOGO_URI = 3; - - private static final String COLUMN_CHANNEL_LOGO = "logo"; + private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO = + "is_first_time_fetch_channel_logo"; - private static final Object sLock = new Object(); - private static final Set<Long> sChannelIdBlackListSet = new HashSet<>(); - private static LoadChannelTask sQueryTask; private static FetchLogoTask sFetchTask; /** - * Fetch the channel logos from TMS data and insert them into TvProvider. + * Fetches the channel logos from the cloud data and insert them into TvProvider. * The previous task is canceled and a new task starts. */ - public static void startFetchingChannelLogos(Context context) { + @AnyThread + public static synchronized void startFetchingChannelLogos( + Context context, List<Channel> channels) { if (!PermissionUtils.hasAccessAllEpg(context)) { // TODO: support this feature for non-system LC app. b/23939816 return; } - synchronized (sLock) { - stopFetchingChannelLogos(); - if (DEBUG) Log.d(TAG, "Request to start fetching logos."); - sQueryTask = new LoadChannelTask(context); - sQueryTask.executeOnDbThread(); + if (sFetchTask != null) { + sFetchTask.cancel(true); } - } - - /** - * Stops the current fetching tasks. This can be called when the Activity pauses. - */ - public static void stopFetchingChannelLogos() { - synchronized (sLock) { - if (DEBUG) Log.d(TAG, "Request to stop fetching logos."); - if (sQueryTask != null) { - sQueryTask.cancel(true); - sQueryTask = null; - } - if (sFetchTask != null) { - sFetchTask.cancel(true); - sFetchTask = null; - } + if (DEBUG) Log.d(TAG, "Request to start fetching logos."); + if (channels == null || channels.isEmpty()) { + return; } + sFetchTask = new FetchLogoTask(context, channels); + sFetchTask.execute(); } private ChannelLogoFetcher() { } - private static final class LoadChannelTask extends AsyncDbTask<Void, Void, List<Channel>> { - private final Context mContext; - - public LoadChannelTask(Context context) { - mContext = context; - } - - @Override - protected List<Channel> doInBackground(Void... arg) { - // Load channels which doesn't have channel logos. - if (DEBUG) Log.d(TAG, "Starts loading the channels from DB"); - String[] projection = - new String[] { Channels._ID, Channels.COLUMN_DISPLAY_NAME }; - String selection = COLUMN_CHANNEL_LOGO + " IS NULL AND " - + Channels.COLUMN_PACKAGE_NAME + "=?"; - String[] selectionArgs = new String[] { mContext.getPackageName() }; - try (Cursor c = mContext.getContentResolver().query(Channels.CONTENT_URI, - projection, selection, selectionArgs, null)) { - if (c == null) { - Log.e(TAG, "Query returns null cursor", new RuntimeException()); - return null; - } - List<Channel> channels = new ArrayList<>(); - while (!isCancelled() && c.moveToNext()) { - long channelId = c.getLong(0); - if (sChannelIdBlackListSet.contains(channelId)) { - continue; - } - channels.add(new Channel.Builder().setId(c.getLong(0)) - .setDisplayName(c.getString(1).toUpperCase(Locale.getDefault())) - .build()); - } - return channels; - } - } - - @Override - protected void onPostExecute(List<Channel> channels) { - synchronized (sLock) { - if (DEBUG) { - int count = channels == null ? 0 : channels.size(); - Log.d(TAG, count + " channels are loaded"); - } - if (sQueryTask == this) { - sQueryTask = null; - if (channels != null && !channels.isEmpty()) { - sFetchTask = new FetchLogoTask(mContext, channels); - sFetchTask.execute(); - } - } - } - } - } - private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> { private final Context mContext; private final List<Channel> mChannels; @@ -180,83 +93,53 @@ public class ChannelLogoFetcher { if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); return null; } - // Load the TMS table data. - if (DEBUG) Log.d(TAG, "Loads TMS data"); - Map<String, String> channelNameLogoUriMap = new HashMap<>(); - try { - channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_US_TABLE_FILE)); - if (isCancelled()) { - if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); - return null; + List<Channel> channelsToUpdate = new ArrayList<>(); + List<Channel> channelsToRemove = new ArrayList<>(); + // Updates or removes the logo by comparing the logo uri which is got from the cloud + // and the stored one. And we assume that the data got form the cloud is 100% + // correct and completed. + SharedPreferences sharedPreferences = + mContext.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS, + Context.MODE_PRIVATE); + SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit(); + Map<String, ?> uncheckedChannels = sharedPreferences.getAll(); + boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean( + PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true); + // Iterating channels. + for (Channel channel : mChannels) { + String channelIdString = Long.toString(channel.getId()); + String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString); + if (!TextUtils.isEmpty(channel.getLogoUri()) + && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) { + channelsToUpdate.add(channel); + sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri()); + } else if (TextUtils.isEmpty(channel.getLogoUri()) + && (!TextUtils.isEmpty(storedChannelLogoUri) + || isFirstTimeFetchChannelLogo)) { + channelsToRemove.add(channel); + sharedPreferencesEditor.remove(channelIdString); } - channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_KR_TABLE_FILE)); - } catch (IOException e) { - Log.e(TAG, "Loading TMS data failed.", e); - return null; } - if (isCancelled()) { - if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); - return null; + + // Removes non existing channels from SharedPreferences. + for (String channelId : uncheckedChannels.keySet()) { + sharedPreferencesEditor.remove(channelId); } - // Iterating channels. - for (Channel channel : mChannels) { + // Updates channel logos. + for (Channel channel : channelsToUpdate) { if (isCancelled()) { if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled"); return null; } - // Download the channel logo. - if (TextUtils.isEmpty(channel.getDisplayName())) { - if (DEBUG) { - Log.d(TAG, "The channel with ID (" + channel.getId() - + ") doesn't have the display name."); - } - sChannelIdBlackListSet.add(channel.getId()); - continue; - } - String channelName = channel.getDisplayName().trim(); - String logoUri = channelNameLogoUriMap.get(channelName); - if (TextUtils.isEmpty(logoUri)) { - if (DEBUG) { - Log.d(TAG, "Can't find a logo URI for channel '" + channelName + "'"); - } - // Find the candidate names. If the channel name is CNN-HD, then find CNNHD - // and CNN. Or if the channel name is KQED+, then find KQED. - String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB); - if (splitNames.length > 1) { - StringBuilder sb = new StringBuilder(); - for (String splitName : splitNames) { - sb.append(splitName); - } - logoUri = channelNameLogoUriMap.get(sb.toString()); - if (DEBUG) { - if (TextUtils.isEmpty(logoUri)) { - Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString() - + "'"); - } - } - } - if (TextUtils.isEmpty(logoUri) - && splitNames[0].length() != channelName.length()) { - logoUri = channelNameLogoUriMap.get(splitNames[0]); - if (DEBUG) { - if (TextUtils.isEmpty(logoUri)) { - Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0] - + "'"); - } - } - } - } - if (TextUtils.isEmpty(logoUri)) { - sChannelIdBlackListSet.add(channel.getId()); - continue; - } + // Downloads the channel logo. + String logoUri = channel.getLogoUri(); ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString( mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE); if (bitmapInfo == null) { Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName() + ", " + "logoUri=" + logoUri + "}"); - sChannelIdBlackListSet.add(channel.getId()); continue; } if (isCancelled()) { @@ -264,12 +147,15 @@ public class ChannelLogoFetcher { return null; } - // Insert the logo to DB. + // Inserts the logo to DB. Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId()); try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) { bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os); } catch (IOException e) { Log.e(TAG, "Failed to write " + logoUri + " to " + dstLogoUri, e); + // Removes it from the shared preference for the failed channels to make it + // retry next time. + sharedPreferencesEditor.remove(Long.toString(channel.getId())); continue; } if (DEBUG) { @@ -277,63 +163,30 @@ public class ChannelLogoFetcher { + dstLogoUri + "}"); } } - if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully."); - return null; - } - @WorkerThread - private Map<String, String> readTmsFile(Context context, String fileName) - throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader( - context.getAssets().open(fileName)))) { - Map<String, String> channelNameLogoUriMap = new HashMap<>(); - String line; - while ((line = reader.readLine()) != null && !isCancelled()) { - String[] data = line.split(FIELD_SEPARATOR); - if (data.length != INDEX_LOGO_URI + 1) { - if (DEBUG) Log.d(TAG, "Invalid or comment row: " + line); - continue; - } - addChannelNames(channelNameLogoUriMap, - data[INDEX_NAME].toUpperCase(Locale.getDefault()), - data[INDEX_LOGO_URI]); - addChannelNames(channelNameLogoUriMap, - data[INDEX_SHORT_NAME].toUpperCase(Locale.getDefault()), - data[INDEX_LOGO_URI]); - addChannelNames(channelNameLogoUriMap, - data[INDEX_CALL_SIGN].toUpperCase(Locale.getDefault()), - data[INDEX_LOGO_URI]); + // Removes the logos for the channels that have logos before but now + // their logo uris are null. + boolean deleteChannelLogoFailed = false; + if (!channelsToRemove.isEmpty()) { + ArrayList<ContentProviderOperation> ops = new ArrayList<>(); + for (Channel channel : channelsToRemove) { + ops.add(ContentProviderOperation.newDelete( + TvContract.buildChannelLogoUri(channel.getId())).build()); + } + try { + mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); + } catch (RemoteException | OperationApplicationException e) { + deleteChannelLogoFailed = true; + Log.e(TAG, "Error deleting obsolete channels", e); } - return channelNameLogoUriMap; } - } - - private void addChannelNames(Map<String, String> channelNameLogoUriMap, String channelName, - String logoUri) { - if (!TextUtils.isEmpty(channelName)) { - channelNameLogoUriMap.put(channelName, logoUri); - // Find the candidate names. - // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and - // "W05AA-D" - String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_TMS); - if (splitNames.length > 1) { - for (String name : splitNames) { - name = name.trim(); - if (channelNameLogoUriMap.get(name) == null) { - channelNameLogoUriMap.put(name, logoUri); - } - } - } - } - } - - @Override - protected void onPostExecute(Void result) { - synchronized (sLock) { - if (sFetchTask == this) { - sFetchTask = null; - } + if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) { + sharedPreferencesEditor.putBoolean( + PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false); } + sharedPreferencesEditor.commit(); + if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully."); + return null; } } } diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java index 59021609..29054aa5 100644 --- a/src/com/android/tv/data/ChannelNumber.java +++ b/src/com/android/tv/data/ChannelNumber.java @@ -17,37 +17,38 @@ package com.android.tv.data; import android.support.annotation.NonNull; +import android.text.TextUtils; import android.view.KeyEvent; +import com.android.tv.util.StringUtils; + +import java.util.Objects; + /** * A convenience class to handle channel number. */ public final class ChannelNumber implements Comparable<ChannelNumber> { - public static final String PRIMARY_CHANNEL_DELIMITER = "-"; - public static final String[] CHANNEL_DELIMITERS = {"-", ".", " "}; - private static final int[] CHANNEL_DELIMITER_KEYCODES = { KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_PERIOD, KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_SPACE }; + /** The major part of the channel number. */ public String majorNumber; + /** The flag which indicates whether it has a delimiter or not. */ public boolean hasDelimiter; + /** The major part of the channel number. */ public String minorNumber; public ChannelNumber() { reset(); } - public ChannelNumber(String major, boolean hasDelimiter, String minor) { - setChannelNumber(major, hasDelimiter, minor); - } - public void reset() { setChannelNumber("", false, ""); } - public void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) { + private void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) { this.majorNumber = majorNumber; this.hasDelimiter = hasDelimiter; this.minorNumber = minorNumber; @@ -56,7 +57,7 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { @Override public String toString() { if (hasDelimiter) { - return majorNumber + PRIMARY_CHANNEL_DELIMITER + minorNumber; + return majorNumber + Channel.CHANNEL_NUMBER_DELIMITER + minorNumber; } return majorNumber; } @@ -75,6 +76,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { return major - opponentMajor; } + @Override + public boolean equals(Object obj) { + if (obj instanceof ChannelNumber) { + ChannelNumber channelNumber = (ChannelNumber) obj; + return TextUtils.equals(majorNumber, channelNumber.majorNumber) + && TextUtils.equals(minorNumber, channelNumber.minorNumber) + && hasDelimiter == channelNumber.hasDelimiter; + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return Objects.hash(majorNumber, hasDelimiter, minorNumber); + } + public static boolean isChannelNumberDelimiterKey(int keyCode) { for (int delimiterKeyCode : CHANNEL_DELIMITER_KEYCODES) { if (delimiterKeyCode == keyCode) { @@ -84,22 +101,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { return false; } + /** + * Returns the ChannelNumber instance. + * <p> + * Note that all the channel number argument should be normalized by + * {@link Channel#normalizeDisplayNumber}. The channels retrieved from + * {@link ChannelDataManager} are already normalized. + */ public static ChannelNumber parseChannelNumber(String number) { if (number == null) { return null; } ChannelNumber ret = new ChannelNumber(); - int indexOfDelimiter = -1; - for (String delimiter : CHANNEL_DELIMITERS) { - indexOfDelimiter = number.indexOf(delimiter); - if (indexOfDelimiter >= 0) { - break; - } - } + int indexOfDelimiter = number.indexOf(Channel.CHANNEL_NUMBER_DELIMITER); if (indexOfDelimiter == 0 || indexOfDelimiter == number.length() - 1) { return null; - } - if (indexOfDelimiter < 0) { + } else if (indexOfDelimiter < 0) { ret.majorNumber = number; if (!isInteger(ret.majorNumber)) { return null; @@ -115,25 +132,31 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { return ret; } + /** + * Compares the channel numbers. + * <p> + * Note that all the channel number arguments should be normalized by + * {@link Channel#normalizeDisplayNumber}. The channels retrieved from + * {@link ChannelDataManager} are already normalized. + */ public static int compare(String lhs, String rhs) { ChannelNumber lhsNumber = parseChannelNumber(lhs); ChannelNumber rhsNumber = parseChannelNumber(rhs); + // Null first if (lhsNumber == null && rhsNumber == null) { - return 0; + return StringUtils.compare(lhs, rhs); } else if (lhsNumber == null /* && rhsNumber != null */) { return -1; - } else if (lhsNumber != null && rhsNumber == null) { + } else if (rhsNumber == null) { return 1; } return lhsNumber.compareTo(rhsNumber); } - public static boolean isInteger(String string) { + private static boolean isInteger(String string) { try { Integer.parseInt(string); - } catch(NumberFormatException e) { - return false; - } catch(NullPointerException e) { + } catch(NumberFormatException | NullPointerException e) { return false; } return true; diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java index 6054f089..e33ca18f 100644 --- a/src/com/android/tv/data/InternalDataUtils.java +++ b/src/com/android/tv/data/InternalDataUtils.java @@ -21,7 +21,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.tv.data.Program.CriticScore; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index fe461f14..709863cf 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -38,5 +38,9 @@ public interface StreamInfo { int getAudioChannelCount(); boolean hasClosedCaption(); boolean isVideoAvailable(); + /** + * Returns true, if video or audio is available. + */ + boolean isVideoOrAudioAvailable(); int getVideoUnavailableReason(); } diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java index 3b093b6a..ddd68ad7 100644 --- a/src/com/android/tv/data/epg/EpgFetcher.java +++ b/src/com/android/tv/data/epg/EpgFetcher.java @@ -16,13 +16,11 @@ package com.android.tv.data.epg; -import android.Manifest; import android.annotation.SuppressLint; import android.content.ContentProviderOperation; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; -import android.content.pm.PackageManager; import android.database.Cursor; import android.location.Address; import android.media.tv.TvContentRating; @@ -46,9 +44,11 @@ import com.android.tv.TvApplication; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.ChannelLogoFetcher; import com.android.tv.data.InternalDataUtils; import com.android.tv.data.Lineup; import com.android.tv.data.Program; +import com.android.tv.tuner.util.PostalCodeUtils; import com.android.tv.util.LocationUtils; import com.android.tv.util.RecurringRunner; import com.android.tv.util.Utils; @@ -56,8 +56,10 @@ import com.android.tv.util.Utils; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -69,14 +71,27 @@ public class EpgFetcher { private static final boolean DEBUG = false; private static final int MSG_FETCH_EPG = 1; + private static final int MSG_FAST_FETCH_EPG = 2; private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4); private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1); private static final long LOCATION_INIT_WAIT_MS = TimeUnit.SECONDS.toMillis(10); private static final long LOCATION_ERROR_WAIT_MS = TimeUnit.HOURS.toMillis(1); + private static final long NO_INFO_FETCHED_WAIT_MS = TimeUnit.SECONDS.toMillis(10); private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30); + private static final long PROGRAM_FETCH_SHORT_DURATION_SEC = TimeUnit.HOURS.toSeconds(3); + private static final long PROGRAM_FETCH_LONG_DURATION_SEC = TimeUnit.DAYS.toSeconds(2) + + EPG_PREFETCH_RECURRING_PERIOD_MS / 1000; + + // This equals log2(EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1), + // since we will double waiting time every other trial, therefore this limit the maximum + // waiting time less than half of EPG_PREFETCH_RECURRING_PERIOD_MS. + private static final int NO_INFO_RETRY_LIMIT = 31 - Integer.numberOfLeadingZeros( + (int) (EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1)); + private static final int BATCH_OPERATION_COUNT = 100; + private static final int QUERY_CHANNEL_COUNT = 50; private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry(); private static final String CONTENT_RATING_SEPARATOR = ","; @@ -96,8 +111,11 @@ public class EpgFetcher { private EpgFetcherHandler mHandler; private RecurringRunner mRecurringRunner; private boolean mStarted; + private boolean mScanningChannels; + private int mFetchRetryCount; private long mLastEpgTimestamp = -1; + // @GuardedBy("this") private String mLineupId; public static synchronized EpgFetcher getInstance(Context context) { @@ -122,21 +140,33 @@ public class EpgFetcher { @Override public void onLoadFinished() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()"); - handleChannelChanged(); + if (!mScanningChannels) { + handleChannelChanged(); + } } @Override public void onChannelListUpdated() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()"); - handleChannelChanged(); + if (!mScanningChannels) { + handleChannelChanged(); + } } @Override public void onChannelBrowsableChanged() { if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelBrowsableChanged()"); - handleChannelChanged(); + if (!mScanningChannels) { + handleChannelChanged(); + } } }); + // Warm up to get address, because the first call of getCurrentAddress is usually failed. + try { + LocationUtils.getCurrentAddress(mContext); + } catch (SecurityException | IOException e) { + // Do nothing + } } private void handleChannelChanged() { @@ -145,7 +175,9 @@ public class EpgFetcher { stop(); } } else { - start(); + if (canStart()) { + start(); + } } } @@ -173,17 +205,14 @@ public class EpgFetcher { if (!TextUtils.isEmpty(getLastLineupId())) { return true; } - if (mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - if (DEBUG) Log.d(TAG, "No permission to check the current location."); - return false; + if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + return true; } - try { Address address = LocationUtils.getCurrentAddress(mContext); if (address != null && !TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { - if (DEBUG) Log.d(TAG, "Country not supported: " + address.getCountryCode()); + Log.i(TAG, "Country not supported: " + address.getCountryCode()); return false; } } catch (SecurityException e) { @@ -197,9 +226,13 @@ public class EpgFetcher { /** * Starts fetching EPG. + * + * @param resetNextRunTime if true, next run time is reset, so EPG will be fetched + * {@link #EPG_PREFETCH_RECURRING_PERIOD_MS} later. + * otherwise, EPG is fetched when this method is called. */ @MainThread - public void start() { + private void startInternal(boolean resetNextRunTime) { if (DEBUG) Log.d(TAG, "start()"); if (mStarted) { if (DEBUG) Log.d(TAG, "EpgFetcher thread already started."); @@ -215,19 +248,35 @@ public class EpgFetcher { mHandler = new EpgFetcherHandler(handlerThread.getLooper(), this); mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS, new EpgRunner(), null); - mRecurringRunner.start(); + mRecurringRunner.start(resetNextRunTime); if (DEBUG) Log.d(TAG, "EpgFetcher thread started successfully."); } + @MainThread + public void start() { + if (System.currentTimeMillis() - getLastUpdatedEpgTimestamp() > + EPG_PREFETCH_RECURRING_PERIOD_MS) { + startImmediately(false); + } else { + startInternal(false); + } + } + /** * Starts fetching EPG immediately if possible without waiting for the timer. + * + * @param clearStoredLineupId if true, stored lineup id will be clear before fetching EPG. */ @MainThread - public void startImmediately() { - start(); + public void startImmediately(boolean clearStoredLineupId) { + startInternal(true); if (mStarted) { + if (clearStoredLineupId) { + if (DEBUG) Log.d(TAG, "Clear stored lineup id: " + mLineupId); + setLastLineupId(null); + } if (DEBUG) Log.d(TAG, "Starting fetcher immediately"); - fetchEpg(); + postFetchRequest(true, 0); } } @@ -246,48 +295,71 @@ public class EpgFetcher { mHandler.getLooper().quit(); } - private void fetchEpg() { - fetchEpg(0); + /** + * Notifies EPG fetcher that channel scanning is started. + */ + @MainThread + public void onChannelScanStarted() { + stop(); + mScanningChannels = true; } - private void fetchEpg(long delay) { - mHandler.removeMessages(MSG_FETCH_EPG); - mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, delay); + /** + * Notifies EPG fetcher that channel scanning is finished. + */ + @MainThread + public void onChannelScanFinished() { + mScanningChannels = false; + start(); + } + + private void postFetchRequest(boolean fastFetch, long delay) { + int msg = fastFetch ? MSG_FAST_FETCH_EPG : MSG_FETCH_EPG; + mHandler.removeMessages(msg); + mHandler.sendEmptyMessageDelayed(msg, delay); } private void onFetchEpg() { + onFetchEpg(false); + } + + private void onFetchEpg(boolean fastFetch) { if (DEBUG) Log.d(TAG, "Start fetching EPG."); if (!mEpgReader.isAvailable()) { - if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available."); - fetchEpg(EPG_READER_INIT_WAIT_MS); + Log.i(TAG, "EPG reader is not temporarily available."); + postFetchRequest(fastFetch, EPG_READER_INIT_WAIT_MS); return; } String lineupId = getLastLineupId(); if (lineupId == null) { - Address address; try { - address = LocationUtils.getCurrentAddress(mContext); + PostalCodeUtils.updatePostalCode(mContext); } catch (IOException e) { - if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); - fetchEpg(LOCATION_ERROR_WAIT_MS); - return; + if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); + postFetchRequest(fastFetch, LOCATION_ERROR_WAIT_MS); + return; + } } catch (SecurityException e) { - Log.w(TAG, "No permission to get the current location."); - return; - } - if (address == null) { - if (DEBUG) Log.d(TAG, "Null address returned."); - fetchEpg(LOCATION_INIT_WAIT_MS); + if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + Log.w(TAG, "No permission to get the current location."); + return; + } + } catch (PostalCodeUtils.NoPostalCodeException e) { + Log.i(TAG, "Failed to get the current postal code."); + postFetchRequest(fastFetch, LOCATION_INIT_WAIT_MS); return; } - if (DEBUG) Log.d(TAG, "Current location is " + address); + String postalCode = PostalCodeUtils.getLastPostalCode(mContext); + if (DEBUG) Log.d(TAG, "The current postal code is " + postalCode); - lineupId = getLineupForAddress(address); + lineupId = pickLineupForPostalCode(postalCode); if (lineupId != null) { - if (DEBUG) Log.d(TAG, "Saving lineup " + lineupId + "found for " + address); + Log.i(TAG, "Selecting the lineup " + lineupId); setLastLineupId(lineupId); } else { - if (DEBUG) Log.d(TAG, "No lineup found for " + address); + Log.i(TAG, "Failed to get lineup id"); + retryFetchEpg(fastFetch); return; } } @@ -299,48 +371,109 @@ public class EpgFetcher { return; } - boolean updated = false; List<Channel> channels = mEpgReader.getChannels(lineupId); - for (Channel channel : channels) { - List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId())); - Collections.sort(programs); - if (DEBUG) { - Log.d(TAG, "Fetched " + programs.size() + " programs for channel " + channel); - } - if (updateEpg(channel.getId(), programs)) { - updated = true; + if (channels.isEmpty()) { + Log.i(TAG, "Failed to get EPG channels."); + retryFetchEpg(fastFetch); + return; + } + mFetchRetryCount = 0; + if (!fastFetch) { + for (Channel channel : channels) { + if (!mStarted) { + break; + } + List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId())); + Collections.sort(programs); + Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channel); + updateEpg(channel.getId(), programs); } + setLastUpdatedEpgTimestamp(epgTimestamp); + } else { + handleFastFetch(channels, PROGRAM_FETCH_SHORT_DURATION_SEC); + if (DEBUG) Log.d(TAG, "First fast fetch Done."); + handleFastFetch(channels, PROGRAM_FETCH_LONG_DURATION_SEC); + if (DEBUG) Log.d(TAG, "Second fast fetch Done."); } - final boolean epgUpdated = updated; - setLastUpdatedEpgTimestamp(epgTimestamp); - mHandler.removeMessages(MSG_FETCH_EPG); + if (!fastFetch) { + mHandler.removeMessages(MSG_FETCH_EPG); + } if (DEBUG) Log.d(TAG, "Fetching EPG is finished."); + // Start to fetch channel logos after epg fetching finished. + ChannelLogoFetcher.startFetchingChannelLogos(mContext, channels); } - @Nullable - private String getLineupForAddress(Address address) { - String lineup = null; - if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { - String postalCode = address.getPostalCode(); - if (!TextUtils.isEmpty(postalCode)) { - lineup = getLineupForPostalCode(postalCode); + private void retryFetchEpg(boolean fastFetch) { + if (mFetchRetryCount < NO_INFO_RETRY_LIMIT) { + postFetchRequest(fastFetch, NO_INFO_FETCHED_WAIT_MS * 1 << mFetchRetryCount); + mFetchRetryCount++; + } else { + mFetchRetryCount = 0; + } + } + + private void handleFastFetch(List<Channel> channels, long duration) { + List<Long> channelIds = new ArrayList<>(channels.size()); + for (Channel channel : channels) { + channelIds.add(channel.getId()); + } + Map<Long, List<Program>> allPrograms = new HashMap<>(); + List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT); + for (Long channelId : channelIds) { + queryChannelIds.add(channelId); + if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) { + allPrograms.putAll( + new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration))); + queryChannelIds.clear(); } } - return lineup; + if (!queryChannelIds.isEmpty()) { + allPrograms.putAll( + new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration))); + } + for (Channel channel : channels) { + List<Program> programs = allPrograms.get(channel.getId()); + if (programs == null) continue; + Collections.sort(programs); + Log.i(TAG, "Fast fetched " + programs.size() + " programs for channel " + channel); + updateEpg(channel.getId(), programs); + } } @Nullable - private String getLineupForPostalCode(String postalCode) { + private String pickLineupForPostalCode(String postalCode) { List<Lineup> lineups = mEpgReader.getLineups(postalCode); + int maxCount = 0; + String maxLineupId = null; for (Lineup lineup : lineups) { - // TODO(EPG): handle more than OTA digital - if (lineup.type == Lineup.LINEUP_BROADCAST_DIGITAL) { - if (DEBUG) Log.d(TAG, "Setting lineup to " + lineup.name + "(" + lineup.id + ")"); - return lineup.id; + int count = getMatchedChannelCount(lineup.id); + Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches"); + if (count > maxCount) { + maxCount = count; + maxLineupId = lineup.id; } } - return null; + return maxLineupId; + } + + private int getMatchedChannelCount(String lineupId) { + // Construct a list of display numbers for existing channels. + List<Channel> channels = mChannelDataManager.getChannelList(); + if (channels.isEmpty()) { + if (DEBUG) Log.d(TAG, "No existing channel to compare"); + return 0; + } + List<String> numbers = new ArrayList<>(channels.size()); + for (Channel c : channels) { + // We only support local channels from physical tuners. + if (c.isPhysicalTunerChannel()) { + numbers.add(c.getDisplayNumber()); + } + } + + numbers.retainAll(mEpgReader.getChannelNumbers(lineupId)); + return numbers.size(); } private long getLastUpdatedEpgTimestamp() { @@ -357,16 +490,16 @@ public class EpgFetcher { KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).commit(); } - private String getLastLineupId() { + synchronized private String getLastLineupId() { if (mLineupId == null) { mLineupId = PreferenceManager.getDefaultSharedPreferences(mContext) .getString(KEY_LAST_LINEUP_ID, null); } - if (DEBUG) Log.d(TAG, "Last lineup_id " + mLineupId); + if (DEBUG) Log.d(TAG, "Last lineup is " + mLineupId); return mLineupId; } - private void setLastLineupId(String lineupId) { + synchronized private void setLastLineupId(String lineupId) { mLineupId = lineupId; PreferenceManager.getDefaultSharedPreferences(mContext).edit() .putString(KEY_LAST_LINEUP_ID, lineupId).commit(); @@ -381,19 +514,9 @@ public class EpgFetcher { long startTimeMs = System.currentTimeMillis(); long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION; List<Program> oldPrograms = queryPrograms(channelId, startTimeMs, endTimeMs); - Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null; int oldProgramsIndex = 0; int newProgramsIndex = 0; - // Skip the past programs. They will be automatically removed by the system. - if (currentOldProgram != null) { - long oldStartTimeUtcMillis = currentOldProgram.getStartTimeUtcMillis(); - for (Program program : newPrograms) { - if (program.getEndTimeUtcMillis() > oldStartTimeUtcMillis) { - break; - } - newProgramsIndex++; - } - } + // Compare the new programs with old programs one by one and update/delete the old one // or insert new program if there is no matching program in the database. ArrayList<ContentProviderOperation> ops = new ArrayList<>(); @@ -439,7 +562,7 @@ public class EpgFetcher { } if (addNewProgram) { ops.add(ContentProviderOperation - .newInsert(TvContract.Programs.CONTENT_URI) + .newInsert(Programs.CONTENT_URI) .withValues(toContentValues(newProgram)) .build()); } @@ -501,27 +624,25 @@ public class EpgFetcher { @SuppressWarnings("deprecation") private static ContentValues toContentValues(Program program) { ContentValues values = new ContentValues(); - values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); - putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle()); - putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); + values.put(Programs.COLUMN_CHANNEL_ID, program.getChannelId()); + putValue(values, Programs.COLUMN_TITLE, program.getTitle()); + putValue(values, Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); if (BuildCompat.isAtLeastN()) { - putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, - program.getSeasonNumber()); - putValue(values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, - program.getEpisodeNumber()); + putValue(values, Programs.COLUMN_SEASON_DISPLAY_NUMBER, program.getSeasonNumber()); + putValue(values, Programs.COLUMN_EPISODE_DISPLAY_NUMBER, program.getEpisodeNumber()); } else { - putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); - putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); + putValue(values, Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); + putValue(values, Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); } - putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); - putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); - putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); + putValue(values, Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); + putValue(values, Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription()); + putValue(values, Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); + putValue(values, Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); String[] canonicalGenres = program.getCanonicalGenres(); if (canonicalGenres != null && canonicalGenres.length > 0) { - putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, - Genres.encode(canonicalGenres)); + putValue(values, Programs.COLUMN_CANONICAL_GENRE, Genres.encode(canonicalGenres)); } else { - putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, ""); + putValue(values, Programs.COLUMN_CANONICAL_GENRE, ""); } TvContentRating[] ratings = program.getContentRatings(); if (ratings != null && ratings.length > 0) { @@ -530,14 +651,13 @@ public class EpgFetcher { sb.append(CONTENT_RATING_SEPARATOR); sb.append(ratings[i].flattenToString()); } - putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, sb.toString()); + putValue(values, Programs.COLUMN_CONTENT_RATING, sb.toString()); } else { - putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, ""); + putValue(values, Programs.COLUMN_CONTENT_RATING, ""); } - values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - program.getStartTimeUtcMillis()); - values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); - putValue(values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, + values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, program.getStartTimeUtcMillis()); + values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); + putValue(values, Programs.COLUMN_INTERNAL_PROVIDER_DATA, InternalDataUtils.serializeInternalProviderData(program)); return values; } @@ -569,6 +689,9 @@ public class EpgFetcher { case MSG_FETCH_EPG: epgFetcher.onFetchEpg(); break; + case MSG_FAST_FETCH_EPG: + epgFetcher.onFetchEpg(true); + break; default: super.handleMessage(msg); break; @@ -579,7 +702,7 @@ public class EpgFetcher { private class EpgRunner implements Runnable { @Override public void run() { - fetchEpg(); + postFetchRequest(false, 0); } } } diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java index 4f3b6f52..95cd933e 100644 --- a/src/com/android/tv/data/epg/EpgReader.java +++ b/src/com/android/tv/data/epg/EpgReader.java @@ -22,9 +22,10 @@ import android.support.annotation.WorkerThread; import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; -import com.android.tv.dvr.SeriesInfo; +import com.android.tv.dvr.data.SeriesInfo; import java.util.List; +import java.util.Map; /** * An interface used to retrieve the EPG data. This class should be used in worker thread. @@ -43,21 +44,37 @@ public interface EpgReader { long getEpgTimestamp(); /** - * Returns the channels list. + * Returns the lineups list. + */ + List<Lineup> getLineups(@NonNull String postalCode); + + /** + * Returns the list of channel numbers (unsorted) for the given lineup. The result is used to + * choose the most appropriate lineup among others by comparing the channel numbers of the + * existing channels on the device. + */ + List<String> getChannelNumbers(@NonNull String lineupId); + + /** + * Returns the list of channels for the given lineup. The returned channels should map into the + * existing channels on the device. This method is usually called after selecting the lineup. */ List<Channel> getChannels(@NonNull String lineupId); /** - * Returns the lineups list. + * Returns the programs for the given channel. Must call {@link #getChannels(String)} + * beforehand. Note that the {@code Program} doesn't have valid program ID because it's not + * retrieved from TvProvider. */ - List<Lineup> getLineups(@NonNull String postalCode); + List<Program> getPrograms(long channelId); /** - * Returns the programs for the given channel. The result is sorted by the start time. + * Returns the programs for the given channels. * Note that the {@code Program} doesn't have valid program ID because it's not retrieved from * TvProvider. + * This method is only used to get programs for a short duration typically. */ - List<Program> getPrograms(long channelId); + Map<Long, List<Program>> getPrograms(List<Long> channelIds, long duration); /** * Returns the series information for the given series ID. diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java index 64093f89..220daf22 100644 --- a/src/com/android/tv/data/epg/StubEpgReader.java +++ b/src/com/android/tv/data/epg/StubEpgReader.java @@ -21,10 +21,11 @@ import android.content.Context; import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; -import com.android.tv.dvr.SeriesInfo; +import com.android.tv.dvr.data.SeriesInfo; import java.util.Collections; import java.util.List; +import java.util.Map; /** * A stub class to read EPG. @@ -44,12 +45,17 @@ public class StubEpgReader implements EpgReader{ } @Override - public List<Channel> getChannels(String lineupId) { + public List<Lineup> getLineups(String postalCode) { return Collections.emptyList(); } @Override - public List<Lineup> getLineups(String postalCode) { + public List<String> getChannelNumbers(String lineupId) { + return Collections.emptyList(); + } + + @Override + public List<Channel> getChannels(String lineupId) { return Collections.emptyList(); } @@ -59,6 +65,11 @@ public class StubEpgReader implements EpgReader{ } @Override + public Map<Long, List<Program>> getPrograms(List<Long> channelIds, long duration) { + return Collections.emptyMap(); + } + + @Override public SeriesInfo getSeriesInfo(String seriesId) { return null; } |