1 /*
2  * Copyright (C) 2017 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.dialer.persistentlog;
18 
19 import android.content.Context;
20 import android.os.Handler;
21 import android.os.HandlerThread;
22 import android.support.annotation.AnyThread;
23 import android.support.annotation.NonNull;
24 import android.support.annotation.VisibleForTesting;
25 import android.support.annotation.WorkerThread;
26 import android.support.v4.os.UserManagerCompat;
27 import com.android.dialer.common.Assert;
28 import com.android.dialer.common.LogUtil;
29 import com.android.dialer.strictmode.StrictModeUtils;
30 import java.io.IOException;
31 import java.nio.charset.StandardCharsets;
32 import java.util.ArrayList;
33 import java.util.Calendar;
34 import java.util.List;
35 import java.util.concurrent.CountDownLatch;
36 import java.util.concurrent.LinkedBlockingQueue;
37 
38 /**
39  * Logs data that is persisted across app termination and device reboot. The logs are stored as
40  * rolling files in cache with a limit of {@link #LOG_FILE_SIZE_LIMIT} * {@link
41  * #LOG_FILE_COUNT_LIMIT}. The log writing is batched and there is a {@link #FLUSH_DELAY_MILLIS}
42  * delay before the logs are committed to disk to avoid excessive IO. If the app is terminated
43  * before the logs are committed it will be lost. {@link
44  * com.google.android.apps.dialer.crashreporter.SilentCrashReporter} is expected to handle such
45  * cases.
46  *
47  * <p>{@link #logText(String, String)} should be used to log ad-hoc text logs. TODO(twyen): switch
48  * to structured logging
49  */
50 public final class PersistentLogger {
51 
52   private static final int FLUSH_DELAY_MILLIS = 200;
53   private static final String LOG_FOLDER = "plain_text";
54   private static final int MESSAGE_FLUSH = 1;
55 
56   @VisibleForTesting static final int LOG_FILE_SIZE_LIMIT = 64 * 1024;
57   @VisibleForTesting static final int LOG_FILE_COUNT_LIMIT = 8;
58 
59   private static PersistentLogFileHandler fileHandler;
60 
61   private static HandlerThread loggerThread;
62   private static Handler loggerThreadHandler;
63 
64   private static final LinkedBlockingQueue<byte[]> messageQueue = new LinkedBlockingQueue<>();
65 
PersistentLogger()66   private PersistentLogger() {}
67 
initialize(Context context)68   public static void initialize(Context context) {
69     fileHandler =
70         new PersistentLogFileHandler(LOG_FOLDER, LOG_FILE_SIZE_LIMIT, LOG_FILE_COUNT_LIMIT);
71     loggerThread = new HandlerThread("PersistentLogger");
72     loggerThread.start();
73     loggerThreadHandler =
74         new Handler(
75             loggerThread.getLooper(),
76             (message) -> {
77               if (message.what == MESSAGE_FLUSH) {
78                 if (messageQueue.isEmpty()) {
79                   return true;
80                 }
81                 loggerThreadHandler.removeMessages(MESSAGE_FLUSH);
82                 List<byte[]> messages = new ArrayList<>();
83                 messageQueue.drainTo(messages);
84                 if (!UserManagerCompat.isUserUnlocked(context)) {
85                   return true;
86                 }
87                 try {
88                   fileHandler.writeLogs(messages);
89                 } catch (IOException e) {
90                   LogUtil.e("PersistentLogger.MESSAGE_FLUSH", "error writing message", e);
91                 }
92               }
93               return true;
94             });
95     loggerThreadHandler.post(() -> fileHandler.initialize(context));
96   }
97 
getLoggerThread()98   static HandlerThread getLoggerThread() {
99     return loggerThread;
100   }
101 
102   @AnyThread
logText(String tag, String string)103   public static void logText(String tag, String string) {
104     log(buildTextLog(tag, string));
105   }
106 
107   @VisibleForTesting
108   @AnyThread
log(byte[] data)109   static void log(byte[] data) {
110     messageQueue.add(data);
111     loggerThreadHandler.sendEmptyMessageDelayed(MESSAGE_FLUSH, FLUSH_DELAY_MILLIS);
112   }
113 
114   @VisibleForTesting
115   /** write raw bytes directly to the log file, likely corrupting it. */
rawLogForTest(byte[] data)116   static void rawLogForTest(byte[] data) {
117     try {
118       fileHandler.writeRawLogsForTest(data);
119     } catch (IOException e) {
120       throw new RuntimeException(e);
121     }
122   }
123 
124   /** Dump the log as human readable string. Blocks until the dump is finished. */
125   @NonNull
126   @WorkerThread
dumpLogToString()127   public static String dumpLogToString() {
128     Assert.isWorkerThread();
129     DumpStringRunnable dumpStringRunnable = new DumpStringRunnable();
130     loggerThreadHandler.post(dumpStringRunnable);
131     try {
132       return dumpStringRunnable.get();
133     } catch (InterruptedException e) {
134       Thread.currentThread().interrupt();
135       return "Cannot dump logText: " + e;
136     }
137   }
138 
139   private static class DumpStringRunnable implements Runnable {
140     private String result;
141     private final CountDownLatch latch = new CountDownLatch(1);
142 
143     @Override
run()144     public void run() {
145       result = dumpLogToStringInternal();
146       latch.countDown();
147     }
148 
get()149     public String get() throws InterruptedException {
150       latch.await();
151       return result;
152     }
153   }
154 
155   @NonNull
156   @WorkerThread
dumpLogToStringInternal()157   private static String dumpLogToStringInternal() {
158     StringBuilder result = new StringBuilder();
159     List<byte[]> logs;
160     try {
161       logs = readLogs();
162     } catch (IOException e) {
163       return "Cannot dump logText: " + e;
164     }
165 
166     for (byte[] log : logs) {
167       result.append(new String(log, StandardCharsets.UTF_8)).append("\n");
168     }
169     return result.toString();
170   }
171 
172   @NonNull
173   @WorkerThread
174   @VisibleForTesting
readLogs()175   static List<byte[]> readLogs() throws IOException {
176     Assert.isWorkerThread();
177     return fileHandler.getLogs();
178   }
179 
buildTextLog(String tag, String string)180   private static byte[] buildTextLog(String tag, String string) {
181     Calendar c = StrictModeUtils.bypass(() -> Calendar.getInstance());
182     return String.format("%tm-%td %tH:%tM:%tS.%tL - %s - %s", c, c, c, c, c, c, tag, string)
183         .getBytes(StandardCharsets.UTF_8);
184   }
185 }
186