1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2023 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use size file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril'; 16*6dbdd20aSAndroid Build Coastguard Workerimport {allUnique, range} from '../base/array_utils'; 17*6dbdd20aSAndroid Build Coastguard Workerimport { 18*6dbdd20aSAndroid Build Coastguard Worker compareUniversal, 19*6dbdd20aSAndroid Build Coastguard Worker comparingBy, 20*6dbdd20aSAndroid Build Coastguard Worker ComparisonFn, 21*6dbdd20aSAndroid Build Coastguard Worker SortableValue, 22*6dbdd20aSAndroid Build Coastguard Worker SortDirection, 23*6dbdd20aSAndroid Build Coastguard Worker withDirection, 24*6dbdd20aSAndroid Build Coastguard Worker} from '../base/comparison_utils'; 25*6dbdd20aSAndroid Build Coastguard Workerimport {scheduleFullRedraw} from './raf'; 26*6dbdd20aSAndroid Build Coastguard Workerimport {MenuItem, PopupMenu2} from './menu'; 27*6dbdd20aSAndroid Build Coastguard Workerimport {Button} from './button'; 28*6dbdd20aSAndroid Build Coastguard Worker 29*6dbdd20aSAndroid Build Coastguard Worker// For a table column that can be sorted; the standard popup icon should 30*6dbdd20aSAndroid Build Coastguard Worker// reflect the current sorting direction. This function returns an icon 31*6dbdd20aSAndroid Build Coastguard Worker// corresponding to optional SortDirection according to which the column is 32*6dbdd20aSAndroid Build Coastguard Worker// sorted. (Optional because column might be unsorted) 33*6dbdd20aSAndroid Build Coastguard Workerexport function popupMenuIcon(sortDirection?: SortDirection) { 34*6dbdd20aSAndroid Build Coastguard Worker switch (sortDirection) { 35*6dbdd20aSAndroid Build Coastguard Worker case undefined: 36*6dbdd20aSAndroid Build Coastguard Worker return 'more_horiz'; 37*6dbdd20aSAndroid Build Coastguard Worker case 'DESC': 38*6dbdd20aSAndroid Build Coastguard Worker return 'arrow_drop_down'; 39*6dbdd20aSAndroid Build Coastguard Worker case 'ASC': 40*6dbdd20aSAndroid Build Coastguard Worker return 'arrow_drop_up'; 41*6dbdd20aSAndroid Build Coastguard Worker } 42*6dbdd20aSAndroid Build Coastguard Worker} 43*6dbdd20aSAndroid Build Coastguard Worker 44*6dbdd20aSAndroid Build Coastguard Workerexport interface ColumnDescriptorAttrs<T> { 45*6dbdd20aSAndroid Build Coastguard Worker // Context menu items displayed on the column header. 46*6dbdd20aSAndroid Build Coastguard Worker contextMenu?: m.Child[]; 47*6dbdd20aSAndroid Build Coastguard Worker 48*6dbdd20aSAndroid Build Coastguard Worker // Unique column ID, used to identify which column is currently sorted. 49*6dbdd20aSAndroid Build Coastguard Worker columnId?: string; 50*6dbdd20aSAndroid Build Coastguard Worker 51*6dbdd20aSAndroid Build Coastguard Worker // Sorting predicate: if provided, column would be sortable. 52*6dbdd20aSAndroid Build Coastguard Worker ordering?: ComparisonFn<T>; 53*6dbdd20aSAndroid Build Coastguard Worker 54*6dbdd20aSAndroid Build Coastguard Worker // Simpler way to provide a sorting: instead of full predicate, the function 55*6dbdd20aSAndroid Build Coastguard Worker // can map the row for "sorting key" associated with the column. 56*6dbdd20aSAndroid Build Coastguard Worker sortKey?: (value: T) => SortableValue; 57*6dbdd20aSAndroid Build Coastguard Worker} 58*6dbdd20aSAndroid Build Coastguard Worker 59*6dbdd20aSAndroid Build Coastguard Workerexport class ColumnDescriptor<T> { 60*6dbdd20aSAndroid Build Coastguard Worker name: string; 61*6dbdd20aSAndroid Build Coastguard Worker render: (row: T) => m.Child; 62*6dbdd20aSAndroid Build Coastguard Worker id: string; 63*6dbdd20aSAndroid Build Coastguard Worker contextMenu?: m.Child[]; 64*6dbdd20aSAndroid Build Coastguard Worker ordering?: ComparisonFn<T>; 65*6dbdd20aSAndroid Build Coastguard Worker 66*6dbdd20aSAndroid Build Coastguard Worker constructor( 67*6dbdd20aSAndroid Build Coastguard Worker name: string, 68*6dbdd20aSAndroid Build Coastguard Worker render: (row: T) => m.Child, 69*6dbdd20aSAndroid Build Coastguard Worker attrs?: ColumnDescriptorAttrs<T>, 70*6dbdd20aSAndroid Build Coastguard Worker ) { 71*6dbdd20aSAndroid Build Coastguard Worker this.name = name; 72*6dbdd20aSAndroid Build Coastguard Worker this.render = render; 73*6dbdd20aSAndroid Build Coastguard Worker this.id = attrs?.columnId === undefined ? name : attrs.columnId; 74*6dbdd20aSAndroid Build Coastguard Worker 75*6dbdd20aSAndroid Build Coastguard Worker if (attrs === undefined) { 76*6dbdd20aSAndroid Build Coastguard Worker return; 77*6dbdd20aSAndroid Build Coastguard Worker } 78*6dbdd20aSAndroid Build Coastguard Worker 79*6dbdd20aSAndroid Build Coastguard Worker if (attrs.sortKey !== undefined && attrs.ordering !== undefined) { 80*6dbdd20aSAndroid Build Coastguard Worker throw new Error('only one way to order a column should be specified'); 81*6dbdd20aSAndroid Build Coastguard Worker } 82*6dbdd20aSAndroid Build Coastguard Worker 83*6dbdd20aSAndroid Build Coastguard Worker if (attrs.sortKey !== undefined) { 84*6dbdd20aSAndroid Build Coastguard Worker this.ordering = comparingBy(attrs.sortKey, compareUniversal); 85*6dbdd20aSAndroid Build Coastguard Worker } 86*6dbdd20aSAndroid Build Coastguard Worker if (attrs.ordering !== undefined) { 87*6dbdd20aSAndroid Build Coastguard Worker this.ordering = attrs.ordering; 88*6dbdd20aSAndroid Build Coastguard Worker } 89*6dbdd20aSAndroid Build Coastguard Worker } 90*6dbdd20aSAndroid Build Coastguard Worker} 91*6dbdd20aSAndroid Build Coastguard Worker 92*6dbdd20aSAndroid Build Coastguard Workerexport function numberColumn<T>( 93*6dbdd20aSAndroid Build Coastguard Worker name: string, 94*6dbdd20aSAndroid Build Coastguard Worker getter: (t: T) => number, 95*6dbdd20aSAndroid Build Coastguard Worker contextMenu?: m.Child[], 96*6dbdd20aSAndroid Build Coastguard Worker): ColumnDescriptor<T> { 97*6dbdd20aSAndroid Build Coastguard Worker return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter}); 98*6dbdd20aSAndroid Build Coastguard Worker} 99*6dbdd20aSAndroid Build Coastguard Worker 100*6dbdd20aSAndroid Build Coastguard Workerexport function stringColumn<T>( 101*6dbdd20aSAndroid Build Coastguard Worker name: string, 102*6dbdd20aSAndroid Build Coastguard Worker getter: (t: T) => string, 103*6dbdd20aSAndroid Build Coastguard Worker contextMenu?: m.Child[], 104*6dbdd20aSAndroid Build Coastguard Worker): ColumnDescriptor<T> { 105*6dbdd20aSAndroid Build Coastguard Worker return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter}); 106*6dbdd20aSAndroid Build Coastguard Worker} 107*6dbdd20aSAndroid Build Coastguard Worker 108*6dbdd20aSAndroid Build Coastguard Workerexport function widgetColumn<T>( 109*6dbdd20aSAndroid Build Coastguard Worker name: string, 110*6dbdd20aSAndroid Build Coastguard Worker getter: (t: T) => m.Child, 111*6dbdd20aSAndroid Build Coastguard Worker): ColumnDescriptor<T> { 112*6dbdd20aSAndroid Build Coastguard Worker return new ColumnDescriptor<T>(name, getter); 113*6dbdd20aSAndroid Build Coastguard Worker} 114*6dbdd20aSAndroid Build Coastguard Worker 115*6dbdd20aSAndroid Build Coastguard Workerinterface SortingInfo<T> { 116*6dbdd20aSAndroid Build Coastguard Worker columnId: string; 117*6dbdd20aSAndroid Build Coastguard Worker direction: SortDirection; 118*6dbdd20aSAndroid Build Coastguard Worker // TODO(ddrone): figure out if storing this can be avoided. 119*6dbdd20aSAndroid Build Coastguard Worker ordering: ComparisonFn<T>; 120*6dbdd20aSAndroid Build Coastguard Worker} 121*6dbdd20aSAndroid Build Coastguard Worker 122*6dbdd20aSAndroid Build Coastguard Worker// Encapsulated table data, that contains the input to be displayed, as well as 123*6dbdd20aSAndroid Build Coastguard Worker// some helper information to allow sorting. 124*6dbdd20aSAndroid Build Coastguard Workerexport class TableData<T> { 125*6dbdd20aSAndroid Build Coastguard Worker data: T[]; 126*6dbdd20aSAndroid Build Coastguard Worker private _sortingInfo?: SortingInfo<T>; 127*6dbdd20aSAndroid Build Coastguard Worker private permutation: number[]; 128*6dbdd20aSAndroid Build Coastguard Worker 129*6dbdd20aSAndroid Build Coastguard Worker constructor(data: T[]) { 130*6dbdd20aSAndroid Build Coastguard Worker this.data = data; 131*6dbdd20aSAndroid Build Coastguard Worker this.permutation = range(data.length); 132*6dbdd20aSAndroid Build Coastguard Worker } 133*6dbdd20aSAndroid Build Coastguard Worker 134*6dbdd20aSAndroid Build Coastguard Worker *iterateItems(): Generator<T> { 135*6dbdd20aSAndroid Build Coastguard Worker for (const index of this.permutation) { 136*6dbdd20aSAndroid Build Coastguard Worker yield this.data[index]; 137*6dbdd20aSAndroid Build Coastguard Worker } 138*6dbdd20aSAndroid Build Coastguard Worker } 139*6dbdd20aSAndroid Build Coastguard Worker 140*6dbdd20aSAndroid Build Coastguard Worker items(): T[] { 141*6dbdd20aSAndroid Build Coastguard Worker return Array.from(this.iterateItems()); 142*6dbdd20aSAndroid Build Coastguard Worker } 143*6dbdd20aSAndroid Build Coastguard Worker 144*6dbdd20aSAndroid Build Coastguard Worker setItems(newItems: T[]) { 145*6dbdd20aSAndroid Build Coastguard Worker this.data = newItems; 146*6dbdd20aSAndroid Build Coastguard Worker this.permutation = range(newItems.length); 147*6dbdd20aSAndroid Build Coastguard Worker if (this._sortingInfo !== undefined) { 148*6dbdd20aSAndroid Build Coastguard Worker this.reorder(this._sortingInfo); 149*6dbdd20aSAndroid Build Coastguard Worker } 150*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw(); 151*6dbdd20aSAndroid Build Coastguard Worker } 152*6dbdd20aSAndroid Build Coastguard Worker 153*6dbdd20aSAndroid Build Coastguard Worker resetOrder() { 154*6dbdd20aSAndroid Build Coastguard Worker this.permutation = range(this.data.length); 155*6dbdd20aSAndroid Build Coastguard Worker this._sortingInfo = undefined; 156*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw(); 157*6dbdd20aSAndroid Build Coastguard Worker } 158*6dbdd20aSAndroid Build Coastguard Worker 159*6dbdd20aSAndroid Build Coastguard Worker get sortingInfo(): SortingInfo<T> | undefined { 160*6dbdd20aSAndroid Build Coastguard Worker return this._sortingInfo; 161*6dbdd20aSAndroid Build Coastguard Worker } 162*6dbdd20aSAndroid Build Coastguard Worker 163*6dbdd20aSAndroid Build Coastguard Worker reorder(info: SortingInfo<T>) { 164*6dbdd20aSAndroid Build Coastguard Worker this._sortingInfo = info; 165*6dbdd20aSAndroid Build Coastguard Worker this.permutation.sort( 166*6dbdd20aSAndroid Build Coastguard Worker withDirection( 167*6dbdd20aSAndroid Build Coastguard Worker comparingBy((index: number) => this.data[index], info.ordering), 168*6dbdd20aSAndroid Build Coastguard Worker info.direction, 169*6dbdd20aSAndroid Build Coastguard Worker ), 170*6dbdd20aSAndroid Build Coastguard Worker ); 171*6dbdd20aSAndroid Build Coastguard Worker scheduleFullRedraw(); 172*6dbdd20aSAndroid Build Coastguard Worker } 173*6dbdd20aSAndroid Build Coastguard Worker} 174*6dbdd20aSAndroid Build Coastguard Worker 175*6dbdd20aSAndroid Build Coastguard Workerexport interface TableAttrs<T> { 176*6dbdd20aSAndroid Build Coastguard Worker data: TableData<T>; 177*6dbdd20aSAndroid Build Coastguard Worker columns: ColumnDescriptor<T>[]; 178*6dbdd20aSAndroid Build Coastguard Worker} 179*6dbdd20aSAndroid Build Coastguard Worker 180*6dbdd20aSAndroid Build Coastguard Workerfunction directionOnIndex( 181*6dbdd20aSAndroid Build Coastguard Worker columnId: string, 182*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 183*6dbdd20aSAndroid Build Coastguard Worker info?: SortingInfo<any>, 184*6dbdd20aSAndroid Build Coastguard Worker): SortDirection | undefined { 185*6dbdd20aSAndroid Build Coastguard Worker if (info === undefined) { 186*6dbdd20aSAndroid Build Coastguard Worker return undefined; 187*6dbdd20aSAndroid Build Coastguard Worker } 188*6dbdd20aSAndroid Build Coastguard Worker return info.columnId === columnId ? info.direction : undefined; 189*6dbdd20aSAndroid Build Coastguard Worker} 190*6dbdd20aSAndroid Build Coastguard Worker 191*6dbdd20aSAndroid Build Coastguard Worker// eslint-disable-next-line @typescript-eslint/no-explicit-any 192*6dbdd20aSAndroid Build Coastguard Workerexport class Table implements m.ClassComponent<TableAttrs<any>> { 193*6dbdd20aSAndroid Build Coastguard Worker renderColumnHeader( 194*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 195*6dbdd20aSAndroid Build Coastguard Worker vnode: m.Vnode<TableAttrs<any>>, 196*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 197*6dbdd20aSAndroid Build Coastguard Worker column: ColumnDescriptor<any>, 198*6dbdd20aSAndroid Build Coastguard Worker ): m.Child { 199*6dbdd20aSAndroid Build Coastguard Worker let currDirection: SortDirection | undefined = undefined; 200*6dbdd20aSAndroid Build Coastguard Worker 201*6dbdd20aSAndroid Build Coastguard Worker let items = column.contextMenu; 202*6dbdd20aSAndroid Build Coastguard Worker if (column.ordering !== undefined) { 203*6dbdd20aSAndroid Build Coastguard Worker const ordering = column.ordering; 204*6dbdd20aSAndroid Build Coastguard Worker currDirection = directionOnIndex(column.id, vnode.attrs.data.sortingInfo); 205*6dbdd20aSAndroid Build Coastguard Worker const newItems: m.Child[] = []; 206*6dbdd20aSAndroid Build Coastguard Worker if (currDirection !== 'ASC') { 207*6dbdd20aSAndroid Build Coastguard Worker newItems.push( 208*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 209*6dbdd20aSAndroid Build Coastguard Worker label: 'Sort ascending', 210*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 211*6dbdd20aSAndroid Build Coastguard Worker vnode.attrs.data.reorder({ 212*6dbdd20aSAndroid Build Coastguard Worker columnId: column.id, 213*6dbdd20aSAndroid Build Coastguard Worker direction: 'ASC', 214*6dbdd20aSAndroid Build Coastguard Worker ordering, 215*6dbdd20aSAndroid Build Coastguard Worker }); 216*6dbdd20aSAndroid Build Coastguard Worker }, 217*6dbdd20aSAndroid Build Coastguard Worker }), 218*6dbdd20aSAndroid Build Coastguard Worker ); 219*6dbdd20aSAndroid Build Coastguard Worker } 220*6dbdd20aSAndroid Build Coastguard Worker if (currDirection !== 'DESC') { 221*6dbdd20aSAndroid Build Coastguard Worker newItems.push( 222*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 223*6dbdd20aSAndroid Build Coastguard Worker label: 'Sort descending', 224*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 225*6dbdd20aSAndroid Build Coastguard Worker vnode.attrs.data.reorder({ 226*6dbdd20aSAndroid Build Coastguard Worker columnId: column.id, 227*6dbdd20aSAndroid Build Coastguard Worker direction: 'DESC', 228*6dbdd20aSAndroid Build Coastguard Worker ordering, 229*6dbdd20aSAndroid Build Coastguard Worker }); 230*6dbdd20aSAndroid Build Coastguard Worker }, 231*6dbdd20aSAndroid Build Coastguard Worker }), 232*6dbdd20aSAndroid Build Coastguard Worker ); 233*6dbdd20aSAndroid Build Coastguard Worker } 234*6dbdd20aSAndroid Build Coastguard Worker if (currDirection !== undefined) { 235*6dbdd20aSAndroid Build Coastguard Worker newItems.push( 236*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 237*6dbdd20aSAndroid Build Coastguard Worker label: 'Restore original order', 238*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 239*6dbdd20aSAndroid Build Coastguard Worker vnode.attrs.data.resetOrder(); 240*6dbdd20aSAndroid Build Coastguard Worker }, 241*6dbdd20aSAndroid Build Coastguard Worker }), 242*6dbdd20aSAndroid Build Coastguard Worker ); 243*6dbdd20aSAndroid Build Coastguard Worker } 244*6dbdd20aSAndroid Build Coastguard Worker items = [...newItems, ...(items ?? [])]; 245*6dbdd20aSAndroid Build Coastguard Worker } 246*6dbdd20aSAndroid Build Coastguard Worker 247*6dbdd20aSAndroid Build Coastguard Worker return m( 248*6dbdd20aSAndroid Build Coastguard Worker 'td', 249*6dbdd20aSAndroid Build Coastguard Worker column.name, 250*6dbdd20aSAndroid Build Coastguard Worker items && 251*6dbdd20aSAndroid Build Coastguard Worker m( 252*6dbdd20aSAndroid Build Coastguard Worker PopupMenu2, 253*6dbdd20aSAndroid Build Coastguard Worker { 254*6dbdd20aSAndroid Build Coastguard Worker trigger: m(Button, {icon: popupMenuIcon(currDirection)}), 255*6dbdd20aSAndroid Build Coastguard Worker }, 256*6dbdd20aSAndroid Build Coastguard Worker items, 257*6dbdd20aSAndroid Build Coastguard Worker ), 258*6dbdd20aSAndroid Build Coastguard Worker ); 259*6dbdd20aSAndroid Build Coastguard Worker } 260*6dbdd20aSAndroid Build Coastguard Worker 261*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 262*6dbdd20aSAndroid Build Coastguard Worker checkValid(attrs: TableAttrs<any>) { 263*6dbdd20aSAndroid Build Coastguard Worker if (!allUnique(attrs.columns.map((c) => c.id))) { 264*6dbdd20aSAndroid Build Coastguard Worker throw new Error('column IDs should be unique'); 265*6dbdd20aSAndroid Build Coastguard Worker } 266*6dbdd20aSAndroid Build Coastguard Worker } 267*6dbdd20aSAndroid Build Coastguard Worker 268*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 269*6dbdd20aSAndroid Build Coastguard Worker oncreate(vnode: m.VnodeDOM<TableAttrs<any>, this>) { 270*6dbdd20aSAndroid Build Coastguard Worker this.checkValid(vnode.attrs); 271*6dbdd20aSAndroid Build Coastguard Worker } 272*6dbdd20aSAndroid Build Coastguard Worker 273*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 274*6dbdd20aSAndroid Build Coastguard Worker onupdate(vnode: m.VnodeDOM<TableAttrs<any>, this>) { 275*6dbdd20aSAndroid Build Coastguard Worker this.checkValid(vnode.attrs); 276*6dbdd20aSAndroid Build Coastguard Worker } 277*6dbdd20aSAndroid Build Coastguard Worker 278*6dbdd20aSAndroid Build Coastguard Worker // eslint-disable-next-line @typescript-eslint/no-explicit-any 279*6dbdd20aSAndroid Build Coastguard Worker view(vnode: m.Vnode<TableAttrs<any>>): m.Child { 280*6dbdd20aSAndroid Build Coastguard Worker const attrs = vnode.attrs; 281*6dbdd20aSAndroid Build Coastguard Worker 282*6dbdd20aSAndroid Build Coastguard Worker return m( 283*6dbdd20aSAndroid Build Coastguard Worker 'table.generic-table', 284*6dbdd20aSAndroid Build Coastguard Worker m( 285*6dbdd20aSAndroid Build Coastguard Worker 'thead', 286*6dbdd20aSAndroid Build Coastguard Worker m( 287*6dbdd20aSAndroid Build Coastguard Worker 'tr.header', 288*6dbdd20aSAndroid Build Coastguard Worker attrs.columns.map((column) => this.renderColumnHeader(vnode, column)), 289*6dbdd20aSAndroid Build Coastguard Worker ), 290*6dbdd20aSAndroid Build Coastguard Worker ), 291*6dbdd20aSAndroid Build Coastguard Worker attrs.data.items().map((row) => 292*6dbdd20aSAndroid Build Coastguard Worker m( 293*6dbdd20aSAndroid Build Coastguard Worker 'tr', 294*6dbdd20aSAndroid Build Coastguard Worker attrs.columns.map((column) => m('td', column.render(row))), 295*6dbdd20aSAndroid Build Coastguard Worker ), 296*6dbdd20aSAndroid Build Coastguard Worker ), 297*6dbdd20aSAndroid Build Coastguard Worker ); 298*6dbdd20aSAndroid Build Coastguard Worker } 299*6dbdd20aSAndroid Build Coastguard Worker} 300