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