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