diff options
author | Nick Chalko <nchalko@google.com> | 2015-11-04 15:15:29 -0800 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2015-11-10 13:59:03 -0800 |
commit | 7d67089aa1e9aa2123c3cd2f386d7019a1544db1 (patch) | |
tree | 6f90c2065a853628dd7704788dd41b787acb6fae /src | |
parent | 07b043dc3db83d6d20f0e8513b946830ab00e37b (diff) | |
download | TV-7d67089aa1e9aa2123c3cd2f386d7019a1544db1.tar.gz |
Sync to ub-tv-glee at 1.07.007
hash dce17da9f45fc4304787b1898d9915511b1df954
Change-Id: I08ac6fc0123a6653644281155e35c11b71bc5fa0
Diffstat (limited to 'src')
82 files changed, 3835 insertions, 716 deletions
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java index f5114193..d0d7c595 100644 --- a/src/com/android/tv/ChannelTuner.java +++ b/src/com/android/tv/ChannelTuner.java @@ -16,21 +16,19 @@ package com.android.tv; -import android.content.Context; -import android.database.Cursor; import android.media.tv.TvContract; import android.net.Uri; import android.os.Handler; -import android.support.annotation.WorkerThread; import android.util.Log; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.util.CollectionUtils; +import com.android.tv.util.SoftPreconditions; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,9 +40,6 @@ import java.util.Set; public class ChannelTuner { private static final String TAG = "ChannelTuner"; - private static final int INVALID_INDEX = -1; - - private final Context mContext; private boolean mStarted; private boolean mChannelDataManagerLoaded; private final List<Channel> mChannels = new ArrayList<>(); @@ -56,7 +51,7 @@ public class ChannelTuner { private final Handler mHandler = new Handler(); private final ChannelDataManager mChannelDataManager; - private final Set<Listener> mListeners = new HashSet<>(); + private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); private Channel mCurrentChannel; private final ChannelDataManager.Listener mChannelDataManagerListener = @@ -84,8 +79,7 @@ public class ChannelTuner { } }; - public ChannelTuner(Context context, ChannelDataManager channelDataManager) { - mContext = context; + public ChannelTuner(ChannelDataManager channelDataManager) { mChannelDataManager = channelDataManager; } @@ -286,12 +280,11 @@ public class ChannelTuner { setCurrentChannelAndNotify(channel); return true; } + SoftPreconditions.checkState(mChannelDataManagerLoaded, TAG, "Channel data is not loaded"); Channel newChannel = mChannelMap.get(channel.getId()); if (newChannel != null) { setCurrentChannelAndNotify(newChannel); return true; - } else if (!mChannelDataManagerLoaded) { - return loadChannel(channel.getId()) != null; } return false; } @@ -386,37 +379,4 @@ public class ChannelTuner { } } } - - /** - * Loads and returns a channel which has the given channel ID. - * - * @param channelId The ID of the channel to be loaded. - * @return a channel if it has been loaded. {@code null} if the channel is not found. - */ - @WorkerThread - public Channel loadChannel(long channelId) { - if (channelId < 0) { - return null; - } - if (mChannelDataManagerLoaded) { - return mChannelMap.get(channelId); - } - Channel channel = mChannelMap.get(channelId); - if (channel != null) { - return channel; - } - - Uri uri = TvContract.buildChannelUri(channelId); - try (Cursor c = mContext.getContentResolver().query(uri, Channel.PROJECTION, - null, null, null)) { - if (c != null && c.moveToNext()) { - channel = Channel.fromCursor(c); - List<Channel> channels = new ArrayList<>(mChannels); - channels.add(channel); - updateChannelData(channels); - return channel; - } - } - return null; - } } diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java new file mode 100644 index 00000000..f62987ab --- /dev/null +++ b/src/com/android/tv/Features.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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; + +import static com.android.tv.common.feature.FeatureUtils.AND; +import static com.android.tv.common.feature.FeatureUtils.ON; +import static com.android.tv.common.feature.FeatureUtils.OR; + +import android.support.annotation.VisibleForTesting; + +import com.android.tv.common.feature.Feature; +import com.android.tv.common.feature.GServiceFeature; +import com.android.tv.common.feature.PropertyFeature; +import com.android.tv.util.EngOnlyFeature; + +/** + * List of {@link Feature} for the Live TV App. + * + * <p>Remove the {@code Feature} once it is launched. + */ +public final class Features { + /** + * UI for opting out of analytics. + * + * <p>See <a href="http://b/20228119">b/20228119</a> + */ + public static Feature ANALYTICS_OPT_OUT = new EngOnlyFeature(); + + /** + * Analytics that include sensitive information such as channel or program identifiers. + * + * <p>See <a href="http://b/22062676">b/22062676</a> + */ + public static Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_OUT); + + public static Feature EPG_SEARCH = new PropertyFeature("feature_tv_use_epg_search", false); + + + /** + * A flag which indicates that the on-boarding experience is used or not. + * + * <p>See <a href="http://b/24070322">b/24070322</a> + */ + public static Feature ONBOARDING_EXPERIENCE = new PropertyFeature( + "feature_tv_use_onboarding_exp", false); + + @VisibleForTesting + public static Feature TEST_FEATURE = new PropertyFeature("test_feature", false); + + private Features() { + } +} diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index db4cbffd..92cbd462 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -24,6 +24,7 @@ import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.PixelFormat; import android.graphics.Point; @@ -39,6 +40,7 @@ import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; import android.media.tv.TvView.OnUnhandledInputEventListener; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -61,6 +63,7 @@ import android.widget.FrameLayout; import android.widget.Toast; import com.android.tv.analytics.DurationTimer; +import com.android.tv.analytics.SendConfigInfoRunnable; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; @@ -69,9 +72,11 @@ import com.android.tv.data.OnCurrentProgramUpdatedListener; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; import com.android.tv.data.StreamInfo; +import com.android.tv.data.WatchedHistoryManager; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.menu.Menu; +import com.android.tv.onboarding.OnboardingActivity; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.receiver.AudioCapabilitiesReceiver; @@ -83,6 +88,7 @@ import com.android.tv.ui.InputBannerView; import com.android.tv.ui.KeypadChannelSwitchView; import com.android.tv.ui.OverlayRootView; import com.android.tv.ui.SelectInputView; +import com.android.tv.ui.SelectInputView.OnInputSelectedCallback; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnTuneListener; import com.android.tv.ui.TvOverlayManager; @@ -97,10 +103,15 @@ import com.android.tv.ui.sidepanel.SideFragment; import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment; import com.android.tv.util.CaptionSettings; import com.android.tv.util.ImageCache; +import com.android.tv.util.MemoryManageable; +import com.android.tv.util.OnboardingUtils; +import com.android.tv.util.PermissionUtils; import com.android.tv.util.PipInputManager; import com.android.tv.util.PipInputManager.PipInput; +import com.android.tv.util.RecurringRunner; import com.android.tv.util.SearchManagerHelper; import com.android.tv.util.SetupUtils; +import com.android.tv.util.SoftPreconditions; import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvSettings; @@ -143,6 +154,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private static final float FRAME_RATE_EPSILON = 0.1f; + private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1; + private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; + // Tracker screen names. public static final String SCREEN_NAME = "Main"; private static final String SCREEN_BEHIND_NAME = "Behind"; @@ -201,6 +215,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private AccessibilityManager mAccessibilityManager; private ChannelDataManager mChannelDataManager; private ProgramDataManager mProgramDataManager; + private WatchedHistoryManager mWatchedHistoryManager; private TvInputManagerHelper mTvInputManagerHelper; private ChannelTuner mChannelTuner; private PipInputManager mPipInputManager; @@ -271,6 +286,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private final ArrayDeque<Long> mRecentChannels = new ArrayDeque<>(MAX_RECENT_CHANNELS); private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; + private RecurringRunner mSendConfigInfoRecurringRunner; // A caller which started this activity. (e.g. TvSearch) private String mSource; @@ -375,20 +391,28 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override protected void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG,"onCreate()"); - if(BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { - Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show(); - } super.onCreate(savedInstanceState); - TvApplication tvApplication = (TvApplication) getApplication(); tvApplication.setMainActivity(this); + + if (Features.ONBOARDING_EXPERIENCE.isEnabled(this) + && OnboardingUtils.needToShowOnboarding(this)) { + startActivity(OnboardingActivity.buildIntent(this, getIntent())); + finish(); + return; + } + + if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { + Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show(); + } mTracker = tvApplication.getTracker(); mTvInputManagerHelper = tvApplication.getTvInputManagerHelper(); - mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper, mTracker); - mProgramDataManager = new ProgramDataManager(this); + mChannelDataManager = tvApplication.getChannelDataManager(); + mProgramDataManager = tvApplication.getProgramDataManager(); mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); - mChannelTuner = new ChannelTuner(this, mChannelDataManager); + mProgramDataManager.setPrefetchEnabled(true); + mChannelTuner = new ChannelTuner(mChannelDataManager); mChannelTuner.addListener(mChannelTunerListener); mChannelTuner.start(); mPipInputManager = new PipInputManager(this, mTvInputManagerHelper, mChannelTuner); @@ -458,6 +482,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mPipView.initialize((AppLayerTvView) findViewById(R.id.pip_tv_view), true, screenHeight, shrunkenTvViewHeight); + if (!PermissionUtils.hasAccessWatchedHistory(this)) { + mWatchedHistoryManager = new WatchedHistoryManager(getApplicationContext()); + mWatchedHistoryManager.start(); + mTvView.setWatchedHistoryManager(mWatchedHistoryManager); + } mTvViewUiManager = new TvViewUiManager(this, mTvView, mPipView, (FrameLayout) findViewById(android.R.id.content), mTvOptionsManager); @@ -473,6 +502,36 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC .inflate(R.layout.input_banner, sceneContainer, false); SelectInputView selectInputView = (SelectInputView) getLayoutInflater() .inflate(R.layout.select_input, sceneContainer, false); + selectInputView.setOnInputSelectedCallback(new OnInputSelectedCallback() { + @Override + public void onTunerInputSelected() { + Channel currentChannel = mChannelTuner.getCurrentChannel(); + if (currentChannel != null && !currentChannel.isPassthrough()) { + hideOverlays(); + } else { + tuneToLastWatchedChannelForTunerInput(); + } + } + + @Override + public void onPassthroughInputSelected(TvInputInfo input) { + Channel currentChannel = mChannelTuner.getCurrentChannel(); + String currentInputId = currentChannel == null ? null : currentChannel.getInputId(); + if (TextUtils.equals(input.getId(), currentInputId)) { + hideOverlays(); + } else { + tuneToChannel(Channel.createPassthroughChannel(input.getId())); + } + } + + private void hideOverlays() { + getOverlayManager().hideOverlays( + TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE + | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU); + } + }); mSearchFragment = new ProgramGuideSearchFragment(); mOverlayManager = new TvOverlayManager(this, mChannelTuner, mKeypadChannelSwitchView, mChannelBannerView, inputBannerView, @@ -486,27 +545,36 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC finish(); return; } - if (!TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) { - // Preloads the initial channel to reduce the initial tuning time. - Channel channel = loadInitialChannel(mInitChannelUri); - mInitChannelUri = channel == null ? null : channel.getUri(); - } - // mChannelDataManager.start() and mProgramDataManager.start() are called - // after loadInitialChannel. Unless, loadInitialChannel can be blocked - // by channel data and program data loading. - mChannelDataManager.start(); - mProgramDataManager.start(); - mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, mTracker); + mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null); mAudioCapabilitiesReceiver.register(); mAccessibilityManager = (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE); - + mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1), + new SendConfigInfoRunnable(mTracker, mTvInputManagerHelper)); + mSendConfigInfoRecurringRunner.start(); initForTest(); } @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Restart live channels. + Intent intent = getIntent(); + finish(); + startActivity(intent); + } else { + Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied, + Toast.LENGTH_LONG).show(); + finish(); + } + } + } + + @Override public void onAttachedToWindow() { super.onAttachedToWindow(); WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams( @@ -582,6 +650,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC protected void onResume() { if (DEBUG) Log.d(TAG, "onResume()"); super.onResume(); + if (!PermissionUtils.hasAccessAllEpg(this)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show(); + finish(); + } else if (checkSelfPermission(PERMISSION_READ_TV_LISTINGS) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS}, + PERMISSIONS_REQUEST_READ_TV_LISTINGS); + } + } mTracker.sendScreenView(SCREEN_NAME); SystemProperties.updateSystemProperties(); @@ -600,9 +678,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } if (mChannelTuner.areAllChannelsLoaded()) { markNewChannelsBrowsable(); + resumeTvIfNeeded(); + resumePipIfNeeded(); } - resumeTvIfNeeded(); - resumePipIfNeeded(); mOverlayManager.showMenuWithTimeShiftPauseIfNeeded(); // Note: The following codes are related to pop up an overlay UI after resume. @@ -692,6 +770,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return mActivityResumed; } + /** + * Returns true if {@link #onStart} is called and {@link #onStop} is not called yet. + */ + public boolean isActivityStarted() { + return mActivityStarted; + } + @Override public boolean requestVisibleBehind(boolean enable) { boolean state = super.requestVisibleBehind(enable); @@ -732,10 +817,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Set<String> newInputsWithChannels = new HashSet<>(); for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(true, true)) { String inputId = input.getId(); - if (!setupUtils.hasSetupLaunched(inputId) + if (!setupUtils.isSetupDone(inputId) && mChannelDataManager.getChannelCountForInput(inputId) > 0) { - setupUtils.onSetupLaunched(inputId); - setupUtils.markAsKnownInput(inputId); + setupUtils.onSetupDone(inputId); newInputsWithChannels.add(inputId); if (DEBUG) { Log.d(TAG, "New input " + inputId + " has " @@ -754,32 +838,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - private Channel loadInitialChannel(Uri channelUri) { - if (TvContract.isChannelUriForPassthroughInput(channelUri)) { - throw new IllegalArgumentException("channelUri should be null or tuner input channel"); - } - if (channelUri == null) { - // If any initial channel id is not given, remember the last channel the user watched. - long channelId = Utils.getLastWatchedChannelId(this); - if (channelId != Channel.INVALID_ID) { - channelUri = TvContract.buildChannelUri(channelId); - } - } - if (channelUri == null) { - return null; - } - long channelId = ContentUris.parseId(channelUri); - Channel channel = mChannelTuner.loadChannel(channelId); - if (channel == null) { - // If the requested channel doesn't exist, it's better to tune to the - // last watched channel. - channel = mChannelTuner.loadChannel(Utils.getLastWatchedChannelId(this)); - Log.w(TAG, "The requested channel (id=" + channelId + ") doesn't exist. " - + "The last watched channel will be tuned to."); - } - return channel; - } - private void startTv(Uri channelUri) { if (DEBUG) Log.d(TAG, "startTv Uri=" + channelUri); if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri)) @@ -789,6 +847,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // is playing, we stop the passthrough TV input. stopTv(); } + SoftPreconditions.checkState(TvContract.isChannelUriForPassthroughInput(channelUri) + || mChannelTuner.areAllChannelsLoaded(), + TAG, "startTV assumes that ChannelDataManager is already loaded."); if (mTvView.isPlaying()) { // TV has already started. if (channelUri == null) { @@ -822,11 +883,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC Channel channel = Channel.createPassthroughChannel(channelUri); mChannelTuner.moveToChannel(channel); } else { - Channel channel = loadInitialChannel(channelUri); + long channelId = ContentUris.parseId(channelUri); + Channel channel = mChannelDataManager.getChannel(channelId); if (channel == null || !mChannelTuner.moveToChannel(channel)) { mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0)); - long channelId = (channel == null) ? ContentUris.parseId(channelUri) - : channel.getId(); Log.w(TAG, "The requested channel (id=" + channelId + ") doesn't exist. " + "The first channel will be tuned to."); } @@ -880,8 +940,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mInputIdUnderSetup = input.getId(); mIsSetupActivityCalledByDialog = calledByDialog; + // Call requestVisibleBehind(false) before starting other activity. + // In Activity.requestVisibleBehind(false), this activity is scheduled to be stopped + // immediately if other activity is about to start. And this activity is scheduled to + // to be stopped again after onPause(). + stopTv("startSetupActivity()", false); startActivityForResultSafe(intent, REQUEST_CODE_START_SETUP_ACTIVITY); - SetupUtils.getInstance(this).onSetupLaunched(input.getId()); } catch (ActivityNotFoundException e) { mInputIdUnderSetup = null; Toast.makeText(this, getString(R.string.msg_unable_to_start_setup_activity, @@ -895,7 +959,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY); } - stopTv("startSetupActivity()", false); } public boolean hasCaptioningSettingsActivity() { @@ -1120,31 +1183,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CODE_START_SETUP_ACTIVITY: - if (resultCode == Activity.RESULT_OK) { + if (resultCode == RESULT_OK) { final String inputId = mInputIdUnderSetup; - // When TIS adds several channels, ChannelDataManager.Listener.onChannelList - // Updated() can be called several times. In this case, it is hard to detect - // which one is the last callback. To reduce error prune, we update channel - // list again and make all channels of {@code inputId} browsable. - mChannelDataManager.updateChannels(new Runnable() { + SetupUtils.getInstance(this).onTvInputSetupFinished(inputId, new Runnable() { @Override public void run() { - int count = 0; - boolean browsableChanged = false; - for (Channel channel : mChannelDataManager.getChannelList()) { - if (channel.getInputId().equals(inputId)) { - if (!channel.isBrowsable()) { - mChannelDataManager.updateBrowsable(channel.getId(), true, - true); - browsableChanged = true; - } - ++count; - } - } - if (browsableChanged) { - mChannelDataManager.notifyChannelBrowsableChanged(); - mChannelDataManager.applyUpdatedValuesToDb(); - } + int count = mChannelDataManager.getChannelCountForInput(inputId); String text; if (count > 0) { text = getResources().getQuantityString(R.plurals.msg_channel_added, @@ -1502,6 +1546,20 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } + /** + * Says {@code text} when accessibility is turned on. + */ + public void sendAccessiblityText(String text) { + if (mAccessibilityManager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setClassName(getClass().getName()); + event.setPackageName(getPackageName()); + event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); + event.getText().add(text); + mAccessibilityManager.sendAccessibilityEvent(event); + } + } + private void tune() { if (DEBUG) Log.d(TAG, "tune()"); mTuneDurationTimer.start(); @@ -1596,6 +1654,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (TvContract.isChannelUriForPassthroughInput(channel.getUri())) { TvInputInfo input = mTvInputManagerHelper.getTvInputInfo(channel.getInputId()); event.getText().add(Utils.loadLabel(this, input)); + } else if (TextUtils.isEmpty(channel.getDisplayName())) { + event.getText().add(channel.getDisplayNumber()); } else { event.getText().add(channel.getDisplayNumber() + " " + channel.getDisplayName()); } @@ -1822,8 +1882,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST); - boolean noOverlayUiWhenResume - = mInputToSetUp == null && !mShowProgramGuide && !mShowSelectInputView; + boolean noOverlayUiWhenResume = + mInputToSetUp == null && !mShowProgramGuide && !mShowSelectInputView; if (needToShowBanner && noOverlayUiWhenResume && mOverlayManager.getCurrentDialog() == null) { if (mChannelTuner.getCurrentChannel() == null) { @@ -1994,19 +2054,39 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override protected void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy()"); - mChannelTuner.removeListener(mChannelTunerListener); - mProgramDataManager.removeOnCurrentProgramUpdatedListener( - Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); - mProgramDataManager.stop(); - mChannelDataManager.stop(); - mPipInputManager.stop(); - mOverlayManager.release(); - mChannelTuner.stop(); - mKeypadChannelSwitchView.setChannels(null); + if (mChannelTuner != null) { + mChannelTuner.removeListener(mChannelTunerListener); + mChannelTuner.stop(); + } + TvApplication application = ((TvApplication) getApplication()); + if (mProgramDataManager != null) { + mProgramDataManager.removeOnCurrentProgramUpdatedListener( + Channel.INVALID_ID, mOnCurrentProgramUpdatedListener); + if (application.isCurrentMainActivity(this)) { + mProgramDataManager.setPrefetchEnabled(false); + } + } + if (mPipInputManager != null) { + mPipInputManager.stop(); + } + if (mOverlayManager != null) { + mOverlayManager.release(); + } + if (mKeypadChannelSwitchView != null) { + mKeypadChannelSwitchView.setChannels(null); + } mMemoryManageables.clear(); - ((TvApplication) getApplication()).setMainActivity(null); - mAudioCapabilitiesReceiver.unregister(); + if (mAudioCapabilitiesReceiver != null) { + mAudioCapabilitiesReceiver.unregister(); + } mHandler.removeCallbacksAndMessages(null); + if (application.isCurrentMainActivity(this)) { + application.setMainActivity(null); + } + if (mSendConfigInfoRecurringRunner != null) { + mSendConfigInfoRecurringRunner.stop(); + mSendConfigInfoRecurringRunner = null; + } super.onDestroy(); } @@ -2130,6 +2210,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } switch (keyCode) { case KeyEvent.KEYCODE_DPAD_RIGHT: + if (!PermissionUtils.hasModifyParentalControls(this)) { + // TODO: support this feature for non-system LC app. b/23939816 + return true; + } PinDialogFragment dialog = null; if (mTvView.isScreenBlocked()) { dialog = new PinDialogFragment( @@ -2284,7 +2368,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override public void onUserInteraction() { super.onUserInteraction(); - mOverlayManager.onUserInteraction(); + if (mOverlayManager != null) { + mOverlayManager.onUserInteraction(); + } } public void togglePipView() { @@ -2636,14 +2722,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC int stringId; switch (info.getVideoUnavailableReason()) { case TunableTvView.VIDEO_UNAVAILABLE_REASON_NOT_TUNED: - return; case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: + case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: + case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: return; case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: stringId = R.string.msg_channel_unavailable_weak_signal; break; - case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: - return; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: stringId = R.string.msg_channel_unavailable_unknown; @@ -2712,19 +2797,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC } } - /** - * Interface for the fine-grained memory management. - * The class which wants to release memory based on the system constraints should inherit - * this interface and implement {@link #performTrimMemory}. - */ - public interface MemoryManageable { - /** - * This method is called when the {@link MainActivity#onTrimMemory} is called. - * For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}. - */ - void performTrimMemory(int level); - } - private static class MainActivityHandler extends WeakHandler<MainActivity> { MainActivityHandler(MainActivity mainActivity) { super(mainActivity); diff --git a/src/com/android/tv/SelectInputActivity.java b/src/com/android/tv/SelectInputActivity.java new file mode 100644 index 00000000..c68a1ad0 --- /dev/null +++ b/src/com/android/tv/SelectInputActivity.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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; + +import android.app.Activity; +import android.content.Intent; +import android.media.tv.TvContract; +import android.media.tv.TvInputInfo; +import android.net.Uri; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.android.tv.data.Channel; +import com.android.tv.ui.SelectInputView; +import com.android.tv.ui.SelectInputView.OnInputSelectedCallback; +import com.android.tv.util.Utils; + +/** + * An activity to select input. + */ +public class SelectInputActivity extends Activity { + private SelectInputView mSelectInputView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ((TvApplication) getApplicationContext()).setSelectInputActivity(this); + setContentView(R.layout.activity_select_input); + mSelectInputView = (SelectInputView) findViewById(R.id.scene_transition_common); + mSelectInputView.setOnInputSelectedCallback(new OnInputSelectedCallback() { + @Override + public void onTunerInputSelected() { + startTvWithChannel(TvContract.Channels.CONTENT_URI); + } + + @Override + public void onPassthroughInputSelected(TvInputInfo input) { + startTvWithChannel(TvContract.buildChannelUriForPassthroughInput(input.getId())); + } + + private void startTvWithChannel(Uri channelUri) { + Intent intent = new Intent(Intent.ACTION_VIEW, channelUri, + SelectInputActivity.this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + } + }); + String channelUriString = Utils.getLastWatchedChannelUri(this); + if (channelUriString != null) { + Uri channelUri = Uri.parse(channelUriString); + if (TvContract.isChannelUriForPassthroughInput(channelUri)) { + mSelectInputView.setCurrentChannel(Channel.createPassthroughChannel(channelUri)); + } + // No need to set the tuner channel because it's the default selection. + } + } + + @Override + protected void onResume() { + super.onResume(); + mSelectInputView.onEnterAction(true); + } + + @Override + protected void onPause() { + mSelectInputView.onExitAction(); + super.onPause(); + } + + @Override + protected void onDestroy() { + ((TvApplication) getApplicationContext()).setSelectInputActivity(null); + super.onDestroy(); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_I || keyCode == KeyEvent.KEYCODE_TV_INPUT) { + mSelectInputView.onKeyUp(keyCode, event); + return true; + } + return super.onKeyUp(keyCode, event); + } +} diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java new file mode 100644 index 00000000..45b99776 --- /dev/null +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.media.tv.TvInputInfo; +import android.os.Bundle; +import android.util.Log; + +import com.android.tv.common.TvCommonConstants; +import com.android.tv.util.SetupUtils; +import com.android.tv.util.TvInputManagerHelper; + +/** + * An activity to launch a TV input setup activity. + * + * <p> After setup activity is finished, all channels will be browsable. + */ +public class SetupPassthroughActivity extends Activity { + private static final String TAG = "SetupPassthroughActivity"; + private static final boolean DEBUG = false; + + private static final int REQUEST_START_SETUP_ACTIVITY = 200; + + private TvInputInfo mTvInputInfo; + private Intent mActivityAfterCompletion; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + TvApplication tvApplication = (TvApplication) getApplication(); + TvInputManagerHelper inputManager = tvApplication.getTvInputManagerHelper(); + // It is not only for setting the variable but also for early initialization of + // ChannelDataManager. + String inputId = getIntent().getStringExtra(TvCommonConstants.EXTRA_INPUT_ID); + mTvInputInfo = inputManager.getTvInputInfo(inputId); + if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo); + if (mTvInputInfo == null) { + Log.w(TAG, "There is no input with the ID " + inputId + "."); + finish(); + return; + } + Intent setupIntent = mTvInputInfo.createSetupIntent(); + if (setupIntent == null) { + Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup."); + finish(); + return; + } + SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); + mActivityAfterCompletion = getIntent().getParcelableExtra( + TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION); + if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion); + setupIntent.putExtras(getIntent().getExtras()); + startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_START_SETUP_ACTIVITY || resultCode != Activity.RESULT_OK) { + finish(); + return; + } + SetupUtils.getInstance(this).onTvInputSetupFinished(mTvInputInfo.getId(), new Runnable() { + @Override + public void run() { + if (mActivityAfterCompletion != null) { + try { + startActivity(mActivityAfterCompletion); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Activity launch failed", e); + } + } + finish(); + } + }); + } +} diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index d3a8facb..9c699389 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -16,6 +16,7 @@ package com.android.tv; +import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.Intent; @@ -23,6 +24,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; +import android.os.AsyncTask; import android.os.Bundle; import android.os.StrictMode; import android.util.Log; @@ -30,25 +32,30 @@ import android.view.KeyEvent; import com.android.tv.analytics.Analytics; import com.android.tv.analytics.StubAnalytics; +import com.android.tv.analytics.OptOutPreferenceHelper; import com.android.tv.analytics.StubAnalytics; import com.android.tv.analytics.Tracker; -import com.android.tv.util.RecurringRunner; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.ProgramDataManager; import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.util.List; -import java.util.concurrent.TimeUnit; public class TvApplication extends Application { private static final String TAG = "TvApplication"; private static final boolean DEBUG = false; private static String versionName = ""; - private MainActivity mActivity; + private MainActivity mMainActivity; + private SelectInputActivity mSelectInputActivity; + private Analytics mAnalytics; private Tracker mTracker; private TvInputManagerHelper mTvInputManagerHelper; - private RecurringRunner mSendConfigInfoRecurringRunner; + private ChannelDataManager mChannelDataManager; + private ProgramDataManager mProgramDataManager; + private OptOutPreferenceHelper mOptPreferenceHelper; @Override public void onCreate() { @@ -66,13 +73,33 @@ public class TvApplication extends Application { } StrictMode.setVmPolicy(vmPolicyBuilder.build()); } - Analytics analytics; + if (BuildConfig.ENG && !SystemProperties.ALLOW_ANALYTICS_IN_ENG.getValue()) { - analytics = StubAnalytics.getInstance(this); + mAnalytics = StubAnalytics.getInstance(this); } else { - analytics = StubAnalytics.getInstance(this); + mAnalytics = StubAnalytics.getInstance(this); + } + mTracker = mAnalytics.getDefaultTracker(); + if(Features.ANALYTICS_OPT_OUT.isEnabled(this)) { + mOptPreferenceHelper = new OptOutPreferenceHelper(this); + mOptPreferenceHelper.registerChangeListener(mAnalytics, + OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE); + // always start with analytics off + mAnalytics.setAppOptOut(true); + // then update with the saved preference in an AsyncTask. + new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(Void... voids) { + return mOptPreferenceHelper.getOptOutPreference( + OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE); + } + + @Override + protected void onPostExecute(Boolean result) { + mAnalytics.setAppOptOut(result); + } + }.execute(); } - mTracker = analytics.getDefaultTracker(); try { PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); versionName = pInfo.versionName; @@ -82,16 +109,52 @@ public class TvApplication extends Application { } mTvInputManagerHelper = new TvInputManagerHelper(this); mTvInputManagerHelper.start(); - mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1), - new SendConfigInfoRunnable()); - mSendConfigInfoRecurringRunner.start(); if (DEBUG) Log.i(TAG, "Started Live TV " + versionName); } + /** + * Returns the {@link Analytics}. + */ + public Analytics getAnalytics() { + return mAnalytics; + } + + /** + * Returns the default tracker. + */ public Tracker getTracker() { return mTracker; } + public OptOutPreferenceHelper getOptPreferenceHelper(){ + return mOptPreferenceHelper; + } + + /** + * Returns {@link ChannelDataManager}. + */ + public ChannelDataManager getChannelDataManager() { + if (mChannelDataManager == null) { + mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper, mTracker); + mChannelDataManager.start(); + } + return mChannelDataManager; + } + + /** + * Returns {@link ProgramDataManager}. + */ + public ProgramDataManager getProgramDataManager() { + if (mProgramDataManager == null) { + mProgramDataManager = new ProgramDataManager(this); + mProgramDataManager.start(); + } + return mProgramDataManager; + } + + /** + * Returns {@link TvInputManagerHelper}. + */ public TvInputManagerHelper getTvInputManagerHelper() { return mTvInputManagerHelper; } @@ -101,21 +164,39 @@ public class TvApplication extends Application { * {@link MainActivity#onDestroy}. */ public void setMainActivity(MainActivity activity) { - mActivity = activity; + mMainActivity = activity; + } + + /** + * SelectInputActivity is set in {@link SelectInputActivity#onCreate} and cleared in + * {@link SelectInputActivity#onDestroy}. + */ + public void setSelectInputActivity(SelectInputActivity activity) { + mSelectInputActivity = activity; } /** * Checks if MainActivity is set or not. */ public boolean hasMainActivity() { - return (mActivity != null); + return (mMainActivity != null); + } + + /** + * Returns true, if {@code activity} is the current activity. + * + * Note: MainActivity can start while another MainActivity destroys. In this case, the current + * activity is the newly created activity. + */ + public boolean isCurrentMainActivity(MainActivity activity) { + return mMainActivity == activity; } /** * Handles the global key KEYCODE_TV. */ public void handleTvKey() { - if (mActivity == null || !mActivity.isActivityResumed()) { + if (mMainActivity == null || !mMainActivity.isActivityResumed()) { startMainActivity(null); } } @@ -139,16 +220,22 @@ public class TvApplication extends Application { if (inputCount < 2) { return; } - if (mActivity != null && mActivity.isActivityResumed()) { + Activity activityToHandle = mMainActivity != null && mMainActivity.isActivityResumed() + ? mMainActivity : mSelectInputActivity; + if (activityToHandle != null) { // If startActivity is called, MainActivity.onPause is unnecessarily called. To // prevent it, MainActivity.dispatchKeyEvent is directly called. - mActivity.dispatchKeyEvent( + activityToHandle.dispatchKeyEvent( new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TV_INPUT)); - mActivity.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TV_INPUT)); - } else { + activityToHandle.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_TV_INPUT)); + } else if (mMainActivity != null && mMainActivity.isActivityStarted()) { Bundle extras = new Bundle(); extras.putString(Utils.EXTRA_KEY_ACTION, Utils.EXTRA_ACTION_SHOW_TV_INPUT); startMainActivity(extras); + } else { + startActivity(new Intent(this, SelectInputActivity.class).setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK)); } } @@ -168,35 +255,4 @@ public class TvApplication extends Application { return versionName; } - /** - * Data useful for tracking that doesn't change often. - */ - public static class ConfigurationInfo { - public final int systemInputCount; - public final int nonSystemInputCount; - - public ConfigurationInfo(int systemInputCount, int nonSystemInputCount) { - this.systemInputCount = systemInputCount; - this.nonSystemInputCount = nonSystemInputCount; - } - } - - private class SendConfigInfoRunnable implements Runnable { - @Override - public void run() { - List<TvInputInfo> infoList = mTvInputManagerHelper.getTvInputInfos(false, false); - int systemInputCount = 0; - int nonSystemInputCount = 0; - for (TvInputInfo info : infoList) { - if (mTvInputManagerHelper.isSystemInput(info)) { - systemInputCount++; - } else { - nonSystemInputCount++; - } - } - ConfigurationInfo configurationInfo = new ConfigurationInfo(systemInputCount, - nonSystemInputCount); - mTracker.sendConfigurationInfo(configurationInfo); - } - } } diff --git a/src/com/android/tv/analytics/Analytics.java b/src/com/android/tv/analytics/Analytics.java index d5b99247..27085de7 100644 --- a/src/com/android/tv/analytics/Analytics.java +++ b/src/com/android/tv/analytics/Analytics.java @@ -17,8 +17,22 @@ package com.android.tv.analytics; /** - * Provides a Tracker used for user activity analysis. + * Provides Trackers used for user activity analysis. */ public interface Analytics { - Tracker getDefaultTracker(); + Tracker getDefaultTracker(); + + /** + * Returns whether the state of the application-level opt is on. + */ + boolean isAppOptOut(); + + /** + * Sets or resets the application-level opt out flag. If set, no hits will be sent. + * The value of this flag will <i>not</i> persist across application starts. The + * correct value should thus be set in application initialization code. + * + * @param optOut {@code true} if application-level opt out should be enforced. + */ + void setAppOptOut(boolean optOut); } diff --git a/src/com/android/tv/analytics/ConfigurationInfo.java b/src/com/android/tv/analytics/ConfigurationInfo.java new file mode 100644 index 00000000..41e8baeb --- /dev/null +++ b/src/com/android/tv/analytics/ConfigurationInfo.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.analytics; + +/** + * Data useful for tracking that doesn't change often. + */ +public class ConfigurationInfo { + public final int systemInputCount; + public final int nonSystemInputCount; + + public ConfigurationInfo(int systemInputCount, int nonSystemInputCount) { + this.systemInputCount = systemInputCount; + this.nonSystemInputCount = nonSystemInputCount; + } +} diff --git a/src/com/android/tv/analytics/OptOutPreferenceHelper.java b/src/com/android/tv/analytics/OptOutPreferenceHelper.java new file mode 100644 index 00000000..7fefaa46 --- /dev/null +++ b/src/com/android/tv/analytics/OptOutPreferenceHelper.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.analytics; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.preference.PreferenceManager; + +/** + * Handles the opt out preference for analytics, including updating {@link Analytics} with the + * preference changes. + */ +public final class OptOutPreferenceHelper { + /** + * The {@link SharedPreferences SharedPreferences} key + * "{@value #ANALYTICS_OPT_OUT_KEY}", true means the user has chosen NOT to send + * analytics. + */ + public static final String ANALYTICS_OPT_OUT_KEY = "analytics_opt_out"; + + /** + * The default value for the {@link SharedPreferences SharedPreferences} key + * "{@value #ANALYTICS_OPT_OUT_KEY}" is + * {@value #ANALYTICS_OPT_OUT_DEFAULT_VALUE} + */ + public static final boolean ANALYTICS_OPT_OUT_DEFAULT_VALUE = false; + + private final SharedPreferences userPrefs; + + public OptOutPreferenceHelper(Context context) { + userPrefs = PreferenceManager.getDefaultSharedPreferences(context); + } + + /** + * Creates and registers a change listener that will update analytics. + * + * @param analytics the analytics to update when opt out settings change. + * @param defaultValue the default opt out values + * @return the newly created OptOutChangeListener, keep this pass to + * {@link #unRegisterChangeListener(OptOutChangeListener)} + */ + public OptOutChangeListener registerChangeListener(Analytics analytics, boolean defaultValue) { + OptOutChangeListener changeListener = new OptOutChangeListener(analytics, defaultValue); + userPrefs.registerOnSharedPreferenceChangeListener(changeListener); + return changeListener; + } + + /** + * Unregister a {@link OptOutChangeListener} created by + * {@link #registerChangeListener(Analytics, boolean)} + */ + public void unRegisterChangeListener(OptOutChangeListener changeListener) { + userPrefs.registerOnSharedPreferenceChangeListener(changeListener); + } + + /** + * Returns the saved opt out preference or {@code defaultValue} if it has been set. + */ + public boolean getOptOutPreference(boolean defaultValue) { + return userPrefs.getBoolean(ANALYTICS_OPT_OUT_KEY, defaultValue); + } + + /** + * Sets the opt out preference. + */ + public void setOptOutPreference(boolean optOut) { + userPrefs.edit().putBoolean(ANALYTICS_OPT_OUT_KEY, optOut).apply(); + } + + /** + * Updates Analytics when opt out preference is changed. + * + * <p>{@link OnSharedPreferenceChangeListener} is used so the {@code analytics} object is + * updated even if the preference are modified directly and not by + * {@link OptOutPreferenceHelper}. + */ + public static final class OptOutChangeListener implements OnSharedPreferenceChangeListener { + private final Analytics mAnalytics; + private final boolean mDefaultValue; + + private OptOutChangeListener(Analytics analytics, boolean defaultValue) { + mAnalytics = analytics; + mDefaultValue = defaultValue; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + switch (key) { + case ANALYTICS_OPT_OUT_KEY: + mAnalytics.setAppOptOut( + sharedPreferences.getBoolean(ANALYTICS_OPT_OUT_KEY, mDefaultValue)); + break; + default: + } + } + } +} diff --git a/src/com/android/tv/analytics/SendConfigInfoRunnable.java b/src/com/android/tv/analytics/SendConfigInfoRunnable.java new file mode 100644 index 00000000..c2d5c5fb --- /dev/null +++ b/src/com/android/tv/analytics/SendConfigInfoRunnable.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.analytics; + +import android.media.tv.TvInputInfo; + +import com.android.tv.util.TvInputManagerHelper; + +import java.util.List; + +/** + * Sends ConfigurationInfo once a day. + */ +public class SendConfigInfoRunnable implements Runnable { + private Tracker mTracker; + private TvInputManagerHelper mTvInputManagerHelper; + + public SendConfigInfoRunnable(Tracker tracker, TvInputManagerHelper tvInputManagerHelper) { + this.mTracker = tracker; + this.mTvInputManagerHelper = tvInputManagerHelper; + } + + @Override + public void run() { + List<TvInputInfo> infoList = mTvInputManagerHelper.getTvInputInfos(false, false); + int systemInputCount = 0; + int nonSystemInputCount = 0; + for (TvInputInfo info : infoList) { + if (mTvInputManagerHelper.isSystemInput(info)) { + systemInputCount++; + } else { + nonSystemInputCount++; + } + } + ConfigurationInfo configurationInfo = new ConfigurationInfo(systemInputCount, + nonSystemInputCount); + mTracker.sendConfigurationInfo(configurationInfo); + } +} diff --git a/src/com/android/tv/analytics/StubAnalytics.java b/src/com/android/tv/analytics/StubAnalytics.java index 9b54e7d2..ae4cdafa 100644 --- a/src/com/android/tv/analytics/StubAnalytics.java +++ b/src/com/android/tv/analytics/StubAnalytics.java @@ -17,23 +17,34 @@ package com.android.tv.analytics; import android.app.Application; +import android.content.Context; /** * An implementation of {@link Analytics} that returns a {@link StubTracker}. */ public final class StubAnalytics implements Analytics { - private static final StubAnalytics INSTANCE = new StubAnalytics(); - public static StubAnalytics getInstance(Application application) { - return INSTANCE; + return new StubAnalytics(application); } private final Tracker mTracker = new StubTracker(); + private boolean mOptOut = OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE; - private StubAnalytics() { } + private StubAnalytics(Context context) { + } @Override public Tracker getDefaultTracker() { return mTracker; } + + @Override + public boolean isAppOptOut() { + return mOptOut; + } + + @Override + public void setAppOptOut(boolean optOut) { + mOptOut = optOut; + } } diff --git a/src/com/android/tv/analytics/StubTracker.java b/src/com/android/tv/analytics/StubTracker.java index f7efcb92..6e64ebca 100644 --- a/src/com/android/tv/analytics/StubTracker.java +++ b/src/com/android/tv/analytics/StubTracker.java @@ -19,7 +19,6 @@ package com.android.tv.analytics; import android.support.annotation.VisibleForTesting; import com.android.tv.TimeShiftManager; -import com.android.tv.TvApplication; import com.android.tv.data.Channel; /** @@ -31,7 +30,7 @@ public class StubTracker implements Tracker { public void sendChannelCount(int browsableChannelCount, int totalChannelCount) { } @Override - public void sendConfigurationInfo(TvApplication.ConfigurationInfo info) { } + public void sendConfigurationInfo(ConfigurationInfo info) { } @Override public void sendMainStart() { } @@ -97,9 +96,18 @@ public class StubTracker implements Tracker { public void sendChannelNumberItemChosenByTimeout() { } @Override + public void sendChannelVideoUnavailable(Channel channel, int reason) { } + + @Override public void sendAc3PassthroughCapabilities(boolean isSupported) { } @Override + public void sendInputConnectionFailure(String inputId) { } + + @Override + public void sendInputDisconnected(String inputId) { } + + @Override public void sendShowInputSelection() { } @Override diff --git a/src/com/android/tv/analytics/Tracker.java b/src/com/android/tv/analytics/Tracker.java index 05638871..291fc9ce 100644 --- a/src/com/android/tv/analytics/Tracker.java +++ b/src/com/android/tv/analytics/Tracker.java @@ -17,7 +17,6 @@ package com.android.tv.analytics; import com.android.tv.TimeShiftManager; -import com.android.tv.TvApplication; import com.android.tv.data.Channel; /** @@ -44,7 +43,7 @@ public interface Tracker { * * @param info the configuration info. */ - void sendConfigurationInfo(TvApplication.ConfigurationInfo info); + void sendConfigurationInfo(ConfigurationInfo info); /** * Sends tracking information for starting the MainActivity. @@ -179,6 +178,11 @@ public interface Tracker { void sendChannelNumberItemChosenByTimeout(); /** + * Sends tracking for the reason video is unavailable on a channel. + */ + void sendChannelVideoUnavailable(Channel channel, int reason); + + /** * Sends HDMI AC3 passthrough capabilities. * * @param isSupported {@code true} if the feature is supported; otherwise {@code false}. @@ -186,6 +190,22 @@ public interface Tracker { void sendAc3PassthroughCapabilities(boolean isSupported); /** + * Sends tracking for input a connection failure. + * <p><strong>WARNING</strong> callers must ensure no PII is included in the inputId. + * + * @param inputId the input the failure happened on + */ + void sendInputConnectionFailure(String inputId); + + /** + * Sends tracking for input disconnected. + * <p><strong>WARNING</strong> callers must ensure no PII is included in the inputId. + * + * @param inputId the input the failure happened on + */ + void sendInputDisconnected(String inputId); + + /** * Sends tracking information for showing the input selection view. */ void sendShowInputSelection(); @@ -224,7 +244,7 @@ public interface Tracker { /** * Sends time shift action (pause, ff, etc). * - * @param actionId The label of the side panel + * @param actionId The {@link com.android.tv.TimeShiftManager.TimeShiftActionId} */ void sendTimeShiftAction(@TimeShiftManager.TimeShiftActionId int actionId); } diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 49244c14..7901a01f 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -25,6 +25,7 @@ import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.net.Uri; import android.os.Build; +import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; @@ -198,6 +199,7 @@ public final class Channel { return mDisplayNumber; } + @Nullable public String getDisplayName() { return mDisplayName; } diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index 2325952f..067f2583 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -19,6 +19,8 @@ package com.android.tv.data; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; import android.database.ContentObserver; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; @@ -34,6 +36,8 @@ import android.util.MutableInt; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.util.AsyncDbTask; +import com.android.tv.util.CollectionUtils; +import com.android.tv.util.PermissionUtils; import com.android.tv.util.RecurringRunner; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -60,6 +64,7 @@ public class ChannelDataManager { private static final int MSG_UPDATE_CHANNELS = 1000; private static final long SEND_CHANNEL_STATUS_INTERVAL_MS = TimeUnit.DAYS.toMillis(1); + private static final String SHARED_PREF_BROWSABLE = "browsable_shared_preference"; private final Context mContext; private final TvInputManagerHelper mInputManager; @@ -71,7 +76,7 @@ public class ChannelDataManager { private RecurringRunner mRecurringRunner; private final Tracker mTracker; - private final Set<Listener> mListeners = new HashSet<>(); + private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>(); private final Map<String, MutableInt> mChannelCountMap = new HashMap<>(); private final Channel.DefaultComparator mChannelComparator; @@ -83,6 +88,8 @@ public class ChannelDataManager { private final ContentResolver mContentResolver; private final ContentObserver mChannelObserver; + private final boolean mStoreBrowsableInSharedPreferences; + private final SharedPreferences mBrowsableSharedPreferences; private final TvInputCallback mTvInputCallback = new TvInputCallback() { @Override @@ -134,19 +141,19 @@ public class ChannelDataManager { public ChannelDataManager(Context context, TvInputManagerHelper inputManager, Tracker tracker) { - this(context, inputManager, tracker, context.getContentResolver(), Looper.myLooper()); + this(context, inputManager, tracker, context.getContentResolver()); } @VisibleForTesting ChannelDataManager(Context context, TvInputManagerHelper inputManager, Tracker tracker, - ContentResolver contentResolver, Looper looper) { + ContentResolver contentResolver) { mContext = context; mInputManager = inputManager; mContentResolver = contentResolver; mChannelComparator = new Channel.DefaultComparator(context, inputManager); // Detect duplicate channels while sorting. mChannelComparator.setDetectDuplicatesEnabled(true); - mHandler = new ChannelDataManagerHandler(looper, this); + mHandler = new ChannelDataManagerHandler(this); mChannelObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { @@ -158,6 +165,9 @@ public class ChannelDataManager { mTracker = tracker; mRecurringRunner = new RecurringRunner(mContext, SEND_CHANNEL_STATUS_INTERVAL_MS, new SendChannelStatusRunnable()); + mStoreBrowsableInSharedPreferences = !PermissionUtils.hasAccessAllEpg(mContext); + mBrowsableSharedPreferences = context.getSharedPreferences(SHARED_PREF_BROWSABLE, + Context.MODE_PRIVATE); } @VisibleForTesting @@ -185,6 +195,7 @@ public class ChannelDataManager { * Stops the manager. It clears manager states and runs pending DB operations. Added listeners * aren't automatically removed by this method. */ + @VisibleForTesting public void stop() { if (!mStarted) { return; @@ -413,11 +424,22 @@ public class ChannelDataManager { channelWrapper.mBrowsableInDb = channelWrapper.mChannel.isBrowsable(); } String column = TvContract.Channels.COLUMN_BROWSABLE; - if (browsableIds.size() != 0) { - updateOneColumnValue(column, 1, browsableIds); - } - if (unbrowsableIds.size() != 0) { - updateOneColumnValue(column, 0, unbrowsableIds); + if (mStoreBrowsableInSharedPreferences) { + Editor editor = mBrowsableSharedPreferences.edit(); + for (Long id : browsableIds) { + editor.putBoolean(getBrowsableKey(getChannel(id)), true); + } + for (Long id : unbrowsableIds) { + editor.putBoolean(getBrowsableKey(getChannel(id)), false); + } + editor.apply(); + } else { + if (browsableIds.size() != 0) { + updateOneColumnValue(column, 1, browsableIds); + } + if (unbrowsableIds.size() != 0) { + updateOneColumnValue(column, 0, unbrowsableIds); + } } mBrowsableUpdateChannelIds.clear(); @@ -507,7 +529,7 @@ public class ChannelDataManager { } private class ChannelWrapper { - final Set<ChannelListener> mChannelListeners = new HashSet<>(); + final Set<ChannelListener> mChannelListeners = CollectionUtils.createSmallSet(); final Channel mChannel; boolean mBrowsableInDb; boolean mLockedInDb; @@ -561,7 +583,17 @@ public class ChannelDataManager { boolean channelAdded = false; boolean channelUpdated = false; boolean channelRemoved = false; + Map<String, ?> deletedBrowsableMap = null; + if (mStoreBrowsableInSharedPreferences) { + deletedBrowsableMap = new HashMap<>(mBrowsableSharedPreferences.getAll()); + } for (Channel channel : channels) { + if (mStoreBrowsableInSharedPreferences) { + String browsableKey = getBrowsableKey(channel); + channel.setBrowsable(mBrowsableSharedPreferences.getBoolean(browsableKey, + false)); + deletedBrowsableMap.remove(browsableKey); + } long channelId = channel.getId(); boolean newlyAdded = !removedChannelIds.remove(channelId); ChannelWrapper channelWrapper; @@ -591,6 +623,17 @@ public class ChannelDataManager { } } } + if (mStoreBrowsableInSharedPreferences && !deletedBrowsableMap.isEmpty() + && PermissionUtils.hasReadTvListings(mContext)) { + // If hasReadTvListings(mContext) is false, the given channel list would + // empty. In this case, we skip the browsable data clean up process. + Editor editor = mBrowsableSharedPreferences.edit(); + for (String key : deletedBrowsableMap.keySet()) { + if (DEBUG) Log.d(TAG, "remove key: " + key); + editor.remove(key); + } + editor.apply(); + } for (long id : removedChannelIds) { ChannelWrapper channelWrapper = mChannelWrapperMap.remove(id); @@ -639,6 +682,10 @@ public class ChannelDataManager { */ private void updateOneColumnValue( final String columnName, final int columnValue, final List<Long> ids) { + if (!PermissionUtils.hasAccessAllEpg(mContext)) { + // TODO: support this feature for non-system LC app. b/23939816 + return; + } AsyncDbTask.execute(new Runnable() { @Override public void run() { @@ -650,9 +697,13 @@ public class ChannelDataManager { }); } + private String getBrowsableKey(Channel channel) { + return channel.getInputId() + "|" + channel.getId(); + } + private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> { - public ChannelDataManagerHandler(Looper looper, ChannelDataManager channelDataManager) { - super(looper, channelDataManager); + public ChannelDataManagerHandler(ChannelDataManager channelDataManager) { + super(Looper.getMainLooper(), channelDataManager); } @Override diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java index 166b1d87..5a549f83 100644 --- a/src/com/android/tv/data/ChannelLogoFetcher.java +++ b/src/com/android/tv/data/ChannelLogoFetcher.java @@ -30,6 +30,7 @@ import android.util.Log; import com.android.tv.util.AsyncDbTask; 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; @@ -78,6 +79,10 @@ public class ChannelLogoFetcher { * The previous task is canceled and a new task starts. */ public static void startFetchingChannelLogos(Context context) { + 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."); @@ -201,6 +206,14 @@ public class ChannelLogoFetcher { 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)) { @@ -216,16 +229,21 @@ public class ChannelLogoFetcher { sb.append(splitName); } logoUri = channelNameLogoUriMap.get(sb.toString()); - if (DEBUG && TextUtils.isEmpty(logoUri)) { - Log.d(TAG, "Can't find a logo URI for channel '" + 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 && TextUtils.isEmpty(logoUri)) { - Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0] - + "'"); + if (DEBUG) { + if (TextUtils.isEmpty(logoUri)) { + Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0] + + "'"); + } } } } diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java index 8e9c3cb2..59021609 100644 --- a/src/com/android/tv/data/ChannelNumber.java +++ b/src/com/android/tv/data/ChannelNumber.java @@ -101,22 +101,41 @@ public final class ChannelNumber implements Comparable<ChannelNumber> { } if (indexOfDelimiter < 0) { ret.majorNumber = number; - return ret; + if (!isInteger(ret.majorNumber)) { + return null; + } + } else { + ret.hasDelimiter = true; + ret.majorNumber = number.substring(0, indexOfDelimiter); + ret.minorNumber = number.substring(indexOfDelimiter + 1); + if (!isInteger(ret.majorNumber) || !isInteger(ret.minorNumber)) { + return null; + } } - ret.hasDelimiter = true; - ret.majorNumber = number.substring(0, indexOfDelimiter); - ret.minorNumber = number.substring(indexOfDelimiter + 1); return ret; } public static int compare(String lhs, String rhs) { - if (lhs == null && rhs == null) { + ChannelNumber lhsNumber = parseChannelNumber(lhs); + ChannelNumber rhsNumber = parseChannelNumber(rhs); + if (lhsNumber == null && rhsNumber == null) { return 0; - } else if (lhs == null /* && rhs != null */) { + } else if (lhsNumber == null /* && rhsNumber != null */) { return -1; - } else if (lhs != null && rhs == null) { + } else if (lhsNumber != null && rhsNumber == null) { return 1; } - return parseChannelNumber(lhs).compareTo(parseChannelNumber(rhs)); + return lhsNumber.compareTo(rhsNumber); + } + + public static boolean isInteger(String string) { + try { + Integer.parseInt(string); + } catch(NumberFormatException e) { + return false; + } catch(NullPointerException e) { + return false; + } + return true; } } diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java index c12e7094..3f527433 100644 --- a/src/com/android/tv/data/ProgramDataManager.java +++ b/src/com/android/tv/data/ProgramDataManager.java @@ -32,20 +32,21 @@ import android.util.LongSparseArray; import android.util.LruCache; import com.android.tv.BuildConfig; -import com.android.tv.MainActivity.MemoryManageable; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Clock; +import com.android.tv.util.CollectionUtils; +import com.android.tv.util.MemoryManageable; +import com.android.tv.util.MultiLongSparseArray; +import com.android.tv.util.SoftPreconditions; import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -81,20 +82,21 @@ public class ProgramDataManager implements MemoryManageable { private final Clock mClock; private final ContentResolver mContentResolver; private boolean mStarted; - private long mProgramPrefetchUpdateWaitMs; private ProgramsUpdateTask mProgramsUpdateTask; private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap = new LongSparseArray<>(); - private long mLastPrefetchTaskRunMs; - private ProgramsPrefetchTask mProgramsPrefetchTask; private final Map<Long, Program> mChannelIdCurrentProgramMap = new HashMap<>(); - private final Map<Long, List<OnCurrentProgramUpdatedListener>> - mOnCurrentProgramUpdatedListenersMap = new HashMap<>(); + private final MultiLongSparseArray<OnCurrentProgramUpdatedListener> + mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>(); private final Handler mHandler; - private final List<Listener> mListeners = new ArrayList<>(); + private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); private final ContentObserver mProgramObserver; + private boolean mPrefetchEnabled; + private long mProgramPrefetchUpdateWaitMs; + private long mLastPrefetchTaskRunMs; + private ProgramsPrefetchTask mProgramsPrefetchTask; private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>(); // Any program that ends prior to this time will be removed from the cache @@ -123,11 +125,13 @@ public class ProgramDataManager implements MemoryManageable { if (isProgramUpdatePaused()) { return; } - // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long - // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message and - // send MSG_UPDATE_PREFETCH_PROGRAM again. - mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); - mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); + if (mPrefetchEnabled) { + // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long + // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message and + // send MSG_UPDATE_PREFETCH_PROGRAM again. + mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); + mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); + } } }; mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS; @@ -159,7 +163,9 @@ public class ProgramDataManager implements MemoryManageable { // Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message // to the handler. If not, another DB task can be executed before loading current programs. handleUpdateCurrentPrograms(); - mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); + if (mPrefetchEnabled) { + mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); + } mContentResolver.registerContentObserver(Programs.CONTENT_URI, true, mProgramObserver); } @@ -168,6 +174,7 @@ public class ProgramDataManager implements MemoryManageable { * Stops the manager. It clears manager states and runs pending DB operations. Added listeners * aren't automatically removed by this method. */ + @VisibleForTesting public void stop() { if (!mStarted) { return; @@ -219,12 +226,36 @@ public class ProgramDataManager implements MemoryManageable { } /** + * Enables or Disables program prefetch. + */ + public void setPrefetchEnabled(boolean enable) { + if (mPrefetchEnabled == enable) { + return; + } + if (enable) { + mPrefetchEnabled = true; + mLastPrefetchTaskRunMs = 0; + if (mStarted) { + mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); + } + } else { + mPrefetchEnabled = false; + cancelPrefetchTask(); + mChannelIdProgramCache.clear(); + mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM); + } + } + + /** * Returns the programs for the given channel which ends after the given start time. * + * <p> Prefetch should be enabled to call it. + * * @return {@link List} with Programs. It may includes dummy program if the entry needs DB * operations to get. */ public List<Program> getPrograms(long channelId, long startTime) { + SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); ArrayList<Program> cachedPrograms = mChannelIdProgramCache.get(channelId); if (cachedPrograms == null) { return Collections.emptyList(); @@ -267,13 +298,8 @@ public class ProgramDataManager implements MemoryManageable { */ public void addOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener) { - List<OnCurrentProgramUpdatedListener> listeners = - mOnCurrentProgramUpdatedListenersMap.get(channelId); - if (listeners == null) { - listeners = new ArrayList<>(); - mOnCurrentProgramUpdatedListenersMap.put(channelId, listeners); - } - listeners.add(listener); + mChannelId2ProgramUpdatedListeners + .put(channelId, listener); } /** @@ -282,36 +308,28 @@ public class ProgramDataManager implements MemoryManageable { */ public void removeOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener) { - List<OnCurrentProgramUpdatedListener> listeners = - mOnCurrentProgramUpdatedListenersMap.get(channelId); - if (listeners != null) { - listeners.remove(listener); - // Do not remove list from map although it's empty to reduce GC. - // The unused list is removed in {@link #performTrimMemory} which is called when - // the memory usage is high. - } + mChannelId2ProgramUpdatedListeners + .remove(channelId, listener); } private void notifyCurrentProgramUpdate(long channelId, Program program) { - List<OnCurrentProgramUpdatedListener> listeners = - mOnCurrentProgramUpdatedListenersMap.get(channelId); - if (listeners != null) { - for (OnCurrentProgramUpdatedListener listener : listeners) { - listener.onCurrentProgramUpdated(channelId, program); + + for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners + .get(channelId)) { + listener.onCurrentProgramUpdated(channelId, program); } - } - listeners = mOnCurrentProgramUpdatedListenersMap.get(Channel.INVALID_ID); - if (listeners != null) { - for (OnCurrentProgramUpdatedListener listener : listeners) { - listener.onCurrentProgramUpdated(channelId, program); + for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners + .get(Channel.INVALID_ID)) { + listener.onCurrentProgramUpdated(channelId, program); } - } } private void updateCurrentProgram(long channelId, Program program) { Program previousProgram = mChannelIdCurrentProgramMap.put(channelId, program); if (!Objects.equals(program, previousProgram)) { - removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program); + if (mPrefetchEnabled) { + removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program); + } notifyCurrentProgramUpdate(channelId, program); } @@ -329,6 +347,7 @@ public class ProgramDataManager implements MemoryManageable { private void removePreviousProgramsAndUpdateCurrentProgramInCache( long channelId, Program currentProgram) { + SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); if (!Program.isValid(currentProgram)) { return; } @@ -369,6 +388,11 @@ public class ProgramDataManager implements MemoryManageable { } break; } + if (cachedPrograms.isEmpty()) { + // If all the cached programs finish before mPrefetchTimeRangeStartMs, the + // currentProgram would not have a chance to be inserted to the cache. + cachedPrograms.add(currentProgram); + } mChannelIdProgramCache.put(channelId, cachedPrograms); } @@ -452,6 +476,8 @@ public class ProgramDataManager implements MemoryManageable { if (DEBUG) { Log.d(TAG, "Database is changed while querying. Will retry."); } + } catch (SecurityException e) { + Log.d(TAG, "Security exception during program data query", e); } } if (DEBUG) { @@ -540,7 +566,9 @@ public class ProgramDataManager implements MemoryManageable { removedChannelIds.remove(channelId); } for (Long channelId : removedChannelIds) { - mChannelIdProgramCache.remove(channelId); + if (mPrefetchEnabled) { + mChannelIdProgramCache.remove(channelId); + } mChannelIdCurrentProgramMap.remove(channelId); notifyCurrentProgramUpdate(channelId, null); } @@ -623,8 +651,11 @@ public class ProgramDataManager implements MemoryManageable { * Pause program update. * Updating program data will result in UI refresh, * but UI is fragile to handle it so we'd better disable it for a while. + * + * <p> Prefetch should be enabled to call it. */ public void setPauseProgramUpdate(boolean pauseProgramUpdate) { + SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); if (mPauseProgramUpdate && !pauseProgramUpdate) { if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { // MSG_UPDATE_PRFETCH_PROGRAM can be empty @@ -645,11 +676,16 @@ public class ProgramDataManager implements MemoryManageable { * Sets program data prefetch time range. * Any program data that ends before the start time will be removed from the cache later. * Note that there's no limit for end time. + * + * <p> Prefetch should be enabled to call it. */ public void setPrefetchTimeRange(long startTimeMs) { + SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled."); if (mPrefetchTimeRangeStartMs > startTimeMs) { - // TODO: Do not throw exception and simply refresh the cache. - throw new IllegalArgumentException(""); + // Fetch the programs immediately to re-create the cache. + if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) { + mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM); + } } mPrefetchTimeRangeStartMs = startTimeMs; } @@ -692,13 +728,6 @@ public class ProgramDataManager implements MemoryManageable { @Override public void performTrimMemory(int level) { - // Removes unused listeners. - Iterator<Entry<Long, List<OnCurrentProgramUpdatedListener>>> it = - mOnCurrentProgramUpdatedListenersMap.entrySet().iterator(); - while (it.hasNext()) { - if (it.next().getValue().isEmpty()) { - it.remove(); - } - } + mChannelId2ProgramUpdatedListeners.clearEmptyCache(); } } diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java new file mode 100644 index 00000000..13707ae6 --- /dev/null +++ b/src/com/android/tv/data/WatchedHistoryManager.java @@ -0,0 +1,323 @@ +package com.android.tv.data; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.MainThread; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Scanner; +import java.util.concurrent.TimeUnit; + +/** + * A class to manage watched history. + * + * <p>When there is no access to watched table of TvProvider, + * this class is used to build up watched history and to compute recent channels. + */ +public class WatchedHistoryManager { + private final static String TAG = "WatchedHistoryManager"; + private final boolean DEBUG = false; + + private static final int MAX_HISTORY_SIZE = 10000; + private static final String SHARED_PREF_WATCHED_HISTORY = "watched_history_shared_preference"; + private static final String PREF_KEY_LAST_INDEX = "last_index"; + private static final long MIN_DURATION_MS = TimeUnit.SECONDS.toMillis(10); + private static final long RECENT_CHANNEL_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); + + private final List<WatchedRecord> mWatchedHistory = new ArrayList<>(); + private final List<WatchedRecord> mPendingRecords = new ArrayList<>(); + private long mLastIndex; + private boolean mStarted; + private boolean mLoaded; + private SharedPreferences mSharedPreferences; + private SharedPreferences.OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = + new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + @MainThread + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + if (key.equals(PREF_KEY_LAST_INDEX)) { + final long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1); + if (lastIndex <= mLastIndex) { + return; + } + // onSharedPreferenceChanged is always called in a main thread. + // onNewRecordAdded will be called in the same thread as the thread + // which created this instance. + mHandler.post(new Runnable() { + @Override + public void run() { + for (long i = mLastIndex + 1; i <= lastIndex; ++i) { + WatchedRecord record = decode( + mSharedPreferences.getString(getSharedPreferencesKey(i), + null)); + if (record != null) { + mWatchedHistory.add(record); + if (mListener != null) { + mListener.onNewRecordAdded(record); + } + } + } + mLastIndex = lastIndex; + } + }); + } + } + }; + + private final Context mContext; + private Listener mListener; + private final int mMaxHistorySize; + private Handler mHandler; + + public WatchedHistoryManager(Context context) { + this(context, MAX_HISTORY_SIZE); + } + + @VisibleForTesting + WatchedHistoryManager(Context context, int maxHistorySize) { + mContext = context.getApplicationContext(); + mMaxHistorySize = maxHistorySize; + if (Looper.myLooper() == null) { + mHandler = new Handler(Looper.getMainLooper()); + } else { + mHandler = new Handler(); + } + } + + /** + * Starts the manager. It loads history data from {@link SharedPreferences}. + */ + public void start() { + if (mStarted) { + return; + } + mStarted = true; + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + mSharedPreferences = mContext.getSharedPreferences( + SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE); + mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1); + if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) { + for (int i = 0; i <= mLastIndex; ++i) { + WatchedRecord record = + decode(mSharedPreferences.getString(getSharedPreferencesKey(i), + null)); + if (record != null) { + mWatchedHistory.add(record); + } + } + } else if (mLastIndex >= mMaxHistorySize) { + for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) { + WatchedRecord record = decode(mSharedPreferences.getString( + getSharedPreferencesKey(i), null)); + if (record != null) { + mWatchedHistory.add(record); + } + } + } + return null; + } + + @Override + protected void onPostExecute(Void params) { + mLoaded = true; + if (DEBUG) { + Log.d(TAG, "Loaded: size=" + mWatchedHistory.size() + " index=" + mLastIndex); + } + if (!mPendingRecords.isEmpty()) { + Editor editor = mSharedPreferences.edit(); + for (WatchedRecord record : mPendingRecords) { + mWatchedHistory.add(record); + ++mLastIndex; + editor.putString(getSharedPreferencesKey(mLastIndex), encode(record)); + } + editor.putLong(PREF_KEY_LAST_INDEX, mLastIndex).apply(); + mPendingRecords.clear(); + } + if (mListener != null) { + mListener.onLoadFinished(); + } + mSharedPreferences.registerOnSharedPreferenceChangeListener( + mOnSharedPreferenceChangeListener); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @VisibleForTesting + public boolean isLoaded() { + return mLoaded; + } + + /** + * Logs the record of the watched channel. + */ + public void logChannelViewStop(Channel channel, long endTime, long duration) { + if (duration < MIN_DURATION_MS) { + return; + } + WatchedRecord record = new WatchedRecord(channel.getId(), endTime - duration, duration); + if (mLoaded) { + if (DEBUG) Log.d(TAG, "Log a watched record. " + record); + mWatchedHistory.add(record); + ++mLastIndex; + mSharedPreferences.edit() + .putString(getSharedPreferencesKey(mLastIndex), encode(record)) + .putLong(PREF_KEY_LAST_INDEX, mLastIndex) + .apply(); + if (mListener != null) { + mListener.onNewRecordAdded(record); + } + } else { + mPendingRecords.add(record); + } + } + + /** + * Sets {@link Listener}. + */ + public void setListener(Listener listener) { + mListener = listener; + } + + /** + * Returns watched history in the ascending order of time. In other words, the first element + * is the oldest and the last element is the latest record. + */ + public List<WatchedRecord> getWatchedHistory() { + return Collections.unmodifiableList(mWatchedHistory); + } + + /** + * Returns the list of recently watched channels. + */ + public List<Channel> buildRecentChannel(ChannelDataManager channelDataManager, int maxCount) { + List<Channel> list = new ArrayList<>(); + Map<Long, Long> durationMap = new HashMap<>(); + for (int i = mWatchedHistory.size() - 1; i >= 0; --i) { + WatchedRecord record = mWatchedHistory.get(i); + long channelId = record.channelId; + Channel channel = channelDataManager.getChannel(channelId); + if (channel == null || !channel.isBrowsable()) { + continue; + } + Long duration = durationMap.get(channelId); + if (duration == null) { + duration = 0l; + } + if (duration >= RECENT_CHANNEL_THRESHOLD_MS) { + continue; + } + if (list.isEmpty()) { + // We put the first recent channel regardless of RECENT_CHANNEL_THREASHOLD. + // It has the similar functionality as the previous channel in a usual remote + // controller. + list.add(channel); + durationMap.put(channelId, RECENT_CHANNEL_THRESHOLD_MS); + } else { + duration += record.duration; + durationMap.put(channelId, duration); + if (duration >= RECENT_CHANNEL_THRESHOLD_MS) { + list.add(channel); + } + } + if (list.size() >= maxCount) { + break; + } + } + if (DEBUG) { + Log.d(TAG, "Build recent channel"); + for (Channel channel : list) { + Log.d(TAG, "recent channel: " + channel); + } + } + return list; + } + + @VisibleForTesting + WatchedRecord getRecord(int reverseIndex) { + return mWatchedHistory.get(mWatchedHistory.size() - 1 - reverseIndex); + } + + @VisibleForTesting + WatchedRecord getRecordFromSharedPreferences(int reverseIndex) { + long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1); + long index = lastIndex - reverseIndex; + return decode(mSharedPreferences.getString(getSharedPreferencesKey(index), null)); + } + + private String getSharedPreferencesKey(long index) { + return Long.toString(index % mMaxHistorySize); + } + + public static class WatchedRecord { + public final long channelId; + public final long watchedStartTime; + public final long duration; + + WatchedRecord(long channelId, long watchedStartTime, long duration) { + this.channelId = channelId; + this.watchedStartTime = watchedStartTime; + this.duration = duration; + } + + @Override + public String toString() { + return "WatchedRecord: id=" + channelId + ",watchedStartTime=" + watchedStartTime + + ",duration=" + duration; + } + + @Override + public boolean equals(Object o) { + if (o instanceof WatchedRecord) { + WatchedRecord that = (WatchedRecord) o; + return Objects.equals(channelId, that.channelId) + && Objects.equals(watchedStartTime, that.watchedStartTime) + && Objects.equals(duration, that.duration); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(channelId, watchedStartTime, duration); + } + } + + @VisibleForTesting + String encode(WatchedRecord record) { + return record.channelId + " " + record.watchedStartTime + " " + record.duration; + } + + @VisibleForTesting + WatchedRecord decode(String encodedString) { + try (Scanner scanner = new Scanner(encodedString)) { + long channelId = scanner.nextLong(); + long watchedStartTime = scanner.nextLong(); + long duration = scanner.nextLong(); + return new WatchedRecord(channelId, watchedStartTime, duration); + } catch (Exception e) { + return null; + } + } + + public interface Listener { + /** + * Called when history is loaded. + */ + void onLoadFinished(); + void onNewRecordAdded(WatchedRecord watchedRecord); + } +} diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java index 99da84b0..1339ddf8 100644 --- a/src/com/android/tv/guide/ProgramGrid.java +++ b/src/com/android/tv/guide/ProgramGrid.java @@ -16,20 +16,19 @@ package com.android.tv.guide; -import com.android.tv.R; -import com.android.tv.ui.OnRepeatedKeyInterceptListener; - import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.support.v17.leanback.widget.VerticalGridView; import android.util.AttributeSet; import android.util.Log; -import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import com.android.tv.R; +import com.android.tv.ui.OnRepeatedKeyInterceptListener; + import java.util.ArrayList; import java.util.concurrent.TimeUnit; diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index 468f10e0..d4e4a99d 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -38,12 +38,12 @@ import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.View.MeasureSpec; -import android.view.View.OnScrollChangeListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import com.android.tv.ChannelTuner; +import com.android.tv.Features; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.analytics.DurationTimer; @@ -53,8 +53,6 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.ProgramDataManager; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; -import com.android.tv.ui.OnRepeatedKeyInterceptListener; -import com.android.tv.util.SystemProperties; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -214,7 +212,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mSidePanelGridView.setWindowAlignmentOffsetPercent( VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); // TODO: Remove this check when we ship TV with epg search enabled. - if (SystemProperties.USE_EPG_SEARCH.getValue()) { + if (Features.EPG_SEARCH.isEnabled(mActivity)) { mSearchOrb = (SearchOrbView) mContainer.findViewById( R.id.program_guide_side_panel_search_orb); mSearchOrb.setVisibility(View.VISIBLE); diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index 1babc255..57678c05 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -151,13 +151,13 @@ public class ProgramItemView extends TextView { sItemPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_item_padding); - ColorStateList programTitleColor = ColorStateList.valueOf(res.getColor( + ColorStateList programTitleColor = ColorStateList.valueOf(Utils.getColor(res, R.color.program_guide_table_item_program_title_text_color)); - ColorStateList grayedOutProgramTitleColor = res.getColorStateList( + ColorStateList grayedOutProgramTitleColor = Utils.getColorStateList(res, R.color.program_guide_table_item_grayed_out_program_text_color); - ColorStateList episodeTitleColor = ColorStateList.valueOf(res.getColor( + ColorStateList episodeTitleColor = ColorStateList.valueOf(Utils.getColor(res, R.color.program_guide_table_item_program_episode_title_text_color)); - ColorStateList grayedOutEpisodeTitleColor = ColorStateList.valueOf(res.getColor( + ColorStateList grayedOutEpisodeTitleColor = ColorStateList.valueOf(Utils.getColor(res, R.color.program_guide_table_item_grayed_out_program_episode_title_text_color)); int programTitleSize = res.getDimensionPixelSize( R.dimen.program_guide_table_item_program_title_font_size); diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index fde903d1..216fcf3c 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -23,6 +23,7 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; +import com.android.tv.util.CollectionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -30,6 +31,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -163,43 +165,53 @@ public class ProgramManager { // Should be matched with mSelectedGenreId always. private List<Channel> mFilteredChannels = mChannels; - private final List<Listener> mListeners = new ArrayList<>(); - private final List<TableEntriesUpdatedListener> - mTableEntriesUpdatedListeners = new ArrayList<>(); + private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); + private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = CollectionUtils + .createSmallSet(); + + private final ChannelDataManager.Listener mChannelDataManagerListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + updateChannels(true, false); + } + + @Override + public void onChannelListUpdated() { + updateChannels(true, false); + } + + @Override + public void onChannelBrowsableChanged() { + updateChannels(true, false); + } + }; + + private final ProgramDataManager.Listener mProgramDataManagerListener = + new ProgramDataManager.Listener() { + @Override + public void onProgramUpdated() { + updateTableEntries(true, true); + } + }; public ProgramManager(TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, ProgramDataManager programDataManager) { mTvInputManagerHelper = tvInputManagerHelper; mChannelDataManager = channelDataManager; - mChannelDataManager.addListener(new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - updateChannels(true, false); - } - - @Override - public void onChannelListUpdated() { - updateChannels(true, false); - } - - @Override - public void onChannelBrowsableChanged() { - updateChannels(true, false); - } - }); - mProgramDataManager = programDataManager; - mProgramDataManager.addListener(new ProgramDataManager.Listener() { - @Override - public void onProgramUpdated() { - updateTableEntries(true, true); - } - }); } public void programGuideVisibilityChanged(boolean visible) { mProgramDataManager.setPauseProgramUpdate(visible); + if (visible) { + mChannelDataManager.addListener(mChannelDataManagerListener); + mProgramDataManager.addListener(mProgramDataManagerListener); + } else { + mChannelDataManager.removeListener(mChannelDataManagerListener); + mProgramDataManager.removeListener(mProgramDataManagerListener); + } } /** diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index df764668..cd0e9611 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -101,13 +101,13 @@ public class ProgramTableAdapter extends R.string.program_title_for_no_information); mProgramTitleForBlockedChannel = res.getString( R.string.program_title_for_blocked_channel); - mChannelTextColor = res.getColor( + mChannelTextColor = Utils.getColor(res, R.color.program_guide_table_header_column_channel_number_text_color); - mChannelBlockedTextColor = res.getColor(R.color - .program_guide_table_header_column_channel_number_blocked_text_color); - mDetailTextColor = res.getColor( + mChannelBlockedTextColor = Utils.getColor(res, + R.color.program_guide_table_header_column_channel_number_blocked_text_color); + mDetailTextColor = Utils.getColor(res, R.color.program_guide_table_detail_title_text_color); - mDetailGrayedTextColor = res.getColor( + mDetailGrayedTextColor = Utils.getColor(res, R.color.program_guide_table_detail_title_grayed_text_color); mAnimationDuration = res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration); @@ -117,7 +117,7 @@ public class ProgramTableAdapter extends int episodeTitleSize = res.getDimensionPixelSize( R.dimen.program_guide_table_detail_episode_title_text_size); ColorStateList episodeTitleColor = ColorStateList.valueOf( - res.getColor(R.color.program_guide_table_detail_episode_title_text_color)); + Utils.getColor(res, R.color.program_guide_table_detail_episode_title_text_color)); mEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize, episodeTitleColor, null); diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java index 19f22cc1..54564403 100644 --- a/src/com/android/tv/menu/AppLinkCardView.java +++ b/src/com/android/tv/menu/AppLinkCardView.java @@ -38,6 +38,7 @@ import com.android.tv.R; import com.android.tv.data.Channel; import com.android.tv.util.BitmapUtils; import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.Utils; /** * A view to render an app link card. @@ -94,7 +95,7 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo R.dimen.card_meta_layout_height); mExtendedTextViewCardHeight = getResources().getDimensionPixelOffset( R.dimen.card_meta_layout_height_extended); - mIconColorFilter = getResources().getColor(R.color.app_link_card_icon_color_filter, null); + mIconColorFilter = Utils.getColor(getResources(), R.color.app_link_card_icon_color_filter); } /** @@ -107,7 +108,7 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo @Override public void onBind(Channel channel, boolean selected) { if (DEBUG) { - Log.d(TAG, "onBind(channel=" + channel.getDisplayName() + ", selected=" + selected + Log.d(TAG, "onBind(channelName=" + channel.getDisplayName() + ", selected=" + selected + ")"); } mChannel = channel; @@ -241,27 +242,26 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo } // Try to set the card image with following order: - // 1) Provided poster art image, 2) Activity banner, 3) Application banner, - // 4) Activity logo, 5) Application logo, and 6) default image. + // 1) Provided poster art image, 2) Activity banner, 3) Activity icon, 4) Application banner, + // 5) Application icon, and 6) default image. private void setCardImageWithBanner(ApplicationInfo appInfo) { Drawable banner = null; try { banner = mPackageManager.getActivityBanner(mIntent); + if (banner == null) { + banner = mPackageManager.getActivityIcon(mIntent); + } } catch (PackageManager.NameNotFoundException e) { // do nothing. } - if (banner == null && appInfo != null && appInfo.banner != 0) { - banner = mPackageManager.getApplicationBanner(appInfo); - } - if (banner == null) { - try { - banner = mPackageManager.getActivityLogo(mIntent); - } catch (PackageManager.NameNotFoundException e) { - // do nothing. + + if (banner == null && appInfo != null) { + if (appInfo.banner != 0) { + banner = mPackageManager.getApplicationBanner(appInfo); + } + if (banner == null && appInfo.icon != 0) { + banner = mPackageManager.getApplicationIcon(appInfo); } - } - if (banner == null && appInfo != null && appInfo.logo != 0) { - banner = mPackageManager.getApplicationLogo(appInfo); } if (banner == null) { @@ -285,7 +285,7 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo @Override public void onGenerated(Palette palette) { mMetaViewHolder.setBackgroundColor(palette.getDarkVibrantColor( - getResources().getColor(R.color.channel_card_meta_background, null))); + Utils.getColor(getResources(), R.color.channel_card_meta_background))); } }); } diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java index d6086910..ea4f31e9 100644 --- a/src/com/android/tv/menu/ChannelCardView.java +++ b/src/com/android/tv/menu/ChannelCardView.java @@ -96,13 +96,17 @@ public class ChannelCardView extends BaseCardView<Channel> implements @Override public void onBind(Channel channel, boolean selected) { if (DEBUG) { - Log.d(TAG, "onBind(channel=" + channel.getDisplayName() + ", selected=" + selected + Log.d(TAG, "onBind(channelName=" + channel.getDisplayName() + ", selected=" + selected + ")"); } mChannel = channel; mProgram = null; - mChannelNumberNameView.setText(mChannel.getDisplayNumber() + " " - + mChannel.getDisplayName()); + if (TextUtils.isEmpty(mChannel.getDisplayName())) { + mChannelNumberNameView.setText(mChannel.getDisplayNumber()); + } else { + mChannelNumberNameView.setText(mChannel.getDisplayNumber() + " " + + mChannel.getDisplayName()); + } mChannelNumberNameView.setVisibility(VISIBLE); mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default); mImageView.setBackgroundResource(R.color.channel_card); diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java index b008fa65..d576342c 100644 --- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java +++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java @@ -27,7 +27,7 @@ import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; -import com.android.tv.util.Utils; +import com.android.tv.util.SoftPreconditions; import java.util.List; @@ -68,8 +68,8 @@ public class ChannelsPosterPrefetcher { * Start prefetching of program poster art of recommendation. */ public void prefetch() { + SoftPreconditions.checkState(!isCanceled, TAG, "Prefetch called after cancel was called."); if (isCanceled) { - Utils.engThrowElseWarn(TAG, "Prefetch called after cancel was called."); return; } if (DEBUG) { diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index 8190c976..7a1eacf3 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -30,7 +30,6 @@ import com.android.tv.recommendation.Recommender; import com.android.tv.util.SetupUtils; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -187,7 +186,6 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> } } } - - return Collections.unmodifiableList(channelList); + return channelList; } } diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java index 323ce9c5..1f33bd67 100644 --- a/src/com/android/tv/menu/Menu.java +++ b/src/com/android/tv/menu/Menu.java @@ -132,7 +132,7 @@ public class Menu { mHideAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mMenuView.onHide(); + hideInternal(); } }); mHideAnimator.setTarget(mMenuView); @@ -222,11 +222,15 @@ public class Menu { // mMenuView.onHide() is called in AnimatorListener. mHideAnimator.end(); } else { - mMenuView.onHide(); - mTracker.sendHideMenu(mVisibleTimer.reset()); - if (mOnMenuVisibilityChangeListener != null) { - mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false); - } + hideInternal(); + } + } + + private void hideInternal() { + mMenuView.onHide(); + mTracker.sendHideMenu(mVisibleTimer.reset()); + if (mOnMenuVisibilityChangeListener != null) { + mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false); } } diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java index 187d0e14..265ad840 100644 --- a/src/com/android/tv/menu/MenuLayoutManager.java +++ b/src/com/android/tv/menu/MenuLayoutManager.java @@ -35,6 +35,7 @@ import android.view.ViewGroup.MarginLayoutParams; import android.widget.TextView; import com.android.tv.R; +import com.android.tv.util.SoftPreconditions; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -312,16 +313,16 @@ public class MenuLayoutManager { if (mSelectedPosition == position) { return; } - if (position < 0 || position >= mMenuRowViews.size()) { - String msg = "Invalid position: " + position; - Utils.engThrowElseWarn(TAG, msg, new IllegalArgumentException(msg)); + boolean indexValid = Utils.isIndexValid(mMenuRowViews, position); + SoftPreconditions.checkArgument(indexValid, TAG, "position " + position); + if (!indexValid) { return; } - if (mSelectedPosition >= 0 && mSelectedPosition < mMenuRowViews.size()) { + if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) { mMenuRowViews.get(mSelectedPosition).onDeselected(); } mSelectedPosition = position; - if (mSelectedPosition >= 0 && mSelectedPosition < mMenuRowViews.size()) { + if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) { mMenuRowViews.get(mSelectedPosition).onSelected(false); } if (mMenuView.getVisibility() == View.VISIBLE) { @@ -348,14 +349,15 @@ public class MenuLayoutManager { if (mSelectedPosition == position) { return; } - if (mSelectedPosition < 0 || mSelectedPosition >= mMenuRowViews.size()) { - String msg = "No previous selection: " + mSelectedPosition; - Utils.engThrowElseWarn(TAG, msg, new IllegalStateException(msg)); + boolean oldIndexValid = Utils.isIndexValid(mMenuRowViews, mSelectedPosition); + SoftPreconditions + .checkState(oldIndexValid, TAG, "No previous selection: " + mSelectedPosition); + if (!oldIndexValid) { return; } - if (position < 0 || position >= mMenuRowViews.size()) { - String msg = "Invalid position: " + position; - Utils.engThrowElseWarn(TAG, msg, new IllegalArgumentException(msg)); + boolean newIndexValid = Utils.isIndexValid(mMenuRowViews, position); + SoftPreconditions.checkArgument(newIndexValid, TAG, "position " + position); + if (!newIndexValid) { return; } if (mAnimatorSet != null) { @@ -629,8 +631,8 @@ public class MenuLayoutManager { if (mMenuView.getVisibility() != View.VISIBLE) { int count = mMenuRowViews.size(); for (int i = 0; i < count; ++i) { - mMenuRowViews.get(i).setVisibility(mMenuRows.get(i).isVisible() ? View.VISIBLE - : View.GONE); + mMenuRowViews.get(i) + .setVisibility(mMenuRows.get(i).isVisible() ? View.VISIBLE : View.GONE); } return; } diff --git a/src/com/android/tv/menu/MenuRowView.java b/src/com/android/tv/menu/MenuRowView.java index a6d8c990..6b3b6b5f 100644 --- a/src/com/android/tv/menu/MenuRowView.java +++ b/src/com/android/tv/menu/MenuRowView.java @@ -236,7 +236,11 @@ public abstract class MenuRowView extends LinearLayout { mTitleView.setScaleX(mTitleViewScaleSelected); mTitleView.setScaleY(mTitleViewScaleSelected); } + // Making the content view visible will cause it to set a focus item + // So we store mLastFocusView and reset it + View lastFocusView = mLastFocusView; mContentsView.setVisibility(VISIBLE); + mLastFocusView = lastFocusView; } /** diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java index b7814fa5..1977dde1 100644 --- a/src/com/android/tv/menu/TvOptionsRowAdapter.java +++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java @@ -29,6 +29,7 @@ import com.android.tv.ui.sidepanel.AboutFragment; import com.android.tv.ui.sidepanel.ClosedCaptionFragment; import com.android.tv.ui.sidepanel.DisplayModeFragment; import com.android.tv.ui.sidepanel.MultiAudioFragment; +import com.android.tv.util.PermissionUtils; import com.android.tv.util.PipInputManager; import java.util.ArrayList; @@ -54,7 +55,13 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { mPositionPipAction = actionList.size() - 1; actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION); actionList.add(MenuAction.CHANNEL_SOURCES_ACTION); - actionList.add(MenuAction.PARENTAL_CONTROLS_ACTION); + if (PermissionUtils.hasModifyParentalControls(getMainActivity())) { + actionList.add(MenuAction.PARENTAL_CONTROLS_ACTION); + } else { + // Note: parental control is turned off, when MODIFY_PARENTAL_CONTROLS is not granted. + // But, we may be able to turn on channel lock feature regardless of the permission. + // It's TBD. + } actionList.add(MenuAction.ABOUT_ACTION); for (MenuAction action : actionList) { diff --git a/src/com/android/tv/onboarding/AppOverviewFragment.java b/src/com/android/tv/onboarding/AppOverviewFragment.java new file mode 100644 index 00000000..3427b122 --- /dev/null +++ b/src/com/android/tv/onboarding/AppOverviewFragment.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.onboarding; + +import android.app.Fragment; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.ui.setup.SetupGuidedStepFragment; +import com.android.tv.common.ui.setup.SetupMultiPaneFragment; + +import java.util.List; + +/** + * A fragment for channel source info/setup. + */ +public class AppOverviewFragment extends SetupMultiPaneFragment { + public static final int ACTION_SETUP_SOURCE = 1; + public static final int ACTION_GET_MORE_CHANNELS = 2; + public static final int ACTION_SETUP_USB_TUNER = 3; + + public static final String KEY_AC3_SUPPORT = "key_ac3_support"; + + private boolean mAc3Supported; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + Bundle bundle = getArguments(); + mAc3Supported = bundle.getBoolean(KEY_AC3_SUPPORT); + return view; + } + + @Override + protected Fragment getContentFragment() { + return new ContentFragment(); + } + + @Override + protected boolean needsDoneButton() { + return false; + } + + // AppOverviewFragment should inherit OnboardingPageFragment for animation and command execution + // purpose. So child fragment which inherits GuidedStepFragment is needed. + private class ContentFragment extends SetupGuidedStepFragment { + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getString(R.string.app_overview_text); + String description = mAc3Supported + ? getString(R.string.app_overview_description_has_ac3) + : getString(R.string.app_overview_description_no_ac3); + return new Guidance(title, description, null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + boolean hasTvInput = ((TvApplication) getActivity().getApplicationContext()) + .getTvInputManagerHelper().getTunerTvInputSize() > 0; + Resources res = getResources(); + if (hasTvInput) { + actions.add(new GuidedAction.Builder() + .id(ACTION_SETUP_SOURCE) + .title(res.getString(R.string.app_overview_action_text_setup_source)) + .description(res.getString( + R.string.app_overview_action_description_setup_source)) + .build()); + } + actions.add(new GuidedAction.Builder() + .id(ACTION_GET_MORE_CHANNELS) + .title(res.getString(R.string.app_overview_action_text_play_store)) + .description(res.getString(R.string.app_overview_action_description_play_store)) + .build()); + if (mAc3Supported) { + actions.add(new GuidedAction.Builder() + .id(ACTION_SETUP_USB_TUNER) + .title(res.getString(R.string.app_overview_action_text_usb_tuner)) + .description(res.getString( + R.string.app_overview_action_description_usb_tuner)) + .build()); + } + } + } +} diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java new file mode 100644 index 00000000..4176a70f --- /dev/null +++ b/src/com/android/tv/onboarding/OnboardingActivity.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.onboarding; + +import android.app.Fragment; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.media.tv.TvInputInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.widget.Toast; + +import com.android.tv.R; +import com.android.tv.common.WeakHandler; +import com.android.tv.common.ui.setup.SetupStep; +import com.android.tv.common.ui.setup.SteppedSetupActivity; +import com.android.tv.receiver.AudioCapabilitiesReceiver; +import com.android.tv.util.OnboardingUtils; +import com.android.tv.util.SetupUtils; +import com.android.tv.util.SoftPreconditions; +import com.android.tv.util.Utils; + +import java.util.concurrent.TimeUnit; + +public class OnboardingActivity extends SteppedSetupActivity { + private static final String TAG = "OnboardingActivity"; + + private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion"; + + private static final int MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION = 1; + private static final long AC3_CHECK_WAIT_TIMEOUT = TimeUnit.SECONDS.toMillis(1); + + private static final int REQUEST_CODE_SETUP_USB_TUNER = 1; + + private Handler mHandler = new OnboardingActivityHandler(this); + private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; + private Boolean mAc3Supported; + + /** + * Returns an intent to start {@link OnboardingActivity}. + * + * @param context context to create an intent. Should not be {@code null}. + * @param intentAfterCompletion intent which will be used to start a new activity when this + * activity finishes. Should not be {@code null}. + */ + public static Intent buildIntent(@NonNull Context context, + @NonNull Intent intentAfterCompletion) { + return new Intent(context, OnboardingActivity.class) + .putExtra(OnboardingActivity.KEY_INTENT_AFTER_COMPLETION, intentAfterCompletion); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Register a receiver for HDMI audio plug and wait for the response. + mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, + new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() { + @Override + public void onAc3PassthroughCapabilityChange(boolean capability) { + mAudioCapabilitiesReceiver.unregister(); + mAudioCapabilitiesReceiver = null; + mHandler.removeMessages(MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION); + mAc3Supported = capability; + startFirstStep(); + } + }); + mAudioCapabilitiesReceiver.register(); + mHandler.sendEmptyMessageDelayed(MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION, + AC3_CHECK_WAIT_TIMEOUT); + } + + @Override + protected SetupStep onCreateInitialStep() { + if (mAc3Supported == null) { + return null; + } + if (OnboardingUtils.isFirstRun(this)) { + return new WelcomeStep(null); + } + return new AppOverviewStep(null); + } + + @Override + protected void onDestroy() { + mHandler.removeCallbacksAndMessages(null); + if (mAudioCapabilitiesReceiver != null) { + mAudioCapabilitiesReceiver.unregister(); + mAudioCapabilitiesReceiver = null; + } + super.onDestroy(); + } + + void startFirstStep() { + SoftPreconditions.checkNotNull(mAc3Supported, TAG, + "AC3 passthrough support check hasn't been completed yet."); + startInitialStep(); + } + + + private static class OnboardingActivityHandler extends WeakHandler<OnboardingActivity> { + OnboardingActivityHandler(OnboardingActivity activity) { + // Should run on main thread because onAc3SupportChanged will be called on main thread. + super(Looper.getMainLooper(), activity); + } + + @Override + protected void handleMessage(Message msg, OnboardingActivity activity) { + if (msg.what == MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION) { + activity.mAudioCapabilitiesReceiver.unregister(); + activity.mAudioCapabilitiesReceiver = null; + activity.startFirstStep(); + } + } + } + + void finishActivity() { + Intent intentForNextActivity = (Intent) getIntent().getParcelableExtra( + KEY_INTENT_AFTER_COMPLETION); + if (intentForNextActivity != null) { + startActivity(intentForNextActivity); + } + finish(); + } + + private class WelcomeStep extends SetupStep { + public WelcomeStep(@Nullable SetupStep previousStep) { + super(getFragmentManager(), previousStep); + } + + @Override + public Fragment onCreateFragment() { + return new WelcomeFragment(); + } + + @Override + protected boolean needsToBeAddedToBackStack() { + return false; + } + + @Override + protected boolean needsFragmentTransitionAnimation() { + return false; + } + + @Override + public void executeAction(int actionId) { + switch (actionId) { + case WelcomeFragment.ACTION_NEXT: + OnboardingUtils.setFirstRunCompleted(OnboardingActivity.this); + if (!OnboardingUtils.areChannelsAvailable(OnboardingActivity.this)) { + startStep(new AppOverviewStep(this)); + } else { + // TODO: Go to the correct step. + finishActivity(); + } + break; + } + } + } + + private class AppOverviewStep extends SetupStep { + public AppOverviewStep(@Nullable SetupStep previousStep) { + super(getFragmentManager(), previousStep); + } + + @Override + public Fragment onCreateFragment() { + Fragment fragment = new AppOverviewFragment(); + Bundle bundle = new Bundle(); + bundle.putBoolean(AppOverviewFragment.KEY_AC3_SUPPORT, mAc3Supported); + fragment.setArguments(bundle); + return fragment; + } + + @Override + protected boolean needsToBeAddedToBackStack() { + return false; + } + + @Override + public void executeAction(int actionId) { + switch (actionId) { + case AppOverviewFragment.ACTION_SETUP_SOURCE: + startStep(new SetupSourcesStep(this)); + break; + case AppOverviewFragment.ACTION_GET_MORE_CHANNELS: + // TODO: Implement this. + Toast.makeText(OnboardingActivity.this, "Not implemented yet.", + Toast.LENGTH_SHORT).show(); + break; + } + } + } + + private class SetupSourcesStep extends SetupStep { + public SetupSourcesStep(@Nullable SetupStep previousStep) { + super(getFragmentManager(), previousStep); + } + + @Override + public Fragment onCreateFragment() { + return new SetupSourcesFragment(); + } + + @Override + public void executeAction(int actionId) { + switch (actionId) { + case SetupSourcesFragment.ACTION_DONE: + finishActivity(); + break; + } + } + } +} diff --git a/src/com/android/tv/onboarding/PagingIndicator.java b/src/com/android/tv/onboarding/PagingIndicator.java new file mode 100644 index 00000000..128fa996 --- /dev/null +++ b/src/com/android/tv/onboarding/PagingIndicator.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.onboarding; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import com.android.tv.R; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * A page indicator with dots. + */ +public class PagingIndicator extends View { + // attribute + private final int mDotDiameter; + private final int mDotRadius; + private final int mDotGap; + private int[] mDotCenterX; + private int mDotCenterY; + + // state + private int mPageCount; + private int mCurrentPage; + private int mPreviousPage; + + // drawing + private final Paint mUnselectedPaint; + private final Paint mSelectedPaint; + private final Paint mUnselectingPaint; + private final Paint mSelectingPaint; + private final AnimatorSet mAnimator = new AnimatorSet(); + + public PagingIndicator(Context context) { + this(context, null, 0); + } + + public PagingIndicator(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagingIndicator(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + Resources res = getResources(); + mDotRadius = res.getDimensionPixelSize(R.dimen.onboarding_dot_radius); + mDotDiameter = mDotRadius * 2; + mDotGap = res.getDimensionPixelSize(R.dimen.onboarding_dot_gap); + mUnselectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + // Deprecated method is used because this code should run on L platform. + int unselectedColor = Utils.getColor(res, R.color.onboarding_dot_unselected); + int selectedColor = Utils.getColor(res, R.color.onboarding_dot_selected); + mUnselectedPaint.setColor(unselectedColor); + mSelectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mSelectedPaint.setColor(selectedColor); + mUnselectingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mSelectingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + // Initialize animations. + int duration = res.getInteger(R.integer.setup_slide_anim_duration); + List<Animator> animators = new ArrayList<>(); + animators.add(createColorAnimator(selectedColor, unselectedColor, duration, + mUnselectingPaint)); + animators.add(createColorAnimator(unselectedColor, selectedColor, duration, + mSelectingPaint)); + mAnimator.playTogether(animators); + } + + private Animator createColorAnimator(int fromColor, int toColor, int duration, + final Paint paint) { + ValueAnimator animator = ValueAnimator.ofArgb(fromColor, toColor); + animator.setDuration(duration); + animator.setInterpolator(new DecelerateInterpolator()); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + invalidate(); + } + }); + animator.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + paint.setColor((int) animator.getAnimatedValue()); + invalidate(); + } + }); + return animator; + } + + /** + * Sets the page count. + */ + public void setPageCount(int pages) { + mPageCount = pages; + calculateDotPositions(); + setSelectedPage(0); + } + + /** + * Called when the page has been selected. + */ + public void onPageSelected(int pageIndex, boolean withAnimation) { + if (mAnimator.isStarted()) { + mAnimator.end(); + } + if (withAnimation) { + mPreviousPage = mCurrentPage; + mAnimator.start(); + } + setSelectedPage(pageIndex); + } + + private void calculateDotPositions() { + int left = getPaddingLeft(); + int top = getPaddingTop(); + int right = getWidth() - getPaddingRight(); + int requiredWidth = getRequiredWidth(); + int startLeft = left + ((right - left - requiredWidth) / 2) + mDotRadius; + mDotCenterX = new int[mPageCount]; + for (int i = 0; i < mPageCount; i++) { + mDotCenterX[i] = startLeft + i * (mDotDiameter + mDotGap); + } + mDotCenterY = top + mDotRadius; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int desiredHeight = getDesiredHeight(); + int height; + switch (MeasureSpec.getMode(heightMeasureSpec)) { + case MeasureSpec.EXACTLY: + height = MeasureSpec.getSize(heightMeasureSpec); + break; + case MeasureSpec.AT_MOST: + height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec)); + break; + case MeasureSpec.UNSPECIFIED: + default: + height = desiredHeight; + break; + } + int desiredWidth = getDesiredWidth(); + int width; + switch (MeasureSpec.getMode(widthMeasureSpec)) { + case MeasureSpec.EXACTLY: + width = MeasureSpec.getSize(widthMeasureSpec); + break; + case MeasureSpec.AT_MOST: + width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec)); + break; + case MeasureSpec.UNSPECIFIED: + default: + width = desiredWidth; + break; + } + setMeasuredDimension(width, height); + calculateDotPositions(); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + setMeasuredDimension(width, height); + calculateDotPositions(); + } + + private int getDesiredHeight() { + return getPaddingTop() + mDotDiameter + getPaddingBottom(); + } + + private int getRequiredWidth() { + return mPageCount * mDotDiameter + (mPageCount - 1) * mDotGap; + } + + private int getDesiredWidth() { + return getPaddingLeft() + getRequiredWidth() + getPaddingRight(); + } + + @Override + protected void onDraw(Canvas canvas) { + drawUnselected(canvas); + if (mAnimator.isStarted()) { + drawAnimator(canvas); + } else { + drawSelected(canvas); + } + } + + private void drawUnselected(Canvas canvas) { + for (int page = 0; page < mPageCount; page++) { + canvas.drawCircle(mDotCenterX[page], mDotCenterY, mDotRadius, mUnselectedPaint); + } + } + + private void drawSelected(Canvas canvas) { + canvas.drawCircle(mDotCenterX[mCurrentPage], mDotCenterY, mDotRadius, mSelectedPaint); + } + + private void drawAnimator(Canvas canvas) { + canvas.drawCircle(mDotCenterX[mPreviousPage], mDotCenterY, mDotRadius, mUnselectingPaint); + canvas.drawCircle(mDotCenterX[mCurrentPage], mDotCenterY, mDotRadius, mSelectingPaint); + } + + private void setSelectedPage(int now) { + if (now == mCurrentPage) { + return; + } + mCurrentPage = now; + invalidate(); + } +} diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java new file mode 100644 index 00000000..3572a209 --- /dev/null +++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.onboarding; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.common.ui.setup.SetupGuidedStepFragment; +import com.android.tv.common.ui.setup.SetupMultiPaneFragment; + +import java.util.List; + +/** + * A fragment for channel source info/setup. + */ +public class SetupSourcesFragment extends SetupMultiPaneFragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + setOnClickAction(view.findViewById(R.id.button_done), ACTION_DONE); + return view; + } + + @Override + protected Fragment getContentFragment() { + return new ContentFragment(); + } + + private class ContentFragment extends SetupGuidedStepFragment { + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getString(R.string.setup_sources_text); + String description = getString(R.string.setup_sources_description); + return new Guidance(title, description, null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder().id(ACTION_DONE).title("Done") + .description("Return to previous page").build()); + } + } +} diff --git a/src/com/android/tv/onboarding/WelcomeFragment.java b/src/com/android/tv/onboarding/WelcomeFragment.java new file mode 100644 index 00000000..baeb1b29 --- /dev/null +++ b/src/com/android/tv/onboarding/WelcomeFragment.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.onboarding; + +import android.app.FragmentTransaction; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; + +import com.android.tv.R; +import com.android.tv.common.ui.setup.SetupFragment; + +/** + * A fragment for the onboarding screen. + */ +public class WelcomeFragment extends SetupFragment { + public static final int ACTION_NEXT = 1; + + private int mNumPages; + private String[] mPageTitles; + private String[] mPageDescriptions; + private int mCurrentPageIndex; + + private PagingIndicator mPageIndicator; + private Button mButton; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + mPageTitles = getResources().getStringArray(R.array.welcome_page_titles); + mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions); + mNumPages = mPageTitles.length; + mCurrentPageIndex = 0; + mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator); + mPageIndicator.setPageCount(mNumPages); + mButton = (Button) view.findViewById(R.id.button); + mButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (mCurrentPageIndex == mNumPages - 1) { + onActionClick(ACTION_NEXT); + } else { + showPage(++mCurrentPageIndex); + } + } + }); + showPage(mCurrentPageIndex); + return view; + } + + @Override + protected int getLayoutResourceId() { + return R.layout.fragment_welcome; + } + + /* + * Should return {@link SetupFragment} for the custom animations. + */ + private SetupFragment getPage(int index) { + Bundle args = new Bundle(); + args.putString(WelcomePageFragment.KEY_TITLE, mPageTitles[index]); + args.putString(WelcomePageFragment.KEY_DESCRIPTION, mPageDescriptions[index]); + SetupFragment fragment = new WelcomePageFragment(); + fragment.setArguments(args); + return fragment; + } + + private void showPage(int pageIndex) { + SetupFragment fragment = getPage(pageIndex); + FragmentTransaction ft = getFragmentManager().beginTransaction(); + if (pageIndex != 0) { + ft.setCustomAnimations(SetupFragment.ANIM_ENTER, + SetupFragment.ANIM_EXIT); + } + ft.replace(R.id.page_container, fragment).commit(); + if (pageIndex == mNumPages - 1) { + mButton.setText(R.string.welcome_start_button_text); + } else { + mButton.setText(R.string.welcome_next_button_text); + } + mPageIndicator.onPageSelected(pageIndex, pageIndex != 0); + } +} diff --git a/src/com/android/tv/onboarding/WelcomePageFragment.java b/src/com/android/tv/onboarding/WelcomePageFragment.java new file mode 100644 index 00000000..3c6cd679 --- /dev/null +++ b/src/com/android/tv/onboarding/WelcomePageFragment.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.onboarding; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.common.ui.setup.SetupFragment; + +/** + * A fragment for the onboarding screen. + */ +public class WelcomePageFragment extends SetupFragment { + public static final String KEY_TITLE = "key_title"; + public static final String KEY_DESCRIPTION = "key_description"; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + Bundle args = getArguments(); + ((TextView) view.findViewById(R.id.title)).setText(args.getString(KEY_TITLE)); + ((TextView) view.findViewById(R.id.description)).setText(args.getString(KEY_DESCRIPTION)); + return view; + } + + @Override + protected int getLayoutResourceId() { + return R.layout.fragment_welcome_page; + } +} diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java index 949222a9..55d3cf3a 100644 --- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java +++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java @@ -22,15 +22,17 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.media.AudioFormat; import android.media.AudioManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.android.tv.TvApplication; +import com.android.tv.analytics.Analytics; import com.android.tv.analytics.Tracker; -import java.util.Arrays; - /** - * Creates HDMI plug broadcast receiver, and reports AC3 passthrough capabilities - * to Google Analytics. Call {@link #register} to start receiving notifications, - * and {@link #unregister} to stop. + * Creates HDMI plug broadcast receiver, and reports AC3 passthrough capabilities to Google + * Analytics and listeners. Call {@link #register} to start receiving notifications, and + * {@link #unregister} to stop. */ public final class AudioCapabilitiesReceiver { private static final String PREFS_NAME = "com.android.tv.audio_capabilities"; @@ -45,18 +47,25 @@ public final class AudioCapabilitiesReceiver { private static final int REPORT_REVISION = 1; private final Context mContext; + private final Analytics mAnalytics; private final Tracker mTracker; + @Nullable + private final OnAc3PassthroughCapabilityChangeListener mListener; private final BroadcastReceiver mReceiver = new HdmiAudioPlugBroadcastReceiver(); /** * Constructs a new audio capabilities receiver. * * @param context context for registering to receive broadcasts - * @param tracker tracker object used to upload capabilities info to Google Analytics + * @param listener listener which receives AC3 passthrough capability change notification */ - public AudioCapabilitiesReceiver(Context context, Tracker tracker) { + public AudioCapabilitiesReceiver(@NonNull Context context, + @Nullable OnAc3PassthroughCapabilityChangeListener listener) { mContext = context; - mTracker = tracker; + TvApplication tvApplication = (TvApplication) context.getApplicationContext(); + mAnalytics = tvApplication.getAnalytics(); + mTracker = tvApplication.getTracker(); + mListener = listener; } public void register() { @@ -74,23 +83,36 @@ public final class AudioCapabilitiesReceiver { if (!action.equals(AudioManager.ACTION_HDMI_AUDIO_PLUG)) { return; } - reportAudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS)); + boolean supported = false; + int[] supportedEncodings = intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS); + if (supportedEncodings != null) { + for (int supportedEncoding : supportedEncodings) { + if (supportedEncoding == AudioFormat.ENCODING_AC3) { + supported = true; + break; + } + } + } + if (mListener != null) { + mListener.onAc3PassthroughCapabilityChange(supported); + } + if (!mAnalytics.isAppOptOut()) { + reportAudioCapabilities(supported); + } } } - private void reportAudioCapabilities(int[] supportedEncodings) { - boolean newVal = supportedEncodings != null - && Arrays.binarySearch(supportedEncodings, AudioFormat.ENCODING_AC3) >= 0; + private void reportAudioCapabilities(boolean ac3Supported) { boolean oldVal = getBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, false); boolean reported = getBoolean(SETTINGS_KEY_AC3_PASSTHRU_REPORTED, false); int revision = getInt(SETTINGS_KEY_AC3_REPORT_REVISION, 0); // Send the value just once. But we send it again if the value changed, to include // the case where users have switched TV device with different AC3 passthrough capabilities. - if (!reported || oldVal != newVal || REPORT_REVISION > revision) { - mTracker.sendAc3PassthroughCapabilities(newVal); + if (!reported || oldVal != ac3Supported || REPORT_REVISION > revision) { + mTracker.sendAc3PassthroughCapabilities(ac3Supported); setBoolean(SETTINGS_KEY_AC3_PASSTHRU_REPORTED, true); - setBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, newVal); + setBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, ac3Supported); if (REPORT_REVISION > revision) { setInt(SETTINGS_KEY_AC3_REPORT_REVISION, REPORT_REVISION); } @@ -116,4 +138,14 @@ public final class AudioCapabilitiesReceiver { private void setInt(String key, int val) { getSharedPreferences().edit().putInt(key, val).apply(); } + + /** + * Listener notified when AC3 passthrough capability changes. + */ + public interface OnAc3PassthroughCapabilityChangeListener { + /** + * Called when the AC3 passthrough capability changes. + */ + void onAc3PassthroughCapabilityChange(boolean capability); + } } diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java new file mode 100644 index 00000000..61a9baa9 --- /dev/null +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.receiver; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; + +import com.android.tv.Features; +import com.android.tv.TvActivity; +import com.android.tv.recommendation.NotificationService; +import com.android.tv.util.OnboardingUtils; +import com.android.tv.util.SetupUtils; + +/** + * Boot completed receiver. It's used to start the {@code NotificationService} for recommendation, + * grant permission to the TIS's and enable {@code TvActivity} if necessary. + */ +public class BootCompletedReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + // Start {@link NotificationService}. + Intent notificationIntent = new Intent(context, NotificationService.class); + notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION); + context.startService(notificationIntent); + + // Grant permission to already set up packages after the system has finished booting. + SetupUtils.grantEpgPermissionToSetUpPackages(context); + + // On-boarding experience. + if (Features.ONBOARDING_EXPERIENCE.isEnabled(context)) { + if (OnboardingUtils.isFirstBoot(context)) { + // Enable the application if this is the first run after the on-boarding experience + // is applied just in case when the app is disabled before. + PackageManager pm = context.getPackageManager(); + ComponentName name = new ComponentName(context, TvActivity.class); + if (pm.getComponentEnabledSetting(name) + != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + pm.setComponentEnabledSetting(name, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); + } + OnboardingUtils.setFirstBootCompleted(context); + } + } + } +} diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java index fd9d7baf..a6ab858d 100644 --- a/src/com/android/tv/receiver/PackageIntentsReceiver.java +++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java @@ -25,6 +25,7 @@ import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.os.Handler; +import com.android.tv.Features; import com.android.tv.TvActivity; import com.android.tv.util.SetupUtils; @@ -42,7 +43,8 @@ public class PackageIntentsReceiver extends BroadcastReceiver { private TvInputManager mTvInputManager; private final Handler mHandler = new Handler(); private Runnable mOnPackageUpdatedRunnable; - private boolean mPermissionGranted; + private PackageManager mPackageManager; + private ComponentName mTvActivityComponentName; private void init(Context context) { mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); @@ -51,27 +53,25 @@ public class PackageIntentsReceiver extends BroadcastReceiver { mOnPackageUpdatedRunnable = new Runnable() { @Override public void run() { - List<TvInputInfo> inputs = mTvInputManager.getTvInputList(); - // Enable the MainActivity only if there is at least one tuner type input. - boolean enable = false; - for (TvInputInfo input : inputs) { - if (input.getType() == TvInputInfo.TYPE_TUNER) { - enable = true; - break; + if (!Features.ONBOARDING_EXPERIENCE.isEnabled(applicationContext)) { + List<TvInputInfo> inputs = mTvInputManager.getTvInputList(); + // Enable the MainActivity only if there is at least one tuner type input. + boolean enable = false; + for (TvInputInfo input : inputs) { + if (input.getType() == TvInputInfo.TYPE_TUNER) { + enable = true; + break; + } } + enableTvActivityWithinPackageManager(applicationContext, enable); } - enableTvActivityWithinPackageManager(applicationContext, enable); SetupUtils.getInstance(applicationContext).onInputListUpdated(mTvInputManager); } }; - // Grant permission to already set up packages after the system has finished booting. (Note - // that the PackageIntentsReceiver filters the ACTION_BOOT_COMPLETED action.) - if (!mPermissionGranted) { - SetupUtils.grantEpgPermissionToSetUpPackages(applicationContext); - mPermissionGranted = true; - } + mPackageManager = applicationContext.getPackageManager(); + mTvActivityComponentName = new ComponentName(applicationContext, TvActivity.class); } @Override @@ -82,8 +82,13 @@ public class PackageIntentsReceiver extends BroadcastReceiver { mHandler.removeCallbacks(mOnPackageUpdatedRunnable); mHandler.postDelayed(mOnPackageUpdatedRunnable, TV_INPUT_UPDATE_DELAY_MS); + } + + /** + * Enables/Disables {@link TvActivity}. + */ private void enableTvActivityWithinPackageManager(Context context, boolean enable) { PackageManager pm = context.getPackageManager(); ComponentName name = new ComponentName(context, TvActivity.class); diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java index 835a3e53..c00a508e 100644 --- a/src/com/android/tv/recommendation/NotificationService.java +++ b/src/com/android/tv/recommendation/NotificationService.java @@ -141,11 +141,16 @@ public class NotificationService extends Service implements Recommender.Listener getResources().getDimensionPixelOffset(R.dimen.notif_ch_logo_padding_bottom); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - mTvInputManagerHelper = ((TvApplication) getApplicationContext()).getTvInputManagerHelper(); + TvApplication application = ((TvApplication) getApplicationContext()); + mTvInputManagerHelper = application.getTvInputManagerHelper(); mHandlerThread = new HandlerThread("tv notification"); mHandlerThread.start(); mHandler = new NotificationHandler(mHandlerThread.getLooper(), this); mHandler.sendEmptyMessage(MSG_INITIALIZE_RECOMMENDER); + + // Just called for early initialization. + application.getChannelDataManager(); + application.getProgramDataManager(); } private void handleInitializeRecommender() { @@ -309,8 +314,10 @@ public class NotificationService extends Service implements Recommender.Listener return false; } final Channel channel = cr.getChannel(); - if (DEBUG) Log.d(TAG, "sendNotification (" + channel.getDisplayName() - + " notifyId=" + notificationId + ")"); + if (DEBUG) { + Log.d(TAG, "sendNotification (channelName=" + channel.getDisplayName() + " notifyId=" + + notificationId + ")"); + } Intent intent = new Intent(Intent.ACTION_VIEW, channel.getUri()); intent.putExtra(TUNE_PARAMS_RECOMMENDATION_TYPE, mRecommendationType); final PendingIntent notificationIntent = PendingIntent.getActivity(this, 0, intent, 0); @@ -359,13 +366,15 @@ public class NotificationService extends Service implements Recommender.Listener // This callback will run on the main thread. Bitmap largeIconBitmap = (channelLogo == null) ? posterArtBitmap : overlayChannelLogo(channelLogo, posterArtBitmap); + String channelDisplayName = channel.getDisplayName(); Notification notification = new Notification.Builder(NotificationService.this) .setContentIntent(notificationIntent) .setContentTitle(program.getTitle()) - .setContentText(inputDisplayName + " " - + channel.getDisplayName()) - .setContentInfo(channel.getDisplayName()) + .setContentText(inputDisplayName + " " + + (TextUtils.isEmpty(channelDisplayName) + ? channel.getDisplayNumber() : channelDisplayName)) + .setContentInfo(channelDisplayName) .setAutoCancel(true) .setLargeIcon(largeIconBitmap) .setSmallIcon(R.drawable.ic_launcher_s) @@ -375,8 +384,8 @@ public class NotificationService extends Service implements Recommender.Listener false) .setSortKey(mRecommender.getChannelSortKey(channelId)) .build(); - notification.color = - getResources().getColor(R.color.recommendation_card_background); + notification.color = Utils.getColor(getResources(), + R.color.recommendation_card_background); if (!TextUtils.isEmpty(program.getThumbnailUri())) { notification.extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI, program.getThumbnailUri()); diff --git a/src/com/android/tv/recommendation/RecentChannelEvaluator.java b/src/com/android/tv/recommendation/RecentChannelEvaluator.java index c3482af9..e724f4ce 100644 --- a/src/com/android/tv/recommendation/RecentChannelEvaluator.java +++ b/src/com/android/tv/recommendation/RecentChannelEvaluator.java @@ -61,4 +61,4 @@ public class RecentChannelEvaluator extends Recommender.Evaluator { } return (maxScore > 0.0) ? maxScore : NOT_RECOMMENDED; } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java index 0f59e2bd..693380df 100644 --- a/src/com/android/tv/recommendation/RecommendationDataManager.java +++ b/src/com/android/tv/recommendation/RecommendationDataManager.java @@ -31,11 +31,14 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.data.WatchedHistoryManager; +import com.android.tv.util.PermissionUtils; import java.util.ArrayList; import java.util.Collection; @@ -46,7 +49,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -public class RecommendationDataManager { +public class RecommendationDataManager implements WatchedHistoryManager.Listener { private static final String TAG = "RecommendationDataManager"; private static final UriMatcher sUriMatcher; @@ -93,8 +96,9 @@ public class RecommendationDataManager { private final Set<String> mInputs = new HashSet<>(); private final HandlerThread mHandlerThread; - private final Handler mHandler; + @Nullable + private WatchedHistoryManager mWatchedHistoryManager; private final List<ListenerRecord> mListeners = new ArrayList<>(); @@ -257,12 +261,19 @@ public class RecommendationDataManager { mCancelLoadTask = false; mContext.getContentResolver().registerContentObserver( TvContract.Channels.CONTENT_URI, true, mContentObserver); - mContext.getContentResolver().registerContentObserver( - TvContract.WatchedPrograms.CONTENT_URI, true, mContentObserver); mHandler.obtainMessage(MSG_UPDATE_CHANNELS, TvContract.Channels.CONTENT_URI) .sendToTarget(); - mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY, TvContract.WatchedPrograms.CONTENT_URI) - .sendToTarget(); + if (!PermissionUtils.hasAccessWatchedHistory(mContext)) { + mWatchedHistoryManager = new WatchedHistoryManager(mContext); + mWatchedHistoryManager.setListener(this); + mWatchedHistoryManager.start(); + } else { + mContext.getContentResolver().registerContentObserver( + TvContract.WatchedPrograms.CONTENT_URI, true, mContentObserver); + mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY, + TvContract.WatchedPrograms.CONTENT_URI) + .sendToTarget(); + } mTvInputManager = (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE); mTvInputManager.registerCallback(mInternalCallback, mHandler); for (TvInputInfo input : mTvInputManager.getTvInputList()) { @@ -374,6 +385,41 @@ public class RecommendationDataManager { } } + private WatchedProgram convertFromWatchedHistoryManagerRecords( + WatchedHistoryManager.WatchedRecord watchedRecord) { + long endTime = watchedRecord.watchedStartTime + watchedRecord.duration; + Program program = new Program.Builder() + .setChannelId(watchedRecord.channelId) + .setTitle("") + .setStartTimeUtcMillis(watchedRecord.watchedStartTime) + .setEndTimeUtcMillis(endTime) + .build(); + return new WatchedProgram(program, watchedRecord.watchedStartTime, endTime); + } + + @Override + public void onLoadFinished() { + for (WatchedHistoryManager.WatchedRecord record + : mWatchedHistoryManager.getWatchedHistory()) { + updateChannelRecordFromWatchedProgram( + convertFromWatchedHistoryManagerRecords(record)); + } + mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED); + } + + @Override + public void onNewRecordAdded(WatchedHistoryManager.WatchedRecord watchedRecord) { + ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram( + convertFromWatchedHistoryManagerRecords(watchedRecord)); + if (mChannelRecordMapLoaded && channelRecord != null) { + synchronized (sListenerLock) { + for (ListenerRecord l : mListeners) { + l.postNewWatchLog(channelRecord); + } + } + } + } + private WatchedProgram createWatchedProgramFromWatchedProgramCursor(Cursor cursor) { // Have to initiate the indexes of WatchedProgram Columns. if (mIndexWatchChannelId == -1) { diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java new file mode 100644 index 00000000..4c85af67 --- /dev/null +++ b/src/com/android/tv/search/DataManagerSearch.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.search; + +import android.content.Context; +import android.content.Intent; +import android.media.tv.TvContentRating; +import android.media.tv.TvContract; +import android.media.tv.TvContract.Programs; +import android.media.tv.TvInputManager; +import android.support.annotation.UiThread; +import android.text.TextUtils; +import android.util.Log; + +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.data.ProgramDataManager; +import com.android.tv.search.LocalSearchProvider.SearchResult; +import com.android.tv.util.MainThreadExecutor; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * An implementation of {@link SearchInterface} to search query from {@link ChannelDataManager} + * and {@link ProgramDataManager}. + */ +public class DataManagerSearch implements SearchInterface { + private static final boolean DEBUG = false; + private static final String TAG = "TvProviderSearch"; + + private final Context mContext; + private final TvInputManager mTvInputManager; + private final ChannelDataManager mChannelDataManager; + private final ProgramDataManager mProgramDataManager; + + DataManagerSearch(Context context) { + mContext = context; + mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); + TvApplication application = (TvApplication) context.getApplicationContext(); + mChannelDataManager = application.getChannelDataManager(); + mProgramDataManager = application.getProgramDataManager(); + } + + @Override + public List<SearchResult> search(final String query, final int limit, final int action) { + Future<List<SearchResult>> future = MainThreadExecutor.getInstance() + .submit(new Callable<List<SearchResult>>() { + @Override + public List<SearchResult> call() throws Exception { + return searchFromDataManagers(query, limit, action); + } + }); + + try { + return future.get(); + } catch (InterruptedException e) { + Thread.interrupted(); + return Collections.EMPTY_LIST; + } catch (ExecutionException e) { + Log.w(TAG, "Error searching for " + query, e); + return Collections.EMPTY_LIST; + } + } + + @UiThread + private List<SearchResult> searchFromDataManagers(String query, int limit, int action) { + List<SearchResult> results = new ArrayList<>(); + if (!mChannelDataManager.isDbLoadFinished()) { + return results; + } + if (action == ACTION_TYPE_SWITCH_CHANNEL + || action == ACTION_TYPE_SWITCH_INPUT) { + // Voice search query should be handled by the a system TV app. + return results; + } + Set<Long> channelsFound = new HashSet<>(); + List<Channel> channelList = mChannelDataManager.getBrowsableChannelList(); + query = query.toLowerCase(); + if (TextUtils.isDigitsOnly(query)) { + for (Channel channel : channelList) { + if (channelsFound.contains(channel.getId())) { + continue; + } + if (contains(channel.getDisplayNumber(), query)) { + addResult(results, channelsFound, channel, null); + } + if (results.size() >= limit) { + return results; + } + } + // TODO: recently watched channels may have higher priority. + } + for (Channel channel : channelList) { + if (channelsFound.contains(channel.getId())) { + continue; + } + if (contains(channel.getDisplayName(), query) + || contains(channel.getDescription(), query)) { + addResult(results, channelsFound, channel, null); + } + if (results.size() >= limit) { + return results; + } + } + for (Channel channel : channelList) { + if (channelsFound.contains(channel.getId())) { + continue; + } + Program program = mProgramDataManager.getCurrentProgram(channel.getId()); + if (program == null) { + continue; + } + if (contains(program.getTitle(), query) + && !isRatingBlocked(program.getContentRatings())) { + addResult(results, channelsFound, channel, program); + } + if (results.size() >= limit) { + return results; + } + } + for (Channel channel : channelList) { + if (channelsFound.contains(channel.getId())) { + continue; + } + Program program = mProgramDataManager.getCurrentProgram(channel.getId()); + if (program == null) { + continue; + } + if (contains(program.getDescription(), query) + && !isRatingBlocked(program.getContentRatings())) { + addResult(results, channelsFound, channel, program); + } + if (results.size() >= limit) { + return results; + } + } + return results; + } + + // It assumes that query is already lower cases. + private boolean contains(String string, String query) { + return string != null && string.toLowerCase().contains(query); + } + + /** + * If query is matched to channel, {@code program} should be null. + */ + private void addResult(List<SearchResult> results, Set<Long> channelsFound, Channel channel, + Program program) { + if (program == null) { + program = mProgramDataManager.getCurrentProgram(channel.getId()); + if (program != null && isRatingBlocked(program.getContentRatings())) { + program = null; + } + } + + SearchResult result = new SearchResult(); + + long channelId = channel.getId(); + result.channelId = channelId; + result.channelNumber = channel.getDisplayNumber(); + if (program == null) { + result.title = channel.getDisplayName(); + result.description = channel.getDescription(); + result.imageUri = TvContract.buildChannelLogoUri(channelId).toString(); + result.intentAction = Intent.ACTION_VIEW; + result.intentData = buildIntentData(channelId); + result.contentType = Programs.CONTENT_ITEM_TYPE; + result.isLive = true; + result.progressPercentage = LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE; + } else { + result.title = program.getTitle(); + result.description = buildProgramDescription(channel.getDisplayNumber(), + channel.getDisplayName(), program.getStartTimeUtcMillis(), + program.getEndTimeUtcMillis()); + result.imageUri = program.getPosterArtUri(); + result.intentAction = Intent.ACTION_VIEW; + result.intentData = buildIntentData(channelId); + result.contentType = Programs.CONTENT_ITEM_TYPE; + result.isLive = true; + result.videoWidth = program.getVideoWidth(); + result.videoHeight = program.getVideoHeight(); + result.duration = program.getDurationMillis(); + result.progressPercentage = getProgressPercentage( + program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis()); + } + if (DEBUG) { + Log.d(TAG, "Add a result : channel=" + channel + " program=" + program); + } + results.add(result); + channelsFound.add(channel.getId()); + } + + private String buildProgramDescription(String channelNumber, String channelName, + long programStartUtcMillis, long programEndUtcMillis) { + return Utils.getDurationString(mContext, programStartUtcMillis, programEndUtcMillis, false) + + System.lineSeparator() + channelNumber + " " + channelName; + } + + private int getProgressPercentage(long startUtcMillis, long endUtcMillis) { + long current = System.currentTimeMillis(); + if (startUtcMillis > current || endUtcMillis <= current) { + return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE; + } + return (int)(100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis)); + } + + private String buildIntentData(long channelId) { + return TvContract.buildChannelUri(channelId).buildUpon() + .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH) + .build().toString(); + } + + private boolean isRatingBlocked(TvContentRating[] ratings) { + if (ratings == null || ratings.length == 0 + || !mTvInputManager.isParentalControlsEnabled()) { + return false; + } + for (TvContentRating rating : ratings) { + try { + if (mTvInputManager.isRatingBlocked(rating)) { + return true; + } + } catch (IllegalArgumentException e) { + // Do nothing. + } + } + return false; + } +} diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java index e9cffa31..3cc21ace 100644 --- a/src/com/android/tv/search/LocalSearchProvider.java +++ b/src/com/android/tv/search/LocalSearchProvider.java @@ -25,6 +25,8 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; +import com.android.tv.util.PermissionUtils; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -60,13 +62,12 @@ public class LocalSearchProvider extends ContentProvider { private static final String LIVE_CONTENTS = "1"; static final String SUGGEST_PARAMETER_ACTION = "action"; - static final int DEFAULT_SEARCH_ACTION = TvProviderSearch.ACTION_TYPE_AMBIGUOUS; + static final int DEFAULT_SEARCH_ACTION = SearchInterface.ACTION_TYPE_AMBIGUOUS; - private TvProviderSearch mTvProviderSearch; + private SearchInterface mSearch; @Override public boolean onCreate() { - mTvProviderSearch = new TvProviderSearch(getContext()); return true; } @@ -77,6 +78,11 @@ public class LocalSearchProvider extends ContentProvider { Log.d(TAG, "query(" + uri + ", " + Arrays.toString(projection) + ", " + selection + ", " + Arrays.toString(selectionArgs) + ", " + sortOrder + ")"); } + if (PermissionUtils.hasAccessAllEpg(getContext())) { + mSearch = new TvProviderSearch(getContext()); + } else { + mSearch = new DataManagerSearch(getContext()); + } String query = uri.getLastPathSegment(); int limit = DEFAULT_SEARCH_LIMIT; int action = DEFAULT_SEARCH_ACTION; @@ -88,7 +94,7 @@ public class LocalSearchProvider extends ContentProvider { } List<SearchResult> results = new ArrayList<>(); if (!TextUtils.isEmpty(query)) { - results.addAll(mTvProviderSearch.search(query, limit, action)); + results.addAll(mSearch.search(query, limit, action)); } return createSuggestionsCursor(results); } @@ -165,4 +171,4 @@ public class LocalSearchProvider extends ContentProvider { ", title: " + title; } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java index bb6cdc69..7d6efcb3 100644 --- a/src/com/android/tv/search/ProgramGuideSearchFragment.java +++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java @@ -42,6 +42,7 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.util.ImageLoader; +import com.android.tv.util.PermissionUtils; import java.util.List; @@ -129,7 +130,7 @@ public class ProgramGuideSearchFragment extends SearchFragment { private final ArrayObjectAdapter mResultAdapter = new ArrayObjectAdapter(new ListRowPresenter()); private MainActivity mMainActivity; - private TvProviderSearch mTvProviderSearch; + private SearchInterface mSearch; private int mMainCardWidth; private int mMainCardHeight; private SearchTask mSearchTask; @@ -139,7 +140,11 @@ public class ProgramGuideSearchFragment extends SearchFragment { super.onCreate(savedInstanceState); mMainActivity = (MainActivity) getActivity(); - mTvProviderSearch = new TvProviderSearch(mMainActivity); + if (PermissionUtils.hasAccessAllEpg(mMainActivity)) { + mSearch = new TvProviderSearch(mMainActivity); + } else { + mSearch = new DataManagerSearch(mMainActivity); + } Resources res = getResources(); mMainCardWidth = res.getDimensionPixelSize(R.dimen.card_image_layout_width); mMainCardHeight = res.getDimensionPixelSize(R.dimen.card_image_layout_height); @@ -186,7 +191,7 @@ public class ProgramGuideSearchFragment extends SearchFragment { @Override protected List<LocalSearchProvider.SearchResult> doInBackground(Void... params) { - return mTvProviderSearch.search(mQuery, SEARCH_RESULT_MAX, + return mSearch.search(mQuery, SEARCH_RESULT_MAX, TvProviderSearch.ACTION_TYPE_AMBIGUOUS); } diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java new file mode 100644 index 00000000..7394150e --- /dev/null +++ b/src/com/android/tv/search/SearchInterface.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.search; + +import com.android.tv.search.LocalSearchProvider.SearchResult; + +import java.util.List; + +/** + * Interface for channel and program search. + */ +public interface SearchInterface { + String SOURCE_TV_SEARCH = "TvSearch"; + + int ACTION_TYPE_AMBIGUOUS = 1; + int ACTION_TYPE_SWITCH_CHANNEL = 2; + int ACTION_TYPE_SWITCH_INPUT = 3; + + /** + * Search channels, inputs, or programs. + * This assumes that parental control settings will not be change while searching. + * + * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT}, + * or {@link #ACTION_TYPE_AMBIGUOUS}, + */ + public List<SearchResult> search(String query, int limit, int action); +} diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java index 00eb68bb..a5ad00ff 100644 --- a/src/com/android/tv/search/TvProviderSearch.java +++ b/src/com/android/tv/search/TvProviderSearch.java @@ -33,6 +33,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.tv.search.LocalSearchProvider.SearchResult; +import com.android.tv.util.PermissionUtils; import com.android.tv.util.Utils; import junit.framework.Assert; @@ -48,18 +49,15 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -public class TvProviderSearch { +/** + * An implementation of {@link SearchInterface} to search query from TvProvider directly. + */ +public class TvProviderSearch implements SearchInterface { private static final boolean DEBUG = false; private static final String TAG = "TvProviderSearch"; private static final int NO_LIMIT = 0; - static final int ACTION_TYPE_AMBIGUOUS = 1; - static final int ACTION_TYPE_SWITCH_CHANNEL = 2; - static final int ACTION_TYPE_SWITCH_INPUT = 3; - - private static final String SOURCE_TV_SEARCH = "TvSearch"; - private final Context mContext; private final ContentResolver mContentResolver; private final TvInputManager mTvInputManager; @@ -77,9 +75,14 @@ public class TvProviderSearch { * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT}, * or {@link #ACTION_TYPE_AMBIGUOUS}, */ + @Override @WorkerThread public List<SearchResult> search(String query, int limit, int action) { List<SearchResult> results = new ArrayList<>(); + if (!PermissionUtils.hasAccessAllEpg(mContext)) { + // TODO: support this feature for non-system LC app. b/23939816 + return results; + } Set<Long> channelsFound = new HashSet<>(); if (action == ACTION_TYPE_SWITCH_CHANNEL) { results.addAll(searchChannels(query, channelsFound, limit)); @@ -464,7 +467,6 @@ public class TvProviderSearch { return result; } - @WorkerThread private class ChannelComparatorWithSameDisplayNumber implements Comparator<SearchResult> { private final Map<Long, Long> mMaxWatchStartTimeMap = new HashMap<>(); diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java index 683e6c3a..bf8e69c7 100644 --- a/src/com/android/tv/ui/ChannelBannerView.java +++ b/src/com/android/tv/ui/ChannelBannerView.java @@ -200,8 +200,9 @@ public class ChannelBannerView extends FrameLayout implements Channel.LoadImageC R.dimen.channel_banner_channel_logo_margin_start); mProgramDescriptionTextViewWidth = mResources.getDimensionPixelSize( R.dimen.channel_banner_program_description_width); - mChannelBannerTextColor = mResources.getColor(R.color.channel_banner_text_color); - mChannelBannerDimTextColor = mResources.getColor(R.color.channel_banner_dim_text_color); + mChannelBannerTextColor = Utils.getColor(mResources, R.color.channel_banner_text_color); + mChannelBannerDimTextColor = Utils.getColor(mResources, + R.color.channel_banner_dim_text_color); mResizeAnimDuration = mResources.getInteger(R.integer.channel_banner_fast_anim_duration); mResizeInterpolator = AnimationUtils.loadInterpolator(context, diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java index 3ba2738e..722a3759 100644 --- a/src/com/android/tv/ui/KeypadChannelSwitchView.java +++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; @@ -42,8 +43,7 @@ import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.data.Channel; import com.android.tv.data.ChannelNumber; - -import junit.framework.Assert; +import com.android.tv.util.SoftPreconditions; import java.util.ArrayList; import java.util.List; @@ -62,6 +62,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements private final Tracker mTracker; private final DurationTimer mViewDurationTimer = new DurationTimer(); private boolean mNavigated = false; + @Nullable //Once mChannels is set to null it should not be used again. private List<Channel> mChannels; private TextView mChannelNumberView; private ListView mChannelItemListView; @@ -157,7 +158,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements } else { mSelectedChannel = (Channel) mAdapter.getItem(position); } - if(position!=0 && !mNavigated) { + if (position != 0 && !mNavigated) { mNavigated = true; mTracker.sendChannelInputNavigated(); } @@ -178,7 +179,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - Assert.assertNotNull(mChannels); + SoftPreconditions.checkNotNull(mChannels, TAG, "mChannels"); if (isChannelNumberKey(keyCode)) { onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0); return true; @@ -227,7 +228,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements mAdapter.notifyDataSetChanged(); } - public void setChannels(List<Channel> channels) { + public void setChannels(@Nullable List<Channel> channels) { mChannels = channels; } diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index e347fbb1..b5b6ef34 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -22,6 +22,7 @@ import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; +import android.support.annotation.NonNull; import android.support.v17.leanback.widget.VerticalGridView; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; @@ -33,13 +34,13 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.analytics.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.data.Channel; import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; @@ -53,36 +54,71 @@ public class SelectInputView extends VerticalGridView implements private static final String TAG = "SelectInputView"; private static final boolean DEBUG = false; public static final String SCREEN_NAME = "Input selection"; + private static final int TUNER_INPUT_POSITION = 0; - private final MainActivity mMainActivity; + private final TvApplication mApplication; private final TvInputManagerHelper mTvInputManagerHelper; private final List<TvInputInfo> mInputList = new ArrayList<>(); private final InputsComparator mComparator = new InputsComparator(); private final Tracker mTracker; private final DurationTimer mViewDurationTimer = new DurationTimer(); + private final TvInputCallback mTvInputCallback = new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + buildInputListAndNotify(); + updateSelectedPositionIfNeeded(); + } + + @Override + public void onInputRemoved(String inputId) { + buildInputListAndNotify(); + updateSelectedPositionIfNeeded(); + } + + @Override + public void onInputUpdated(String inputId) { + buildInputListAndNotify(); + updateSelectedPositionIfNeeded(); + } + + @Override + public void onInputStateChanged(String inputId, int state) { + buildInputListAndNotify(); + updateSelectedPositionIfNeeded(); + } + + private void updateSelectedPositionIfNeeded() { + if (!isFocusable() || mSelectedInput == null) { + return; + } + if (!isInputEnabled(mSelectedInput)) { + setSelectedPosition(TUNER_INPUT_POSITION); + return; + } + if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) { + setSelectedPosition(getInputPosition(mSelectedInput.getId())); + } + } + }; + + private Channel mCurrentChannel; + private OnInputSelectedCallback mCallback; private final Runnable mHideRunnable = new Runnable() { @Override public void run() { - // Just dismiss the view when no action is required. - if (mSelectedInput == null - || TextUtils.equals(mSelectedInput.getId(), mCurrentInputId) - || (!mSelectedInput.isPassthroughInput() && mCurrentInputId == null)) { - mMainActivity.getOverlayManager().hideOverlays( - TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE - | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU); + if (mSelectedInput == null) { return; } // TODO: pass english label to tracker http://b/22355024 final String label = mSelectedInput.loadLabel(getContext()).toString(); mTracker.sendInputSelected(label); - if (mSelectedInput.isPassthroughInput()) { - mMainActivity.tuneToChannel( - Channel.createPassthroughChannel(mSelectedInput.getId())); - } else { - mMainActivity.tuneToLastWatchedChannelForTunerInput(); + if (mCallback != null) { + if (mSelectedInput.isPassthroughInput()) { + mCallback.onPassthroughInputSelected(mSelectedInput); + } else { + mCallback.onTunerInputSelected(); + } } } }; @@ -97,9 +133,6 @@ public class SelectInputView extends VerticalGridView implements private boolean mResetTransitionAlpha; private TvInputInfo mSelectedInput; - // The ID of the currently selected pass-through input. The null value means that the currently - // selected input is a tuner. - private String mCurrentInputId; private int mMaxItemWidth; public SelectInputView(Context context) { @@ -114,59 +147,21 @@ public class SelectInputView extends VerticalGridView implements super(context, attrs, defStyleAttr); setAdapter(new InputListAdapter()); - mMainActivity = (MainActivity) context; + mApplication = (TvApplication) context.getApplicationContext(); mTracker = ((TvApplication) context.getApplicationContext()).getTracker(); - mTvInputManagerHelper = mMainActivity.getTvInputManagerHelper(); - mTvInputManagerHelper.addCallback(new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - buildInputListAndNotify(); - updateSelectedPositionIfNeeded(); - } - - @Override - public void onInputRemoved(String inputId) { - buildInputListAndNotify(); - updateSelectedPositionIfNeeded(); - } - - @Override - public void onInputUpdated(String inputId) { - buildInputListAndNotify(); - updateSelectedPositionIfNeeded(); - } - - @Override - public void onInputStateChanged(String inputId, int state) { - buildInputListAndNotify(); - updateSelectedPositionIfNeeded(); - } - - private void updateSelectedPositionIfNeeded() { - if (!isFocusable() || mSelectedInput == null) { - return; - } - if (!isInputEnabled(mSelectedInput)) { - setSelectedPosition(0); - return; - } - if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) { - setSelectedPosition(getInputPosition(mSelectedInput.getId())); - } - } - }); + mTvInputManagerHelper = mApplication.getTvInputManagerHelper(); Resources resources = context.getResources(); mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height); mShowDurationMillis = resources.getInteger(R.integer.select_input_show_duration); mRippleAnimDurationMillis = resources.getInteger( R.integer.select_input_ripple_anim_duration); - mTextColorPrimary = resources.getColor(R.color.select_input_text_color_primary); - mTextColorSecondary = resources.getColor(R.color.select_input_text_color_secondary); - mTextColorDisabled = resources.getColor(R.color.select_input_text_color_disabled); + mTextColorPrimary = Utils.getColor(resources, R.color.select_input_text_color_primary); + mTextColorSecondary = Utils.getColor(resources, R.color.select_input_text_color_secondary); + mTextColorDisabled = Utils.getColor(resources, R.color.select_input_text_color_disabled); mItemViewForMeasure = LayoutInflater.from(context).inflate( - R.layout.select_input_item, null, false); + R.layout.select_input_item, this, false); buildInputListAndNotify(); } @@ -204,15 +199,15 @@ public class SelectInputView extends VerticalGridView implements mResetTransitionAlpha = fromEmptyScene; buildInputListAndNotify(); - Channel channel = mMainActivity.getCurrentChannel(); - mCurrentInputId = channel != null && channel.isPassthrough() ? channel.getInputId() : null; - if (mCurrentInputId != null - && !isInputEnabled(mTvInputManagerHelper.getTvInputInfo(mCurrentInputId))) { - // If current input is disabled, the first item will be focused. The tuner input - // is usually the first item. - setSelectedPosition(0); + mTvInputManagerHelper.addCallback(mTvInputCallback); + String currentInputId = mCurrentChannel != null && mCurrentChannel.isPassthrough() ? + mCurrentChannel.getInputId() : null; + if (currentInputId != null + && !isInputEnabled(mTvInputManagerHelper.getTvInputInfo(currentInputId))) { + // If current input is disabled, the tuner input will be focused. + setSelectedPosition(TUNER_INPUT_POSITION); } else { - setSelectedPosition(getInputPosition(mCurrentInputId)); + setSelectedPosition(getInputPosition(currentInputId)); } setFocusable(true); requestFocus(); @@ -226,12 +221,13 @@ public class SelectInputView extends VerticalGridView implements } } } - return 0; + return TUNER_INPUT_POSITION; } @Override public void onExitAction() { mTracker.sendHideInputSelection(mViewDurationTimer.reset()); + mTvInputManagerHelper.removeCallback(mTvInputCallback); removeCallbacks(mHideRunnable); } @@ -302,6 +298,21 @@ public class SelectInputView extends VerticalGridView implements != TvInputManager.INPUT_STATE_DISCONNECTED; } + /** + * Sets a callback which receives the notifications of input selection. + */ + public void setOnInputSelectedCallback(OnInputSelectedCallback callback) { + mCallback = callback; + } + + /** + * Sets the current channel. The initial selection will be the input which contains the + * {@code channel}. + */ + public void setCurrentChannel(Channel channel) { + mCurrentChannel = channel; + } + class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @@ -438,4 +449,19 @@ public class SelectInputView extends VerticalGridView implements } } } + + /** + * A callback interface for the input selection. + */ + public static interface OnInputSelectedCallback { + /** + * Called when the tuner input is selected. + */ + void onTunerInputSelected(); + + /** + * Called when the passthrough input is selected. + */ + void onPassthroughInputSelected(@NonNull TvInputInfo input); + } } diff --git a/src/com/android/tv/ui/SetupView.java b/src/com/android/tv/ui/SetupView.java index 330b7e9f..cb25f6f9 100644 --- a/src/com/android/tv/ui/SetupView.java +++ b/src/com/android/tv/ui/SetupView.java @@ -378,7 +378,7 @@ public class SetupView extends FullscreenDialogView { final TvInputInfo input = mInputList.get(position); viewHolder.mTitle.setText(input.loadLabel(getContext())); int channelCount = mChannelDataManager.getChannelCountForInput(input.getId()); - if (mSetupUtils.hasSetupLaunched(input.getId())) { + if (mSetupUtils.isSetupDone(input.getId())) { if (channelCount == 0) { viewHolder.mDescription.setText(R.string.setup_input_no_channels); } else { diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index eba43594..f526c33c 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -34,6 +34,7 @@ import android.media.tv.TvView.TvInputCallback; import android.net.Uri; import android.os.Bundle; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; @@ -43,6 +44,7 @@ import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.TextView; import com.android.tv.R; @@ -52,8 +54,10 @@ import com.android.tv.analytics.Tracker; import com.android.tv.common.TvCommonConstants; import com.android.tv.data.Channel; import com.android.tv.data.StreamInfo; +import com.android.tv.data.WatchedHistoryManager; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.recommendation.NotificationService; +import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -98,6 +102,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private Channel mCurrentChannel; private TvInputManagerHelper mInputManagerHelper; private ContentRatingsManager mContentRatingsManager; + @Nullable + private WatchedHistoryManager mWatchedHistoryManager; private boolean mStarted; private TvInputInfo mInputInfo; private OnTuneListener mOnTuneListener; @@ -121,6 +127,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private boolean mIsPip; private int mScreenHeight; private int mShrunkenTvViewHeight; + private boolean mCanModifyParentalControls; @TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE; private TimeShiftListener mTimeShiftListener; @@ -135,7 +142,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private final View mBlockScreenView; private final View mBlockScreenDescriptionView; - private final View mBlockScreenIconView; + private final ImageView mBlockScreenIconView; private final View mBlockScreenShrunkenIconView; private final TextView mBlockScreenTextView; @@ -144,7 +151,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private final Animator mBlockScreenDescriptionFadeOut; // A View to hide screen when there's problem in video playback. - private final View mHideScreenView; + private final TextView mHideScreenView; // A View to block screen until onContentAllowed is received if parental control is on. private final View mBlockScreenForTuneView; @@ -164,6 +171,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onConnectionFailed(String inputId) { Log.w(TAG, "Failed to bind an input"); + mTracker.sendInputConnectionFailure(inputId); Channel channel = mCurrentChannel; mCurrentChannel = null; mInputInfo = null; @@ -182,6 +190,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @Override public void onDisconnected(String inputId) { Log.w(TAG, "Session is released by crash"); + mTracker.sendInputDisconnected(inputId); Channel channel = mCurrentChannel; mCurrentChannel = null; mInputInfo = null; @@ -273,6 +282,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { if (mOnTuneListener != null) { mOnTuneListener.onStreamInfoChanged(TunableTvView.this); } + switch (reason) { + case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: + case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: + case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: + mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason); + default: + // do nothing + } } @Override @@ -314,12 +331,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo { super(context, attrs, defStyleAttr, defStyleRes); inflate(getContext(), R.layout.tunable_tv_view, this); - mTracker = ((TvApplication) context.getApplicationContext()).getTracker(); + TvApplication tvApplication = (TvApplication) context.getApplicationContext(); + mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context); + mTracker = tvApplication.getTracker(); mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL; mBlockScreenView = findViewById(R.id.block_screen); mBlockScreenDescriptionView = findViewById(R.id.block_screen_description); - mBlockScreenIconView = mBlockScreenView.findViewById(R.id.block_screen_icon); + mBlockScreenIconView = (ImageView) mBlockScreenView.findViewById(R.id.block_screen_icon); + if (!mCanModifyParentalControls) { + mBlockScreenIconView.setImageResource(R.drawable.ic_message_lock_no_permission); + mBlockScreenIconView.setScaleType(ImageView.ScaleType.CENTER); + } mBlockScreenShrunkenIconView = mBlockScreenView.findViewById( R.id.block_screen_shrunken_icon); mBlockScreenTextView = (TextView) mBlockScreenView.findViewById(R.id.block_screen_text); @@ -355,7 +378,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } }); - mHideScreenView = findViewById(R.id.hide_screen); + mHideScreenView = (TextView) findViewById(R.id.hide_screen); mBufferingSpinnerView = findViewById(R.id.buffering_spinner); mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune); mDimScreenView = findViewById(R.id.dim); @@ -401,7 +424,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } mStarted = false; if (mCurrentChannel != null) { - mTracker.sendChannelViewStop(mCurrentChannel, mChannelViewTimer.reset()); + long duration = mChannelViewTimer.reset(); + mTracker.sendChannelViewStop(mCurrentChannel, duration); + if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { + mWatchedHistoryManager.logChannelViewStop(mCurrentChannel, + System.currentTimeMillis(), duration); + } } reset(); } @@ -420,6 +448,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mTvView.setMain(); } + public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) { + mWatchedHistoryManager = watchedHistoryManager; + } + public boolean isPlaying() { return mStarted; } @@ -451,14 +483,17 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return false; } if (mCurrentChannel != null) { - mTracker.sendChannelViewStop(mCurrentChannel, mChannelViewTimer.reset()); + long duration = mChannelViewTimer.reset(); + mTracker.sendChannelViewStop(mCurrentChannel, duration); + if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) { + mWatchedHistoryManager.logChannelViewStop(mCurrentChannel, + System.currentTimeMillis(), duration); + } } mOnTuneListener = listener; mCurrentChannel = channel; boolean tunedByRecommendation = params != null && params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null; - mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation); - mChannelViewTimer.start(); boolean needSurfaceSizeUpdate = false; if (!inputInfo.equals(mInputInfo)) { mInputInfo = inputInfo; @@ -471,6 +506,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } needSurfaceSizeUpdate = true; } + mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation); + mChannelViewTimer.start(); mVideoWidth = 0; mVideoHeight = 0; mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; @@ -840,7 +877,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mBlockScreenTextView.setText(""); break; case BLOCK_SCREEN_TYPE_NORMAL: - mBlockScreenTextView.setText(R.string.tvview_channel_locked); + if (mCanModifyParentalControls) { + mBlockScreenTextView.setText(R.string.tvview_channel_locked); + } else { + mBlockScreenTextView.setText(R.string.tvview_channel_locked_no_permission); + } break; } } else if (mBlockedContentRating != null) { @@ -859,10 +900,20 @@ public class TunableTvView extends FrameLayout implements StreamInfo { break; case BLOCK_SCREEN_TYPE_NORMAL: if (TextUtils.isEmpty(name)) { - mBlockScreenTextView.setText(R.string.tvview_content_locked); + if (mCanModifyParentalControls) { + mBlockScreenTextView.setText(R.string.tvview_content_locked); + } else { + mBlockScreenTextView.setText( + R.string.tvview_content_locked_no_permission); + } } else { - mBlockScreenTextView.setText(getContext().getString( - R.string.tvview_content_locked_format, name)); + if (mCanModifyParentalControls) { + mBlockScreenTextView.setText(getContext().getString( + R.string.tvview_content_locked_format, name)); + } else { + mBlockScreenTextView.setText(getContext().getString( + R.string.tvview_content_locked_format_no_permission, name)); + } } break; } @@ -903,8 +954,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo { private void hideScreenByVideoAvailability(int reason) { switch (reason) { + case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: + mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setText(R.string.tvview_msg_audio_only); + mBufferingSpinnerView.setVisibility(GONE); + unmuteIfPossible(); + break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: mBufferingSpinnerView.setVisibility(VISIBLE); + mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: @@ -912,12 +970,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo { case VIDEO_UNAVAILABLE_REASON_NOT_TUNED: default: mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setText(null); mBufferingSpinnerView.setVisibility(GONE); + mute(); break; } mVideoAvailable = false; mVideoUnavailableReason = reason; - mute(); } private void unhideScreenByVideoAvailability() { diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java index 7096893f..444b5c0c 100644 --- a/src/com/android/tv/ui/TvTransitionManager.java +++ b/src/com/android/tv/ui/TvTransitionManager.java @@ -123,6 +123,7 @@ public class TvTransitionManager extends TransitionManager { initIfNeeded(); if (mCurrentScene != mSelectInputScene) { transitionTo(mSelectInputScene); + mSelectInputView.setCurrentChannel(mMainActivity.getCurrentChannel()); } } diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java index f93cf45c..d767906b 100644 --- a/src/com/android/tv/ui/TvViewUiManager.java +++ b/src/com/android/tv/ui/TvViewUiManager.java @@ -47,6 +47,7 @@ import com.android.tv.R; import com.android.tv.TvOptionsManager; import com.android.tv.data.DisplayMode; import com.android.tv.util.TvSettings; +import com.android.tv.util.Utils; /** * The TvViewUiManager is responsible for handling UI layouting and animation of main and PIP @@ -778,8 +779,9 @@ public class TvViewUiManager { // Set marginEnd as well because setTvViewPosition uses both start/end margin. layoutParams.setMarginEnd(mScreenWidth - layoutParams.width - marginStart); - setBackgroundColor(mResources.getColor(isTvViewFullScreen() ? R.color.tvactivity_background - : R.color.tvactivity_background_on_shrunken_tvview), layoutParams, animate); + setBackgroundColor(Utils.getColor(mResources, isTvViewFullScreen() + ? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview), + layoutParams, animate); setTvViewPosition(layoutParams, tvViewFrame, animate); // Update the current display mode. diff --git a/src/com/android/tv/ui/sidepanel/AboutFragment.java b/src/com/android/tv/ui/sidepanel/AboutFragment.java index 5b7444b2..e880fe37 100644 --- a/src/com/android/tv/ui/sidepanel/AboutFragment.java +++ b/src/com/android/tv/ui/sidepanel/AboutFragment.java @@ -16,12 +16,17 @@ package com.android.tv.ui.sidepanel; +import android.app.Activity; +import android.content.Context; import android.view.View; +import android.widget.CompoundButton; import android.widget.TextView; +import com.android.tv.Features; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.analytics.OptOutPreferenceHelper; import com.android.tv.dialog.WebDialogFragment; import com.android.tv.license.LicenseUtils; @@ -29,16 +34,15 @@ import java.util.ArrayList; import java.util.List; /** - * Shows version and optional license information. + * Shows version, optional license information and Analytics OptOut. */ public class AboutFragment extends SideFragment { - private final static String TAG = "AboutFragment"; private static final String TRACKER_LABEL = "about"; /** * Shows the application version name. */ - public static class VersionItem extends Item { + private static final class VersionItem extends Item { @Override protected int getResourceId() { return R.layout.option_item_simple; @@ -61,7 +65,7 @@ public class AboutFragment extends SideFragment { /** * Opens a dialog showing open source licenses. */ - public static class LicenseActionItem extends ActionItem { + public static final class LicenseActionItem extends ActionItem { public final static String DIALOG_TAG = LicenseActionItem.class.getSimpleName(); public static final String TRACKER_LABEL = "Open Source Licenses"; private final MainActivity mMainActivity; @@ -79,6 +83,70 @@ public class AboutFragment extends SideFragment { } } + /** + * Sets the users preference for allowing analytics. + */ + private static final class AllowAnalyticsItem extends SwitchItem { + //TODO: change this to use SwitchPreference + private final OptOutPreferenceHelper mPreferenceHelper; + private TextView mDescriptionView; + private int mOriginalMaxDescriptionLine; + private MainActivity mMainActivity; + private View mBoundView; + + public AllowAnalyticsItem(Context context) { + super(context.getResources().getString(R.string.about_menu_improve), + context.getResources().getString(R.string.about_menu_improve), + context.getResources().getString(R.string.about_menu_improve_summary)); + mPreferenceHelper = ((TvApplication) context.getApplicationContext()) + .getOptPreferenceHelper(); + } + + @Override + protected void onBind(View view) { + super.onBind(view); + mDescriptionView = (TextView) view.findViewById(getDescriptionViewId()); + mOriginalMaxDescriptionLine = mDescriptionView.getMaxLines(); + mDescriptionView.setMaxLines(Integer.MAX_VALUE); + mMainActivity = (MainActivity) view.getContext(); + mBoundView = view; + } + + @Override + protected void onUnbind() { + super.onUnbind(); + mDescriptionView.setMaxLines(mOriginalMaxDescriptionLine); + mDescriptionView = null; + mMainActivity = null; + mBoundView = null; + } + + @Override + protected void onUpdate() { + super.onUpdate(); + setChecked(!mPreferenceHelper + .getOptOutPreference(OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE)); + } + + @Override + protected void onSelected() { + super.onSelected(); + mPreferenceHelper.setOptOutPreference(!isChecked()); + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + if (mMainActivity != null && mBoundView != null && mBoundView.hasFocus()) { + // Quick fix for accessibility + // TODO: Need to change the resource in the future. + mMainActivity.sendAccessiblityText(checked ? + mMainActivity.getString(R.string.options_item_pip_on) + : mMainActivity.getString(R.string.options_item_pip_off)); + } + } + } + @Override protected String getTitle() { return getResources().getString(R.string.side_panel_title_about); @@ -93,8 +161,12 @@ public class AboutFragment extends SideFragment { protected List<Item> getItemList() { List<Item> items = new ArrayList<>(); items.add(new VersionItem()); - if (LicenseUtils.hasLicenses(getActivity().getAssets())) { - items.add(new LicenseActionItem((MainActivity) getActivity())); + Activity activity = getActivity(); + if (LicenseUtils.hasLicenses(activity.getAssets())) { + items.add(new LicenseActionItem((MainActivity) activity)); + } + if (Features.ANALYTICS_OPT_OUT.isEnabled(activity)) { + items.add(new AllowAnalyticsItem(activity)); } return items; } diff --git a/src/com/android/tv/ui/sidepanel/ActionItem.java b/src/com/android/tv/ui/sidepanel/ActionItem.java index c75eff9b..23aff91c 100644 --- a/src/com/android/tv/ui/sidepanel/ActionItem.java +++ b/src/com/android/tv/ui/sidepanel/ActionItem.java @@ -70,4 +70,4 @@ public abstract class ActionItem extends Item { iconView.setVisibility(View.GONE); } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java b/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java index a95b8149..7289034f 100644 --- a/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java +++ b/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java @@ -101,4 +101,4 @@ public class ChannelSourcesFragment extends SideFragment { .show(); } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/ui/sidepanel/CheckBoxItem.java b/src/com/android/tv/ui/sidepanel/CheckBoxItem.java index 205f1bc8..79c2b0a7 100644 --- a/src/com/android/tv/ui/sidepanel/CheckBoxItem.java +++ b/src/com/android/tv/ui/sidepanel/CheckBoxItem.java @@ -77,4 +77,4 @@ public class CheckBoxItem extends CompoundButtonItem { protected void onSelected() { setChecked(!isChecked()); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/ui/sidepanel/Item.java b/src/com/android/tv/ui/sidepanel/Item.java index da9b39b0..4ae6e523 100644 --- a/src/com/android/tv/ui/sidepanel/Item.java +++ b/src/com/android/tv/ui/sidepanel/Item.java @@ -86,4 +86,4 @@ public abstract class Item { } } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/ui/sidepanel/RadioButtonItem.java b/src/com/android/tv/ui/sidepanel/RadioButtonItem.java index b6c36795..e0477493 100644 --- a/src/com/android/tv/ui/sidepanel/RadioButtonItem.java +++ b/src/com/android/tv/ui/sidepanel/RadioButtonItem.java @@ -41,4 +41,4 @@ public class RadioButtonItem extends CompoundButtonItem { protected void onSelected() { setChecked(true); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/ui/sidepanel/SwitchItem.java b/src/com/android/tv/ui/sidepanel/SwitchItem.java index c04a72ac..ef9966a5 100644 --- a/src/com/android/tv/ui/sidepanel/SwitchItem.java +++ b/src/com/android/tv/ui/sidepanel/SwitchItem.java @@ -20,11 +20,15 @@ import com.android.tv.R; public class SwitchItem extends CompoundButtonItem { public SwitchItem(String title) { - super(title, null); + this(title, null, null); } public SwitchItem(String checkedTitle, String uncheckedTitle) { - super(checkedTitle, uncheckedTitle, null); + this(checkedTitle, uncheckedTitle, null); + } + + public SwitchItem(String checkedTitle, String uncheckedTitle, String description) { + super(checkedTitle, uncheckedTitle, description); } @Override @@ -41,4 +45,4 @@ public class SwitchItem extends CompoundButtonItem { protected void onSelected() { setChecked(!isChecked()); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java index c878900a..1df7fe59 100644 --- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java +++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java @@ -82,4 +82,4 @@ public class ProgramRestrictionsFragment extends SideFragment { items.add(ratingsItem); return items; } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 82d42377..fbc93f3a 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -162,6 +162,9 @@ public abstract class AsyncDbTask<Params, Progress, Result> } return null; } + } catch (SecurityException e) { + Log.d(TAG, "Security exception during query", e); + return null; } } diff --git a/src/com/android/tv/util/BooleanSystemProperty.java b/src/com/android/tv/util/BooleanSystemProperty.java deleted file mode 100644 index 6786868e..00000000 --- a/src/com/android/tv/util/BooleanSystemProperty.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.util; - -import android.util.Log; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; - -/** - * Lazy loaded boolean system property. - * - * <p>Set with <code>adb shell setprop <em>key</em> <em>value</em></code> where: - * Values 'n', 'no', '0', 'false' or 'off' are considered false. - * Values 'y', 'yes', '1', 'true' or 'on' are considered true. - * (case sensitive). See <a href= - * "https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/SystemProperties.java" - * >android.os.SystemProperties.getBoolean</a>. - */ -public final class BooleanSystemProperty { - private final static String TAG = "BooleanSystemProperty"; - private final static boolean DEBUG = false; - private static final List<BooleanSystemProperty> ALL_PROPERTIES = new ArrayList<>(); - private final boolean mDefaultValue; - private final String mKey; - private Boolean mValue = null; - - public BooleanSystemProperty(String key, boolean defaultValue) { - mDefaultValue = defaultValue; - mKey = key; - ALL_PROPERTIES.add(this); - } - - public static void resetAll() { - for (BooleanSystemProperty prop : ALL_PROPERTIES) { - prop.reset(); - } - } - - /** - * Gets system properties set by <code>adb shell setprop <em>key</em> <em>value</em></code> - * - * @param key the property key. - * @param defaultValue the value to return if the property is undefined or empty. - * @return the system property value or the default value. - */ - private static boolean getBoolean(String key, boolean defaultValue) { - try { - final Class<?> systemProperties = Class.forName("android.os.SystemProperties"); - final Method get = systemProperties.getMethod("getBoolean", String.class, Boolean.TYPE); - return (boolean) get.invoke(null, key, defaultValue); - } catch (Exception e) { - Log.e(TAG, "Error getting boolean for " + key, e); - // This should never happen - return defaultValue; - } - } - - /** - * Clears the cached value. The next call to getValue will check {@code - * android.os.SystemProperties}. - */ - public void reset() { - mValue = null; - } - - /** - * Returns the value of the system property. - * - * <p>If the value is cached get the value from {@code android.os.SystemProperties} with the - * default set in the constructor. - */ - public boolean getValue() { - if (mValue == null) { - mValue = getBoolean(mKey, mDefaultValue); - if (DEBUG) Log.d(TAG, mKey + "=" + mValue); - } - return mValue; - } -} diff --git a/src/com/android/tv/util/Clock.java b/src/com/android/tv/util/Clock.java index 58653068..f6c3782e 100644 --- a/src/com/android/tv/util/Clock.java +++ b/src/com/android/tv/util/Clock.java @@ -31,4 +31,4 @@ public interface Clock { return System.currentTimeMillis(); } }; -} +}
\ No newline at end of file diff --git a/src/com/android/tv/util/CollectionUtils.java b/src/com/android/tv/util/CollectionUtils.java new file mode 100644 index 00000000..e07bac90 --- /dev/null +++ b/src/com/android/tv/util/CollectionUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.util; + +import android.os.Build; +import android.util.ArraySet; + +import java.util.HashSet; +import java.util.Set; + +/** + * Static utilities for collections + */ +public class CollectionUtils { + /** + * Returns a new Set suitable for small data sets. + * + * <p>In M and above this is a ArraySet otherwise it is a HashSet + */ + public static <T> Set<T> createSmallSet() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return new ArraySet<T>(); + } else { + return new HashSet<T>(); + } + } +} diff --git a/src/com/android/tv/receiver/NotificationReceiver.java b/src/com/android/tv/util/EngOnlyFeature.java index 0bcb44c4..904e2369 100644 --- a/src/com/android/tv/receiver/NotificationReceiver.java +++ b/src/com/android/tv/util/EngOnlyFeature.java @@ -11,22 +11,22 @@ * 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. + * limitations under the License */ -package com.android.tv.receiver; +package com.android.tv.util; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import com.android.tv.recommendation.NotificationService; +import com.android.tv.BuildConfig; +import com.android.tv.common.feature.Feature; -public class NotificationReceiver extends BroadcastReceiver { +/** + * A feature that is only available on {@link BuildConfig#ENG} builds. + */ +public final class EngOnlyFeature implements Feature { @Override - public void onReceive(Context context, Intent intent) { - Intent notificationIntent = new Intent(context, NotificationService.class); - notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION); - context.startService(notificationIntent); + public boolean isEnabled(Context context) { + return BuildConfig.ENG; } } diff --git a/src/com/android/tv/util/ImageCache.java b/src/com/android/tv/util/ImageCache.java index 67a63a59..e849da89 100644 --- a/src/com/android/tv/util/ImageCache.java +++ b/src/com/android/tv/util/ImageCache.java @@ -20,13 +20,12 @@ import android.support.annotation.VisibleForTesting; import android.util.Log; import android.util.LruCache; -import com.android.tv.MainActivity; import com.android.tv.util.BitmapUtils.ScaledBitmapInfo; /** * A convenience class for caching bitmap. */ -public class ImageCache implements MainActivity.MemoryManageable { +public class ImageCache implements MemoryManageable { private static final float MAX_CACHE_SIZE_PERCENT = 0.8f; private static final float MIN_CACHE_SIZE_PERCENT = 0.05f; private static final float DEFAULT_CACHE_SIZE_PERCENT = 0.1f; diff --git a/src/com/android/tv/util/MainThreadExecutor.java b/src/com/android/tv/util/MainThreadExecutor.java new file mode 100644 index 00000000..817286f7 --- /dev/null +++ b/src/com/android/tv/util/MainThreadExecutor.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.util; + +import android.os.Handler; +import android.os.Looper; + +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * An executor service that executes its tasks on the main thread. + * + * Shutting down this executor is not supported. + */ +public class MainThreadExecutor extends AbstractExecutorService { + + private final static MainThreadExecutor INSTANCE = new MainThreadExecutor(); + + public final static MainThreadExecutor getInstance() { + return INSTANCE; + } + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable runnable) { + if (Looper.getMainLooper() == Looper.myLooper()) { + runnable.run(); + } else { + mHandler.post(runnable); + } + } + + /** + * Not supported and throws an exception when used. + */ + @Override + @Deprecated + public void shutdown() { + throw new UnsupportedOperationException(); + } + + /** + * Not supported and throws an exception when used. + */ + @Override + @Deprecated + public List<Runnable> shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + /** + * Not supported and throws an exception when used. + */ + @Override + @Deprecated + public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException { + throw new UnsupportedOperationException(); + } +}
\ No newline at end of file diff --git a/src/com/android/tv/util/MemoryManageable.java b/src/com/android/tv/util/MemoryManageable.java new file mode 100644 index 00000000..c5e5d869 --- /dev/null +++ b/src/com/android/tv/util/MemoryManageable.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.util; + +/** + * Interface for the fine-grained memory management. + * The class which wants to release memory based on the system constraints should inherit + * this interface and implement {@link #performTrimMemory}. + */ +public interface MemoryManageable { + /** + * For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}. + */ + void performTrimMemory(int level); +} diff --git a/src/com/android/tv/util/MultiLongSparseArray.java b/src/com/android/tv/util/MultiLongSparseArray.java new file mode 100644 index 00000000..4c067892 --- /dev/null +++ b/src/com/android/tv/util/MultiLongSparseArray.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.util; + +import android.support.annotation.VisibleForTesting; +import android.util.LongSparseArray; + +import java.util.Collections; +import java.util.Set; + +/** + * Uses a {@link LongSparseArray} to hold sets of {@code T}. + * + * <p>This has the same memory and performance trade offs listed in {@link LongSparseArray}. + */ +public class MultiLongSparseArray<T> { + @VisibleForTesting + static final int DEFAULT_MAX_EMPTIES_KEPT = 4; + private final LongSparseArray<Set<T>> mSparseArray; + private final Set<T>[] mEmptySets; + private int mEmptyIndex = -1; + + public MultiLongSparseArray() { + mSparseArray = new LongSparseArray<>(); + mEmptySets = new Set[DEFAULT_MAX_EMPTIES_KEPT]; + } + + public MultiLongSparseArray(int initialCapacity, int emptyCacheSize) { + mSparseArray = new LongSparseArray<>(initialCapacity); + mEmptySets = new Set[emptyCacheSize]; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, T value) { + Set<T> values = mSparseArray.get(key); + if (values == null) { + values = getEmptySet(); + mSparseArray.put(key, values); + } + values.add(value); + } + + /** + * Removes the value at the specified index. + */ + public void remove(long key, T value) { + Set<T> values = mSparseArray.get(key); + if (values != null) { + values.remove(value); + if (values.isEmpty()) { + mSparseArray.remove(key); + cacheEmptySet(values); + } + } + } + + /** + * Gets the set of Objects mapped from the specified key, or an empty set + * if no such mapping has been made. + */ + public Iterable<T> get(long key) { + Set<T> values = mSparseArray.get(key); + return values == null ? Collections.EMPTY_SET : values; + } + + /** + * Clears cached empty sets. + */ + public void clearEmptyCache() { + while (mEmptyIndex >= 0) { + mEmptySets[mEmptyIndex--] = null; + } + } + + @VisibleForTesting + int getEmptyCacheSize() { + return mEmptyIndex + 1; + } + + private void cacheEmptySet(Set<T> emptySet) { + if (mEmptyIndex < DEFAULT_MAX_EMPTIES_KEPT - 1) { + mEmptySets[++mEmptyIndex] = emptySet; + } + } + + private Set<T> getEmptySet() { + if (mEmptyIndex < 0) { + return CollectionUtils.createSmallSet(); + } + Set<T> emptySet = mEmptySets[mEmptyIndex]; + mEmptySets[mEmptyIndex--] = null; + return emptySet; + } + + @Override + public String toString() { + return mSparseArray.toString() + "(emptyCacheSize=" + getEmptyCacheSize() + ")"; + } +} diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java new file mode 100644 index 00000000..c693185e --- /dev/null +++ b/src/com/android/tv/util/OnboardingUtils.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract.Channels; +import android.preference.PreferenceManager; +import android.support.annotation.UiThread; + +import com.android.tv.TvApplication; +import com.android.tv.data.ChannelDataManager; + +/** + * A utility class related to onboarding experience. + */ +public final class OnboardingUtils { + private static final String PREF_KEY_IS_FIRST_BOOT = "pref_onbaording_is_first_boot"; + private static final String PREF_KEY_IS_FIRST_RUN = "pref_onbaording_is_first_run"; + private static final String PREF_KEY_ARE_CHANNELS_AVAILABLE = + "pref_onbaording_are_channels_available"; + + /** + * Checks if this is the first boot after the onboarding experience has been applied. + */ + public static boolean isFirstBoot(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(PREF_KEY_IS_FIRST_BOOT, true); + } + + /** + * Marks that the first boot has been completed. + */ + public static void setFirstBootCompleted(Context context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(PREF_KEY_IS_FIRST_BOOT, false) + .apply(); + } + + /** + * Checks if this is the first run of {@link com.android.tv.MainActivity} after the + * onboarding experience has been applied. + */ + public static boolean isFirstRun(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(PREF_KEY_IS_FIRST_RUN, true); + } + + /** + * Marks that the first run of {@link com.android.tv.MainActivity} has been completed. + */ + public static void setFirstRunCompleted(Context context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(PREF_KEY_IS_FIRST_RUN, false) + .apply(); + } + + /** + * Checks whether the onboarding screen should be shown or not. + */ + public static boolean needToShowOnboarding(Context context) { + return isFirstRun(context) || !areChannelsAvailable(context); + } + + /** + * Checks if there are any available tuner channels. + */ + @UiThread + public static boolean areChannelsAvailable(Context context) { + ChannelDataManager manager = ((TvApplication) context.getApplicationContext()) + .getChannelDataManager(); + if (manager.isDbLoadFinished()) { + return manager.getChannelCount() != 0; + } + // This method should block the UI thread. + ContentResolver resolver = context.getContentResolver(); + try (Cursor c = resolver.query(Channels.CONTENT_URI, new String[] {Channels._ID}, null, + null, null)) { + return c.getCount() != 0; + } + } +} diff --git a/src/com/android/tv/util/PermissionUtils.java b/src/com/android/tv/util/PermissionUtils.java new file mode 100644 index 00000000..f39dba81 --- /dev/null +++ b/src/com/android/tv/util/PermissionUtils.java @@ -0,0 +1,70 @@ +package com.android.tv.util; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +/** + * Util class to handle permissions. + */ +public class PermissionUtils { + private static Boolean sHasAccessAllEpgPermission; + private static Boolean sHasAccessWatchedHistoryPermission; + private static Boolean sHasModifyParentalControlsPermission; + + public static boolean hasAccessAllEpg(Context context) { + if (sHasAccessAllEpgPermission == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + sHasAccessAllEpgPermission = context.checkSelfPermission( + "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA") + == PackageManager.PERMISSION_GRANTED; + } else { + sHasAccessAllEpgPermission = context.getPackageManager().checkPermission( + "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA", + context.getPackageName()) == PackageManager.PERMISSION_GRANTED; + } + } + return sHasAccessAllEpgPermission; + } + + public static boolean hasAccessWatchedHistory(Context context) { + if (sHasAccessWatchedHistoryPermission == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + sHasAccessWatchedHistoryPermission = context.checkSelfPermission( + "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS") + == PackageManager.PERMISSION_GRANTED; + } else { + sHasAccessWatchedHistoryPermission = context.getPackageManager().checkPermission( + "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS", + context.getPackageName()) == PackageManager.PERMISSION_GRANTED; + } + } + return sHasAccessWatchedHistoryPermission; + } + + public static boolean hasModifyParentalControls(Context context) { + if (sHasModifyParentalControlsPermission == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + sHasModifyParentalControlsPermission = context.checkSelfPermission( + "android.permission.MODIFY_PARENTAL_CONTROLS") + == PackageManager.PERMISSION_GRANTED; + } else { + sHasModifyParentalControlsPermission = context.getPackageManager().checkPermission( + "android.permission.MODIFY_PARENTAL_CONTROLS", + context.getPackageName()) == PackageManager.PERMISSION_GRANTED; + } + } + return sHasModifyParentalControlsPermission; + } + + public static boolean hasReadTvListings(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return context.checkSelfPermission("android.permission.READ_TV_LISTINGS") + == PackageManager.PERMISSION_GRANTED; + } else { + return context.getPackageManager().checkPermission( + "android.permission.MODIFY_PARENTAL_CONTROLS", + context.getPackageName()) == PackageManager.PERMISSION_GRANTED; + } + } +} diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java index d5817907..3e4db654 100644 --- a/src/com/android/tv/util/PipInputManager.java +++ b/src/com/android/tv/util/PipInputManager.java @@ -30,7 +30,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -50,7 +49,7 @@ public class PipInputManager { private final ChannelTuner mChannelTuner; private boolean mStarted; private final Map<String, PipInput> mPipInputMap = new HashMap<>(); // inputId -> PipInput - private final Set<Listener> mListeners = new HashSet<>(); + private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); private final TvInputCallback mTvInputCallback = new TvInputCallback() { @Override diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java index f51918e9..dcede666 100644 --- a/src/com/android/tv/util/RecurringRunner.java +++ b/src/com/android/tv/util/RecurringRunner.java @@ -52,8 +52,8 @@ public final class RecurringRunner { } public void start() { + SoftPreconditions.checkState(!mRunning, TAG, "start is called twice."); if (mRunning) { - Utils.engThrowElseWarn(TAG, "start is called twice.", new IllegalStateException()); return; } mRunning = true; diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 683216a5..46299d3b 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -27,6 +27,10 @@ import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; + import java.util.HashSet; import java.util.Set; @@ -40,18 +44,20 @@ public class SetupUtils { // Known inputs are inputs which are shown in SetupView before. When a new input is installed, // the input will not be included in "PREF_KEY_KNOWN_INPUTS". private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs"; - // Set up inputs are inputs whose setup activity has been launched from Live channels app. + // Set up inputs are inputs whose setup activity has been launched and finished successfully. private static final String PREF_KEY_SET_UP_INPUTS = "set_up_inputs"; private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune"; private static SetupUtils sSetupUtils; + private final TvApplication mTvApplication; private final SharedPreferences mSharedPreferences; private final Set<String> mKnownInputs; private final Set<String> mSetUpInputs; private boolean mIsFirstTune; - private SetupUtils(Context context) { - mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + private SetupUtils(TvApplication tvApplication) { + mTvApplication = tvApplication; + mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(tvApplication); mSetUpInputs = new HashSet<>(mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, new HashSet<String>())); mKnownInputs = new HashSet<>(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, @@ -66,10 +72,66 @@ public class SetupUtils { if (sSetupUtils != null) { return sSetupUtils; } - sSetupUtils = new SetupUtils(context.getApplicationContext()); + sSetupUtils = new SetupUtils((TvApplication) context.getApplicationContext()); return sSetupUtils; } + /** + * Additional work after the setup of TV input. + */ + public void onTvInputSetupFinished(final String inputId, final Runnable postRunnable) { + // When TIS adds several channels, ChannelDataManager.Listener.onChannelList + // Updated() can be called several times. In this case, it is hard to detect + // which one is the last callback. To reduce error prune, we update channel + // list again and make all channels of {@code inputId} browsable. + onSetupDone(inputId); + final ChannelDataManager manager = mTvApplication.getChannelDataManager(); + if (!manager.isDbLoadFinished()) { + manager.addListener(new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + manager.removeListener(this); + updateChannelBrowsable(mTvApplication, inputId, postRunnable); + } + + @Override + public void onChannelListUpdated() { } + + @Override + public void onChannelBrowsableChanged() { } + }); + } else { + updateChannelBrowsable(mTvApplication, inputId, postRunnable); + } + } + + private static void updateChannelBrowsable(Context context, final String inputId, + final Runnable postRunnable) { + TvApplication tvApplication = (TvApplication) context.getApplicationContext(); + final ChannelDataManager manager = tvApplication.getChannelDataManager(); + manager.updateChannels(new Runnable() { + @Override + public void run() { + boolean browsableChanged = false; + for (Channel channel : manager.getChannelList()) { + if (channel.getInputId().equals(inputId)) { + if (!channel.isBrowsable()) { + manager.updateBrowsable(channel.getId(), true, true); + browsableChanged = true; + } + } + } + if (browsableChanged) { + manager.notifyChannelBrowsableChanged(); + manager.applyUpdatedValuesToDb(); + } + if (postRunnable != null) { + postRunnable.run(); + } + } + }); + } + public boolean isFirstTune() { return mIsFirstTune; } @@ -91,14 +153,14 @@ public class SetupUtils { } /** - * Returns true, if {@code inputId}'s setup activity has been launched. + * Returns {@code true}, if {@code inputId}'s setup has been done before. */ - public boolean hasSetupLaunched(String inputId) { - boolean launched = mSetUpInputs.contains(inputId); + public boolean isSetupDone(String inputId) { + boolean done = mSetUpInputs.contains(inputId); if (DEBUG) { - Log.d(TAG, "hasSetupLaunched: (input=" + inputId + ", result= " + launched + ")"); + Log.d(TAG, "isSetupDone: (input=" + inputId + ", result= " + done + ")"); } - return launched; + return done; } /** @@ -120,7 +182,7 @@ public class SetupUtils { */ public static void grantEpgPermissionToSetUpPackages(Context context) { // TvProvider allows granting of Uri permissions starting from MNC. - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); Set<String> setUpInputs = new HashSet<>(sharedPreferences.getStringSet( @@ -143,7 +205,7 @@ public class SetupUtils { */ public static void grantEpgPermission(Context context, String packageName) { // TvProvider allows granting of Uri permissions starting from MNC. - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (DEBUG) { Log.d(TAG, "grantEpgPermission(context=" + context + ", packageName=" + packageName + ")"); @@ -155,7 +217,7 @@ public class SetupUtils { context.grantUriPermission(packageName, TvContract.Programs.CONTENT_URI, modeFlags); } catch (SecurityException e) { Log.e(TAG, "Either TvProvider does not allow granting of Uri permissions or the app" - + " does not have permission" + e); + + " does not have permission.", e); } } } @@ -194,11 +256,15 @@ public class SetupUtils { } /** - * Called when an setup activity is launched. Once it is called, {@link #hasSetupLaunched} - * will return true for {@code inputId}. + * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} + * for {@code inputId}. */ - public void onSetupLaunched(String inputId) { - if (DEBUG) Log.d(TAG, "onSetupLaunched: input=" + inputId); + public void onSetupDone(String inputId) { + if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId); + if (!mKnownInputs.contains(inputId)) { + Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId); + mKnownInputs.add(inputId); + } mSetUpInputs.add(inputId); mSharedPreferences.edit() .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); diff --git a/src/com/android/tv/util/SoftPreconditions.java b/src/com/android/tv/util/SoftPreconditions.java new file mode 100644 index 00000000..5c2a8170 --- /dev/null +++ b/src/com/android/tv/util/SoftPreconditions.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.util; + +import android.text.TextUtils; +import android.util.Log; + +import com.android.tv.BuildConfig; + +/** + * Simple static methods to be called at the start of your own methods to verify + * correct arguments and state. + * + * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and + * logs a warning when it is false. + * + * <p>This is based on com.android.internal.util.Preconditions. + */ +public final class SoftPreconditions { + private static final String TAG = "SoftPreconditions"; + + /** + * Throws or logs if an expression involving the parameter of the calling + * method is not true. + * + * @param expression a boolean expression + * @param tag Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @throws IllegalArgumentException if {@code expression} is true + */ + public static void checkArgument(final boolean expression, String tag, String msg) { + if (!expression) { + warn(tag, "Illegal argument", msg, new IllegalArgumentException(msg)); + } + } + + /** + * Throws or logs if an expression involving the parameter of the calling + * method is not true. + * + * @param expression a boolean expression + * @throws IllegalArgumentException if {@code expression} is true + */ + public static void checkArgument(final boolean expression) { + checkArgument(expression, null, null); + } + + /** + * Throws or logs if an and object is null. + * + * @param reference an object reference + * @param tag Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @return true if the object is null + * @throws NullPointerException if {@code reference} is null + */ + public static <T> T checkNotNull(final T reference, String tag, String msg) { + if (reference == null) { + warn(tag, "Null Pointer", msg, new NullPointerException(msg)); + } + return reference; + } + + /** + * Throws or logs if an and object is null. + * + * @param reference an object reference + * @return true if the object is null + * @throws NullPointerException if {@code reference} is null + */ + public static <T> T checkNotNull(final T reference) { + return checkNotNull(reference, null, null); + } + + /** + * Throws or logs if an expression involving the state of the calling + * instance, but not involving any parameters to the calling method is not true. + * + * @param expression a boolean expression + * @param tag Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @throws IllegalStateException if {@code expression} is true + */ + public static void checkState(final boolean expression, String tag, String msg) { + if (!expression) { + warn(tag, "Illegal State", msg, new IllegalStateException(msg)); + } + } + + /** + * Throws or logs if an expression involving the state of the calling + * instance, but not involving any parameters to the calling method is not true. + * + * @param expression a boolean expression + * @throws IllegalStateException if {@code expression} is true + */ + public static void checkState(final boolean expression) { + checkState(expression, null, null); + } + + /** + * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. + * + * @param tag Used to identify the source of a log message. It usually + * identifies the class or activity where the log call occurs. + * @param msg The message you would like logged + * @param e The exception to throw + */ + public static void warn(String tag, String prefix, String msg, RuntimeException e) + throws RuntimeException{ + if (BuildConfig.ENG) { + throw e; + } else { + if (TextUtils.isEmpty(tag)) { + tag = TAG; + } + String logMessage; + if (msg == null) { + logMessage = prefix; + } else { + logMessage = prefix + ": " + msg; + } + Log.w(tag, logMessage, e); + } + } + + private SoftPreconditions() { + } +} diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java index 88266cad..235161b6 100644 --- a/src/com/android/tv/util/SystemProperties.java +++ b/src/com/android/tv/util/SystemProperties.java @@ -16,6 +16,8 @@ package com.android.tv.util; +import com.android.tv.common.BooleanSystemProperty; + /** * A convenience class for getting TV related system properties. */ @@ -51,12 +53,6 @@ public final class SystemProperties { "tv_use_debug_keys", false); /** - * When true search is available in the EPG. Defaults to false. - */ - public static final BooleanSystemProperty USE_EPG_SEARCH = new BooleanSystemProperty( - "tv_use_epg_search", false); // TODO: remove this flag. - - /** * Send {@link com.android.tv.analytics.Tracker} information. Defaults to {@code true}. */ public static final BooleanSystemProperty USE_TRACKER = new BooleanSystemProperty( diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index 66c0ba81..d11ecd82 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -29,8 +29,6 @@ import android.util.Log; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; -import junit.framework.Assert; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -50,8 +48,7 @@ public class TvInputManagerHelper { static { BUNDLED_PACKAGE_SET.add("com.android.tv"); - BUNDLED_PACKAGE_SET.add("com.android.tv"); - BUNDLED_PACKAGE_SET.add("com.google.android.usbtuner"); + BUNDLED_PACKAGE_SET.add("com.android.usbtuner"); } private final Context mContext; @@ -123,10 +120,6 @@ public class TvInputManagerHelper { return; } mStarted = true; - List<TvInputInfo> inputs = mTvInputManager.getTvInputList(); - if (inputs.size() < 1) { - return; - } mTvInputManager.registerCallback(mInternalCallback, mHandler); mInputMap.clear(); mInputStateMap.clear(); @@ -137,7 +130,8 @@ public class TvInputManagerHelper { mInputStateMap.put(inputId, state); mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input)); } - Assert.assertEquals(mInputStateMap.size(), mInputMap.size()); + SoftPreconditions.checkState(mInputStateMap.size() == mInputMap.size(), TAG, + "mInputStateMap not the same size as mInputMap"); mContentRatingsManager.update(); } @@ -215,7 +209,7 @@ public class TvInputManagerHelper { } /** - * Loads label of {@param info}. + * Loads label of {@code info}. * * It's visible for comparator test to mock TvInputInfo. * Package private is enough for this method, but public is necessary to workaround mockito @@ -230,18 +224,18 @@ public class TvInputManagerHelper { * Returns if TV input exists with the input id. */ public boolean hasTvInputInfo(String inputId) { + SoftPreconditions.checkState(mStarted, TAG, + "hasTvInputInfo() called before TvInputManagerHelper was started."); if (!mStarted) { - Utils.engThrowElseWarn(TAG, - "hasTvInputInfo() called before TvInputManagerHelper was started."); return false; } return !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null; } public TvInputInfo getTvInputInfo(String inputId) { + SoftPreconditions.checkState(mStarted, TAG, + "getTvInputInfo() called before TvInputManagerHelper was started."); if (!mStarted) { - Utils.engThrowElseWarn(TAG, - "getTvInputInfo() called before TvInputManagerHelper was started."); return null; } if (inputId == null) { @@ -270,8 +264,10 @@ public class TvInputManagerHelper { } public int getInputState(String inputId) { + SoftPreconditions.checkState(mStarted, TAG, "AvailabilityManager not started"); if (!mStarted) { - throw new IllegalStateException("AvailabilityManager doesn't started"); + return TvInputManager.INPUT_STATE_DISCONNECTED; + } Integer state = mInputStateMap.get(inputId); if (state == null) { diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index a0ed0924..81b469fe 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -17,11 +17,15 @@ package com.android.tv.util; import android.annotation.SuppressLint; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.Resources.Theme; import android.database.Cursor; import android.media.tv.TvContentRating; import android.media.tv.TvContract; @@ -29,6 +33,7 @@ import android.media.tv.TvContract.Channels; import android.media.tv.TvInputInfo; import android.media.tv.TvTrackInfo; import android.net.Uri; +import android.os.Build; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; @@ -38,14 +43,15 @@ import android.text.format.DateUtils; import android.util.Log; import android.view.View; -import com.android.tv.BuildConfig; +import com.android.tv.Features; import com.android.tv.R; +import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; - import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; @@ -386,6 +392,8 @@ public class Utils { String language = context.getString(R.string.default_language); if (track.getLanguage() != null) { language = new Locale(track.getLanguage()).getDisplayName(); + } else { + Log.d(TAG, "No language information found for the audio track: " + track); } StringBuilder metadata = new StringBuilder(); @@ -405,8 +413,13 @@ public class Utils { metadata.append(context.getString(R.string.multi_audio_channel_surround_8)); break; default: - metadata.append(context.getString(R.string.multi_audio_channel_suffix, - track.getAudioChannelCount())); + if (track.getAudioChannelCount() > 0) { + metadata.append(context.getString(R.string.multi_audio_channel_suffix, + track.getAudioChannelCount())); + } else { + Log.d(TAG, "Invalid audio channel count (" + track.getAudioChannelCount() + + ") found for the audio track: " + track); + } break; } if (showSampleRate) { @@ -568,30 +581,103 @@ public class Utils { } /** - * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. + * Check if the index is valid for the collection, + * @param collection the collection + * @param index the index position to test + * @return index >= 0 && index < collection.size(). + */ + public static boolean isIndexValid(@Nullable Collection<?> collection, int index) { + return collection == null ? false : index >= 0 && index < collection.size(); + } + + /** + * Returns a color integer associated with a particular resource ID. * - * @param tag Used to log message. - * @param msg The message + * @see #getColor(android.content.res.Resources,int,Theme) */ - public static void engThrowElseWarn(String tag, String msg) { - if (BuildConfig.ENG) { - throw new RuntimeException(msg); + public static int getColor(Resources res, int id) { + return getColor(res, id, null); + } + + /** + * Returns a color integer associated with a particular resource ID. + * + * <p>In M version, {@link android.content.res.Resources#getColor(int)} was deprecated and + * {@link android.content.res.Resources#getColor(int,Theme)} was newly added. + * + * @see android.content.res.Resources#getColor(int) + */ + public static int getColor(Resources res, int id, @Nullable Theme theme) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return res.getColor(id, theme); } else { - Log.w(tag, msg); + return res.getColor(id); } } /** - * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning. + * Returns a color state list associated with a particular resource ID. + * + * @see #getColorStateList(android.content.res.Resources,int,Theme) + */ + public static ColorStateList getColorStateList(Resources res, int id) { + return getColorStateList(res, id, null); + } + + /** + * Returns a color state list associated with a particular resource ID. + * + * <p>In M version, {@link android.content.res.Resources#getColorStateList(int)} was deprecated + * and {@link android.content.res.Resources#getColorStateList(int,Theme)} was newly added. * - * @param tag Used to log message. - * @param msg The message + * @see android.content.res.Resources#getColorStateList(int) */ - public static void engThrowElseWarn(String tag, String msg, RuntimeException e) { - if (BuildConfig.ENG) { - throw e; + public static ColorStateList getColorStateList(Resources res, int id, @Nullable Theme theme) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return res.getColorStateList(id, theme); } else { - Log.w(tag, msg); + return res.getColorStateList(id); + } + } + + + private static final class SyncRunnable implements Runnable { + private final Runnable mTarget; + private boolean mComplete; + + public SyncRunnable(Runnable target) { + mTarget = target; + } + + @Override + public void run() { + try { + mTarget.run(); + } finally { + synchronized (this) { + mComplete = true; + notifyAll(); + } + } + } + + public void waitForComplete() { + boolean interrupted = false; + synchronized (this) { + try { + while (!mComplete) { + try { + wait(); + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } } } } |