// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chromecast.shell; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.WindowManager; import android.widget.Toast; import org.chromium.base.CommandLine; import org.chromium.content.browser.ActivityContentVideoViewClient; import org.chromium.content.browser.ContentVideoViewClient; import org.chromium.content.browser.ContentViewClient; import org.chromium.content.browser.ContentViewCore; import org.chromium.ui.base.WindowAndroid; /** * Activity for managing the Cast shell. */ public class CastShellActivity extends Activity { private static final String TAG = "CastShellActivity"; private static final String ACTIVE_SHELL_URL_KEY = "activeUrl"; private static final int DEFAULT_HEIGHT_PIXELS = 720; public static final String ACTION_EXTRA_RESOLUTION_HEIGHT = "org.chromium.chromecast.shell.intent.extra.RESOLUTION_HEIGHT"; private CastWindowManager mCastWindowManager; private AudioManager mAudioManager; private BroadcastReceiver mBroadcastReceiver; // Native window instance. // TODO(byungchul, gunsch): CastShellActivity, CastWindowAndroid, and native CastWindowAndroid // have a one-to-one relationship. Consider instantiating CastWindow here and CastWindow having // this native shell instance. private long mNativeCastWindow; /** * Returns whether or not CastShellActivity should launch the browser startup sequence. * Intended to be overridden. */ protected boolean shouldLaunchBrowser() { return true; } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); exitIfUrlMissing(); if (shouldLaunchBrowser()) { if (!CastBrowserHelper.initializeBrowser(getApplicationContext())) { Toast.makeText(this, R.string.browser_process_initialization_failed, Toast.LENGTH_SHORT).show(); finish(); } } // Whenever our app is visible, volume controls should modify the music stream. // For more information read: // http://developer.android.com/training/managing-audio/volume-playback.html setVolumeControlStream(AudioManager.STREAM_MUSIC); // Set flags to both exit sleep mode when this activity starts and // avoid entering sleep mode while playing media. We cannot distinguish // between video and audio so this applies to both. getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); setContentView(R.layout.cast_shell_activity); mCastWindowManager = (CastWindowManager) findViewById(R.id.shell_container); mCastWindowManager.setDelegate(new CastWindowManager.Delegate() { @Override public void onCreated() { } @Override public void onClosed() { mNativeCastWindow = 0; mCastWindowManager.setDelegate(null); finish(); } }); setResolution(); mCastWindowManager.setWindow(new WindowAndroid(this)); registerBroadcastReceiver(); String url = getIntent().getDataString(); Log.d(TAG, "onCreate startupUrl: " + url); mNativeCastWindow = mCastWindowManager.launchCastWindow(url); getActiveContentViewCore().setContentViewClient(new ContentViewClient() { @Override public ContentVideoViewClient getContentVideoViewClient() { return new ActivityContentVideoViewClient(CastShellActivity.this); } }); } @Override protected void onDestroy() { super.onDestroy(); unregisterBroadcastReceiver(); if (mNativeCastWindow != 0) { mCastWindowManager.stopCastWindow(mNativeCastWindow, false /* gracefully */); mNativeCastWindow = 0; } } @Override protected void onNewIntent(Intent intent) { // Only handle direct intents (e.g. "fling") if this activity is also managing // the browser process. if (!shouldLaunchBrowser()) return; String url = intent.getDataString(); Log.d(TAG, "onNewIntent: " + url); // Reset broadcast intent uri and receiver. setIntent(intent); exitIfUrlMissing(); getActiveCastWindow().loadUrl(url); } @Override protected void onResume() { super.onResume(); // Inform ContentView that this activity is being shown. ContentViewCore view = getActiveContentViewCore(); if (view != null) view.onShow(); // Request audio focus so any other audio playback doesn't continue in the background. if (mAudioManager.requestAudioFocus( null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.e(TAG, "Failed to obtain audio focus"); } } @Override protected void onPause() { // As soon as the cast app is no longer in the foreground, we ought to immediately tear // everything down. Apps should not continue running and playing sound in the background. super.onPause(); // Release the audio focus. Note that releasing audio focus does not stop audio playback, // it just notifies the framework that this activity has stopped playing audio. if (mAudioManager.abandonAudioFocus(null) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.e(TAG, "Failed to abandon audio focus"); } ContentViewCore view = getActiveContentViewCore(); if (view != null) view.onHide(); finishGracefully(); } protected void finishGracefully() { if (mNativeCastWindow != 0) { mCastWindowManager.stopCastWindow(mNativeCastWindow, true /* gracefully */); mNativeCastWindow = 0; } } private void registerBroadcastReceiver() { if (mBroadcastReceiver == null) { mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received intent: action=" + intent.getAction()); if (CastWindowAndroid.ACTION_ENABLE_DEV_TOOLS.equals(intent.getAction())) { mCastWindowManager.nativeEnableDevTools(true); } else if (CastWindowAndroid.ACTION_DISABLE_DEV_TOOLS.equals( intent.getAction())) { mCastWindowManager.nativeEnableDevTools(false); } } }; } IntentFilter devtoolsBroadcastIntentFilter = new IntentFilter(); devtoolsBroadcastIntentFilter.addAction(CastWindowAndroid.ACTION_ENABLE_DEV_TOOLS); devtoolsBroadcastIntentFilter.addAction(CastWindowAndroid.ACTION_DISABLE_DEV_TOOLS); LocalBroadcastManager.getInstance(this) .registerReceiver(mBroadcastReceiver, devtoolsBroadcastIntentFilter); } private void unregisterBroadcastReceiver() { LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this); broadcastManager.unregisterReceiver(mBroadcastReceiver); } private void setResolution() { int requestedHeight = getIntent().getIntExtra( ACTION_EXTRA_RESOLUTION_HEIGHT, DEFAULT_HEIGHT_PIXELS); int displayHeight = getResources().getDisplayMetrics().heightPixels; // Clamp within [DEFAULT_HEIGHT_PIXELS, displayHeight] int desiredHeight = Math.min(displayHeight, Math.max(DEFAULT_HEIGHT_PIXELS, requestedHeight)); double deviceScaleFactor = ((double) displayHeight) / desiredHeight; Log.d(TAG, "Using scale factor " + deviceScaleFactor + " to set height " + desiredHeight); CommandLine.getInstance().appendSwitchWithValue("force-device-scale-factor", String.valueOf(deviceScaleFactor)); } private void exitIfUrlMissing() { Intent intent = getIntent(); if (intent != null && intent.getData() != null && !intent.getData().equals(Uri.EMPTY)) { return; } // Log an exception so that the exit cause is obvious when reading the logs. Log.e(TAG, "Activity will not start", new IllegalArgumentException("Intent did not contain a valid url")); System.exit(-1); } /** * @return The currently visible {@link CastWindowAndroid} or null if one is not showing. */ public CastWindowAndroid getActiveCastWindow() { return mCastWindowManager.getActiveCastWindow(); } /** * @return The {@link ContentViewCore} owned by the currently visible {@link CastWindowAndroid}, * or null if one is not showing. */ public ContentViewCore getActiveContentViewCore() { CastWindowAndroid shell = getActiveCastWindow(); return shell != null ? shell.getContentViewCore() : null; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode != KeyEvent.KEYCODE_BACK) { return super.onKeyUp(keyCode, event); } // Just finish this activity to go back to the previous activity or launcher. finishGracefully(); return true; } @Override public boolean dispatchKeyEvent(KeyEvent event) { int keyCode = event.getKeyCode(); if (keyCode == KeyEvent.KEYCODE_BACK) { return super.dispatchKeyEvent(event); } return false; } @Override public boolean dispatchGenericMotionEvent(MotionEvent ev) { return false; } @Override public boolean dispatchKeyShortcutEvent(KeyEvent event) { return false; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return false; } @Override public boolean dispatchTrackballEvent(MotionEvent ev) { return false; } }