| // 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); |