| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import './diagnose_info_table.js'; |
| |
| import {CustomElement} from '//resources/js/custom_element.js'; |
| import type {Time, TimeDelta} from '//resources/mojo/mojo/public/mojom/base/time.mojom-webui.js'; |
| |
| import type {DiagnoseInfoTableElement} from './diagnose_info_table.js'; |
| import {getTemplate} from './diagnose_info_view.html.js'; |
| import type {AccessPointData, GeolocationDiagnostics, NetworkLocationDiagnostics, NetworkLocationResponse, PositionCacheDiagnostics, WifiPollingPolicyDiagnostics} from './geolocation_internals.mojom-webui.js'; |
| import {INVALID_CHANNEL, INVALID_RADIO_SIGNAL_STRENGTH, INVALID_SIGNAL_TO_NOISE} from './geolocation_internals.mojom-webui.js'; |
| import type {GeopositionResult} from './geoposition.mojom-webui.js'; |
| import {BAD_ACCURACY, BAD_ALTITUDE, BAD_HEADING, BAD_LATITUDE_LONGITUDE, BAD_SPEED} from './geoposition.mojom-webui.js'; |
| |
| export const PROVIDER_STATE_TABLE_ID = 'provider-state-table'; |
| const PROVIDER_STATE_ENUM: {[key: number]: string} = { |
| 0: 'Stop', |
| 1: 'High Accuracy', |
| 2: 'Low Accuracy', |
| 3: 'Blocked By System Permission', |
| }; |
| export const LOCATION_PROVIDER_MANAGER_MODE_TABLE_ID = |
| 'location-provider-manager-mode-table'; |
| const LOCATION_PROVIDER_MANAGER_MODE_ENUM: {[key: number]: string} = { |
| 0: 'kNetworkOnly', |
| 1: 'kPlatformOnly', |
| 2: 'kCustomOnly', |
| 3: 'kHybridPlatform', |
| 4: 'kHybridFallbackNetwork', |
| 5: 'kHybridPlatform2', |
| }; |
| export const WATCH_TABLE_ID = 'watch-position-table'; |
| export const WIFI_DATA_TABLE_ID = 'wifi-data-table'; |
| export const POSITION_CACHE_TABLE_ID = 'position-cache-table'; |
| export const WIFI_POLLING_POLICY_TABLE_ID = 'wifi-polling-policy-table'; |
| export const LAST_NETWORK_REQUEST_TABLE_ID = 'last-network-request-table'; |
| export const LAST_NETWORK_RESPONSE_TABLE_ID = 'last-network-response-table'; |
| |
| // Converts `mojoTime` from `mojom_base.mojom.Time` to `Date`. |
| function mojoTimeToDate(mojoTime: Time) { |
| // The Javascript `Date()` is based off of the number of milliseconds since |
| // the UNIX epoch (1970-01-01 00::00:00 UTC), while `internalValue`` |
| // of the `base::Time` (represented in mojom.Time) represents the |
| // number of microseconds since the Windows FILETIME epoch |
| // (1601-01-01 00:00:00 UTC). This computes the final Javascript time by |
| // computing the epoch delta and the conversion from microseconds to |
| // milliseconds. |
| const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0); |
| const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0); |
| // `epochDeltaInMs` is equal to `base::Time::kTimeTToMicrosecondsOffset`. |
| const epochDeltaInMs = unixEpoch - windowsEpoch; |
| const timeInMs = Number(mojoTime.internalValue) / 1000; |
| return new Date(timeInMs - epochDeltaInMs); |
| } |
| |
| // Returns a string representation of `mojoTime`. |
| function stringifyMojoTime(mojoTime: Time|null) { |
| if (!mojoTime) { |
| return 'None'; |
| } |
| return mojoTimeToDate(mojoTime).toLocaleString(); |
| } |
| |
| // Returns a string representation of `mojoGeopositionResult`. |
| function stringifyMojoGeopositionResult( |
| mojoGeopositionResult: GeopositionResult|null) { |
| if (!mojoGeopositionResult) { |
| return 'None'; |
| } |
| if ('position' in mojoGeopositionResult) { |
| const mojoGeoposition = mojoGeopositionResult.position; |
| if (mojoGeoposition!.latitude === BAD_LATITUDE_LONGITUDE || |
| mojoGeoposition!.longitude === BAD_LATITUDE_LONGITUDE) { |
| return 'Invalid geoposition'; |
| } |
| const components = []; |
| let latLong = |
| `${mojoGeoposition!.latitude}°, ${mojoGeoposition!.longitude}°`; |
| if (mojoGeoposition!.accuracy !== BAD_ACCURACY) { |
| latLong += ` ±${mojoGeoposition!.accuracy} m`; |
| } |
| components.push(latLong); |
| if (mojoGeoposition!.altitude !== BAD_ALTITUDE) { |
| let altitude = `${mojoGeoposition!.altitude} m`; |
| if (mojoGeoposition!.altitudeAccuracy !== BAD_ACCURACY) { |
| altitude += ` ±${mojoGeoposition!.altitudeAccuracy} m`; |
| } |
| components.push(altitude); |
| } |
| if (mojoGeoposition!.heading !== BAD_HEADING) { |
| components.push(`${mojoGeoposition!.heading}°`); |
| } |
| if (mojoGeoposition!.speed !== BAD_SPEED) { |
| components.push(`${mojoGeoposition!.speed} m/s`); |
| } |
| components.push(stringifyMojoTime(mojoGeoposition!.timestamp)); |
| return components.join('; '); |
| } |
| if ('error' in mojoGeopositionResult) { |
| const mojoGeopositionError = mojoGeopositionResult.error; |
| return `${mojoGeopositionError!.errorMessage} (${ |
| mojoGeopositionError!.errorCode})`; |
| } |
| return 'Invalid result'; |
| } |
| |
| // Return a string representation of `TimeDelta` in second. |
| function stringifyMojoTimeDelta(mojoTime: TimeDelta|undefined) { |
| if (!mojoTime) { |
| return 'None'; |
| } |
| return `${Number(mojoTime.microseconds) / 1000000}`; |
| } |
| |
| export class DiagnoseInfoViewElement extends CustomElement { |
| static get is() { |
| return 'diagnose-info-view'; |
| } |
| |
| static override get template() { |
| return getTemplate(); |
| } |
| |
| watchPositionSuccess = (position: GeolocationPosition) => { |
| const data: Record<string, string> = {}; |
| data['timestamp'] = new Date(position.timestamp).toLocaleString(); |
| |
| for (const key in position.coords) { |
| const value = position.coords[key as keyof GeolocationCoordinates]; |
| if (typeof value === 'number' || typeof value === 'string') { |
| data[key] = value.toString(); |
| } |
| } |
| this.updateWatchPositionTable(data); |
| }; |
| |
| watchPositionError = (error: GeolocationPositionError) => { |
| const data: Record<string, string> = {}; |
| data['timestamp'] = new Date().toLocaleString(); |
| data['fail reason'] = `${error.message}, code: ${error.code}`; |
| this.updateWatchPositionTable(data); |
| }; |
| |
| private providerStateTable_: DiagnoseInfoTableElement; |
| private locationProviderManagerModeTable_: DiagnoseInfoTableElement; |
| private wifiDataTable_: DiagnoseInfoTableElement; |
| private positionCacheTable_: DiagnoseInfoTableElement; |
| private watchPositionTable_: DiagnoseInfoTableElement; |
| private wifiPollingPolicyTable_: DiagnoseInfoTableElement; |
| private lastNetworkRequestTable_: DiagnoseInfoTableElement; |
| private lastNetworkResponseTable_: DiagnoseInfoTableElement; |
| |
| constructor() { |
| super(); |
| this.providerStateTable_ = |
| this.getRequiredElement<DiagnoseInfoTableElement>( |
| `#${PROVIDER_STATE_TABLE_ID}`); |
| this.locationProviderManagerModeTable_ = |
| this.getRequiredElement<DiagnoseInfoTableElement>( |
| `#${LOCATION_PROVIDER_MANAGER_MODE_TABLE_ID}`); |
| this.wifiDataTable_ = this.getRequiredElement<DiagnoseInfoTableElement>( |
| `#${WIFI_DATA_TABLE_ID}`); |
| this.positionCacheTable_ = |
| this.getRequiredElement<DiagnoseInfoTableElement>( |
| `#${POSITION_CACHE_TABLE_ID}`); |
| this.watchPositionTable_ = |
| this.getRequiredElement<DiagnoseInfoTableElement>(`#${WATCH_TABLE_ID}`); |
| this.wifiPollingPolicyTable_ = |
| this.getRequiredElement<DiagnoseInfoTableElement>( |
| `#${WIFI_POLLING_POLICY_TABLE_ID}`); |
| this.lastNetworkRequestTable_ = |
| this.getRequiredElement<DiagnoseInfoTableElement>( |
| `#${LAST_NETWORK_REQUEST_TABLE_ID}`); |
| this.lastNetworkResponseTable_ = |
| this.getRequiredElement<DiagnoseInfoTableElement>( |
| `#${LAST_NETWORK_RESPONSE_TABLE_ID}`); |
| } |
| |
| updateDiagnosticsTables(data: GeolocationDiagnostics) { |
| this.updateProviderState(data.providerState); |
| this.updateLocationProviderManagerMode(data.locationProviderManagerMode); |
| this.updateNetworkLocationDiagnostics(data.networkLocationDiagnostics); |
| this.updatePositionCacheDiagnostics(data.positionCacheDiagnostics); |
| this.updateWifiPollingPolicyTable(data.wifiPollingPolicyDiagnostics); |
| } |
| |
| updateLastNetworkRequestTable(request: AccessPointData[]) { |
| this.updateLastNetworkRequest(request); |
| } |
| |
| updateLastNetworkResponseTable(response: NetworkLocationResponse|null) { |
| this.updateLastNetworkResponse(response); |
| } |
| |
| updateProviderState(providerState: number) { |
| let providerStateString = PROVIDER_STATE_ENUM[providerState]; |
| if (providerStateString === undefined) { |
| providerStateString = 'Invalid state'; |
| } |
| this.providerStateTable_.updateTable( |
| PROVIDER_STATE_TABLE_ID, [{'Provider State': providerStateString}]); |
| } |
| |
| updateLocationProviderManagerMode(locationProviderManagerMode: number|null) { |
| if (locationProviderManagerMode === null) { |
| return; |
| } |
| let locationProviderManagerModeString = |
| LOCATION_PROVIDER_MANAGER_MODE_ENUM[locationProviderManagerMode]; |
| if (locationProviderManagerModeString === undefined) { |
| locationProviderManagerModeString = 'Invalid state'; |
| } |
| this.locationProviderManagerModeTable_.updateTable( |
| LOCATION_PROVIDER_MANAGER_MODE_TABLE_ID, [ |
| {'Location Provider Manager Mode': locationProviderManagerModeString}, |
| ]); |
| } |
| |
| accessPointDataToRecordArray(data: AccessPointData[]) { |
| const tableData: Array<Record<string, string>> = []; |
| for (const accessPointData of data) { |
| const row: Record<string, string> = {}; |
| row['MAC address'] = accessPointData.macAddress; |
| if (accessPointData.radioSignalStrength === |
| INVALID_RADIO_SIGNAL_STRENGTH) { |
| row['Signal strength'] = 'N/A'; |
| } else { |
| row['Signal strength'] = `${accessPointData.radioSignalStrength} dBm`; |
| } |
| if (accessPointData.channel === INVALID_CHANNEL) { |
| row['Channel'] = 'N/A'; |
| } else { |
| row['Channel'] = accessPointData.channel.toString(); |
| } |
| if (accessPointData.signalToNoise === INVALID_SIGNAL_TO_NOISE) { |
| row['Signal to Noise Ratio'] = 'N/A'; |
| } else { |
| row['Signal to Noise Ratio'] = `${accessPointData.signalToNoise} dB`; |
| } |
| if (accessPointData.timestamp) { |
| row['Timestamp'] = stringifyMojoTime(accessPointData.timestamp); |
| } else { |
| row['Timestamp'] = 'N/A'; |
| } |
| tableData.push(row); |
| } |
| if (tableData.length === 0) { |
| const row: Record<string, string> = {}; |
| row['MAC address'] = 'No access point data'; |
| row['Signal strength'] = ''; |
| row['Channel'] = ''; |
| row['Signal to Noise Ratio'] = ''; |
| row['Timestamp'] = ''; |
| tableData.push(row); |
| } |
| return tableData; |
| } |
| |
| updateNetworkLocationDiagnostics(networkLocationDiagnostics: |
| NetworkLocationDiagnostics|null) { |
| if (!networkLocationDiagnostics) { |
| this.wifiDataTable_.hideTable(); |
| return; |
| } |
| let wifiData: Array<Record<string, string>> = []; |
| if (networkLocationDiagnostics.accessPointData !== null) { |
| wifiData = this.accessPointDataToRecordArray( |
| networkLocationDiagnostics.accessPointData); |
| } |
| let footerMessage; |
| if (networkLocationDiagnostics.wifiTimestamp === null) { |
| footerMessage = 'No Wi-Fi data received'; |
| } else { |
| footerMessage = `Wi-Fi data last received ${ |
| stringifyMojoTime(networkLocationDiagnostics.wifiTimestamp)}`; |
| } |
| this.wifiDataTable_.updateTable( |
| WIFI_DATA_TABLE_ID, wifiData, footerMessage); |
| } |
| |
| updatePositionCacheDiagnostics(positionCacheDiagnostics: |
| PositionCacheDiagnostics|null) { |
| if (!positionCacheDiagnostics) { |
| this.positionCacheTable_.hideTable(); |
| return; |
| } |
| const row: Record<string, string> = {}; |
| row['Cache size'] = positionCacheDiagnostics.cacheSize.toString(); |
| row['Last cache hit'] = stringifyMojoTime(positionCacheDiagnostics.lastHit); |
| row['Last cache miss'] = |
| stringifyMojoTime(positionCacheDiagnostics.lastMiss); |
| if (!positionCacheDiagnostics.hitRate) { |
| row['Cache hit rate'] = 'N/A'; |
| } else { |
| row['Cache hit rate'] = `${positionCacheDiagnostics.hitRate * 100}%`; |
| } |
| row['Last result'] = stringifyMojoGeopositionResult( |
| positionCacheDiagnostics.lastNetworkResult); |
| this.positionCacheTable_.updateTable(POSITION_CACHE_TABLE_ID, [row]); |
| } |
| |
| updateWatchPositionTable(data: Record<string, string>) { |
| const footerMessage = `Last updated ${new Date().toLocaleString()}`; |
| this.watchPositionTable_.updateTable(WATCH_TABLE_ID, [data], footerMessage); |
| } |
| |
| updateWifiPollingPolicyTable(data: WifiPollingPolicyDiagnostics|null) { |
| if (!data) { |
| this.wifiPollingPolicyTable_.hideTable(); |
| return; |
| } |
| const row: Record<string, string> = {}; |
| row['Interval start time'] = stringifyMojoTime(data.intervalStart); |
| row['Interval duration (sec)'] = |
| stringifyMojoTimeDelta(data.intervalDuration); |
| row['Polling interval (sec)'] = |
| stringifyMojoTimeDelta(data.pollingInterval); |
| row['Default interval (sec)'] = |
| stringifyMojoTimeDelta(data.defaultInterval); |
| row['No change interval (sec)'] = |
| stringifyMojoTimeDelta(data.noChangeInterval); |
| row['Two no change interval (sec)'] = |
| stringifyMojoTimeDelta(data.twoNoChangeInterval); |
| row['No Wi-Fi interval (sec)'] = |
| stringifyMojoTimeDelta(data.noWifiInterval); |
| this.wifiPollingPolicyTable_.updateTable( |
| WIFI_POLLING_POLICY_TABLE_ID, [row]); |
| } |
| |
| updateLastNetworkRequest(request: AccessPointData[]) { |
| const tableData = this.accessPointDataToRecordArray(request); |
| const footerMessage = `Request sent at ${new Date().toLocaleString()}`; |
| this.lastNetworkRequestTable_.updateTable( |
| LAST_NETWORK_REQUEST_TABLE_ID, tableData, footerMessage); |
| } |
| |
| updateLastNetworkResponse(response: NetworkLocationResponse|null) { |
| let positionEstimate; |
| let footerMessage = `Response received at ${new Date().toLocaleString()}`; |
| if (response) { |
| positionEstimate = `${response.latitude}°, ${response.longitude}°`; |
| if (response.accuracy) { |
| positionEstimate += ` ±${response.accuracy} m`; |
| } |
| } else { |
| positionEstimate = 'None'; |
| footerMessage += ' with no position estimate'; |
| } |
| const row: Record<string, string> = {}; |
| row['Position estimate'] = positionEstimate; |
| const tableData: Array<Record<string, string>> = []; |
| tableData.push(row); |
| this.lastNetworkResponseTable_.updateTable( |
| LAST_NETWORK_RESPONSE_TABLE_ID, tableData, footerMessage); |
| } |
| |
| outputTables(): Record<string, any> { |
| const tables = this.$all('diagnose-info-table'); |
| const output: Record<string, any> = {}; |
| output['LocationInternals'] = []; |
| for (const table of tables) { |
| if (!table.visible()) { |
| continue; |
| } |
| output['LocationInternals'].push(table.outputTable()); |
| } |
| return output; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'diagnose-info-view': DiagnoseInfoViewElement; |
| } |
| } |
| |
| customElements.define(DiagnoseInfoViewElement.is, DiagnoseInfoViewElement); |