1 // Copyright 2019 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 package org.chromium.android_webview.devui;
5 
6 import android.content.Intent;
7 import android.graphics.PorterDuff;
8 import android.graphics.PorterDuffColorFilter;
9 import android.graphics.drawable.Drawable;
10 import android.net.Uri;
11 import android.os.Build;
12 import android.os.Bundle;
13 import android.provider.Settings;
14 import android.view.Menu;
15 import android.view.MenuItem;
16 import android.view.View;
17 import android.widget.LinearLayout;
18 import android.widget.TextView;
19 
20 import androidx.annotation.IntDef;
21 import androidx.fragment.app.Fragment;
22 import androidx.fragment.app.FragmentActivity;
23 import androidx.fragment.app.FragmentManager;
24 import androidx.fragment.app.FragmentTransaction;
25 
26 import org.chromium.base.ApiCompatibilityUtils;
27 import org.chromium.base.metrics.RecordHistogram;
28 
29 import java.util.HashMap;
30 import java.util.Map;
31 
32 /**
33  * Dev UI main activity.
34  * It shows persistent errors and helps to navigate to WebView developer tools.
35  */
36 public class MainActivity extends FragmentActivity {
37     private PersistentErrorView mErrorView;
38     private WebViewPackageError mDifferentPackageError;
39     private boolean mDifferentPackageErrorVisible;
40     private boolean mSwitchFragmentOnResume;
41     final Map<Integer, Integer> mFragmentIdMap = new HashMap<>();
42 
43     // Keep in sync with DeveloperUiService.java
44     public static final String FRAGMENT_ID_INTENT_EXTRA = "fragment-id";
45     public static final int FRAGMENT_ID_HOME = 0;
46     public static final int FRAGMENT_ID_CRASHES = 1;
47     public static final int FRAGMENT_ID_FLAGS = 2;
48 
49     // These values are persisted to logs. Entries should not be renumbered and
50     // numeric values should never be reused.
51     @IntDef({MenuChoice.SWITCH_PROVIDER, MenuChoice.REPORT_BUG, MenuChoice.CHECK_UPDATES,
52             MenuChoice.CRASHES_REFRESH, MenuChoice.ABOUT_DEVTOOLS})
53     public @interface MenuChoice {
54         int SWITCH_PROVIDER = 0;
55         int REPORT_BUG = 1;
56         int CHECK_UPDATES = 2;
57         int CRASHES_REFRESH = 3;
58         int ABOUT_DEVTOOLS = 4;
59         int COUNT = 5;
60     }
61 
logMenuSelection(@enuChoice int selectedMenuItem)62     public static void logMenuSelection(@MenuChoice int selectedMenuItem) {
63         RecordHistogram.recordEnumeratedHistogram(
64                 "Android.WebView.DevUi.MenuSelection", selectedMenuItem, MenuChoice.COUNT);
65     }
66 
67     // These values are persisted to logs. Entries should not be renumbered and
68     // numeric values should never be reused.
69     @IntDef({FragmentNavigation.HOME_FRAGMENT, FragmentNavigation.CRASHES_LIST_FRAGMENT,
70             FragmentNavigation.FLAGS_FRAGMENT})
71     private @interface FragmentNavigation {
72         int HOME_FRAGMENT = 0;
73         int CRASHES_LIST_FRAGMENT = 1;
74         int FLAGS_FRAGMENT = 2;
75         int COUNT = 3;
76     }
77 
78     /**
79      * Logs a navigation to a fragment. Requires a suffix from histograms.xml ("AnyMethod",
80      * "FromIntent", or "NavBar") to determine which histogram to log.
81      *
82      * @param histogramSuffix one of the suffixes listed in histograms.xml
83      * @param selectedFragmentId one of FRAGMENT_ID_HOME, FRAGMENT_ID_CRASHES, or FRAGMENT_ID_FLAGS
84      */
logFragmentNavigation(String histogramSuffix, int selectedFragmentId)85     private static void logFragmentNavigation(String histogramSuffix, int selectedFragmentId) {
86         // Map FRAGMENT_ID_* to FragmentNavigation value (so FRAGMENT_ID_* values are permitted to
87         // change in the future without messing up logs).
88         @FragmentNavigation
89         int sample;
90         switch (selectedFragmentId) {
91             default:
92                 // Fall through.
93             case FRAGMENT_ID_HOME:
94                 sample = FragmentNavigation.HOME_FRAGMENT;
95                 break;
96             case FRAGMENT_ID_CRASHES:
97                 sample = FragmentNavigation.CRASHES_LIST_FRAGMENT;
98                 break;
99             case FRAGMENT_ID_FLAGS:
100                 sample = FragmentNavigation.FLAGS_FRAGMENT;
101                 break;
102         }
103         RecordHistogram.recordEnumeratedHistogram(
104                 "Android.WebView.DevUi.FragmentNavigation." + histogramSuffix, sample,
105                 FragmentNavigation.COUNT);
106     }
107 
108     @Override
onCreate(Bundle savedInstanceState)109     protected void onCreate(Bundle savedInstanceState) {
110         super.onCreate(savedInstanceState);
111 
112         setContentView(R.layout.activity_main);
113 
114         // Let onResume handle showing the initial Fragment.
115         mSwitchFragmentOnResume = true;
116 
117         mErrorView = new PersistentErrorView(this, R.id.main_error_view);
118         mDifferentPackageError = new WebViewPackageError(this, mErrorView);
119 
120         // Set up bottom navigation bar:
121         mFragmentIdMap.put(R.id.navigation_home, FRAGMENT_ID_HOME);
122         mFragmentIdMap.put(R.id.navigation_crash_ui, FRAGMENT_ID_CRASHES);
123         mFragmentIdMap.put(R.id.navigation_flags_ui, FRAGMENT_ID_FLAGS);
124         LinearLayout bottomNavBar = findViewById(R.id.nav_view);
125         View.OnClickListener listener = (View view) -> {
126             assert mFragmentIdMap.containsKey(view.getId()) : "Unexpected view ID: " + view.getId();
127             int fragmentId = mFragmentIdMap.get(view.getId());
128             switchFragment(fragmentId);
129             logFragmentNavigation("NavBar", fragmentId);
130         };
131         final int childCount = bottomNavBar.getChildCount();
132         for (int i = 0; i < childCount; ++i) {
133             View v = bottomNavBar.getChildAt(i);
134             v.setOnClickListener(listener);
135         }
136 
137         FragmentManager fm = getSupportFragmentManager();
138         fm.registerFragmentLifecycleCallbacks(
139                 new FragmentManager.FragmentLifecycleCallbacks() {
140                     @Override
141                     public void onFragmentResumed(FragmentManager fm, Fragment f) {
142                         if (!mDifferentPackageErrorVisible) {
143                             if (f instanceof DevUiBaseFragment) {
144                                 ((DevUiBaseFragment) f).maybeShowErrorView(mErrorView);
145                             }
146                         }
147                     }
148                 },
149                 /* recursive */ false);
150 
151         // The boolean value doesn't matter, we only care about the total count.
152         RecordHistogram.recordBooleanHistogram("Android.WebView.DevUi.AppLaunch", true);
153     }
154 
switchFragment(int chosenFragmentId)155     private void switchFragment(int chosenFragmentId) {
156         DevUiBaseFragment fragment = null;
157         switch (chosenFragmentId) {
158             default:
159                 chosenFragmentId = FRAGMENT_ID_HOME;
160                 // Fall through.
161             case FRAGMENT_ID_HOME:
162                 fragment = new HomeFragment();
163                 break;
164             case FRAGMENT_ID_CRASHES:
165                 fragment = new CrashesListFragment();
166                 break;
167             case FRAGMENT_ID_FLAGS:
168                 fragment = new FlagsFragment();
169                 break;
170         }
171         assert fragment != null;
172         logFragmentNavigation("AnyMethod", chosenFragmentId);
173 
174         // Switch fragments
175         FragmentManager fm = getSupportFragmentManager();
176         FragmentTransaction transaction = fm.beginTransaction();
177         transaction.replace(R.id.content_fragment, fragment);
178         transaction.commit();
179 
180         // Update the bottom toolbar
181         LinearLayout bottomNavBar = findViewById(R.id.nav_view);
182         final int childCount = bottomNavBar.getChildCount();
183         for (int i = 0; i < childCount; ++i) {
184             View view = bottomNavBar.getChildAt(i);
185             assert mFragmentIdMap.containsKey(view.getId()) : "Unexpected view ID: " + view.getId();
186             int fragmentId = mFragmentIdMap.get(view.getId());
187             assert view instanceof TextView : "Bottom bar must have TextViews as direct children";
188             TextView textView = (TextView) view;
189 
190             boolean isSelectedFragment = chosenFragmentId == fragmentId;
191             ApiCompatibilityUtils.setTextAppearance(textView,
192                     isSelectedFragment ? R.style.SelectedNavigationButton
193                                        : R.style.UnselectedNavigationButton);
194             int color = isSelectedFragment ? getResources().getColor(R.color.navigation_selected)
195                                            : getResources().getColor(R.color.navigation_unselected);
196             for (Drawable drawable : textView.getCompoundDrawables()) {
197                 if (drawable != null) {
198                     drawable.mutate();
199                     drawable.setColorFilter(
200                             new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
201                 }
202             }
203         }
204     }
205 
206     @Override
onNewIntent(Intent intent)207     protected void onNewIntent(Intent intent) {
208         super.onNewIntent(intent);
209         // Store the Intent so we can switch Fragments in onResume (which is called next). Only need
210         // to switch Fragment if the Intent specifies to do so.
211         setIntent(intent);
212         mSwitchFragmentOnResume = intent.hasExtra(FRAGMENT_ID_INTENT_EXTRA);
213     }
214 
215     @Override
onResume()216     protected void onResume() {
217         super.onResume();
218 
219         // Check package status in onResume() to hide/show the error message if the user
220         // changes WebView implementation from system settings and then returns back to the
221         // activity.
222         mDifferentPackageErrorVisible = mDifferentPackageError.showMessageIfDifferent();
223 
224         // Don't change Fragment unless we have a new Intent, since the user might just be coming
225         // back to this through the task switcher.
226         if (!mSwitchFragmentOnResume) return;
227 
228         // Ensure we only switch the first time we see a new Intent.
229         mSwitchFragmentOnResume = false;
230 
231         // Default to HomeFragment if not specified.
232         int fragmentId = FRAGMENT_ID_HOME;
233         // FRAGMENT_ID_INTENT_EXTRA is an optional extra to specify which fragment to open. At the
234         // moment, it's specified only by DeveloperUiService (so make sure these constants stay in
235         // sync).
236         Bundle extras = getIntent().getExtras();
237         if (extras != null) {
238             fragmentId = extras.getInt(FRAGMENT_ID_INTENT_EXTRA, fragmentId);
239         }
240         switchFragment(fragmentId);
241         logFragmentNavigation("FromIntent", fragmentId);
242     }
243 
244     @Override
onCreateOptionsMenu(Menu menu)245     public boolean onCreateOptionsMenu(Menu menu) {
246         getMenuInflater().inflate(R.menu.options_menu, menu);
247         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
248             // Switching WebView providers is only possible for API >= 24.
249             MenuItem item = menu.findItem(R.id.options_menu_switch_provider);
250             item.setVisible(false);
251         }
252         return true;
253     }
254 
255     @Override
onOptionsItemSelected(MenuItem item)256     public boolean onOptionsItemSelected(MenuItem item) {
257         if (item.getItemId() == R.id.options_menu_switch_provider
258                 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
259             logMenuSelection(MenuChoice.SWITCH_PROVIDER);
260             startActivity(new Intent(Settings.ACTION_WEBVIEW_SETTINGS));
261             return true;
262         } else if (item.getItemId() == R.id.options_menu_report_bug) {
263             logMenuSelection(MenuChoice.REPORT_BUG);
264             Uri reportUri = new Uri.Builder()
265                                     .scheme("https")
266                                     .authority("bugs.chromium.org")
267                                     .path("/p/chromium/issues/entry")
268                                     .appendQueryParameter("template", "Webview+Bugs")
269                                     .appendQueryParameter("labels",
270                                             "Via-WebView-DevTools,Pri-3,Type-Bug,OS-Android")
271                                     .build();
272             startActivity(new Intent(Intent.ACTION_VIEW, reportUri));
273             return true;
274         } else if (item.getItemId() == R.id.options_menu_check_updates) {
275             logMenuSelection(MenuChoice.CHECK_UPDATES);
276             try {
277                 Uri marketUri = new Uri.Builder()
278                                         .scheme("market")
279                                         .authority("details")
280                                         .appendQueryParameter("id", this.getPackageName())
281                                         .build();
282                 startActivity(new Intent(Intent.ACTION_VIEW, marketUri));
283             } catch (Exception e) {
284                 Uri marketUri = new Uri.Builder()
285                                         .scheme("https")
286                                         .authority("play.google.com")
287                                         .path("/store/apps/details")
288                                         .appendQueryParameter("id", this.getPackageName())
289                                         .build();
290                 startActivity(new Intent(Intent.ACTION_VIEW, marketUri));
291             }
292             return true;
293         } else if (item.getItemId() == R.id.options_menu_about_devui) {
294             logMenuSelection(MenuChoice.ABOUT_DEVTOOLS);
295             Uri uri = Uri.parse(
296                     "https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/developer-ui.md");
297             startActivity(new Intent(Intent.ACTION_VIEW, uri));
298             return true;
299         }
300         return super.onOptionsItemSelected(item);
301     }
302 }
303