1/*
2 * Copyright (C) 2016 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
17package com.android.inputmethod.latin.utils;
18
19import android.content.Intent;
20import android.content.pm.PackageManager;
21import android.inputmethodservice.InputMethodService;
22import android.net.Uri;
23import android.os.Environment;
24import android.os.Handler;
25import android.os.HandlerThread;
26import android.os.Process;
27import android.util.Log;
28import android.view.MotionEvent;
29
30import com.android.inputmethod.latin.LatinImeLogger;
31
32import java.io.BufferedReader;
33import java.io.File;
34import java.io.FileInputStream;
35import java.io.FileNotFoundException;
36import java.io.FileOutputStream;
37import java.io.FileReader;
38import java.io.IOException;
39import java.io.PrintWriter;
40import java.nio.channels.FileChannel;
41import java.text.SimpleDateFormat;
42import java.util.Date;
43import java.util.Locale;
44
45public final class UsabilityStudyLogUtils {
46    // TODO: remove code duplication with ResearchLog class
47    private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
48    private static final String FILENAME = "log.txt";
49    private final Handler mLoggingHandler;
50    private File mFile;
51    private File mDirectory;
52    private InputMethodService mIms;
53    private PrintWriter mWriter;
54    private final Date mDate;
55    private final SimpleDateFormat mDateFormat;
56
57    private UsabilityStudyLogUtils() {
58        mDate = new Date();
59        mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ", Locale.US);
60
61        HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
62                Process.THREAD_PRIORITY_BACKGROUND);
63        handlerThread.start();
64        mLoggingHandler = new Handler(handlerThread.getLooper());
65    }
66
67    // Initialization-on-demand holder
68    private static final class OnDemandInitializationHolder {
69        public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils();
70    }
71
72    public static UsabilityStudyLogUtils getInstance() {
73        return OnDemandInitializationHolder.sInstance;
74    }
75
76    public void init(final InputMethodService ims) {
77        mIms = ims;
78        mDirectory = ims.getFilesDir();
79    }
80
81    private void createLogFileIfNotExist() {
82        if ((mFile == null || !mFile.exists())
83                && (mDirectory != null && mDirectory.exists())) {
84            try {
85                mWriter = getPrintWriter(mDirectory, FILENAME, false);
86            } catch (final IOException e) {
87                Log.e(USABILITY_TAG, "Can't create log file.");
88            }
89        }
90    }
91
92    public static void writeBackSpace(final int x, final int y) {
93        UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
94    }
95
96    public static void writeChar(final char c, final int x, final int y) {
97        String inputChar = String.valueOf(c);
98        switch (c) {
99            case '\n':
100                inputChar = "<enter>";
101                break;
102            case '\t':
103                inputChar = "<tab>";
104                break;
105            case ' ':
106                inputChar = "<space>";
107                break;
108        }
109        UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
110        LatinImeLogger.onPrintAllUsabilityStudyLogs();
111    }
112
113    public static void writeMotionEvent(final MotionEvent me) {
114        final int action = me.getActionMasked();
115        final long eventTime = me.getEventTime();
116        final int pointerCount = me.getPointerCount();
117        for (int index = 0; index < pointerCount; index++) {
118            final int id = me.getPointerId(index);
119            final int x = (int)me.getX(index);
120            final int y = (int)me.getY(index);
121            final float size = me.getSize(index);
122            final float pressure = me.getPressure(index);
123
124            final String eventTag;
125            switch (action) {
126            case MotionEvent.ACTION_UP:
127                eventTag = "[Up]";
128                break;
129            case MotionEvent.ACTION_DOWN:
130                eventTag = "[Down]";
131                break;
132            case MotionEvent.ACTION_POINTER_UP:
133                eventTag = "[PointerUp]";
134                break;
135            case MotionEvent.ACTION_POINTER_DOWN:
136                eventTag = "[PointerDown]";
137                break;
138            case MotionEvent.ACTION_MOVE:
139                eventTag = "[Move]";
140                break;
141            default:
142                eventTag = "[Action" + action + "]";
143                break;
144            }
145            getInstance().write(eventTag + eventTime + "," + id + "," + x + "," + y + "," + size
146                    + "," + pressure);
147        }
148    }
149
150    public void write(final String log) {
151        mLoggingHandler.post(new Runnable() {
152            @Override
153            public void run() {
154                createLogFileIfNotExist();
155                final long currentTime = System.currentTimeMillis();
156                mDate.setTime(currentTime);
157
158                final String printString = String.format(Locale.US, "%s\t%d\t%s\n",
159                        mDateFormat.format(mDate), currentTime, log);
160                if (LatinImeLogger.sDBG) {
161                    Log.d(USABILITY_TAG, "Write: " + log);
162                }
163                mWriter.print(printString);
164            }
165        });
166    }
167
168    private synchronized String getBufferedLogs() {
169        mWriter.flush();
170        final StringBuilder sb = new StringBuilder();
171        final BufferedReader br = getBufferedReader();
172        String line;
173        try {
174            while ((line = br.readLine()) != null) {
175                sb.append('\n');
176                sb.append(line);
177            }
178        } catch (final IOException e) {
179            Log.e(USABILITY_TAG, "Can't read log file.");
180        } finally {
181            if (LatinImeLogger.sDBG) {
182                Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString());
183            }
184            try {
185                br.close();
186            } catch (final IOException e) {
187                // ignore.
188            }
189        }
190        return sb.toString();
191    }
192
193    public void emailResearcherLogsAll() {
194        mLoggingHandler.post(new Runnable() {
195            @Override
196            public void run() {
197                final Date date = new Date();
198                date.setTime(System.currentTimeMillis());
199                final String currentDateTimeString =
200                        new SimpleDateFormat("yyyyMMdd-HHmmssZ", Locale.US).format(date);
201                if (mFile == null) {
202                    Log.w(USABILITY_TAG, "No internal log file found.");
203                    return;
204                }
205                if (mIms.checkCallingOrSelfPermission(
206                            android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
207                                    != PackageManager.PERMISSION_GRANTED) {
208                    Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE");
209                    return;
210                }
211                mWriter.flush();
212                final String destPath = Environment.getExternalStorageDirectory()
213                        + "/research-" + currentDateTimeString + ".log";
214                final File destFile = new File(destPath);
215                try {
216                    final FileInputStream srcStream = new FileInputStream(mFile);
217                    final FileOutputStream destStream = new FileOutputStream(destFile);
218                    final FileChannel src = srcStream.getChannel();
219                    final FileChannel dest = destStream.getChannel();
220                    src.transferTo(0, src.size(), dest);
221                    src.close();
222                    srcStream.close();
223                    dest.close();
224                    destStream.close();
225                } catch (final FileNotFoundException e1) {
226                    Log.w(USABILITY_TAG, e1);
227                    return;
228                } catch (final IOException e2) {
229                    Log.w(USABILITY_TAG, e2);
230                    return;
231                }
232                if (!destFile.exists()) {
233                    Log.w(USABILITY_TAG, "Dest file doesn't exist.");
234                    return;
235                }
236                final Intent intent = new Intent(Intent.ACTION_SEND);
237                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
238                if (LatinImeLogger.sDBG) {
239                    Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI());
240                }
241                intent.setType("text/plain");
242                intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath));
243                intent.putExtra(Intent.EXTRA_SUBJECT,
244                        "[Research Logs] " + currentDateTimeString);
245                mIms.startActivity(intent);
246            }
247        });
248    }
249
250    public void printAll() {
251        mLoggingHandler.post(new Runnable() {
252            @Override
253            public void run() {
254                mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0);
255            }
256        });
257    }
258
259    public void clearAll() {
260        mLoggingHandler.post(new Runnable() {
261            @Override
262            public void run() {
263                if (mFile != null && mFile.exists()) {
264                    if (LatinImeLogger.sDBG) {
265                        Log.d(USABILITY_TAG, "Delete log file.");
266                    }
267                    mFile.delete();
268                    mWriter.close();
269                }
270            }
271        });
272    }
273
274    private BufferedReader getBufferedReader() {
275        createLogFileIfNotExist();
276        try {
277            return new BufferedReader(new FileReader(mFile));
278        } catch (final FileNotFoundException e) {
279            return null;
280        }
281    }
282
283    private PrintWriter getPrintWriter(final File dir, final String filename,
284            final boolean renew) throws IOException {
285        mFile = new File(dir, filename);
286        if (mFile.exists()) {
287            if (renew) {
288                mFile.delete();
289            }
290        }
291        return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */);
292    }
293}
294