114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton/*
214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * Copyright 2018 The Android Open Source Project
314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton *
414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * Licensed under the Apache License, Version 2.0 (the "License");
514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * you may not use this file except in compliance with the License.
614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * You may obtain a copy of the License at
714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton *
814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton *      http://www.apache.org/licenses/LICENSE-2.0
914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton *
1014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * Unless required by applicable law or agreed to in writing, software
1114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * distributed under the License is distributed on an "AS IS" BASIS,
1214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * See the License for the specific language governing permissions and
1414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton * limitations under the License.
1514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton */
1614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
1714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonpackage androidx.webkit;
1814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
1914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport static org.junit.Assert.assertEquals;
20610473e5bb1295c992f8be92311bbfad96f03311Gustav Senntonimport static org.junit.Assume.assumeTrue;
2114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
2214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport android.support.test.filters.MediumTest;
2314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport android.support.test.runner.AndroidJUnit4;
2414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport android.webkit.JavascriptInterface;
2514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport android.webkit.WebResourceRequest;
2614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport android.webkit.WebResourceResponse;
2714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport android.webkit.WebView;
2814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
2914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport org.junit.After;
3014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport org.junit.Before;
3114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport org.junit.Test;
3214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport org.junit.runner.RunWith;
3314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
3414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport java.io.ByteArrayInputStream;
3514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport java.util.ArrayList;
3614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport java.util.List;
3714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonimport java.util.concurrent.Callable;
3814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
3914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton@MediumTest
4014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton@RunWith(AndroidJUnit4.class)
4114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Senntonpublic class ServiceWorkerClientCompatTest {
4214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
4314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    // The BASE_URL does not matter since the tests will intercept the load, but it should be https
4414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    // for the Service Worker registration to succeed.
4514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final String BASE_URL = "https://www.example.com/";
4614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final String INDEX_URL = BASE_URL + "index.html";
4714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final String SW_URL = BASE_URL + "sw.js";
4814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final String FETCH_URL = BASE_URL + "fetch.html";
4914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
5014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final String JS_INTERFACE_NAME = "Android";
5114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final int POLLING_TIMEOUT = 10 * 1000;
5214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
5314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    // static HTML page always injected instead of the url loaded.
5414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final String INDEX_RAW_HTML =
5514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            "<!DOCTYPE html>\n"
5614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "<html>\n"
5714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "  <body>\n"
5814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "    <script>\n"
5914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "      navigator.serviceWorker.register('sw.js').then(function(reg) {\n"
6014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "         " + JS_INTERFACE_NAME + ".registrationSuccess();\n"
6114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "      }).catch(function(err) {\n"
6214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "         console.error(err);\n"
6314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "      });\n"
6414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "    </script>\n"
6514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "  </body>\n"
6614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "</html>\n";
6714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final String SW_RAW_HTML = "fetch('fetch.html');";
6814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static final String SW_UNREGISTER_RAW_JS =
6914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            "navigator.serviceWorker.getRegistration().then(function(r) {"
7014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "  r.unregister().then(function(success) {"
7114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "    if (success) " + JS_INTERFACE_NAME + ".unregisterSuccess();"
7214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "    else console.error('unregister() was not successful');"
7314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "  });"
7414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "}).catch(function(err) {"
7514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "   console.error(err);"
7614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    + "});";
7714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
7814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private JavascriptStatusReceiver mJavascriptStatusReceiver;
7914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private WebViewOnUiThread mOnUiThread;
8014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
8114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient,
8214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    // so this test subclasses the WebViewClient from WebViewOnUiThread.
8314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    private static class InterceptClient extends WebViewOnUiThread.WaitForLoadedClient {
8414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
8514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        InterceptClient(WebViewOnUiThread webViewOnUiThread) throws Exception {
8614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            super(webViewOnUiThread);
8714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        }
8814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
8914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        @Override
9014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        public WebResourceResponse shouldInterceptRequest(WebView view,
9114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                WebResourceRequest request) {
9214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            // Only return content for INDEX_URL, deny all other requests.
9314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            try {
9414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                if (request.getUrl().toString().equals(INDEX_URL)) {
9514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    return new WebResourceResponse("text/html", "utf-8",
9614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                            new ByteArrayInputStream(INDEX_RAW_HTML.getBytes("UTF-8")));
9714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                }
9814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            } catch (java.io.UnsupportedEncodingException e) { }
9914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            return new WebResourceResponse("text/html", "UTF-8", null);
10014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        }
10114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    }
10214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
10314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    public static class InterceptServiceWorkerClient extends ServiceWorkerClientCompat {
10414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        private List<WebResourceRequest> mInterceptedRequests = new ArrayList<WebResourceRequest>();
10514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
10614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        @Override
10714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
10814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            // Records intercepted requests and only return content for SW_URL.
10914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            mInterceptedRequests.add(request);
11014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            try {
11114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                if (request.getUrl().toString().equals(SW_URL)) {
11214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                    return new WebResourceResponse("application/javascript", "utf-8",
11314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                            new ByteArrayInputStream(SW_RAW_HTML.getBytes("UTF-8")));
11414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                }
11514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            } catch (java.io.UnsupportedEncodingException e) { }
11614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            return new WebResourceResponse("text/html", "UTF-8", null);
11714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        }
11814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
11914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        List<WebResourceRequest> getInterceptedRequests() {
12014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            return mInterceptedRequests;
12114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        }
12214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    }
12314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
12414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    @Before
12514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    public void setUp() throws Exception {
12614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        mOnUiThread = new WebViewOnUiThread();
12714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        mOnUiThread.getSettings().setJavaScriptEnabled(true);
12814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
12914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        mJavascriptStatusReceiver = new JavascriptStatusReceiver();
13014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        mOnUiThread.addJavascriptInterface(mJavascriptStatusReceiver, JS_INTERFACE_NAME);
13114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        mOnUiThread.setWebViewClient(new InterceptClient(mOnUiThread));
13214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    }
13314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
13414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    @After
13514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    public void tearDown() throws Exception {
13614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        if (mOnUiThread != null) {
13714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            mOnUiThread.cleanUp();
13814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        }
13914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    }
14014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
14114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    // Test correct invocation of shouldInterceptRequest for Service Workers.
14214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    @Test
14314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    public void testServiceWorkerClientInterceptCallback() throws Exception {
144610473e5bb1295c992f8be92311bbfad96f03311Gustav Sennton        assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BASIC_USAGE));
145610473e5bb1295c992f8be92311bbfad96f03311Gustav Sennton        assumeTrue(WebViewFeature.isFeatureSupported(
146610473e5bb1295c992f8be92311bbfad96f03311Gustav Sennton                WebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST));
14714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
14814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        final InterceptServiceWorkerClient mInterceptServiceWorkerClient =
14914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                new InterceptServiceWorkerClient();
15014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        ServiceWorkerControllerCompat swController = ServiceWorkerControllerCompat.getInstance();
15114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        swController.setServiceWorkerClient(mInterceptServiceWorkerClient);
15214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
15314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        mOnUiThread.loadUrlAndWaitForCompletion(INDEX_URL);
15414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
15514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        Callable<Boolean> registrationSuccess = new Callable<Boolean>() {
15614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            @Override
15714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            public Boolean call() {
15814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                return mJavascriptStatusReceiver.mRegistrationSuccess;
15914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            }
16014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        };
16114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        PollingCheck.check("JS could not register Service Worker", POLLING_TIMEOUT,
16214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                registrationSuccess);
16314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
16414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        Callable<Boolean> receivedRequest = new Callable<Boolean>() {
16514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            @Override
16614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            public Boolean call() {
16714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                return mInterceptServiceWorkerClient.getInterceptedRequests().size() >= 2;
16814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            }
16914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        };
17014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        PollingCheck.check("Service Worker intercept callbacks not invoked", POLLING_TIMEOUT,
17114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                receivedRequest);
17214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
17314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        List<WebResourceRequest> requests = mInterceptServiceWorkerClient.getInterceptedRequests();
17414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        assertEquals(2, requests.size());
17514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        assertEquals(SW_URL, requests.get(0).getUrl().toString());
17614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        assertEquals(FETCH_URL, requests.get(1).getUrl().toString());
17714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
17814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        // Clean-up, make sure to unregister the Service Worker.
17914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        mOnUiThread.evaluateJavascript(SW_UNREGISTER_RAW_JS, null);
18014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        Callable<Boolean> unregisterSuccess = new Callable<Boolean>() {
18114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            @Override
18214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            public Boolean call() {
18314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                return mJavascriptStatusReceiver.mUnregisterSuccess;
18414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            }
18514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        };
18614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        PollingCheck.check("JS could not unregister Service Worker", POLLING_TIMEOUT,
18714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton                unregisterSuccess);
18814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    }
18914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
19014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    // Object added to the page via AddJavascriptInterface() that is used by the test Javascript to
19114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    // notify back to Java if the Service Worker registration was successful.
19214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    public static final class JavascriptStatusReceiver {
19314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        public volatile boolean mRegistrationSuccess = false;
19414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        public volatile boolean mUnregisterSuccess = false;
19514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
19614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        @JavascriptInterface
19714b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        public void registrationSuccess() {
19814b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            mRegistrationSuccess = true;
19914b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        }
20014b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton
20114b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        @JavascriptInterface
20214b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        public void unregisterSuccess() {
20314b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton            mUnregisterSuccess = true;
20414b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton        }
20514b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton    }
20614b9f252b74caf73f6a2967722a465f075b3bc1eGustav Sennton}
207