diff options
Diffstat (limited to 'src/com/android/tv/data')
19 files changed, 758 insertions, 1238 deletions
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 1204a49f..eda188e4 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -28,8 +28,7 @@ import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; -import com.android.tv.common.CommonConstants; -import com.android.tv.common.util.CommonUtils; +import com.android.tv.common.TvCommonConstants; import com.android.tv.util.ImageLoader; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -125,7 +124,7 @@ public final class Channel { channel.mAppLinkIconUri = cursor.getString(index++); channel.mAppLinkPosterArtUri = cursor.getString(index++); channel.mAppLinkIntentUri = cursor.getString(index++); - if (CommonUtils.isBundledInput(channel.mInputId)) { + if (Utils.isBundledInput(channel.mInputId)) { channel.mRecordingProhibited = cursor.getInt(index++) != 0; } return channel; @@ -626,7 +625,7 @@ public final class Channel { if (intent.resolveActivityInfo(pm, 0) != null) { mAppLinkIntent = intent; mAppLinkIntent.putExtra( - CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); + TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); mAppLinkType = APP_LINK_TYPE_CHANNEL; return; } else { @@ -643,7 +642,7 @@ public final class Channel { mAppLinkIntent = pm.getLeanbackLaunchIntentForPackage(mPackageName); if (mAppLinkIntent != null) { mAppLinkIntent.putExtra( - CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); + TvCommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); mAppLinkType = APP_LINK_TYPE_APP; } } diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index 68fbdb6a..e4d1cd85 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -38,11 +38,11 @@ import android.support.annotation.VisibleForTesting; import android.util.ArraySet; import android.util.Log; import android.util.MutableInt; +import com.android.tv.common.SharedPreferencesUtils; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.WeakHandler; -import com.android.tv.common.util.PermissionUtils; -import com.android.tv.common.util.SharedPreferencesUtils; import com.android.tv.util.AsyncDbTask; +import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.io.IOException; @@ -62,7 +62,6 @@ import java.util.concurrent.CopyOnWriteArraySet; * methods are called in only the main thread. */ @AnyThread -@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed public class ChannelDataManager { private static final String TAG = "ChannelDataManager"; private static final boolean DEBUG = false; diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java index 8aaaf73a..2dc43102 100644 --- a/src/com/android/tv/data/ChannelLogoFetcher.java +++ b/src/com/android/tv/data/ChannelLogoFetcher.java @@ -28,10 +28,10 @@ import android.os.RemoteException; import android.support.annotation.MainThread; import android.text.TextUtils; import android.util.Log; -import com.android.tv.common.util.PermissionUtils; -import com.android.tv.common.util.SharedPreferencesUtils; +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.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -42,7 +42,6 @@ import java.util.Map; * 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. */ -@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed public class ChannelLogoFetcher { private static final String TAG = "ChannelLogoFetcher"; private static final boolean DEBUG = false; diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java index 1623b33d..63f8a972 100644 --- a/src/com/android/tv/data/ChannelNumber.java +++ b/src/com/android/tv/data/ChannelNumber.java @@ -19,7 +19,7 @@ package com.android.tv.data; import android.support.annotation.NonNull; import android.text.TextUtils; import android.view.KeyEvent; -import com.android.tv.common.util.StringUtils; +import com.android.tv.util.StringUtils; import java.util.Objects; /** A convenience class to handle channel number. */ @@ -43,23 +43,6 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { reset(); } - /** - * {@code lhs} and {@code rhs} are equivalent if {@link ChannelNumber#compare(String, String)} - * is 0 or if only one has a delimiter and both {@link ChannelNumber#majorNumber} equals. - */ - public static boolean equivalent(String lhs, String rhs) { - if (compare(lhs, rhs) == 0) { - return true; - } - // Match if only one has delimiter - ChannelNumber lhsNumber = parseChannelNumber(lhs); - ChannelNumber rhsNumber = parseChannelNumber(rhs); - return lhsNumber != null - && rhsNumber != null - && lhsNumber.hasDelimiter != rhsNumber.hasDelimiter - && lhsNumber.majorNumber.equals(rhsNumber.majorNumber); - } - public void reset() { setChannelNumber("", false, ""); } diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java index 99a3d4e8..4c30d395 100644 --- a/src/com/android/tv/data/InternalDataUtils.java +++ b/src/com/android/tv/data/InternalDataUtils.java @@ -33,7 +33,6 @@ import java.util.List; * android.media.tv.TvContract.Programs#COLUMN_INTERNAL_PROVIDER_DATA} field in the {@link * android.media.tv.TvContract.Programs}. */ -@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed public final class InternalDataUtils { private static final boolean DEBUG = false; private static final String TAG = "InternalDataUtils"; diff --git a/src/com/android/tv/data/Lineup.java b/src/com/android/tv/data/Lineup.java index 4393cd3d..0f11c1cc 100644 --- a/src/com/android/tv/data/Lineup.java +++ b/src/com/android/tv/data/Lineup.java @@ -19,45 +19,23 @@ package com.android.tv.data; import android.support.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Collections; -import java.util.List; /** A class that represents a lineup. */ public class Lineup { /** The ID of this lineup. */ - public String getId() { - return id; - } + public final String id; /** The type associated with this lineup. */ - public int getType() { - return type; - } - - /** The human readable name associated with this lineup. */ - public String getName() { - return name; - } + public final int type; /** The human readable name associated with this lineup. */ - public String getLocation() { - return location; - } - - /** An unmodifiable list of channel numbers that this lineup has. */ - public List<String> getChannels() { - return channels; - } + public final String name; - private final String id; - - private final int type; - - private final String name; - - private final String location; - - private final List<String> channels; + /** + * Location this lineup can be found. This is a human readable description of a geographic + * location. + */ + public final String location; @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -66,9 +44,7 @@ public class Lineup { LINEUP_BROADCAST_DIGITAL, LINEUP_BROADCAST_ANALOG, LINEUP_IPTV, - LINEUP_MVPD, - LINEUP_INTERNET, - LINEUP_OTHER + LINEUP_MVPD }) public @interface LineupType {} @@ -88,23 +64,16 @@ public class Lineup { public static final int LINEUP_IPTV = 4; /** - * Indicates the lineup is either satellite, cable or IPTV but we are not sure which specific + * Indicates the lineup is either satelite, cable or IPTV but we are not sure which specific * type. */ public static final int LINEUP_MVPD = 5; - /** Lineup type for Internet. */ - public static final int LINEUP_INTERNET = 6; - - /** Lineup type for other. */ - public static final int LINEUP_OTHER = 7; - /** Creates a lineup. */ - public Lineup(String id, int type, String name, String location, List<String> channels) { + public Lineup(String id, int type, String name, String location) { this.id = id; this.type = type; this.name = name; this.location = location; - this.channels = Collections.unmodifiableList(channels); } } diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java index ac78147b..b103a5d7 100644 --- a/src/com/android/tv/data/PreviewDataManager.java +++ b/src/com/android/tv/data/PreviewDataManager.java @@ -36,7 +36,7 @@ import android.support.media.tv.PreviewProgram; import android.util.Log; import android.util.Pair; import com.android.tv.R; -import com.android.tv.common.util.PermissionUtils; +import com.android.tv.util.PermissionUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashMap; @@ -47,22 +47,22 @@ import java.util.concurrent.CopyOnWriteArraySet; /** Class to manage the preview data. */ @TargetApi(Build.VERSION_CODES.O) @MainThread -@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed public class PreviewDataManager { private static final String TAG = "PreviewDataManager"; - private static final boolean DEBUG = false; + // STOPSHIP: set it to false. + private static final boolean DEBUG = true; /** Invalid preview channel ID. */ public static final long INVALID_PREVIEW_CHANNEL_ID = -1; - @IntDef({TYPE_DEFAULT_PREVIEW_CHANNEL, TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL}) + @IntDef({(int) TYPE_DEFAULT_PREVIEW_CHANNEL, (int) TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL}) @Retention(RetentionPolicy.SOURCE) public @interface PreviewChannelType {} /** Type of default preview channel */ - public static final int TYPE_DEFAULT_PREVIEW_CHANNEL = 1; + public static final long TYPE_DEFAULT_PREVIEW_CHANNEL = 1; /** Type of recorded program channel */ - public static final int TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2; + public static final long TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2; private final Context mContext; private final ContentResolver mContentResolver; @@ -604,8 +604,7 @@ public class PreviewDataManager { .setPosterArtUri(program.getPosterArtUri()) .setIntentUri(program.getIntentUri()) .setPreviewVideoUri(program.getPreviewVideoUri()) - .setInternalProviderId(Long.toString(program.getId())) - .setContentId(program.getIntentUri().toString()); + .setInternalProviderId(Long.toString(program.getId())); return builder.build(); } diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java index 252092b0..845ca9d4 100644 --- a/src/com/android/tv/data/PreviewProgramContent.java +++ b/src/com/android/tv/data/PreviewProgramContent.java @@ -17,18 +17,17 @@ package com.android.tv.data; import android.content.Context; +import android.media.tv.TvContract; import android.net.Uri; -import android.support.annotation.VisibleForTesting; -import android.support.media.tv.TvContractCompat; import android.text.TextUtils; import android.util.Pair; -import com.android.tv.TvSingletons; +import com.android.tv.TvApplication; import com.android.tv.dvr.data.RecordedProgram; import java.util.Objects; /** A class to store the content of preview programs. */ public class PreviewProgramContent { - @VisibleForTesting static final String PARAM_INPUT = "input"; + private static final String PARAM_INPUT = "input"; private long mId; private long mPreviewChannelId; @@ -44,30 +43,17 @@ public class PreviewProgramContent { public static PreviewProgramContent createFromProgram( Context context, long previewChannelId, Program program) { Channel channel = - TvSingletons.getSingletons(context) + TvApplication.getSingletons(context) .getChannelDataManager() .getChannel(program.getChannelId()); - return channel == null ? null : createFromProgram(previewChannelId, program, channel); - } - - /** Create preview program content from {@link RecordedProgram} */ - public static PreviewProgramContent createFromRecordedProgram( - Context context, long previewChannelId, RecordedProgram recordedProgram) { - Channel channel = - TvSingletons.getSingletons(context) - .getChannelDataManager() - .getChannel(recordedProgram.getChannelId()); - return createFromRecordedProgram(previewChannelId, recordedProgram, channel); - } - - @VisibleForTesting - static PreviewProgramContent createFromProgram( - long previewChannelId, Program program, Channel channel) { + if (channel == null) { + return null; + } String channelDisplayName = channel.getDisplayName(); return new PreviewProgramContent.Builder() .setId(program.getId()) .setPreviewChannelId(previewChannelId) - .setType(TvContractCompat.PreviewPrograms.TYPE_CHANNEL) + .setType(TvContract.PreviewPrograms.TYPE_CHANNEL) .setLive(true) .setTitle(program.getTitle()) .setDescription( @@ -82,15 +68,22 @@ public class PreviewProgramContent { .build(); } - @VisibleForTesting - static PreviewProgramContent createFromRecordedProgram( - long previewChannelId, RecordedProgram recordedProgram, Channel channel) { - String channelDisplayName = channel == null ? null : channel.getDisplayName(); - Uri recordedProgramUri = TvContractCompat.buildRecordedProgramUri(recordedProgram.getId()); + /** Create preview program content from {@link RecordedProgram} */ + public static PreviewProgramContent createFromRecordedProgram( + Context context, long previewChannelId, RecordedProgram recordedProgram) { + Channel channel = + TvApplication.getSingletons(context) + .getChannelDataManager() + .getChannel(recordedProgram.getChannelId()); + String channelDisplayName = null; + if (channel != null) { + channelDisplayName = channel.getDisplayName(); + } + Uri recordedProgramUri = TvContract.buildRecordedProgramUri(recordedProgram.getId()); return new PreviewProgramContent.Builder() .setId(recordedProgram.getId()) .setPreviewChannelId(previewChannelId) - .setType(TvContractCompat.PreviewPrograms.TYPE_CLIP) + .setType(TvContract.PreviewPrograms.TYPE_CLIP) .setTitle(recordedProgram.getTitle()) .setDescription(channelDisplayName != null ? channelDisplayName : "") .setPosterArtUri(Uri.parse(recordedProgram.getPosterArtUri())) diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java index 30a3033e..f47a3a06 100644 --- a/src/com/android/tv/data/Program.java +++ b/src/com/android/tv/data/Program.java @@ -33,9 +33,8 @@ import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import com.android.tv.common.BuildConfig; +import com.android.tv.common.CollectionUtils; import com.android.tv.common.TvContentRatingCache; -import com.android.tv.common.util.CollectionUtils; -import com.android.tv.common.util.CommonUtils; import com.android.tv.util.ImageLoader; import com.android.tv.util.Utils; import java.io.Serializable; @@ -129,7 +128,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P builder.setEndTimeUtcMillis(cursor.getLong(index++)); builder.setVideoWidth((int) cursor.getLong(index++)); builder.setVideoHeight((int) cursor.getLong(index++)); - if (CommonUtils.isInBundledPackageSet(packageName)) { + if (Utils.isInBundledPackageSet(packageName)) { InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder); } index++; @@ -476,9 +475,6 @@ public final class Program extends BaseProgram implements Comparable<Program>, P public static ContentValues toContentValues(Program program) { ContentValues values = new ContentValues(); values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); - if (!TextUtils.isEmpty(program.getPackageName())) { - values.put(Programs.COLUMN_PACKAGE_NAME, program.getPackageName()); - } putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle()); putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java index 3a7693a4..639ac99a 100644 --- a/src/com/android/tv/data/ProgramDataManager.java +++ b/src/com/android/tv/data/ProgramDataManager.java @@ -35,8 +35,8 @@ import android.util.LongSparseArray; import android.util.LruCache; import com.android.tv.common.MemoryManageable; import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.util.Clock; import com.android.tv.util.AsyncDbTask; +import com.android.tv.util.Clock; import com.android.tv.util.MultiLongSparseArray; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -52,7 +52,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @MainThread -@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed public class ProgramDataManager implements MemoryManageable { private static final String TAG = "ProgramDataManager"; private static final boolean DEBUG = false; diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java index 25ba7716..8c9756b0 100644 --- a/src/com/android/tv/data/WatchedHistoryManager.java +++ b/src/com/android/tv/data/WatchedHistoryManager.java @@ -1,18 +1,3 @@ -/* - * Copyright (C) 2017 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.android.tv.data; import android.content.Context; @@ -27,7 +12,7 @@ import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; -import com.android.tv.common.util.SharedPreferencesUtils; +import com.android.tv.common.SharedPreferencesUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -43,7 +28,6 @@ import java.util.concurrent.TimeUnit; * * <p>Note that this class is not thread safe. Please use this on one thread. */ -@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed public class WatchedHistoryManager { private static final String TAG = "WatchedHistoryManager"; private static final boolean DEBUG = false; diff --git a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java deleted file mode 100644 index 90d109d7..00000000 --- a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2018 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.data.epg; - -import com.android.tv.data.Channel; - -/** - * Hand copy of generated Autovalue class. - * - * TODO get autovalue working - */ -final class AutoValue_EpgReader_EpgChannel extends EpgReader.EpgChannel { - - private final Channel channel; - private final String epgChannelId; - - AutoValue_EpgReader_EpgChannel( - Channel channel, - String epgChannelId) { - if (channel == null) { - throw new NullPointerException("Null channel"); - } - this.channel = channel; - if (epgChannelId == null) { - throw new NullPointerException("Null epgChannelId"); - } - this.epgChannelId = epgChannelId; - } - - @Override - public Channel getChannel() { - return channel; - } - - @Override - public String getEpgChannelId() { - return epgChannelId; - } - - @Override - public String toString() { - return "EpgChannel{" - + "channel=" + channel + ", " - + "epgChannelId=" + epgChannelId - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof EpgReader.EpgChannel) { - EpgReader.EpgChannel that = (EpgReader.EpgChannel) o; - return (this.channel.equals(that.getChannel())) - && (this.epgChannelId.equals(that.getEpgChannelId())); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= this.channel.hashCode(); - h *= 1000003; - h ^= this.epgChannelId.hashCode(); - return h; - } - -} - diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java index 30123ee5..89d5f494 100644 --- a/src/com/android/tv/data/epg/EpgFetchHelper.java +++ b/src/com/android/tv/data/epg/EpgFetchHelper.java @@ -27,15 +27,13 @@ import android.preference.PreferenceManager; import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; -import com.android.tv.common.util.Clock; import com.android.tv.data.Program; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; -/** The helper class for {@link EpgFetcher} */ -@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed +/** The helper class for {@link com.android.tv.data.epg.EpgFetcher} */ class EpgFetchHelper { private static final String TAG = "EpgFetchHelper"; private static final boolean DEBUG = false; @@ -66,14 +64,13 @@ class EpgFetchHelper { * @param fetchedPrograms the newly fetched program data. * @return {@code true} if new program data are successfully updated. Otherwise {@code false}. */ - static boolean updateEpgData( - Context context, Clock clock, long channelId, List<Program> fetchedPrograms) { + static boolean updateEpgData(Context context, long channelId, List<Program> fetchedPrograms) { final int fetchedProgramsCount = fetchedPrograms.size(); if (fetchedProgramsCount == 0) { return false; } boolean updated = false; - long startTimeMs = clock.currentTimeMillis(); + long startTimeMs = System.currentTimeMillis(); long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION_MS; List<Program> oldPrograms = queryPrograms(context, channelId, startTimeMs, endTimeMs); int oldProgramsIndex = 0; diff --git a/src/com/android/tv/data/epg/EpgFetchService.java b/src/com/android/tv/data/epg/EpgFetchService.java deleted file mode 100644 index aa4f3588..00000000 --- a/src/com/android/tv/data/epg/EpgFetchService.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2017 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.data.epg; - -import android.app.job.JobParameters; -import android.app.job.JobService; -import com.android.tv.Starter; -import com.android.tv.TvSingletons; -import com.android.tv.data.ChannelDataManager; - -/** JobService to Fetch EPG data. */ -public class EpgFetchService extends JobService { - private EpgFetcher mEpgFetcher; - private ChannelDataManager mChannelDataManager; - - @Override - public void onCreate() { - super.onCreate(); - Starter.start(this); - TvSingletons tvSingletons = TvSingletons.getSingletons(getApplicationContext()); - mEpgFetcher = tvSingletons.getEpgFetcher(); - mChannelDataManager = tvSingletons.getChannelDataManager(); - } - - @Override - public boolean onStartJob(JobParameters params) { - if (!mChannelDataManager.isDbLoadFinished()) { - mChannelDataManager.addListener( - new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - mChannelDataManager.removeListener(this); - if (!mEpgFetcher.executeFetchTaskIfPossible( - EpgFetchService.this, params)) { - jobFinished(params, false); - } - } - - @Override - public void onChannelListUpdated() {} - - @Override - public void onChannelBrowsableChanged() {} - }); - return true; - } else { - return mEpgFetcher.executeFetchTaskIfPossible(this, params); - } - } - - @Override - public boolean onStopJob(JobParameters params) { - mEpgFetcher.stopFetchingJob(); - return false; - } -} diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java index 9c24613d..b10bdc1b 100644 --- a/src/com/android/tv/data/epg/EpgFetcher.java +++ b/src/com/android/tv/data/epg/EpgFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2016 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,13 +16,138 @@ package com.android.tv.data.epg; +import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.media.tv.TvInputInfo; +import android.net.TrafficStats; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.AnyThread; import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.ApplicationSingletons; +import com.android.tv.Features; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.TvCommonUtils; +import com.android.tv.config.RemoteConfigUtils; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.ChannelLogoFetcher; +import com.android.tv.data.Lineup; +import com.android.tv.data.Program; +import com.android.tv.perf.EventNames; +import com.android.tv.perf.PerformanceMonitor; +import com.android.tv.perf.TimerEvent; +import com.android.tv.tuner.util.PostalCodeUtils; +import com.android.tv.util.LocationUtils; +import com.android.tv.util.NetworkTrafficTags; +import com.android.tv.util.Utils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; -/** Fetch EPG routinely or on-demand during channel scanning */ -public interface EpgFetcher { +/** + * The service class to fetch EPG routinely or on-demand during channel scanning + * + * <p>Since the default executor of {@link AsyncTask} is {@link AsyncTask#SERIAL_EXECUTOR}, only one + * task can run at a time. Because fetching EPG takes long time, the fetching task shouldn't run on + * the serial executor. Instead, it should run on the {@link AsyncTask#THREAD_POOL_EXECUTOR}. + */ +public class EpgFetcher { + private static final String TAG = "EpgFetcher"; + private static final boolean DEBUG = false; + + private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101; + + private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10); + + private static final int REASON_EPG_READER_NOT_READY = 1; + private static final int REASON_LOCATION_INFO_UNAVAILABLE = 2; + private static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3; + private static final int REASON_NO_EPG_DATA_RETURNED = 4; + private static final int REASON_NO_NEW_EPG = 5; + + private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10); + + private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3); + private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2); + + private static final int DEFAULT_ROUTINE_INTERVAL_HOUR = 4; + private static final String KEY_ROUTINE_INTERVAL = "live_channels_epg_fetcher_interval_hour"; + + private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1; + private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2; + private static final int MSG_FINISH_FETCH_DURING_SCAN = 3; + private static final int MSG_RETRY_PREPARE_FETCH_DURING_SCAN = 4; + + private static final int QUERY_CHANNEL_COUNT = 50; + private static final int MINIMUM_CHANNELS_TO_DECIDE_LINEUP = 3; + + private static EpgFetcher sInstance; + + private final Context mContext; + private final ChannelDataManager mChannelDataManager; + private final EpgReader mEpgReader; + private final PerformanceMonitor mPerformanceMonitor; + private FetchAsyncTask mFetchTask; + private FetchDuringScanHandler mFetchDuringScanHandler; + private long mEpgTimeStamp; + private List<Lineup> mPossibleLineups; + private final Object mPossibleLineupsLock = new Object(); + private final Object mFetchDuringScanHandlerLock = new Object(); + // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished. + private boolean mScanStarted; + + private final long mRoutineIntervalMs; + private final long mEpgDataExpiredTimeLimitMs; + private final long mFastFetchDurationSec; + + public static EpgFetcher getInstance(Context context) { + if (sInstance == null) { + sInstance = new EpgFetcher(context); + } + return sInstance; + } + + /** Creates and returns {@link EpgReader}. */ + public static EpgReader createEpgReader(Context context, String region) { + return new StubEpgReader(context); + } + + private EpgFetcher(Context context) { + mContext = context.getApplicationContext(); + ApplicationSingletons applicationSingletons = TvApplication.getSingletons(mContext); + mChannelDataManager = applicationSingletons.getChannelDataManager(); + mPerformanceMonitor = applicationSingletons.getPerformanceMonitor(); + mEpgReader = createEpgReader(mContext, LocationUtils.getCurrentCountry(mContext)); + + int remoteInteval = + (int) + RemoteConfigUtils.getRemoteConfig( + context, KEY_ROUTINE_INTERVAL, DEFAULT_ROUTINE_INTERVAL_HOUR); + mRoutineIntervalMs = + remoteInteval < 0 + ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR) + : TimeUnit.HOURS.toMillis(remoteInteval); + mEpgDataExpiredTimeLimitMs = mRoutineIntervalMs * 2; + mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + mRoutineIntervalMs / 1000; + } /** * Starts the routine service of EPG fetching. It use {@link JobScheduler} to schedule the EPG @@ -30,30 +155,590 @@ public interface EpgFetcher { * channel scanning of tuner input is started. */ @MainThread - void startRoutineService(); + public void startRoutineService() { + JobScheduler jobScheduler = + (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE); + for (JobInfo job : jobScheduler.getAllPendingJobs()) { + if (job.getId() == EPG_ROUTINELY_FETCHING_JOB_ID) { + return; + } + } + JobInfo job = + new JobInfo.Builder( + EPG_ROUTINELY_FETCHING_JOB_ID, + new ComponentName(mContext, EpgFetchService.class)) + .setPeriodic(mRoutineIntervalMs) + .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL) + .setPersisted(true) + .build(); + jobScheduler.schedule(job); + Log.i(TAG, "EPG fetching routine service started."); + } /** * Fetches EPG immediately if current EPG data are out-dated, i.e., not successfully updated by * routine fetching service due to various reasons. */ @MainThread - void fetchImmediatelyIfNeeded(); + public void fetchImmediatelyIfNeeded() { + if (TvCommonUtils.isRunningInTest()) { + // Do not run EpgFetcher in test. + return; + } + new AsyncTask<Void, Void, Long>() { + @Override + protected Long doInBackground(Void... args) { + return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext); + } + + @Override + protected void onPostExecute(Long result) { + if (System.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) + > mEpgDataExpiredTimeLimitMs) { + Log.i(TAG, "EPG data expired. Start fetching immediately."); + fetchImmediately(); + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } /** Fetches EPG immediately. */ @MainThread - void fetchImmediately(); + public void fetchImmediately() { + if (!mChannelDataManager.isDbLoadFinished()) { + mChannelDataManager.addListener( + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + mChannelDataManager.removeListener(this); + executeFetchTaskIfPossible(null, null); + } + + @Override + public void onChannelListUpdated() {} + + @Override + public void onChannelBrowsableChanged() {} + }); + } else { + executeFetchTaskIfPossible(null, null); + } + } /** Notifies EPG fetch service that channel scanning is started. */ @MainThread - void onChannelScanStarted(); + public void onChannelScanStarted() { + if (mScanStarted || !Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) { + return; + } + mScanStarted = true; + stopFetchingJob(); + synchronized (mFetchDuringScanHandlerLock) { + if (mFetchDuringScanHandler == null) { + HandlerThread thread = new HandlerThread("EpgFetchDuringScan"); + thread.start(); + mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper()); + } + mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN); + } + Log.i(TAG, "EPG fetching on channel scanning started."); + } /** Notifies EPG fetch service that channel scanning is finished. */ @MainThread - void onChannelScanFinished(); + public void onChannelScanFinished() { + if (!mScanStarted) { + return; + } + mScanStarted = false; + mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN); + } @MainThread - boolean executeFetchTaskIfPossible(JobService jobService, JobParameters params); + private void stopFetchingJob() { + if (DEBUG) Log.d(TAG, "Try to stop routinely fetching job..."); + if (mFetchTask != null) { + mFetchTask.cancel(true); + mFetchTask = null; + Log.i(TAG, "EPG routinely fetching job stopped."); + } + } @MainThread - void stopFetchingJob(); + private boolean executeFetchTaskIfPossible(JobService service, JobParameters params) { + SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished()); + if (!TvCommonUtils.isRunningInTest() && checkFetchPrerequisite()) { + mFetchTask = new FetchAsyncTask(service, params); + mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return true; + } + return false; + } + + @MainThread + private boolean checkFetchPrerequisite() { + if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job."); + if (!Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) { + Log.i( + TAG, + "Cannot start routine service: country not supported: " + + LocationUtils.getCurrentCountry(mContext)); + return false; + } + if (mFetchTask != null) { + // Fetching job is already running or ready to run, no need to start again. + return false; + } + if (mFetchDuringScanHandler != null) { + if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels."); + return false; + } + if (getTunerChannelCount() == 0) { + if (DEBUG) Log.d(TAG, "Cannot start routine service: no internal tuner channels."); + return false; + } + if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) { + return true; + } + if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + return true; + } + return true; + } + + @MainThread + private int getTunerChannelCount() { + for (TvInputInfo input : + TvApplication.getSingletons(mContext) + .getTvInputManagerHelper() + .getTvInputInfos(true, true)) { + String inputId = input.getId(); + if (Utils.isInternalTvInput(mContext, inputId)) { + return mChannelDataManager.getChannelCountForInput(inputId); + } + } + return 0; + } + + @AnyThread + private void clearUnusedLineups(@Nullable String lineupId) { + synchronized (mPossibleLineupsLock) { + if (mPossibleLineups == null) { + return; + } + for (Lineup lineup : mPossibleLineups) { + if (!TextUtils.equals(lineupId, lineup.id)) { + mEpgReader.clearCachedChannels(lineup.id); + } + } + mPossibleLineups = null; + } + } + + @WorkerThread + private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) { + if (!mEpgReader.isAvailable()) { + Log.i(TAG, "EPG reader is temporarily unavailable."); + return REASON_EPG_READER_NOT_READY; + } + // Checks the EPG Timestamp. + mEpgTimeStamp = mEpgReader.getEpgTimestamp(); + if (mEpgTimeStamp <= EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)) { + if (DEBUG) Log.d(TAG, "No new EPG."); + return REASON_NO_NEW_EPG; + } + // Updates postal code. + boolean postalCodeChanged = false; + try { + postalCodeChanged = PostalCodeUtils.updatePostalCode(mContext); + } catch (IOException e) { + if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); + if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + return REASON_LOCATION_INFO_UNAVAILABLE; + } + } catch (SecurityException e) { + Log.w(TAG, "No permission to get the current location."); + if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { + return REASON_LOCATION_PERMISSION_NOT_GRANTED; + } + } catch (PostalCodeUtils.NoPostalCodeException e) { + Log.i(TAG, "Cannot get address or postal code."); + return REASON_LOCATION_INFO_UNAVAILABLE; + } + // Updates possible lineups if necessary. + SoftPreconditions.checkState(mPossibleLineups == null, TAG, "Possible lineups not reset."); + if (postalCodeChanged + || forceUpdatePossibleLineups + || EpgFetchHelper.getLastLineupId(mContext) == null) { + // To prevent main thread being blocked, though theoretically it should not happen. + List<Lineup> possibleLineups = + mEpgReader.getLineups(PostalCodeUtils.getLastPostalCode(mContext)); + if (possibleLineups.isEmpty()) { + return REASON_NO_EPG_DATA_RETURNED; + } + for (Lineup lineup : possibleLineups) { + mEpgReader.preloadChannels(lineup.id); + } + synchronized (mPossibleLineupsLock) { + mPossibleLineups = possibleLineups; + } + EpgFetchHelper.setLastLineupId(mContext, null); + } + return null; + } + + @WorkerThread + private void batchFetchEpg(List<Channel> channels, long durationSec) { + Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + channels.size()); + if (channels.size() == 0) { + return; + } + List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT); + for (Channel channel : channels) { + queryChannelIds.add(channel.getId()); + if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) { + batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec)); + queryChannelIds.clear(); + } + } + if (!queryChannelIds.isEmpty()) { + batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec)); + } + } + + @WorkerThread + private void batchUpdateEpg(Map<Long, List<Program>> allPrograms) { + for (Map.Entry<Long, List<Program>> entry : allPrograms.entrySet()) { + List<Program> programs = entry.getValue(); + if (programs == null) { + continue; + } + Collections.sort(programs); + Log.i( + TAG, + "Batch fetched " + programs.size() + " programs for channel " + entry.getKey()); + EpgFetchHelper.updateEpgData(mContext, entry.getKey(), programs); + } + } + + @Nullable + @WorkerThread + private String pickBestLineupId(List<Channel> currentChannelList) { + String maxLineupId = null; + synchronized (mPossibleLineupsLock) { + if (mPossibleLineups == null) { + return null; + } + int maxCount = 0; + for (Lineup lineup : mPossibleLineups) { + int count = getMatchedChannelCount(lineup.id, currentChannelList); + Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches"); + if (count > maxCount) { + maxCount = count; + maxLineupId = lineup.id; + } + } + } + return maxLineupId; + } + + @WorkerThread + private int getMatchedChannelCount(String lineupId, List<Channel> currentChannelList) { + // Construct a list of display numbers for existing channels. + if (currentChannelList.isEmpty()) { + if (DEBUG) Log.d(TAG, "No existing channel to compare"); + return 0; + } + List<String> numbers = new ArrayList<>(currentChannelList.size()); + for (Channel channel : currentChannelList) { + // We only support channels from internal tuner inputs. + if (Utils.isInternalTvInput(mContext, channel.getInputId())) { + numbers.add(channel.getDisplayNumber()); + } + } + numbers.retainAll(mEpgReader.getChannelNumbers(lineupId)); + return numbers.size(); + } + + public static class EpgFetchService extends JobService { + private EpgFetcher mEpgFetcher; + + @Override + public void onCreate() { + super.onCreate(); + TvApplication.setCurrentRunningProcess(this, true); + mEpgFetcher = EpgFetcher.getInstance(this); + } + + @Override + public boolean onStartJob(JobParameters params) { + if (!mEpgFetcher.mChannelDataManager.isDbLoadFinished()) { + mEpgFetcher.mChannelDataManager.addListener( + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + mEpgFetcher.mChannelDataManager.removeListener(this); + if (!mEpgFetcher.executeFetchTaskIfPossible( + EpgFetchService.this, params)) { + jobFinished(params, false); + } + } + + @Override + public void onChannelListUpdated() {} + + @Override + public void onChannelBrowsableChanged() {} + }); + return true; + } else { + return mEpgFetcher.executeFetchTaskIfPossible(this, params); + } + } + + @Override + public boolean onStopJob(JobParameters params) { + mEpgFetcher.stopFetchingJob(); + return false; + } + } + + private class FetchAsyncTask extends AsyncTask<Void, Void, Integer> { + private final JobService mService; + private final JobParameters mParams; + private List<Channel> mCurrentChannelList; + private TimerEvent mTimerEvent; + + private FetchAsyncTask(JobService service, JobParameters params) { + mService = service; + mParams = params; + } + + @Override + protected void onPreExecute() { + mTimerEvent = mPerformanceMonitor.startTimer(); + mCurrentChannelList = mChannelDataManager.getChannelList(); + } + + @Override + protected Integer doInBackground(Void... args) { + final int oldTag = TrafficStats.getThreadStatsTag(); + TrafficStats.setThreadStatsTag(NetworkTrafficTags.EPG_FETCH); + try { + if (DEBUG) Log.d(TAG, "Start EPG routinely fetching."); + Integer failureReason = prepareFetchEpg(false); + // InterruptedException might be caught by RPC, we should check it here. + if (failureReason != null || this.isCancelled()) { + return failureReason; + } + String lineupId = EpgFetchHelper.getLastLineupId(mContext); + lineupId = lineupId == null ? pickBestLineupId(mCurrentChannelList) : lineupId; + if (lineupId != null) { + Log.i(TAG, "Selecting the lineup " + lineupId); + // During normal fetching process, the lineup ID should be confirmed since all + // channels are known, clear up possible lineups to save resources. + EpgFetchHelper.setLastLineupId(mContext, lineupId); + clearUnusedLineups(lineupId); + } else { + Log.i(TAG, "Failed to get lineup id"); + return REASON_NO_EPG_DATA_RETURNED; + } + final List<Channel> channels = mEpgReader.getChannels(lineupId); + // InterruptedException might be caught by RPC, we should check it here. + if (this.isCancelled()) { + return null; + } + if (channels.isEmpty()) { + Log.i(TAG, "Failed to get EPG channels."); + return REASON_NO_EPG_DATA_RETURNED; + } + if (System.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) + > mEpgDataExpiredTimeLimitMs) { + batchFetchEpg(channels, mFastFetchDurationSec); + } + new Handler(mContext.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + ChannelLogoFetcher.startFetchingChannelLogos( + mContext, channels); + } + }); + for (Channel channel : channels) { + if (this.isCancelled()) { + return null; + } + long channelId = channel.getId(); + List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channelId)); + // InterruptedException might be caught by RPC, we should check it here. + Collections.sort(programs); + Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channelId); + EpgFetchHelper.updateEpgData(mContext, channelId, programs); + } + EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp); + if (DEBUG) Log.d(TAG, "Fetching EPG is finished."); + return null; + } finally { + TrafficStats.setThreadStatsTag(oldTag); + } + } + + @Override + protected void onPostExecute(Integer failureReason) { + mFetchTask = null; + if (failureReason == null + || failureReason == REASON_LOCATION_PERMISSION_NOT_GRANTED + || failureReason == REASON_NO_NEW_EPG) { + jobFinished(false); + } else { + // Applies back-off policy + jobFinished(true); + } + mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK); + mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK); + } + + @Override + protected void onCancelled(Integer failureReason) { + clearUnusedLineups(null); + jobFinished(false); + } + + private void jobFinished(boolean reschedule) { + if (mService != null && mParams != null) { + // Task is executed from JobService, need to report jobFinished. + mService.jobFinished(mParams, reschedule); + } + } + } + + @WorkerThread + private class FetchDuringScanHandler extends Handler { + private final Set<Long> mFetchedChannelIdsDuringScan = new HashSet<>(); + private String mPossibleLineupId; + + private final ChannelDataManager.Listener mDuringScanChannelListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()"); + if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP + && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { + Message.obtain( + FetchDuringScanHandler.this, + MSG_CHANNEL_UPDATED_DURING_SCAN, + new ArrayList<>(mChannelDataManager.getChannelList())) + .sendToTarget(); + } + } + + @Override + public void onChannelListUpdated() { + if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()"); + if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP + && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { + Message.obtain( + FetchDuringScanHandler.this, + MSG_CHANNEL_UPDATED_DURING_SCAN, + mChannelDataManager.getChannelList()) + .sendToTarget(); + } + } + + @Override + public void onChannelBrowsableChanged() { + // Do nothing + } + }; + + @AnyThread + private FetchDuringScanHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PREPARE_FETCH_DURING_SCAN: + case MSG_RETRY_PREPARE_FETCH_DURING_SCAN: + onPrepareFetchDuringScan(); + break; + case MSG_CHANNEL_UPDATED_DURING_SCAN: + if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { + onChannelUpdatedDuringScan((List<Channel>) msg.obj); + } + break; + case MSG_FINISH_FETCH_DURING_SCAN: + removeMessages(MSG_RETRY_PREPARE_FETCH_DURING_SCAN); + if (hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { + sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN); + } else { + onFinishFetchDuringScan(); + } + break; + } + } + + private void onPrepareFetchDuringScan() { + Integer failureReason = prepareFetchEpg(true); + if (failureReason != null) { + sendEmptyMessageDelayed( + MSG_RETRY_PREPARE_FETCH_DURING_SCAN, FETCH_DURING_SCAN_WAIT_TIME_MS); + return; + } + mChannelDataManager.addListener(mDuringScanChannelListener); + } + + private void onChannelUpdatedDuringScan(List<Channel> currentChannelList) { + String lineupId = pickBestLineupId(currentChannelList); + Log.i(TAG, "Fast fetch channels for lineup ID: " + lineupId); + if (TextUtils.isEmpty(lineupId)) { + if (TextUtils.isEmpty(mPossibleLineupId)) { + return; + } + } else if (!TextUtils.equals(lineupId, mPossibleLineupId)) { + mFetchedChannelIdsDuringScan.clear(); + mPossibleLineupId = lineupId; + } + List<Long> currentChannelIds = new ArrayList<>(); + for (Channel channel : currentChannelList) { + currentChannelIds.add(channel.getId()); + } + mFetchedChannelIdsDuringScan.retainAll(currentChannelIds); + List<Channel> newChannels = new ArrayList<>(); + for (Channel channel : mEpgReader.getChannels(mPossibleLineupId)) { + if (!mFetchedChannelIdsDuringScan.contains(channel.getId())) { + newChannels.add(channel); + mFetchedChannelIdsDuringScan.add(channel.getId()); + } + } + batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC); + } + + private void onFinishFetchDuringScan() { + mChannelDataManager.removeListener(mDuringScanChannelListener); + EpgFetchHelper.setLastLineupId(mContext, mPossibleLineupId); + clearUnusedLineups(null); + mFetchedChannelIdsDuringScan.clear(); + synchronized (mFetchDuringScanHandlerLock) { + if (!hasMessages(MSG_PREPARE_FETCH_DURING_SCAN)) { + removeCallbacksAndMessages(null); + getLooper().quit(); + mFetchDuringScanHandler = null; + } + } + // Clear timestamp to make routine service start right away. + EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0); + Log.i(TAG, "EPG Fetching during channel scanning finished."); + new Handler(Looper.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + fetchImmediately(); + } + }); + } + } } diff --git a/src/com/android/tv/data/epg/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java deleted file mode 100644 index 523fc50c..00000000 --- a/src/com/android/tv/data/epg/EpgFetcherImpl.java +++ /dev/null @@ -1,814 +0,0 @@ -/* - * Copyright (C) 2016 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.data.epg; - -import android.app.job.JobInfo; -import android.app.job.JobParameters; -import android.app.job.JobScheduler; -import android.app.job.JobService; -import android.content.ComponentName; -import android.content.Context; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.media.tv.TvInputInfo; -import android.net.TrafficStats; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.support.annotation.AnyThread; -import android.support.annotation.MainThread; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; -import android.text.TextUtils; -import android.util.Log; -import com.android.tv.TvFeatures; -import com.android.tv.TvSingletons; -import com.android.tv.common.BuildConfig; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.config.RemoteConfigUtils; -import com.android.tv.common.util.Clock; -import com.android.tv.common.util.CommonUtils; -import com.android.tv.common.util.LocationUtils; -import com.android.tv.common.util.NetworkTrafficTags; -import com.android.tv.common.util.PermissionUtils; -import com.android.tv.common.util.PostalCodeUtils; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.data.ChannelLogoFetcher; -import com.android.tv.data.Lineup; -import com.android.tv.data.Program; - - -import com.android.tv.perf.EventNames; -import com.android.tv.perf.PerformanceMonitor; -import com.android.tv.perf.TimerEvent; -import com.android.tv.util.Utils; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * The service class to fetch EPG routinely or on-demand during channel scanning - * - * <p>Since the default executor of {@link AsyncTask} is {@link AsyncTask#SERIAL_EXECUTOR}, only one - * task can run at a time. Because fetching EPG takes long time, the fetching task shouldn't run on - * the serial executor. Instead, it should run on the {@link AsyncTask#THREAD_POOL_EXECUTOR}. - */ -@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed -public class EpgFetcherImpl implements EpgFetcher { - private static final String TAG = "EpgFetcherImpl"; - private static final boolean DEBUG = false; - - private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101; - - private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10); - - @VisibleForTesting static final int REASON_EPG_READER_NOT_READY = 1; - @VisibleForTesting static final int REASON_LOCATION_INFO_UNAVAILABLE = 2; - @VisibleForTesting static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3; - @VisibleForTesting static final int REASON_NO_EPG_DATA_RETURNED = 4; - @VisibleForTesting static final int REASON_NO_NEW_EPG = 5; - @VisibleForTesting static final int REASON_ERROR = 6; - @VisibleForTesting static final int REASON_CLOUD_EPG_FAILURE = 7; - @VisibleForTesting static final int REASON_NO_BUILT_IN_CHANNELS = 8; - - private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10); - - private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3); - private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2); - - private static final int DEFAULT_ROUTINE_INTERVAL_HOUR = 4; - private static final String KEY_ROUTINE_INTERVAL = "live_channels_epg_fetcher_interval_hour"; - - private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1; - private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2; - private static final int MSG_FINISH_FETCH_DURING_SCAN = 3; - private static final int MSG_RETRY_PREPARE_FETCH_DURING_SCAN = 4; - - private static final int QUERY_CHANNEL_COUNT = 50; - private static final int MINIMUM_CHANNELS_TO_DECIDE_LINEUP = 3; - - private final Context mContext; - private final ChannelDataManager mChannelDataManager; - private final EpgReader mEpgReader; - private final PerformanceMonitor mPerformanceMonitor; - private FetchAsyncTask mFetchTask; - private FetchDuringScanHandler mFetchDuringScanHandler; - private long mEpgTimeStamp; - private List<Lineup> mPossibleLineups; - private final Object mPossibleLineupsLock = new Object(); - private final Object mFetchDuringScanHandlerLock = new Object(); - // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished. - private boolean mScanStarted; - - private final long mRoutineIntervalMs; - private final long mEpgDataExpiredTimeLimitMs; - private final long mFastFetchDurationSec; - private Clock mClock; - - public static EpgFetcher create(Context context) { - context = context.getApplicationContext(); - TvSingletons tvSingletons = TvSingletons.getSingletons(context); - ChannelDataManager channelDataManager = tvSingletons.getChannelDataManager(); - PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor(); - EpgReader epgReader = tvSingletons.providesEpgReader().get(); - Clock clock = tvSingletons.getClock(); - int routineIntervalMs = - (int) - RemoteConfigUtils.getRemoteConfig( - context, KEY_ROUTINE_INTERVAL, DEFAULT_ROUTINE_INTERVAL_HOUR); - - return new EpgFetcherImpl( - context, - channelDataManager, - epgReader, - performanceMonitor, - clock, - routineIntervalMs); - } - - @VisibleForTesting - EpgFetcherImpl( - Context context, - ChannelDataManager channelDataManager, - EpgReader epgReader, - PerformanceMonitor performanceMonitor, - Clock clock, - long routineIntervalMs) { - mContext = context; - mChannelDataManager = channelDataManager; - mEpgReader = epgReader; - mPerformanceMonitor = performanceMonitor; - mClock = clock; - mRoutineIntervalMs = - routineIntervalMs <= 0 - ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR) - : TimeUnit.HOURS.toMillis(routineIntervalMs); - mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2; - mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000; - } - - private static Set<Channel> getExistingChannelsForMyPackage(Context context) { - HashSet<Channel> channels = new HashSet<>(); - String selection = null; - String[] selectionArgs = null; - String myPackageName = context.getPackageName(); - if (PermissionUtils.hasAccessAllEpg(context)) { - selection = "package_name=?"; - selectionArgs = new String[] {myPackageName}; - } - try (Cursor c = - context.getContentResolver() - .query( - TvContract.Channels.CONTENT_URI, - Channel.PROJECTION, - selection, - selectionArgs, - null)) { - if (c != null) { - while (c.moveToNext()) { - Channel channel = Channel.fromCursor(c); - if (DEBUG) Log.d(TAG, "Found " + channel); - if (myPackageName.equals(channel.getPackageName())) { - channels.add(channel); - } - } - } - } - if (DEBUG) - Log.d(TAG, "Found " + channels.size() + " channels for package " + myPackageName); - return channels; - } - - @Override - @MainThread - public void startRoutineService() { - JobScheduler jobScheduler = - (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE); - for (JobInfo job : jobScheduler.getAllPendingJobs()) { - if (job.getId() == EPG_ROUTINELY_FETCHING_JOB_ID) { - return; - } - } - JobInfo job = - new JobInfo.Builder( - EPG_ROUTINELY_FETCHING_JOB_ID, - new ComponentName(mContext, EpgFetchService.class)) - .setPeriodic(mRoutineIntervalMs) - .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL) - .setPersisted(true) - .build(); - jobScheduler.schedule(job); - Log.i(TAG, "EPG fetching routine service started."); - } - - @Override - @MainThread - public void fetchImmediatelyIfNeeded() { - if (CommonUtils.isRunningInTest()) { - // Do not run EpgFetcher in test. - return; - } - new AsyncTask<Void, Void, Long>() { - @Override - protected Long doInBackground(Void... args) { - return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext); - } - - @Override - protected void onPostExecute(Long result) { - if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) - > mEpgDataExpiredTimeLimitMs) { - Log.i(TAG, "EPG data expired. Start fetching immediately."); - fetchImmediately(); - } - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - @Override - @MainThread - public void fetchImmediately() { - if (DEBUG) Log.d(TAG, "fetchImmediately"); - if (!mChannelDataManager.isDbLoadFinished()) { - mChannelDataManager.addListener( - new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - mChannelDataManager.removeListener(this); - executeFetchTaskIfPossible(null, null); - } - - @Override - public void onChannelListUpdated() {} - - @Override - public void onChannelBrowsableChanged() {} - }); - } else { - executeFetchTaskIfPossible(null, null); - } - } - - @Override - @MainThread - public void onChannelScanStarted() { - if (mScanStarted || !TvFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) { - return; - } - mScanStarted = true; - stopFetchingJob(); - synchronized (mFetchDuringScanHandlerLock) { - if (mFetchDuringScanHandler == null) { - HandlerThread thread = new HandlerThread("EpgFetchDuringScan"); - thread.start(); - mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper()); - } - mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN); - } - Log.i(TAG, "EPG fetching on channel scanning started."); - } - - @Override - @MainThread - public void onChannelScanFinished() { - if (!mScanStarted) { - return; - } - mScanStarted = false; - mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN); - } - - @MainThread - @Override - public void stopFetchingJob() { - if (DEBUG) Log.d(TAG, "Try to stop routinely fetching job..."); - if (mFetchTask != null) { - mFetchTask.cancel(true); - mFetchTask = null; - Log.i(TAG, "EPG routinely fetching job stopped."); - } - } - - @MainThread - @Override - public boolean executeFetchTaskIfPossible(JobService service, JobParameters params) { - if (DEBUG) Log.d(TAG, "executeFetchTaskIfPossible"); - SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished()); - if (!CommonUtils.isRunningInTest() && checkFetchPrerequisite()) { - mFetchTask = createFetchTask(service, params); - mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - return true; - } - return false; - } - - @VisibleForTesting - FetchAsyncTask createFetchTask(JobService service, JobParameters params) { - return new FetchAsyncTask(service, params); - } - - @MainThread - private boolean checkFetchPrerequisite() { - if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job."); - if (!TvFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) { - Log.i( - TAG, - "Cannot start routine service: country not supported: " - + LocationUtils.getCurrentCountry(mContext)); - return false; - } - if (mFetchTask != null) { - // Fetching job is already running or ready to run, no need to start again. - return false; - } - if (mFetchDuringScanHandler != null) { - if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels."); - return false; - } - return true; - } - - @MainThread - private int getTunerChannelCount() { - for (TvInputInfo input : - TvSingletons.getSingletons(mContext) - .getTvInputManagerHelper() - .getTvInputInfos(true, true)) { - String inputId = input.getId(); - if (Utils.isInternalTvInput(mContext, inputId)) { - return mChannelDataManager.getChannelCountForInput(inputId); - } - } - return 0; - } - - @AnyThread - private void clearUnusedLineups(@Nullable String lineupId) { - synchronized (mPossibleLineupsLock) { - if (mPossibleLineups == null) { - return; - } - for (Lineup lineup : mPossibleLineups) { - if (!TextUtils.equals(lineupId, lineup.getId())) { - mEpgReader.clearCachedChannels(lineup.getId()); - } - } - mPossibleLineups = null; - } - } - - @WorkerThread - private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) { - if (!mEpgReader.isAvailable()) { - Log.i(TAG, "EPG reader is temporarily unavailable."); - return REASON_EPG_READER_NOT_READY; - } - // Checks the EPG Timestamp. - mEpgTimeStamp = mEpgReader.getEpgTimestamp(); - if (mEpgTimeStamp <= EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)) { - if (DEBUG) Log.d(TAG, "No new EPG."); - return REASON_NO_NEW_EPG; - } - // Updates postal code. - boolean postalCodeChanged = false; - try { - postalCodeChanged = PostalCodeUtils.updatePostalCode(mContext); - } catch (IOException e) { - if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e); - if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { - return REASON_LOCATION_INFO_UNAVAILABLE; - } - } catch (SecurityException e) { - Log.w(TAG, "No permission to get the current location."); - if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) { - return REASON_LOCATION_PERMISSION_NOT_GRANTED; - } - } catch (PostalCodeUtils.NoPostalCodeException e) { - Log.i(TAG, "Cannot get address or postal code."); - return REASON_LOCATION_INFO_UNAVAILABLE; - } - // Updates possible lineups if necessary. - SoftPreconditions.checkState(mPossibleLineups == null, TAG, "Possible lineups not reset."); - if (postalCodeChanged - || forceUpdatePossibleLineups - || EpgFetchHelper.getLastLineupId(mContext) == null) { - // To prevent main thread being blocked, though theoretically it should not happen. - String lastPostalCode = PostalCodeUtils.getLastPostalCode(mContext); - List<Lineup> possibleLineups = mEpgReader.getLineups(lastPostalCode); - if (possibleLineups.isEmpty()) { - Log.i(TAG, "No lineups found for " + lastPostalCode); - return REASON_NO_EPG_DATA_RETURNED; - } - for (Lineup lineup : possibleLineups) { - mEpgReader.preloadChannels(lineup.getId()); - } - synchronized (mPossibleLineupsLock) { - mPossibleLineups = possibleLineups; - } - EpgFetchHelper.setLastLineupId(mContext, null); - } - return null; - } - - @WorkerThread - private void batchFetchEpg(Set<EpgReader.EpgChannel> epgChannels, long durationSec) { - Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + epgChannels.size()); - if (epgChannels.size() == 0) { - return; - } - Set<EpgReader.EpgChannel> batch = new HashSet<>(QUERY_CHANNEL_COUNT); - for (EpgReader.EpgChannel epgChannel : epgChannels) { - batch.add(epgChannel); - if (batch.size() >= QUERY_CHANNEL_COUNT) { - batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec)); - batch.clear(); - } - } - if (!batch.isEmpty()) { - batchUpdateEpg(mEpgReader.getPrograms(batch, durationSec)); - } - } - - @WorkerThread - private void batchUpdateEpg(Map<EpgReader.EpgChannel, Collection<Program>> allPrograms) { - for (Map.Entry<EpgReader.EpgChannel, Collection<Program>> entry : allPrograms.entrySet()) { - List<Program> programs = new ArrayList(entry.getValue()); - if (programs == null) { - continue; - } - Collections.sort(programs); - Log.i( - TAG, - "Batch fetched " + programs.size() + " programs for channel " + entry.getKey()); - EpgFetchHelper.updateEpgData( - mContext, mClock, entry.getKey().getChannel().getId(), programs); - } - } - - @Nullable - @WorkerThread - private String pickBestLineupId(Set<Channel> currentChannels) { - String maxLineupId = null; - synchronized (mPossibleLineupsLock) { - if (mPossibleLineups == null) { - return null; - } - int maxCount = 0; - for (Lineup lineup : mPossibleLineups) { - int count = getMatchedChannelCount(lineup.getId(), currentChannels); - Log.i(TAG, lineup.getName() + " (" + lineup.getId() + ") - " + count + " matches"); - if (count > maxCount) { - maxCount = count; - maxLineupId = lineup.getId(); - } - } - } - return maxLineupId; - } - - @WorkerThread - private int getMatchedChannelCount(String lineupId, Set<Channel> currentChannels) { - // Construct a list of display numbers for existing channels. - if (currentChannels.isEmpty()) { - if (DEBUG) Log.d(TAG, "No existing channel to compare"); - return 0; - } - List<String> numbers = new ArrayList<>(currentChannels.size()); - for (Channel channel : currentChannels) { - // We only support channels from internal tuner inputs. - if (Utils.isInternalTvInput(mContext, channel.getInputId())) { - numbers.add(channel.getDisplayNumber()); - } - } - numbers.retainAll(mEpgReader.getChannelNumbers(lineupId)); - return numbers.size(); - } - - @VisibleForTesting - class FetchAsyncTask extends AsyncTask<Void, Void, Integer> { - private final JobService mService; - private final JobParameters mParams; - private Set<Channel> mCurrentChannels; - private TimerEvent mTimerEvent; - - private FetchAsyncTask(JobService service, JobParameters params) { - mService = service; - mParams = params; - } - - @Override - protected void onPreExecute() { - mTimerEvent = mPerformanceMonitor.startTimer(); - mCurrentChannels = new HashSet<>(mChannelDataManager.getChannelList()); - } - - @Override - protected Integer doInBackground(Void... args) { - final int oldTag = TrafficStats.getThreadStatsTag(); - TrafficStats.setThreadStatsTag(NetworkTrafficTags.EPG_FETCH); - try { - if (DEBUG) Log.d(TAG, "Start EPG routinely fetching."); - Integer builtInResult = fetchEpgForBuiltInTuner(); - boolean anyCloudEpgFailure = false; - boolean anyCloudEpgSuccess = false; - return builtInResult; - } finally { - TrafficStats.setThreadStatsTag(oldTag); - } - } - - private Set<Channel> getExistingChannelsFor(String inputId) { - Set<Channel> result = new HashSet<>(); - try (Cursor cursor = - mContext.getContentResolver() - .query( - TvContract.buildChannelsUriForInput(inputId), - Channel.PROJECTION, - null, - null, - null)) { - while (cursor.moveToNext()) { - result.add(Channel.fromCursor(cursor)); - } - return result; - } - } - - private Integer fetchEpgForBuiltInTuner() { - try { - Integer failureReason = prepareFetchEpg(false); - // InterruptedException might be caught by RPC, we should check it here. - if (failureReason != null || this.isCancelled()) { - return failureReason; - } - String lineupId = EpgFetchHelper.getLastLineupId(mContext); - lineupId = lineupId == null ? pickBestLineupId(mCurrentChannels) : lineupId; - if (lineupId != null) { - Log.i(TAG, "Selecting the lineup " + lineupId); - // During normal fetching process, the lineup ID should be confirmed since all - // channels are known, clear up possible lineups to save resources. - EpgFetchHelper.setLastLineupId(mContext, lineupId); - clearUnusedLineups(lineupId); - } else { - Log.i(TAG, "Failed to get lineup id"); - return REASON_NO_EPG_DATA_RETURNED; - } - Set<Channel> existingChannelsForMyPackage = - getExistingChannelsForMyPackage(mContext); - if (existingChannelsForMyPackage.isEmpty()) { - return REASON_NO_BUILT_IN_CHANNELS; - } - return fetchEpgFor(lineupId, existingChannelsForMyPackage); - } catch (Exception e) { - Log.w(TAG, "Failed to update EPG for builtin tuner", e); - return REASON_ERROR; - } - } - - @Nullable - private Integer fetchEpgFor(String lineupId, Set<Channel> existingChannels) { - if (DEBUG) { - Log.d( - TAG, - "Starting Fetching EPG is for " - + lineupId - + " with channelCount " - + existingChannels.size()); - } - final Set<EpgReader.EpgChannel> channels = - mEpgReader.getChannels(existingChannels, lineupId); - // InterruptedException might be caught by RPC, we should check it here. - if (this.isCancelled()) { - return null; - } - if (channels.isEmpty()) { - Log.i(TAG, "Failed to get EPG channels for " + lineupId); - return REASON_NO_EPG_DATA_RETURNED; - } - if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext) - > mEpgDataExpiredTimeLimitMs) { - batchFetchEpg(channels, mFastFetchDurationSec); - } - new Handler(mContext.getMainLooper()) - .post( - new Runnable() { - @Override - public void run() { - ChannelLogoFetcher.startFetchingChannelLogos( - mContext, asChannelList(channels)); - } - }); - for (EpgReader.EpgChannel epgChannel : channels) { - if (this.isCancelled()) { - return null; - } - List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(epgChannel)); - // InterruptedException might be caught by RPC, we should check it here. - Collections.sort(programs); - Log.i( - TAG, - "Fetched " - + programs.size() - + " programs for channel " - + epgChannel.getChannel()); - EpgFetchHelper.updateEpgData( - mContext, mClock, epgChannel.getChannel().getId(), programs); - } - EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp); - if (DEBUG) Log.d(TAG, "Fetching EPG is for " + lineupId); - return null; - } - - @Override - protected void onPostExecute(Integer failureReason) { - mFetchTask = null; - if (failureReason == null - || failureReason == REASON_LOCATION_PERMISSION_NOT_GRANTED - || failureReason == REASON_NO_NEW_EPG) { - jobFinished(false); - } else { - // Applies back-off policy - jobFinished(true); - } - mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK); - mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK); - } - - @Override - protected void onCancelled(Integer failureReason) { - clearUnusedLineups(null); - jobFinished(false); - } - - private void jobFinished(boolean reschedule) { - if (mService != null && mParams != null) { - // Task is executed from JobService, need to report jobFinished. - mService.jobFinished(mParams, reschedule); - } - } - } - - private List<Channel> asChannelList(Set<EpgReader.EpgChannel> epgChannels) { - List<Channel> result = new ArrayList<>(epgChannels.size()); - for (EpgReader.EpgChannel epgChannel : epgChannels) { - result.add(epgChannel.getChannel()); - } - return result; - } - - @WorkerThread - private class FetchDuringScanHandler extends Handler { - private final Set<Long> mFetchedChannelIdsDuringScan = new HashSet<>(); - private String mPossibleLineupId; - - private final ChannelDataManager.Listener mDuringScanChannelListener = - new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()"); - if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP - && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { - Message.obtain( - FetchDuringScanHandler.this, - MSG_CHANNEL_UPDATED_DURING_SCAN, - getExistingChannelsForMyPackage(mContext)) - .sendToTarget(); - } - } - - @Override - public void onChannelListUpdated() { - if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()"); - if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP - && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { - Message.obtain( - FetchDuringScanHandler.this, - MSG_CHANNEL_UPDATED_DURING_SCAN, - getExistingChannelsForMyPackage(mContext)) - .sendToTarget(); - } - } - - @Override - public void onChannelBrowsableChanged() { - // Do nothing - } - }; - - @AnyThread - private FetchDuringScanHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_PREPARE_FETCH_DURING_SCAN: - case MSG_RETRY_PREPARE_FETCH_DURING_SCAN: - onPrepareFetchDuringScan(); - break; - case MSG_CHANNEL_UPDATED_DURING_SCAN: - if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { - onChannelUpdatedDuringScan((Set<Channel>) msg.obj); - } - break; - case MSG_FINISH_FETCH_DURING_SCAN: - removeMessages(MSG_RETRY_PREPARE_FETCH_DURING_SCAN); - if (hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) { - sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN); - } else { - onFinishFetchDuringScan(); - } - break; - default: - // do nothing - } - } - - private void onPrepareFetchDuringScan() { - Integer failureReason = prepareFetchEpg(true); - if (failureReason != null) { - sendEmptyMessageDelayed( - MSG_RETRY_PREPARE_FETCH_DURING_SCAN, FETCH_DURING_SCAN_WAIT_TIME_MS); - return; - } - mChannelDataManager.addListener(mDuringScanChannelListener); - } - - private void onChannelUpdatedDuringScan(Set<Channel> currentChannels) { - String lineupId = pickBestLineupId(currentChannels); - Log.i(TAG, "Fast fetch channels for lineup ID: " + lineupId); - if (TextUtils.isEmpty(lineupId)) { - if (TextUtils.isEmpty(mPossibleLineupId)) { - return; - } - } else if (!TextUtils.equals(lineupId, mPossibleLineupId)) { - mFetchedChannelIdsDuringScan.clear(); - mPossibleLineupId = lineupId; - } - List<Long> currentChannelIds = new ArrayList<>(); - for (Channel channel : currentChannels) { - currentChannelIds.add(channel.getId()); - } - mFetchedChannelIdsDuringScan.retainAll(currentChannelIds); - Set<EpgReader.EpgChannel> newChannels = new HashSet<>(); - for (EpgReader.EpgChannel epgChannel : - mEpgReader.getChannels(currentChannels, mPossibleLineupId)) { - if (!mFetchedChannelIdsDuringScan.contains(epgChannel.getChannel().getId())) { - newChannels.add(epgChannel); - mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().getId()); - } - } - batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC); - } - - private void onFinishFetchDuringScan() { - mChannelDataManager.removeListener(mDuringScanChannelListener); - EpgFetchHelper.setLastLineupId(mContext, mPossibleLineupId); - clearUnusedLineups(null); - mFetchedChannelIdsDuringScan.clear(); - synchronized (mFetchDuringScanHandlerLock) { - if (!hasMessages(MSG_PREPARE_FETCH_DURING_SCAN)) { - removeCallbacksAndMessages(null); - getLooper().quit(); - mFetchDuringScanHandler = null; - } - } - // Clear timestamp to make routine service start right away. - EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0); - Log.i(TAG, "EPG Fetching during channel scanning finished."); - new Handler(Looper.getMainLooper()) - .post( - new Runnable() { - @Override - public void run() { - fetchImmediately(); - } - }); - } - } -} diff --git a/src/com/android/tv/data/epg/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java deleted file mode 100644 index de0478fc..00000000 --- a/src/com/android/tv/data/epg/EpgInputWhiteList.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2017 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.data.epg; - -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.Log; -import com.android.tv.common.BuildConfig; -import com.android.tv.common.config.api.RemoteConfig; -import com.android.tv.common.experiments.Experiments; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** Checks if a package or a input is white listed. */ -public final class EpgInputWhiteList { - private static final boolean DEBUG = false; - private static final String TAG = "EpgInputWhiteList"; - @VisibleForTesting public static final String KEY = "live_channels_3rd_party_epg_inputs"; - private static final String QA_DEV_INPUTS = - "com.example.partnersupportsampletvinput/.SampleTvInputService"; - - /** Returns the package portion of a inputId */ - @Nullable - public static String getPackageFromInput(@Nullable String inputId) { - return inputId == null ? null : inputId.substring(0, inputId.indexOf("/")); - } - - private final RemoteConfig remoteConfig; - - public EpgInputWhiteList(RemoteConfig remoteConfig) { - this.remoteConfig = remoteConfig; - } - - public boolean isInputWhiteListed(String inputId) { - return getWhiteListedInputs().contains(inputId); - } - - public boolean isPackageWhiteListed(String packageName) { - if (DEBUG) Log.d(TAG, "isPackageWhiteListed " + packageName); - Set<String> whiteList = getWhiteListedInputs(); - for (String good : whiteList) { - try { - String goodPackage = getPackageFromInput(good); - if (goodPackage.equals(packageName)) { - return true; - } - } catch (Exception e) { - if (DEBUG) Log.d(TAG, "Error parsing package name of " + good, e); - continue; - } - } - return false; - } - - private Set<String> getWhiteListedInputs() { - Set<String> result = toInputSet(remoteConfig.getString(KEY)); - if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) { - HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS)); - if (result.isEmpty()) { - result = moreInputs; - } else { - result.addAll(moreInputs); - } - } - if (DEBUG) Log.d(TAG, "getWhiteListedInputs " + result); - return result; - } - - private Set<String> toInputSet(String value) { - if (TextUtils.isEmpty(value)) { - return Collections.EMPTY_SET; - } - return new HashSet(Arrays.asList(value.split(","))); - } -} diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java index 9c881439..d10a852c 100644 --- a/src/com/android/tv/data/epg/EpgReader.java +++ b/src/com/android/tv/data/epg/EpgReader.java @@ -23,27 +23,12 @@ import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; import com.android.tv.dvr.data.SeriesInfo; -import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Set; /** An interface used to retrieve the EPG data. This class should be used in worker thread. */ @WorkerThread public interface EpgReader { - - /** Value class that holds a EpgChannelId and its corresponding Channel */ - // TODO(b/72052568): Get autovalue to work in aosp - abstract class EpgChannel { - public static EpgChannel createEpgChannel(Channel channel, String epgChannelId) { - return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId); - } - - public abstract Channel getChannel(); - - public abstract String getEpgChannelId(); - } - /** Checks if the reader is available. */ boolean isAvailable(); @@ -70,7 +55,7 @@ public interface EpgReader { * 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. */ - Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId); + List<Channel> getChannels(@NonNull String lineupId); /** Pre-loads and caches channels for a given lineup. */ void preloadChannels(@NonNull String lineupId); @@ -80,19 +65,18 @@ public interface EpgReader { void clearCachedChannels(@NonNull String lineupId); /** - * Returns the programs for the given channel. Must call {@link #getChannels(Set, String)} + * 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<Program> getPrograms(EpgChannel epgChannel); + List<Program> getPrograms(long channelId); /** * 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. */ - Map<EpgChannel, Collection<Program>> getPrograms( - @NonNull Set<EpgChannel> epgChannels, long duration); + Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration); /** Returns the series information for the given series ID. */ SeriesInfo getSeriesInfo(@NonNull String seriesId); diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java index 9a87619d..49409a1d 100644 --- a/src/com/android/tv/data/epg/StubEpgReader.java +++ b/src/com/android/tv/data/epg/StubEpgReader.java @@ -22,11 +22,9 @@ import com.android.tv.data.Channel; import com.android.tv.data.Lineup; import com.android.tv.data.Program; import com.android.tv.dvr.data.SeriesInfo; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; /** A stub class to read EPG. */ public class StubEpgReader implements EpgReader { @@ -58,8 +56,8 @@ public class StubEpgReader implements EpgReader { } @Override - public Set<EpgChannel> getChannels(Set<Channel> inputChannels, @NonNull String lineupId) { - return Collections.emptySet(); + public List<Channel> getChannels(@NonNull String lineupId) { + return Collections.emptyList(); } @Override @@ -73,13 +71,12 @@ public class StubEpgReader implements EpgReader { } @Override - public List<Program> getPrograms(EpgChannel epgChannel) { + public List<Program> getPrograms(long channelId) { return Collections.emptyList(); } @Override - public Map<EpgChannel, Collection<Program>> getPrograms( - @NonNull Set<EpgChannel> channels, long duration) { + public Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration) { return Collections.emptyMap(); } |