1// Copyright (C) 2020 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {runValidator, ValidationResult} from '../base/validators'; 16import {getDefaultRecordingTargets, RecordingTarget} from '../common/state'; 17import { 18 createEmptyRecordConfig, 19 NamedRecordConfig, 20 namedRecordConfigValidator, 21 RecordConfig, 22 recordConfigValidator, 23} from '../controller/record_config_types'; 24 25const LOCAL_STORAGE_RECORD_CONFIGS_KEY = 'recordConfigs'; 26const LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY = 'autosaveConfig'; 27const LOCAL_STORAGE_RECORD_TARGET_OS_KEY = 'recordTargetOS'; 28 29export class RecordConfigStore { 30 recordConfigs: Array<ValidationResult<NamedRecordConfig>>; 31 recordConfigNames: Set<string>; 32 33 constructor() { 34 this.recordConfigs = []; 35 this.recordConfigNames = new Set(); 36 this.reloadFromLocalStorage(); 37 } 38 39 private _save() { 40 window.localStorage.setItem( 41 LOCAL_STORAGE_RECORD_CONFIGS_KEY, 42 JSON.stringify(this.recordConfigs.map((x) => x.result)), 43 ); 44 } 45 46 save(recordConfig: RecordConfig, title?: string): void { 47 // We reload from local storage in case of concurrent 48 // modifications of local storage from a different tab. 49 this.reloadFromLocalStorage(); 50 51 const savedTitle = title ? title : new Date().toJSON(); 52 const config: NamedRecordConfig = { 53 title: savedTitle, 54 config: recordConfig, 55 key: new Date().toJSON(), 56 }; 57 58 this.recordConfigs.push({result: config, invalidKeys: [], extraKeys: []}); 59 this.recordConfigNames.add(savedTitle); 60 61 this._save(); 62 } 63 64 overwrite(recordConfig: RecordConfig, key: string) { 65 // We reload from local storage in case of concurrent 66 // modifications of local storage from a different tab. 67 this.reloadFromLocalStorage(); 68 69 const found = this.recordConfigs.find((e) => e.result.key === key); 70 if (found === undefined) { 71 throw new Error('trying to overwrite non-existing config'); 72 } 73 74 found.result.config = recordConfig; 75 76 this._save(); 77 } 78 79 delete(key: string): void { 80 // We reload from local storage in case of concurrent 81 // modifications of local storage from a different tab. 82 this.reloadFromLocalStorage(); 83 84 let idx = -1; 85 for (let i = 0; i < this.recordConfigs.length; ++i) { 86 if (this.recordConfigs[i].result.key === key) { 87 idx = i; 88 break; 89 } 90 } 91 92 if (idx !== -1) { 93 this.recordConfigNames.delete(this.recordConfigs[idx].result.title); 94 this.recordConfigs.splice(idx, 1); 95 this._save(); 96 } else { 97 // TODO(bsebastien): Show a warning message to the user in the UI. 98 console.warn("The config selected doesn't exist any more"); 99 } 100 } 101 102 private clearRecordConfigs(): void { 103 this.recordConfigs = []; 104 this.recordConfigNames.clear(); 105 this._save(); 106 } 107 108 reloadFromLocalStorage(): void { 109 const configsLocalStorage = window.localStorage.getItem( 110 LOCAL_STORAGE_RECORD_CONFIGS_KEY, 111 ); 112 113 if (configsLocalStorage) { 114 this.recordConfigNames.clear(); 115 116 try { 117 const validConfigLocalStorage: Array< 118 ValidationResult<NamedRecordConfig> 119 > = []; 120 const parsedConfigsLocalStorage = JSON.parse(configsLocalStorage); 121 122 // Check if it's an array. 123 if (!Array.isArray(parsedConfigsLocalStorage)) { 124 this.clearRecordConfigs(); 125 return; 126 } 127 128 for (let i = 0; i < parsedConfigsLocalStorage.length; ++i) { 129 try { 130 validConfigLocalStorage.push( 131 runValidator( 132 namedRecordConfigValidator, 133 parsedConfigsLocalStorage[i], 134 ), 135 ); 136 } catch { 137 // Parsing failed with unrecoverable error (e.g. title or key are 138 // missing), ignore the result. 139 console.log( 140 'Validation of saved record config has failed: ' + 141 JSON.stringify(parsedConfigsLocalStorage[i]), 142 ); 143 } 144 } 145 146 this.recordConfigs = validConfigLocalStorage; 147 this._save(); 148 } catch (e) { 149 this.clearRecordConfigs(); 150 } 151 } else { 152 this.clearRecordConfigs(); 153 } 154 } 155 156 canSave(title: string) { 157 return !this.recordConfigNames.has(title); 158 } 159} 160 161// This class is a singleton to avoid many instances 162// conflicting as they attempt to edit localStorage. 163export const recordConfigStore = new RecordConfigStore(); 164 165export class AutosaveConfigStore { 166 config: RecordConfig; 167 168 // Whether the current config is a default one or has been saved before. 169 // Used to determine whether the button to load "last started config" should 170 // be present in the recording profiles list. 171 hasSavedConfig: boolean; 172 173 constructor() { 174 this.hasSavedConfig = false; 175 this.config = createEmptyRecordConfig(); 176 const savedItem = window.localStorage.getItem( 177 LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY, 178 ); 179 if (savedItem === null) { 180 return; 181 } 182 const parsed = JSON.parse(savedItem); 183 if (parsed !== null && typeof parsed === 'object') { 184 this.config = runValidator(recordConfigValidator, parsed).result; 185 this.hasSavedConfig = true; 186 } 187 } 188 189 get(): RecordConfig { 190 return this.config; 191 } 192 193 save(newConfig: RecordConfig) { 194 window.localStorage.setItem( 195 LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY, 196 JSON.stringify(newConfig), 197 ); 198 this.config = newConfig; 199 this.hasSavedConfig = true; 200 } 201} 202 203export const autosaveConfigStore = new AutosaveConfigStore(); 204 205export class RecordTargetStore { 206 recordTargetOS: string | null; 207 208 constructor() { 209 this.recordTargetOS = window.localStorage.getItem( 210 LOCAL_STORAGE_RECORD_TARGET_OS_KEY, 211 ); 212 } 213 214 get(): string | null { 215 return this.recordTargetOS; 216 } 217 218 getValidTarget(): RecordingTarget { 219 const validTargets = getDefaultRecordingTargets(); 220 const savedOS = this.get(); 221 222 const validSavedTarget = validTargets.find((el) => el.os === savedOS); 223 return validSavedTarget || validTargets[0]; 224 } 225 226 save(newTargetOS: string) { 227 window.localStorage.setItem( 228 LOCAL_STORAGE_RECORD_TARGET_OS_KEY, 229 newTargetOS, 230 ); 231 this.recordTargetOS = newTargetOS; 232 } 233} 234 235export const recordTargetStore = new RecordTargetStore(); 236