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.chrome.shell;
6
7import android.app.Activity;
8import android.content.Intent;
9import android.os.Bundle;
10import android.text.TextUtils;
11import android.util.Log;
12import android.view.KeyEvent;
13import android.view.Menu;
14import android.view.MenuItem;
15import android.view.View;
16import android.view.ViewGroup;
17import android.widget.Toast;
18
19import org.chromium.base.ApiCompatibilityUtils;
20import org.chromium.base.BaseSwitches;
21import org.chromium.base.CommandLine;
22import org.chromium.base.ContentUriUtils;
23import org.chromium.base.MemoryPressureListener;
24import org.chromium.base.VisibleForTesting;
25import org.chromium.base.library_loader.ProcessInitException;
26import org.chromium.chrome.browser.DevToolsServer;
27import org.chromium.chrome.browser.FileProviderHelper;
28import org.chromium.chrome.browser.appmenu.AppMenuHandler;
29import org.chromium.chrome.browser.appmenu.AppMenuPropertiesDelegate;
30import org.chromium.chrome.browser.dom_distiller.DomDistillerTabUtils;
31import org.chromium.chrome.browser.printing.PrintingControllerFactory;
32import org.chromium.chrome.browser.printing.TabPrinter;
33import org.chromium.chrome.browser.share.ShareHelper;
34import org.chromium.chrome.shell.sync.SyncController;
35import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils;
36import org.chromium.content.browser.ActivityContentVideoViewClient;
37import org.chromium.content.browser.BrowserStartupController;
38import org.chromium.content.browser.ContentViewCore;
39import org.chromium.content.browser.DeviceUtils;
40import org.chromium.printing.PrintManagerDelegateImpl;
41import org.chromium.printing.PrintingController;
42import org.chromium.sync.signin.AccountManagerHelper;
43import org.chromium.sync.signin.ChromeSigninController;
44import org.chromium.ui.base.ActivityWindowAndroid;
45import org.chromium.ui.base.WindowAndroid;
46
47/**
48 * The {@link android.app.Activity} component of a basic test shell to test Chrome features.
49 */
50public class ChromeShellActivity extends Activity implements AppMenuPropertiesDelegate {
51    private static final String TAG = "ChromeShellActivity";
52
53    /**
54     * Factory used to set up a mock ActivityWindowAndroid for testing.
55     */
56    public interface ActivityWindowAndroidFactory {
57        /**
58         * @return ActivityWindowAndroid for the given activity.
59         */
60        public ActivityWindowAndroid getActivityWindowAndroid(Activity activity);
61    }
62
63    private static ActivityWindowAndroidFactory sWindowAndroidFactory =
64            new ActivityWindowAndroidFactory() {
65                @Override
66                public ActivityWindowAndroid getActivityWindowAndroid(Activity activity) {
67                    return new ActivityWindowAndroid(activity);
68                }
69            };
70
71    private WindowAndroid mWindow;
72    private TabManager mTabManager;
73    private ChromeShellToolbar mToolbar;
74    private DevToolsServer mDevToolsServer;
75    private SyncController mSyncController;
76    private PrintingController mPrintingController;
77
78    /**
79     * Factory used to set up a mock AppMenuHandler for testing.
80     */
81    public interface AppMenuHandlerFactory {
82        /**
83         * @return AppMenuHandler for the given activity and menu resource id.
84         */
85        public AppMenuHandler getAppMenuHandler(Activity activity,
86                AppMenuPropertiesDelegate delegate, int menuResourceId);
87    }
88
89    private static AppMenuHandlerFactory sAppMenuHandlerFactory =
90            new AppMenuHandlerFactory() {
91                @Override
92                public AppMenuHandler getAppMenuHandler(Activity activity,
93                        AppMenuPropertiesDelegate delegate, int menuResourceId) {
94                    return new AppMenuHandler(activity, delegate, menuResourceId);
95                }
96            };
97    private AppMenuHandler mAppMenuHandler;
98
99    @Override
100    protected void onCreate(final Bundle savedInstanceState) {
101        super.onCreate(savedInstanceState);
102
103        ChromeShellApplication.initCommandLine();
104        waitForDebuggerIfNeeded();
105
106        DeviceUtils.addDeviceSpecificUserAgentSwitch(this);
107
108        BrowserStartupController.StartupCallback callback =
109                new BrowserStartupController.StartupCallback() {
110                    @Override
111                    public void onSuccess(boolean alreadyStarted) {
112                        finishInitialization(savedInstanceState);
113                    }
114
115                    @Override
116                    public void onFailure() {
117                        Toast.makeText(ChromeShellActivity.this,
118                                       R.string.browser_process_initialization_failed,
119                                       Toast.LENGTH_SHORT).show();
120                        Log.e(TAG, "Chromium browser process initialization failed");
121                        finish();
122                    }
123                };
124        try {
125            BrowserStartupController.get(this).startBrowserProcessesAsync(callback);
126        } catch (ProcessInitException e) {
127            Log.e(TAG, "Unable to load native library.", e);
128            System.exit(-1);
129        }
130    }
131
132    private void finishInitialization(final Bundle savedInstanceState) {
133        setContentView(R.layout.chrome_shell_activity);
134        mTabManager = (TabManager) findViewById(R.id.tab_manager);
135
136        mWindow = sWindowAndroidFactory.getActivityWindowAndroid(this);
137        mWindow.restoreInstanceState(savedInstanceState);
138        mTabManager.initialize(mWindow, new ActivityContentVideoViewClient(this) {
139            @Override
140            public boolean onShowCustomView(View view) {
141                if (mTabManager == null) return false;
142                boolean success = super.onShowCustomView(view);
143                mTabManager.setOverlayVideoMode(true);
144                return success;
145            }
146
147            @Override
148            public void onDestroyContentVideoView() {
149                super.onDestroyContentVideoView();
150                if (mTabManager != null) {
151                    mTabManager.setOverlayVideoMode(false);
152                }
153            }
154        });
155
156        String startupUrl = getUrlFromIntent(getIntent());
157        if (!TextUtils.isEmpty(startupUrl)) {
158            mTabManager.setStartupUrl(startupUrl);
159        }
160        mToolbar = (ChromeShellToolbar) findViewById(R.id.toolbar);
161        mAppMenuHandler = sAppMenuHandlerFactory.getAppMenuHandler(this, this, R.menu.main_menu);
162        mToolbar.setMenuHandler(mAppMenuHandler);
163
164        mDevToolsServer = new DevToolsServer("chrome_shell");
165        mDevToolsServer.setRemoteDebuggingEnabled(
166                true, DevToolsServer.Security.ALLOW_DEBUG_PERMISSION);
167
168        mPrintingController = PrintingControllerFactory.create(this);
169
170        mSyncController = SyncController.get(this);
171        // In case this method is called after the first onStart(), we need to inform the
172        // SyncController that we have started.
173        mSyncController.onStart();
174        ContentUriUtils.setFileProviderUtil(new FileProviderHelper());
175    }
176
177    @Override
178    protected void onDestroy() {
179        super.onDestroy();
180
181        if (mDevToolsServer != null) mDevToolsServer.destroy();
182        mDevToolsServer = null;
183    }
184
185    @Override
186    protected void onSaveInstanceState(Bundle outState) {
187        // TODO(dtrainor): Save/restore the tab state.
188        if (mWindow != null) mWindow.saveInstanceState(outState);
189    }
190
191    @Override
192    public boolean onKeyUp(int keyCode, KeyEvent event) {
193        if (keyCode == KeyEvent.KEYCODE_BACK) {
194            ChromeShellTab tab = getActiveTab();
195            if (tab != null && tab.canGoBack()) {
196                tab.goBack();
197                return true;
198            }
199        }
200
201        return super.onKeyUp(keyCode, event);
202    }
203
204    @Override
205    protected void onNewIntent(Intent intent) {
206        if (MemoryPressureListener.handleDebugIntent(this, intent.getAction())) return;
207
208        String url = getUrlFromIntent(intent);
209        if (!TextUtils.isEmpty(url)) {
210            ChromeShellTab tab = getActiveTab();
211            if (tab != null) tab.loadUrlWithSanitization(url);
212        }
213    }
214
215    @Override
216    protected void onStop() {
217        super.onStop();
218
219        if (mToolbar != null) mToolbar.hideSuggestions();
220
221        ContentViewCore viewCore = getActiveContentViewCore();
222        if (viewCore != null) viewCore.onHide();
223    }
224
225    @Override
226    protected void onStart() {
227        super.onStart();
228
229        ContentViewCore viewCore = getActiveContentViewCore();
230        if (viewCore != null) viewCore.onShow();
231
232        if (mSyncController != null) {
233            mSyncController.onStart();
234        }
235    }
236
237    @Override
238    public void onActivityResult(int requestCode, int resultCode, Intent data) {
239        mWindow.onActivityResult(requestCode, resultCode, data);
240    }
241
242    /**
243     * @return The {@link WindowAndroid} associated with this activity.
244     */
245    public WindowAndroid getWindowAndroid() {
246        return mWindow;
247    }
248
249    /**
250     * @return The {@link ChromeShellTab} that is currently visible.
251     */
252    public ChromeShellTab getActiveTab() {
253        return mTabManager != null ? mTabManager.getCurrentTab() : null;
254    }
255
256    /**
257     * @return The ContentViewCore of the active tab.
258     */
259    public ContentViewCore getActiveContentViewCore() {
260        ChromeShellTab tab = getActiveTab();
261        return tab != null ? tab.getContentViewCore() : null;
262    }
263
264    /**
265     * Creates a {@link ChromeShellTab} with a URL specified by {@code url}.
266     *
267     * @param url The URL the new {@link ChromeShellTab} should start with.
268     */
269    @VisibleForTesting
270    public void createTab(String url) {
271        mTabManager.createTab(url);
272    }
273
274    /**
275     * Override the menu key event to show AppMenu.
276     */
277    @Override
278    public boolean onKeyDown(int keyCode, KeyEvent event) {
279        if (keyCode == KeyEvent.KEYCODE_MENU && event.getRepeatCount() == 0) {
280            if (mToolbar != null) mToolbar.hideSuggestions();
281            mAppMenuHandler.showAppMenu(findViewById(R.id.menu_button), true, false);
282            return true;
283        }
284        return super.onKeyDown(keyCode, event);
285    }
286
287    @SuppressWarnings("deprecation")
288    @Override
289    public boolean onOptionsItemSelected(MenuItem item) {
290        ChromeShellTab activeTab = getActiveTab();
291        if (activeTab != null) {
292            ViewGroup containerView = activeTab.getContentViewCore().getContainerView();
293            if (containerView.isFocusable() && containerView.isFocusableInTouchMode()) {
294                containerView.requestFocus();
295            }
296        }
297        switch (item.getItemId()) {
298            case R.id.signin:
299                if (ChromeSigninController.get(this).isSignedIn()) {
300                    SyncController.openSignOutDialog(getFragmentManager());
301                } else if (AccountManagerHelper.get(this).hasGoogleAccounts()) {
302                    SyncController.openSigninDialog(getFragmentManager());
303                } else {
304                    Toast.makeText(this, R.string.signin_no_account, Toast.LENGTH_SHORT).show();
305                }
306                return true;
307            case R.id.print:
308                if (activeTab != null) {
309                    mPrintingController.startPrint(new TabPrinter(activeTab),
310                            new PrintManagerDelegateImpl(this));
311                }
312                return true;
313            case R.id.distill_page:
314                if (activeTab != null) {
315                    DomDistillerTabUtils.distillCurrentPageAndView(
316                            activeTab.getContentViewCore().getWebContents());
317                }
318                return true;
319            case R.id.back_menu_id:
320                if (activeTab != null && activeTab.canGoBack()) {
321                    activeTab.goBack();
322                }
323                return true;
324            case R.id.forward_menu_id:
325                if (activeTab != null && activeTab.canGoForward()) {
326                    activeTab.goForward();
327                }
328                return true;
329            case R.id.share_menu_id:
330            case R.id.direct_share_menu_id:
331                ShareHelper.share(item.getItemId() == R.id.direct_share_menu_id, this,
332                        activeTab.getTitle(), activeTab.getUrl(), null,
333                        Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
334                return true;
335            default:
336                return super.onOptionsItemSelected(item);
337        }
338    }
339
340    private void waitForDebuggerIfNeeded() {
341        if (CommandLine.getInstance().hasSwitch(BaseSwitches.WAIT_FOR_JAVA_DEBUGGER)) {
342            Log.e(TAG, "Waiting for Java debugger to connect...");
343            android.os.Debug.waitForDebugger();
344            Log.e(TAG, "Java debugger connected. Resuming execution.");
345        }
346    }
347
348    private static String getUrlFromIntent(Intent intent) {
349        return intent != null ? intent.getDataString() : null;
350    }
351
352    @Override
353    public boolean shouldShowAppMenu() {
354        return true;
355    }
356
357    @Override
358    public void prepareMenu(Menu menu) {
359        menu.setGroupVisible(R.id.MAIN_MENU, true);
360        ChromeShellTab activeTab = getActiveTab();
361
362        // Disable the "Back" menu item if there is no page to go to.
363        MenuItem backMenuItem = menu.findItem(R.id.back_menu_id);
364        backMenuItem.setEnabled(activeTab != null ? activeTab.canGoBack() : false);
365
366        // Disable the "Forward" menu item if there is no page to go to.
367        MenuItem forwardMenuItem = menu.findItem(R.id.forward_menu_id);
368        forwardMenuItem.setEnabled(activeTab != null ? activeTab.canGoForward() : false);
369
370        // ChromeShell does not know about bookmarks yet
371        menu.findItem(R.id.bookmark_this_page_id).setEnabled(true);
372
373        MenuItem signinItem = menu.findItem(R.id.signin);
374        if (ChromeSigninController.get(this).isSignedIn()) {
375            signinItem.setTitle(ChromeSigninController.get(this).getSignedInAccountName());
376        } else {
377            signinItem.setTitle(R.string.signin_sign_in);
378        }
379
380        menu.findItem(R.id.print).setVisible(ApiCompatibilityUtils.isPrintingSupported());
381
382        MenuItem distillPageItem = menu.findItem(R.id.distill_page);
383        if (CommandLine.getInstance().hasSwitch(ChromeShellSwitches.ENABLE_DOM_DISTILLER)) {
384            String url = activeTab != null ? activeTab.getUrl() : null;
385            distillPageItem.setEnabled(!DomDistillerUrlUtils.isDistilledPage(url));
386            distillPageItem.setVisible(true);
387        } else {
388            distillPageItem.setVisible(false);
389        }
390        ShareHelper.configureDirectShareMenuItem(this, menu.findItem(R.id.direct_share_menu_id));
391    }
392
393    @VisibleForTesting
394    public AppMenuHandler getAppMenuHandler() {
395        return mAppMenuHandler;
396    }
397
398    @Override
399    public int getMenuThemeResourceId() {
400        return R.style.OverflowMenuTheme;
401    }
402
403    @VisibleForTesting
404    public static void setActivityWindowAndroidFactory(ActivityWindowAndroidFactory factory) {
405        sWindowAndroidFactory = factory;
406    }
407
408    @VisibleForTesting
409    public static void setAppMenuHandlerFactory(AppMenuHandlerFactory factory) {
410        sAppMenuHandlerFactory = factory;
411    }
412}
413