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 5 package org.chromium.chrome.shell; 6 7 import android.app.Activity; 8 import android.content.Intent; 9 import android.os.Bundle; 10 import android.text.TextUtils; 11 import android.util.Log; 12 import android.view.KeyEvent; 13 import android.view.Menu; 14 import android.view.MenuItem; 15 import android.view.View; 16 import android.widget.Toast; 17 18 import com.google.common.annotations.VisibleForTesting; 19 20 import org.chromium.base.ApiCompatibilityUtils; 21 import org.chromium.base.BaseSwitches; 22 import org.chromium.base.CommandLine; 23 import org.chromium.base.MemoryPressureListener; 24 import org.chromium.base.library_loader.ProcessInitException; 25 import org.chromium.chrome.browser.DevToolsServer; 26 import org.chromium.chrome.browser.appmenu.AppMenuHandler; 27 import org.chromium.chrome.browser.appmenu.AppMenuPropertiesDelegate; 28 import org.chromium.chrome.browser.dom_distiller.DomDistillerTabUtils; 29 import org.chromium.chrome.browser.printing.PrintingControllerFactory; 30 import org.chromium.chrome.browser.printing.TabPrinter; 31 import org.chromium.chrome.browser.share.ShareHelper; 32 import org.chromium.chrome.shell.sync.SyncController; 33 import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils; 34 import org.chromium.content.browser.ActivityContentVideoViewClient; 35 import org.chromium.content.browser.BrowserStartupController; 36 import org.chromium.content.browser.ContentViewCore; 37 import org.chromium.content.browser.DeviceUtils; 38 import org.chromium.content.common.ContentSwitches; 39 import org.chromium.printing.PrintManagerDelegateImpl; 40 import org.chromium.printing.PrintingController; 41 import org.chromium.sync.signin.AccountManagerHelper; 42 import org.chromium.sync.signin.ChromeSigninController; 43 import org.chromium.ui.base.ActivityWindowAndroid; 44 import org.chromium.ui.base.WindowAndroid; 45 46 /** 47 * The {@link android.app.Activity} component of a basic test shell to test Chrome features. 48 */ 49 public class ChromeShellActivity extends Activity implements AppMenuPropertiesDelegate { 50 private static final String TAG = "ChromeShellActivity"; 51 private static final String CHROME_DISTILLER_SCHEME = "chrome-distiller"; 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 if (!CommandLine.getInstance().hasSwitch( 144 ContentSwitches.DISABLE_OVERLAY_FULLSCREEN_VIDEO_SUBTITLE)) { 145 mTabManager.setOverlayVideoMode(true); 146 } 147 return success; 148 } 149 150 @Override 151 public void onDestroyContentVideoView() { 152 super.onDestroyContentVideoView(); 153 if (mTabManager != null && !CommandLine.getInstance().hasSwitch( 154 ContentSwitches.DISABLE_OVERLAY_FULLSCREEN_VIDEO_SUBTITLE)) { 155 mTabManager.setOverlayVideoMode(false); 156 } 157 } 158 }); 159 160 String startupUrl = getUrlFromIntent(getIntent()); 161 if (!TextUtils.isEmpty(startupUrl)) { 162 mTabManager.setStartupUrl(startupUrl); 163 } 164 mToolbar = (ChromeShellToolbar) findViewById(R.id.toolbar); 165 mAppMenuHandler = sAppMenuHandlerFactory.getAppMenuHandler(this, this, R.menu.main_menu); 166 mToolbar.setMenuHandler(mAppMenuHandler); 167 168 mDevToolsServer = new DevToolsServer("chrome_shell"); 169 mDevToolsServer.setRemoteDebuggingEnabled(true); 170 171 mPrintingController = PrintingControllerFactory.create(this); 172 173 mSyncController = SyncController.get(this); 174 // In case this method is called after the first onStart(), we need to inform the 175 // SyncController that we have started. 176 mSyncController.onStart(); 177 } 178 179 @Override 180 protected void onDestroy() { 181 super.onDestroy(); 182 183 if (mDevToolsServer != null) mDevToolsServer.destroy(); 184 mDevToolsServer = null; 185 } 186 187 @Override 188 protected void onSaveInstanceState(Bundle outState) { 189 // TODO(dtrainor): Save/restore the tab state. 190 if (mWindow != null) mWindow.saveInstanceState(outState); 191 } 192 193 @Override 194 public boolean onKeyUp(int keyCode, KeyEvent event) { 195 if (keyCode == KeyEvent.KEYCODE_BACK) { 196 ChromeShellTab tab = getActiveTab(); 197 if (tab != null && tab.canGoBack()) { 198 tab.goBack(); 199 return true; 200 } 201 } 202 203 return super.onKeyUp(keyCode, event); 204 } 205 206 @Override 207 protected void onNewIntent(Intent intent) { 208 if (MemoryPressureListener.handleDebugIntent(this, intent.getAction())) return; 209 210 String url = getUrlFromIntent(intent); 211 if (!TextUtils.isEmpty(url)) { 212 ChromeShellTab tab = getActiveTab(); 213 if (tab != null) tab.loadUrlWithSanitization(url); 214 } 215 } 216 217 @Override 218 protected void onStop() { 219 super.onStop(); 220 221 if (mToolbar != null) mToolbar.hideSuggestions(); 222 223 ContentViewCore viewCore = getActiveContentViewCore(); 224 if (viewCore != null) viewCore.onHide(); 225 } 226 227 @Override 228 protected void onStart() { 229 super.onStart(); 230 231 ContentViewCore viewCore = getActiveContentViewCore(); 232 if (viewCore != null) viewCore.onShow(); 233 234 if (mSyncController != null) { 235 mSyncController.onStart(); 236 } 237 } 238 239 @Override 240 public void onActivityResult(int requestCode, int resultCode, Intent data) { 241 mWindow.onActivityResult(requestCode, resultCode, data); 242 } 243 244 /** 245 * @return The {@link WindowAndroid} associated with this activity. 246 */ 247 public WindowAndroid getWindowAndroid() { 248 return mWindow; 249 } 250 251 /** 252 * @return The {@link ChromeShellTab} that is currently visible. 253 */ 254 public ChromeShellTab getActiveTab() { 255 return mTabManager != null ? mTabManager.getCurrentTab() : null; 256 } 257 258 /** 259 * @return The ContentViewCore of the active tab. 260 */ 261 public ContentViewCore getActiveContentViewCore() { 262 ChromeShellTab tab = getActiveTab(); 263 return tab != null ? tab.getContentViewCore() : null; 264 } 265 266 /** 267 * Creates a {@link ChromeShellTab} with a URL specified by {@code url}. 268 * 269 * @param url The URL the new {@link ChromeShellTab} should start with. 270 */ 271 @VisibleForTesting 272 public void createTab(String url) { 273 mTabManager.createTab(url); 274 } 275 276 /** 277 * Override the menu key event to show AppMenu. 278 */ 279 @Override 280 public boolean onKeyDown(int keyCode, KeyEvent event) { 281 if (keyCode == KeyEvent.KEYCODE_MENU && event.getRepeatCount() == 0) { 282 mAppMenuHandler.showAppMenu(findViewById(R.id.menu_button), true, false); 283 return true; 284 } 285 return super.onKeyDown(keyCode, event); 286 } 287 288 @Override 289 public boolean onOptionsItemSelected(MenuItem item) { 290 ChromeShellTab activeTab = getActiveTab(); 291 switch (item.getItemId()) { 292 case R.id.signin: 293 if (ChromeSigninController.get(this).isSignedIn()) { 294 SyncController.openSignOutDialog(getFragmentManager()); 295 } else if (AccountManagerHelper.get(this).hasGoogleAccounts()) { 296 SyncController.openSigninDialog(getFragmentManager()); 297 } else { 298 Toast.makeText(this, R.string.signin_no_account, Toast.LENGTH_SHORT).show(); 299 } 300 return true; 301 case R.id.print: 302 if (activeTab != null) { 303 mPrintingController.startPrint(new TabPrinter(activeTab), 304 new PrintManagerDelegateImpl(this)); 305 } 306 return true; 307 case R.id.distill_page: 308 if (activeTab != null) { 309 DomDistillerTabUtils.distillCurrentPageAndView( 310 activeTab.getContentViewCore().getWebContents()); 311 } 312 return true; 313 case R.id.back_menu_id: 314 if (activeTab != null && activeTab.canGoBack()) { 315 activeTab.goBack(); 316 } 317 return true; 318 case R.id.forward_menu_id: 319 if (activeTab != null && activeTab.canGoForward()) { 320 activeTab.goForward(); 321 } 322 return true; 323 case R.id.share_menu_id: 324 case R.id.direct_share_menu_id: 325 ShareHelper.share(item.getItemId() == R.id.direct_share_menu_id, this, 326 activeTab.getTitle(), activeTab.getUrl(), null); 327 return true; 328 default: 329 return super.onOptionsItemSelected(item); 330 } 331 } 332 333 private void waitForDebuggerIfNeeded() { 334 if (CommandLine.getInstance().hasSwitch(BaseSwitches.WAIT_FOR_JAVA_DEBUGGER)) { 335 Log.e(TAG, "Waiting for Java debugger to connect..."); 336 android.os.Debug.waitForDebugger(); 337 Log.e(TAG, "Java debugger connected. Resuming execution."); 338 } 339 } 340 341 private static String getUrlFromIntent(Intent intent) { 342 return intent != null ? intent.getDataString() : null; 343 } 344 345 @Override 346 public boolean shouldShowAppMenu() { 347 return true; 348 } 349 350 @Override 351 public void prepareMenu(Menu menu) { 352 menu.setGroupVisible(R.id.MAIN_MENU, true); 353 ChromeShellTab activeTab = getActiveTab(); 354 355 // Disable the "Back" menu item if there is no page to go to. 356 MenuItem backMenuItem = menu.findItem(R.id.back_menu_id); 357 backMenuItem.setEnabled(activeTab != null ? activeTab.canGoBack() : false); 358 359 // Disable the "Forward" menu item if there is no page to go to. 360 MenuItem forwardMenuItem = menu.findItem(R.id.forward_menu_id); 361 forwardMenuItem.setEnabled(activeTab != null ? activeTab.canGoForward() : false); 362 363 // ChromeShell does not know about bookmarks yet 364 menu.findItem(R.id.bookmark_this_page_id).setEnabled(true); 365 366 MenuItem signinItem = menu.findItem(R.id.signin); 367 if (ChromeSigninController.get(this).isSignedIn()) { 368 signinItem.setTitle(ChromeSigninController.get(this).getSignedInAccountName()); 369 } else { 370 signinItem.setTitle(R.string.signin_sign_in); 371 } 372 373 menu.findItem(R.id.print).setVisible(ApiCompatibilityUtils.isPrintingSupported()); 374 375 MenuItem distillPageItem = menu.findItem(R.id.distill_page); 376 if (CommandLine.getInstance().hasSwitch(ChromeShellSwitches.ENABLE_DOM_DISTILLER)) { 377 String url = activeTab != null ? activeTab.getUrl() : null; 378 distillPageItem.setEnabled(!DomDistillerUrlUtils.isUrlReportable( 379 CHROME_DISTILLER_SCHEME, url)); 380 distillPageItem.setVisible(true); 381 } else { 382 distillPageItem.setVisible(false); 383 } 384 ShareHelper.configureDirectShareMenuItem(this, menu.findItem(R.id.direct_share_menu_id)); 385 } 386 387 @VisibleForTesting 388 public AppMenuHandler getAppMenuHandler() { 389 return mAppMenuHandler; 390 } 391 392 @Override 393 public int getMenuThemeResourceId() { 394 return R.style.OverflowMenuTheme; 395 } 396 397 @VisibleForTesting 398 public static void setActivityWindowAndroidFactory(ActivityWindowAndroidFactory factory) { 399 sWindowAndroidFactory = factory; 400 } 401 402 @VisibleForTesting 403 public static void setAppMenuHandlerFactory(AppMenuHandlerFactory factory) { 404 sAppMenuHandlerFactory = factory; 405 } 406 } 407