Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(175)

Side by Side Diff: chrome/android/java/src/org/chromium/chrome/browser/tab/Tab.java

Issue 2233023002: Adding BlimpNavigationController to Tab (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@nav_handler_remove
Patch Set: Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.chrome.browser.tab; 5 package org.chromium.chrome.browser.tab;
6 6
7 import android.app.Activity; 7 import android.app.Activity;
8 import android.app.Application; 8 import android.app.Application;
9 import android.content.Context; 9 import android.content.Context;
10 import android.content.Intent; 10 import android.content.Intent;
(...skipping 21 matching lines...) Expand all
32 import org.chromium.base.ApplicationStatus; 32 import org.chromium.base.ApplicationStatus;
33 import org.chromium.base.ContextUtils; 33 import org.chromium.base.ContextUtils;
34 import org.chromium.base.ObserverList; 34 import org.chromium.base.ObserverList;
35 import org.chromium.base.ObserverList.RewindableIterator; 35 import org.chromium.base.ObserverList.RewindableIterator;
36 import org.chromium.base.ThreadUtils; 36 import org.chromium.base.ThreadUtils;
37 import org.chromium.base.TraceEvent; 37 import org.chromium.base.TraceEvent;
38 import org.chromium.base.VisibleForTesting; 38 import org.chromium.base.VisibleForTesting;
39 import org.chromium.base.annotations.CalledByNative; 39 import org.chromium.base.annotations.CalledByNative;
40 import org.chromium.base.metrics.RecordHistogram; 40 import org.chromium.base.metrics.RecordHistogram;
41 import org.chromium.base.metrics.RecordUserAction; 41 import org.chromium.base.metrics.RecordUserAction;
42 import org.chromium.blimp_public.contents.BlimpContents;
43 import org.chromium.blimp_public.contents.BlimpContentsObserver;
42 import org.chromium.chrome.R; 44 import org.chromium.chrome.R;
43 import org.chromium.chrome.browser.ChromeActivity; 45 import org.chromium.chrome.browser.ChromeActivity;
44 import org.chromium.chrome.browser.ChromeApplication; 46 import org.chromium.chrome.browser.ChromeApplication;
45 import org.chromium.chrome.browser.ChromeFeatureList; 47 import org.chromium.chrome.browser.ChromeFeatureList;
46 import org.chromium.chrome.browser.ChromeVersionInfo; 48 import org.chromium.chrome.browser.ChromeVersionInfo;
47 import org.chromium.chrome.browser.FrozenNativePage; 49 import org.chromium.chrome.browser.FrozenNativePage;
48 import org.chromium.chrome.browser.IntentHandler; 50 import org.chromium.chrome.browser.IntentHandler;
49 import org.chromium.chrome.browser.IntentHandler.TabOpenType; 51 import org.chromium.chrome.browser.IntentHandler.TabOpenType;
50 import org.chromium.chrome.browser.NativePage; 52 import org.chromium.chrome.browser.NativePage;
51 import org.chromium.chrome.browser.SwipeRefreshHandler; 53 import org.chromium.chrome.browser.SwipeRefreshHandler;
(...skipping 533 matching lines...) Expand 10 before | Expand all | Expand 10 after
585 // swapped web contents, schedule the enabling of fullscreen now. 587 // swapped web contents, schedule the enabling of fullscreen now.
586 scheduleEnableFullscreenLoadDelayIfNecessary(); 588 scheduleEnableFullscreenLoadDelayIfNecessary();
587 589
588 if (didFinishLoad) { 590 if (didFinishLoad) {
589 // Simulate the PAGE_LOAD_FINISHED notification that we did not get. 591 // Simulate the PAGE_LOAD_FINISHED notification that we did not get.
590 didFinishPageLoad(); 592 didFinishPageLoad();
591 } 593 }
592 } 594 }
593 }; 595 };
594 596
597 private class TabBlimpContentsObserver implements BlimpContentsObserver {
David Trainor- moved to gerrit 2016/08/11 21:40:19 We could make this a separate file like the TabWeb
shaktisahu 2016/08/12 22:11:49 I think when a nested class starts growing, we sho
598 private Tab mTab;
599
600 public TabBlimpContentsObserver(Tab tab) {
601 mTab = tab;
602 }
603 @Override
604 public void onNavigationStateChanged() {
605 mTab.updateTitle();
606 RewindableIterator<TabObserver> observers = mTab.getTabObservers();
607 while (observers.hasNext()) {
608 observers.next().onUrlUpdated(mTab);
609 }
610 }
611 }
612
595 private TabDelegateFactory mDelegateFactory; 613 private TabDelegateFactory mDelegateFactory;
596 614
597 private TopControlsVisibilityDelegate mTopControlsVisibilityDelegate; 615 private TopControlsVisibilityDelegate mTopControlsVisibilityDelegate;
598 616
599 /** 617 /**
600 * Creates an instance of a {@link Tab}. 618 * Creates an instance of a {@link Tab}.
601 * 619 *
602 * This constructor may be called before the native library has been loaded, so any additions 620 * This constructor may be called before the native library has been loaded, so any additions
603 * must be vetted for library calls. 621 * must be vetted for library calls.
604 * 622 *
(...skipping 122 matching lines...) Expand 10 before | Expand all | Expand 10 after
727 * @param observer The {@link TabObserver} to remove. 745 * @param observer The {@link TabObserver} to remove.
728 */ 746 */
729 public void removeObserver(TabObserver observer) { 747 public void removeObserver(TabObserver observer) {
730 mObservers.removeObserver(observer); 748 mObservers.removeObserver(observer);
731 } 749 }
732 750
733 /** 751 /**
734 * @return Whether or not this tab has a previous navigation entry. 752 * @return Whether or not this tab has a previous navigation entry.
735 */ 753 */
736 public boolean canGoBack() { 754 public boolean canGoBack() {
737 return getWebContents() != null && getWebContents().getNavigationControl ler().canGoBack(); 755 if (isBlimpTab()) {
756 return getBlimpContents() != null
757 && getBlimpContents().getNavigationController().canGoBack();
758 } else {
759 return getWebContents() != null
760 && getWebContents().getNavigationController().canGoBack();
761 }
738 } 762 }
739 763
740 /** 764 /**
741 * @return Whether or not this tab has a navigation entry after the current one. 765 * @return Whether or not this tab has a navigation entry after the current one.
742 */ 766 */
743 public boolean canGoForward() { 767 public boolean canGoForward() {
744 return getWebContents() != null && getWebContents().getNavigationControl ler() 768 if (isBlimpTab()) {
745 .canGoForward(); 769 return getBlimpContents() != null
770 && getBlimpContents().getNavigationController().canGoForward ();
771 } else {
772 return getWebContents() != null
773 && getWebContents().getNavigationController().canGoForward() ;
774 }
746 } 775 }
747 776
748 /** 777 /**
749 * Goes to the navigation entry before the current one. 778 * Goes to the navigation entry before the current one.
750 */ 779 */
751 public void goBack() { 780 public void goBack() {
752 if (getWebContents() != null) getWebContents().getNavigationController() .goBack(); 781 if (isBlimpTab()) {
782 if (getBlimpContents() != null) getBlimpContents().getNavigationCont roller().goBack();
783 } else {
784 if (getWebContents() != null) getWebContents().getNavigationControll er().goBack();
785 }
753 } 786 }
754 787
755 /** 788 /**
756 * Goes to the navigation entry after the current one. 789 * Goes to the navigation entry after the current one.
757 */ 790 */
758 public void goForward() { 791 public void goForward() {
759 if (getWebContents() != null) getWebContents().getNavigationController() .goForward(); 792 if (isBlimpTab()) {
793 if (getBlimpContents() != null) {
794 getBlimpContents().getNavigationController().goForward();
795 }
796 } else {
797 if (getWebContents() != null) getWebContents().getNavigationControll er().goForward();
798 }
760 } 799 }
761 800
762 /** 801 /**
763 * Loads the current navigation if there is a pending lazy load (after tab r estore). 802 * Loads the current navigation if there is a pending lazy load (after tab r estore).
764 */ 803 */
765 public void loadIfNecessary() { 804 public void loadIfNecessary() {
766 if (getWebContents() != null) getWebContents().getNavigationController() .loadIfNecessary(); 805 if (getWebContents() != null) getWebContents().getNavigationController() .loadIfNecessary();
767 } 806 }
768 807
769 /** 808 /**
(...skipping 214 matching lines...) Expand 10 before | Expand all | Expand 10 after
984 1023
985 printingController.setPendingPrint(new TabPrinter(this), 1024 printingController.setPendingPrint(new TabPrinter(this),
986 new PrintManagerDelegateImpl(getActivity())); 1025 new PrintManagerDelegateImpl(getActivity()));
987 } 1026 }
988 1027
989 /** 1028 /**
990 * Reloads the current page content. 1029 * Reloads the current page content.
991 */ 1030 */
992 public void reload() { 1031 public void reload() {
993 // TODO(dtrainor): Should we try to rebuild the ContentView if it's froz en? 1032 // TODO(dtrainor): Should we try to rebuild the ContentView if it's froz en?
994 if (getWebContents() != null) getWebContents().getNavigationController() .reload(true); 1033 if (isBlimpTab()) {
1034 if (getBlimpContents() != null) {
1035 getBlimpContents().getNavigationController().reload(true);
David Trainor- moved to gerrit 2016/08/11 21:40:19 Do we need the boolean argument for blimp for now?
shaktisahu 2016/08/12 22:11:49 Yea, we don't use this argument right now. It shou
1036 }
1037 } else {
1038 if (getWebContents() != null) getWebContents().getNavigationControll er().reload(true);
1039 }
995 } 1040 }
996 1041
997 /** 1042 /**
998 * Reloads the current page content. 1043 * Reloads the current page content.
999 * This version ignores the cache and reloads from the network. 1044 * This version ignores the cache and reloads from the network.
1000 */ 1045 */
1001 public void reloadIgnoringCache() { 1046 public void reloadIgnoringCache() {
1002 if (getWebContents() != null) { 1047 if (getWebContents() != null) {
1003 getWebContents().getNavigationController().reloadBypassingCache(true ); 1048 getWebContents().getNavigationController().reloadBypassingCache(true );
1004 } 1049 }
(...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after
1125 } 1170 }
1126 1171
1127 /** 1172 /**
1128 * @return The web contents associated with this tab. 1173 * @return The web contents associated with this tab.
1129 */ 1174 */
1130 public WebContents getWebContents() { 1175 public WebContents getWebContents() {
1131 return mContentViewCore != null ? mContentViewCore.getWebContents() : nu ll; 1176 return mContentViewCore != null ? mContentViewCore.getWebContents() : nu ll;
1132 } 1177 }
1133 1178
1134 /** 1179 /**
1180 * @return The blimp contents associated with this tab, if in blimp mode.
1181 */
1182 public BlimpContents getBlimpContents() {
1183 if (mNativeTabAndroid == 0) return null;
1184 return nativeGetBlimpContents(mNativeTabAndroid);
David Trainor- moved to gerrit 2016/08/11 21:40:19 JNI calls aren't free. Should we be storing this
shaktisahu 2016/08/12 22:11:49 Done.
1185 }
1186
1187 /**
1188 * @return Whether or not this tab is running in blimp mode.
1189 */
1190 public boolean isBlimpTab() {
1191 return false;
1192 }
1193
1194 /**
1135 * @return The profile associated with this tab. 1195 * @return The profile associated with this tab.
1136 */ 1196 */
1137 public Profile getProfile() { 1197 public Profile getProfile() {
1138 if (mNativeTabAndroid == 0) return null; 1198 if (mNativeTabAndroid == 0) return null;
1139 return nativeGetProfileAndroid(mNativeTabAndroid); 1199 return nativeGetProfileAndroid(mNativeTabAndroid);
1140 } 1200 }
1141 1201
1142 /** 1202 /**
1143 * For more information about the uniqueness of {@link #getId()} see comment s on {@link Tab}. 1203 * For more information about the uniqueness of {@link #getId()} see comment s on {@link Tab}.
1144 * @see Tab 1204 * @see Tab
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
1181 } 1241 }
1182 1242
1183 /** 1243 /**
1184 * Set whether or not the {@link ContentViewCore} should be using a desktop user agent for the 1244 * Set whether or not the {@link ContentViewCore} should be using a desktop user agent for the
1185 * currently loaded page. 1245 * currently loaded page.
1186 * @param useDesktop If {@code true}, use a desktop user agent. Otherwi se use a mobile one. 1246 * @param useDesktop If {@code true}, use a desktop user agent. Otherwi se use a mobile one.
1187 * @param reloadOnChange Reload the page if the user agent has changed. 1247 * @param reloadOnChange Reload the page if the user agent has changed.
1188 */ 1248 */
1189 public void setUseDesktopUserAgent(boolean useDesktop, boolean reloadOnChang e) { 1249 public void setUseDesktopUserAgent(boolean useDesktop, boolean reloadOnChang e) {
1190 if (getWebContents() != null) { 1250 if (getWebContents() != null) {
1191 getWebContents().getNavigationController() 1251 getWebContents().getNavigationController().setUseDesktopUserAgent(
1192 .setUseDesktopUserAgent(useDesktop, reloadOnChange); 1252 useDesktop, reloadOnChange);
1193 } 1253 }
1194 } 1254 }
1195 1255
1196 /** 1256 /**
1197 * @return Whether or not the {@link ContentViewCore} is using a desktop use r agent. 1257 * @return Whether or not the {@link ContentViewCore} is using a desktop use r agent.
1198 */ 1258 */
1199 public boolean getUseDesktopUserAgent() { 1259 public boolean getUseDesktopUserAgent() {
1200 return getWebContents() != null && getWebContents().getNavigationControl ler() 1260 return getWebContents() != null
1201 .getUseDesktopUserAgent(); 1261 && getWebContents().getNavigationController().getUseDesktopUserA gent();
1202 } 1262 }
1203 1263
1204 /** 1264 /**
1205 * @return The current {@link ConnectionSecurityLevel} for the tab. 1265 * @return The current {@link ConnectionSecurityLevel} for the tab.
1206 */ 1266 */
1207 // TODO(tedchoc): Remove this and transition all clients to use ToolbarModel directly. 1267 // TODO(tedchoc): Remove this and transition all clients to use ToolbarModel directly.
1208 public int getSecurityLevel() { 1268 public int getSecurityLevel() {
1209 return SecurityStateModel.getSecurityLevelForWebContents(getWebContents( )); 1269 return SecurityStateModel.getSecurityLevelForWebContents(getWebContents( ));
1210 } 1270 }
1211 1271
(...skipping 231 matching lines...) Expand 10 before | Expand all | Expand 10 after
1443 // TODO(dtrainor): Remove this and move to a pull model instead of p ushing the layer. 1503 // TODO(dtrainor): Remove this and move to a pull model instead of p ushing the layer.
1444 attachTabContentManager(tabContentManager); 1504 attachTabContentManager(tabContentManager);
1445 1505
1446 // If there is a frozen WebContents state or a pending lazy load, do n't create a new 1506 // If there is a frozen WebContents state or a pending lazy load, do n't create a new
1447 // WebContents. 1507 // WebContents.
1448 if (getFrozenContentsState() != null || getPendingLoadParams() != nu ll) { 1508 if (getFrozenContentsState() != null || getPendingLoadParams() != nu ll) {
1449 if (unfreeze) unfreezeContents(); 1509 if (unfreeze) unfreezeContents();
1450 return; 1510 return;
1451 } 1511 }
1452 1512
1513 if (isBlimpTab() && getBlimpContents() == null) {
1514 nativeInitBlimpContents(mNativeTabAndroid);
1515 getBlimpContents().addObserver(new TabBlimpContentsObserver(this ));
1516 }
1517
1453 boolean creatingWebContents = webContents == null; 1518 boolean creatingWebContents = webContents == null;
1454 if (creatingWebContents) { 1519 if (creatingWebContents) {
1455 webContents = WebContentsFactory.createWebContents(isIncognito() , initiallyHidden); 1520 webContents = WebContentsFactory.createWebContents(isIncognito() , initiallyHidden);
1456 } 1521 }
1457 1522
1458 ContentViewCore contentViewCore = ContentViewCore.fromWebContents(we bContents); 1523 ContentViewCore contentViewCore = ContentViewCore.fromWebContents(we bContents);
1459 1524
1460 if (contentViewCore == null) { 1525 if (contentViewCore == null) {
1461 initContentViewCore(webContents); 1526 initContentViewCore(webContents);
1462 } else { 1527 } else {
(...skipping 580 matching lines...) Expand 10 before | Expand all | Expand 10 after
2043 return mIsInitialized; 2108 return mIsInitialized;
2044 } 2109 }
2045 2110
2046 /** 2111 /**
2047 * @return The URL associated with the tab. 2112 * @return The URL associated with the tab.
2048 */ 2113 */
2049 @CalledByNative 2114 @CalledByNative
2050 public String getUrl() { 2115 public String getUrl() {
2051 String url = getWebContents() != null ? getWebContents().getUrl() : ""; 2116 String url = getWebContents() != null ? getWebContents().getUrl() : "";
2052 2117
2118 if (isBlimpTab()) {
2119 url = getBlimpContents() != null ? getBlimpContents().getNavigationC ontroller().getUrl()
2120 : "";
2121 }
2122
2053 // If we have a ContentView, or a NativePage, or the url is not empty, w e have a WebContents 2123 // If we have a ContentView, or a NativePage, or the url is not empty, w e have a WebContents
2054 // so cache the WebContent's url. If not use the cached version. 2124 // so cache the WebContent's url. If not use the cached version.
2055 if (getContentViewCore() != null || getNativePage() != null || !TextUtil s.isEmpty(url)) { 2125 if (getContentViewCore() != null || getNativePage() != null || !TextUtil s.isEmpty(url)) {
2056 mUrl = url; 2126 mUrl = url;
2057 } 2127 }
2058 2128
2059 return mUrl != null ? mUrl : ""; 2129 return mUrl != null ? mUrl : "";
2060 } 2130 }
2061 2131
2062 /** 2132 /**
2063 * @return The tab title. 2133 * @return The tab title.
2064 */ 2134 */
2065 @CalledByNative 2135 @CalledByNative
2066 public String getTitle() { 2136 public String getTitle() {
2067 if (mTitle == null) updateTitle(); 2137 if (mTitle == null) updateTitle();
2068 return mTitle; 2138 return mTitle;
2069 } 2139 }
2070 2140
2071 void updateTitle() { 2141 void updateTitle() {
2072 if (isFrozen()) return; 2142 if (isFrozen()) return;
2073 2143
2074 // When restoring the tabs, the title will no longer be populated, so re quest it from the 2144 // When restoring the tabs, the title will no longer be populated, so re quest it from the
2075 // ContentViewCore or NativePage (if present). 2145 // ContentViewCore or NativePage (if present).
2076 String title = ""; 2146 String title = "";
2077 if (mNativePage != null) { 2147 if (mNativePage != null) {
2078 title = mNativePage.getTitle(); 2148 title = mNativePage.getTitle();
2149 } else if (getBlimpContents() != null) {
2150 title = getBlimpContents().getNavigationController().getTitle();
2079 } else if (getWebContents() != null) { 2151 } else if (getWebContents() != null) {
2080 title = getWebContents().getTitle(); 2152 title = getWebContents().getTitle();
2081 } 2153 }
2082 updateTitle(title); 2154 updateTitle(title);
2083 } 2155 }
2084 2156
2085 /** 2157 /**
2086 * Cache the title for the current page. 2158 * Cache the title for the current page.
2087 * 2159 *
2088 * {@link ContentViewClient#onUpdateTitle} is unreliable, particularly for n avigating backwards 2160 * {@link ContentViewClient#onUpdateTitle} is unreliable, particularly for n avigating backwards
(...skipping 1155 matching lines...) Expand 10 before | Expand all | Expand 10 after
3244 String packageName = ContextUtils.getApplicationContext().getPackageName (); 3316 String packageName = ContextUtils.getApplicationContext().getPackageName ();
3245 return getLaunchType() == TabLaunchType.FROM_EXTERNAL_APP 3317 return getLaunchType() == TabLaunchType.FROM_EXTERNAL_APP
3246 && !TextUtils.equals(getAppAssociatedWith(), packageName); 3318 && !TextUtils.equals(getAppAssociatedWith(), packageName);
3247 } 3319 }
3248 3320
3249 private native void nativeInit(); 3321 private native void nativeInit();
3250 private native void nativeDestroy(long nativeTabAndroid); 3322 private native void nativeDestroy(long nativeTabAndroid);
3251 private native void nativeInitWebContents(long nativeTabAndroid, boolean inc ognito, 3323 private native void nativeInitWebContents(long nativeTabAndroid, boolean inc ognito,
3252 WebContents webContents, TabWebContentsDelegateAndroid delegate, 3324 WebContents webContents, TabWebContentsDelegateAndroid delegate,
3253 ContextMenuPopulator contextMenuPopulator); 3325 ContextMenuPopulator contextMenuPopulator);
3326 private native void nativeInitBlimpContents(long nativeTabAndroid);
3327 private native BlimpContents nativeGetBlimpContents(long nativeTabAndroid);
3254 private native void nativeUpdateDelegates(long nativeTabAndroid, 3328 private native void nativeUpdateDelegates(long nativeTabAndroid,
3255 TabWebContentsDelegateAndroid delegate, ContextMenuPopulator context MenuPopulator); 3329 TabWebContentsDelegateAndroid delegate, ContextMenuPopulator context MenuPopulator);
3256 private native void nativeDestroyWebContents(long nativeTabAndroid, boolean deleteNative); 3330 private native void nativeDestroyWebContents(long nativeTabAndroid, boolean deleteNative);
3257 private native Profile nativeGetProfileAndroid(long nativeTabAndroid); 3331 private native Profile nativeGetProfileAndroid(long nativeTabAndroid);
3258 private native int nativeLoadUrl(long nativeTabAndroid, String url, String e xtraHeaders, 3332 private native int nativeLoadUrl(long nativeTabAndroid, String url, String e xtraHeaders,
3259 ResourceRequestBody postData, int transition, String referrerUrl, in t referrerPolicy, 3333 ResourceRequestBody postData, int transition, String referrerUrl, in t referrerPolicy,
3260 boolean isRendererInitiated, boolean shoulReplaceCurrentEntry, 3334 boolean isRendererInitiated, boolean shoulReplaceCurrentEntry,
3261 long intentReceivedTimestamp, boolean hasUserGesture); 3335 long intentReceivedTimestamp, boolean hasUserGesture);
3262 private native void nativeSetActiveNavigationEntryTitleForUrl(long nativeTab Android, String url, 3336 private native void nativeSetActiveNavigationEntryTitleForUrl(long nativeTab Android, String url,
3263 String title); 3337 String title);
(...skipping 10 matching lines...) Expand all
3274 private native void nativeSetInterceptNavigationDelegate(long nativeTabAndro id, 3348 private native void nativeSetInterceptNavigationDelegate(long nativeTabAndro id,
3275 InterceptNavigationDelegate delegate); 3349 InterceptNavigationDelegate delegate);
3276 private native void nativeAttachToTabContentManager(long nativeTabAndroid, 3350 private native void nativeAttachToTabContentManager(long nativeTabAndroid,
3277 TabContentManager tabContentManager); 3351 TabContentManager tabContentManager);
3278 private native void nativeAttachOverlayWebContents( 3352 private native void nativeAttachOverlayWebContents(
3279 long nativeTabAndroid, WebContents webContents, boolean visible); 3353 long nativeTabAndroid, WebContents webContents, boolean visible);
3280 private native void nativeDetachOverlayWebContents( 3354 private native void nativeDetachOverlayWebContents(
3281 long nativeTabAndroid, WebContents webContents); 3355 long nativeTabAndroid, WebContents webContents);
3282 private native boolean nativeHasPrerenderedUrl(long nativeTabAndroid, String url); 3356 private native boolean nativeHasPrerenderedUrl(long nativeTabAndroid, String url);
3283 } 3357 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698