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 {getDefaultRecordingTargets, RecordingTarget} from '../common/state'; 16import { 17 createEmptyRecordConfig, 18 NamedRecordConfig, 19 namedRecordConfigValidator, 20 RecordConfig, 21 recordConfigValidator 22} from '../controller/record_config_types'; 23import {runValidator, ValidationResult} from '../controller/validators'; 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 save(recordConfig: RecordConfig, title?: string): void { 46 // We reload from local storage in case of concurrent 47 // modifications of local storage from a different tab. 48 this.reloadFromLocalStorage(); 49 50 const savedTitle = title ? title : new Date().toJSON(); 51 const config: NamedRecordConfig = { 52 title: savedTitle, 53 config: recordConfig, 54 key: new Date().toJSON() 55 }; 56 57 this.recordConfigs.push({result: config, invalidKeys: [], extraKeys: []}); 58 this.recordConfigNames.add(savedTitle); 59 60 this._save(); 61 } 62 63 overwrite(recordConfig: RecordConfig, key: string) { 64 // We reload from local storage in case of concurrent 65 // modifications of local storage from a different tab. 66 this.reloadFromLocalStorage(); 67 68 const found = this.recordConfigs.find((e) => e.result.key === key); 69 if (found === undefined) { 70 throw new Error('trying to overwrite non-existing config'); 71 } 72 73 found.result.config = recordConfig; 74 75 this._save(); 76 } 77 78 delete(key: string): void { 79 // We reload from local storage in case of concurrent 80 // modifications of local storage from a different tab. 81 this.reloadFromLocalStorage(); 82 83 let idx = -1; 84 for (let i = 0; i < this.recordConfigs.length; ++i) { 85 if (this.recordConfigs[i].result.key === key) { 86 idx = i; 87 break; 88 } 89 } 90 91 if (idx !== -1) { 92 this.recordConfigNames.delete(this.recordConfigs[idx].result.title); 93 this.recordConfigs.splice(idx, 1); 94 this._save(); 95 } else { 96 // TODO(bsebastien): Show a warning message to the user in the UI. 97 console.warn('The config selected doesn\'t exist any more'); 98 } 99 } 100 101 private clearRecordConfigs(): void { 102 this.recordConfigs = []; 103 this.recordConfigNames.clear(); 104 this._save(); 105 } 106 107 reloadFromLocalStorage(): void { 108 const configsLocalStorage = 109 window.localStorage.getItem(LOCAL_STORAGE_RECORD_CONFIGS_KEY); 110 111 if (configsLocalStorage) { 112 this.recordConfigNames.clear(); 113 114 try { 115 const validConfigLocalStorage: 116 Array<ValidationResult<NamedRecordConfig>> = []; 117 const parsedConfigsLocalStorage = JSON.parse(configsLocalStorage); 118 119 // Check if it's an array. 120 if (!Array.isArray(parsedConfigsLocalStorage)) { 121 this.clearRecordConfigs(); 122 return; 123 } 124 125 for (let i = 0; i < parsedConfigsLocalStorage.length; ++i) { 126 try { 127 validConfigLocalStorage.push(runValidator( 128 namedRecordConfigValidator, parsedConfigsLocalStorage[i])); 129 } catch { 130 // Parsing failed with unrecoverable error (e.g. title or key are 131 // missing), ignore the result. 132 console.log( 133 'Validation of saved record config has failed: ' + 134 JSON.stringify(parsedConfigsLocalStorage[i])); 135 } 136 } 137 138 this.recordConfigs = validConfigLocalStorage; 139 this._save(); 140 } catch (e) { 141 this.clearRecordConfigs(); 142 } 143 } else { 144 this.clearRecordConfigs(); 145 } 146 } 147 148 canSave(title: string) { 149 return !this.recordConfigNames.has(title); 150 } 151} 152 153// This class is a singleton to avoid many instances 154// conflicting as they attempt to edit localStorage. 155export const recordConfigStore = new RecordConfigStore(); 156 157export class AutosaveConfigStore { 158 config: RecordConfig; 159 160 // Whether the current config is a default one or has been saved before. 161 // Used to determine whether the button to load "last started config" should 162 // be present in the recording profiles list. 163 hasSavedConfig: boolean; 164 165 constructor() { 166 this.hasSavedConfig = false; 167 this.config = createEmptyRecordConfig(); 168 const savedItem = 169 window.localStorage.getItem(LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY); 170 if (savedItem === null) { 171 return; 172 } 173 const parsed = JSON.parse(savedItem); 174 if (parsed !== null && typeof parsed === 'object') { 175 this.config = runValidator(recordConfigValidator, parsed).result; 176 this.hasSavedConfig = true; 177 } 178 } 179 180 get(): RecordConfig { 181 return this.config; 182 } 183 184 save(newConfig: RecordConfig) { 185 window.localStorage.setItem( 186 LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY, JSON.stringify(newConfig)); 187 this.config = newConfig; 188 this.hasSavedConfig = true; 189 } 190} 191 192export const autosaveConfigStore = new AutosaveConfigStore(); 193 194export class RecordTargetStore { 195 recordTargetOS: string|null; 196 197 constructor() { 198 this.recordTargetOS = 199 window.localStorage.getItem(LOCAL_STORAGE_RECORD_TARGET_OS_KEY); 200 } 201 202 get(): string|null { 203 return this.recordTargetOS; 204 } 205 206 getValidTarget(): RecordingTarget { 207 const validTargets = getDefaultRecordingTargets(); 208 const savedOS = this.get(); 209 210 const validSavedTarget = validTargets.find((el) => el.os === savedOS); 211 return validSavedTarget || validTargets[0]; 212 } 213 214 save(newTargetOS: string) { 215 window.localStorage.setItem( 216 LOCAL_STORAGE_RECORD_TARGET_OS_KEY, newTargetOS); 217 this.recordTargetOS = newTargetOS; 218 } 219} 220 221export const recordTargetStore = new RecordTargetStore(); 222