diff options
-rw-r--r-- | src/com/android/browser/Browser.java | 2 | ||||
-rw-r--r-- | src/com/android/browser/BrowserActivity.java | 56 | ||||
-rw-r--r-- | src/com/android/browser/EngineInitializer.java | 458 | ||||
-rw-r--r-- | src/com/android/browser/test/EngineInitializerTest.java | 212 | ||||
-rw-r--r-- | swe_android_browser.gypi | 29 | ||||
-rw-r--r-- | tests/AndroidManifest.xml | 7 | ||||
-rw-r--r-- | tests/startup/src/com/android/browser/tests/EngineInitializerTest.java | 305 |
7 files changed, 601 insertions, 468 deletions
diff --git a/src/com/android/browser/Browser.java b/src/com/android/browser/Browser.java index d1c127d7..6fb8ca27 100644 --- a/src/com/android/browser/Browser.java +++ b/src/com/android/browser/Browser.java @@ -63,7 +63,7 @@ public class Browser extends ChromiumApplication { Log.v(LOGTAG, "Browser.onActivityCreated: activity=" + activity); } if (!(activity instanceof BrowserActivity) && !(activity instanceof BrowserLauncher) ) { - EngineInitializer.getInstance().initializeSync((Context) Browser.this); + EngineInitializer.initializeSync((Context) Browser.this); } } diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java index 9b310e2f..f1fc4219 100644 --- a/src/com/android/browser/BrowserActivity.java +++ b/src/com/android/browser/BrowserActivity.java @@ -36,7 +36,6 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.view.Window; import org.chromium.base.VisibleForTesting; @@ -50,7 +49,7 @@ import java.util.Locale; import org.codeaurora.swe.CookieManager; import org.codeaurora.swe.WebView; -public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreDrawListener { +public class BrowserActivity extends Activity { public static final String ACTION_SHOW_BOOKMARKS = "show_bookmarks"; public static final String ACTION_SHOW_BROWSER = "show_browser"; @@ -84,7 +83,10 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD }; private Bundle mSavedInstanceState; - private EngineInitializer mEngineInitializer; + private EngineInitializer.ActivityScheduler mActivityScheduler; + public EngineInitializer.ActivityScheduler getScheduler() { + return mActivityScheduler; + } @Override public void onCreate(Bundle icicle) { @@ -115,8 +117,7 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD } */ - mEngineInitializer = EngineInitializer.getInstance(); - mEngineInitializer.onActivityCreate(BrowserActivity.this); + mActivityScheduler = EngineInitializer.onActivityCreate(BrowserActivity.this); Thread.setDefaultUncaughtExceptionHandler(new CrashLogExceptionHandler(this)); @@ -128,23 +129,9 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD ViewGroup topLayout = (ViewGroup) findViewById(R.id.main_content); topLayout.requestTransparentRegion(topLayout); - // Add pre-draw listener to start the controller after engine initialization. - final ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver(); - observer.addOnPreDrawListener(this); - - mEngineInitializer.initializeResourceExtractor(this); + EngineInitializer.onPostActivityCreate(BrowserActivity.this); } - @Override - public boolean onPreDraw() - { - final ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver(); - observer.removeOnPreDrawListener(this); - mEngineInitializer.onPreDraw(); - return true; - } - - public static boolean isTablet(Context context) { return context.getResources().getBoolean(R.bool.isTablet); } @@ -166,7 +153,7 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD return controller; } - public void onEngineInitializationComplete() { + public void startController() { Intent intent = (mSavedInstanceState == null) ? getIntent() : null; mController.start(intent); } @@ -180,7 +167,7 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD @Override protected void onNewIntent(Intent intent) { if (shouldIgnoreIntents()) return; - mEngineInitializer.onNewIntent(intent); + EngineInitializer.onNewIntent(BrowserActivity.this, intent); // Note: Do not add any more application logic in this method. // Move any additional app logic into handleOnNewIntent(). } @@ -221,7 +208,7 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD @Override protected void onStart() { super.onStart(); - mEngineInitializer.onActivityStart(); + EngineInitializer.onActivityStart(BrowserActivity.this); } @Override @@ -230,7 +217,7 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD if (LOGV_ENABLED) { Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this); } - mEngineInitializer.onActivityResume(); + EngineInitializer.onActivityResume(BrowserActivity.this); // Note: Do not add any more application logic in this method. // Move any additional app logic into handleOnResume(). } @@ -245,12 +232,17 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD @Override protected void onStop() { - mEngineInitializer.onActivityStop(); + EngineInitializer.onActivityStop(BrowserActivity.this); super.onStop(); // Note: Do not add any more application logic in this method. // Move any additional app logic into handleOnStop(). } + protected void handleOnStop() { + CookieManager.getInstance().flushCookieStore(); + mController.onPause(); + } + @Override public boolean onMenuOpened(int featureId, Menu menu) { if (Window.FEATURE_OPTIONS_PANEL == featureId) { @@ -285,7 +277,7 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD @Override protected void onPause() { - mEngineInitializer.onActivityPause(); + EngineInitializer.onActivityPause(BrowserActivity.this); super.onPause(); // Note: Do not add any more application logic in this method. // Move any additional app logic into handleOnPause(). @@ -295,21 +287,13 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD // Note: Intentionally left blank } - protected void handleOnStop() { - CookieManager.getInstance().flushCookieStore(); - mController.onPause(); - } - @Override protected void onDestroy() { if (LOGV_ENABLED) { Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this); } super.onDestroy(); - // mEngineInitializer can be null if onCreate is not called before onDestroy - // it happens when starting the activity with an intent while the screen is locked. - if (mEngineInitializer != null) - mEngineInitializer.onActivityDestroy(); + EngineInitializer.onActivityDestroy(BrowserActivity.this); mController.onDestroy(); mController = NullController.INSTANCE; if (!Locale.getDefault().equals(mCurrentLocale) || killOnExitDialog) { @@ -402,7 +386,7 @@ public class BrowserActivity extends Activity implements ViewTreeObserver.OnPreD @Override protected void onActivityResult (int requestCode, int resultCode, Intent intent) { - mEngineInitializer.onActivityResult(requestCode, resultCode, intent); + EngineInitializer.onActivityResult(BrowserActivity.this, requestCode, resultCode, intent); } protected void handleOnActivityResult (int requestCode, int resultCode, Intent intent) { diff --git a/src/com/android/browser/EngineInitializer.java b/src/com/android/browser/EngineInitializer.java index ef884584..7a0c2364 100644 --- a/src/com/android/browser/EngineInitializer.java +++ b/src/com/android/browser/EngineInitializer.java @@ -37,34 +37,31 @@ import android.os.Handler; import android.os.Looper; import android.os.StrictMode; import android.util.Log; +import android.view.ViewTreeObserver; import org.codeaurora.swe.BrowserCommandLine; import org.codeaurora.swe.Engine; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import org.chromium.base.VisibleForTesting; public class EngineInitializer { - private final static String LOGTAG = "EngineInitializer"; - - private BrowserActivity mActivity; - private boolean mNotifyActivity = false; - private boolean mActivityReady = false; - private boolean mActivityDestroyed = false; - private boolean mActivityStartPending = false; - private boolean mOnResumePending = false; - - private boolean mFirstDrawCompleted = false; - private boolean mLibraryLoaded = false; - private boolean mInitializationCompleted = false; + private final static String LOGTAG = "EngineInitializer"; + //Command line flag for strict mode + private final static String STRICT_MODE = "enable-strict-mode"; - private Handler mUiThreadHandler; + private static boolean mInitializationStarted = false; + private static boolean mSynchronousInitialization = false; + private static boolean mInitializationCompleted = false; + private static Handler mUiThreadHandler; - class ActivityResult + static class ActivityResult { public Intent data; public int requestCode; @@ -77,48 +74,198 @@ public class EngineInitializer { this.data = data; } } - private ArrayList<ActivityResult> mPendingActivityResults = null; - private ArrayList<Intent> mPendingIntents = null; - private static EngineInitializer sEngineInitializer = null; - public static EngineInitializer getInstance() { - if (sEngineInitializer == null) { - sEngineInitializer = new EngineInitializer(); + public static class ActivityScheduler implements ViewTreeObserver.OnPreDrawListener + { + private BrowserActivity mActivity = null; + private ArrayList<ActivityResult> mPendingActivityResults = null; + private ArrayList<Intent> mPendingIntents = null; + + private boolean mFirstDrawCompleted = false; + private boolean mOnStartPending = false; + private boolean mOnPausePending = false; + private boolean mEngineInitialized = false; + private boolean mCanForwardEvents = false; + + public ActivityScheduler(BrowserActivity activity) + { + mActivity = activity; + mFirstDrawCompleted = false; + mOnStartPending = false; + mOnPausePending = false; + mPendingIntents = null; + mPendingActivityResults = null; + mEngineInitialized = false; + mCanForwardEvents = false; + } + + @VisibleForTesting + public boolean firstDrawCompleted() { return mFirstDrawCompleted; } + @VisibleForTesting + public boolean onStartPending() { return mOnStartPending; } + @VisibleForTesting + public boolean onPausePending() { return mOnPausePending; } + @VisibleForTesting + public boolean engineInitialized() { return mEngineInitialized; } + @VisibleForTesting + public boolean canForwardEvents() { return mCanForwardEvents; } + + public void processPendingEvents() { + assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread."; + + if (mOnStartPending) { + mOnStartPending = false; + mActivity.handleOnStart(); + } + if (mOnPausePending) { + mActivity.handleOnPause(); + mOnPausePending = false; + } + if (mPendingIntents != null) { + for (int i = 0; i < mPendingIntents.size(); i++) { + mActivity.handleOnNewIntent(mPendingIntents.get(i)); + } + mPendingIntents = null; + } + if (mPendingActivityResults != null) { + for (int i = 0; i < mPendingActivityResults.size(); i++) { + ActivityResult result = mPendingActivityResults.get(i); + mActivity.handleOnActivityResult(result.requestCode, result.resultCode, result.data); + } + mPendingActivityResults = null; + } + mCanForwardEvents = true; + } + + public void onActivityCreate(boolean engineInitialized) { + mEngineInitialized = engineInitialized; + if (!mEngineInitialized) { + // Engine initialization is not completed, we should wait for the onPreDraw() notification. + final ViewTreeObserver observer = mActivity.getWindow().getDecorView().getViewTreeObserver(); + observer.addOnPreDrawListener(this); + } else { + mFirstDrawCompleted = true; + mCanForwardEvents = true; + } + } + + @Override + public boolean onPreDraw() { + final ViewTreeObserver observer = mActivity.getWindow().getDecorView().getViewTreeObserver(); + observer.removeOnPreDrawListener(this); + + if (mFirstDrawCompleted) + return true; + + mFirstDrawCompleted = true; + if (mEngineInitialized) { + postOnUiThread(new Runnable() { + @Override + public void run() { + mActivity.startController(); + processPendingEvents(); + } + }); + } + return true; + } + + public void onEngineInitializationCompletion(boolean synchronous) { + if (synchronous) { + // Don't wait for pre-draw notification if it is synchronous + onPreDraw(); + } + mEngineInitialized = true; + if (mFirstDrawCompleted) { + mActivity.startController(); + processPendingEvents(); + } + } + + public void onActivityPause() { + if (mCanForwardEvents) { + mActivity.handleOnPause(); + return; + } + mOnPausePending = true; + } + + public void onActivityResume() { + if (mCanForwardEvents) { + mActivity.handleOnResume(); + return; + } + mOnPausePending = false; + } + + public void onActivityStart() { + if (mCanForwardEvents) { + mActivity.handleOnStart(); + // TODO: We have no reliable mechanism to know when the app goes background. + //ChildProcessLauncher.onBroughtToForeground(); + return; + } + mOnStartPending = true; + } + + public void onActivityStop() { + if (!mCanForwardEvents) { + initializeSync(mActivity.getApplicationContext()); + } + mActivity.handleOnStop(); + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mCanForwardEvents) { + mActivity.handleOnActivityResult(requestCode, resultCode, data); + return; + } + if (mPendingActivityResults == null) { + mPendingActivityResults = new ArrayList<ActivityResult>(1); + } + mPendingActivityResults.add(new ActivityResult(requestCode, resultCode, data)); + } + + public void onNewIntent(Intent intent) { + if (mCanForwardEvents) { + mActivity.handleOnNewIntent(intent); + return; + } + + if (mPendingIntents == null) { + mPendingIntents = new ArrayList<Intent>(1); + } + mPendingIntents.add(intent); } - return sEngineInitializer; } + private static HashMap<BrowserActivity, ActivityScheduler> mActivitySchedulerMap = null; private static long sDelayForTesting = 0; - //Command line flag for strict mode - private static final String STRICT_MODE = "enable-strict-mode"; - @VisibleForTesting public static void setDelayForTesting(long delay) { sDelayForTesting = delay; } - private EngineInitializer() { - mUiThreadHandler = new Handler(Looper.getMainLooper()); - } - @VisibleForTesting - public boolean isInitialized() + public static boolean isInitialized() { return mInitializationCompleted; } - public boolean runningOnUiThread() { + public static boolean runningOnUiThread() { return mUiThreadHandler.getLooper() == Looper.myLooper(); } - public void postOnUiThread(Runnable task) { + public static void postOnUiThread(Runnable task) { mUiThreadHandler.post(task); } - private class InitializeTask extends AsyncTask<Void, Void, Boolean> { - public InitializeTask() { + private static class InitializeTask extends AsyncTask<Void, Void, Boolean> { + private Context mApplicationContext; + public InitializeTask(Context ctx) { + mApplicationContext = ctx; } @Override protected Boolean doInBackground(Void... unused) { @@ -129,9 +276,9 @@ public class EngineInitializer { Thread.sleep(sDelayForTesting); } - Engine.loadNativeLibraries(mActivity.getApplicationContext()); + Engine.loadNativeLibraries(mApplicationContext); - Engine.warmUpChildProcess(mActivity.getApplicationContext()); + Engine.warmUpChildProcess(mApplicationContext); return true; } @@ -144,17 +291,14 @@ public class EngineInitializer { @Override protected void onPostExecute (Boolean result) { - mLibraryLoaded = true; - if (mFirstDrawCompleted) { - completeInitializationOnUiThread(mActivity.getApplicationContext()); - } + completeInitializationOnUiThread(mApplicationContext); } } - private InitializeTask mInitializeTask = null; + private static InitializeTask mInitializeTask = null; - public void initializeSync(Context ctx) { + public static void initializeSync(Context ctx) { assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread."; - + mSynchronousInitialization = true; if (mInitializeTask != null) { try { // Wait for the InitializeTask to finish. @@ -168,40 +312,47 @@ public class EngineInitializer { } } completeInitializationOnUiThread(ctx); + mSynchronousInitialization = false; } - private void reset(BrowserActivity newActivity) { - mActivity = newActivity; - mActivityStartPending = false; - mOnResumePending = false; - mNotifyActivity = true; - mActivityReady = false; - mPendingIntents = null; - mPendingActivityResults = null; - mFirstDrawCompleted = false; - mActivityDestroyed = false; + private static void initialize(Context ctx) { + if (!mInitializationCompleted) { + if (!mInitializationStarted) { + mInitializationStarted = true; + mUiThreadHandler = new Handler(Looper.getMainLooper()); + Engine.initializeCommandLine(ctx); + mInitializeTask = new InitializeTask(ctx); + mInitializeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + mActivitySchedulerMap = new HashMap<BrowserActivity, ActivityScheduler>(); + } else { + // This is not the first activity, wait for the engine initialization to finish. + initializeSync(ctx); + } + } } - public void onActivityCreate(BrowserActivity activity) { + public static ActivityScheduler onActivityCreate(BrowserActivity activity) { assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread."; - reset(activity); + + Context ctx = activity.getApplicationContext(); + ActivityScheduler scheduler = new ActivityScheduler(activity); + initialize(ctx); + + scheduler.onActivityCreate(mInitializationCompleted); if (!mInitializationCompleted) { - Engine.initializeCommandLine(mActivity.getApplicationContext()); - mInitializeTask = new InitializeTask(); - mInitializeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + mActivitySchedulerMap.put(activity, scheduler); } + return scheduler; } - private void completeInitialization() { - postOnUiThread(new Runnable() { - @Override - public void run() { - completeInitializationOnUiThread(mActivity.getApplicationContext()); - } - }); + public static void onPostActivityCreate(BrowserActivity activity) { + EngineInitializer.initializeResourceExtractor(activity); + if (EngineInitializer.isInitialized()) { + activity.startController(); + } } - private void completeInitializationOnUiThread(Context ctx) { + private static void completeInitializationOnUiThread(Context ctx) { assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread."; if (!mInitializationCompleted) { @@ -230,184 +381,55 @@ public class EngineInitializer { //Enable remote debugging by default Engine.setWebContentsDebuggingEnabled(true); mInitializationCompleted = true; - mLibraryLoaded = true; + mInitializationStarted = true; BrowserSettings.getInstance().onEngineInitializationComplete(); - } - if (mActivity != null && mNotifyActivity) { - mNotifyActivity = false; - postOnUiThread(new Runnable() { - @Override - public void run() { - mActivity.onEngineInitializationComplete(); - mActivityReady = true; - processPendingEvents(); - } - }); - } - - } - - private void completeInitializationAsynOnUiThread(final Context ctx) { - assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread."; - - if (!mInitializationCompleted) { - // TODO: Evaluate the benefit of async Engine.initialize() - Engine.StartupCallback callback = - new Engine.StartupCallback() { - @Override - public void onSuccess(boolean alreadyStarted) { - if (Looper.myLooper() == Looper.getMainLooper()) { - Log.e(LOGTAG, "SWE engine initialization success"); - // Add the browser commandline options - BrowserConfig.getInstance(ctx).initCommandLineSwitches(); - - //Note: Only enable this for debugging. - if (BrowserCommandLine.hasSwitch(STRICT_MODE)) { - Log.v(LOGTAG, "StrictMode enabled"); - StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() - .detectDiskReads() - .detectDiskWrites() - .detectNetwork() - .penaltyLog() - .build()); - StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() - .detectLeakedSqlLiteObjects() - .detectLeakedClosableObjects() - .penaltyLog() - .penaltyDeath() - .build()); - } - - //Enable remote debugging by default - Engine.setWebContentsDebuggingEnabled(true); - mInitializationCompleted = true; - mLibraryLoaded = true; - BrowserSettings.getInstance().onEngineInitializationComplete(); - - if (mActivity != null && mNotifyActivity) { - mNotifyActivity = false; - postOnUiThread(new Runnable() { - @Override - public void run() { - mActivity.onEngineInitializationComplete(); - mActivityReady = true; - processPendingEvents(); - } - }); - } - } - } - - @Override - public void onFailure() { - Log.e(LOGTAG, "SWE engine initialization failed"); - } - }; - Engine.initialize(ctx, callback); - } - } + Engine.resumeTracing(ctx); - private void processPendingEvents() { - assert runningOnUiThread() : "Tried to initialize the engine on the wrong thread."; - - if (mActivityStartPending) { - mActivityStartPending = false; - onActivityStart(); - } - if (mPendingIntents != null) { - for (int i = 0; i < mPendingIntents.size(); i++) { - mActivity.handleOnNewIntent(mPendingIntents.get(i)); + if (mActivitySchedulerMap != null) { + for (Map.Entry<BrowserActivity, ActivityScheduler> entry : mActivitySchedulerMap.entrySet()) { + entry.getValue().onEngineInitializationCompletion(mSynchronousInitialization); + } + mActivitySchedulerMap.clear(); } - mPendingIntents = null; } - if (mPendingActivityResults != null) { - for (int i = 0; i < mPendingActivityResults.size(); i++) { - ActivityResult result = mPendingActivityResults.get(i); - mActivity.handleOnActivityResult(result.requestCode, result.resultCode, result.data); - } - mPendingActivityResults = null; - } - if (mOnResumePending && !mActivityDestroyed) { - onActivityResume(); - } - mOnResumePending = false; } - public void onPreDraw() { - mFirstDrawCompleted = true; - if (mLibraryLoaded) { - completeInitialization(); - } + public static void initializeResourceExtractor(Context ctx) { + Engine.startExtractingResources(ctx); } - public void initializeResourceExtractor(Context ctx) { - Engine.startExtractingResources(ctx); + public static void onPreDraw(BrowserActivity activity) { + activity.getScheduler().onPreDraw(); } - public void onActivityPause() { - mOnResumePending = false; - if (mActivityReady) { - mActivity.handleOnPause(); - } + public static void onActivityPause(BrowserActivity activity) { + activity.getScheduler().onActivityPause(); } - public void onActivityStop() { - mActivityStartPending = false; - if (mActivityReady) { - Engine.pauseTracing(mActivity.getApplicationContext()); - mActivity.handleOnStop(); - } + public static void onActivityStop(BrowserActivity activity) { + activity.getScheduler().onActivityStop(); } - public void onActivityResume() { - if (mActivityReady) { - mActivity.handleOnResume(); - return; - } - mOnResumePending = true; + public static void onActivityResume(BrowserActivity activity) { + activity.getScheduler().onActivityResume(); } - public void onActivityStart() { - if (mActivityReady) { - Engine.resumeTracing(mActivity.getApplicationContext()); - mActivity.handleOnStart(); - // TODO: We have no reliable mechanism to know when the app goes background. - //ChildProcessLauncher.onBroughtToForeground(); - return; - } - mActivityStartPending = true; + public static void onActivityStart(BrowserActivity activity) { + activity.getScheduler().onActivityStart(); } - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (mActivityReady) { - mActivity.handleOnActivityResult(requestCode, resultCode, data); - return; - } - if (mPendingActivityResults == null) { - mPendingActivityResults = new ArrayList<ActivityResult>(1); - } - mPendingActivityResults.add(new ActivityResult(requestCode, resultCode, data)); + public static void onActivityResult(BrowserActivity activity, int requestCode, int resultCode, Intent data) { + activity.getScheduler().onActivityResult(requestCode, resultCode, data); } - public void onNewIntent(Intent intent) { + public static void onNewIntent(BrowserActivity activity, Intent intent) { if (BrowserActivity.ACTION_RESTART.equals(intent.getAction())) { Engine.releaseSpareChildProcess(); } - if (mActivityReady) { - mActivity.handleOnNewIntent(intent); - return; - } - - if (mPendingIntents == null) { - mPendingIntents = new ArrayList<Intent>(1); - } - mPendingIntents.add(intent); + activity.getScheduler().onNewIntent(intent); } - public void onActivityDestroy() { + public static void onActivityDestroy(BrowserActivity activity) { Engine.releaseSpareChildProcess(); - mActivityDestroyed = true; } - - } diff --git a/src/com/android/browser/test/EngineInitializerTest.java b/src/com/android/browser/test/EngineInitializerTest.java deleted file mode 100644 index f1d8acc1..00000000 --- a/src/com/android/browser/test/EngineInitializerTest.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (c) 2014 The Linux Foundation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * * Neither the name of The Linux Foundation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR - * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE - * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN - * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -package com.android.browser.test; - -import android.app.Activity; -import android.app.Instrumentation; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.test.InstrumentationTestCase; -import android.view.KeyEvent; - -import com.android.browser.BrowserActivity; -import com.android.browser.BrowserPreferencesPage; -import com.android.browser.EngineInitializer; - -public class EngineInitializerTest extends InstrumentationTestCase { - - private Instrumentation mInstrumentation; - private Context mContext; - - @Override - protected void setUp() throws Exception { - super.setUp(); - mInstrumentation = getInstrumentation(); - mContext = getInstrumentation().getTargetContext(); - } - - - public void test01() throws Throwable { - Intent localIntent = new Intent(); - localIntent.setClassName("com.android.swe.browser", BrowserActivity.class.getName()); - localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - final Activity activity = mInstrumentation.startActivitySync(localIntent); - - Intent restart = new Intent(BrowserActivity.ACTION_RESTART, null); - restart.setClassName("com.android.swe.browser", BrowserActivity.class.getName()); - restart.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - mInstrumentation.callActivityOnNewIntent(activity, restart); - - Thread.sleep(2000); - - mInstrumentation.waitForIdleSync(); - - assertEquals(EngineInitializer.getInstance().isInitialized(), true); - - } - - - public void test02() throws Throwable { - - EngineInitializer.setDelayForTesting(1000); - - Intent localIntent = new Intent(); - localIntent.setClassName("com.android.swe.browser", BrowserActivity.class.getName()); - localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - final Activity browserActivity = mInstrumentation.startActivitySync(localIntent); - - Intent pref = new Intent(); - pref.setClassName("com.android.swe.browser", BrowserPreferencesPage.class.getName()); - pref.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - final Activity preferencesActivity = mInstrumentation.startActivitySync(pref); - - Thread.sleep(2000); - - mInstrumentation.waitForIdleSync(); - - assertEquals(EngineInitializer.getInstance().isInitialized(), true); - } - - - public void test03() throws Throwable { - - EngineInitializer.setDelayForTesting(2000); - - Intent pref = new Intent(); - pref.setClassName("com.android.swe.browser", BrowserPreferencesPage.class.getName()); - pref.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - final Activity preferencesActivity = mInstrumentation.startActivitySync(pref); - - assertEquals(EngineInitializer.getInstance().isInitialized(), true); - - Intent localIntent = new Intent(); - localIntent.setClassName("com.android.swe.browser", BrowserActivity.class.getName()); - localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - final Activity browserActivity = mInstrumentation.startActivitySync(localIntent); - - Thread.sleep(3000); - - mInstrumentation.waitForIdleSync(); - - } - - - public void test04() throws Throwable { - - EngineInitializer.setDelayForTesting(2000); - - - Intent localIntent = new Intent(); - localIntent.setClassName("com.android.swe.browser", BrowserActivity.class.getName()); - localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - final Activity browserActivity = mInstrumentation.startActivitySync(localIntent); - - final Intent restart = new Intent(BrowserActivity.ACTION_RESTART, null); - restart.setClassName("com.android.swe.browser", BrowserActivity.class.getName()); - restart.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - runTestOnUiThread(new Runnable () { - @Override - public void run() { - mInstrumentation.callActivityOnNewIntent(browserActivity, restart); - } - }); - - Intent pref = new Intent(); - pref.setClassName("com.android.swe.browser", BrowserPreferencesPage.class.getName()); - pref.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - final Activity preferencesActivity = mInstrumentation.startActivitySync(pref); - - Thread.sleep(3000); - - mInstrumentation.waitForIdleSync(); - - assertEquals(EngineInitializer.getInstance().isInitialized(), true); - } - - - public void test05() throws Throwable { - - EngineInitializer.setDelayForTesting(2000); - - - Intent localIntent = new Intent(); - localIntent.setClassName("com.android.swe.browser", BrowserActivity.class.getName()); - localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - final Activity browserActivity = mInstrumentation.startActivitySync(localIntent); - - runTestOnUiThread(new Runnable () { - @Override - public void run() { - Bundle outState = new Bundle(); - mInstrumentation.callActivityOnSaveInstanceState(browserActivity, outState); - mInstrumentation.callActivityOnDestroy(browserActivity); - } - }); - - Thread.sleep(3000); - - mInstrumentation.waitForIdleSync(); - - assertEquals(EngineInitializer.getInstance().isInitialized(), true); - } - - - public void test06() throws Throwable { - - EngineInitializer.setDelayForTesting(2000); - - - Intent localIntent = new Intent(); - localIntent.setClassName("com.android.swe.browser", BrowserActivity.class.getName()); - localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_NEW_TASK); - - final Activity browserActivity = mInstrumentation.startActivitySync(localIntent); - - mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_MENU); - - Thread.sleep(3000); - - mInstrumentation.waitForIdleSync(); - - assertEquals(EngineInitializer.getInstance().isInitialized(), true); - } - -} diff --git a/swe_android_browser.gypi b/swe_android_browser.gypi index ce875e57..0a6dca5a 100644 --- a/swe_android_browser.gypi +++ b/swe_android_browser.gypi @@ -1,6 +1,7 @@ { 'variables' : { 'manifest_package_name%' : 'org.codeaurora.swe.browser.beta', + 'manifest_test_package_name%' : 'org.codeaurora.swe.browser.beta.tests', }, 'targets' : [ { @@ -84,5 +85,33 @@ }, 'includes': [ '../../build/android/jinja_template.gypi' ], }, + { + 'target_name': 'swe_android_browser_fake_apk', + 'type': 'none', + 'dependencies': [ + 'swe_android_browser_apk', + ], + 'includes': [ '../../build/apk_fake_jar.gypi' ], + }, + + { + 'target_name': 'swe_android_browser_tests_apk', + 'type': 'none', + 'dependencies': [ + 'swe_android_browser_fake_apk', + '../base/base.gyp:base_java_test_support', + '../content/content_shell_and_tests.gyp:content_java_test_support', + '../net/net.gyp:net_java_test_support', + ], + 'variables': { + 'apk_name': 'SWEBrowserTests', + 'override_package_name': '<(manifest_test_package_name)', + 'android_manifest_path': './tests/AndroidManifest.xml', + 'java_in_dir': './tests/startup', + 'is_test_apk': 1, + 'test_suite_name': 'swe_android_browser_tests', + }, + 'includes': [ '../../build/java_apk.gypi' ], + }, ], } diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index cb1ffc77..984275b2 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -31,12 +31,17 @@ "adb shell am instrument -w com.android.browser.tests/android.test.InstrumentationTestRunner" --> <instrumentation android:name="android.test.InstrumentationTestRunner" - android:targetPackage="com.android.browser" + android:targetPackage="org.codeaurora.swe.browser.beta" android:label="Tests for Browser."/> + <uses-permission android:name="android.permission.RUN_INSTRUMENTATION" /> + <uses-permission android:name="android.permission.INJECT_EVENTS" /> + + <!-- <instrumentation android:name="com.android.browser.BrowserLaunchPerformance" android:targetPackage="com.android.browser" android:label="Browser Launch Performance"> </instrumentation> + --> </manifest> diff --git a/tests/startup/src/com/android/browser/tests/EngineInitializerTest.java b/tests/startup/src/com/android/browser/tests/EngineInitializerTest.java new file mode 100644 index 00000000..ce8e3206 --- /dev/null +++ b/tests/startup/src/com/android/browser/tests/EngineInitializerTest.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2014 The Linux Foundation. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +package com.android.browser.tests; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.SystemClock; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import com.android.browser.BrowserActivity; +import com.android.browser.BrowserPreferencesPage; +import com.android.browser.EngineInitializer; + +public class EngineInitializerTest extends InstrumentationTestCase { + + private static final String gTestTargetPackageName = "org.codeaurora.swe.browser.beta"; + private static final int gEngineInitDelay = 2000; + private Instrumentation mInstrumentation; + private Context mContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mInstrumentation = getInstrumentation(); + mContext = getInstrumentation().getTargetContext(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + @LargeTest + public void test01() throws Throwable { + + Intent localIntent = new Intent(); + localIntent.setClassName(gTestTargetPackageName, BrowserActivity.class.getName()); + localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + + final Intent restart = new Intent(BrowserActivity.ACTION_RESTART, null); + restart.setClassName(gTestTargetPackageName, BrowserActivity.class.getName()); + restart.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + EngineInitializer.setDelayForTesting(gEngineInitDelay); + final BrowserActivity activity = (BrowserActivity) mInstrumentation.startActivitySync(localIntent); + runTestOnUiThread(new Runnable() { + @Override + public void run() { + assertEquals(EngineInitializer.isInitialized(), false); + assertEquals(activity.getScheduler().canForwardEvents(), false); + assertEquals(activity.getScheduler().onStartPending(), true); + assertEquals(activity.getScheduler().engineInitialized(), false); + assertEquals(activity.getScheduler().onPausePending(), false); + + mInstrumentation.callActivityOnPause(activity); + assertEquals(activity.getScheduler().onPausePending(), true); + + mInstrumentation.callActivityOnStop(activity); + assertEquals(EngineInitializer.isInitialized(), true); + assertEquals(activity.getScheduler().engineInitialized(), true); + assertEquals(activity.getScheduler().onStartPending(), false); + assertEquals(activity.getScheduler().onPausePending(), false); + assertEquals(activity.getScheduler().canForwardEvents(), true); + + mInstrumentation.callActivityOnNewIntent(activity, restart); + } + }); + mInstrumentation.waitForIdleSync(); + } + + @LargeTest + public void test02() throws Throwable { + + EngineInitializer.setDelayForTesting(gEngineInitDelay); + + Intent localIntent = new Intent(); + localIntent.setClassName(gTestTargetPackageName, BrowserActivity.class.getName()); + localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + final BrowserActivity activity = (BrowserActivity)mInstrumentation.startActivitySync(localIntent); + runTestOnUiThread(new Runnable() { + @Override + public void run() { + assertEquals(EngineInitializer.isInitialized(), false); + assertEquals(activity.getScheduler().onStartPending(), true); + assertEquals(activity.getScheduler().onPausePending(), false); + assertEquals(activity.getScheduler().engineInitialized(), false); + assertEquals(activity.getScheduler().canForwardEvents(), false); + + mInstrumentation.callActivityOnPause(activity); + assertEquals(activity.getScheduler().onPausePending(), true); + + mInstrumentation.callActivityOnResume(activity); + assertEquals(activity.getScheduler().onPausePending(), false); + + mInstrumentation.callActivityOnPause(activity); + assertEquals(activity.getScheduler().onStartPending(), true); + assertEquals(activity.getScheduler().onPausePending(), true); + assertEquals(activity.getScheduler().canForwardEvents(), false); + } + }); + + Intent pref = new Intent(); + pref.setClassName(gTestTargetPackageName, BrowserPreferencesPage.class.getName()); + pref.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + final Activity preferencesActivity = mInstrumentation.startActivitySync(pref); + runTestOnUiThread(new Runnable() { + @Override + public void run() { + assertEquals(EngineInitializer.isInitialized(), true); + assertEquals(activity.getScheduler().onStartPending(), false); + assertEquals(activity.getScheduler().onPausePending(), false); + assertEquals(activity.getScheduler().engineInitialized(), true); + assertEquals(activity.getScheduler().canForwardEvents(), true); + } + }); + + mInstrumentation.waitForIdleSync(); + } + + @LargeTest + public void test03() throws Throwable { + + EngineInitializer.setDelayForTesting(gEngineInitDelay); + + Intent pref = new Intent(); + pref.setClassName(gTestTargetPackageName, BrowserPreferencesPage.class.getName()); + pref.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + assertEquals(EngineInitializer.isInitialized(), false); + final Activity preferencesActivity = mInstrumentation.startActivitySync(pref); + assertEquals(EngineInitializer.isInitialized(), true); + + Intent localIntent = new Intent(); + localIntent.setClassName(gTestTargetPackageName, BrowserActivity.class.getName()); + localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + final BrowserActivity activity = (BrowserActivity)mInstrumentation.startActivitySync(localIntent); + runTestOnUiThread(new Runnable() { + @Override + public void run() { + assertEquals(EngineInitializer.isInitialized(), true); + assertEquals(activity.getScheduler().onStartPending(), false); + assertEquals(activity.getScheduler().onPausePending(), false); + assertEquals(activity.getScheduler().engineInitialized(), true); + assertEquals(activity.getScheduler().canForwardEvents(), true); + } + }); + + mInstrumentation.waitForIdleSync(); + } + + @LargeTest + public void test04() throws Throwable { + + EngineInitializer.setDelayForTesting(gEngineInitDelay); + + Intent localIntent = new Intent(); + localIntent.setClassName(gTestTargetPackageName, BrowserActivity.class.getName()); + localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + assertEquals(EngineInitializer.isInitialized(), false); + final BrowserActivity activity = (BrowserActivity)mInstrumentation.startActivitySync(localIntent); + runTestOnUiThread(new Runnable() { + @Override + public void run() { + assertEquals(EngineInitializer.isInitialized(), false); + assertEquals(activity.getScheduler().onStartPending(), true); + assertEquals(activity.getScheduler().onPausePending(), false); + assertEquals(activity.getScheduler().engineInitialized(), false); + assertEquals(activity.getScheduler().canForwardEvents(), false); + } + }); + + final Intent restart = new Intent(BrowserActivity.ACTION_RESTART, null); + restart.setClassName(gTestTargetPackageName, BrowserActivity.class.getName()); + restart.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + runTestOnUiThread(new Runnable() { + @Override + public void run() { + mInstrumentation.callActivityOnNewIntent(activity, restart); + } + }); + + Intent pref = new Intent(); + pref.setClassName(gTestTargetPackageName, BrowserPreferencesPage.class.getName()); + pref.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + final Activity preferencesActivity = mInstrumentation.startActivitySync(pref); + assertEquals(EngineInitializer.isInitialized(), true); + + mInstrumentation.waitForIdleSync(); + } + + + @LargeTest + public void test05() throws Throwable { + + EngineInitializer.setDelayForTesting(gEngineInitDelay); + + Intent localIntent = new Intent(); + localIntent.setClassName(gTestTargetPackageName, BrowserActivity.class.getName()); + localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + final BrowserActivity activity = (BrowserActivity)mInstrumentation.startActivitySync(localIntent); + + runTestOnUiThread(new Runnable() { + @Override + public void run() { + Bundle outState = new Bundle(); + mInstrumentation.callActivityOnPause(activity); + assertEquals(activity.getScheduler().onStartPending(), true); + assertEquals(activity.getScheduler().onPausePending(), true); + mInstrumentation.callActivityOnSaveInstanceState(activity, outState); + assertEquals(activity.getScheduler().engineInitialized(), false); + assertEquals(activity.getScheduler().canForwardEvents(), false); + mInstrumentation.callActivityOnStop(activity); + assertEquals(EngineInitializer.isInitialized(), true); + assertEquals(activity.getScheduler().engineInitialized(), true); + mInstrumentation.callActivityOnRestart(activity); + } + }); + + mInstrumentation.waitForIdleSync(); + } + + @LargeTest + public void test06() throws Throwable { + + EngineInitializer.setDelayForTesting(gEngineInitDelay); + + Intent localIntent = new Intent(); + localIntent.setClassName(gTestTargetPackageName, BrowserActivity.class.getName()); + localIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + final BrowserActivity activity = (BrowserActivity)mInstrumentation.startActivitySync(localIntent); + assertEquals(EngineInitializer.isInitialized(), false); + + mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_MENU); + long now = SystemClock.uptimeMillis(); + mInstrumentation.sendTrackballEventSync(MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 100, 100, 0)); + mInstrumentation.sendTrackballEventSync(MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 100, 100, 0)); + now = SystemClock.uptimeMillis(); + mInstrumentation.sendPointerSync(MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 100, 100, 0)); + mInstrumentation.sendPointerSync(MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 100, 100, 0)); + + mInstrumentation.callActivityOnUserLeaving(activity); + + mInstrumentation.waitForIdleSync(); + + runTestOnUiThread(new Runnable() { + @Override + public void run() { + Bundle outState = new Bundle(); + mInstrumentation.callActivityOnPause(activity); + assertEquals(activity.getScheduler().onStartPending(), true); + assertEquals(activity.getScheduler().onPausePending(), true); + mInstrumentation.callActivityOnSaveInstanceState(activity, outState); + assertEquals(activity.getScheduler().engineInitialized(), false); + assertEquals(activity.getScheduler().canForwardEvents(), false); + mInstrumentation.callActivityOnStop(activity); + assertEquals(EngineInitializer.isInitialized(), true); + mInstrumentation.callActivityOnRestart(activity); + } + }); + + mInstrumentation.waitForIdleSync(); + } +} |