LatinIME/java/src/com/android/inputmethod/latin/ResearchLogger.java

319 lines
11 KiB
Java

/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.android.inputmethod.latin;
import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import android.view.MotionEvent;
import com.android.inputmethod.keyboard.Keyboard;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Logs the use of the LatinIME keyboard.
*
* This class logs operations on the IME keyboard, including what the user has typed.
* Data is stored locally in a file in app-specific storage.
*
* This functionality is off by default. See {@link ProductionFlag.IS_EXPERIMENTAL}.
*/
public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = ResearchLogger.class.getSimpleName();
private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
private static final ResearchLogger sInstance = new ResearchLogger(new LogFileManager());
public static boolean sIsLogging = false;
/* package */ final Handler mLoggingHandler;
private InputMethodService mIms;
private final Date mDate;
private final SimpleDateFormat mDateFormat;
/**
* Isolates management of files. This variable should never be null, but can be changed
* to support testing.
*/
private LogFileManager mLogFileManager;
/**
* Manages the file(s) that stores the logs.
*
* Handles creation, deletion, and provides Readers, Writers, and InputStreams to access
* the logs.
*/
public static class LogFileManager {
private static final String DEFAULT_FILENAME = "log.txt";
private static final String DEFAULT_LOG_DIRECTORY = "researchLogger";
private static final long LOGFILE_PURGE_INTERVAL = 1000 * 60 * 60 * 24;
private InputMethodService mIms;
private File mFile;
private PrintWriter mPrintWriter;
/* package */ LogFileManager() {
}
public void init(InputMethodService ims) {
mIms = ims;
}
public synchronized void createLogFile() {
try {
createLogFile(DEFAULT_LOG_DIRECTORY, DEFAULT_FILENAME);
} catch (FileNotFoundException e) {
Log.w(TAG, e);
}
}
public synchronized void createLogFile(String dir, String filename)
throws FileNotFoundException {
if (mIms == null) {
Log.w(TAG, "InputMethodService is not configured. Logging is off.");
return;
}
File filesDir = mIms.getFilesDir();
if (filesDir == null || !filesDir.exists()) {
Log.w(TAG, "Storage directory does not exist. Logging is off.");
return;
}
File directory = new File(filesDir, dir);
if (!directory.exists()) {
boolean wasCreated = directory.mkdirs();
if (!wasCreated) {
Log.w(TAG, "Log directory cannot be created. Logging is off.");
return;
}
}
close();
mFile = new File(directory, filename);
mFile.setReadable(false, false);
boolean append = true;
if (mFile.exists() && mFile.lastModified() + LOGFILE_PURGE_INTERVAL <
System.currentTimeMillis()) {
append = false;
}
mPrintWriter = new PrintWriter(new FileOutputStream(mFile, append), true);
}
public synchronized boolean append(String s) {
if (mPrintWriter == null) {
Log.w(TAG, "PrintWriter is null");
return false;
} else {
mPrintWriter.print(s);
return !mPrintWriter.checkError();
}
}
public synchronized void reset() {
if (mPrintWriter != null) {
mPrintWriter.close();
mPrintWriter = null;
}
if (mFile != null && mFile.exists()) {
mFile.delete();
mFile = null;
}
}
public synchronized void close() {
if (mPrintWriter != null) {
mPrintWriter.close();
mPrintWriter = null;
mFile = null;
}
}
}
private ResearchLogger(LogFileManager logFileManager) {
mDate = new Date();
mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ");
HandlerThread handlerThread = new HandlerThread("ResearchLogger logging task",
Process.THREAD_PRIORITY_BACKGROUND);
handlerThread.start();
mLoggingHandler = new Handler(handlerThread.getLooper());
mLogFileManager = logFileManager;
}
public static ResearchLogger getInstance() {
return sInstance;
}
public static void init(InputMethodService ims, SharedPreferences prefs) {
sInstance.initInternal(ims, prefs);
}
public void initInternal(InputMethodService ims, SharedPreferences prefs) {
mIms = ims;
if (mLogFileManager != null) {
mLogFileManager.init(ims);
mLogFileManager.createLogFile();
}
if (prefs != null) {
sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
prefs.registerOnSharedPreferenceChangeListener(this);
}
}
/**
* Change to a different logFileManager.
*
* @throws IllegalArgumentException if logFileManager is null
*/
void setLogFileManager(LogFileManager manager) {
if (manager == null) {
throw new IllegalArgumentException("warning: trying to set null logFileManager");
} else {
mLogFileManager = manager;
}
}
/**
* Represents a category of logging events that share the same subfield structure.
*/
private static enum LogGroup {
MOTION_EVENT("m"),
KEY("k"),
CORRECTION("c"),
STATE_CHANGE("s");
private final String mLogString;
private LogGroup(String logString) {
mLogString = logString;
}
}
public void logMotionEvent(final int action, final long eventTime, final int id,
final int x, final int y, final float size, final float pressure) {
final String eventTag;
switch (action) {
case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break;
case MotionEvent.ACTION_UP: eventTag = "[Up]"; break;
case MotionEvent.ACTION_DOWN: eventTag = "[Down]"; break;
case MotionEvent.ACTION_POINTER_UP: eventTag = "[PointerUp]"; break;
case MotionEvent.ACTION_POINTER_DOWN: eventTag = "[PointerDown]"; break;
case MotionEvent.ACTION_MOVE: eventTag = "[Move]"; break;
case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break;
default: eventTag = "[Action" + action + "]"; break;
}
if (!TextUtils.isEmpty(eventTag)) {
StringBuilder sb = new StringBuilder();
sb.append(eventTag);
sb.append('\t'); sb.append(eventTime);
sb.append('\t'); sb.append(id);
sb.append('\t'); sb.append(x);
sb.append('\t'); sb.append(y);
sb.append('\t'); sb.append(size);
sb.append('\t'); sb.append(pressure);
write(LogGroup.MOTION_EVENT, sb.toString());
}
}
public void logKeyEvent(int code, int x, int y) {
final StringBuilder sb = new StringBuilder();
sb.append(Keyboard.printableCode(code));
sb.append('\t'); sb.append(x);
sb.append('\t'); sb.append(y);
write(LogGroup.KEY, sb.toString());
}
public void logCorrection(String subgroup, String before, String after, int position) {
final StringBuilder sb = new StringBuilder();
sb.append(subgroup);
sb.append('\t'); sb.append(before);
sb.append('\t'); sb.append(after);
sb.append('\t'); sb.append(position);
write(LogGroup.CORRECTION, sb.toString());
}
public void logStateChange(String subgroup, String details) {
write(LogGroup.STATE_CHANGE, subgroup + "\t" + details);
}
public static enum UnsLogGroup {
// TODO: expand to include one flag per log point
// TODO: support selective enabling of flags
ON_UPDATE_SELECTION;
public boolean isEnabled = true;
}
public static void logUnstructured(UnsLogGroup logGroup, String details) {
}
private void write(final LogGroup logGroup, final String log) {
// TODO: rewrite in native for better performance
mLoggingHandler.post(new Runnable() {
@Override
public void run() {
final long currentTime = System.currentTimeMillis();
final long upTime = SystemClock.uptimeMillis();
final StringBuilder builder = new StringBuilder();
builder.append(currentTime);
builder.append('\t'); builder.append(upTime);
builder.append('\t'); builder.append(logGroup.mLogString);
builder.append('\t'); builder.append(log);
if (LatinImeLogger.sDBG) {
Log.d(TAG, "Write: " + '[' + logGroup.mLogString + ']' + log);
}
if (mLogFileManager.append(builder.toString())) {
// success
} else {
if (LatinImeLogger.sDBG) {
Log.w(TAG, "Unable to write to log.");
}
}
}
});
}
public void clearAll() {
mLoggingHandler.post(new Runnable() {
@Override
public void run() {
if (LatinImeLogger.sDBG) {
Log.d(TAG, "Delete log file.");
}
mLogFileManager.reset();
}
});
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if (key == null || prefs == null) {
return;
}
sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
}
}