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