OLD | NEW |
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 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.printing; | 5 package org.chromium.chrome.browser.printing; |
6 | 6 |
7 import android.annotation.TargetApi; | 7 import android.annotation.TargetApi; |
8 import android.os.Build; | 8 import android.os.Build; |
9 import android.os.CancellationSignal; | 9 import android.os.CancellationSignal; |
10 import android.os.ParcelFileDescriptor; | 10 import android.os.ParcelFileDescriptor; |
11 import android.print.PageRange; | 11 import android.print.PageRange; |
12 import android.print.PrintAttributes; | 12 import android.print.PrintAttributes; |
13 import android.print.PrintDocumentAdapter; | 13 import android.print.PrintDocumentAdapter; |
14 import android.print.PrintDocumentInfo; | 14 import android.print.PrintDocumentInfo; |
15 import android.support.test.InstrumentationRegistry; | |
16 import android.support.test.filters.LargeTest; | 15 import android.support.test.filters.LargeTest; |
17 | 16 |
18 import org.junit.Assert; | |
19 import org.junit.Before; | |
20 import org.junit.Rule; | |
21 import org.junit.Test; | |
22 import org.junit.runner.RunWith; | |
23 | |
24 import org.chromium.base.ApiCompatibilityUtils; | 17 import org.chromium.base.ApiCompatibilityUtils; |
25 import org.chromium.base.test.util.CommandLineFlags; | 18 import org.chromium.base.test.util.CommandLineFlags; |
26 import org.chromium.base.test.util.DisabledTest; | 19 import org.chromium.base.test.util.DisabledTest; |
27 import org.chromium.base.test.util.Feature; | 20 import org.chromium.base.test.util.Feature; |
28 import org.chromium.base.test.util.RetryOnFailure; | 21 import org.chromium.base.test.util.RetryOnFailure; |
29 import org.chromium.base.test.util.TestFileUtil; | 22 import org.chromium.base.test.util.TestFileUtil; |
30 import org.chromium.base.test.util.UrlUtils; | 23 import org.chromium.base.test.util.UrlUtils; |
31 import org.chromium.chrome.browser.ChromeActivity; | 24 import org.chromium.chrome.browser.ChromeActivity; |
32 import org.chromium.chrome.browser.ChromeSwitches; | |
33 import org.chromium.chrome.browser.tab.Tab; | 25 import org.chromium.chrome.browser.tab.Tab; |
34 import org.chromium.chrome.test.ChromeActivityTestRule; | 26 import org.chromium.chrome.test.ChromeActivityTestCaseBase; |
35 import org.chromium.chrome.test.ChromeJUnit4ClassRunner; | |
36 import org.chromium.chrome.test.util.browser.TabTitleObserver; | 27 import org.chromium.chrome.test.util.browser.TabTitleObserver; |
37 import org.chromium.content.common.ContentSwitches; | 28 import org.chromium.content.common.ContentSwitches; |
38 import org.chromium.printing.PrintDocumentAdapterWrapper; | 29 import org.chromium.printing.PrintDocumentAdapterWrapper; |
39 import org.chromium.printing.PrintManagerDelegate; | 30 import org.chromium.printing.PrintManagerDelegate; |
40 import org.chromium.printing.PrintingControllerImpl; | 31 import org.chromium.printing.PrintingControllerImpl; |
41 | 32 |
42 import java.io.File; | 33 import java.io.File; |
43 import java.io.FileInputStream; | 34 import java.io.FileInputStream; |
44 import java.io.IOException; | 35 import java.io.IOException; |
45 import java.util.concurrent.Callable; | 36 import java.util.concurrent.Callable; |
46 import java.util.concurrent.FutureTask; | 37 import java.util.concurrent.FutureTask; |
47 import java.util.concurrent.TimeUnit; | 38 import java.util.concurrent.TimeUnit; |
48 | 39 |
49 /** | 40 /** |
50 * Tests Android printing. | 41 * Tests Android printing. |
51 * TODO(cimamoglu): Add a test with cancellation. | 42 * TODO(cimamoglu): Add a test with cancellation. |
52 * TODO(cimamoglu): Add a test with multiple, stacked onLayout/onWrite calls. | 43 * TODO(cimamoglu): Add a test with multiple, stacked onLayout/onWrite calls. |
53 * TODO(cimamoglu): Add a test which emulates Chromium failing to generate a PDF
. | 44 * TODO(cimamoglu): Add a test which emulates Chromium failing to generate a PDF
. |
54 */ | 45 */ |
55 @RunWith(ChromeJUnit4ClassRunner.class) | |
56 @RetryOnFailure | 46 @RetryOnFailure |
57 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, | 47 public class PrintingControllerTest extends ChromeActivityTestCaseBase<ChromeAct
ivity> { |
58 ChromeActivityTestRule.DISABLE_NETWORK_PREDICTION_FLAG}) | |
59 public class PrintingControllerTest { | |
60 @Rule | |
61 public ChromeActivityTestRule<ChromeActivity> mActivityTestRule = | |
62 new ChromeActivityTestRule<>(ChromeActivity.class); | |
63 | 48 |
64 private static final String TEMP_FILE_NAME = "temp_print"; | 49 private static final String TEMP_FILE_NAME = "temp_print"; |
65 private static final String TEMP_FILE_EXTENSION = ".pdf"; | 50 private static final String TEMP_FILE_EXTENSION = ".pdf"; |
66 private static final String PRINT_JOB_NAME = "foo"; | 51 private static final String PRINT_JOB_NAME = "foo"; |
67 private static final String URL = UrlUtils.encodeHtmlDataUri( | 52 private static final String URL = UrlUtils.encodeHtmlDataUri( |
68 "<html><head></head><body>foo</body></html>"); | 53 "<html><head></head><body>foo</body></html>"); |
69 private static final String PDF_PREAMBLE = "%PDF-1"; | 54 private static final String PDF_PREAMBLE = "%PDF-1"; |
70 private static final long TEST_TIMEOUT = 20000L; | 55 private static final long TEST_TIMEOUT = 20000L; |
71 | 56 |
72 @Before | 57 public PrintingControllerTest() { |
73 public void setUp() throws InterruptedException { | 58 super(ChromeActivity.class); |
| 59 } |
| 60 |
| 61 @Override |
| 62 public void startMainActivity() throws InterruptedException { |
74 // Do nothing. | 63 // Do nothing. |
75 } | 64 } |
76 | 65 |
77 private static class LayoutResultCallbackWrapperMock implements | 66 private static class LayoutResultCallbackWrapperMock implements |
78 PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper { | 67 PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper { |
79 @Override | 68 @Override |
80 public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {} | 69 public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {} |
81 | 70 |
82 @Override | 71 @Override |
83 public void onLayoutFailed(CharSequence error) {} | 72 public void onLayoutFailed(CharSequence error) {} |
(...skipping 12 matching lines...) Expand all Loading... |
96 | 85 |
97 @Override | 86 @Override |
98 public void onWriteCancelled() {} | 87 public void onWriteCancelled() {} |
99 } | 88 } |
100 | 89 |
101 /** | 90 /** |
102 * Test a basic printing flow by emulating the corresponding system calls to
the printing | 91 * Test a basic printing flow by emulating the corresponding system calls to
the printing |
103 * controller: onStart, onLayout, onWrite, onFinish. Each one is called onc
e, and in this | 92 * controller: onStart, onLayout, onWrite, onFinish. Each one is called onc
e, and in this |
104 * order, in the UI thread. | 93 * order, in the UI thread. |
105 */ | 94 */ |
106 @Test | |
107 @TargetApi(Build.VERSION_CODES.KITKAT) | 95 @TargetApi(Build.VERSION_CODES.KITKAT) |
108 @LargeTest | 96 @LargeTest |
109 @Feature({"Printing"}) | 97 @Feature({"Printing"}) |
110 public void testNormalPrintingFlow() throws Throwable { | 98 public void testNormalPrintingFlow() throws Throwable { |
111 if (!ApiCompatibilityUtils.isPrintingSupported()) return; | 99 if (!ApiCompatibilityUtils.isPrintingSupported()) return; |
112 | 100 |
113 mActivityTestRule.startMainActivityWithURL(URL); | 101 startMainActivityWithURL(URL); |
114 final Tab currentTab = mActivityTestRule.getActivity().getActivityTab(); | 102 final Tab currentTab = getActivity().getActivityTab(); |
115 | 103 |
116 final PrintingControllerImpl printingController = createControllerOnUiTh
read(); | 104 final PrintingControllerImpl printingController = createControllerOnUiTh
read(); |
117 | 105 |
118 startControllerOnUiThread(printingController, currentTab); | 106 startControllerOnUiThread(printingController, currentTab); |
119 // {@link PrintDocumentAdapter#onStart} is always called first. | 107 // {@link PrintDocumentAdapter#onStart} is always called first. |
120 callStartOnUiThread(printingController); | 108 callStartOnUiThread(printingController); |
121 | 109 |
122 // Create a temporary file to save the PDF. | 110 // Create a temporary file to save the PDF. |
123 final File cacheDir = | 111 final File cacheDir = getInstrumentation().getTargetContext().getCacheDi
r(); |
124 InstrumentationRegistry.getInstrumentation().getTargetContext().
getCacheDir(); | |
125 final File tempFile = File.createTempFile(TEMP_FILE_NAME, TEMP_FILE_EXTE
NSION, cacheDir); | 112 final File tempFile = File.createTempFile(TEMP_FILE_NAME, TEMP_FILE_EXTE
NSION, cacheDir); |
126 final ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(te
mpFile, | 113 final ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(te
mpFile, |
127 (ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_RE
AD_WRITE)); | 114 (ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_RE
AD_WRITE)); |
128 | 115 |
129 PrintAttributes attributes = new PrintAttributes.Builder() | 116 PrintAttributes attributes = new PrintAttributes.Builder() |
130 .setMediaSize(PrintAttributes.MediaSize.ISO_A4) | 117 .setMediaSize(PrintAttributes.MediaSize.ISO_A4) |
131 .setResolution(new PrintAttributes.Resolution("foo", "bar", 300,
300)) | 118 .setResolution(new PrintAttributes.Resolution("foo", "bar", 300,
300)) |
132 .setMinMargins(PrintAttributes.Margins.NO_MARGINS) | 119 .setMinMargins(PrintAttributes.Margins.NO_MARGINS) |
133 .build(); | 120 .build(); |
134 | 121 |
(...skipping 11 matching lines...) Expand all Loading... |
146 @Override | 133 @Override |
147 public void onLayoutFinished(PrintDocumentInfo info, boolean
changed) { | 134 public void onLayoutFinished(PrintDocumentInfo info, boolean
changed) { |
148 callWriteOnUiThread(printingController, fileDescriptor,
result); | 135 callWriteOnUiThread(printingController, fileDescriptor,
result); |
149 } | 136 } |
150 }); | 137 }); |
151 | 138 |
152 FileInputStream in = null; | 139 FileInputStream in = null; |
153 try { | 140 try { |
154 // This blocks until the PDF is generated. | 141 // This blocks until the PDF is generated. |
155 result.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS); | 142 result.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS); |
156 Assert.assertTrue(tempFile.length() > 0); | 143 assertTrue(tempFile.length() > 0); |
157 in = new FileInputStream(tempFile); | 144 in = new FileInputStream(tempFile); |
158 byte[] b = new byte[PDF_PREAMBLE.length()]; | 145 byte[] b = new byte[PDF_PREAMBLE.length()]; |
159 in.read(b); | 146 in.read(b); |
160 String preamble = new String(b); | 147 String preamble = new String(b); |
161 Assert.assertEquals(PDF_PREAMBLE, preamble); | 148 assertEquals(PDF_PREAMBLE, preamble); |
162 } finally { | 149 } finally { |
163 callFinishOnUiThread(printingController); | 150 callFinishOnUiThread(printingController); |
164 if (in != null) in.close(); | 151 if (in != null) in.close(); |
165 // Close the descriptor, if not closed already. | 152 // Close the descriptor, if not closed already. |
166 fileDescriptor.close(); | 153 fileDescriptor.close(); |
167 TestFileUtil.deleteFile(tempFile.getAbsolutePath()); | 154 TestFileUtil.deleteFile(tempFile.getAbsolutePath()); |
168 } | 155 } |
169 | 156 |
170 } | 157 } |
171 | 158 |
172 /** | 159 /** |
173 * Test for http://crbug.com/528909 | 160 * Test for http://crbug.com/528909 |
174 * | 161 * |
175 * @SmallTest | 162 * @SmallTest |
176 * @Feature({"Printing"}) | 163 * @Feature({"Printing"}) |
177 */ | 164 */ |
178 @Test | |
179 @CommandLineFlags.Add(ContentSwitches.DISABLE_POPUP_BLOCKING) | 165 @CommandLineFlags.Add(ContentSwitches.DISABLE_POPUP_BLOCKING) |
180 @DisabledTest(message = "crbug.com/532652") | 166 @DisabledTest(message = "crbug.com/532652") |
181 public void testPrintClosedWindow() throws Throwable { | 167 public void testPrintClosedWindow() throws Throwable { |
182 if (!ApiCompatibilityUtils.isPrintingSupported()) return; | 168 if (!ApiCompatibilityUtils.isPrintingSupported()) return; |
183 | 169 |
184 String html = "<html><head><title>printwindowclose</title></head><body><
script>" | 170 String html = "<html><head><title>printwindowclose</title></head><body><
script>" |
185 + "function printClosedWindow() {" | 171 + "function printClosedWindow() {" |
186 + " w = window.open(); w.close();" | 172 + " w = window.open(); w.close();" |
187 + " setTimeout(()=>{w.print(); document.title='completed'}, 0);
" | 173 + " setTimeout(()=>{w.print(); document.title='completed'}, 0);
" |
188 + "}</script></body></html>"; | 174 + "}</script></body></html>"; |
189 | 175 |
190 mActivityTestRule.startMainActivityWithURL("data:text/html;charset=utf-8
," + html); | 176 startMainActivityWithURL("data:text/html;charset=utf-8," + html); |
191 | 177 |
192 Tab mTab = mActivityTestRule.getActivity().getActivityTab(); | 178 Tab mTab = getActivity().getActivityTab(); |
193 Assert.assertEquals( | 179 assertEquals("title does not match initial title", "printwindowclose", m
Tab.getTitle()); |
194 "title does not match initial title", "printwindowclose", mTab.g
etTitle()); | |
195 | 180 |
196 TabTitleObserver mOnTitleUpdatedHelper = new TabTitleObserver(mTab, "com
pleted"); | 181 TabTitleObserver mOnTitleUpdatedHelper = new TabTitleObserver(mTab, "com
pleted"); |
197 mActivityTestRule.runJavaScriptCodeInCurrentTab("printClosedWindow();"); | 182 runJavaScriptCodeInCurrentTab("printClosedWindow();"); |
198 mOnTitleUpdatedHelper.waitForTitleUpdate(5); | 183 mOnTitleUpdatedHelper.waitForTitleUpdate(5); |
199 Assert.assertEquals("JS did not finish running", "completed", mTab.getTi
tle()); | 184 assertEquals("JS did not finish running", "completed", mTab.getTitle()); |
200 } | 185 } |
201 | 186 |
202 private PrintingControllerImpl createControllerOnUiThread() { | 187 private PrintingControllerImpl createControllerOnUiThread() { |
203 try { | 188 try { |
204 final FutureTask<PrintingControllerImpl> task = | 189 final FutureTask<PrintingControllerImpl> task = |
205 new FutureTask<PrintingControllerImpl>(new Callable<Printing
ControllerImpl>() { | 190 new FutureTask<PrintingControllerImpl>(new Callable<Printing
ControllerImpl>() { |
206 @Override | 191 @Override |
207 public PrintingControllerImpl call() throws Exception { | 192 public PrintingControllerImpl call() throws Exception { |
208 return (PrintingControllerImpl) PrintingControllerIm
pl.create( | 193 return (PrintingControllerImpl) PrintingControllerIm
pl.create( |
209 new PrintDocumentAdapterWrapper(), | 194 new PrintDocumentAdapterWrapper(), |
210 PRINT_JOB_NAME); | 195 PRINT_JOB_NAME); |
211 } | 196 } |
212 }); | 197 }); |
213 | 198 |
214 InstrumentationRegistry.getInstrumentation().runOnMainSync(task); | 199 runTestOnUiThread(task); |
215 PrintingControllerImpl result = task.get(TEST_TIMEOUT, TimeUnit.MILL
ISECONDS); | 200 PrintingControllerImpl result = task.get(TEST_TIMEOUT, TimeUnit.MILL
ISECONDS); |
216 return result; | 201 return result; |
217 } catch (Throwable e) { | 202 } catch (Throwable e) { |
218 Assert.fail("Error on creating PrintingControllerImpl on the UI thre
ad: " + e); | 203 fail("Error on creating PrintingControllerImpl on the UI thread: " +
e); |
219 } | 204 } |
220 return null; | 205 return null; |
221 } | 206 } |
222 | 207 |
223 private void startControllerOnUiThread(final PrintingControllerImpl controll
er, | 208 private void startControllerOnUiThread(final PrintingControllerImpl controll
er, |
224 final Tab tab) { | 209 final Tab tab) { |
225 try { | 210 try { |
226 final PrintManagerDelegate mockPrintManagerDelegate = new PrintManag
erDelegate() { | 211 final PrintManagerDelegate mockPrintManagerDelegate = new PrintManag
erDelegate() { |
227 @Override | 212 @Override |
228 public void print(String printJobName, PrintDocumentAdapter docu
mentAdapter, | 213 public void print(String printJobName, PrintDocumentAdapter docu
mentAdapter, |
229 PrintAttributes attributes) { | 214 PrintAttributes attributes) { |
230 // Do nothing, as we will emulate the framework call sequenc
e within the test. | 215 // Do nothing, as we will emulate the framework call sequenc
e within the test. |
231 } | 216 } |
232 }; | 217 }; |
233 | 218 |
234 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runna
ble() { | 219 runTestOnUiThread(new Runnable() { |
235 @Override | 220 @Override |
236 public void run() { | 221 public void run() { |
237 controller.startPrint(new TabPrinter(tab), mockPrintManagerD
elegate); | 222 controller.startPrint(new TabPrinter(tab), mockPrintManagerD
elegate); |
238 } | 223 } |
239 }); | 224 }); |
240 } catch (Throwable e) { | 225 } catch (Throwable e) { |
241 Assert.fail("Error on calling startPrint of PrintingControllerImpl "
+ e); | 226 fail("Error on calling startPrint of PrintingControllerImpl " + e); |
242 } | 227 } |
243 } | 228 } |
244 | 229 |
245 private void callStartOnUiThread(final PrintingControllerImpl controller) { | 230 private void callStartOnUiThread(final PrintingControllerImpl controller) { |
246 try { | 231 try { |
247 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runna
ble() { | 232 runTestOnUiThread(new Runnable() { |
248 @Override | 233 @Override |
249 public void run() { | 234 public void run() { |
250 controller.onStart(); | 235 controller.onStart(); |
251 } | 236 } |
252 }); | 237 }); |
253 } catch (Throwable e) { | 238 } catch (Throwable e) { |
254 Assert.fail("Error on calling onStart of PrintingControllerImpl " +
e); | 239 fail("Error on calling onStart of PrintingControllerImpl " + e); |
255 } | 240 } |
256 } | 241 } |
257 | 242 |
258 private void callLayoutOnUiThread( | 243 private void callLayoutOnUiThread( |
259 final PrintingControllerImpl controller, | 244 final PrintingControllerImpl controller, |
260 final PrintAttributes oldAttributes, | 245 final PrintAttributes oldAttributes, |
261 final PrintAttributes newAttributes, | 246 final PrintAttributes newAttributes, |
262 final PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper layout
ResultCallback) { | 247 final PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper layout
ResultCallback) { |
263 try { | 248 try { |
264 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runna
ble() { | 249 runTestOnUiThread(new Runnable() { |
265 @Override | 250 @Override |
266 public void run() { | 251 public void run() { |
267 controller.onLayout( | 252 controller.onLayout( |
268 oldAttributes, | 253 oldAttributes, |
269 newAttributes, | 254 newAttributes, |
270 new CancellationSignal(), | 255 new CancellationSignal(), |
271 layoutResultCallback, | 256 layoutResultCallback, |
272 null); | 257 null); |
273 } | 258 } |
274 }); | 259 }); |
275 } catch (Throwable e) { | 260 } catch (Throwable e) { |
276 Assert.fail("Error on calling onLayout of PrintingControllerImpl " +
e); | 261 fail("Error on calling onLayout of PrintingControllerImpl " + e); |
277 } | 262 } |
278 } | 263 } |
279 | 264 |
280 @TargetApi(Build.VERSION_CODES.KITKAT) | 265 @TargetApi(Build.VERSION_CODES.KITKAT) |
281 private void callWriteOnUiThread( | 266 private void callWriteOnUiThread( |
282 final PrintingControllerImpl controller, | 267 final PrintingControllerImpl controller, |
283 final ParcelFileDescriptor descriptor, | 268 final ParcelFileDescriptor descriptor, |
284 final FutureTask<Boolean> result) { | 269 final FutureTask<Boolean> result) { |
285 try { | 270 try { |
286 controller.onWrite( | 271 controller.onWrite( |
287 new PageRange[] {PageRange.ALL_PAGES}, | 272 new PageRange[] {PageRange.ALL_PAGES}, |
288 descriptor, | 273 descriptor, |
289 new CancellationSignal(), | 274 new CancellationSignal(), |
290 new WriteResultCallbackWrapperMock() { | 275 new WriteResultCallbackWrapperMock() { |
291 @Override | 276 @Override |
292 public void onWriteFinished(PageRange[] pages) { | 277 public void onWriteFinished(PageRange[] pages) { |
293 try { | 278 try { |
294 descriptor.close(); | 279 descriptor.close(); |
295 // Result is ready, signal to continue. | 280 // Result is ready, signal to continue. |
296 result.run(); | 281 result.run(); |
297 } catch (IOException ex) { | 282 } catch (IOException ex) { |
298 Assert.fail("Failed file operation: " + ex.toStr
ing()); | 283 fail("Failed file operation: " + ex.toString()); |
299 } | 284 } |
300 } | 285 } |
301 } | 286 } |
302 ); | 287 ); |
303 } catch (Throwable e) { | 288 } catch (Throwable e) { |
304 Assert.fail("Error on calling onWriteInternal of PrintingControllerI
mpl " + e); | 289 fail("Error on calling onWriteInternal of PrintingControllerImpl " +
e); |
305 } | 290 } |
306 } | 291 } |
307 | 292 |
308 private void callFinishOnUiThread(final PrintingControllerImpl controller) { | 293 private void callFinishOnUiThread(final PrintingControllerImpl controller) { |
309 try { | 294 try { |
310 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runna
ble() { | 295 runTestOnUiThread(new Runnable() { |
311 @Override | 296 @Override |
312 public void run() { | 297 public void run() { |
313 controller.onFinish(); | 298 controller.onFinish(); |
314 } | 299 } |
315 }); | 300 }); |
316 } catch (Throwable e) { | 301 } catch (Throwable e) { |
317 Assert.fail("Error on calling onFinish of PrintingControllerImpl " +
e); | 302 fail("Error on calling onFinish of PrintingControllerImpl " + e); |
318 } | 303 } |
319 } | 304 } |
320 } | 305 } |
OLD | NEW |