public DistributionDescriptor getDescriptor() { File descFile = getDistributionFile("preferences.json"); if (descFile == null) { // Logging and existence checks are handled in getDistributionFile. return null; } try { JSONObject all = FileUtils.readJSONObjectFromFile(descFile); if (!all.has("Global")) { Log.e(LOGTAG, "Distribution preferences.json has no Global entry!"); return null; } return new DistributionDescriptor(all.getJSONObject("Global")); } catch (IOException e) { Log.e(LOGTAG, "Error getting distribution descriptor file.", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } catch (JSONException e) { Log.e(LOGTAG, "Error parsing preferences.json", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } }
/** * Get the Android preferences from the preferences.json file, if any exist. * * @return The preferences in a JSONObject, or an empty JSONObject if no preferences are defined. */ public JSONObject getAndroidPreferences() { final File descFile = getDistributionFile("preferences.json"); if (descFile == null) { // Logging and existence checks are handled in getDistributionFile. return new JSONObject(); } try { final JSONObject all = FileUtils.readJSONObjectFromFile(descFile); if (!all.has("AndroidPreferences")) { return new JSONObject(); } return all.getJSONObject("AndroidPreferences"); } catch (IOException e) { Log.e(LOGTAG, "Error getting distribution descriptor file.", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return new JSONObject(); } catch (JSONException e) { Log.e(LOGTAG, "Error parsing preferences.json", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return new JSONObject(); } }
/** * Fetch the provided URI, returning a {@link JarInputStream} if the response body is appropriate. * * <p>Protected to allow for mocking. * * @return the entity body as a stream, or null on failure. */ @SuppressWarnings("static-method") @RobocopTarget protected JarInputStream fetchDistribution(URI uri, HttpURLConnection connection) throws IOException { final int status = connection.getResponseCode(); Log.d(LOGTAG, "Distribution fetch: " + status); // We record HTTP statuses as 2xx, 3xx, 4xx, 5xx => 2, 3, 4, 5. final int value; if (status > 599 || status < 100) { Log.wtf(LOGTAG, "Unexpected HTTP status code: " + status); value = CODE_CATEGORY_STATUS_OUT_OF_RANGE; } else { value = status / 100; } Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, value); if (status != 200) { Log.w(LOGTAG, "Got status " + status + " fetching distribution."); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE); return null; } final String contentType = connection.getContentType(); if (contentType == null || !contentType.startsWith(EXPECTED_CONTENT_TYPE)) { Log.w(LOGTAG, "Malformed response: invalid Content-Type."); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE); return null; } return new JarInputStream(new BufferedInputStream(connection.getInputStream()), true); }
private static void recordFetchTelemetry(final Exception exception) { if (exception == null) { // Should never happen. Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION); return; } if (exception instanceof UnknownHostException) { // Unknown host => we're offline. Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_OFFLINE); return; } if (exception instanceof SSLException) { Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SSL_ERROR); return; } if (exception instanceof ProtocolException || exception instanceof SocketException) { Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SOCKET_ERROR); return; } Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION); }
private void addTab() { if (mCurrentPanel == Panel.NORMAL_TABS) { Telemetry.sendUIEvent( TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_tab"); mActivity.addTab(); } else { Telemetry.sendUIEvent( TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_private_tab"); mActivity.addPrivateTab(); } mActivity.autoHideTabs(); }
/** * Update the hide/show state of the preference and save the HomeConfig changes. * * @param pref Preference to update * @param toHide New hidden state of the preference */ protected void setHidden(PanelsPreference pref, boolean toHide) { final String id = pref.getKey(); mConfigEditor.setDisabled(id, toHide); mConfigEditor.apply(); if (toHide) { Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_HIDE, Method.DIALOG, id); } else { Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_SHOW, Method.DIALOG, id); } pref.setHidden(toHide); setDefaultFromConfig(); }
@Override public void onReceive(final Context context, final Intent intent) { // This will hold the intent to redispatch final Intent redirect; if (intent.getAction().equals(ACTION_LAUNCH_BROWSER)) { redirect = buildRedirectIntent( Intent.ACTION_MAIN, AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME, intent); Telemetry.sendUIEvent( TelemetryContract.Event.LAUNCH, TelemetryContract.Method.WIDGET, "browser"); } else if (intent.getAction().equals(ACTION_LAUNCH_NEW_TAB)) { redirect = buildRedirectIntent( Intent.ACTION_VIEW, AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME, intent); Telemetry.sendUIEvent( TelemetryContract.Event.LAUNCH, TelemetryContract.Method.WIDGET, "new-tab"); } else if (intent.getAction().equals(ACTION_LAUNCH_SEARCH)) { redirect = buildRedirectIntent( Intent.ACTION_VIEW, AppConstants.SEARCH_PACKAGE_NAME, AppConstants.SEARCH_INTENT_CLASS_NAME, intent); Telemetry.sendUIEvent( TelemetryContract.Event.LAUNCH, TelemetryContract.Method.WIDGET, "search"); } else { redirect = null; } if (redirect != null) { try { context.startActivity(redirect); } catch (Exception ex) { // When this is built stand alone, its hardcoded to try and launch nightly. // If that fails, just fire a generic VIEW intent. Intent redirect2 = buildRedirectIntent(Intent.ACTION_VIEW, null, null, intent); context.startActivity(redirect2); } } super.onReceive(context, intent); }
@Override public void onItemClicked(RecyclerView recyclerView, int position, View v) { final int viewType = getAdapter().getItemViewType(position); final CombinedHistoryAdapter.ItemType itemType = CombinedHistoryAdapter.ItemType.viewTypeToItemType(viewType); switch (itemType) { case CLIENT: mOnPanelLevelChangeListener.onPanelLevelChange(PanelLevel.CHILD); ((CombinedHistoryAdapter) getAdapter()).showChildView(position); break; case HIDDEN_DEVICES: if (mDialogBuilder != null) { mDialogBuilder.createAndShowDialog( ((CombinedHistoryAdapter) getAdapter()).getHiddenClients()); } break; case NAVIGATION_BACK: mOnPanelLevelChangeListener.onPanelLevelChange(PARENT); ((CombinedHistoryAdapter) getAdapter()).exitChildView(); break; case CHILD: case HISTORY: if (mOnUrlOpenListener != null) { final TwoLinePageRow historyItem = (TwoLinePageRow) v; Telemetry.sendUIEvent( TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "history"); mOnUrlOpenListener.onUrlOpen( historyItem.getUrl(), EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); } break; } }
private void openNow(Intent intent) { Intent forwardIntent = new Intent(intent); forwardIntent.setClassName( getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(forwardIntent); TabQueueHelper.removeNotification(getApplicationContext()); GeckoSharedPrefs.forApp(getApplicationContext()) .edit() .remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE) .remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME) .apply(); Telemetry.sendUIEvent( TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-now"); executorService.submit( new Runnable() { @Override public void run() { int queuedTabCount = TabQueueHelper.getTabQueueLength(TabQueueService.this); Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount); } }); }
public void load( Context appContext, FragmentManager fm, final FirstrunAnimationContainer.OnFinishListener onFinishListener) { final List<FirstrunPagerConfig.FirstrunPanelConfig> panels; if (Restrictions.isUserRestricted(context)) { panels = FirstrunPagerConfig.getRestricted(); } else { panels = FirstrunPagerConfig.getDefault(appContext); if (panels.size() == 1) { mTabStrip.setVisibility(GONE); } } setAdapter(new ViewPagerAdapter(fm, panels)); this.pagerNavigation = new FirstrunPanel.PagerNavigation() { @Override public void next() { final int currentPage = FirstrunPager.this.getCurrentItem(); if (currentPage < FirstrunPager.this.getAdapter().getCount() - 1) { FirstrunPager.this.setCurrentItem(currentPage + 1); } } @Override public void finish() { if (onFinishListener != null) { onFinishListener.onFinish(); } } }; addOnPageChangeListener( new OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { mDecor.onPageScrolled(position, positionOffset, positionOffsetPixels); } @Override public void onPageSelected(int i) { mDecor.onPageSelected(i); Telemetry.sendUIEvent( TelemetryContract.Event.SHOW, TelemetryContract.Method.PANEL, "onboarding." + i); } @Override public void onPageScrollStateChanged(int i) {} }); animateLoad(); // Record telemetry for first onboarding panel, for baseline. Telemetry.sendUIEvent( TelemetryContract.Event.SHOW, TelemetryContract.Method.PANEL, "onboarding.0"); }
public JSONArray getBookmarks() { File bookmarks = getDistributionFile("bookmarks.json"); if (bookmarks == null) { // Logging and existence checks are handled in getDistributionFile. return null; } try { return new JSONArray(FileUtils.readStringFromFile(bookmarks)); } catch (IOException e) { Log.e(LOGTAG, "Error getting bookmarks", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } catch (JSONException e) { Log.e(LOGTAG, "Error parsing bookmarks.json", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } }
@Override public void uninstall(CustomListPreference pref) { final String id = pref.getKey(); mConfigEditor.uninstall(id); mConfigEditor.apply(); Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_REMOVE, Method.DIALOG, id); super.uninstall(pref); }
public void moveDown(PanelsPreference pref) { final int panelIndex = pref.getIndex(); if (panelIndex < getPreferenceCount() - 1) { final String panelKey = pref.getKey(); mConfigEditor.moveTo(panelKey, panelIndex + 1); final State state = mConfigEditor.apply(); Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_MOVE, Method.DIALOG, panelKey); refresh(state, panelKey); } }
@Override public boolean onMenuItemClick(MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.close_all_tabs) { if (mCurrentPanel == Panel.NORMAL_TABS) { final String extras = getResources().getResourceEntryName(itemId); Telemetry.sendUIEvent( TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras); // Disable the menu button so that the menu won't interfere with the tab close animation. mMenuButton.setEnabled(false); ((CloseAllPanelView) mPanelNormal).closeAll(); } else { Log.e(LOGTAG, "Close all tabs menu item should only be visible for normal tabs panel"); } return true; } if (itemId == R.id.close_private_tabs) { if (mCurrentPanel == Panel.PRIVATE_TABS) { final String extras = getResources().getResourceEntryName(itemId); Telemetry.sendUIEvent( TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras); ((CloseAllPanelView) mPanelPrivate).closeAll(); } else { Log.e(LOGTAG, "Close private tabs menu item should only be visible for private tabs panel"); } return true; } if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) { hide(); } return mActivity.onOptionsItemSelected(item); }
@Override public void setDefault(CustomListPreference pref) { super.setDefault(pref); final String id = pref.getKey(); final String defaultPanelId = mConfigEditor.getDefaultPanelId(); if (defaultPanelId != null && defaultPanelId.equals(id)) { return; } mConfigEditor.setDefault(id); mConfigEditor.apply(); Telemetry.sendUIEvent(TelemetryContract.Event.PANEL_SET_DEFAULT, Method.DIALOG, id); }
private URI getReferredDistribution(ReferrerDescriptor descriptor) { final String content = descriptor.content; if (content == null) { return null; } // We restrict here to avoid injection attacks. After all, // we're downloading a distribution payload based on intent input. if (!content.matches("^[a-zA-Z0-9]+$")) { Log.e(LOGTAG, "Invalid referrer content: " + content); Telemetry.addToHistogram(HISTOGRAM_REFERRER_INVALID, 1); return null; } try { return new URI(FETCH_PROTOCOL, FETCH_HOSTNAME, FETCH_PATH + content + FETCH_EXTENSION, null); } catch (URISyntaxException e) { // This should never occur. Log.wtf(LOGTAG, "Invalid URI with content " + content + "!"); return null; } }
/** * If applicable, download and select the distribution specified in the referrer intent. * * @return true if a referrer-supplied distribution was selected. */ private boolean checkIntentDistribution(final ReferrerDescriptor referrer) { if (referrer == null) { return false; } URI uri = getReferredDistribution(referrer); if (uri == null) { return false; } long start = SystemClock.uptimeMillis(); Log.v(LOGTAG, "Downloading referred distribution: " + uri); try { final HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); // If the Search Activity starts, and we handle the referrer intent, this'll return // null. Recover gracefully in this case. final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface(); final String ua; if (geckoInterface == null) { // Fall back to GeckoApp's default implementation. ua = HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE; } else { ua = geckoInterface.getDefaultUAString(); } connection.setRequestProperty(HTTP.USER_AGENT, ua); connection.setRequestProperty("Accept", EXPECTED_CONTENT_TYPE); try { final JarInputStream distro; try { distro = fetchDistribution(uri, connection); } catch (Exception e) { Log.e(LOGTAG, "Error fetching distribution from network.", e); recordFetchTelemetry(e); return false; } long end = SystemClock.uptimeMillis(); final long duration = end - start; Log.d(LOGTAG, "Distro fetch took " + duration + "ms; result? " + (distro != null)); Telemetry.addToHistogram( HISTOGRAM_DOWNLOAD_TIME_MS, clamp(MAX_DOWNLOAD_TIME_MSEC, duration)); if (distro == null) { // Nothing to do. return false; } // Try to copy distribution files from the fetched stream. try { Log.d(LOGTAG, "Copying files from fetched zip."); if (copyFilesFromStream(distro)) { // We always copy to the data dir, and we only copy files from // a 'distribution' subdirectory. Now determine our actual distribution directory. return checkDataDistribution(); } } catch (SecurityException e) { Log.e(LOGTAG, "Security exception copying files. Corrupt or malicious?", e); Telemetry.addToHistogram( HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION); } catch (Exception e) { Log.e(LOGTAG, "Error copying files from distribution.", e); Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_EXCEPTION); } finally { distro.close(); } } finally { connection.disconnect(); } } catch (IOException e) { Log.e(LOGTAG, "Error copying distribution files from network.", e); recordFetchTelemetry(e); } return false; }
/** * Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls. Must * not be called from UI or Gecko threads. */ public static Map<String, Map<String, Object>> getForUrls( final ContentResolver cr, final List<String> urls, final List<String> columns) { ThreadUtils.assertNotOnUiThread(); ThreadUtils.assertNotOnGeckoThread(); final Map<String, Map<String, Object>> data = new HashMap<String, Map<String, Object>>(); // Nothing to query for if (urls.isEmpty() || columns.isEmpty()) { Log.e(LOGTAG, "Queried metadata for nothing"); return data; } // Search the cache for any of these urls List<String> urlsToQuery = new ArrayList<String>(); for (String url : urls) { final Map<String, Object> hit = cache.get(url); if (hit != null) { // Cache hit! data.put(url, hit); } else { urlsToQuery.add(url); } } Telemetry.HistogramAdd("FENNEC_TILES_CACHE_HIT", data.size()); // If everything was in the cache, we're done! if (urlsToQuery.size() == 0) { return Collections.unmodifiableMap(data); } final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN); // We need the url to build our final HashMap, so we force it to be included in the query. if (!columns.contains(URLMetadataTable.URL_COLUMN)) { columns.add(URLMetadataTable.URL_COLUMN); } final Cursor cursor = cr.query( URLMetadataTable.CONTENT_URI, columns.toArray(new String[columns.size()]), // columns, selection, // selection urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs null); try { if (!cursor.moveToFirst()) { return Collections.unmodifiableMap(data); } do { final Map<String, Object> metadata = fromCursor(cursor); final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN)); data.put(url, metadata); cache.put(url, metadata); } while (cursor.moveToNext()); } finally { cursor.close(); } return Collections.unmodifiableMap(data); }