diff options
Diffstat (limited to 'src/com/android')
22 files changed, 880 insertions, 22 deletions
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java index fe138980..351f0010 100644 --- a/src/com/android/tv/ChannelTuner.java +++ b/src/com/android/tv/ChannelTuner.java @@ -36,7 +36,7 @@ import java.util.Map; import java.util.Set; /** - * It manages the current tuned channel among browsable channels. And it determines the next channel + * Manages the current tuned channel among browsable channels, and determines the next channel * by channel up/down. But, it doesn't actually tune through TvView. */ @MainThread diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java index ea17751b..57fc883e 100644 --- a/src/com/android/tv/InputSessionManager.java +++ b/src/com/android/tv/InputSessionManager.java @@ -18,6 +18,7 @@ package com.android.tv; import android.annotation.TargetApi; import android.content.Context; +import android.media.tv.AitInfo; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; import android.media.tv.TvTrackInfo; @@ -582,6 +583,12 @@ public class InputSessionManager { public void onSignalStrength(String inputId, int value) { mDelegate.onSignalStrength(inputId, value); } + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onAitInfoUpdated(String inputId, AitInfo aitInfo) { + mDelegate.onAitInfoUpdated(inputId, aitInfo); + } } /** Called when the {@link TvView} channel is changed. */ diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index 8dbafe47..cea293de 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -18,6 +18,7 @@ package com.android.tv; import static com.android.tv.common.feature.SystemAppFeature.SYSTEM_APP_FEATURE; +import android.annotation.TargetApi; import android.app.Activity; import android.app.PendingIntent; import android.app.SearchManager; @@ -32,6 +33,7 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; import android.hardware.display.DisplayManager; +import android.media.tv.AitInfo; import android.media.tv.TvContentRating; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; @@ -40,6 +42,8 @@ import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.media.tv.TvTrackInfo; import android.media.tv.TvView.OnUnhandledInputEventListener; +import android.media.tv.interactive.TvInteractiveAppManager; +import android.media.tv.interactive.TvInteractiveAppView; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -105,6 +109,8 @@ import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener; import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.dialog.InteractiveAppDialogFragment; +import com.android.tv.dialog.InteractiveAppDialogFragment.OnInteractiveAppCheckedListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.recorder.ConflictChecker; @@ -115,6 +121,7 @@ import com.android.tv.dvr.ui.DvrStopRecordingFragment; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.features.TvFeatures; import com.android.tv.guide.ProgramItemView; +import com.android.tv.interactive.IAppManager; import com.android.tv.menu.Menu; import com.android.tv.onboarding.OnboardingActivity; import com.android.tv.parental.ContentRatingsManager; @@ -193,7 +200,8 @@ public class MainActivity extends Activity OnPinCheckedListener, ChannelChanger, HasSingletons<MySingletons>, - HasAndroidInjector { + HasAndroidInjector, + OnInteractiveAppCheckedListener { private static final String TAG = "MainActivity"; private static final boolean DEBUG = false; private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; @@ -254,6 +262,9 @@ public class MainActivity extends Activity SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF); SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_ON); SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_TIME_CHANGED); + if (Build.VERSION.SDK_INT > 33) { // TIRAMISU + SYSTEM_INTENT_FILTER.addAction(TvInteractiveAppManager.ACTION_APP_LINK_COMMAND); + } } private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; @@ -365,6 +376,8 @@ public class MainActivity extends Activity private String mLastInputIdFromIntent; + private IAppManager mIAppManager; + private final Handler mHandler = new MainActivityHandler(this); private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>(); @@ -406,6 +419,13 @@ public class MainActivity extends Activity tune(true); } break; + case TvInteractiveAppManager.ACTION_APP_LINK_COMMAND: + if (DEBUG) { + Log.d(TAG, "Received action link command"); + } + // TODO: handle the command + break; + default: // fall out } } @@ -545,8 +565,10 @@ public class MainActivity extends Activity return; } setContentView(R.layout.activity_tv); + TvInteractiveAppView tvInteractiveAppView = findViewById(R.id.tv_app_view); mTvView = findViewById(R.id.main_tunable_tv_view); - mTvView.initialize(mProgramDataManager, mTvInputManagerHelper, mLegacyFlags); + mTvView.initialize( + mProgramDataManager, mTvInputManagerHelper, mLegacyFlags, tvInteractiveAppView); mTvView.setOnUnhandledInputEventListener( new OnUnhandledInputEventListener() { @Override @@ -717,8 +739,8 @@ public class MainActivity extends Activity mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null); mAudioCapabilitiesReceiver.register(); Intent nowPlayingIntent = new Intent(this, MainActivity.class); - PendingIntent pendingIntent = - PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0); + PendingIntent pendingIntent = PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, + nowPlayingIntent, PendingIntent.FLAG_IMMUTABLE); mMediaSessionWrapper = new MediaSessionWrapper(this, pendingIntent); mTvViewUiManager.restoreDisplayMode(false); @@ -732,9 +754,21 @@ public class MainActivity extends Activity mDvrConflictChecker = new ConflictChecker(this); } initForTest(); + if (TvFeatures.HAS_TIAF.isEnabled(this)) { + mIAppManager = new IAppManager(this, mTvView, mHandler); + } Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end"); } + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onInteractiveAppChecked(boolean checked) { + TvSettings.setTvIAppOn(getApplicationContext(), checked); + if (checked) { + mIAppManager.processHeldAitInfo(); + } + } + private void startOnboardingActivity() { startActivity(OnboardingActivity.buildIntent(this, getIntent())); finish(); @@ -833,7 +867,7 @@ public class MainActivity extends Activity mMainDurationTimer.start(); applyParentalControlSettings(); - registerReceiver(mBroadcastReceiver, SYSTEM_INTENT_FILTER); + registerReceiver(mBroadcastReceiver, SYSTEM_INTENT_FILTER, Context.RECEIVER_EXPORTED); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { Intent notificationIntent = new Intent(this, NotificationService.class); @@ -1081,7 +1115,7 @@ public class MainActivity extends Activity } mTvView.start(); - mAudioManagerHelper.setVolumeByAudioFocusStatus(); + mAudioManagerHelper.requestAudioFocus(); tune(true); } @@ -1126,6 +1160,9 @@ public class MainActivity extends Activity private void stopAll(boolean keepVisibleBehind) { mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION); stopTv("stopAll()", keepVisibleBehind); + if (mIAppManager != null) { + mIAppManager.stop(); + } } public TvInputManagerHelper getTvInputManagerHelper() { @@ -1138,7 +1175,7 @@ public class MainActivity extends Activity * @param calledByPopup If true, startSetupActivity is invoked from the setup fragment. */ public void startSetupActivity(TvInputInfo input, boolean calledByPopup) { - Intent intent = CommonUtils.createSetupIntent(input); + Intent intent = mSetupUtils.createSetupIntent(this, input); if (intent == null) { Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show(); return; @@ -1425,6 +1462,9 @@ public class MainActivity extends Activity if (DeveloperPreferences.LOG_KEYEVENT.get(this)) { Log.d(TAG, "dispatchKeyEvent(" + event + ")"); } + if (mIAppManager != null && mIAppManager.dispatchKeyEvent(event)) { + return true; + } // If an activity is closed on a back key down event, back key down events with none zero // repeat count or a back key up event can be happened without the first back key down // event which should be ignored in this activity. @@ -1631,7 +1671,7 @@ public class MainActivity extends Activity } } - private void stopTv() { + public void stopTv() { stopTv(null, false); } @@ -1932,12 +1972,21 @@ public class MainActivity extends Activity @VisibleForTesting protected void applyMultiAudio(String trackId) { + applyMultiAudio(false, trackId); + } + + @VisibleForTesting + protected void applyMultiAudio(boolean allowAutoSelection, String trackId) { + if (!allowAutoSelection && trackId == null) { + selectTrack(TvTrackInfo.TYPE_AUDIO, null, UNDEFINED_TRACK_INDEX); + mTvOptionsManager.onMultiAudioChanged(null); + return; + } List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO); if (tracks == null) { mTvOptionsManager.onMultiAudioChanged(null); return; } - TvTrackInfo bestTrack = null; if (trackId != null) { for (TvTrackInfo track : tracks) { @@ -2459,7 +2508,7 @@ public class MainActivity extends Activity return handled; } - private boolean isKeyEventBlocked() { + public boolean isKeyEventBlocked() { // If the current channel is a passthrough channel, we don't handle the key events in TV // activity. Instead, the key event will be handled by the passthrough TV input. return mChannelTuner.isCurrentChannelPassthrough(); @@ -2907,7 +2956,7 @@ public class MainActivity extends Activity } applyDisplayRefreshRate(info.getVideoFrameRate()); mTvViewUiManager.updateTvAspectRatio(); - applyMultiAudio( + applyMultiAudio(allowAutoSelectionOfTrack, allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO)); applyClosedCaption(); mOverlayManager.getMenu().onStreamInfoChanged(); @@ -2989,6 +3038,14 @@ public class MainActivity extends Activity TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH); } } + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onAitInfoUpdated(String inputId, AitInfo aitInfo) { + if (mIAppManager != null) { + mIAppManager.onAitInfoUpdated(aitInfo); + } + } } private class MySingletonsImpl implements MySingletons { @@ -3047,5 +3104,8 @@ public class MainActivity extends Activity @ContributesAndroidInjector abstract DvrScheduleFragment contributesDvrScheduleFragment(); + + @ContributesAndroidInjector + abstract InteractiveAppDialogFragment contributesInteractiveAppDialogFragment(); } } diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java index e7f89108..2a4a556f 100644 --- a/src/com/android/tv/SetupPassthroughActivity.java +++ b/src/com/android/tv/SetupPassthroughActivity.java @@ -109,7 +109,6 @@ public class SetupPassthroughActivity extends Activity { finish(); return; } - SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion); // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during // setupIntent.putExtras(intent.getExtras()). @@ -127,6 +126,7 @@ public class SetupPassthroughActivity extends Activity { finish(); return; } + SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName); startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY); } catch (ActivityNotFoundException e) { Log.e(TAG, "Can't find activity: " + setupIntent.getComponent()); diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java index 5d0e9c82..59e2406f 100644 --- a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java +++ b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java @@ -15,11 +15,14 @@ */ package com.android.tv.audiotvservice; +import android.annotation.TargetApi; import android.app.Notification; import android.app.Service; import android.content.Intent; import android.media.session.MediaSession; +import android.media.tv.AitInfo; import android.net.Uri; +import android.os.Build; import android.os.IBinder; import android.support.annotation.Nullable; import android.util.Log; @@ -99,4 +102,8 @@ public class AudioOnlyTvService extends Service implements OnTuneListener { @Override public void onChannelSignalStrength() {} + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onAitInfoUpdated(String inputId, AitInfo aitInfo) {} } diff --git a/src/com/android/tv/data/ChannelImpl.java b/src/com/android/tv/data/ChannelImpl.java index f31290d0..5be1179d 100644 --- a/src/com/android/tv/data/ChannelImpl.java +++ b/src/com/android/tv/data/ChannelImpl.java @@ -18,6 +18,7 @@ package com.android.tv.data; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.media.tv.TvContract; @@ -673,7 +674,18 @@ public final class ChannelImpl implements Channel { if (!TextUtils.isEmpty(mAppLinkText) && !TextUtils.isEmpty(mAppLinkIntentUri)) { try { Intent intent = Intent.parseUri(mAppLinkIntentUri, Intent.URI_INTENT_SCHEME); - if (intent.resolveActivityInfo(pm, 0) != null) { + ActivityInfo activityInfo = intent.resolveActivityInfo(pm, 0); + if (activityInfo != null) { + String packageName = activityInfo.packageName; + // Prevent creation of App Links to private activities in this package + boolean isProtectedActivity = packageName != null + && (packageName.equals(CommonConstants.BASE_PACKAGE) + || packageName.startsWith(CommonConstants.BASE_PACKAGE + ".")); + if (isProtectedActivity) { + Log.w(TAG,"Attempt to add app link to protected activity: " + + mAppLinkIntentUri); + return; + } mAppLinkIntent = intent; mAppLinkIntent.putExtra( CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index e4237bf4..f323423c 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -44,6 +44,8 @@ public interface StreamInfo { int getAudioChannelCount(); + float getStreamVolume(); + boolean hasClosedCaption(); boolean isVideoAvailable(); diff --git a/src/com/android/tv/dialog/InteractiveAppDialogFragment.java b/src/com/android/tv/dialog/InteractiveAppDialogFragment.java new file mode 100755 index 00000000..c5ffbaac --- /dev/null +++ b/src/com/android/tv/dialog/InteractiveAppDialogFragment.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2022 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.dialog; + +import android.annotation.TargetApi; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.Button; +import android.widget.TextView; +import com.android.tv.R; +import com.android.tv.common.SoftPreconditions; + +import java.util.function.Function; + +import dagger.android.AndroidInjection; + +@TargetApi(Build.VERSION_CODES.TIRAMISU) +public class InteractiveAppDialogFragment extends SafeDismissDialogFragment { + private static final boolean DEBUG = false; + + public static final String DIALOG_TAG = InteractiveAppDialogFragment.class.getName(); + private static final String TRACKER_LABEL = "Interactive App Dialog"; + private static final String TV_IAPP_NAME = "tv_iapp_name"; + private boolean mIsChoseOK; + private String mIAppName; + private Function mUpdateAitInfo; + + public static InteractiveAppDialogFragment create(String iappName) { + InteractiveAppDialogFragment fragment = new InteractiveAppDialogFragment(); + Bundle args = new Bundle(); + args.putString(TV_IAPP_NAME, iappName); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onAttach(Context context) { + AndroidInjection.inject(this); + super.onAttach(context); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIAppName = getArguments().getString(TV_IAPP_NAME); + setStyle(STYLE_NO_TITLE, 0); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dlg = super.onCreateDialog(savedInstanceState); + dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation; + mIsChoseOK = false; + return dlg; + } + + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } + + @Override + public void onStart() { + super.onStart(); + // Dialog size is determined by its windows size, not inflated view size. + // So apply view size to window after the DialogFragment.onStart() where dialog is shown. + Dialog dlg = getDialog(); + if (dlg != null) { + dlg.getWindow() + .setLayout( + getResources().getDimensionPixelSize(R.dimen.pin_dialog_width), + LayoutParams.WRAP_CONTENT); + } + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View v = inflater.inflate(R.layout.tv_app_dialog, container, false); + TextView mTitleView = (TextView) v.findViewById(R.id.title); + mTitleView.setText(getString(R.string.tv_app_dialog_title, mIAppName)); + Button okButton = v.findViewById(R.id.ok); + okButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + exit(true); + } + }); + Button cancelButton = v.findViewById(R.id.cancel); + cancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + exit(false); + } + }); + return v; + } + + private void exit(boolean isokclick) { + mIsChoseOK = isokclick; + dismiss(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + SoftPreconditions.checkState(getActivity() instanceof OnInteractiveAppCheckedListener); + if (getActivity() instanceof OnInteractiveAppCheckedListener) { + ((OnInteractiveAppCheckedListener) getActivity()) + .onInteractiveAppChecked(mIsChoseOK); + } + } + + public interface OnInteractiveAppCheckedListener { + void onInteractiveAppChecked(boolean checked); + } +} diff --git a/src/com/android/tv/dvr/recorder/RecordingScheduler.java b/src/com/android/tv/dvr/recorder/RecordingScheduler.java index f309537d..475c17f8 100644 --- a/src/com/android/tv/dvr/recorder/RecordingScheduler.java +++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java @@ -322,7 +322,8 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START; if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt); Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); - PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); + PendingIntent alarmIntent = + PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE); // This will cancel the previous alarm. mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); } else { diff --git a/src/com/android/tv/features/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java index 5282c28c..ebd7cb9a 100644 --- a/src/com/android/tv/features/TvFeatures.java +++ b/src/com/android/tv/features/TvFeatures.java @@ -101,5 +101,8 @@ public final class TvFeatures extends CommonFeatures { /** Use input blocklist to disable partner's tuner input. */ public static final Feature USE_PARTNER_INPUT_BLOCKLIST = ON; + /** Support for interactive applications using the TIAF **/ + public static final Feature HAS_TIAF = Sdk.AT_LEAST_T; + private TvFeatures() {} } diff --git a/src/com/android/tv/interactive/IAppManager.java b/src/com/android/tv/interactive/IAppManager.java new file mode 100644 index 00000000..682b35c6 --- /dev/null +++ b/src/com/android/tv/interactive/IAppManager.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2022 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.interactive; + +import static com.android.tv.util.CaptionSettings.OPTION_OFF; +import static com.android.tv.util.CaptionSettings.OPTION_ON; + +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.media.tv.TvTrackInfo; +import android.media.tv.interactive.TvInteractiveAppManager; +import android.media.tv.AitInfo; +import android.media.tv.interactive.TvInteractiveAppService; +import android.media.tv.interactive.TvInteractiveAppServiceInfo; +import android.media.tv.interactive.TvInteractiveAppView; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.InputEvent; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.util.ContentUriUtils; +import com.android.tv.data.api.Channel; +import com.android.tv.dialog.InteractiveAppDialogFragment; +import com.android.tv.features.TvFeatures; +import com.android.tv.ui.TunableTvView; +import com.android.tv.util.TvSettings; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@TargetApi(Build.VERSION_CODES.TIRAMISU) +public class IAppManager { + private static final String TAG = "IAppManager"; + private static final boolean DEBUG = false; + + private final MainActivity mMainActivity; + private final TvInteractiveAppManager mTvIAppManager; + private final TvInteractiveAppView mTvIAppView; + private final TunableTvView mTvView; + private final Handler mHandler; + private AitInfo mCurrentAitInfo; + private AitInfo mHeldAitInfo; // AIT info that has been held pending dialog confirmation + private boolean mTvAppDialogShown = false; + + public IAppManager(@NonNull MainActivity parentActivity, @NonNull TunableTvView tvView, + @NonNull Handler handler) { + SoftPreconditions.checkFeatureEnabled(parentActivity, TvFeatures.HAS_TIAF, TAG); + + mMainActivity = parentActivity; + mTvView = tvView; + mHandler = handler; + mTvIAppManager = mMainActivity.getSystemService(TvInteractiveAppManager.class); + mTvIAppView = mMainActivity.findViewById(R.id.tv_app_view); + if (mTvIAppManager == null || mTvIAppView == null) { + Log.e(TAG, "Could not find interactive app view or manager"); + return; + } + + ExecutorService executor = Executors.newSingleThreadExecutor(); + mTvIAppManager.registerCallback( + executor, + new MyInteractiveAppManagerCallback() + ); + mTvIAppView.setCallback( + executor, + new MyInteractiveAppViewCallback() + ); + mTvIAppView.setOnUnhandledInputEventListener(executor, + inputEvent -> { + if (mMainActivity.isKeyEventBlocked()) { + return true; + } + if (inputEvent instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent) inputEvent; + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN + && keyEvent.isLongPress()) { + if (mMainActivity.onKeyLongPress(keyEvent.getKeyCode(), keyEvent)) { + return true; + } + } + if (keyEvent.getAction() == KeyEvent.ACTION_UP) { + return mMainActivity.onKeyUp(keyEvent.getKeyCode(), keyEvent); + } else if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + return mMainActivity.onKeyDown(keyEvent.getKeyCode(), keyEvent); + } + } + return false; + }); + } + + public void stop() { + mTvIAppView.stopInteractiveApp(); + mTvIAppView.reset(); + mCurrentAitInfo = null; + } + + /* + * Update current info based on ait info that was held when the dialog was shown. + */ + public void processHeldAitInfo() { + if (mHeldAitInfo != null) { + onAitInfoUpdated(mHeldAitInfo); + } + } + + public boolean dispatchKeyEvent(KeyEvent event) { + if (mTvIAppView != null && mTvIAppView.getVisibility() == View.VISIBLE + && mTvIAppView.dispatchKeyEvent(event)){ + return true; + } + return false; + } + + public void onAitInfoUpdated(AitInfo aitInfo) { + if (mTvIAppManager == null || aitInfo == null) { + return; + } + if (mCurrentAitInfo != null && mCurrentAitInfo.getType() == aitInfo.getType()) { + if (DEBUG) { + Log.d(TAG, "Ignoring AIT update: Same type as current"); + } + return; + } + + List<TvInteractiveAppServiceInfo> tvIAppInfoList = + mTvIAppManager.getTvInteractiveAppServiceList(); + if (tvIAppInfoList.isEmpty()) { + if (DEBUG) { + Log.d(TAG, "Ignoring AIT update: No interactive app services registered"); + } + return; + } + + // App Type ID numbers allocated by DVB Services + int type = -1; + switch (aitInfo.getType()) { + case 0x0010: // HBBTV + type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_HBBTV; + break; + case 0x0006: // DCAP-J: DCAP Java applications + case 0x0007: // DCAP-X: DCAP XHTML applications + type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_ATSC; + break; + case 0x0001: // Ginga-J + case 0x0009: // Ginga-NCL + case 0x000b: // Ginga-HTML5 + type = TvInteractiveAppServiceInfo.INTERACTIVE_APP_TYPE_GINGA; + break; + default: + Log.e(TAG, "AIT info contained unknown type: " + aitInfo.getType()); + return; + } + + if (TvSettings.isTvIAppOn(mMainActivity.getApplicationContext())) { + mTvAppDialogShown = false; + for (TvInteractiveAppServiceInfo info : tvIAppInfoList) { + if ((info.getSupportedTypes() & type) > 0) { + mCurrentAitInfo = aitInfo; + if (mTvIAppView != null) { + mTvIAppView.setVisibility(View.VISIBLE); + mTvIAppView.prepareInteractiveApp(info.getId(), type); + } + break; + } + } + } else if (!mTvAppDialogShown) { + if (DEBUG) { + Log.d(TAG, "TV IApp is not enabled"); + } + + for (TvInteractiveAppServiceInfo info : tvIAppInfoList) { + if ((info.getSupportedTypes() & type) > 0) { + mMainActivity.getOverlayManager().showDialogFragment( + InteractiveAppDialogFragment.DIALOG_TAG, + InteractiveAppDialogFragment.create(info.getServiceInfo().packageName), + false); + mHeldAitInfo = aitInfo; + mTvAppDialogShown = true; + break; + } + } + } + } + + private class MyInteractiveAppManagerCallback extends + TvInteractiveAppManager.TvInteractiveAppCallback { + @Override + public void onInteractiveAppServiceAdded(String iAppServiceId) {} + + @Override + public void onInteractiveAppServiceRemoved(String iAppServiceId) {} + + @Override + public void onInteractiveAppServiceUpdated(String iAppServiceId) {} + + @Override + public void onTvInteractiveAppServiceStateChanged(String iAppServiceId, int type, int state, + int err) { + if (state == TvInteractiveAppManager.SERVICE_STATE_READY && mTvIAppView != null) { + mTvIAppView.startInteractiveApp(); + mTvIAppView.setTvView(mTvView.getTvView()); + if (mTvView.getTvView() != null) { + mTvView.getTvView().setInteractiveAppNotificationEnabled(true); + } + } + } + } + + private class MyInteractiveAppViewCallback extends + TvInteractiveAppView.TvInteractiveAppCallback { + @Override + public void onPlaybackCommandRequest(String iAppServiceId, String cmdType, + Bundle parameters) { + if (mTvView == null || cmdType == null) { + return; + } + switch (cmdType) { + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE: + if (parameters == null) { + return; + } + String uriString = parameters.getString( + TvInteractiveAppService.COMMAND_PARAMETER_KEY_CHANNEL_URI); + if (uriString != null) { + Uri channelUri = Uri.parse(uriString); + Channel channel = mMainActivity.getChannelDataManager().getChannel( + ContentUriUtils.safeParseId(channelUri)); + if (channel != null) { + mHandler.post(() -> mMainActivity.tuneToChannel(channel)); + } + } + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SELECT_TRACK: + if (mTvView != null && parameters != null) { + int trackType = parameters.getInt( + TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_TYPE, + -1); + String trackId = parameters.getString( + TvInteractiveAppService.COMMAND_PARAMETER_KEY_TRACK_ID, + null); + switch (trackType) { + case TvTrackInfo.TYPE_AUDIO: + // When trackId is null, deselects current audio track. + mHandler.post(() -> mMainActivity.selectAudioTrack(trackId)); + break; + case TvTrackInfo.TYPE_SUBTITLE: + // When trackId is null, turns off captions. + mHandler.post(() -> mMainActivity.selectSubtitleTrack( + trackId == null ? OPTION_OFF : OPTION_ON, trackId)); + break; + } + } + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_SET_STREAM_VOLUME: + if (parameters == null) { + return; + } + float volume = parameters.getFloat( + TvInteractiveAppService.COMMAND_PARAMETER_KEY_VOLUME, -1); + if (volume >= 0.0 && volume <= 1.0) { + mHandler.post(() -> mTvView.setStreamVolume(volume)); + } + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_NEXT: + mHandler.post(mMainActivity::channelUp); + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_TUNE_PREV: + mHandler.post(mMainActivity::channelDown); + break; + case TvInteractiveAppService.PLAYBACK_COMMAND_TYPE_STOP: + int mode = 1; // TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK + if (parameters != null) { + mode = parameters.getInt( + /* TvInteractiveAppService.COMMAND_PARAMETER_KEY_STOP_MODE */ + "command_stop_mode", + /*TvInteractiveAppService.COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK*/ + 1); + } + mHandler.post(mMainActivity::stopTv); + break; + default: + Log.e(TAG, "PlaybackCommandRequest had unknown cmdType:" + + cmdType); + break; + } + } + + @Override + public void onStateChanged(String iAppServiceId, int state, int err) { + } + + @Override + public void onBiInteractiveAppCreated(String iAppServiceId, Uri biIAppUri, + String biIAppId) {} + + @Override + public void onTeletextAppStateChanged(String iAppServiceId, int state) {} + + @Override + public void onSetVideoBounds(String iAppServiceId, Rect rect) { + if (mTvView != null) { + ViewGroup.MarginLayoutParams layoutParams = mTvView.getTvViewLayoutParams(); + layoutParams.setMargins(rect.left, rect.top, rect.right, rect.bottom); + mTvView.setTvViewLayoutParams(layoutParams); + } + } + + @Override + @TargetApi(34) + public void onRequestCurrentVideoBounds(@NonNull String iAppServiceId) { + mHandler.post( + () -> { + if (DEBUG) { + Log.d(TAG, "onRequestCurrentVideoBounds service ID = " + + iAppServiceId); + } + Rect bounds = new Rect(mTvView.getLeft(), mTvView.getTop(), + mTvView.getRight(), mTvView.getBottom()); + mTvIAppView.sendCurrentVideoBounds(bounds); + }); + } + + @Override + public void onRequestCurrentChannelUri(String iAppServiceId) { + if (mTvIAppView == null) { + return; + } + Channel currentChannel = mMainActivity.getCurrentChannel(); + Uri currentUri = (currentChannel == null) + ? null + : currentChannel.getUri(); + mTvIAppView.sendCurrentChannelUri(currentUri); + } + + @Override + public void onRequestCurrentChannelLcn(String iAppServiceId) { + if (mTvIAppView == null) { + return; + } + Channel currentChannel = mMainActivity.getCurrentChannel(); + if (currentChannel == null || currentChannel.getDisplayNumber() == null) { + return; + } + // Expected format is major channel number, delimiter, minor channel number + String displayNumber = currentChannel.getDisplayNumber(); + String format = "[0-9]+" + Channel.CHANNEL_NUMBER_DELIMITER + "[0-9]+"; + if (!displayNumber.matches(format)) { + return; + } + // Major channel number is returned + String[] numbers = displayNumber.split( + String.valueOf(Channel.CHANNEL_NUMBER_DELIMITER)); + mTvIAppView.sendCurrentChannelLcn(Integer.parseInt(numbers[0])); + } + + @Override + public void onRequestStreamVolume(String iAppServiceId) { + if (mTvIAppView == null || mTvView == null) { + return; + } + mTvIAppView.sendStreamVolume(mTvView.getStreamVolume()); + } + + @Override + public void onRequestTrackInfoList(String iAppServiceId) { + if (mTvIAppView == null || mTvView == null) { + return; + } + List<TvTrackInfo> allTracks = new ArrayList<>(); + int[] trackTypes = new int[] {TvTrackInfo.TYPE_AUDIO, + TvTrackInfo.TYPE_VIDEO, TvTrackInfo.TYPE_SUBTITLE}; + + for (int trackType : trackTypes) { + List<TvTrackInfo> currentTracks = mTvView.getTracks(trackType); + if (currentTracks == null) { + continue; + } + for (TvTrackInfo track : currentTracks) { + if (track != null) { + allTracks.add(track); + } + } + } + mTvIAppView.sendTrackInfoList(allTracks); + } + + @Override + public void onRequestCurrentTvInputId(String iAppServiceId) { + if (mTvIAppView == null) { + return; + } + Channel currentChannel = mMainActivity.getCurrentChannel(); + String currentInputId = (currentChannel == null) + ? null + : currentChannel.getInputId(); + mTvIAppView.sendCurrentTvInputId(currentInputId); + } + + @Override + public void onRequestSigning(String iAppServiceId, String signingId, String algorithm, + String alias, byte[] data) {} + } +} diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java index dd386d81..0ce5d931 100644 --- a/src/com/android/tv/onboarding/OnboardingActivity.java +++ b/src/com/android/tv/onboarding/OnboardingActivity.java @@ -193,7 +193,7 @@ public class OnboardingActivity extends SetupActivity { params.getString( SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); TvInputInfo input = mInputManager.getTvInputInfo(inputId); - Intent intent = CommonUtils.createSetupIntent(input); + Intent intent = mSetupUtils.createSetupIntent(this, input); if (intent == null) { Toast.makeText( this, diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java index 5fa7606d..9578e243 100644 --- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java +++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java @@ -67,7 +67,8 @@ public final class AudioCapabilitiesReceiver { } public void register() { - mContext.registerReceiver(mReceiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)); + mContext.registerReceiver(mReceiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG), + Context.RECEIVER_EXPORTED); } public void unregister() { diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java index 0eb03bec..0bf6ecf3 100644 --- a/src/com/android/tv/receiver/BootCompletedReceiver.java +++ b/src/com/android/tv/receiver/BootCompletedReceiver.java @@ -56,6 +56,11 @@ public class BootCompletedReceiver extends BroadcastReceiver { Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); return; } + String action = intent.getAction(); + if (!Intent.ACTION_BOOT_COMPLETED.equals(action)) { + Log.w(TAG, "invalid action " + action); + return; + } if (DEBUG) Log.d(TAG, "boot completed " + intent); Starter.start(context); diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java index 7bf04692..b39ac4ea 100644 --- a/src/com/android/tv/setup/SystemSetupActivity.java +++ b/src/com/android/tv/setup/SystemSetupActivity.java @@ -53,6 +53,7 @@ public class SystemSetupActivity extends SetupActivity { private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; @Inject TvInputManagerHelper mInputManager; + @Inject SetupUtils mSetupUtils; @Inject UiFlags mUiFlags; @Override @@ -97,7 +98,7 @@ public class SystemSetupActivity extends SetupActivity { params.getString( SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); TvInputInfo input = mInputManager.getTvInputInfo(inputId); - Intent intent = CommonUtils.createSetupIntent(input); + Intent intent = mSetupUtils.createSetupIntent(this, input); if (intent == null) { Toast.makeText( this, diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java index a0cfad32..8265d178 100644 --- a/src/com/android/tv/ui/SelectInputView.java +++ b/src/com/android/tv/ui/SelectInputView.java @@ -287,10 +287,18 @@ public class SelectInputView extends VerticalGridView CharSequence customLabel = input.loadCustomLabel(getContext()); CharSequence label = input.loadLabel(getContext()); if (TextUtils.isEmpty(customLabel) || customLabel.equals(label)) { - inputLabelView.setText(label); + if (input.isPassthroughInput()) { + inputLabelView.setText(label); + } else { + inputLabelView.setText(R.string.input_long_label_for_tuner); + } secondaryInputLabelView.setVisibility(View.GONE); } else { - inputLabelView.setText(customLabel); + if (input.isPassthroughInput()) { + inputLabelView.setText(customLabel); + } else { + inputLabelView.setText(R.string.input_long_label_for_tuner); + } secondaryInputLabelView.setText(label); secondaryInputLabelView.setVisibility(View.VISIBLE); } diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index a736e79d..3ac841c2 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -19,6 +19,7 @@ package com.android.tv.ui; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; @@ -28,12 +29,14 @@ import android.graphics.PorterDuff; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.PlaybackParams; +import android.media.tv.AitInfo; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.tv.TvView.OnUnhandledInputEventListener; +import android.media.tv.interactive.TvInteractiveAppView; import android.net.ConnectivityManager; import android.net.Uri; import android.os.AsyncTask; @@ -194,6 +197,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV private final InputSessionManager mInputSessionManager; private int mChannelSignalStrength; + private TvInteractiveAppView mTvIAppView; private final TvInputCallbackCompat mCallback = new TvInputCallbackCompat() { @@ -413,6 +417,25 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV mOnTuneListener.onChannelSignalStrength(); } } + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + @Override + public void onAitInfoUpdated(String inputId, AitInfo aitInfo) { + if (!TvFeatures.HAS_TIAF.isEnabled(getContext())) { + return; + } + if (DEBUG) { + Log.d(TAG, + "onAitInfoUpdated: {inputId=" + + inputId + + ", AitInfo=(" + + aitInfo + +")}"); + } + if (mOnTuneListener != null) { + mOnTuneListener.onAitInfoUpdated(inputId, aitInfo); + } + } }; public TunableTvView(Context context) { @@ -476,18 +499,26 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV }); mAccessibilityManager = context.getSystemService(AccessibilityManager.class); } + public void initialize( + ProgramDataManager programDataManager, + TvInputManagerHelper tvInputManagerHelper, + LegacyFlags legacyFlags) { + initialize(programDataManager, tvInputManagerHelper, legacyFlags, null); + } public void initialize( ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper, - LegacyFlags mLegacyFlags) { + LegacyFlags legacyFlags, + TvInteractiveAppView tvIAppView) { mTvView = findViewById(R.id.tv_view); - mTvView.setUseSecureSurface(!BuildConfig.ENG && !mLegacyFlags.enableDeveloperFeatures()); + mTvView.setUseSecureSurface(!BuildConfig.ENG && !legacyFlags.enableDeveloperFeatures()); mProgramDataManager = programDataManager; mInputManagerHelper = tvInputManagerHelper; mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager(); mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings(); + mTvIAppView = tvIAppView; if (mInputSessionManager != null) { mTvViewSession = mInputSessionManager.createTvViewSession(mTvView, this, mCallback); } else { @@ -715,6 +746,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV } } + @Override + public float getStreamVolume() { + return mIsMuted + ? 0 + : mVolume; + } + /** * Sets fixed size for the internal {@link android.view.Surface} of {@link * android.media.tv.TvView}. If either {@code width} or {@code height} is non positive, the @@ -773,6 +811,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV void onContentAllowed(); void onChannelSignalStrength(); + + @TargetApi(Build.VERSION_CODES.TIRAMISU) + void onAitInfoUpdated(String inputId, AitInfo aitInfo); } public void unblockContent(TvContentRating rating) { @@ -976,6 +1017,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV return; } mBlockScreenView.setVisibility(VISIBLE); + if (mTvIAppView != null) { + mTvIAppView.setVisibility(INVISIBLE); + } mBlockScreenView.setBackgroundImage(null); if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) { mBlockScreenView.setIconVisibility(true); @@ -1007,6 +1051,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV if (mBlockScreenView.getVisibility() == VISIBLE) { mBlockScreenView.fadeOut(); } + if (mTvIAppView != null) { + mTvIAppView.setVisibility(VISIBLE); + } } } diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java index cf1a9113..19af23b9 100644 --- a/src/com/android/tv/ui/TvOverlayManager.java +++ b/src/com/android/tv/ui/TvOverlayManager.java @@ -55,6 +55,7 @@ import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.RecentlyWatchedDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; +import com.android.tv.dialog.InteractiveAppDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.ui.browse.DvrBrowseActivity; import com.android.tv.guide.ProgramGuide; @@ -198,6 +199,7 @@ public class TvOverlayManager implements AccessibilityStateChangeListener { AVAILABLE_DIALOG_TAGS.add(LicenseDialogFragment.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG); AVAILABLE_DIALOG_TAGS.add(HalfSizedDialogFragment.DIALOG_TAG); + AVAILABLE_DIALOG_TAGS.add(InteractiveAppDialogFragment.DIALOG_TAG); } private final MainActivity mMainActivity; diff --git a/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java b/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java new file mode 100755 index 00000000..b56a1d66 --- /dev/null +++ b/src/com/android/tv/ui/sidepanel/InteractiveAppSettingsFragment.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 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.ui.sidepanel; + +import com.android.tv.R; +import com.android.tv.util.TvSettings; +import java.util.ArrayList; +import java.util.List; + +public class InteractiveAppSettingsFragment extends SideFragment { + private static final String TRACKER_LABEL = "Interactive Application Settings"; + @Override + protected String getTitle() { + return getString(R.string.interactive_app_settings); + } + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } + @Override + protected List<Item> getItemList() { + List<Item> items = new ArrayList<>(); + items.add( + new SwitchItem( + getString(R.string.tv_iapp_on), + getString(R.string.tv_iapp_off)) { + @Override + protected void onUpdate() { + super.onUpdate(); + setChecked(TvSettings.isTvIAppOn(getContext())); + } + @Override + protected void onSelected() { + super.onSelected(); + boolean checked = isChecked(); + TvSettings.setTvIAppOn(getContext(), checked); + } + }); + return items; + } +} diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java index 1c03b6a9..762a190c 100644 --- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java +++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java @@ -29,6 +29,7 @@ import com.android.tv.common.CommonPreferences; import com.android.tv.common.customization.CustomizationManager; import com.android.tv.common.util.PermissionUtils; import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.features.TvFeatures; import com.android.tv.license.LicenseSideFragment; import com.android.tv.license.Licenses; import com.android.tv.util.Utils; @@ -190,6 +191,22 @@ public class SettingsFragment extends SideFragment { } }); } + + //Interactive Application Settings + if (TvFeatures.HAS_TIAF.isEnabled(getContext())) + { + items.add( + new ActionItem(getString(R.string.interactive_app_settings)) { + @Override + protected void onSelected() { + getMainActivity() + .getOverlayManager() + .getSideFragmentManager() + .show(new InteractiveAppSettingsFragment(), false); + } + }); + } + // Show version. SimpleActionItem version = new SimpleActionItem( diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 52b3e3e8..aaee1047 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -31,14 +31,18 @@ import android.support.annotation.UiThread; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.dagger.annotations.ApplicationContext; import com.android.tv.common.singletons.HasTvInputId; +import com.android.tv.common.util.CommonUtils; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.api.Channel; import com.android.tv.tunerinputcontroller.BuiltInTunerManager; import com.google.common.base.Optional; + +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -362,6 +366,52 @@ public class SetupUtils { } /** + * Create a Intent to launch setup activity for {@code inputId}. The setup activity defined + * in the overlayable resources precedes the one defined in the corresponding TV input service. + */ + @Nullable + public Intent createSetupIntent(Context context, TvInputInfo input) { + String[] componentStrings = context.getResources() + .getStringArray(R.array.setup_ComponentNames); + + if (componentStrings != null) { + for (String component : componentStrings) { + String[] split = component.split("#"); + if (split.length != 2) { + Log.w(TAG, "Invalid component item: " + Arrays.toString(split)); + continue; + } + + final String inputId = split[0].trim(); + if (inputId.equals(input.getId())) { + final String flattenedComponentName = split[1].trim(); + final ComponentName componentName = ComponentName + .unflattenFromString(flattenedComponentName); + if (componentName == null) { + Log.w(TAG, "Failed to unflatten component: " + flattenedComponentName); + continue; + } + + final Intent overlaySetupIntent = new Intent(Intent.ACTION_MAIN); + overlaySetupIntent.setComponent(componentName); + overlaySetupIntent.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId); + + PackageManager pm = context.getPackageManager(); + if (overlaySetupIntent.resolveActivityInfo(pm, 0) == null) { + Log.w(TAG, "unable to find component" + flattenedComponentName); + continue; + } + + Log.i(TAG, "overlay input id: " + inputId + + " to setup activity: " + flattenedComponentName); + return CommonUtils.createSetupIntent(overlaySetupIntent, inputId); + } + } + } + return CommonUtils.createSetupIntent(input); + } + + /** * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} * for {@code inputId}. */ diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java index ae79e7e5..1a5434cb 100644 --- a/src/com/android/tv/util/TvSettings.java +++ b/src/com/android/tv/util/TvSettings.java @@ -53,6 +53,9 @@ public final class TvSettings { private static final String PREF_CONTENT_RATING_LEVEL = "pref.content_rating_level"; private static final String PREF_DISABLE_PIN_UNTIL = "pref.disable_pin_until"; + // tviapp settings + private static final String PREF_TV_IAPP_STATES = "pref.tviapp_on"; + @Retention(RetentionPolicy.SOURCE) @IntDef({ CONTENT_RATING_LEVEL_NONE, @@ -242,4 +245,16 @@ public final class TvSettings { .putLong(PREF_DISABLE_PIN_UNTIL, timeMillis) .apply(); } + + public static boolean isTvIAppOn(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(PREF_TV_IAPP_STATES, false); + } + + public static void setTvIAppOn(Context context, boolean isOn) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(PREF_TV_IAPP_STATES, isOn) + .apply(); + } } |