Merge commit '525bbec9eccbf5bd4581c2b9908e46f61c4431ad' into jb-mr2-dev
commit
ba0e497a0c
|
@ -21,7 +21,10 @@
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="english_ime_name" msgid="178705338187710493">"Klawiatura Android"</string>
|
<string name="english_ime_name" msgid="178705338187710493">"Klawiatura Android"</string>
|
||||||
<string name="spell_checker_service_name" msgid="6268342166872202903">"Sprawdzanie pisowni w Androidzie"</string>
|
<!-- no translation found for spell_checker_service_name (6268342166872202903) -->
|
||||||
<string name="english_ime_settings" msgid="7470027018752707691">"Ustawienia klawiatury Android"</string>
|
<skip />
|
||||||
<string name="android_spell_checker_settings" msgid="8397842018475560441">"Ustawienia sprawdzania pisowni"</string>
|
<!-- no translation found for english_ime_settings (7470027018752707691) -->
|
||||||
|
<skip />
|
||||||
|
<!-- no translation found for android_spell_checker_settings (8397842018475560441) -->
|
||||||
|
<skip />
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -308,6 +308,8 @@
|
||||||
- operation[CHAR LIMIT=100] -->
|
- operation[CHAR LIMIT=100] -->
|
||||||
<!-- TODO: remove translatable=false attribute once text is stable -->
|
<!-- TODO: remove translatable=false attribute once text is stable -->
|
||||||
<string name="research_feedback_demonstration_instructions" translatable="false">Please demonstrate the issue you are writing about.\n\nWhen finished, select the \"Bug?\" button again."</string>
|
<string name="research_feedback_demonstration_instructions" translatable="false">Please demonstrate the issue you are writing about.\n\nWhen finished, select the \"Bug?\" button again."</string>
|
||||||
|
<!-- Title of a preference to send feedback. [CHAR LIMIT=30]-->
|
||||||
|
<string name="send_feedback">Send feedback</string>
|
||||||
<!-- Temporary notification of recording failure [CHAR LIMIT=100] -->
|
<!-- Temporary notification of recording failure [CHAR LIMIT=100] -->
|
||||||
<!-- TODO: remove translatable=false attribute once text is stable -->
|
<!-- TODO: remove translatable=false attribute once text is stable -->
|
||||||
<string name="research_feedback_recording_failure" translatable="false">Recording cancelled due to timeout</string>
|
<string name="research_feedback_recording_failure" translatable="false">Recording cancelled due to timeout</string>
|
||||||
|
|
|
@ -176,6 +176,9 @@
|
||||||
android:key="pref_show_setup_wizard_icon"
|
android:key="pref_show_setup_wizard_icon"
|
||||||
android:title="@string/show_setup_wizard_icon" />
|
android:title="@string/show_setup_wizard_icon" />
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
<PreferenceScreen
|
||||||
|
android:key="send_feedback"
|
||||||
|
android:title="@string/send_feedback" />
|
||||||
<PreferenceScreen
|
<PreferenceScreen
|
||||||
android:key="debug_settings"
|
android:key="debug_settings"
|
||||||
android:title="Debug settings"
|
android:title="Debug settings"
|
||||||
|
|
|
@ -57,7 +57,7 @@ public final class DebugSettings extends PreferenceFragment
|
||||||
if (usabilityStudyPref instanceof CheckBoxPreference) {
|
if (usabilityStudyPref instanceof CheckBoxPreference) {
|
||||||
final CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
|
final CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
|
||||||
checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE,
|
checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE,
|
||||||
ResearchLogger.DEFAULT_USABILITY_STUDY_MODE));
|
LatinImeLogger.getUsabilityStudyMode(prefs)));
|
||||||
checkbox.setSummary(R.string.settings_warning_researcher_mode);
|
checkbox.setSummary(R.string.settings_warning_researcher_mode);
|
||||||
}
|
}
|
||||||
final Preference statisticsLoggingPref = findPreference(PREF_STATISTICS_LOGGING);
|
final Preference statisticsLoggingPref = findPreference(PREF_STATISTICS_LOGGING);
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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.latin;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class FeedbackUtils {
|
||||||
|
public static boolean isFeedbackFormSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showFeedbackForm(Context context) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -430,7 +430,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
|
||||||
initSuggest();
|
initSuggest();
|
||||||
|
|
||||||
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
||||||
ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
|
ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest);
|
||||||
}
|
}
|
||||||
mDisplayOrientation = getResources().getConfiguration().orientation;
|
mDisplayOrientation = getResources().getConfiguration().orientation;
|
||||||
|
|
||||||
|
@ -565,6 +565,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
|
||||||
}
|
}
|
||||||
mSettings.onDestroy();
|
mSettings.onDestroy();
|
||||||
unregisterReceiver(mReceiver);
|
unregisterReceiver(mReceiver);
|
||||||
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
||||||
|
ResearchLogger.getInstance().onDestroy();
|
||||||
|
}
|
||||||
// TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
|
// TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
|
||||||
// Service yet.
|
// Service yet.
|
||||||
if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
||||||
|
|
|
@ -37,6 +37,10 @@ public final class LatinImeLogger implements SharedPreferences.OnSharedPreferenc
|
||||||
public static void commit() {
|
public static void commit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean getUsabilityStudyMode(final SharedPreferences prefs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static void onDestroy() {
|
public static void onDestroy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
||||||
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
|
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
|
||||||
"pref_suppress_language_switch_key";
|
"pref_suppress_language_switch_key";
|
||||||
|
|
||||||
|
public static final String PREF_SEND_FEEDBACK = "send_feedback";
|
||||||
|
|
||||||
private Resources mRes;
|
private Resources mRes;
|
||||||
private SharedPreferences mPrefs;
|
private SharedPreferences mPrefs;
|
||||||
private Locale mCurrentLocale;
|
private Locale mCurrentLocale;
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin;
|
package com.android.inputmethod.latin;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.backup.BackupManager;
|
import android.app.backup.BackupManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
@ -26,6 +27,7 @@ import android.os.Bundle;
|
||||||
import android.preference.CheckBoxPreference;
|
import android.preference.CheckBoxPreference;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
|
import android.preference.Preference.OnPreferenceClickListener;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.view.inputmethod.InputMethodSubtype;
|
import android.view.inputmethod.InputMethodSubtype;
|
||||||
|
@ -103,6 +105,25 @@ public final class SettingsFragment extends InputMethodSettingsFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Preference feedbackSettings = findPreference(Settings.PREF_SEND_FEEDBACK);
|
||||||
|
if (feedbackSettings != null) {
|
||||||
|
if (FeedbackUtils.isFeedbackFormSupported()) {
|
||||||
|
feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceClick(Preference arg0) {
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
FeedbackUtils.showFeedbackForm(activity);
|
||||||
|
if (!activity.isFinishing()) {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
miscSettings.removePreference(feedbackSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final boolean showVoiceKeyOption = res.getBoolean(
|
final boolean showVoiceKeyOption = res.getBoolean(
|
||||||
R.bool.config_enable_show_voice_key_option);
|
R.bool.config_enable_show_voice_key_option);
|
||||||
if (!showVoiceKeyOption) {
|
if (!showVoiceKeyOption) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import android.os.Process;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.android.inputmethod.annotations.UsedForTesting;
|
||||||
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
@ -77,6 +78,7 @@ public final class Utils {
|
||||||
private RingCharBuffer() {
|
private RingCharBuffer() {
|
||||||
// Intentional empty constructor for singleton.
|
// Intentional empty constructor for singleton.
|
||||||
}
|
}
|
||||||
|
@UsedForTesting
|
||||||
public static RingCharBuffer getInstance() {
|
public static RingCharBuffer getInstance() {
|
||||||
return sRingCharBuffer;
|
return sRingCharBuffer;
|
||||||
}
|
}
|
||||||
|
@ -93,6 +95,7 @@ public final class Utils {
|
||||||
return ret < 0 ? ret + BUFSIZE : ret;
|
return ret < 0 ? ret + BUFSIZE : ret;
|
||||||
}
|
}
|
||||||
// TODO: accept code points
|
// TODO: accept code points
|
||||||
|
@UsedForTesting
|
||||||
public void push(char c, int x, int y) {
|
public void push(char c, int x, int y) {
|
||||||
if (!mEnabled) return;
|
if (!mEnabled) return;
|
||||||
mCharBuf[mEnd] = c;
|
mCharBuf[mEnd] = c;
|
||||||
|
|
|
@ -28,5 +28,5 @@ public final class ProductionFlag {
|
||||||
// USES_DEVELOPMENT_ONLY_DIAGNOSTICS must be false for any production build.
|
// USES_DEVELOPMENT_ONLY_DIAGNOSTICS must be false for any production build.
|
||||||
public static final boolean USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG = false;
|
public static final boolean USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG = false;
|
||||||
|
|
||||||
public static final boolean IS_HARDWARE_KEYBOARD_SUPPORTED = false;
|
public static final boolean IS_HARDWARE_KEYBOARD_SUPPORTED = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package com.android.inputmethod.research;
|
package com.android.inputmethod.research;
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.JsonWriter;
|
import android.util.JsonWriter;
|
||||||
|
@ -45,7 +44,7 @@ import java.util.List;
|
||||||
* will not violate the user's privacy. Checks for this may include whether other LogUnits have
|
* will not violate the user's privacy. Checks for this may include whether other LogUnits have
|
||||||
* been published recently, or whether the LogUnit contains numbers, etc.
|
* been published recently, or whether the LogUnit contains numbers, etc.
|
||||||
*/
|
*/
|
||||||
/* package */ class LogUnit {
|
public class LogUnit {
|
||||||
private static final String TAG = LogUnit.class.getSimpleName();
|
private static final String TAG = LogUnit.class.getSimpleName();
|
||||||
private static final boolean DEBUG = false
|
private static final boolean DEBUG = false
|
||||||
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
|
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
|
||||||
|
@ -121,22 +120,6 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public synchronized void publishTo(final ResearchLog researchLog,
|
public synchronized void publishTo(final ResearchLog researchLog,
|
||||||
final boolean canIncludePrivateData) {
|
final boolean canIncludePrivateData) {
|
||||||
// Prepare debugging output if necessary
|
|
||||||
final StringWriter debugStringWriter;
|
|
||||||
final JsonWriter debugJsonWriter;
|
|
||||||
if (DEBUG) {
|
|
||||||
debugStringWriter = new StringWriter();
|
|
||||||
debugJsonWriter = new JsonWriter(debugStringWriter);
|
|
||||||
debugJsonWriter.setIndent(" ");
|
|
||||||
try {
|
|
||||||
debugJsonWriter.beginArray();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Could not open array in JsonWriter", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debugStringWriter = null;
|
|
||||||
debugJsonWriter = null;
|
|
||||||
}
|
|
||||||
// Write out any logStatement that passes the privacy filter.
|
// Write out any logStatement that passes the privacy filter.
|
||||||
final int size = mLogStatementList.size();
|
final int size = mLogStatementList.size();
|
||||||
if (size != 0) {
|
if (size != 0) {
|
||||||
|
@ -159,29 +142,12 @@ import java.util.List;
|
||||||
outputLogUnitStart(jsonWriter, canIncludePrivateData);
|
outputLogUnitStart(jsonWriter, canIncludePrivateData);
|
||||||
}
|
}
|
||||||
logStatement.outputToLocked(jsonWriter, mTimeList.get(i), mValuesList.get(i));
|
logStatement.outputToLocked(jsonWriter, mTimeList.get(i), mValuesList.get(i));
|
||||||
if (DEBUG) {
|
|
||||||
logStatement.outputToLocked(debugJsonWriter, mTimeList.get(i),
|
|
||||||
mValuesList.get(i));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (jsonWriter != null) {
|
if (jsonWriter != null) {
|
||||||
// We must have called logUnitStart earlier, so emit a logUnitStop.
|
// We must have called logUnitStart earlier, so emit a logUnitStop.
|
||||||
outputLogUnitStop(jsonWriter);
|
outputLogUnitStop(jsonWriter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (DEBUG) {
|
|
||||||
try {
|
|
||||||
debugJsonWriter.endArray();
|
|
||||||
debugJsonWriter.flush();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Could not close array in JsonWriter", e);
|
|
||||||
}
|
|
||||||
final String bigString = debugStringWriter.getBuffer().toString();
|
|
||||||
final String[] lines = bigString.split("\n");
|
|
||||||
for (String line : lines) {
|
|
||||||
Log.d(TAG, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String WORD_KEY = "_wo";
|
private static final String WORD_KEY = "_wo";
|
||||||
|
|
|
@ -18,6 +18,7 @@ package com.android.inputmethod.research;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.android.inputmethod.annotations.UsedForTesting;
|
||||||
import com.android.inputmethod.latin.Dictionary;
|
import com.android.inputmethod.latin.Dictionary;
|
||||||
import com.android.inputmethod.latin.Suggest;
|
import com.android.inputmethod.latin.Suggest;
|
||||||
import com.android.inputmethod.latin.define.ProductionFlag;
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
|
@ -65,7 +66,11 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
|
||||||
// The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams.
|
// The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams.
|
||||||
public static final int N_GRAM_SIZE = 2;
|
public static final int N_GRAM_SIZE = 2;
|
||||||
|
|
||||||
private Suggest mSuggest;
|
// TODO: Remove dependence on Suggest, and pass in Dictionary as a parameter to an appropriate
|
||||||
|
// method.
|
||||||
|
private final Suggest mSuggest;
|
||||||
|
@UsedForTesting
|
||||||
|
private Dictionary mDictionaryForTesting;
|
||||||
private boolean mIsStopping = false;
|
private boolean mIsStopping = false;
|
||||||
|
|
||||||
/* package for test */ int mNumWordsBetweenNGrams;
|
/* package for test */ int mNumWordsBetweenNGrams;
|
||||||
|
@ -74,17 +79,23 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
|
||||||
// after a sample is taken.
|
// after a sample is taken.
|
||||||
/* package for test */ int mNumWordsUntilSafeToSample;
|
/* package for test */ int mNumWordsUntilSafeToSample;
|
||||||
|
|
||||||
public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore) {
|
public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore,
|
||||||
|
final Suggest suggest) {
|
||||||
super(N_GRAM_SIZE + wordsBetweenSamples);
|
super(N_GRAM_SIZE + wordsBetweenSamples);
|
||||||
mNumWordsBetweenNGrams = wordsBetweenSamples;
|
mNumWordsBetweenNGrams = wordsBetweenSamples;
|
||||||
mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore;
|
mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore;
|
||||||
}
|
|
||||||
|
|
||||||
public void setSuggest(final Suggest suggest) {
|
|
||||||
mSuggest = suggest;
|
mSuggest = suggest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
/* package for test */ void setDictionaryForTesting(final Dictionary dictionary) {
|
||||||
|
mDictionaryForTesting = dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
private Dictionary getDictionary() {
|
private Dictionary getDictionary() {
|
||||||
|
if (mDictionaryForTesting != null) {
|
||||||
|
return mDictionaryForTesting;
|
||||||
|
}
|
||||||
if (mSuggest == null || !mSuggest.hasMainDictionary()) return null;
|
if (mSuggest == null || !mSuggest.hasMainDictionary()) return null;
|
||||||
return mSuggest.getMainDictionary();
|
return mSuggest.getMainDictionary();
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,11 @@ import android.content.Context;
|
||||||
import android.util.JsonWriter;
|
import android.util.JsonWriter;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.android.inputmethod.annotations.UsedForTesting;
|
||||||
import com.android.inputmethod.latin.define.ProductionFlag;
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
|
@ -38,18 +38,24 @@ import java.util.concurrent.TimeUnit;
|
||||||
/**
|
/**
|
||||||
* Logs the use of the LatinIME keyboard.
|
* Logs the use of the LatinIME keyboard.
|
||||||
*
|
*
|
||||||
* This class logs operations on the IME keyboard, including what the user has typed.
|
* This class logs operations on the IME keyboard, including what the user has typed. Data is
|
||||||
* Data is stored locally in a file in app-specific storage.
|
* written to a {@link JsonWriter}, which will write to a local file.
|
||||||
|
*
|
||||||
|
* The JsonWriter is created on-demand by calling {@link #getInitializedJsonWriterLocked}.
|
||||||
|
*
|
||||||
|
* This class uses an executor to perform file-writing operations on a separate thread. It also
|
||||||
|
* tries to avoid creating unnecessary files if there is nothing to write. It also handles
|
||||||
|
* flushing, making sure it happens, but not too frequently.
|
||||||
*
|
*
|
||||||
* This functionality is off by default. See
|
* This functionality is off by default. See
|
||||||
* {@link ProductionFlag#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}.
|
* {@link ProductionFlag#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}.
|
||||||
*/
|
*/
|
||||||
public class ResearchLog {
|
public class ResearchLog {
|
||||||
|
// TODO: Automatically initialize the JsonWriter rather than requiring the caller to manage it.
|
||||||
private static final String TAG = ResearchLog.class.getSimpleName();
|
private static final String TAG = ResearchLog.class.getSimpleName();
|
||||||
private static final boolean DEBUG = false
|
private static final boolean DEBUG = false
|
||||||
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
|
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
|
||||||
private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
|
private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
|
||||||
private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4;
|
|
||||||
|
|
||||||
/* package */ final ScheduledExecutorService mExecutor;
|
/* package */ final ScheduledExecutorService mExecutor;
|
||||||
/* package */ final File mFile;
|
/* package */ final File mFile;
|
||||||
|
@ -89,28 +95,33 @@ public class ResearchLog {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void close(final Runnable onClosed) {
|
/**
|
||||||
|
* Waits for any publication requests to finish and closes the {@link JsonWriter} used for
|
||||||
|
* output.
|
||||||
|
*
|
||||||
|
* See class comment for details about {@code JsonWriter} construction.
|
||||||
|
*
|
||||||
|
* @param onClosed run after the close() operation has completed asynchronously
|
||||||
|
*/
|
||||||
|
private synchronized void close(final Runnable onClosed) {
|
||||||
mExecutor.submit(new Callable<Object>() {
|
mExecutor.submit(new Callable<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object call() throws Exception {
|
public Object call() throws Exception {
|
||||||
try {
|
try {
|
||||||
if (mHasWrittenData) {
|
if (mHasWrittenData) {
|
||||||
mJsonWriter.endArray();
|
mJsonWriter.endArray();
|
||||||
mJsonWriter.flush();
|
|
||||||
mJsonWriter.close();
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "wrote log to " + mFile);
|
|
||||||
}
|
|
||||||
mHasWrittenData = false;
|
mHasWrittenData = false;
|
||||||
} else {
|
}
|
||||||
if (DEBUG) {
|
mJsonWriter.flush();
|
||||||
Log.d(TAG, "close() called, but no data, not outputting");
|
mJsonWriter.close();
|
||||||
}
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "wrote log to " + mFile);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.d(TAG, "error when closing ResearchLog:");
|
Log.d(TAG, "error when closing ResearchLog:", e);
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
// Marking the file as read-only signals that this log file is ready to be
|
||||||
|
// uploaded.
|
||||||
if (mFile != null && mFile.exists()) {
|
if (mFile != null && mFile.exists()) {
|
||||||
mFile.setWritable(false, false);
|
mFile.setWritable(false, false);
|
||||||
}
|
}
|
||||||
|
@ -125,9 +136,24 @@ public class ResearchLog {
|
||||||
mExecutor.shutdown();
|
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<Object>() {
|
mExecutor.submit(new Callable<Object>() {
|
||||||
@Override
|
@Override
|
||||||
public Object call() throws Exception {
|
public Object call() throws Exception {
|
||||||
|
@ -139,7 +165,10 @@ public class ResearchLog {
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mFile != null) {
|
if (mFile != null) {
|
||||||
mIsAbortSuccessful = mFile.delete();
|
mFile.delete();
|
||||||
|
}
|
||||||
|
if (onAbort != null) {
|
||||||
|
onAbort.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -149,14 +178,25 @@ public class ResearchLog {
|
||||||
mExecutor.shutdown();
|
mExecutor.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean blockingAbort() throws InterruptedException {
|
/**
|
||||||
abort();
|
* Block until the research log has aborted or {@code timeout} occurs.
|
||||||
mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
|
*
|
||||||
return mIsAbortSuccessful;
|
* @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 {
|
@UsedForTesting
|
||||||
mExecutor.awaitTermination(delay, timeUnit);
|
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() {
|
/* package */ synchronized void flush() {
|
||||||
|
@ -186,6 +226,12 @@ public class ResearchLog {
|
||||||
mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
|
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) {
|
public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) {
|
||||||
try {
|
try {
|
||||||
mExecutor.submit(new Callable<Object>() {
|
mExecutor.submit(new Callable<Object>() {
|
||||||
|
@ -196,10 +242,10 @@ public class ResearchLog {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (RejectedExecutionException e) {
|
} catch (final RejectedExecutionException e) {
|
||||||
// TODO: Add code to record loss of data, and report.
|
// TODO: Add code to record loss of data, and report.
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution");
|
Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
// field holds a channel name, the developer does not have to re-enter it when using the
|
||||||
// feedback mechanism to generate multiple tests.
|
// feedback mechanism to generate multiple tests.
|
||||||
private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false;
|
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;
|
/* package */ static boolean sIsLogging = false;
|
||||||
private static final int OUTPUT_FORMAT_VERSION = 5;
|
private static final int OUTPUT_FORMAT_VERSION = 5;
|
||||||
private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
|
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
|
// Whether all words should be recorded, leaving unsampled word between bigrams. Useful for
|
||||||
// testing.
|
// testing.
|
||||||
/* package for test */ static final boolean IS_LOGGING_EVERYTHING = false
|
/* 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
|
// constants related to specific log points
|
||||||
private static final String WHITESPACE_SEPARATORS = " \t\n\r";
|
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 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 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 final ResearchLogger sInstance = new ResearchLogger();
|
||||||
private static String sAccountType = null;
|
private static String sAccountType = null;
|
||||||
private static String sAllowedAccountDomain = 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;
|
/* package */ ResearchLog mMainResearchLog;
|
||||||
// mFeedbackLog records all events for the session, private or not (excepting
|
// mFeedbackLog records all events for the session, private or not (excepting
|
||||||
// passwords). It is written to permanent storage only if the user explicitly commands
|
// 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"
|
Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area"
|
||||||
// U+E001 is in the "private-use area"
|
// U+E001 is in the "private-use area"
|
||||||
/* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
|
/* 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;
|
protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
|
||||||
// set when LatinIME should ignore an onUpdateSelection() callback that
|
// set when LatinIME should ignore an onUpdateSelection() callback that
|
||||||
// arises from operations in this class
|
// arises from operations in this class
|
||||||
|
@ -206,11 +196,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
private final Statistics mStatistics;
|
private final Statistics mStatistics;
|
||||||
private final MotionEventReader mMotionEventReader = new MotionEventReader();
|
private final MotionEventReader mMotionEventReader = new MotionEventReader();
|
||||||
private final Replayer mReplayer = Replayer.getInstance();
|
private final Replayer mReplayer = Replayer.getInstance();
|
||||||
|
private ResearchLogDirectory mResearchLogDirectory;
|
||||||
|
|
||||||
private Intent mUploadIntent;
|
private Intent mUploadIntent;
|
||||||
private Intent mUploadNowIntent;
|
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.
|
// 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,
|
// 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;
|
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;
|
assert latinIME != null;
|
||||||
if (latinIME == null) {
|
mLatinIME = latinIME;
|
||||||
Log.w(TAG, "IMS is null; logging is off");
|
mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
|
||||||
} else {
|
mPrefs.registerOnSharedPreferenceChangeListener(this);
|
||||||
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);
|
|
||||||
|
|
||||||
final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
|
// Initialize fields from preferences
|
||||||
final long now = System.currentTimeMillis();
|
sIsLogging = ResearchSettings.readResearchLoggerEnabledFlag(mPrefs);
|
||||||
if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) {
|
|
||||||
final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
|
// Initialize fields from resources
|
||||||
cleanupLoggingDir(mFilesDir, timeHorizon);
|
|
||||||
Editor e = prefs.edit();
|
|
||||||
e.putLong(PREF_LAST_CLEANUP_TIME, now);
|
|
||||||
e.apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final Resources res = latinIME.getResources();
|
final Resources res = latinIME.getResources();
|
||||||
sAccountType = res.getString(R.string.research_account_type);
|
sAccountType = res.getString(R.string.research_account_type);
|
||||||
sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain);
|
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);
|
mUploadIntent = new Intent(mLatinIME, UploaderService.class);
|
||||||
mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
|
mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
|
||||||
mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
|
mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
|
||||||
mReplayer.setKeyboardSwitcher(keyboardSwitcher);
|
|
||||||
|
|
||||||
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
|
||||||
scheduleUploadingService(mLatinIME);
|
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);
|
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) {
|
public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
|
||||||
mMainKeyboardView = mainKeyboardView;
|
mMainKeyboardView = mainKeyboardView;
|
||||||
maybeShowSplashScreen();
|
maybeShowSplashScreen();
|
||||||
|
@ -324,14 +296,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
mMainKeyboardView = null;
|
mMainKeyboardView = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasSeenSplash() {
|
public void onDestroy() {
|
||||||
return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false);
|
if (mPrefs != null) {
|
||||||
|
mPrefs.unregisterOnSharedPreferenceChangeListener(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dialog mSplashDialog = null;
|
private Dialog mSplashDialog = null;
|
||||||
|
|
||||||
private void maybeShowSplashScreen() {
|
private void maybeShowSplashScreen() {
|
||||||
if (hasSeenSplash()) {
|
if (ResearchSettings.readHasSeenSplash(mPrefs)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mSplashDialog != null && mSplashDialog.isShowing()) {
|
if (mSplashDialog != null && mSplashDialog.isShowing()) {
|
||||||
|
@ -384,53 +358,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onUserLoggingConsent() {
|
public void onUserLoggingConsent() {
|
||||||
setLoggingAllowed(true);
|
|
||||||
if (mPrefs == null) {
|
if (mPrefs == null) {
|
||||||
return;
|
mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
|
||||||
|
if (mPrefs == null) return;
|
||||||
}
|
}
|
||||||
final Editor e = mPrefs.edit();
|
sIsLogging = true;
|
||||||
e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true);
|
ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, true);
|
||||||
e.apply();
|
ResearchSettings.writeHasSeenSplash(mPrefs, true);
|
||||||
restart();
|
restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLoggingAllowed(boolean enableLogging) {
|
private void setLoggingAllowed(final boolean enableLogging) {
|
||||||
if (mPrefs == null) {
|
if (mPrefs == null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
Editor e = mPrefs.edit();
|
|
||||||
e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging);
|
|
||||||
e.apply();
|
|
||||||
sIsLogging = enableLogging;
|
sIsLogging = enableLogging;
|
||||||
}
|
ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkForEmptyEditor() {
|
private void checkForEmptyEditor() {
|
||||||
|
@ -469,14 +410,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
// Log.w(TAG, "not in usability mode; not logging");
|
// Log.w(TAG, "not in usability mode; not logging");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mFilesDir == null || !mFilesDir.exists()) {
|
|
||||||
Log.w(TAG, "IME storage directory does not exist. Cannot start logging.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mMainLogBuffer == null) {
|
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);
|
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
|
@Override
|
||||||
protected void publish(final ArrayList<LogUnit> logUnits,
|
protected void publish(final ArrayList<LogUnit> logUnits,
|
||||||
boolean canIncludePrivateData) {
|
boolean canIncludePrivateData) {
|
||||||
|
@ -499,7 +438,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
mMainLogBuffer.setSuggest(mSuggest);
|
|
||||||
}
|
}
|
||||||
if (mFeedbackLogBuffer == null) {
|
if (mFeedbackLogBuffer == null) {
|
||||||
resetFeedbackLogging();
|
resetFeedbackLogging();
|
||||||
|
@ -507,7 +445,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetFeedbackLogging() {
|
private void resetFeedbackLogging() {
|
||||||
mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
|
mFeedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
|
||||||
|
System.currentTimeMillis()), mLatinIME);
|
||||||
mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
|
mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -524,42 +463,29 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
commitCurrentLogUnit();
|
commitCurrentLogUnit();
|
||||||
mMainLogBuffer.setIsStopping();
|
mMainLogBuffer.setIsStopping();
|
||||||
mMainLogBuffer.shiftAndPublishAll();
|
mMainLogBuffer.shiftAndPublishAll();
|
||||||
mMainResearchLog.close(null /* callback */);
|
mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
|
||||||
mMainLogBuffer = null;
|
mMainLogBuffer = null;
|
||||||
}
|
}
|
||||||
if (mFeedbackLogBuffer != null) {
|
if (mFeedbackLogBuffer != null) {
|
||||||
mFeedbackLog.close(null /* callback */);
|
mFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
|
||||||
mFeedbackLogBuffer = null;
|
mFeedbackLogBuffer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean abort() {
|
public void abort() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "abort called");
|
Log.d(TAG, "abort called");
|
||||||
}
|
}
|
||||||
boolean didAbortMainLog = false;
|
|
||||||
if (mMainLogBuffer != null) {
|
if (mMainLogBuffer != null) {
|
||||||
mMainLogBuffer.clear();
|
mMainLogBuffer.clear();
|
||||||
try {
|
mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
|
||||||
didAbortMainLog = mMainResearchLog.blockingAbort();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// Don't know whether this succeeded or not. We assume not; this is reported
|
|
||||||
// to the caller.
|
|
||||||
}
|
|
||||||
mMainLogBuffer = null;
|
mMainLogBuffer = null;
|
||||||
}
|
}
|
||||||
boolean didAbortFeedbackLog = false;
|
|
||||||
if (mFeedbackLogBuffer != null) {
|
if (mFeedbackLogBuffer != null) {
|
||||||
mFeedbackLogBuffer.clear();
|
mFeedbackLogBuffer.clear();
|
||||||
try {
|
mFeedbackLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
|
||||||
didAbortFeedbackLog = mFeedbackLog.blockingAbort();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// Don't know whether this succeeded or not. We assume not; this is reported
|
|
||||||
// to the caller.
|
|
||||||
}
|
|
||||||
mFeedbackLogBuffer = null;
|
mFeedbackLogBuffer = null;
|
||||||
}
|
}
|
||||||
return didAbortMainLog && didAbortFeedbackLog;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restart() {
|
private void restart() {
|
||||||
|
@ -576,7 +502,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
|
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
|
||||||
if (key == null || prefs == null) {
|
if (key == null || prefs == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -598,7 +524,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
presentFeedbackDialog(latinIME);
|
presentFeedbackDialog(latinIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void presentFeedbackDialog(LatinIME latinIME) {
|
public void presentFeedbackDialog(final LatinIME latinIME) {
|
||||||
if (isMakingUserRecording()) {
|
if (isMakingUserRecording()) {
|
||||||
saveRecording();
|
saveRecording();
|
||||||
}
|
}
|
||||||
|
@ -642,9 +568,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
|
|
||||||
private void startRecordingInternal() {
|
private void startRecordingInternal() {
|
||||||
if (mUserRecordingLog != null) {
|
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);
|
mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
|
||||||
mUserRecordingLogBuffer = new LogBuffer();
|
mUserRecordingLogBuffer = new LogBuffer();
|
||||||
resetRecordingTimer();
|
resetRecordingTimer();
|
||||||
|
@ -680,7 +607,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
|
|
||||||
private void cancelRecording() {
|
private void cancelRecording() {
|
||||||
if (mUserRecordingLog != null) {
|
if (mUserRecordingLog != null) {
|
||||||
mUserRecordingLog.abort();
|
mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
|
||||||
}
|
}
|
||||||
mUserRecordingLog = null;
|
mUserRecordingLog = null;
|
||||||
mUserRecordingLogBuffer = null;
|
mUserRecordingLogBuffer = null;
|
||||||
|
@ -692,7 +619,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
private void saveRecording() {
|
private void saveRecording() {
|
||||||
commitCurrentLogUnit();
|
commitCurrentLogUnit();
|
||||||
publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true);
|
publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true);
|
||||||
mUserRecordingLog.close(null);
|
mUserRecordingLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
|
||||||
mUserRecordingLog = null;
|
mUserRecordingLog = null;
|
||||||
mUserRecordingLogBuffer = null;
|
mUserRecordingLogBuffer = null;
|
||||||
|
|
||||||
|
@ -804,12 +731,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
feedbackContents, accountName, recording);
|
feedbackContents, accountName, recording);
|
||||||
mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
|
mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
|
||||||
publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
|
publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
|
||||||
mSavedFeedbackLog.close(new Runnable() {
|
mSavedFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
|
||||||
@Override
|
uploadNow();
|
||||||
public void run() {
|
|
||||||
uploadNow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) {
|
if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) {
|
||||||
final Handler handler = new Handler();
|
final Handler handler = new Handler();
|
||||||
|
@ -830,9 +753,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
if (mPrefs == null) {
|
if (mPrefs == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Editor e = mPrefs.edit();
|
mPrefs.edit().putString(PREF_RESEARCH_SAVED_CHANNEL, channelName).apply();
|
||||||
e.putString(PREF_RESEARCH_SAVED_CHANNEL, channelName);
|
|
||||||
e.apply();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -847,10 +768,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
mInFeedbackDialog = false;
|
mInFeedbackDialog = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initSuggest(Suggest suggest) {
|
public void initSuggest(final Suggest suggest) {
|
||||||
mSuggest = suggest;
|
mSuggest = suggest;
|
||||||
|
// MainLogBuffer has out-of-date Suggest object. Need to close it down and create a new
|
||||||
|
// one.
|
||||||
if (mMainLogBuffer != null) {
|
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) {
|
private String scrubWord(String word) {
|
||||||
final Dictionary dictionary = getDictionary();
|
final Dictionary dictionary = getDictionary();
|
||||||
if (dictionary == null) {
|
if (dictionary == null) {
|
||||||
|
@ -1197,9 +1109,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
|
||||||
0);
|
0);
|
||||||
final Integer versionCode = packageInfo.versionCode;
|
final Integer versionCode = packageInfo.versionCode;
|
||||||
final String versionName = packageInfo.versionName;
|
final String versionName = packageInfo.versionName;
|
||||||
|
final String uuid = ResearchSettings.readResearchLoggerUuid(researchLogger.mPrefs);
|
||||||
researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL,
|
researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL,
|
||||||
researchLogger.mUUIDString, editorInfo.packageName,
|
uuid, editorInfo.packageName, Integer.toHexString(editorInfo.inputType),
|
||||||
Integer.toHexString(editorInfo.inputType),
|
|
||||||
Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
|
Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
|
||||||
Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
|
Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
|
||||||
OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING,
|
OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING,
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,190 +16,45 @@
|
||||||
|
|
||||||
package com.android.inputmethod.research;
|
package com.android.inputmethod.research;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.app.AlarmManager;
|
import android.app.AlarmManager;
|
||||||
import android.app.IntentService;
|
import android.app.IntentService;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
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.os.Bundle;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.android.inputmethod.latin.R;
|
|
||||||
import com.android.inputmethod.latin.define.ProductionFlag;
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
/**
|
||||||
import java.io.File;
|
* Service to invoke the uploader.
|
||||||
import java.io.FileFilter;
|
*
|
||||||
import java.io.FileInputStream;
|
* Can be regularly invoked, invoked on boot, etc.
|
||||||
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;
|
|
||||||
|
|
||||||
public final class UploaderService extends IntentService {
|
public final class UploaderService extends IntentService {
|
||||||
private static final String TAG = UploaderService.class.getSimpleName();
|
private static final String TAG = UploaderService.class.getSimpleName();
|
||||||
private static final boolean DEBUG = false
|
private static final boolean DEBUG = false
|
||||||
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
|
&& 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 long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
|
||||||
public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
|
public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
|
||||||
+ ".extra.UPLOAD_UNCONDITIONALLY";
|
+ ".extra.UPLOAD_UNCONDITIONALLY";
|
||||||
private static final int BUF_SIZE = 1024 * 8;
|
|
||||||
protected static final int TIMEOUT_IN_MS = 1000 * 4;
|
protected static final int TIMEOUT_IN_MS = 1000 * 4;
|
||||||
|
|
||||||
private boolean mCanUpload;
|
|
||||||
private File mFilesDir;
|
|
||||||
private URL mUrl;
|
|
||||||
|
|
||||||
public UploaderService() {
|
public UploaderService() {
|
||||||
super("Research Uploader Service");
|
super("Research Uploader Service");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
protected void onHandleIntent(final Intent intent) {
|
||||||
super.onCreate();
|
final Uploader uploader = new Uploader(this);
|
||||||
|
if (!uploader.isPossibleToUpload()) return;
|
||||||
mCanUpload = false;
|
if (isUploadingUnconditionally(intent.getExtras()) || uploader.isConvenientToUpload()) {
|
||||||
mFilesDir = null;
|
uploader.doUpload();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean isUploadingUnconditionally(final Bundle bundle) {
|
||||||
protected void onHandleIntent(Intent intent) {
|
if (bundle == null) return false;
|
||||||
if (!mCanUpload) {
|
if (bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) {
|
||||||
return;
|
return bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY);
|
||||||
}
|
}
|
||||||
boolean isUploadingUnconditionally = false;
|
return 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue