Index: chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsActivity.java |
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsActivity.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsActivity.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..49201cd15c82f6202791c27263ac25d671ed8258 |
--- /dev/null |
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsActivity.java |
@@ -0,0 +1,354 @@ |
+// Copyright 2016 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.graphics.Color; |
+import android.media.AudioManager; |
+import android.net.Uri; |
+import android.os.Bundle; |
+import android.os.Handler; |
+import android.os.PatternMatcher; |
+import android.support.v4.content.LocalBroadcastManager; |
+import android.view.KeyEvent; |
+import android.view.MotionEvent; |
+import android.view.WindowManager; |
+import android.widget.FrameLayout; |
+import android.widget.Toast; |
+ |
+import org.chromium.base.Log; |
+import org.chromium.base.annotations.JNINamespace; |
+import org.chromium.content.browser.ActivityContentVideoViewEmbedder; |
+import org.chromium.content.browser.ContentVideoViewEmbedder; |
+import org.chromium.content.browser.ContentView; |
+import org.chromium.content.browser.ContentViewClient; |
+import org.chromium.content.browser.ContentViewCore; |
+import org.chromium.content.browser.ContentViewRenderView; |
+import org.chromium.content_public.browser.WebContents; |
+import org.chromium.ui.base.ViewAndroidDelegate; |
+import org.chromium.ui.base.WindowAndroid; |
+ |
+/** |
+ * Activity for displaying a WebContents in CastShell. |
+ * |
+ * Typically, this class is controlled by CastContentWindowAndroid, which will |
+ * start a new instance of this activity. If the CastContentWindowAndroid is |
+ * destroyed, CastWebContentsActivity should finish(). Similarily, if this |
+ * activity is destroyed, CastContentWindowAndroid should be notified by intent. |
+ */ |
+@JNINamespace("chromecast::shell") |
+public class CastWebContentsActivity extends Activity { |
+ private static final String TAG = "cr_CastWebActivity"; |
+ private static final boolean DEBUG = true; |
+ |
+ private Handler mHandler; |
+ private String mInstanceId; |
+ private BroadcastReceiver mWindowDestroyedBroadcastReceiver; |
+ private IntentFilter mWindowDestroyedIntentFilter; |
+ private FrameLayout mCastWebContentsLayout; |
+ private AudioManager mAudioManager; |
+ private ContentViewClient mContentViewClient; |
+ private ContentViewRenderView mContentViewRenderView; |
+ private WindowAndroid mWindow; |
+ private ContentViewCore mContentViewCore; |
+ private ContentView mContentView; |
+ |
+ private static final int TEARDOWN_GRACE_PERIOD_TIMEOUT_MILLIS = 300; |
+ public static final String ACTION_DATA_SCHEME = "cast"; |
+ public static final String ACTION_DATA_AUTHORITY = "webcontents"; |
+ |
+ public static final String ACTION_EXTRA_WEB_CONTENTS = |
+ "com.google.android.apps.castshell.intent.extra.WEB_CONTENTS"; |
+ public static final String ACTION_EXTRA_KEY_CODE = |
+ "com.google.android.apps.castshell.intent.extra.KEY_CODE"; |
+ public static final String ACTION_KEY_EVENT = |
+ "com.google.android.apps.castshell.intent.action.KEY_EVENT"; |
+ public static final String ACTION_STOP_ACTIVITY = |
+ "com.google.android.apps.castshell.intent.action.STOP_ACTIVITY"; |
+ public static final String ACTION_ACTIVITY_STOPPED = |
+ "com.google.android.apps.castshell.intent.action.ACTIVITY_STOPPED"; |
+ |
+ @Override |
+ protected void onCreate(final Bundle savedInstanceState) { |
+ if (DEBUG) Log.d(TAG, "onCreate"); |
+ super.onCreate(savedInstanceState); |
+ |
+ mHandler = new Handler(); |
+ |
+ // TODO(derekjchow): Remove this call. |
+ 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 = CastAudioManager.getAudioManager(this); |
+ |
+ setContentView(R.layout.cast_web_contents_activity); |
+ |
+ mWindow = new WindowAndroid(this); |
+ mContentViewRenderView = new ContentViewRenderView(this) { |
+ @Override |
+ protected void onReadyToRender() { |
+ setOverlayVideoMode(true); |
+ } |
+ }; |
+ mContentViewRenderView.onNativeLibraryLoaded(mWindow); |
+ // Setting the background color to black avoids rendering a white splash screen |
+ // before the players are loaded. See crbug/307113 for details. |
+ mContentViewRenderView.setSurfaceViewBackgroundColor(Color.BLACK); |
+ |
+ mCastWebContentsLayout = (FrameLayout) findViewById(R.id.web_contents_container); |
+ mCastWebContentsLayout.addView(mContentViewRenderView, |
+ new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, |
+ FrameLayout.LayoutParams.MATCH_PARENT)); |
+ |
+ Intent intent = getIntent(); |
+ handleIntent(intent); |
+ } |
+ |
+ protected void handleIntent(Intent intent) { |
+ intent.setExtrasClassLoader(WebContents.class.getClassLoader()); |
+ mInstanceId = intent.getData().getPath(); |
+ |
+ final String instanceId = mInstanceId; |
+ if (mWindowDestroyedBroadcastReceiver != null) { |
+ LocalBroadcastManager.getInstance(this).unregisterReceiver( |
+ mWindowDestroyedBroadcastReceiver); |
+ } |
+ mWindowDestroyedBroadcastReceiver = new BroadcastReceiver() { |
+ @Override |
+ public void onReceive(Context context, Intent intent) { |
+ detachWebContentsIfAny(); |
+ maybeFinishLater(); |
+ } |
+ }; |
+ mWindowDestroyedIntentFilter = new IntentFilter(); |
+ mWindowDestroyedIntentFilter.addDataScheme(intent.getData().getScheme()); |
+ mWindowDestroyedIntentFilter.addDataAuthority(intent.getData().getAuthority(), null); |
+ mWindowDestroyedIntentFilter.addDataPath(mInstanceId, PatternMatcher.PATTERN_LITERAL); |
+ mWindowDestroyedIntentFilter.addAction(ACTION_STOP_ACTIVITY); |
+ LocalBroadcastManager.getInstance(this).registerReceiver( |
+ mWindowDestroyedBroadcastReceiver, mWindowDestroyedIntentFilter); |
+ |
+ WebContents webContents = |
+ (WebContents) intent.getParcelableExtra(ACTION_EXTRA_WEB_CONTENTS); |
+ if (webContents == null) { |
+ Log.e(TAG, "Received null WebContents in intent."); |
+ maybeFinishLater(); |
+ return; |
+ } |
+ |
+ detachWebContentsIfAny(); |
+ showWebContents(webContents); |
+ } |
+ |
+ @Override |
+ protected void onNewIntent(Intent intent) { |
+ if (DEBUG) Log.d(TAG, "onNewIntent"); |
+ |
+ // If we're currently finishing this activity, we should start a new activity to |
+ // display the new app. |
+ if (isFinishing()) { |
+ Log.d(TAG, "Activity is finishing, starting new activity."); |
+ int flags = intent.getFlags(); |
+ flags = flags & ~Intent.FLAG_ACTIVITY_SINGLE_TOP; |
+ intent.setFlags(flags); |
+ startActivity(intent); |
+ return; |
+ } |
+ |
+ handleIntent(intent); |
+ } |
+ |
+ @Override |
+ protected void onDestroy() { |
+ if (DEBUG) Log.d(TAG, "onDestroy"); |
+ super.onDestroy(); |
+ } |
+ |
+ @Override |
+ protected void onStart() { |
+ if (DEBUG) Log.d(TAG, "onStart"); |
+ super.onStart(); |
+ } |
+ |
+ @Override |
+ protected void onStop() { |
+ if (DEBUG) Log.d(TAG, "onStop"); |
+ |
+ detachWebContentsIfAny(); |
+ releaseStreamMuteIfNecessary(); |
+ super.onStop(); |
+ } |
+ |
+ @Override |
+ protected void onResume() { |
+ if (DEBUG) Log.d(TAG, "onResume"); |
+ super.onResume(); |
+ |
+ 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() { |
+ if (DEBUG) Log.d(TAG, "onPause"); |
+ 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"); |
+ } |
+ } |
+ |
+ @Override |
+ public boolean dispatchKeyEvent(KeyEvent event) { |
+ if (DEBUG) Log.d(TAG, "dispatchKeyEvent"); |
+ int keyCode = event.getKeyCode(); |
+ int action = event.getAction(); |
+ |
+ // Similar condition for all single-click events. |
+ if (action == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { |
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER |
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE |
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY |
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE |
+ || keyCode == KeyEvent.KEYCODE_MEDIA_STOP |
+ || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT |
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS) { |
+ Intent intent = new Intent(ACTION_KEY_EVENT, getInstanceUri()); |
+ intent.putExtra(ACTION_EXTRA_KEY_CODE, keyCode); |
+ LocalBroadcastManager.getInstance(this).sendBroadcastSync(intent); |
+ return true; |
+ } |
+ } |
+ |
+ 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; |
+ } |
+ |
+ @SuppressWarnings("deprecation") |
+ private void releaseStreamMuteIfNecessary() { |
+ AudioManager audioManager = CastAudioManager.getAudioManager(this); |
+ boolean isMuted = false; |
+ try { |
+ isMuted = (Boolean) audioManager.getClass() |
+ .getMethod("isStreamMute", int.class) |
+ .invoke(audioManager, AudioManager.STREAM_MUSIC); |
+ } catch (Exception e) { |
+ Log.e(TAG, "Cannot call AudioManager.isStreamMute().", e); |
+ } |
+ |
+ if (isMuted) { |
+ // Note: this is a no-op on fixed-volume devices. |
+ audioManager.setStreamMute(AudioManager.STREAM_MUSIC, false); |
+ } |
+ } |
+ |
+ // Closes this activity if a new WebContents is not being displayed. |
+ private void maybeFinishLater() { |
+ Log.d(TAG, "maybeFinishLater"); |
+ final String currentInstanceId = mInstanceId; |
+ mHandler.postDelayed(new Runnable() { |
+ @Override |
+ public void run() { |
+ if (currentInstanceId == mInstanceId) { |
+ Log.d(TAG, "Finishing."); |
+ finish(); |
+ } |
+ } |
+ }, TEARDOWN_GRACE_PERIOD_TIMEOUT_MILLIS); |
+ } |
+ |
+ // Sets webContents to be the currently displayed webContents. |
+ private void showWebContents(WebContents webContents) { |
+ if (DEBUG) Log.d(TAG, "showWebContents"); |
+ |
+ // Set ContentVideoViewEmbedder to allow video playback. |
+ nativeSetContentVideoViewEmbedder(webContents, new ActivityContentVideoViewEmbedder(this)); |
+ |
+ // TODO(derekjchow): productVersion |
+ mContentViewCore = new ContentViewCore(this, ""); |
+ mContentView = ContentView.createContentView(this, mContentViewCore); |
+ mContentViewCore.initialize(ViewAndroidDelegate.createBasicDelegate(mContentView), |
+ mContentView, webContents, mWindow); |
+ mContentViewClient = new ContentViewClient(); |
+ mContentViewCore.setContentViewClient(mContentViewClient); |
+ // Enable display of current webContents. |
+ if (getParent() != null) mContentViewCore.onShow(); |
+ mCastWebContentsLayout.addView( |
+ mContentView, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, |
+ FrameLayout.LayoutParams.MATCH_PARENT)); |
+ mContentView.requestFocus(); |
+ mContentViewRenderView.setCurrentContentViewCore(mContentViewCore); |
+ } |
+ |
+ // Remove the currently displayed webContents. no-op if nothing is being displayed. |
+ private void detachWebContentsIfAny() { |
+ if (DEBUG) Log.d(TAG, "detachWebContentsIfAny"); |
+ if (mContentView != null) { |
+ mCastWebContentsLayout.removeView(mContentView); |
+ mContentView = null; |
+ mContentViewCore = null; |
+ |
+ // Inform CastContentWindowAndroid we're detaching. |
+ Intent intent = new Intent(ACTION_ACTIVITY_STOPPED, getInstanceUri()); |
+ LocalBroadcastManager.getInstance(this).sendBroadcastSync(intent); |
+ } |
+ } |
+ |
+ private Uri getInstanceUri() { |
+ Uri instanceUri = new Uri.Builder() |
+ .scheme(CastWebContentsActivity.ACTION_DATA_SCHEME) |
+ .authority(CastWebContentsActivity.ACTION_DATA_AUTHORITY) |
+ .path(mInstanceId) |
+ .build(); |
+ return instanceUri; |
+ } |
+ |
+ private native void nativeSetContentVideoViewEmbedder( |
+ WebContents webContents, ContentVideoViewEmbedder embedder); |
+} |