CastShellActivity.java revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.chromecast.shell;
6
7import android.app.Activity;
8import android.content.BroadcastReceiver;
9import android.content.Context;
10import android.content.Intent;
11import android.content.IntentFilter;
12import android.media.AudioManager;
13import android.net.Uri;
14import android.os.Bundle;
15import android.support.v4.content.LocalBroadcastManager;
16import android.util.Log;
17import android.view.KeyEvent;
18import android.view.MotionEvent;
19import android.view.WindowManager;
20import android.widget.Toast;
21
22import org.chromium.base.CommandLine;
23import org.chromium.content.browser.ActivityContentVideoViewClient;
24import org.chromium.content.browser.ContentVideoViewClient;
25import org.chromium.content.browser.ContentViewClient;
26import org.chromium.content.browser.ContentViewCore;
27import org.chromium.ui.base.WindowAndroid;
28
29/**
30 * Activity for managing the Cast shell.
31 */
32public class CastShellActivity extends Activity {
33    private static final String TAG = "CastShellActivity";
34
35    private static final String ACTIVE_SHELL_URL_KEY = "activeUrl";
36    private static final int DEFAULT_HEIGHT_PIXELS = 720;
37    public static final String ACTION_EXTRA_RESOLUTION_HEIGHT =
38        "org.chromium.chromecast.shell.intent.extra.RESOLUTION_HEIGHT";
39
40    private CastWindowManager mCastWindowManager;
41    private AudioManager mAudioManager;
42    private BroadcastReceiver mBroadcastReceiver;
43
44    // Native window instance.
45    // TODO(byungchul, gunsch): CastShellActivity, CastWindowAndroid, and native CastWindowAndroid
46    // have a one-to-one relationship. Consider instantiating CastWindow here and CastWindow having
47    // this native shell instance.
48    private long mNativeCastWindow;
49
50    /**
51     * Returns whether or not CastShellActivity should launch the browser startup sequence.
52     * Intended to be overridden.
53     */
54    protected boolean shouldLaunchBrowser() {
55        return true;
56    }
57
58    @Override
59    protected void onCreate(final Bundle savedInstanceState) {
60        super.onCreate(savedInstanceState);
61        exitIfUrlMissing();
62
63        if (shouldLaunchBrowser()) {
64            if (!CastBrowserHelper.initializeBrowser(getApplicationContext())) {
65                Toast.makeText(this,
66                        R.string.browser_process_initialization_failed,
67                        Toast.LENGTH_SHORT).show();
68                finish();
69            }
70        }
71
72        // Whenever our app is visible, volume controls should modify the music stream.
73        // For more information read:
74        // http://developer.android.com/training/managing-audio/volume-playback.html
75        setVolumeControlStream(AudioManager.STREAM_MUSIC);
76
77        // Set flags to both exit sleep mode when this activity starts and
78        // avoid entering sleep mode while playing media. We cannot distinguish
79        // between video and audio so this applies to both.
80        getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
81        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
82
83        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
84
85        setContentView(R.layout.cast_shell_activity);
86        mCastWindowManager = (CastWindowManager) findViewById(R.id.shell_container);
87        mCastWindowManager.setDelegate(new CastWindowManager.Delegate() {
88                @Override
89                public void onCreated() {
90                }
91
92                @Override
93                public void onClosed() {
94                    mNativeCastWindow = 0;
95                    mCastWindowManager.setDelegate(null);
96                    finish();
97                }
98            });
99        setResolution();
100        mCastWindowManager.setWindow(new WindowAndroid(this));
101
102        registerBroadcastReceiver();
103
104        String url = getIntent().getDataString();
105        Log.d(TAG, "onCreate startupUrl: " + url);
106        mNativeCastWindow = mCastWindowManager.launchCastWindow(url);
107
108        getActiveContentViewCore().setContentViewClient(new ContentViewClient() {
109            @Override
110            public ContentVideoViewClient getContentVideoViewClient() {
111                return new ActivityContentVideoViewClient(CastShellActivity.this);
112            }
113        });
114    }
115
116    @Override
117    protected void onDestroy() {
118        super.onDestroy();
119
120        unregisterBroadcastReceiver();
121
122        if (mNativeCastWindow != 0) {
123            mCastWindowManager.stopCastWindow(mNativeCastWindow, false /* gracefully */);
124            mNativeCastWindow = 0;
125        }
126    }
127
128    @Override
129    protected void onNewIntent(Intent intent) {
130        // Only handle direct intents (e.g. "fling") if this activity is also managing
131        // the browser process.
132        if (!shouldLaunchBrowser()) return;
133
134        String url = intent.getDataString();
135        Log.d(TAG, "onNewIntent: " + url);
136
137        // Reset broadcast intent uri and receiver.
138        setIntent(intent);
139        exitIfUrlMissing();
140        getActiveCastWindow().loadUrl(url);
141    }
142
143    @Override
144    protected void onResume() {
145        super.onResume();
146
147        // Inform ContentView that this activity is being shown.
148        ContentViewCore view = getActiveContentViewCore();
149        if (view != null) view.onShow();
150
151        // Request audio focus so any other audio playback doesn't continue in the background.
152        if (mAudioManager.requestAudioFocus(
153                null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
154                != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
155            Log.e(TAG, "Failed to obtain audio focus");
156        }
157    }
158
159    @Override
160    protected void onPause() {
161        // As soon as the cast app is no longer in the foreground, we ought to immediately tear
162        // everything down. Apps should not continue running and playing sound in the background.
163        super.onPause();
164
165        // Release the audio focus. Note that releasing audio focus does not stop audio playback,
166        // it just notifies the framework that this activity has stopped playing audio.
167        if (mAudioManager.abandonAudioFocus(null) != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
168            Log.e(TAG, "Failed to abandon audio focus");
169        }
170
171        ContentViewCore view = getActiveContentViewCore();
172        if (view != null) view.onHide();
173
174        finishGracefully();
175    }
176
177    protected void finishGracefully() {
178        if (mNativeCastWindow != 0) {
179            mCastWindowManager.stopCastWindow(mNativeCastWindow, true /* gracefully */);
180            mNativeCastWindow = 0;
181        }
182    }
183
184    private void registerBroadcastReceiver() {
185        if (mBroadcastReceiver == null) {
186            mBroadcastReceiver = new BroadcastReceiver() {
187                @Override
188                public void onReceive(Context context, Intent intent) {
189                    Log.d(TAG, "Received intent: action=" + intent.getAction());
190                    if (CastWindowAndroid.ACTION_ENABLE_DEV_TOOLS.equals(intent.getAction())) {
191                        mCastWindowManager.nativeEnableDevTools(true);
192                    } else if (CastWindowAndroid.ACTION_DISABLE_DEV_TOOLS.equals(
193                            intent.getAction())) {
194                        mCastWindowManager.nativeEnableDevTools(false);
195                    }
196                }
197            };
198        }
199
200        IntentFilter devtoolsBroadcastIntentFilter = new IntentFilter();
201        devtoolsBroadcastIntentFilter.addAction(CastWindowAndroid.ACTION_ENABLE_DEV_TOOLS);
202        devtoolsBroadcastIntentFilter.addAction(CastWindowAndroid.ACTION_DISABLE_DEV_TOOLS);
203        LocalBroadcastManager.getInstance(this)
204                .registerReceiver(mBroadcastReceiver, devtoolsBroadcastIntentFilter);
205    }
206
207    private void unregisterBroadcastReceiver() {
208        LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
209        broadcastManager.unregisterReceiver(mBroadcastReceiver);
210    }
211
212    private void setResolution() {
213        int requestedHeight = getIntent().getIntExtra(
214                ACTION_EXTRA_RESOLUTION_HEIGHT, DEFAULT_HEIGHT_PIXELS);
215        int displayHeight = getResources().getDisplayMetrics().heightPixels;
216        // Clamp within [DEFAULT_HEIGHT_PIXELS, displayHeight]
217        int desiredHeight =
218                Math.min(displayHeight, Math.max(DEFAULT_HEIGHT_PIXELS, requestedHeight));
219        double deviceScaleFactor = ((double) displayHeight) / desiredHeight;
220        Log.d(TAG, "Using scale factor " + deviceScaleFactor + " to set height " + desiredHeight);
221        CommandLine.getInstance().appendSwitchWithValue("force-device-scale-factor",
222                String.valueOf(deviceScaleFactor));
223    }
224
225    private void exitIfUrlMissing() {
226        Intent intent = getIntent();
227        if (intent != null && intent.getData() != null && !intent.getData().equals(Uri.EMPTY)) {
228            return;
229        }
230        // Log an exception so that the exit cause is obvious when reading the logs.
231        Log.e(TAG, "Activity will not start",
232            new IllegalArgumentException("Intent did not contain a valid url"));
233        System.exit(-1);
234    }
235
236    /**
237     * @return The currently visible {@link CastWindowAndroid} or null if one is not showing.
238     */
239    public CastWindowAndroid getActiveCastWindow() {
240        return mCastWindowManager.getActiveCastWindow();
241    }
242
243    /**
244     * @return The {@link ContentViewCore} owned by the currently visible {@link CastWindowAndroid},
245     *         or null if one is not showing.
246     */
247    public ContentViewCore getActiveContentViewCore() {
248        CastWindowAndroid shell = getActiveCastWindow();
249        return shell != null ? shell.getContentViewCore() : null;
250    }
251
252    @Override
253    public boolean onKeyUp(int keyCode, KeyEvent event) {
254        if (keyCode != KeyEvent.KEYCODE_BACK) {
255            return super.onKeyUp(keyCode, event);
256        }
257
258        // Just finish this activity to go back to the previous activity or launcher.
259        finishGracefully();
260        return true;
261    }
262
263    @Override
264    public boolean dispatchKeyEvent(KeyEvent event) {
265        int keyCode = event.getKeyCode();
266        if (keyCode == KeyEvent.KEYCODE_BACK) {
267            return super.dispatchKeyEvent(event);
268        }
269        return false;
270    }
271
272    @Override
273    public boolean dispatchGenericMotionEvent(MotionEvent ev) {
274        return false;
275    }
276
277    @Override
278    public boolean dispatchKeyShortcutEvent(KeyEvent event) {
279        return false;
280    }
281
282    @Override
283    public boolean dispatchTouchEvent(MotionEvent ev) {
284        return false;
285    }
286
287    @Override
288    public boolean dispatchTrackballEvent(MotionEvent ev) {
289        return false;
290    }
291}
292