1 /*******************************************************************************
2  *      Copyright (C) 2013 Google Inc.
3  *      Licensed to The Android Open Source Project.
4  *
5  *      Licensed under the Apache License, Version 2.0 (the "License");
6  *      you may not use this file except in compliance with the License.
7  *      You may obtain a copy of the License at
8  *
9  *           http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *      Unless required by applicable law or agreed to in writing, software
12  *      distributed under the License is distributed on an "AS IS" BASIS,
13  *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *      See the License for the specific language governing permissions and
15  *      limitations under the License.
16  *******************************************************************************/
17 
18 package com.android.mail;
19 
20 import com.android.mail.utils.LogTag;
21 import com.android.mail.utils.LogUtils;
22 
23 import android.app.Service;
24 import android.content.Intent;
25 import android.os.IBinder;
26 import android.util.Pair;
27 
28 import java.io.FileDescriptor;
29 import java.io.PrintWriter;
30 import java.util.Date;
31 import java.util.HashMap;
32 import java.util.LinkedList;
33 import java.util.Map;
34 import java.util.Queue;
35 
36 /**
37  * A write-only device for sensitive logs. Turned on only during debugging.
38  *
39  * Dump valuable system state by sending a local broadcast to the associated activity.
40  * Broadcast receivers are responsible for dumping state as they see fit.
41  * This service is only started when the log level is high, so there is no risk of user
42  * data being logged by mistake.
43  *
44  * To add logging to this service, call {@link #log(String, String, Object...)} with a tag name,
45  * which is a class name, like "AbstractActivityController", which is a unique ID. Then, add to the
46  * resulting buffer any information of interest at logging time. This is kept in a ring buffer,
47  * which is overwritten with new information.
48  */
49 public class MailLogService extends Service {
50     /**
51      * This is the top level flag that enables this service.
52      */
53     public static boolean DEBUG_ENABLED = false;
54 
55     /** The tag which needs to be turned to DEBUG to get logging going. */
56     protected static final String LOG_TAG = LogTag.getLogTag();
57 
58     /**
59      * A circular buffer of {@value #SIZE} lines.  To  insert into this buffer,
60      * call the {@link #put(String)} method.  To retrieve the most recent logs,
61      * call the {@link #toString()} method.
62      */
63     private static class CircularBuffer {
64         // We accept fifty lines of input.
65         public static final int SIZE = 50;
66         /** The actual list of strings to be printed. */
67         final Queue<Pair<Long, String>> mList = new LinkedList<Pair<Long, String>>();
68         /** The current size of the buffer */
69         int mCurrentSize = 0;
70 
71         /** Create an empty log buffer. */
CircularBuffer()72         private CircularBuffer() {
73             // Do nothing
74         }
75 
76         /** Get the current timestamp */
dateToString(long timestamp)77         private static String dateToString(long timestamp) {
78             final Date d = new Date(timestamp);
79             return String.format("%d-%d %d:%d:%d: ", d.getDay(), d.getMonth(), d.getHours(),
80                     d.getMinutes(), d.getSeconds());
81         }
82 
83         /**
84          * Insert a log message into the buffer. This might evict the oldest message if the log
85          * is at capacity.
86          * @param message a log message for this buffer.
87          */
put(String message)88         private synchronized void put(String message) {
89             if (mCurrentSize == SIZE) {
90                 // At capacity, we'll remove the head, and add to the tail. Size is unchanged.
91                 mList.remove();
92             } else {
93                 // Less than capacity. Adding a new element at the end.
94                 mCurrentSize++;
95             }
96             // Add the current timestamp along with the message.
97             mList.add(new Pair<Long, String>(System.currentTimeMillis(), message));
98         }
99 
100         @Override
toString()101         public String toString() {
102             final StringBuilder builder = new StringBuilder();
103             for (final Pair<Long, String> s : mList) {
104                 // Print the timestamp as an actual date, and then the message.
105                 builder.append(dateToString(s.first));
106                 builder.append(s.second);
107                 // Put a newline at the end of each log line.
108                 builder.append("\n");
109             }
110             return builder.toString();
111         }
112     }
113 
114     /** Header printed at the start of the dump. */
115     private static final String HEADER = "**** MailLogService ***\n";
116     /** Map of current tag -> log. */
117     private static final Map<String, CircularBuffer> sLogs = new HashMap<String, CircularBuffer>();
118 
119     @Override
onBind(Intent intent)120     public IBinder onBind(Intent intent) {
121         return null;
122     }
123 
124     /**
125      * Return the circular buffer associated with this tag, or create a new buffer if none is
126      * currently associated.
127      * @param tag a string to identify a unique tag.
128      * @return a circular buffer associated with a string tag.
129      */
getOrCreate(String tag)130     private static CircularBuffer getOrCreate(String tag) {
131         if (sLogs.containsKey(tag)) {
132             return sLogs.get(tag);
133         }
134         // Create a new CircularBuffer with this tag
135         final CircularBuffer buffer = new CircularBuffer();
136         sLogs.put(tag, buffer);
137         return buffer;
138     }
139 
140     /**
141      * Return true if the logging level is high enough for this service to function.
142      * @return true if this service is functioning at the current log level. False otherwise.
143      */
isLoggingLevelHighEnough()144     public static boolean isLoggingLevelHighEnough() {
145         return LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG);
146     }
147 
148     /**
149      * Add to the log for the tag given.
150      * @param tag a unique tag to add the message to
151      * @param format a string format for the message
152      * @param args optional list of arguments for the format.
153      */
log(String tag, String format, Object... args)154     public static void log(String tag, String format, Object... args) {
155         if (!DEBUG_ENABLED || !isLoggingLevelHighEnough()) {
156             return;
157         }
158         // The message we are printing.
159         final String logMessage = String.format(format, args);
160         // Find the circular buffer to go with this tag, or create a new one.
161         getOrCreate(tag).put(logMessage);
162     }
163 
164     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)165     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
166         if (!DEBUG_ENABLED) {
167             return;
168         }
169         writer.print(HEADER);
170         // Go through all the tags, and write them all out sequentially.
171         for (final String tag : sLogs.keySet()) {
172             // Write out a sub-header: Logging for tag "MyModuleName"
173             writer.append("Logging for tag: \"");
174             writer.append(tag);
175             writer.append("\"\n");
176 
177             writer.append(sLogs.get(tag).toString());
178         }
179         // Go through all the buffers.
180         super.dump(fd, writer,args);
181     }
182 }
183