chromium/chrome/browser/resources/location_internals/diagnose_info_view.ts

// 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 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 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.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.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}]);
  }

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