| 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);
 | 
| +}
 | 
| 
 |