diff --git a/java/res/values-pl/strings-appname.xml b/java/res/values-pl/strings-appname.xml index 4d244d705..e460644a3 100644 --- a/java/res/values-pl/strings-appname.xml +++ b/java/res/values-pl/strings-appname.xml @@ -21,7 +21,10 @@ "Klawiatura Android" - "Sprawdzanie pisowni w Androidzie" - "Ustawienia klawiatury Android" - "Ustawienia sprawdzania pisowni" + + + + + + diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 03dce9ca6..273525100 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -308,6 +308,8 @@ - operation[CHAR LIMIT=100] --> Please demonstrate the issue you are writing about.\n\nWhen finished, select the \"Bug?\" button again." + + Send feedback Recording cancelled due to timeout diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml index 51f580721..e299ce4f8 100644 --- a/java/res/xml/prefs.xml +++ b/java/res/xml/prefs.xml @@ -176,6 +176,9 @@ android:key="pref_show_setup_wizard_icon" android:title="@string/show_setup_wizard_icon" /> + () { @Override public Object call() throws Exception { try { if (mHasWrittenData) { mJsonWriter.endArray(); - mJsonWriter.flush(); - mJsonWriter.close(); - if (DEBUG) { - Log.d(TAG, "wrote log to " + mFile); - } mHasWrittenData = false; - } else { - if (DEBUG) { - Log.d(TAG, "close() called, but no data, not outputting"); - } + } + mJsonWriter.flush(); + mJsonWriter.close(); + if (DEBUG) { + Log.d(TAG, "wrote log to " + mFile); } } catch (Exception e) { - Log.d(TAG, "error when closing ResearchLog:"); - e.printStackTrace(); + Log.d(TAG, "error when closing ResearchLog:", e); } finally { + // Marking the file as read-only signals that this log file is ready to be + // uploaded. if (mFile != null && mFile.exists()) { mFile.setWritable(false, false); } @@ -125,9 +136,24 @@ public class ResearchLog { mExecutor.shutdown(); } - private boolean mIsAbortSuccessful; + /** + * Block until the research log has shut down and spooled out all output or {@code timeout} + * occurs. + * + * @param timeout time to wait for close in milliseconds + */ + public void blockingClose(final long timeout) { + close(null); + awaitTermination(timeout, TimeUnit.MILLISECONDS); + } - public synchronized void abort() { + /** + * Waits for publication requests to finish, closes the JsonWriter, but then deletes the backing + * output file. + * + * @param onAbort run after the abort() operation has completed asynchronously + */ + private synchronized void abort(final Runnable onAbort) { mExecutor.submit(new Callable() { @Override public Object call() throws Exception { @@ -139,7 +165,10 @@ public class ResearchLog { } } finally { if (mFile != null) { - mIsAbortSuccessful = mFile.delete(); + mFile.delete(); + } + if (onAbort != null) { + onAbort.run(); } } return null; @@ -149,14 +178,25 @@ public class ResearchLog { mExecutor.shutdown(); } - public boolean blockingAbort() throws InterruptedException { - abort(); - mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); - return mIsAbortSuccessful; + /** + * Block until the research log has aborted or {@code timeout} occurs. + * + * @param timeout time to wait for close in milliseconds + */ + public void blockingAbort(final long timeout) { + abort(null); + awaitTermination(timeout, TimeUnit.MILLISECONDS); } - public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException { - mExecutor.awaitTermination(delay, timeUnit); + @UsedForTesting + public void awaitTermination(final long delay, final TimeUnit timeUnit) { + try { + if (!mExecutor.awaitTermination(delay, timeUnit)) { + Log.e(TAG, "ResearchLog executor timed out while awaiting terminaion"); + } + } catch (final InterruptedException e) { + Log.e(TAG, "ResearchLog executor interrupted while awaiting terminaion", e); + } } /* package */ synchronized void flush() { @@ -186,6 +226,12 @@ public class ResearchLog { mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); } + /** + * Queues up {@code logUnit} to be published in the background. + * + * @param logUnit the {@link LogUnit} to be published + * @param canIncludePrivateData whether private data in the LogUnit should be included + */ public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) { try { mExecutor.submit(new Callable() { @@ -196,10 +242,10 @@ public class ResearchLog { return null; } }); - } catch (RejectedExecutionException e) { + } catch (final RejectedExecutionException e) { // TODO: Add code to record loss of data, and report. if (DEBUG) { - Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution"); + Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution", e); } } } diff --git a/java/src/com/android/inputmethod/research/ResearchLogDirectory.java b/java/src/com/android/inputmethod/research/ResearchLogDirectory.java new file mode 100644 index 000000000..291dea5d0 --- /dev/null +++ b/java/src/com/android/inputmethod/research/ResearchLogDirectory.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2013 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.research; + +import android.content.Context; +import android.util.Log; + +import java.io.File; +import java.io.FileFilter; + +/** + * Manages log files. + * + * This class handles all aspects where and how research log data is stored. This includes + * generating log filenames in the correct place with the correct names, and cleaning up log files + * under this directory. + */ +public class ResearchLogDirectory { + public static final String TAG = ResearchLogDirectory.class.getSimpleName(); + /* package */ static final String LOG_FILENAME_PREFIX = "researchLog"; + private static final String FILENAME_SUFFIX = ".txt"; + private static final String USER_RECORDING_FILENAME_PREFIX = "recording"; + + private static final ReadOnlyLogFileFilter sUploadableLogFileFilter = + new ReadOnlyLogFileFilter(); + + private final File mFilesDir; + + static class ReadOnlyLogFileFilter implements FileFilter { + @Override + public boolean accept(final File pathname) { + return pathname.getName().startsWith(ResearchLogDirectory.LOG_FILENAME_PREFIX) + && !pathname.canWrite(); + } + } + + /** + * Creates a new ResearchLogDirectory, creating the storage directory if it does not exist. + */ + public ResearchLogDirectory(final Context context) { + mFilesDir = getLoggingDirectory(context); + if (mFilesDir == null) { + throw new NullPointerException("No files directory specified"); + } + if (!mFilesDir.exists()) { + mFilesDir.mkdirs(); + } + } + + private File getLoggingDirectory(final Context context) { + // TODO: Switch to using a subdirectory of getFilesDir(). + return context.getFilesDir(); + } + + /** + * Get an array of log files that are ready for uploading. + * + * A file is ready for uploading if it is marked as read-only. + * + * @return the array of uploadable files + */ + public File[] getUploadableLogFiles() { + try { + return mFilesDir.listFiles(sUploadableLogFileFilter); + } catch (final SecurityException e) { + Log.e(TAG, "Could not cleanup log directory, permission denied", e); + return new File[0]; + } + } + + public void cleanupLogFilesOlderThan(final long time) { + try { + for (final File file : mFilesDir.listFiles()) { + final String filename = file.getName(); + if ((filename.startsWith(LOG_FILENAME_PREFIX) + || filename.startsWith(USER_RECORDING_FILENAME_PREFIX)) + && (file.lastModified() < time)) { + file.delete(); + } + } + } catch (final SecurityException e) { + Log.e(TAG, "Could not cleanup log directory, permission denied", e); + } + } + + public File getLogFilePath(final long time) { + return new File(mFilesDir, getUniqueFilename(LOG_FILENAME_PREFIX, time)); + } + + public File getUserRecordingFilePath(final long time) { + return new File(mFilesDir, getUniqueFilename(USER_RECORDING_FILENAME_PREFIX, time)); + } + + private static String getUniqueFilename(final String prefix, final long time) { + return prefix + "-" + time + FILENAME_SUFFIX; + } +} diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 0d6dbfff6..a38a226f0 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -124,17 +124,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // field holds a channel name, the developer does not have to re-enter it when using the // feedback mechanism to generate multiple tests. private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false; - public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; /* package */ static boolean sIsLogging = false; private static final int OUTPUT_FORMAT_VERSION = 5; private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; - private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash"; - /* package */ static final String LOG_FILENAME_PREFIX = "researchLog"; - private static final String LOG_FILENAME_SUFFIX = ".txt"; - /* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording"; - private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt"; - private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = - new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); // Whether all words should be recorded, leaving unsampled word between bigrams. Useful for // testing. /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false @@ -156,15 +148,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // constants related to specific log points private static final String WHITESPACE_SEPARATORS = " \t\n\r"; private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 - private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel"; + private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = 5 * 1000; + private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = 5 * 1000; + private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; + private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS; + private static final ResearchLogger sInstance = new ResearchLogger(); private static String sAccountType = null; private static String sAllowedAccountDomain = null; - // to write to a different filename, e.g., for testing, set mFile before calling start() - /* package */ File mFilesDir; - /* package */ String mUUIDString; /* package */ ResearchLog mMainResearchLog; // mFeedbackLog records all events for the session, private or not (excepting // passwords). It is written to permanent storage only if the user explicitly commands @@ -190,9 +183,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area" // U+E001 is in the "private-use area" /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; - private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; - private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; - private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS; protected static final int SUSPEND_DURATION_IN_MINUTES = 1; // set when LatinIME should ignore an onUpdateSelection() callback that // arises from operations in this class @@ -206,11 +196,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private final Statistics mStatistics; private final MotionEventReader mMotionEventReader = new MotionEventReader(); private final Replayer mReplayer = Replayer.getInstance(); + private ResearchLogDirectory mResearchLogDirectory; private Intent mUploadIntent; private Intent mUploadNowIntent; - private LogUnit mCurrentLogUnit = new LogUnit(); + /* package for test */ LogUnit mCurrentLogUnit = new LogUnit(); // Gestured or tapped words may be committed after the gesture of the next word has started. // To ensure that the gesture data of the next word is not associated with the previous word, @@ -239,50 +230,42 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return sInstance; } - public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) { + public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher, + final Suggest suggest) { assert latinIME != null; - if (latinIME == null) { - Log.w(TAG, "IMS is null; logging is off"); - } else { - mFilesDir = latinIME.getFilesDir(); - if (mFilesDir == null || !mFilesDir.exists()) { - Log.w(TAG, "IME storage directory does not exist."); - } - } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(latinIME); - if (prefs != null) { - mUUIDString = getUUID(prefs); - if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) { - Editor e = prefs.edit(); - e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE); - e.apply(); - } - sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); - prefs.registerOnSharedPreferenceChangeListener(this); + mLatinIME = latinIME; + mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME); + mPrefs.registerOnSharedPreferenceChangeListener(this); - final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L); - final long now = System.currentTimeMillis(); - if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) { - final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS; - cleanupLoggingDir(mFilesDir, timeHorizon); - Editor e = prefs.edit(); - e.putLong(PREF_LAST_CLEANUP_TIME, now); - e.apply(); - } - } + // Initialize fields from preferences + sIsLogging = ResearchSettings.readResearchLoggerEnabledFlag(mPrefs); + + // Initialize fields from resources final Resources res = latinIME.getResources(); sAccountType = res.getString(R.string.research_account_type); sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain); - mLatinIME = latinIME; - mPrefs = prefs; + + // Initialize directory manager + mResearchLogDirectory = new ResearchLogDirectory(mLatinIME); + cleanLogDirectoryIfNeeded(mResearchLogDirectory, System.currentTimeMillis()); + + // Initialize external services mUploadIntent = new Intent(mLatinIME, UploaderService.class); mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); - mReplayer.setKeyboardSwitcher(keyboardSwitcher); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { scheduleUploadingService(mLatinIME); } + mReplayer.setKeyboardSwitcher(keyboardSwitcher); + } + + private void cleanLogDirectoryIfNeeded(final ResearchLogDirectory researchLogDirectory, + final long now) { + final long lastCleanupTime = ResearchSettings.readResearchLastDirCleanupTime(mPrefs); + if (now - lastCleanupTime < DURATION_BETWEEN_DIR_CLEANUP_IN_MS) return; + final long oldestAllowedFileTime = now - MAX_LOGFILE_AGE_IN_MS; + mResearchLogDirectory.cleanupLogFilesOlderThan(oldestAllowedFileTime); + ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now); } /** @@ -304,17 +287,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); } - private void cleanupLoggingDir(final File dir, final long time) { - for (File file : dir.listFiles()) { - final String filename = file.getName(); - if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX) - || filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX)) - && file.lastModified() < time) { - file.delete(); - } - } - } - public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { mMainKeyboardView = mainKeyboardView; maybeShowSplashScreen(); @@ -324,14 +296,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mMainKeyboardView = null; } - private boolean hasSeenSplash() { - return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false); + public void onDestroy() { + if (mPrefs != null) { + mPrefs.unregisterOnSharedPreferenceChangeListener(this); + } } private Dialog mSplashDialog = null; private void maybeShowSplashScreen() { - if (hasSeenSplash()) { + if (ResearchSettings.readHasSeenSplash(mPrefs)) { return; } if (mSplashDialog != null && mSplashDialog.isShowing()) { @@ -384,53 +358,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } public void onUserLoggingConsent() { - setLoggingAllowed(true); if (mPrefs == null) { - return; + mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); + if (mPrefs == null) return; } - final Editor e = mPrefs.edit(); - e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true); - e.apply(); + sIsLogging = true; + ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, true); + ResearchSettings.writeHasSeenSplash(mPrefs, true); restart(); } - private void setLoggingAllowed(boolean enableLogging) { - if (mPrefs == null) { - return; - } - Editor e = mPrefs.edit(); - e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); - e.apply(); + private void setLoggingAllowed(final boolean enableLogging) { + if (mPrefs == null) return; sIsLogging = enableLogging; - } - - private static int sLogFileCounter = 0; - - private File createLogFile(final File filesDir) { - final StringBuilder sb = new StringBuilder(); - sb.append(LOG_FILENAME_PREFIX).append('-'); - sb.append(mUUIDString).append('-'); - sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-'); - // Sometimes logFiles are created within milliseconds of each other. Append a counter to - // separate these. - if (sLogFileCounter < Integer.MAX_VALUE) { - sLogFileCounter++; - } else { - // Wrap the counter, in the unlikely event of overflow. - sLogFileCounter = 0; - } - sb.append(sLogFileCounter); - sb.append(LOG_FILENAME_SUFFIX); - return new File(filesDir, sb.toString()); - } - - private File createUserRecordingFile(final File filesDir) { - final StringBuilder sb = new StringBuilder(); - sb.append(USER_RECORDING_FILENAME_PREFIX).append('-'); - sb.append(mUUIDString).append('-'); - sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); - sb.append(USER_RECORDING_FILENAME_SUFFIX); - return new File(filesDir, sb.toString()); + ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, enableLogging); } private void checkForEmptyEditor() { @@ -469,14 +410,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // Log.w(TAG, "not in usability mode; not logging"); return; } - if (mFilesDir == null || !mFilesDir.exists()) { - Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); - return; - } if (mMainLogBuffer == null) { - mMainResearchLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME); + mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath( + System.currentTimeMillis()), mLatinIME); final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1); - mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore) { + mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore, + mSuggest) { @Override protected void publish(final ArrayList logUnits, boolean canIncludePrivateData) { @@ -499,7 +438,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } }; - mMainLogBuffer.setSuggest(mSuggest); } if (mFeedbackLogBuffer == null) { resetFeedbackLogging(); @@ -507,7 +445,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private void resetFeedbackLogging() { - mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME); + mFeedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath( + System.currentTimeMillis()), mLatinIME); mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE); } @@ -524,42 +463,29 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang commitCurrentLogUnit(); mMainLogBuffer.setIsStopping(); mMainLogBuffer.shiftAndPublishAll(); - mMainResearchLog.close(null /* callback */); + mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); mMainLogBuffer = null; } if (mFeedbackLogBuffer != null) { - mFeedbackLog.close(null /* callback */); + mFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); mFeedbackLogBuffer = null; } } - public boolean abort() { + public void abort() { if (DEBUG) { Log.d(TAG, "abort called"); } - boolean didAbortMainLog = false; if (mMainLogBuffer != null) { mMainLogBuffer.clear(); - try { - didAbortMainLog = mMainResearchLog.blockingAbort(); - } catch (InterruptedException e) { - // Don't know whether this succeeded or not. We assume not; this is reported - // to the caller. - } + mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); mMainLogBuffer = null; } - boolean didAbortFeedbackLog = false; if (mFeedbackLogBuffer != null) { mFeedbackLogBuffer.clear(); - try { - didAbortFeedbackLog = mFeedbackLog.blockingAbort(); - } catch (InterruptedException e) { - // Don't know whether this succeeded or not. We assume not; this is reported - // to the caller. - } + mFeedbackLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); mFeedbackLogBuffer = null; } - return didAbortMainLog && didAbortFeedbackLog; } private void restart() { @@ -576,7 +502,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } @Override - public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { if (key == null || prefs == null) { return; } @@ -598,7 +524,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang presentFeedbackDialog(latinIME); } - public void presentFeedbackDialog(LatinIME latinIME) { + public void presentFeedbackDialog(final LatinIME latinIME) { if (isMakingUserRecording()) { saveRecording(); } @@ -642,9 +568,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private void startRecordingInternal() { if (mUserRecordingLog != null) { - mUserRecordingLog.abort(); + mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); } - mUserRecordingFile = createUserRecordingFile(mFilesDir); + mUserRecordingFile = mResearchLogDirectory.getUserRecordingFilePath( + System.currentTimeMillis()); mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME); mUserRecordingLogBuffer = new LogBuffer(); resetRecordingTimer(); @@ -680,7 +607,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private void cancelRecording() { if (mUserRecordingLog != null) { - mUserRecordingLog.abort(); + mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); } mUserRecordingLog = null; mUserRecordingLogBuffer = null; @@ -692,7 +619,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private void saveRecording() { commitCurrentLogUnit(); publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true); - mUserRecordingLog.close(null); + mUserRecordingLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); mUserRecordingLog = null; mUserRecordingLogBuffer = null; @@ -804,12 +731,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang feedbackContents, accountName, recording); mFeedbackLogBuffer.shiftIn(feedbackLogUnit); publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */); - mSavedFeedbackLog.close(new Runnable() { - @Override - public void run() { - uploadNow(); - } - }); + mSavedFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); + uploadNow(); if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) { final Handler handler = new Handler(); @@ -830,9 +753,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (mPrefs == null) { return; } - final Editor e = mPrefs.edit(); - e.putString(PREF_RESEARCH_SAVED_CHANNEL, channelName); - e.apply(); + mPrefs.edit().putString(PREF_RESEARCH_SAVED_CHANNEL, channelName).apply(); } } @@ -847,10 +768,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mInFeedbackDialog = false; } - public void initSuggest(Suggest suggest) { + public void initSuggest(final Suggest suggest) { mSuggest = suggest; + // MainLogBuffer has out-of-date Suggest object. Need to close it down and create a new + // one. if (mMainLogBuffer != null) { - mMainLogBuffer.setSuggest(mSuggest); + stop(); + start(); } } @@ -1139,18 +1063,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - private static String getUUID(final SharedPreferences prefs) { - String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); - if (null == uuidString) { - UUID uuid = UUID.randomUUID(); - uuidString = uuid.toString(); - Editor editor = prefs.edit(); - editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); - editor.apply(); - } - return uuidString; - } - private String scrubWord(String word) { final Dictionary dictionary = getDictionary(); if (dictionary == null) { @@ -1197,9 +1109,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 0); final Integer versionCode = packageInfo.versionCode; final String versionName = packageInfo.versionName; + final String uuid = ResearchSettings.readResearchLoggerUuid(researchLogger.mPrefs); researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL, - researchLogger.mUUIDString, editorInfo.packageName, - Integer.toHexString(editorInfo.inputType), + uuid, editorInfo.packageName, Integer.toHexString(editorInfo.inputType), Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, diff --git a/java/src/com/android/inputmethod/research/ResearchSettings.java b/java/src/com/android/inputmethod/research/ResearchSettings.java new file mode 100644 index 000000000..c0bc03fde --- /dev/null +++ b/java/src/com/android/inputmethod/research/ResearchSettings.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 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.research; + +import android.content.SharedPreferences; + +import java.util.UUID; + +public final class ResearchSettings { + public static final String PREF_RESEARCH_LOGGER_UUID = "pref_research_logger_uuid"; + public static final String PREF_RESEARCH_LOGGER_ENABLED_FLAG = + "pref_research_logger_enabled_flag"; + public static final String PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH = + "pref_research_logger_has_seen_splash"; + public static final String PREF_RESEARCH_LAST_DIR_CLEANUP_TIME = + "pref_research_last_dir_cleanup_time"; + + private ResearchSettings() { + // Intentional empty constructor for singleton. + } + + public static String readResearchLoggerUuid(final SharedPreferences prefs) { + if (prefs.contains(PREF_RESEARCH_LOGGER_UUID)) { + return prefs.getString(PREF_RESEARCH_LOGGER_UUID, null); + } + // Generate a random string as uuid if not yet set + final String newUuid = UUID.randomUUID().toString(); + prefs.edit().putString(PREF_RESEARCH_LOGGER_UUID, newUuid).apply(); + return newUuid; + } + + public static boolean readResearchLoggerEnabledFlag(final SharedPreferences prefs) { + return prefs.getBoolean(PREF_RESEARCH_LOGGER_ENABLED_FLAG, false); + } + + public static void writeResearchLoggerEnabledFlag(final SharedPreferences prefs, + final boolean isEnabled) { + prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_ENABLED_FLAG, isEnabled).apply(); + } + + public static boolean readHasSeenSplash(final SharedPreferences prefs) { + return prefs.getBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, false); + } + + public static void writeHasSeenSplash(final SharedPreferences prefs, + final boolean hasSeenSplash) { + prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, hasSeenSplash).apply(); + } + + public static long readResearchLastDirCleanupTime(final SharedPreferences prefs) { + return prefs.getLong(PREF_RESEARCH_LAST_DIR_CLEANUP_TIME, 0L); + } + + public static void writeResearchLastDirCleanupTime(final SharedPreferences prefs, + final long lastDirCleanupTime) { + prefs.edit().putLong(PREF_RESEARCH_LAST_DIR_CLEANUP_TIME, lastDirCleanupTime).apply(); + } +} diff --git a/java/src/com/android/inputmethod/research/Uploader.java b/java/src/com/android/inputmethod/research/Uploader.java new file mode 100644 index 000000000..ba05ec12b --- /dev/null +++ b/java/src/com/android/inputmethod/research/Uploader.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2013 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.research; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.text.TextUtils; +import android.util.Log; + +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.define.ProductionFlag; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Manages the uploading of ResearchLog files. + */ +public final class Uploader { + private static final String TAG = Uploader.class.getSimpleName(); + private static final boolean DEBUG = false + && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; + // Set IS_INHIBITING_AUTO_UPLOAD to true for local testing + private static final boolean IS_INHIBITING_AUTO_UPLOAD = false + && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; + private static final int BUF_SIZE = 1024 * 8; + + private final Context mContext; + private final ResearchLogDirectory mResearchLogDirectory; + private final URL mUrl; + + public Uploader(final Context context) { + mContext = context; + mResearchLogDirectory = new ResearchLogDirectory(context); + + final String urlString = context.getString(R.string.research_logger_upload_url); + if (TextUtils.isEmpty(urlString)) { + mUrl = null; + return; + } + URL url = null; + try { + url = new URL(urlString); + } catch (final MalformedURLException e) { + Log.e(TAG, "Bad URL for uploading", e); + } + mUrl = url; + } + + public boolean isPossibleToUpload() { + return hasUploadingPermission() && mUrl != null && !IS_INHIBITING_AUTO_UPLOAD; + } + + private boolean hasUploadingPermission() { + final PackageManager packageManager = mContext.getPackageManager(); + return packageManager.checkPermission(Manifest.permission.INTERNET, + mContext.getPackageName()) == PackageManager.PERMISSION_GRANTED; + } + + public boolean isConvenientToUpload() { + return isExternallyPowered() && hasWifiConnection(); + } + + private boolean isExternallyPowered() { + final Intent intent = mContext.registerReceiver(null, new IntentFilter( + Intent.ACTION_BATTERY_CHANGED)); + final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + return pluggedState == BatteryManager.BATTERY_PLUGGED_AC + || pluggedState == BatteryManager.BATTERY_PLUGGED_USB; + } + + private boolean hasWifiConnection() { + final ConnectivityManager manager = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return wifiInfo.isConnected(); + } + + public void doUpload() { + final File[] files = mResearchLogDirectory.getUploadableLogFiles(); + if (files == null) return; + for (final File file : files) { + uploadFile(file); + } + } + + private void uploadFile(final File file) { + if (DEBUG) { + Log.d(TAG, "attempting upload of " + file.getAbsolutePath()); + } + final int contentLength = (int) file.length(); + HttpURLConnection connection = null; + InputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(file); + connection = (HttpURLConnection) mUrl.openConnection(); + connection.setRequestMethod("PUT"); + connection.setDoOutput(true); + connection.setFixedLengthStreamingMode(contentLength); + final OutputStream outputStream = connection.getOutputStream(); + uploadContents(fileInputStream, outputStream); + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + Log.d(TAG, "upload failed: " + connection.getResponseCode()); + final InputStream netInputStream = connection.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader( + netInputStream)); + String line; + while ((line = reader.readLine()) != null) { + Log.d(TAG, "| " + reader.readLine()); + } + reader.close(); + return; + } + file.delete(); + if (DEBUG) { + Log.d(TAG, "upload successful"); + } + } catch (final IOException e) { + Log.e(TAG, "Exception uploading file", e); + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (final IOException e) { + Log.e(TAG, "Exception closing uploaded file", e); + } + } + if (connection != null) { + connection.disconnect(); + } + } + } + + private static void uploadContents(final InputStream is, final OutputStream os) + throws IOException { + // TODO: Switch to NIO. + final byte[] buf = new byte[BUF_SIZE]; + int numBytesRead; + while ((numBytesRead = is.read(buf)) != -1) { + os.write(buf, 0, numBytesRead); + } + } +} diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java index 5a6b62704..6a9f5c1f4 100644 --- a/java/src/com/android/inputmethod/research/UploaderService.java +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -16,190 +16,45 @@ package com.android.inputmethod.research; -import android.Manifest; import android.app.AlarmManager; import android.app.IntentService; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.BatteryManager; import android.os.Bundle; -import android.util.Log; -import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.define.ProductionFlag; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - +/** + * Service to invoke the uploader. + * + * Can be regularly invoked, invoked on boot, etc. + */ public final class UploaderService extends IntentService { private static final String TAG = UploaderService.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - // Set IS_INHIBITING_AUTO_UPLOAD to true for local testing - private static final boolean IS_INHIBITING_AUTO_UPLOAD = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR; public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName() + ".extra.UPLOAD_UNCONDITIONALLY"; - private static final int BUF_SIZE = 1024 * 8; protected static final int TIMEOUT_IN_MS = 1000 * 4; - private boolean mCanUpload; - private File mFilesDir; - private URL mUrl; - public UploaderService() { super("Research Uploader Service"); } @Override - public void onCreate() { - super.onCreate(); - - mCanUpload = false; - mFilesDir = null; - mUrl = null; - - final PackageManager packageManager = getPackageManager(); - final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET, - getPackageName()) == PackageManager.PERMISSION_GRANTED; - if (!hasPermission) { - return; - } - - try { - final String urlString = getString(R.string.research_logger_upload_url); - if (urlString == null || urlString.equals("")) { - return; - } - mFilesDir = getFilesDir(); - mUrl = new URL(urlString); - mCanUpload = true; - } catch (MalformedURLException e) { - e.printStackTrace(); + protected void onHandleIntent(final Intent intent) { + final Uploader uploader = new Uploader(this); + if (!uploader.isPossibleToUpload()) return; + if (isUploadingUnconditionally(intent.getExtras()) || uploader.isConvenientToUpload()) { + uploader.doUpload(); } } - @Override - protected void onHandleIntent(Intent intent) { - if (!mCanUpload) { - return; + private boolean isUploadingUnconditionally(final Bundle bundle) { + if (bundle == null) return false; + if (bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) { + return bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY); } - boolean isUploadingUnconditionally = false; - Bundle bundle = intent.getExtras(); - if (bundle != null && bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) { - isUploadingUnconditionally = bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY); - } - doUpload(isUploadingUnconditionally); - } - - private boolean isExternallyPowered() { - final Intent intent = registerReceiver(null, new IntentFilter( - Intent.ACTION_BATTERY_CHANGED)); - final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); - return pluggedState == BatteryManager.BATTERY_PLUGGED_AC - || pluggedState == BatteryManager.BATTERY_PLUGGED_USB; - } - - private boolean hasWifiConnection() { - final ConnectivityManager manager = - (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - return wifiInfo.isConnected(); - } - - private void doUpload(final boolean isUploadingUnconditionally) { - if (!isUploadingUnconditionally && (!isExternallyPowered() || !hasWifiConnection() - || IS_INHIBITING_AUTO_UPLOAD)) { - return; - } - if (mFilesDir == null) { - return; - } - final File[] files = mFilesDir.listFiles(new FileFilter() { - @Override - public boolean accept(File pathname) { - return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX) - && !pathname.canWrite(); - } - }); - boolean success = true; - if (files.length == 0) { - success = false; - } - for (final File file : files) { - if (!uploadFile(file)) { - success = false; - } - } - } - - private boolean uploadFile(File file) { - if (DEBUG) { - Log.d(TAG, "attempting upload of " + file.getAbsolutePath()); - } - boolean success = false; - final int contentLength = (int) file.length(); - HttpURLConnection connection = null; - InputStream fileInputStream = null; - try { - fileInputStream = new FileInputStream(file); - connection = (HttpURLConnection) mUrl.openConnection(); - connection.setRequestMethod("PUT"); - connection.setDoOutput(true); - connection.setFixedLengthStreamingMode(contentLength); - final OutputStream os = connection.getOutputStream(); - final byte[] buf = new byte[BUF_SIZE]; - int numBytesRead; - while ((numBytesRead = fileInputStream.read(buf)) != -1) { - os.write(buf, 0, numBytesRead); - if (DEBUG) { - Log.d(TAG, new String(buf)); - } - } - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - Log.d(TAG, "upload failed: " + connection.getResponseCode()); - InputStream netInputStream = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(netInputStream)); - String line; - while ((line = reader.readLine()) != null) { - Log.d(TAG, "| " + reader.readLine()); - } - reader.close(); - return success; - } - file.delete(); - success = true; - if (DEBUG) { - Log.d(TAG, "upload successful"); - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (fileInputStream != null) { - try { - fileInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (connection != null) { - connection.disconnect(); - } - } - return success; + return false; } }