1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.android_webview.test.devui;
6 
7 import static androidx.test.espresso.Espresso.onData;
8 import static androidx.test.espresso.Espresso.onView;
9 import static androidx.test.espresso.action.ViewActions.click;
10 import static androidx.test.espresso.action.ViewActions.longClick;
11 import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
12 import static androidx.test.espresso.assertion.ViewAssertions.matches;
13 import static androidx.test.espresso.intent.Intents.assertNoUnverifiedIntents;
14 import static androidx.test.espresso.intent.Intents.intended;
15 import static androidx.test.espresso.intent.Intents.intending;
16 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
17 import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
18 import static androidx.test.espresso.matcher.ViewMatchers.withId;
19 import static androidx.test.espresso.matcher.ViewMatchers.withText;
20 
21 import static org.hamcrest.MatcherAssert.assertThat;
22 import static org.hamcrest.Matchers.allOf;
23 import static org.hamcrest.Matchers.anything;
24 import static org.hamcrest.Matchers.is;
25 import static org.hamcrest.Matchers.not;
26 
27 import static org.chromium.android_webview.test.common.crash.CrashInfoTest.createCrashInfo;
28 import static org.chromium.android_webview.test.devui.DeveloperUiTestUtils.getClipBoardTextOnUiThread;
29 import static org.chromium.android_webview.test.devui.DeveloperUiTestUtils.setClipBoardTextOnUiThread;
30 import static org.chromium.android_webview.test.devui.DeveloperUiTestUtils.withCount;
31 
32 import android.app.Activity;
33 import android.app.Instrumentation.ActivityResult;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.pm.ApplicationInfo;
37 import android.content.pm.PackageManager;
38 import android.content.pm.ResolveInfo;
39 import android.content.res.Resources;
40 import android.graphics.Bitmap;
41 import android.graphics.Canvas;
42 import android.graphics.drawable.Drawable;
43 import android.support.test.InstrumentationRegistry;
44 import android.view.View;
45 import android.widget.ImageView;
46 
47 import androidx.annotation.IdRes;
48 import androidx.test.espresso.DataInteraction;
49 import androidx.test.espresso.intent.Intents;
50 import androidx.test.espresso.intent.matcher.IntentMatchers;
51 import androidx.test.espresso.intent.rule.IntentsTestRule;
52 import androidx.test.filters.LargeTest;
53 import androidx.test.filters.MediumTest;
54 
55 import org.hamcrest.Description;
56 import org.hamcrest.Matcher;
57 import org.hamcrest.TypeSafeMatcher;
58 import org.junit.After;
59 import org.junit.Assume;
60 import org.junit.Rule;
61 import org.junit.Test;
62 import org.junit.runner.RunWith;
63 
64 import org.chromium.android_webview.common.PlatformServiceBridge;
65 import org.chromium.android_webview.common.crash.CrashInfo;
66 import org.chromium.android_webview.common.crash.CrashInfo.UploadState;
67 import org.chromium.android_webview.common.crash.CrashUploadUtil;
68 import org.chromium.android_webview.common.crash.CrashUploadUtil.CrashUploadDelegate;
69 import org.chromium.android_webview.common.crash.SystemWideCrashDirectories;
70 import org.chromium.android_webview.devui.CrashesListFragment;
71 import org.chromium.android_webview.devui.MainActivity;
72 import org.chromium.android_webview.devui.R;
73 import org.chromium.android_webview.devui.WebViewPackageError;
74 import org.chromium.android_webview.devui.util.CrashBugUrlFactory;
75 import org.chromium.android_webview.devui.util.WebViewPackageHelper;
76 import org.chromium.android_webview.test.AwJUnit4ClassRunner;
77 import org.chromium.base.Callback;
78 import org.chromium.base.FileUtils;
79 import org.chromium.base.test.util.Batch;
80 import org.chromium.base.test.util.CallbackHelper;
81 import org.chromium.base.test.util.Feature;
82 import org.chromium.components.minidump_uploader.CrashFileManager;
83 import org.chromium.content_public.browser.test.util.TestThreadUtils;
84 
85 import java.io.File;
86 import java.io.FileWriter;
87 import java.io.IOException;
88 import java.util.Date;
89 import java.util.List;
90 import java.util.Locale;
91 import java.util.concurrent.ExecutionException;
92 import java.util.concurrent.TimeUnit;
93 
94 /**
95  * UI tests for {@link CrashesListFragment}.
96  */
97 @LargeTest
98 @RunWith(AwJUnit4ClassRunner.class)
99 @Batch(Batch.PER_CLASS)
100 public class CrashesListFragmentTest {
101     private static final String FAKE_APP_PACKAGE_NAME = "com.test.some_package";
102     private static final String CRASH_REPORT_BUTTON_TEXT = "File bug report";
103     private static final String CRASH_UPLOAD_BUTTON_TEXT = "Upload this crash report";
104 
105     @Rule
106     public IntentsTestRule mRule =
107             new IntentsTestRule<MainActivity>(MainActivity.class, false, false);
108 
109     @After
tearDown()110     public void tearDown() {
111         FileUtils.recursivelyDeleteFile(SystemWideCrashDirectories.getWebViewCrashDir(), null);
112         FileUtils.recursivelyDeleteFile(SystemWideCrashDirectories.getWebViewCrashLogDir(), null);
113         // Activity is launched, i.e the test is not skipped.
114         if (mRule.getActivity() != null) {
115             // Tests are responsible for verifying every Intent they trigger.
116             assertNoUnverifiedIntents();
117         }
118     }
119 
launchCrashesFragment()120     private void launchCrashesFragment() {
121         Intent intent = new Intent();
122         intent.putExtra(MainActivity.FRAGMENT_ID_INTENT_EXTRA, MainActivity.FRAGMENT_ID_CRASHES);
123         mRule.launchActivity(intent);
124         onView(withId(R.id.fragment_crashes_list)).check(matches(isDisplayed()));
125 
126         // Stub all external intents, to avoid launching other apps (ex. system browser), has to be
127         // done after launching the activity.
128         intending(not(IntentMatchers.isInternal()))
129                 .respondWith(new ActivityResult(Activity.RESULT_OK, null));
130     }
131 
createMinidumpFile(CrashInfo crashInfo)132     private static File createMinidumpFile(CrashInfo crashInfo) throws IOException {
133         CrashFileManager crashFileManager =
134                 new CrashFileManager(SystemWideCrashDirectories.getOrCreateWebViewCrashDir());
135         File dir = crashFileManager.getCrashDirectory();
136         dir.mkdirs();
137         String suffix;
138         switch (crashInfo.uploadState) {
139             case UPLOADED:
140                 suffix = ".up";
141                 break;
142             case SKIPPED:
143                 suffix = ".skipped";
144                 break;
145             case PENDING_USER_REQUESTED:
146                 suffix = ".forced";
147                 break;
148             default:
149                 suffix = ".dmp";
150         }
151         return File.createTempFile(
152                 "test_minidump", "-" + crashInfo.localId + suffix + ".try0", dir);
153     }
154 
appendUploadedEntryToLog(CrashInfo crashInfo)155     private static File appendUploadedEntryToLog(CrashInfo crashInfo) throws IOException {
156         CrashFileManager crashFileManager =
157                 new CrashFileManager(SystemWideCrashDirectories.getOrCreateWebViewCrashDir());
158         File logFile = crashFileManager.getCrashUploadLogFile();
159         logFile.getParentFile().mkdirs();
160         FileWriter writer = new FileWriter(logFile, /* append= */ true);
161         StringBuilder sb = new StringBuilder();
162         sb.append(TimeUnit.MILLISECONDS.toSeconds(crashInfo.uploadTime));
163         sb.append(",");
164         sb.append(crashInfo.uploadId);
165         sb.append(",");
166         sb.append(crashInfo.localId);
167         sb.append('\n');
168         try {
169             writer.write(sb.toString());
170         } finally {
171             writer.close();
172         }
173 
174         return logFile;
175     }
176 
writeJsonLogFile(CrashInfo crashInfo)177     private static File writeJsonLogFile(CrashInfo crashInfo) throws IOException {
178         File dir = SystemWideCrashDirectories.getOrCreateWebViewCrashLogDir();
179         File jsonFile = File.createTempFile("test_minidump-", crashInfo.localId + ".json", dir);
180         FileWriter writer = new FileWriter(jsonFile);
181         writer.write(crashInfo.serializeToJson());
182         writer.close();
183         return jsonFile;
184     }
185 
getCrashListLoadedListener()186     private CallbackHelper getCrashListLoadedListener() throws ExecutionException {
187         return TestThreadUtils.runOnUiThreadBlocking(() -> {
188             final CallbackHelper helper = new CallbackHelper();
189             CrashesListFragment.setCrashInfoLoadedListenerForTesting(
190                     () -> { helper.notifyCalled(); });
191             return helper;
192         });
193     }
194 
195     /**
196      * Matches that a {@link ImageView} has the given {@link Drawable}.
197      */
withDrawable(Drawable expectedDrawable)198     private static Matcher<View> withDrawable(Drawable expectedDrawable) {
199         return new TypeSafeMatcher<View>() {
200             @Override
201             public boolean matchesSafely(View view) {
202                 if (!(view instanceof ImageView)) {
203                     return false;
204                 }
205                 return drawableEquals(((ImageView) view).getDrawable(), expectedDrawable);
206             }
207 
208             @Override
209             public void describeTo(Description description) {
210                 description.appendText("with Drawable");
211             }
212         };
213     }
214 
215     /**
216      * Matches that a {@link ImageView} has the given {@link Drawable}.
217      *
218      * @param expectedId the id resource for the given drawable
219      */
220     private static Matcher<View> withDrawable(@IdRes int expectedId) {
221         return new TypeSafeMatcher<View>() {
222             private Resources mResources =
223                     InstrumentationRegistry.getTargetContext().getResources();
224 
225             @Override
226             public boolean matchesSafely(View view) {
227                 Drawable expectedDrawable = mResources.getDrawable(expectedId);
228                 return withDrawable(expectedDrawable).matches(view);
229             }
230 
231             @Override
232             public void describeTo(Description description) {
233                 try {
234                     description.appendText("with Drawable Id: ")
235                             .appendText(mResources.getResourceName(expectedId));
236                 } catch (Resources.NotFoundException e) {
237                     description.appendText("with Drawable Id (resource name not found): ")
238                             .appendText(Integer.toString(expectedId));
239                 }
240             }
241         };
242     }
243 
244     private static boolean drawableEquals(Drawable actualDrawable, Drawable expectedDrawable) {
245         if (actualDrawable == null || expectedDrawable == null) {
246             return false;
247         }
248         Bitmap actualBitmap = getBitmap(actualDrawable);
249         Bitmap expectedBitmap = getBitmap(expectedDrawable);
250         return actualBitmap.sameAs(expectedBitmap);
251     }
252 
253     // Convert a drawable to a Bitmap for comparison.
254     private static Bitmap getBitmap(Drawable drawable) {
255         Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
256                 drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
257         Canvas canvas = new Canvas(bitmap);
258         drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
259         drawable.draw(canvas);
260         return bitmap;
261     }
262 
263     /**
264      * Check that the given crash item header shows the correct package name, capture date and icon
265      * for the given {@code crashInfo}.
266      *
267      * @param {@link DataInteraction} represents the crash item header.
268      * @param {@link CrashInfo} to match.
269      * @return the same {@code headerDataInteraction} passed for the convenience of chaining.
270      */
271     private static DataInteraction checkUnknownPackageCrashItemHeader(
272             DataInteraction headerDataInteraction, CrashInfo crashInfo) {
273         return checkPackageCrashItemHeader(headerDataInteraction, crashInfo, FAKE_APP_PACKAGE_NAME);
274     }
275 
276     /**
277      * Check that the given crash item header shows the "unknown app" package name, capture date and
278      * icon for the given {@code crashInfo}.
279      *
280      * @param {@link DataInteraction} represents the crash item header.
281      * @param {@link CrashInfo} to match.
282      * @return the same {@code headerDataInteraction} passed for the convenience of chaining.
283      */
284     private static DataInteraction checkMissingPackageInfoCrashItemHeader(
285             DataInteraction headerDataInteraction, CrashInfo crashInfo) {
286         return checkPackageCrashItemHeader(headerDataInteraction, crashInfo, "unknown app");
287     }
288 
289     /**
290      * Check that the given crash item header shows the given package name, capture date and
291      * icon for the given {@code crashInfo}.
292      *
293      * @param {@link DataInteraction} represents the crash item header.
294      * @param {@link CrashInfo} to match.
295      * @param packageName to match.
296      * @return the same {@code headerDataInteraction} passed for the convenience of chaining.
297      */
298     private static DataInteraction checkPackageCrashItemHeader(
299             DataInteraction headerDataInteraction, CrashInfo crashInfo, String packageName) {
300         String captureDate = new Date(crashInfo.captureTime).toString();
301         headerDataInteraction.onChildView(withId(android.R.id.text1))
302                 .check(matches(withText(packageName)));
303         headerDataInteraction.onChildView(withId(android.R.id.text2))
304                 .check(matches(withText(captureDate)));
305         // There should not be an app with FAKE_APP_PACKAGE_NAME so system default icon should be
306         // shown.
307         headerDataInteraction.onChildView(withId(R.id.crash_package_icon))
308                 .check(matches(withDrawable(android.R.drawable.sym_def_app_icon)));
309 
310         return headerDataInteraction;
311     }
312 
313     /**
314      * Perform click on hide crash button by checking the required conditions for the button.
315      *
316      * @param {@link DataInteraction} represents the crash item body.
317      */
318     private static void clickHideCrashButton(DataInteraction bodyDataInteraction) {
319         bodyDataInteraction.onChildView(withId(R.id.crash_hide_button))
320                 .check(matches(isDisplayed()))
321                 .check(matches(isEnabled()))
322                 .check(matches(withDrawable(R.drawable.ic_delete)))
323                 .perform(click());
324     }
325 
326     /**
327      * Check that the given crash item body shows the correct uploadState, uploadId and uploadDate.
328      *
329      * @param {@link DataInteraction} represents the crash item body.
330      * @param {@link CrashInfo} to match its upload status.
331      * @return the same {@code headerDataInteraction} passed for the convenience of chaining.
332      */
333     private static DataInteraction checkCrashItemUploadStatus(
334             DataInteraction bodyDataInteraction, CrashInfo crashInfo) {
335         DataInteraction uploadStatusDataInteraction =
336                 bodyDataInteraction.onChildView(withId(R.id.upload_status));
337         String uploadState = CrashesListFragment.uploadStateString(crashInfo.uploadState);
338         uploadStatusDataInteraction.onChildView(withId(android.R.id.text1))
339                 .check(matches(withText(uploadState)));
340         String uploadInfo = crashInfo.uploadState == UploadState.UPLOADED
341                 ? new Date(crashInfo.uploadTime).toString() + "\nID: " + crashInfo.uploadId
342                 : "";
343         uploadStatusDataInteraction.onChildView(withId(android.R.id.text2))
344                 .check(matches(withText(uploadInfo)));
345 
346         return bodyDataInteraction;
347     }
348 
349     private static class TestPlatformServiceBridge extends PlatformServiceBridge {
350         private boolean mCanUseGms;
351         private boolean mUserConsent;
352 
353         TestPlatformServiceBridge(boolean canUseGms, boolean userConsent) {
354             mCanUseGms = canUseGms;
355             mUserConsent = userConsent;
356         }
357 
358         @Override
359         public boolean canUseGms() {
360             return mCanUseGms;
361         }
362 
363         @Override
364         public void queryMetricsSetting(Callback<Boolean> callback) {
365             callback.onResult(mUserConsent);
366         }
367     }
368 
369     @Test
370     @Feature({"AndroidWebView"})
371     public void testShowingSingleCrashReport_uploaded() throws Throwable {
372         final long systemTime = System.currentTimeMillis();
373         CrashInfo crashInfo = createCrashInfo("123456", systemTime, "0abcde123456",
374                 systemTime + 1000, FAKE_APP_PACKAGE_NAME, UploadState.UPLOADED);
375 
376         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
377         assertThat("upload log file should exist", appendUploadedEntryToLog(crashInfo).exists());
378 
379         CallbackHelper helper = getCrashListLoadedListener();
380         int crashListLoadInitCount = helper.getCallCount();
381         launchCrashesFragment();
382         helper.waitForCallback(crashListLoadInitCount, 1);
383 
384         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
385 
386         // Check crash item header
387         checkUnknownPackageCrashItemHeader(onData(anything()).atPosition(0), crashInfo)
388                 .perform(click()); // click to expand it
389         // The body is considered item#2 in the list view after expansion
390         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
391         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
392         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
393 
394         bodyDataInteraction.onChildView(withId(R.id.crash_report_button))
395                 .check(matches(isDisplayed()))
396                 .check(matches(isEnabled()))
397                 .check(matches(withText(CRASH_REPORT_BUTTON_TEXT)));
398         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button))
399                 .check(matches(not(isDisplayed())));
400         bodyDataInteraction.onChildView(withId(R.id.crash_hide_button))
401                 .check(matches(isDisplayed()))
402                 .check(matches(isEnabled()))
403                 .check(matches(withDrawable(R.drawable.ic_delete)));
404     }
405 
406     @Test
407     @Feature({"AndroidWebView"})
408     public void testOpenBugReportCrash() throws Throwable {
409         final long systemTime = System.currentTimeMillis();
410         CrashInfo crashInfo = createCrashInfo("123456", systemTime, "0abcde123456",
411                 systemTime + 1000, FAKE_APP_PACKAGE_NAME, UploadState.UPLOADED);
412 
413         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
414         assertThat("upload log file should exist", appendUploadedEntryToLog(crashInfo).exists());
415 
416         CallbackHelper helper = getCrashListLoadedListener();
417         int crashListLoadInitCount = helper.getCallCount();
418         launchCrashesFragment();
419         helper.waitForCallback(crashListLoadInitCount, 1);
420 
421         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
422         // Click the header to expand the list item.
423         onData(anything()).atPosition(0).perform(click());
424         // The body is considered item#2 in the list view after expansion.
425         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
426         onData(anything())
427                 .atPosition(1)
428                 .onChildView(withId(R.id.crash_report_button))
429                 .perform(click());
430         onView(withText(CrashesListFragment.CRASH_BUG_DIALOG_MESSAGE))
431                 .check(matches(isDisplayed()));
432         // button2 is the AlertDialog negative button id.
433         onView(withId(android.R.id.button2)).check(matches(withText("Dismiss"))).perform(click());
434         onView(withText(CrashesListFragment.CRASH_BUG_DIALOG_MESSAGE)).check(doesNotExist());
435         // Verify that no intents are sent out.
436         Intents.times(0);
437 
438         onData(anything())
439                 .atPosition(1)
440                 .onChildView(withId(R.id.crash_report_button))
441                 .perform(click());
442         // button1 is the AlertDialog positive button id.
443         onView(withId(android.R.id.button1))
444                 .check(matches(withText("Provide more info")))
445                 .perform(click());
446         onView(withText(CrashesListFragment.CRASH_BUG_DIALOG_MESSAGE)).check(doesNotExist());
447         Intent expectedIntent = new CrashBugUrlFactory(crashInfo).getReportIntent();
448         // TODO(hazems): use IntentMatchers.filterEquals() after pulling the new version of
449         // espresso-intents
450         intended(allOf(IntentMatchers.hasAction(expectedIntent.getAction()),
451                 IntentMatchers.hasData(expectedIntent.getData())));
452     }
453 
454     @Test
455     @LargeTest
456     @Feature({"AndroidWebView"})
457     public void testShowingSingleCrashReport_pending() throws Throwable {
458         final long systemTime = System.currentTimeMillis();
459         CrashInfo crashInfo = createCrashInfo(
460                 "123456", systemTime, null, -1, FAKE_APP_PACKAGE_NAME, UploadState.PENDING);
461 
462         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
463         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
464 
465         CallbackHelper helper = getCrashListLoadedListener();
466         int crashListLoadInitCount = helper.getCallCount();
467         launchCrashesFragment();
468         helper.waitForCallback(crashListLoadInitCount, 1);
469 
470         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
471 
472         // Check crash item header
473         checkUnknownPackageCrashItemHeader(onData(anything()).atPosition(0), crashInfo)
474                 .perform(click()); // click to expand it
475         // The body is considered item#2 in the list view after expansion
476         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
477         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
478         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
479 
480         bodyDataInteraction.onChildView(withId(R.id.crash_report_button))
481                 .check(matches(isDisplayed()))
482                 .check(matches(withText(CRASH_REPORT_BUTTON_TEXT)))
483                 .check(matches(not(isEnabled())));
484         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button))
485                 .check(matches(isDisplayed()))
486                 .check(matches(withText(CRASH_UPLOAD_BUTTON_TEXT)))
487                 .check(matches(isEnabled()));
488         bodyDataInteraction.onChildView(withId(R.id.crash_hide_button))
489                 .check(matches(isDisplayed()))
490                 .check(matches(isEnabled()))
491                 .check(matches(withDrawable(R.drawable.ic_delete)));
492     }
493 
494     @Test
495     @Feature({"AndroidWebView"})
496     public void testShowingSingleCrashReport_pendingUserRequest() throws Throwable {
497         final long systemTime = System.currentTimeMillis();
498         CrashInfo crashInfo = createCrashInfo("123456", systemTime, null, -1, FAKE_APP_PACKAGE_NAME,
499                 UploadState.PENDING_USER_REQUESTED);
500 
501         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
502         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
503 
504         CallbackHelper helper = getCrashListLoadedListener();
505         int crashListLoadInitCount = helper.getCallCount();
506         launchCrashesFragment();
507         helper.waitForCallback(crashListLoadInitCount, 1);
508 
509         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
510 
511         // Check crash item header
512         checkUnknownPackageCrashItemHeader(onData(anything()).atPosition(0), crashInfo)
513                 .perform(click()); // click to expand it
514         // The body is considered item#2 in the list view after expansion
515         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
516         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
517         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
518 
519         bodyDataInteraction.onChildView(withId(R.id.crash_report_button))
520                 .check(matches(isDisplayed()))
521                 .check(matches(withText(CRASH_REPORT_BUTTON_TEXT)))
522                 .check(matches(not(isEnabled())))
523                 .perform(click());
524         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button))
525                 .check(matches(not(isDisplayed())));
526         bodyDataInteraction.onChildView(withId(R.id.crash_hide_button))
527                 .check(matches(isDisplayed()))
528                 .check(matches(isEnabled()))
529                 .check(matches(withDrawable(R.drawable.ic_delete)));
530     }
531 
532     @Test
533     @Feature({"AndroidWebView"})
534     public void testShowingSingleCrashReport_skipped() throws Throwable {
535         final long systemTime = System.currentTimeMillis();
536         CrashInfo crashInfo = createCrashInfo(
537                 "123456", systemTime, null, -1, FAKE_APP_PACKAGE_NAME, UploadState.SKIPPED);
538 
539         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
540         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
541 
542         CallbackHelper helper = getCrashListLoadedListener();
543         int crashListLoadInitCount = helper.getCallCount();
544         launchCrashesFragment();
545         helper.waitForCallback(crashListLoadInitCount, 1);
546 
547         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
548 
549         // Check crash item header
550         checkUnknownPackageCrashItemHeader(onData(anything()).atPosition(0), crashInfo)
551                 .perform(click()); // click to expand it
552         // The body is considered item#2 in the list view after expansion
553         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
554         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
555         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
556 
557         bodyDataInteraction.onChildView(withId(R.id.crash_report_button))
558                 .check(matches(isDisplayed()))
559                 .check(matches(withText(CRASH_REPORT_BUTTON_TEXT)))
560                 .check(matches(not(isEnabled())));
561         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button))
562                 .check(matches(isDisplayed()))
563                 .check(matches(withText(CRASH_UPLOAD_BUTTON_TEXT)))
564                 .check(matches(isEnabled()));
565         bodyDataInteraction.onChildView(withId(R.id.crash_hide_button))
566                 .check(matches(isDisplayed()))
567                 .check(matches(isEnabled()))
568                 .check(matches(withDrawable(R.drawable.ic_delete)));
569     }
570 
571     @Test
572     @Feature({"AndroidWebView"})
573     public void testForceUploadSkippedCrashReport_noWifi() throws Throwable {
574         final long systemTime = System.currentTimeMillis();
575         CrashInfo crashInfo = createCrashInfo(
576                 "123456", systemTime, null, -1, FAKE_APP_PACKAGE_NAME, UploadState.SKIPPED);
577 
578         File minidumpFile = createMinidumpFile(crashInfo);
579         assertThat("temp minidump file should exist", minidumpFile.exists());
580         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
581 
582         CrashUploadUtil.setCrashUploadDelegateForTesting(new CrashUploadDelegate() {
583             @Override
584             public void scheduleNewJob(Context context) {}
585 
586             @Override
587             public boolean isNetworkUnmetered(Context context) {
588                 return false;
589             }
590         });
591 
592         CallbackHelper helper = getCrashListLoadedListener();
593         int crashListLoadInitCount = helper.getCallCount();
594         launchCrashesFragment();
595         helper.waitForCallback(crashListLoadInitCount, 1);
596 
597         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
598 
599         // click on the crash item header to expand
600         onData(anything()).atPosition(0).perform(click());
601         // The body is considered item#2 in the list view after expansion
602         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
603         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
604         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
605 
606         // Firstly test clicking the upload button, and dismissing the dialog
607         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button)).perform(click());
608         onView(withText(CrashesListFragment.NO_WIFI_DIALOG_MESSAGE)).check(matches(isDisplayed()));
609         // button2 is the AlertDialog negative button id.
610         onView(withId(android.R.id.button2)).check(matches(withText("Cancel"))).perform(click());
611         // Check no changes in the view after dismissing the dialog
612         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
613         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button))
614                 .check(matches(isDisplayed()));
615 
616         // Secondly test clicking the upload button, and proceeding with upload.
617         crashListLoadInitCount = helper.getCallCount();
618         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button)).perform(click());
619         onView(withText(CrashesListFragment.NO_WIFI_DIALOG_MESSAGE)).check(matches(isDisplayed()));
620         // button1 is the AlertDialog positive button id.
621         onView(withId(android.R.id.button1)).check(matches(withText("Upload"))).perform(click());
622         helper.waitForCallback(crashListLoadInitCount, 1);
623         // upload button is now hidden
624         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button))
625                 .check(matches(not(isDisplayed())));
626         crashInfo.uploadState = UploadState.PENDING_USER_REQUESTED;
627         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
628 
629         // Check that minidump file suffix is changed to ".forced"
630         File renamedMinidumpFile =
631                 new File(minidumpFile.getAbsolutePath().replace("skipped", "forced"));
632         assertThat("skipped minidump file shouldn't exist", not(minidumpFile.exists()));
633         assertThat("renamed forced minidump file should exist", renamedMinidumpFile.exists());
634     }
635 
636     @Test
637     @LargeTest
638     @Feature({"AndroidWebView"})
639     public void testForceUploadSkippedCrashReport_withWifi() throws Throwable {
640         final long systemTime = System.currentTimeMillis();
641         CrashInfo crashInfo = createCrashInfo(
642                 "123456", systemTime, null, -1, FAKE_APP_PACKAGE_NAME, UploadState.SKIPPED);
643 
644         File minidumpFile = createMinidumpFile(crashInfo);
645         assertThat("temp minidump file should exist", minidumpFile.exists());
646         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
647 
648         CrashUploadUtil.setCrashUploadDelegateForTesting(new CrashUploadDelegate() {
649             @Override
650             public void scheduleNewJob(Context context) {}
651 
652             @Override
653             public boolean isNetworkUnmetered(Context context) {
654                 return true;
655             }
656         });
657 
658         CallbackHelper helper = getCrashListLoadedListener();
659         int crashListLoadInitCount = helper.getCallCount();
660         launchCrashesFragment();
661         helper.waitForCallback(crashListLoadInitCount, 1);
662 
663         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
664 
665         // click on the crash item header to expand
666         onData(anything()).atPosition(0).perform(click());
667         // The body is considered item#2 in the list view after expansion
668         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
669         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
670         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
671 
672         crashListLoadInitCount = helper.getCallCount();
673         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button)).perform(click());
674         helper.waitForCallback(crashListLoadInitCount, 1);
675         // upload button is now hidden
676         bodyDataInteraction.onChildView(withId(R.id.crash_upload_button))
677                 .check(matches(not(isDisplayed())));
678         crashInfo.uploadState = UploadState.PENDING_USER_REQUESTED;
679         checkCrashItemUploadStatus(bodyDataInteraction, crashInfo);
680 
681         // Check that minidump file suffix is changed to ".forced"
682         File renamedMinidumpFile =
683                 new File(minidumpFile.getAbsolutePath().replace("skipped", "forced"));
684         assertThat("skipped minidump file shouldn't exist", not(minidumpFile.exists()));
685         assertThat("renamed forced minidump file should exist", renamedMinidumpFile.exists());
686     }
687 
688     @Test
689     @LargeTest
690     @Feature({"AndroidWebView"})
691     // Test when a crash has a known package name that can be found using PackageManager
692     public void testInstalledPackageInfo() throws Throwable {
693         Context context = InstrumentationRegistry.getTargetContext();
694         PackageManager packageManager = context.getPackageManager();
695         // Use the system settings package as a fake app where a crash happened because it's more
696         // likely to be available on every device. If it's not found, skip the test.
697         final String appPackageName = "com.android.settings";
698         ApplicationInfo appInfo;
699         try {
700             appInfo = packageManager.getApplicationInfo(appPackageName, 0);
701         } catch (PackageManager.NameNotFoundException e) {
702             appInfo = null;
703         }
704         Assume.assumeNotNull(
705                 "This test assumes \"com.android.settings\" package is available", appInfo);
706 
707         final long systemTime = System.currentTimeMillis();
708         CrashInfo crashInfo = createCrashInfo(
709                 "123456", systemTime, null, -1, appPackageName, UploadState.PENDING);
710 
711         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
712         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
713 
714         CallbackHelper helper = getCrashListLoadedListener();
715         int crashListLoadInitCount = helper.getCallCount();
716         launchCrashesFragment();
717         helper.waitForCallback(crashListLoadInitCount, 1);
718 
719         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
720 
721         DataInteraction headerDataInteraction = onData(anything()).atPosition(0);
722         headerDataInteraction.onChildView(withId(android.R.id.text1))
723                 .check(matches(withText(appPackageName)));
724         headerDataInteraction.onChildView(withId(R.id.crash_package_icon))
725                 .check(matches(withDrawable(packageManager.getApplicationIcon(appInfo))));
726     }
727 
728     @Test
729     @Feature({"AndroidWebView"})
730     // Test when app package name field is missing in the crash info.
731     public void testMissingPackageInfo() throws Throwable {
732         final long systemTime = System.currentTimeMillis();
733         CrashInfo crashInfo =
734                 createCrashInfo("123456", systemTime, null, -1, null, UploadState.PENDING);
735 
736         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
737         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
738 
739         CallbackHelper helper = getCrashListLoadedListener();
740         int crashListLoadInitCount = helper.getCallCount();
741         launchCrashesFragment();
742         helper.waitForCallback(crashListLoadInitCount, 1);
743 
744         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
745 
746         checkMissingPackageInfoCrashItemHeader(onData(anything()).atPosition(0), crashInfo);
747     }
748 
749     @Test
750     @Feature({"AndroidWebView"})
751     // Test when crash is missing json, but has upload log file and minidump.
752     public void testShowingSingleCrashReport_uploaded_missingJson() throws Throwable {
753         CrashInfo crashInfo = createCrashInfo("123456", -1, null, 1000, null, UploadState.UPLOADED);
754 
755         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
756         assertThat("upload log file should exist", appendUploadedEntryToLog(crashInfo).exists());
757 
758         CallbackHelper helper = getCrashListLoadedListener();
759         int crashListLoadInitCount = helper.getCallCount();
760         launchCrashesFragment();
761         helper.waitForCallback(crashListLoadInitCount, 1);
762 
763         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
764 
765         checkMissingPackageInfoCrashItemHeader(onData(anything()).atPosition(0), crashInfo);
766     }
767 
768     @Test
769     @Feature({"AndroidWebView"})
770     // Test when crash is missing json, but has upload log file and minidump.
771     public void testShowingSingleCrashReport_pending_missingJson() throws Throwable {
772         CrashInfo crashInfo = createCrashInfo("123456", -1, null, 1000, null, UploadState.PENDING);
773 
774         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
775 
776         CallbackHelper helper = getCrashListLoadedListener();
777         int crashListLoadInitCount = helper.getCallCount();
778         launchCrashesFragment();
779         helper.waitForCallback(crashListLoadInitCount, 1);
780 
781         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
782 
783         checkMissingPackageInfoCrashItemHeader(onData(anything()).atPosition(0), crashInfo);
784     }
785 
786     @Test
787     @Feature({"AndroidWebView"})
788     public void testMaxNumberOfCrashes() throws Throwable {
789         final long systemTime = System.currentTimeMillis();
790         final int crashReportsNum = CrashesListFragment.MAX_CRASHES_NUMBER + 10;
791         CrashInfo[] crashInfo = new CrashInfo[crashReportsNum];
792         for (int i = 0; i < crashReportsNum; ++i) {
793             // Set capture time with an arbitrary chosen 2 second difference to make sure crashes
794             // are shown in descending order with most recent crash first.
795             crashInfo[i] = createCrashInfo("abcd" + Integer.toString(i), systemTime + i * 2000,
796                     null, -1, FAKE_APP_PACKAGE_NAME, UploadState.PENDING);
797 
798             assertThat(
799                     "temp minidump file should exist", createMinidumpFile(crashInfo[i]).exists());
800             assertThat("temp json log file should exist", writeJsonLogFile(crashInfo[i]).exists());
801         }
802 
803         CallbackHelper helper = getCrashListLoadedListener();
804         int crashListLoadInitCount = helper.getCallCount();
805         launchCrashesFragment();
806         helper.waitForCallback(crashListLoadInitCount, 1);
807 
808         onView(withId(R.id.crashes_list))
809                 .check(matches(withCount(CrashesListFragment.MAX_CRASHES_NUMBER)));
810         // Check that only the most recent MAX_CRASHES_NUMBER crashes are shown.
811         for (int i = 0; i < CrashesListFragment.MAX_CRASHES_NUMBER; ++i) {
812             // Crashes should be shown with the most recent first, i.e the reverse of the order
813             // they are initialized at.
814             checkUnknownPackageCrashItemHeader(
815                     onData(anything()).atPosition(i), crashInfo[crashReportsNum - i - 1]);
816         }
817     }
818 
819     @Test
820     @Feature({"AndroidWebView"})
821     public void testHideCrashButton_uploaded() throws Throwable {
822         final long systemTime = System.currentTimeMillis();
823         CrashInfo crashInfo = createCrashInfo("123456", systemTime, "0abcde123456",
824                 systemTime + 1000, FAKE_APP_PACKAGE_NAME, UploadState.UPLOADED);
825 
826         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
827         assertThat("upload log file should exist", appendUploadedEntryToLog(crashInfo).exists());
828 
829         CallbackHelper helper = getCrashListLoadedListener();
830         int crashListLoadInitCount = helper.getCallCount();
831         launchCrashesFragment();
832         helper.waitForCallback(crashListLoadInitCount, 1);
833 
834         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
835 
836         // Check crash item header
837         checkUnknownPackageCrashItemHeader(onData(anything()).atPosition(0), crashInfo)
838                 .perform(click()); // click to expand it
839         // The body is considered item#2 in the list view after expansion
840         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
841         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
842 
843         crashListLoadInitCount = helper.getCallCount();
844         clickHideCrashButton(bodyDataInteraction);
845         helper.waitForCallback(crashListLoadInitCount, 1);
846 
847         onView(withId(R.id.crashes_list)).check(matches(withCount(0)));
848     }
849 
850     @Test
851     @Feature({"AndroidWebView"})
852     public void testHideCrashButton_pending() throws Throwable {
853         final long systemTime = System.currentTimeMillis();
854         CrashInfo crashInfo = createCrashInfo(
855                 "123456", systemTime, null, -1, FAKE_APP_PACKAGE_NAME, UploadState.PENDING);
856 
857         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
858         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
859 
860         CallbackHelper helper = getCrashListLoadedListener();
861         int crashListLoadInitCount = helper.getCallCount();
862         launchCrashesFragment();
863         helper.waitForCallback(crashListLoadInitCount, 1);
864 
865         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
866 
867         // Check crash item header
868         checkUnknownPackageCrashItemHeader(onData(anything()).atPosition(0), crashInfo)
869                 .perform(click()); // click to expand it
870         // The body is considered item#2 in the list view after expansion
871         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
872         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
873 
874         crashListLoadInitCount = helper.getCallCount();
875         clickHideCrashButton(bodyDataInteraction);
876         helper.waitForCallback(crashListLoadInitCount, 1);
877 
878         onView(withId(R.id.crashes_list)).check(matches(withCount(0)));
879     }
880 
881     @Test
882     @Feature({"AndroidWebView"})
883     public void testHideCrashButton_uploaded_missingJson() throws Throwable {
884         CrashInfo crashInfo = createCrashInfo("123456", -1, null, 1000, null, UploadState.UPLOADED);
885 
886         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
887         assertThat("upload log file should exist", appendUploadedEntryToLog(crashInfo).exists());
888 
889         CallbackHelper helper = getCrashListLoadedListener();
890         int crashListLoadInitCount = helper.getCallCount();
891         launchCrashesFragment();
892         helper.waitForCallback(crashListLoadInitCount, 1);
893 
894         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
895 
896         // Check crash item header
897         checkMissingPackageInfoCrashItemHeader(onData(anything()).atPosition(0), crashInfo)
898                 .perform(click()); // click to expand it
899         // The body is considered item#2 in the list view after expansion
900         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
901         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
902 
903         crashListLoadInitCount = helper.getCallCount();
904         clickHideCrashButton(bodyDataInteraction);
905         helper.waitForCallback(crashListLoadInitCount, 1);
906 
907         onView(withId(R.id.crashes_list)).check(matches(withCount(0)));
908     }
909 
910     @Test
911     @Feature({"AndroidWebView"})
912     public void testHideCrashButton_pending_missingJson() throws Throwable {
913         CrashInfo crashInfo = createCrashInfo("123456", -1, null, -1, null, UploadState.PENDING);
914 
915         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
916 
917         CallbackHelper helper = getCrashListLoadedListener();
918         int crashListLoadInitCount = helper.getCallCount();
919         launchCrashesFragment();
920         helper.waitForCallback(crashListLoadInitCount, 1);
921 
922         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
923 
924         // Check crash item header
925         checkMissingPackageInfoCrashItemHeader(onData(anything()).atPosition(0), crashInfo)
926                 .perform(click()); // click to expand it
927         // The body is considered item#2 in the list view after expansion
928         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
929         DataInteraction bodyDataInteraction = onData(anything()).atPosition(1);
930 
931         crashListLoadInitCount = helper.getCallCount();
932         clickHideCrashButton(bodyDataInteraction);
933         helper.waitForCallback(crashListLoadInitCount, 1);
934 
935         onView(withId(R.id.crashes_list)).check(matches(withCount(0)));
936     }
937 
938     @Test
939     @LargeTest
940     @Feature({"AndroidWebView"})
941     public void testRefreshMenuOption() throws Throwable {
942         CallbackHelper helper = getCrashListLoadedListener();
943         int crashListLoadInitCount = helper.getCallCount();
944         launchCrashesFragment();
945         helper.waitForCallback(crashListLoadInitCount, 1);
946 
947         onView(withId(R.id.crashes_list)).check(matches(withCount(0)));
948 
949         final long systemTime = System.currentTimeMillis();
950         CrashInfo crashInfo = createCrashInfo(
951                 "123456", systemTime, null, -1, FAKE_APP_PACKAGE_NAME, UploadState.PENDING);
952 
953         assertThat("temp minidump file should exist", createMinidumpFile(crashInfo).exists());
954         assertThat("temp json log file should exist", writeJsonLogFile(crashInfo).exists());
955 
956         crashListLoadInitCount = helper.getCallCount();
957         onView(withText("Refresh")).check(matches(isDisplayed()));
958         onView(withText("Refresh")).perform(click());
959         helper.waitForCallback(crashListLoadInitCount, 1);
960 
961         onView(withId(R.id.crashes_list)).check(matches(withCount(1)));
962         checkUnknownPackageCrashItemHeader(onData(anything()).atPosition(0), crashInfo);
963     }
964 
965     @Test
966     @LargeTest
967     @Feature({"AndroidWebView"})
968     public void testLongPressCopy() throws Throwable {
969         Context context = InstrumentationRegistry.getTargetContext();
970         final long systemTime = System.currentTimeMillis();
971         CrashInfo uploadedCrashInfo = createCrashInfo("123456", systemTime - 1000, "0abcde123456",
972                 systemTime, FAKE_APP_PACKAGE_NAME, UploadState.UPLOADED);
973         CrashInfo pendingCrashInfo = createCrashInfo(
974                 "78910", systemTime - 2000, null, -1, FAKE_APP_PACKAGE_NAME, UploadState.PENDING);
975 
976         assertThat("temp json log file for uploaded crash should exist",
977                 writeJsonLogFile(uploadedCrashInfo).exists());
978         assertThat("upload log file should exist",
979                 appendUploadedEntryToLog(uploadedCrashInfo).exists());
980 
981         assertThat("temp minidump file for pending crash should exist",
982                 createMinidumpFile(pendingCrashInfo).exists());
983         assertThat("temp json log file for pending crash should exist",
984                 writeJsonLogFile(pendingCrashInfo).exists());
985 
986         CallbackHelper helper = getCrashListLoadedListener();
987         int crashListLoadInitCount = helper.getCallCount();
988         launchCrashesFragment();
989         helper.waitForCallback(crashListLoadInitCount, 1);
990 
991         onView(withId(R.id.crashes_list)).check(matches(withCount(2)));
992 
993         // click on the first crash item header to expand
994         onData(anything()).atPosition(0).perform(click());
995         // long click on the crash item body to copy
996         onData(anything()).atPosition(1).perform(longClick());
997         String expectedUploadInfo = new Date(uploadedCrashInfo.uploadTime).toString()
998                 + "\nID: " + uploadedCrashInfo.uploadId;
999         assertThat(getClipBoardTextOnUiThread(context), is(expectedUploadInfo));
1000 
1001         // click on the first crash item header to collapse
1002         onData(anything()).atPosition(0).perform(click());
1003         // click on the second crash item header to expand
1004         onData(anything()).atPosition(1).perform(click());
1005         // Clear clipboard content
1006         setClipBoardTextOnUiThread(context, "", "");
1007         // Crash body is now the second item in the list view, long click on the crash item body to
1008         // copy.
1009         onData(anything()).atPosition(2).perform(longClick());
1010         // This a pending upload, nothing should be copied
1011         assertThat(getClipBoardTextOnUiThread(context), is(""));
1012     }
1013 
1014     @Test
1015     @MediumTest
1016     @Feature({"AndroidWebView"})
1017     public void testConsentErrorMessage_notShown_differentWebViewPackageIsShown() throws Throwable {
1018         Context context = InstrumentationRegistry.getTargetContext();
1019         // Inject a dummy PackageInfo as the current WebView package to make sure it will always be
1020         // different from the test's app package.
1021         WebViewPackageHelper.setCurrentWebViewPackageForTesting(
1022                 HomeFragmentTest.FAKE_WEBVIEW_PACKAGE);
1023         PlatformServiceBridge.injectInstance(
1024                 new TestPlatformServiceBridge(/*canUseGms=*/true, /*userConsent=*/false));
1025         launchCrashesFragment();
1026 
1027         String expectedErrorMessage = String.format(Locale.US,
1028                 WebViewPackageError.DIFFERENT_WEBVIEW_PROVIDER_ERROR_MESSAGE,
1029                 WebViewPackageHelper.loadLabel(context));
1030         onView(withId(R.id.main_error_view)).check(matches(isDisplayed()));
1031         onView(withId(R.id.error_text)).check(matches(withText(expectedErrorMessage)));
1032     }
1033 
1034     @Test
1035     @MediumTest
1036     @Feature({"AndroidWebView"})
1037     public void testConsentErrorMessage_notShown_userConsented() throws Throwable {
1038         Context context = InstrumentationRegistry.getTargetContext();
1039         // Inject test app package as the current WebView package.
1040         WebViewPackageHelper.setCurrentWebViewPackageForTesting(
1041                 WebViewPackageHelper.getContextPackageInfo(context));
1042         PlatformServiceBridge.injectInstance(
1043                 new TestPlatformServiceBridge(/*canUseGms=*/true, /*userConsent=*/true));
1044         launchCrashesFragment();
1045 
1046         onView(withId(R.id.main_error_view)).check(matches(not(isDisplayed())));
1047     }
1048 
1049     @Test
1050     @MediumTest
1051     @Feature({"AndroidWebView"})
1052     public void testConsentErrorMessage_shown_canUseGms() throws Throwable {
1053         Context context = InstrumentationRegistry.getTargetContext();
1054 
1055         Intent settingsIntent =
1056                 new Intent(CrashesListFragment.USAGE_AND_DIAGONSTICS_ACTIVITY_INTENT_ACTION);
1057         List<ResolveInfo> intentResolveInfo =
1058                 context.getPackageManager().queryIntentActivities(settingsIntent, 0);
1059         Assume.assumeTrue(
1060                 "This test assumes \"usage& diagonstics\" settings can be found on the device",
1061                 intentResolveInfo.size() > 0);
1062 
1063         // Inject test app package as the current WebView package.
1064         WebViewPackageHelper.setCurrentWebViewPackageForTesting(
1065                 WebViewPackageHelper.getContextPackageInfo(context));
1066         PlatformServiceBridge.injectInstance(
1067                 new TestPlatformServiceBridge(/*canUseGms=*/true, /*userConsent=*/false));
1068         launchCrashesFragment();
1069 
1070         onView(withId(R.id.main_error_view)).check(matches(isDisplayed()));
1071         onView(withId(R.id.error_text))
1072                 .check(matches(
1073                         withText(CrashesListFragment.CRASH_COLLECTION_DISABLED_ERROR_MESSAGE)));
1074         onView(withId(R.id.action_button))
1075                 .check(matches(withText("Open Settings")))
1076                 .perform(click());
1077         intended(IntentMatchers.hasAction(
1078                 CrashesListFragment.USAGE_AND_DIAGONSTICS_ACTIVITY_INTENT_ACTION));
1079     }
1080 
1081     @Test
1082     @MediumTest
1083     @Feature({"AndroidWebView"})
1084     public void testConsentErrorMessage_shown_onlyInCrashFragment() throws Throwable {
1085         Context context = InstrumentationRegistry.getTargetContext();
1086         // Inject test app package as the current WebView package.
1087         WebViewPackageHelper.setCurrentWebViewPackageForTesting(
1088                 WebViewPackageHelper.getContextPackageInfo(context));
1089         PlatformServiceBridge.injectInstance(
1090                 new TestPlatformServiceBridge(/*canUseGms=*/true, /*userConsent=*/false));
1091         launchCrashesFragment();
1092 
1093         onView(withId(R.id.main_error_view)).check(matches(isDisplayed()));
1094         onView(withId(R.id.error_text))
1095                 .check(matches(
1096                         withText(CrashesListFragment.CRASH_COLLECTION_DISABLED_ERROR_MESSAGE)));
1097 
1098         // CrashesListFragment -> FlagsFragment (Not shown)
1099         onView(withId(R.id.navigation_flags_ui)).perform(click());
1100         onView(withId(R.id.main_error_view)).check(matches(not(isDisplayed())));
1101         // FlagsFragment -> HomeFragment (Not shown)
1102         onView(withId(R.id.navigation_home)).perform(click());
1103         onView(withId(R.id.main_error_view)).check(matches(not(isDisplayed())));
1104         // HomeFragment -> CrashesListFragment (shown again)
1105         onView(withId(R.id.navigation_crash_ui)).perform(click());
1106         onView(withId(R.id.main_error_view)).check(matches(isDisplayed()));
1107         onView(withId(R.id.error_text))
1108                 .check(matches(
1109                         withText(CrashesListFragment.CRASH_COLLECTION_DISABLED_ERROR_MESSAGE)));
1110     }
1111 
1112     @Test
1113     @MediumTest
1114     @Feature({"AndroidWebView"})
1115     public void testConsentErrorMessage_shown_cannotUseGms() throws Throwable {
1116         Context context = InstrumentationRegistry.getTargetContext();
1117         // Inject test app package as the current WebView package.
1118         WebViewPackageHelper.setCurrentWebViewPackageForTesting(
1119                 WebViewPackageHelper.getContextPackageInfo(context));
1120         PlatformServiceBridge.injectInstance(
1121                 new TestPlatformServiceBridge(/*canUseGms=*/false, /*userConsent=*/false));
1122         launchCrashesFragment();
1123 
1124         onView(withId(R.id.main_error_view)).check(matches(isDisplayed()));
1125         onView(withId(R.id.error_text))
1126                 .check(matches(withText(CrashesListFragment.NO_GMS_ERROR_MESSAGE)));
1127         onView(withId(R.id.action_button)).check(matches(not(isDisplayed())));
1128     }
1129 }
1130