1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.garagemode; 18 19 import android.car.builtin.os.BuildHelper; 20 import android.car.builtin.util.Slogf; 21 import android.util.AtomicFile; 22 23 import com.android.car.CarLocalServices; 24 import com.android.car.internal.util.IndentingPrintWriter; 25 import com.android.car.systeminterface.SystemInterface; 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import java.io.BufferedReader; 29 import java.io.File; 30 import java.io.FileOutputStream; 31 import java.io.IOException; 32 import java.io.InputStreamReader; 33 import java.io.PrintWriter; 34 import java.io.StringWriter; 35 import java.nio.charset.StandardCharsets; 36 import java.sql.Date; 37 import java.text.SimpleDateFormat; 38 import java.time.Clock; 39 import java.util.Locale; 40 41 /** 42 * GarageModeRecorder is saving Garage mode start/finish times. 43 * Information is stored in plain text file and printed to car_service dumpsys. 44 */ 45 public final class GarageModeRecorder { 46 @VisibleForTesting 47 static final String GARAGE_MODE_RECORDING_FILE_NAME = "GarageModeSession.txt"; 48 @VisibleForTesting 49 static final String SESSION_START_TIME = "Session start time: "; 50 @VisibleForTesting 51 static final String SESSION_FINISH_TIME = "Session finish time: "; 52 @VisibleForTesting 53 static final String SESSION_DURATION = "Session duration: "; 54 @VisibleForTesting 55 static final String TIME_UNIT_MS = " ms"; 56 @VisibleForTesting 57 static final String SESSION_WAS_CANCELLED = "Session was cancelled : "; 58 @VisibleForTesting 59 static final String DATE_FORMAT = "HH:mm:ss.SSS z MM/dd/yyyy"; 60 @VisibleForTesting 61 static final String GARAGE_MODE_RECORDER_IS_SACTIVE = "GarageModeRecorder is %sactive\n"; 62 @VisibleForTesting 63 static final String NOT = "not "; 64 65 private static final String GARAGEMODE_DIR_NAME = "garagemode"; 66 private static final String TAG = "GarageModeRecorder"; 67 private static final int EVENT_SESSION_START = 1; 68 private static final int EVENT_SESSION_FINISH = 2; 69 private static final int EVENT_SESSION_CANCELLED = 3; 70 private static final String FALLBACK_CAR_DIR_PATH = "/data/system/car"; 71 72 private final SimpleDateFormat mDateFormat; 73 private final AtomicFile mGarageModeRecorderFile; 74 private final Clock mClock; 75 private long mSessionStartTime; 76 private long mSessionFinishTime; 77 GarageModeRecorder(Clock clock)78 public GarageModeRecorder(Clock clock) { 79 mDateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.getDefault()); 80 mClock = clock; 81 SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class); 82 File systemCarDir = systemInterface == null ? new File(FALLBACK_CAR_DIR_PATH) 83 : systemInterface.getSystemCarDir(); 84 File garageModeDir = new File(systemCarDir, GARAGEMODE_DIR_NAME); 85 garageModeDir.mkdirs(); 86 87 mGarageModeRecorderFile = new AtomicFile( 88 new File(garageModeDir, GARAGE_MODE_RECORDING_FILE_NAME)); 89 } 90 91 /** 92 * Prints garage mode session history to the {@code dumpsys}. 93 */ dump(IndentingPrintWriter writer)94 public void dump(IndentingPrintWriter writer) { 95 //TODO(b/223241457) Add dump to protobuf 96 writer.printf(GARAGE_MODE_RECORDER_IS_SACTIVE, (isRecorderEnabled() ? "" : NOT)); 97 98 if (!isRecorderEnabled()) return; 99 100 readFileToWriter(writer); 101 } 102 103 /** 104 * Saves information about start of the Garage mode. 105 */ startSession()106 public void startSession() { 107 if (!isRecorderEnabled()) return; 108 109 if (mSessionStartTime != 0) { 110 Slogf.e(TAG, "Error, garage mode session is started twice, previous start - %s", 111 mDateFormat.format(mSessionStartTime)); 112 return; 113 } 114 115 mSessionStartTime = mClock.millis(); 116 recordEvent(EVENT_SESSION_START); 117 } 118 119 /** 120 * Saves information about finish of the Garage mode. 121 */ finishSession()122 public void finishSession() { 123 if (!isRecorderEnabled()) return; 124 125 if (mSessionStartTime == 0) { 126 Slogf.e(TAG, "Error, garage mode session finish called without start"); 127 return; 128 } 129 130 mSessionFinishTime = mClock.millis(); 131 recordEvent(EVENT_SESSION_FINISH); 132 cleanupRecorder(); 133 } 134 135 /** 136 * Save information about cancellation of the Garage mode. 137 */ cancelSession()138 public void cancelSession() { 139 if (!isRecorderEnabled()) return; 140 141 if (mSessionStartTime == 0) { 142 Slogf.e(TAG, "Error, garage mode session cancel called without start"); 143 return; 144 } 145 146 mSessionFinishTime = mClock.millis(); 147 recordEvent(EVENT_SESSION_CANCELLED); 148 cleanupRecorder(); 149 } 150 cleanupRecorder()151 private void cleanupRecorder() { 152 mSessionStartTime = 0; 153 mSessionFinishTime = 0; 154 } 155 156 // recording is not available on user builds 157 @VisibleForTesting isRecorderEnabled()158 boolean isRecorderEnabled() { 159 return !BuildHelper.isUserBuild(); 160 } 161 writeToSessionFile(String buffer, boolean append)162 private void writeToSessionFile(String buffer, boolean append) { 163 StringWriter oldContents = new StringWriter(); 164 165 if (append) { 166 readFileToWriter(new PrintWriter(oldContents)); 167 } 168 169 try (FileOutputStream outStream = mGarageModeRecorderFile.startWrite()) { 170 if (append) { 171 outStream.write(oldContents.toString().getBytes(StandardCharsets.UTF_8)); 172 } 173 174 outStream.write(buffer.getBytes(StandardCharsets.UTF_8)); 175 mGarageModeRecorderFile.finishWrite(outStream); 176 } catch (IOException e) { 177 Slogf.w(TAG, e, "Failed to write buffer of size %d", buffer.length()); 178 } 179 } 180 recordEvent(int event)181 private void recordEvent(int event) { 182 StringBuilder stringBuilder = new StringBuilder(); 183 boolean appendToFile = true; 184 185 switch (event) { 186 case EVENT_SESSION_START: 187 appendToFile = false; 188 stringBuilder.append(SESSION_START_TIME); 189 stringBuilder.append(mDateFormat.format(new Date(mSessionStartTime))); 190 stringBuilder.append('\n'); 191 break; 192 case EVENT_SESSION_FINISH: 193 stringBuilder.append(SESSION_FINISH_TIME); 194 stringBuilder.append(mDateFormat.format(new Date(mSessionFinishTime))); 195 stringBuilder.append('\n'); 196 stringBuilder.append(SESSION_DURATION); 197 stringBuilder.append(mSessionFinishTime - mSessionStartTime); 198 stringBuilder.append(TIME_UNIT_MS); 199 stringBuilder.append('\n'); 200 break; 201 case EVENT_SESSION_CANCELLED: 202 stringBuilder.append(SESSION_WAS_CANCELLED); 203 stringBuilder.append(mDateFormat.format(new Date(mSessionFinishTime))); 204 stringBuilder.append('\n'); 205 break; 206 default: 207 break; 208 } 209 210 writeToSessionFile(stringBuilder.toString(), appendToFile); 211 } 212 readFileToWriter(PrintWriter writer)213 private void readFileToWriter(PrintWriter writer) { 214 if (!mGarageModeRecorderFile.getBaseFile().exists()) { 215 Slogf.e(TAG, "GarageMode session file is not found %s", 216 mGarageModeRecorderFile.getBaseFile().getAbsolutePath()); 217 return; // nothing to write to dump 218 } 219 220 try (BufferedReader reader = new BufferedReader( 221 new InputStreamReader(mGarageModeRecorderFile.openRead()))) { 222 223 int lineCount = 0; 224 while (reader.ready()) { 225 writer.println(reader.readLine()); 226 lineCount++; 227 } 228 229 Slogf.d(TAG, "Read %d lines from GarageMode session file ", lineCount); 230 } catch (IOException e) { 231 Slogf.e(TAG, e, "Error reading GarageMode session file %s", 232 mGarageModeRecorderFile.getBaseFile().getAbsolutePath(), e); 233 } 234 } 235 } 236