diff options
Diffstat (limited to 'java/src')
15 files changed, 1015 insertions, 814 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 1b309a439..0cc0b6320 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -110,7 +110,6 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key new WeakHashMap<Key, MoreKeysPanel>(); private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint; - private final PointerTrackerParams mPointerTrackerParams; private final SuddenJumpingTouchEventHandler mTouchScreenRegulator; protected KeyDetector mKeyDetector; @@ -127,11 +126,26 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key private static final int MSG_LONGPRESS_KEY = 2; private static final int MSG_DOUBLE_TAP = 3; - private final KeyTimerParams mParams; + private final int mKeyRepeatStartTimeout; + private final int mKeyRepeatInterval; + private final int mLongPressKeyTimeout; + private final int mLongPressShiftKeyTimeout; + private final int mIgnoreAltCodeKeyTimeout; - public KeyTimerHandler(MainKeyboardView outerInstance, KeyTimerParams params) { + public KeyTimerHandler(final MainKeyboardView outerInstance, + final TypedArray mainKeyboardViewAttr) { super(outerInstance); - mParams = params; + + mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0); + mKeyRepeatInterval = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyRepeatInterval, 0); + mLongPressKeyTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_longPressKeyTimeout, 0); + mLongPressShiftKeyTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_longPressShiftKeyTimeout, 0); + mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); } @Override @@ -146,7 +160,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key final Key currentKey = tracker.getKey(); if (currentKey != null && currentKey.mCode == msg.arg1) { tracker.onRegisterKey(currentKey); - startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval); + startKeyRepeatTimer(tracker, mKeyRepeatInterval); } break; case MSG_LONGPRESS_KEY: @@ -167,7 +181,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key @Override public void startKeyRepeatTimer(PointerTracker tracker) { - startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout); + startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout); } public void cancelKeyRepeatTimer() { @@ -185,7 +199,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key final int delay; switch (code) { case Keyboard.CODE_SHIFT: - delay = mParams.mLongPressShiftKeyTimeout; + delay = mLongPressShiftKeyTimeout; break; default: delay = 0; @@ -206,15 +220,15 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key final int delay; switch (key.mCode) { case Keyboard.CODE_SHIFT: - delay = mParams.mLongPressShiftKeyTimeout; + delay = mLongPressShiftKeyTimeout; break; default: if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) { // We use longer timeout for sliding finger input started from the symbols // mode key. - delay = mParams.mLongPressKeyTimeout * 3; + delay = mLongPressKeyTimeout * 3; } else { - delay = mParams.mLongPressKeyTimeout; + delay = mLongPressKeyTimeout; } break; } @@ -268,7 +282,7 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } sendMessageDelayed( - obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout); + obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout); if (isTyping) { return; } @@ -307,50 +321,6 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key } } - public static class PointerTrackerParams { - public final boolean mSlidingKeyInputEnabled; - public final int mTouchNoiseThresholdTime; - public final float mTouchNoiseThresholdDistance; - - public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); - - private PointerTrackerParams() { - mSlidingKeyInputEnabled = false; - mTouchNoiseThresholdTime =0; - mTouchNoiseThresholdDistance = 0; - } - - public PointerTrackerParams(TypedArray mainKeyboardViewAttr) { - mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( - R.styleable.MainKeyboardView_slidingKeyInputEnable, false); - mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); - mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0); - } - } - - static class KeyTimerParams { - public final int mKeyRepeatStartTimeout; - public final int mKeyRepeatInterval; - public final int mLongPressKeyTimeout; - public final int mLongPressShiftKeyTimeout; - public final int mIgnoreAltCodeKeyTimeout; - - public KeyTimerParams(TypedArray mainKeyboardViewAttr) { - mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0); - mKeyRepeatInterval = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyRepeatInterval, 0); - mLongPressKeyTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_longPressKeyTimeout, 0); - mLongPressShiftKeyTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_longPressShiftKeyTimeout, 0); - mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); - } - } - public MainKeyboardView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.mainKeyboardViewStyle); } @@ -389,19 +359,15 @@ public class MainKeyboardView extends KeyboardView implements PointerTracker.Key final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId( R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0); - final KeyTimerParams keyTimerParams = new KeyTimerParams(a); - mPointerTrackerParams = new PointerTrackerParams(a); - final float keyHysteresisDistance = a.getDimension( R.styleable.MainKeyboardView_keyHysteresisDistance, 0); mKeyDetector = new KeyDetector(keyHysteresisDistance); - mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams); + mKeyTimerHandler = new KeyTimerHandler(this, a); mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean( R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false); + PointerTracker.setParameters(a); a.recycle(); - PointerTracker.setParameters(mPointerTrackerParams); - mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator( languageOnSpacebarFadeoutAnimatorResId, this); mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator( diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index d9a7cb418..67f37a7e7 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -16,6 +16,7 @@ package com.android.inputmethod.keyboard; +import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.os.SystemClock; @@ -28,6 +29,7 @@ import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.ResearchLogger; @@ -118,9 +120,36 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } + static class PointerTrackerParams { + public final boolean mSlidingKeyInputEnabled; + public final int mTouchNoiseThresholdTime; + public final float mTouchNoiseThresholdDistance; + public final int mTouchNoiseThresholdDistanceSquared; + + public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); + + private PointerTrackerParams() { + mSlidingKeyInputEnabled = false; + mTouchNoiseThresholdTime = 0; + mTouchNoiseThresholdDistance = 0.0f; + mTouchNoiseThresholdDistanceSquared = 0; + } + + public PointerTrackerParams(TypedArray mainKeyboardViewAttr) { + mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( + R.styleable.MainKeyboardView_slidingKeyInputEnable, false); + mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); + final float touchNouseThresholdDistance = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0); + mTouchNoiseThresholdDistance = touchNouseThresholdDistance; + mTouchNoiseThresholdDistanceSquared = + (int)(touchNouseThresholdDistance * touchNouseThresholdDistance); + } + } + // Parameters for pointer handling. - private static MainKeyboardView.PointerTrackerParams sParams; - private static int sTouchNoiseThresholdDistanceSquared; + private static PointerTrackerParams sParams; private static boolean sNeedsPhantomSuddenMoveEventHack; private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList(); @@ -192,14 +221,11 @@ public class PointerTracker implements PointerTrackerQueue.Element { sPointerTrackerQueue = null; } sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack; - - setParameters(MainKeyboardView.PointerTrackerParams.DEFAULT); + sParams = PointerTrackerParams.DEFAULT; } - public static void setParameters(MainKeyboardView.PointerTrackerParams params) { - sParams = params; - sTouchNoiseThresholdDistanceSquared = (int)( - params.mTouchNoiseThresholdDistance * params.mTouchNoiseThresholdDistance); + public static void setParameters(final TypedArray mainKeyboardViewAttr) { + sParams = new PointerTrackerParams(mainKeyboardViewAttr); } private static void updateGestureHandlingMode() { @@ -210,17 +236,17 @@ public class PointerTracker implements PointerTrackerQueue.Element { } // Note that this method is called from a non-UI thread. - public static void setMainDictionaryAvailability(boolean mainDictionaryAvailable) { + public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { sMainDictionaryAvailable = mainDictionaryAvailable; updateGestureHandlingMode(); } - public static void setGestureHandlingEnabledByUser(boolean gestureHandlingEnabledByUser) { + public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser; updateGestureHandlingMode(); } - public static PointerTracker getPointerTracker(final int id, KeyEventHandler handler) { + public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) { final ArrayList<PointerTracker> trackers = sTrackers; // Create pointer trackers until we can get 'id+1'-th tracker, if needed. @@ -236,7 +262,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false; } - public static void setKeyboardActionListener(KeyboardActionListener listener) { + public static void setKeyboardActionListener(final KeyboardActionListener listener) { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); @@ -244,7 +270,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public static void setKeyDetector(KeyDetector keyDetector) { + public static void setKeyDetector(final KeyDetector keyDetector) { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); @@ -298,9 +324,10 @@ public class PointerTracker implements PointerTrackerQueue.Element { sAggregratedPointers.reset(); } - private PointerTracker(int id, KeyEventHandler handler) { - if (handler == null) + private PointerTracker(final int id, final KeyEventHandler handler) { + if (handler == null) { throw new NullPointerException(); + } mPointerId = id; mGestureStroke = new GestureStroke(id); setKeyDetectorInner(handler.getKeyDetector()); @@ -310,7 +337,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { } // Returns true if keyboard has been changed by this callback. - private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) { + private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) { if (mInGesture) { return false; } @@ -335,7 +362,8 @@ public class PointerTracker implements PointerTrackerQueue.Element { // Note that we need primaryCode argument because the keyboard may in shifted state and the // primaryCode is different from {@link Key#mCode}. - private void callListenerOnCodeInput(Key key, int primaryCode, int x, int y) { + private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x, + final int y) { final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); final int code = altersCode ? key.mAltCode : primaryCode; @@ -364,7 +392,8 @@ public class PointerTracker implements PointerTrackerQueue.Element { // Note that we need primaryCode argument because the keyboard may in shifted state and the // primaryCode is different from {@link Key#mCode}. - private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) { + private void callListenerOnRelease(final Key key, final int primaryCode, + final boolean withSliding) { if (mInGesture) { return; } @@ -387,15 +416,16 @@ public class PointerTracker implements PointerTrackerQueue.Element { } private void callListenerOnCancelInput() { - if (DEBUG_LISTENER) + if (DEBUG_LISTENER) { Log.d(TAG, "onCancelInput"); + } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_callListenerOnCancelInput(); } mListener.onCancelInput(); } - private void setKeyDetectorInner(KeyDetector keyDetector) { + private void setKeyDetectorInner(final KeyDetector keyDetector) { mKeyDetector = keyDetector; mKeyboard = keyDetector.getKeyboard(); mIsAlphabetKeyboard = mKeyboard.mId.isAlphabetKeyboard(); @@ -425,11 +455,11 @@ public class PointerTracker implements PointerTrackerQueue.Element { return mCurrentKey != null && mCurrentKey.isModifier(); } - public Key getKeyOn(int x, int y) { + public Key getKeyOn(final int x, final int y) { return mKeyDetector.detectHitKey(x, y); } - private void setReleasedKeyGraphics(Key key) { + private void setReleasedKeyGraphics(final Key key) { mDrawingProxy.dismissKeyPreview(this); if (key == null) { return; @@ -460,7 +490,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void setPressedKeyGraphics(Key key) { + private void setPressedKeyGraphics(final Key key) { if (key == null) { return; } @@ -499,17 +529,17 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void updateReleaseKeyGraphics(Key key) { + private void updateReleaseKeyGraphics(final Key key) { key.onReleased(); mDrawingProxy.invalidateKey(key); } - private void updatePressKeyGraphics(Key key) { + private void updatePressKeyGraphics(final Key key) { key.onPressed(); mDrawingProxy.invalidateKey(key); } - public void drawGestureTrail(Canvas canvas, Paint paint) { + public void drawGestureTrail(final Canvas canvas, final Paint paint) { if (mInGesture) { mGestureStroke.drawGestureTrail(canvas, paint); } @@ -527,22 +557,22 @@ public class PointerTracker implements PointerTrackerQueue.Element { return mDownTime; } - private Key onDownKey(int x, int y, long eventTime) { + private Key onDownKey(final int x, final int y, final long eventTime) { mDownTime = eventTime; return onMoveToNewKey(onMoveKeyInternal(x, y), x, y); } - private Key onMoveKeyInternal(int x, int y) { + private Key onMoveKeyInternal(final int x, final int y) { mLastX = x; mLastY = y; return mKeyDetector.detectHitKey(x, y); } - private Key onMoveKey(int x, int y) { + private Key onMoveKey(final int x, final int y) { return onMoveKeyInternal(x, y); } - private Key onMoveToNewKey(Key newKey, int x, int y) { + private Key onMoveToNewKey(final Key newKey, final int x, final int y) { mCurrentKey = newKey; mKeyX = x; mKeyY = y; @@ -557,14 +587,14 @@ public class PointerTracker implements PointerTrackerQueue.Element { mListener.onStartBatchInput(); } - private void updateBatchInput(InputPointers batchPoints) { + private void updateBatchInput(final InputPointers batchPoints) { if (DEBUG_LISTENER) { Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize()); } mListener.onUpdateBatchInput(batchPoints); } - private void endBatchInput(InputPointers batchPoints) { + private void endBatchInput(final InputPointers batchPoints) { if (DEBUG_LISTENER) { Log.d(TAG, "onEndBatchInput: batchPoints=" + batchPoints.getPointerSize()); } @@ -585,7 +615,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { mLastRecognitionTime = 0; } - private boolean updateBatchInputRecognitionState(long eventTime, int size) { + private boolean updateBatchInputRecognitionState(final long eventTime, final int size) { if (size > mLastRecognitionPointSize && eventTime > mLastRecognitionTime + MIN_GESTURE_RECOGNITION_TIME) { mLastRecognitionPointSize = size; @@ -595,8 +625,8 @@ public class PointerTracker implements PointerTrackerQueue.Element { return false; } - public void processMotionEvent(int action, int x, int y, long eventTime, - KeyEventHandler handler) { + public void processMotionEvent(final int action, final int x, final int y, final long eventTime, + final KeyEventHandler handler) { switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: @@ -615,9 +645,11 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public void onDownEvent(int x, int y, long eventTime, KeyEventHandler handler) { - if (DEBUG_EVENT) + public void onDownEvent(final int x, final int y, final long eventTime, + final KeyEventHandler handler) { + if (DEBUG_EVENT) { printTouchEvent("onDownEvent:", x, y, eventTime); + } mDrawingProxy = handler.getDrawingProxy(); mTimerProxy = handler.getTimerProxy(); @@ -629,7 +661,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { final int dx = x - mLastX; final int dy = y - mLastY; final int distanceSquared = (dx * dx + dy * dy); - if (distanceSquared < sTouchNoiseThresholdDistanceSquared) { + if (distanceSquared < sParams.mTouchNoiseThresholdDistanceSquared) { if (DEBUG_MODE) Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT + " distance=" + distanceSquared); @@ -665,7 +697,7 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void onDownEventInternal(int x, int y, long eventTime) { + private void onDownEventInternal(final int x, final int y, final long eventTime) { Key key = onDownKey(x, y, eventTime); // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding // from modifier key, or 3) this pointer's KeyDetector always allows sliding input. @@ -690,15 +722,15 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void startSlidingKeyInput(Key key) { + private void startSlidingKeyInput(final Key key) { if (!mIsInSlidingKeyInput) { mIgnoreModifierKey = key.isModifier(); } mIsInSlidingKeyInput = true; } - private void onGestureMoveEvent(PointerTracker tracker, int x, int y, long eventTime, - boolean isHistorical, Key key) { + private void onGestureMoveEvent(final PointerTracker tracker, final int x, final int y, + final long eventTime, final boolean isHistorical, final Key key) { final int gestureTime = (int)(eventTime - tracker.getDownTime()); if (sShouldHandleGesture && mIsPossibleGesture) { final GestureStroke stroke = mGestureStroke; @@ -717,11 +749,13 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public void onMoveEvent(int x, int y, long eventTime, MotionEvent me) { - if (DEBUG_MOVE_EVENT) + public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) { + if (DEBUG_MOVE_EVENT) { printTouchEvent("onMoveEvent:", x, y, eventTime); - if (mKeyAlreadyProcessed) + } + if (mKeyAlreadyProcessed) { return; + } if (me != null) { // Add historical points to gesture path. @@ -811,8 +845,9 @@ public class PointerTracker implements PointerTrackerQueue.Element { // touch panels when there are close multiple touches. // Caveat: When in chording input mode with a modifier key, we don't use // this hack. - if (me != null && me.getPointerCount() > 1 - && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) { + final PointerTrackerQueue queue = sPointerTrackerQueue; + if (queue != null && queue.size() > 1 + && !queue.hasModifierKeyOlderThan(this)) { onUpEventInternal(); } if (!mIsPossibleGesture) { @@ -841,9 +876,10 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public void onUpEvent(int x, int y, long eventTime) { - if (DEBUG_EVENT) + public void onUpEvent(final int x, final int y, final long eventTime) { + if (DEBUG_EVENT) { printTouchEvent("onUpEvent :", x, y, eventTime); + } final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { @@ -865,9 +901,10 @@ public class PointerTracker implements PointerTrackerQueue.Element { // This pointer tracker needs to keep the key top graphics "pressed", but needs to get a // "virtual" up event. @Override - public void onPhantomUpEvent(long eventTime) { - if (DEBUG_EVENT) + public void onPhantomUpEvent(final long eventTime) { + if (DEBUG_EVENT) { printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime); + } onUpEventInternal(); mKeyAlreadyProcessed = true; } @@ -898,14 +935,15 @@ public class PointerTracker implements PointerTrackerQueue.Element { // This event will be recognized as a regular code input. Clear unused batch points so they // are not mistakenly included in the next batch event. clearBatchInputPointsOfAllPointerTrackers(); - if (mKeyAlreadyProcessed) + if (mKeyAlreadyProcessed) { return; + } if (mCurrentKey != null && !mCurrentKey.isRepeatable()) { detectAndSendKey(mCurrentKey, mKeyX, mKeyY); } } - public void onShowMoreKeysPanel(int x, int y, KeyEventHandler handler) { + public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) { abortBatchInput(); onLongPressed(); mIsShowingMoreKeysPanel = true; @@ -921,9 +959,10 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - public void onCancelEvent(int x, int y, long eventTime) { - if (DEBUG_EVENT) + public void onCancelEvent(final int x, final int y, final long eventTime) { + if (DEBUG_EVENT) { printTouchEvent("onCancelEvt:", x, y, eventTime); + } final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { @@ -943,24 +982,25 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void startRepeatKey(Key key) { + private void startRepeatKey(final Key key) { if (key != null && key.isRepeatable() && !mInGesture) { onRegisterKey(key); mTimerProxy.startKeyRepeatTimer(this); } } - public void onRegisterKey(Key key) { + public void onRegisterKey(final Key key) { if (key != null) { detectAndSendKey(key, key.mX, key.mY); mTimerProxy.startTypingStateTimer(key); } } - private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, Key newKey) { - if (mKeyDetector == null) + private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final Key newKey) { + if (mKeyDetector == null) { throw new NullPointerException("keyboard and/or key detector not set"); - Key curKey = mCurrentKey; + } + final Key curKey = mCurrentKey; if (newKey == curKey) { return false; } else if (curKey != null) { @@ -971,24 +1011,25 @@ public class PointerTracker implements PointerTrackerQueue.Element { } } - private void startLongPressTimer(Key key) { + private void startLongPressTimer(final Key key) { if (key != null && key.isLongPressEnabled() && !mInGesture) { mTimerProxy.startLongPressTimer(this); } } - private void detectAndSendKey(Key key, int x, int y) { + private void detectAndSendKey(final Key key, final int x, final int y) { if (key == null) { callListenerOnCancelInput(); return; } - int code = key.mCode; + final int code = key.mCode; callListenerOnCodeInput(key, code, x, y); callListenerOnRelease(key, code, false); } - private void printTouchEvent(String title, int x, int y, long eventTime) { + private void printTouchEvent(final String title, final int x, final int y, + final long eventTime) { final Key key = mKeyDetector.detectHitKey(x, y); final String code = KeyDetector.printableCode(key); Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title, diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index cbc916a7e..ff2feb51d 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -93,7 +93,7 @@ public class InputPointers { } mXCoordinates.append(xCoordinates, startPos, length); mYCoordinates.append(yCoordinates, startPos, length); - mPointerIds.fill(pointerId, startPos, length); + mPointerIds.fill(pointerId, mPointerIds.getLength(), length); mTimes.append(times, startPos, length); } @@ -124,4 +124,10 @@ public class InputPointers { public int[] getTimes() { return mTimes.getPrimitiveArray(); } + + @Override + public String toString() { + return "size=" + getPointerSize() + " id=" + mPointerIds + " time=" + mTimes + + " x=" + mXCoordinates + " y=" + mYCoordinates; + } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 4d5f93b8f..df200cd0e 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -1261,11 +1261,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mLastKeyTime = when; mConnection.beginBatchEdit(); - - if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); - } - final KeyboardSwitcher switcher = mKeyboardSwitcher; // The space state depends only on the last character pressed and its own previous // state. Here, we revert the space state to neutral if the key is actually modifying @@ -1347,6 +1342,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mLastComposedWord.deactivate(); mEnteredText = null; mConnection.endBatchEdit(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); + } } // Called from PointerTracker through the KeyboardActionListener interface diff --git a/java/src/com/android/inputmethod/latin/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/ResizableIntArray.java index 387d45a53..c660f92c4 100644 --- a/java/src/com/android/inputmethod/latin/ResizableIntArray.java +++ b/java/src/com/android/inputmethod/latin/ResizableIntArray.java @@ -131,4 +131,16 @@ public class ResizableIntArray { mLength = endPos; } } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mLength; i++) { + if (i != 0) { + sb.append(","); + } + sb.append(mArray[i]); + } + return "[" + sb + "]"; + } } diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java new file mode 100644 index 000000000..5124a35a6 --- /dev/null +++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.research; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Arrange for the uploading service to be run on regular intervals. + */ +public final class BootBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { + ResearchLogger.scheduleUploadingService(context); + } + } +} diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java index c9f3b476a..11eae8813 100644 --- a/java/src/com/android/inputmethod/research/FeedbackActivity.java +++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java @@ -18,10 +18,7 @@ package com.android.inputmethod.research; import android.app.Activity; import android.os.Bundle; -import android.text.Editable; -import android.view.View; import android.widget.CheckBox; -import android.widget.EditText; import com.android.inputmethod.latin.R; @@ -31,6 +28,11 @@ public class FeedbackActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.research_feedback_activity); final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout); + final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history); + final CharSequence cs = checkbox.getText(); + final String actualString = String.format(cs.toString(), + ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE); + checkbox.setText(actualString); layout.setActivity(this); } diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java new file mode 100644 index 000000000..ae7b1579a --- /dev/null +++ b/java/src/com/android/inputmethod/research/LogBuffer.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.research; + +import com.android.inputmethod.latin.CollectionUtils; + +import java.util.LinkedList; + +/** + * A buffer that holds a fixed number of LogUnits. + * + * LogUnits are added in and shifted out in temporal order. Only a subset of the LogUnits are + * actual words; the other LogUnits do not count toward the word limit. Once the buffer reaches + * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to + * stay under the capacity limit. + */ +public class LogBuffer { + protected final LinkedList<LogUnit> mLogUnits; + /* package for test */ int mWordCapacity; + // The number of members of mLogUnits that are actual words. + protected int mNumActualWords; + + /** + * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and + * unlimited number of non-word LogUnits), and that outputs its result to a researchLog. + * + * @param wordCapacity maximum number of words + */ + LogBuffer(final int wordCapacity) { + if (wordCapacity <= 0) { + throw new IllegalArgumentException("wordCapacity must be 1 or greater."); + } + mLogUnits = CollectionUtils.newLinkedList(); + mWordCapacity = wordCapacity; + mNumActualWords = 0; + } + + /** + * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's + * (oldest first) if word capacity is reached. + */ + public void shiftIn(LogUnit newLogUnit) { + if (newLogUnit.getWord() == null) { + // This LogUnit isn't a word, so it doesn't count toward the word-limit. + mLogUnits.add(newLogUnit); + return; + } + if (mNumActualWords == mWordCapacity) { + shiftOutThroughFirstWord(); + } + mLogUnits.add(newLogUnit); + mNumActualWords++; // Must be a word, or we wouldn't be here. + } + + private void shiftOutThroughFirstWord() { + while (!mLogUnits.isEmpty()) { + final LogUnit logUnit = mLogUnits.removeFirst(); + onShiftOut(logUnit); + if (logUnit.hasWord()) { + // Successfully shifted out a word-containing LogUnit and made space for the new + // LogUnit. + mNumActualWords--; + break; + } + } + } + + /** + * Removes all LogUnits from the buffer without calling onShiftOut(). + */ + public void clear() { + mLogUnits.clear(); + mNumActualWords = 0; + } + + /** + * Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn. LogUnits are + * removed in the order entered. This method is not called when shiftOut is called directly. + * + * Base class does nothing; subclasses may override. + */ + protected void onShiftOut(LogUnit logUnit) { + } + + /** + * Called to deliberately remove the oldest LogUnit. Usually called when draining the + * LogBuffer. + */ + public LogUnit shiftOut() { + if (mLogUnits.isEmpty()) { + return null; + } + final LogUnit logUnit = mLogUnits.removeFirst(); + if (logUnit.hasWord()) { + mNumActualWords--; + } + return logUnit; + } +} diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java new file mode 100644 index 000000000..d8b3a29ff --- /dev/null +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.research; + +import com.android.inputmethod.latin.CollectionUtils; + +import java.util.ArrayList; + +/** + * A group of log statements related to each other. + * + * A LogUnit is collection of LogStatements, each of which is generated by at a particular point + * in the code. (There is no LogStatement class; the data is stored across the instance variables + * here.) A single LogUnit's statements can correspond to all the calls made while in the same + * composing region, or all the calls between committing the last composing region, and the first + * character of the next composing region. + * + * Individual statements in a log may be marked as potentially private. If so, then they are only + * published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit + * 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. + */ +/* package */ class LogUnit { + private final ArrayList<String[]> mKeysList = CollectionUtils.newArrayList(); + private final ArrayList<Object[]> mValuesList = CollectionUtils.newArrayList(); + private final ArrayList<Boolean> mIsPotentiallyPrivate = CollectionUtils.newArrayList(); + private String mWord; + private boolean mContainsDigit; + + public void addLogStatement(final String[] keys, final Object[] values, + final Boolean isPotentiallyPrivate) { + mKeysList.add(keys); + mValuesList.add(values); + mIsPotentiallyPrivate.add(isPotentiallyPrivate); + } + + public void publishTo(final ResearchLog researchLog, final boolean isIncludingPrivateData) { + final int size = mKeysList.size(); + for (int i = 0; i < size; i++) { + if (!mIsPotentiallyPrivate.get(i) || isIncludingPrivateData) { + researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); + } + } + } + + public void setWord(String word) { + mWord = word; + } + + public String getWord() { + return mWord; + } + + public boolean hasWord() { + return mWord != null; + } + + public void setContainsDigit() { + mContainsDigit = true; + } + + public boolean hasDigit() { + return mContainsDigit; + } + + public boolean isEmpty() { + return mKeysList.isEmpty(); + } +} diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java new file mode 100644 index 000000000..745768d35 --- /dev/null +++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.research; + +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.Suggest; + +import java.util.Random; + +public class MainLogBuffer extends LogBuffer { + // The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams. + private static final int N_GRAM_SIZE = 2; + // The number of words between n-grams to omit from the log. + private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = 18; + + private final ResearchLog mResearchLog; + private Suggest mSuggest; + + // The minimum periodicity with which n-grams can be sampled. E.g. mWinWordPeriod is 10 if + // every 10th bigram is sampled, i.e., words 1-8 are not, but the bigram at words 9 and 10, etc. + // for 11-18, and the bigram at words 19 and 20. If an n-gram is not safe (e.g. it contains a + // number in the middle or an out-of-vocabulary word), then sampling is delayed until a safe + // n-gram does appear. + /* package for test */ int mMinWordPeriod; + + // Counter for words left to suppress before an n-gram can be sampled. Reset to mMinWordPeriod + // after a sample is taken. + /* package for test */ int mWordsUntilSafeToSample; + + public MainLogBuffer(final ResearchLog researchLog) { + super(N_GRAM_SIZE); + mResearchLog = researchLog; + mMinWordPeriod = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES + N_GRAM_SIZE; + final Random random = new Random(); + mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod); + } + + public void setSuggest(Suggest suggest) { + mSuggest = suggest; + } + + @Override + public void shiftIn(final LogUnit newLogUnit) { + super.shiftIn(newLogUnit); + if (newLogUnit.hasWord()) { + if (mWordsUntilSafeToSample > 0) { + mWordsUntilSafeToSample--; + } + } + } + + public void resetWordCounter() { + mWordsUntilSafeToSample = mMinWordPeriod; + } + + /** + * Determines whether the content of the MainLogBuffer can be safely uploaded in its complete + * form and still protect the user's privacy. + * + * The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any + * non-character data that is typed between words. The decision about privacy is made based on + * the buffer's entire content. If it is decided that the privacy risks are too great to upload + * the contents of this buffer, a censored version of the LogItems may still be uploaded. E.g., + * the screen orientation and other characteristics about the device can be uploaded without + * revealing much about the user. + */ + public boolean isSafeToLog() { + // Check that we are not sampling too frequently. Having sampled recently might disclose + // too much of the user's intended meaning. + if (mWordsUntilSafeToSample > 0) { + return false; + } + if (mSuggest == null || !mSuggest.hasMainDictionary()) { + // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a word + // is out-of-vocabulary or not. Therefore, we must judge the entire buffer contents to + // potentially pose a privacy risk. + return false; + } + // Reload the dictionary in case it has changed (e.g., because the user has changed + // languages). + final Dictionary dictionary = mSuggest.getMainDictionary(); + if (dictionary == null) { + return false; + } + // Check each word in the buffer. If any word poses a privacy threat, we cannot upload the + // complete buffer contents in detail. + final int length = mLogUnits.size(); + for (int i = 0; i < length; i++) { + final LogUnit logUnit = mLogUnits.get(i); + final String word = logUnit.getWord(); + if (word == null) { + // Digits outside words are a privacy threat. + if (logUnit.hasDigit()) { + return false; + } + } else { + // Words not in the dictionary are a privacy threat. + if (!(dictionary.isValidWord(word))) { + return false; + } + } + } + // All checks have passed; this buffer's content can be safely uploaded. + return true; + } + + @Override + protected void onShiftOut(LogUnit logUnit) { + if (mResearchLog != null) { + mResearchLog.publish(logUnit, false /* isIncludingPrivateData */); + } + } +} diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index 18bf3c07f..71a6d6a78 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -26,7 +26,6 @@ import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.research.ResearchLogger.LogUnit; import java.io.BufferedWriter; import java.io.File; @@ -37,6 +36,7 @@ import java.io.OutputStreamWriter; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -51,21 +51,22 @@ import java.util.concurrent.TimeUnit; */ public class ResearchLog { private static final String TAG = ResearchLog.class.getSimpleName(); - private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( - new OutputStreamWriter(new NullOutputStream())); + private static final boolean DEBUG = false; + private static final long FLUSH_DELAY_IN_MS = 1000 * 5; + private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4; - final ScheduledExecutorService mExecutor; + /* package */ final ScheduledExecutorService mExecutor; /* package */ final File mFile; private JsonWriter mJsonWriter = NULL_JSON_WRITER; + // true if at least one byte of data has been written out to the log file. This must be + // remembered because JsonWriter requires that calls matching calls to beginObject and + // endObject, as well as beginArray and endArray, and the file is opened lazily, only when + // it is certain that data will be written. Alternatively, the matching call exceptions + // could be caught, but this might suppress other errors. + private boolean mHasWrittenData = false; - private int mLoggingState; - private static final int LOGGING_STATE_UNSTARTED = 0; - private static final int LOGGING_STATE_READY = 1; // don't create file until necessary - private static final int LOGGING_STATE_RUNNING = 2; - private static final int LOGGING_STATE_STOPPING = 3; - private static final int LOGGING_STATE_STOPPED = 4; - private static final long FLUSH_DELAY_IN_MS = 1000 * 5; - + private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( + new OutputStreamWriter(new NullOutputStream())); private static class NullOutputStream extends OutputStream { /** {@inheritDoc} */ @Override @@ -84,128 +85,81 @@ public class ResearchLog { } } - public ResearchLog(File outputFile) { - mExecutor = Executors.newSingleThreadScheduledExecutor(); + public ResearchLog(final File outputFile) { if (outputFile == null) { throw new IllegalArgumentException(); } + mExecutor = Executors.newSingleThreadScheduledExecutor(); mFile = outputFile; - mLoggingState = LOGGING_STATE_UNSTARTED; - } - - public synchronized void start() throws IOException { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_READY; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - break; - } } - public synchronized void stop() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_STOPPED; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - try { - mJsonWriter.endArray(); - mJsonWriter.flush(); - mJsonWriter.close(); - } finally { - boolean success = mFile.setWritable(false, false); - mLoggingState = LOGGING_STATE_STOPPED; - } - return null; + public synchronized void close() { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + try { + if (mHasWrittenData) { + mJsonWriter.endArray(); + mJsonWriter.flush(); + mJsonWriter.close(); + mHasWrittenData = false; } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - mLoggingState = LOGGING_STATE_STOPPING; - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + } catch (Exception e) { + Log.d(TAG, "error when closing ResearchLog:"); + e.printStackTrace(); + } finally { + if (mFile.exists()) { + mFile.setWritable(false, false); + } + } + return null; + } + }); + removeAnyScheduledFlush(); + mExecutor.shutdown(); } - public boolean isAlive() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - return true; - } - return false; - } + private boolean mIsAbortSuccessful; - public void waitUntilStopped(final int timeoutInMs) throws InterruptedException { + public synchronized void abort() { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + try { + if (mHasWrittenData) { + mJsonWriter.endArray(); + mJsonWriter.close(); + mHasWrittenData = false; + } + } finally { + mIsAbortSuccessful = mFile.delete(); + } + return null; + } + }); removeAnyScheduledFlush(); mExecutor.shutdown(); - mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS); } - public synchronized void abort() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - mLoggingState = LOGGING_STATE_STOPPED; - isAbortSuccessful = true; - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - try { - mJsonWriter.endArray(); - mJsonWriter.close(); - } finally { - isAbortSuccessful = mFile.delete(); - } - return null; - } - }); - removeAnyScheduledFlush(); - mExecutor.shutdown(); - mLoggingState = LOGGING_STATE_STOPPING; - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + public boolean blockingAbort() throws InterruptedException { + abort(); + mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); + return mIsAbortSuccessful; } - private boolean isAbortSuccessful; - public boolean isAbortSuccessful() { - return isAbortSuccessful; + public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException { + mExecutor.awaitTermination(delay, timeUnit); } /* package */ synchronized void flush() { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - removeAnyScheduledFlush(); - mExecutor.submit(mFlushCallable); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } + removeAnyScheduledFlush(); + mExecutor.submit(mFlushCallable); } - private Callable<Object> mFlushCallable = new Callable<Object>() { + private final Callable<Object> mFlushCallable = new Callable<Object>() { @Override public Object call() throws Exception { - if (mLoggingState == LOGGING_STATE_RUNNING) { - mJsonWriter.flush(); - } + mJsonWriter.flush(); return null; } }; @@ -224,56 +178,40 @@ public class ResearchLog { mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); } - public synchronized void publishPublicEvents(final LogUnit logUnit) { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - logUnit.publishPublicEventsTo(ResearchLog.this); - scheduleFlush(); - return null; - } - }); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: - } - } - - public synchronized void publishAllEvents(final LogUnit logUnit) { - switch (mLoggingState) { - case LOGGING_STATE_UNSTARTED: - break; - case LOGGING_STATE_READY: - case LOGGING_STATE_RUNNING: - mExecutor.submit(new Callable<Object>() { - @Override - public Object call() throws Exception { - logUnit.publishAllEventsTo(ResearchLog.this); - scheduleFlush(); - return null; - } - }); - break; - case LOGGING_STATE_STOPPING: - case LOGGING_STATE_STOPPED: + public synchronized void publish(final LogUnit logUnit, final boolean isIncludingPrivateData) { + try { + mExecutor.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + logUnit.publishTo(ResearchLog.this, isIncludingPrivateData); + scheduleFlush(); + return null; + } + }); + } catch (RejectedExecutionException e) { + // TODO: Add code to record loss of data, and report. } } private static final String CURRENT_TIME_KEY = "_ct"; private static final String UPTIME_KEY = "_ut"; private static final String EVENT_TYPE_KEY = "_ty"; + void outputEvent(final String[] keys, final Object[] values) { - // not thread safe. + // Not thread safe. + if (keys.length == 0) { + return; + } + if (DEBUG) { + if (keys.length != values.length + 1) { + Log.d(TAG, "Key and Value list sizes do not match. " + keys[0]); + } + } try { if (mJsonWriter == NULL_JSON_WRITER) { mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); - mJsonWriter.setLenient(true); mJsonWriter.beginArray(); + mHasWrittenData = true; } mJsonWriter.beginObject(); mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); @@ -283,8 +221,8 @@ public class ResearchLog { for (int i = 0; i < length; i++) { mJsonWriter.name(keys[i + 1]); Object value = values[i]; - if (value instanceof String) { - mJsonWriter.value((String) value); + if (value instanceof CharSequence) { + mJsonWriter.value(value.toString()); } else if (value instanceof Number) { mJsonWriter.value((Number) value); } else if (value instanceof Boolean) { @@ -331,14 +269,11 @@ public class ResearchLog { SuggestedWords words = (SuggestedWords) value; mJsonWriter.beginObject(); mJsonWriter.name("typedWordValid").value(words.mTypedWordValid); - mJsonWriter.name("willAutoCorrect") - .value(words.mWillAutoCorrect); + mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect); mJsonWriter.name("isPunctuationSuggestions") - .value(words.mIsPunctuationSuggestions); - mJsonWriter.name("isObsoleteSuggestions") - .value(words.mIsObsoleteSuggestions); - mJsonWriter.name("isPrediction") - .value(words.mIsPrediction); + .value(words.mIsPunctuationSuggestions); + mJsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions); + mJsonWriter.name("isPrediction").value(words.mIsPrediction); mJsonWriter.name("words"); mJsonWriter.beginArray(); final int size = words.size(); @@ -363,8 +298,8 @@ public class ResearchLog { try { mJsonWriter.close(); } catch (IllegalStateException e1) { - // assume that this is just the json not being terminated properly. - // ignore + // Assume that this is just the json not being terminated properly. + // Ignore } catch (IOException e1) { e1.printStackTrace(); } finally { diff --git a/java/src/com/android/inputmethod/research/ResearchLogUploader.java b/java/src/com/android/inputmethod/research/ResearchLogUploader.java deleted file mode 100644 index 3b1213009..000000000 --- a/java/src/com/android/inputmethod/research/ResearchLogUploader.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.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.util.Log; - -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.R.string; - -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; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public final class ResearchLogUploader { - private static final String TAG = ResearchLogUploader.class.getSimpleName(); - private static final int UPLOAD_INTERVAL_IN_MS = 1000 * 60 * 15; // every 15 min - private static final int BUF_SIZE = 1024 * 8; - - private final boolean mCanUpload; - private final Context mContext; - private final File mFilesDir; - private final URL mUrl; - private final ScheduledExecutorService mExecutor; - - private Runnable doUploadRunnable = new UploadRunnable(null, false); - - public ResearchLogUploader(final Context context, final File filesDir) { - mContext = context; - mFilesDir = filesDir; - final PackageManager packageManager = context.getPackageManager(); - final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET, - context.getPackageName()) == PackageManager.PERMISSION_GRANTED; - if (!hasPermission) { - mCanUpload = false; - mUrl = null; - mExecutor = null; - return; - } - URL tempUrl = null; - boolean canUpload = false; - ScheduledExecutorService executor = null; - try { - final String urlString = context.getString(R.string.research_logger_upload_url); - if (urlString == null || urlString.equals("")) { - return; - } - tempUrl = new URL(urlString); - canUpload = true; - executor = Executors.newSingleThreadScheduledExecutor(); - } catch (MalformedURLException e) { - tempUrl = null; - e.printStackTrace(); - return; - } finally { - mCanUpload = canUpload; - mUrl = tempUrl; - mExecutor = executor; - } - } - - public void start() { - if (mCanUpload) { - Log.d(TAG, "scheduling regular uploading"); - mExecutor.scheduleWithFixedDelay(doUploadRunnable, UPLOAD_INTERVAL_IN_MS, - UPLOAD_INTERVAL_IN_MS, TimeUnit.MILLISECONDS); - } else { - Log.d(TAG, "no permission to upload"); - } - } - - public void uploadNow(final Callback callback) { - // Perform an immediate upload. Note that this should happen even if there is - // another upload happening right now, as it may have missed the latest changes. - // TODO: Reschedule regular upload tests starting from now. - if (mCanUpload) { - mExecutor.submit(new UploadRunnable(callback, true)); - } - } - - public interface Callback { - public void onUploadCompleted(final boolean success); - } - - 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(); - } - - class UploadRunnable implements Runnable { - private final Callback mCallback; - private final boolean mForceUpload; - - public UploadRunnable(final Callback callback, final boolean forceUpload) { - mCallback = callback; - mForceUpload = forceUpload; - } - - @Override - public void run() { - doUpload(); - } - - private void doUpload() { - if (!mForceUpload && (!isExternallyPowered() || !hasWifiConnection())) { - return; - } - if (mFilesDir == null) { - return; - } - final File[] files = mFilesDir.listFiles(new FileFilter() { - @Override - public boolean accept(File pathname) { - return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX) - && !pathname.canWrite(); - } - }); - boolean success = true; - if (files.length == 0) { - success = false; - } - for (final File file : files) { - if (!uploadFile(file)) { - success = false; - } - } - if (mCallback != null) { - mCallback.onUploadCompleted(success); - } - } - - private boolean uploadFile(File file) { - Log.d(TAG, "attempting upload of " + file.getAbsolutePath()); - boolean success = false; - final int contentLength = (int) file.length(); - HttpURLConnection connection = null; - InputStream fileIs = null; - try { - fileIs = 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 = fileIs.read(buf)) != -1) { - os.write(buf, 0, numBytesRead); - } - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - Log.d(TAG, "upload failed: " + connection.getResponseCode()); - InputStream netIs = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(netIs)); - String line; - while ((line = reader.readLine()) != null) { - Log.d(TAG, "| " + reader.readLine()); - } - reader.close(); - return success; - } - file.delete(); - success = true; - Log.d(TAG, "upload successful"); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (fileIs != null) { - try { - fileIs.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (connection != null) { - connection.disconnect(); - } - } - return success; - } - } -} diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 09a22eff1..918fcf5a1 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -18,11 +18,14 @@ package com.android.inputmethod.research; import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; +import android.app.AlarmManager; import android.app.AlertDialog; import android.app.Dialog; +import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; +import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; @@ -68,11 +71,8 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Locale; import java.util.UUID; @@ -98,24 +98,26 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); private static final boolean IS_SHOWING_INDICATOR = true; private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false; + public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; // 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 int ABORT_TIMEOUT_IN_MS = 10 * 1000; // timeout to notify user private static final ResearchLogger sInstance = new ResearchLogger(); // to write to a different filename, e.g., for testing, set mFile before calling start() /* package */ File mFilesDir; /* package */ String mUUIDString; /* package */ ResearchLog mMainResearchLog; - // The mIntentionalResearchLog 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 // the system to do so. - /* package */ ResearchLog mIntentionalResearchLog; - // LogUnits are queued here and released only when the user requests the intentional log. - private List<LogUnit> mIntentionalResearchLogQueue = CollectionUtils.newArrayList(); + // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are + // complete. + /* package */ ResearchLog mFeedbackLog; + /* package */ MainLogBuffer mMainLogBuffer; + /* package */ LogBuffer mFeedbackLogBuffer; private boolean mIsPasswordView = false; private boolean mIsLoggingSuspended = false; @@ -141,7 +143,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private InputMethodService mInputMethodService; private final Statistics mStatistics; - private ResearchLogUploader mResearchLogUploader; + private Intent mUploadIntent; + private PendingIntent mUploadPendingIntent; + + private LogUnit mCurrentLogUnit = new LogUnit(); private ResearchLogger() { mStatistics = Statistics.getInstance(); @@ -181,10 +186,33 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang e.apply(); } } - mResearchLogUploader = new ResearchLogUploader(ims, mFilesDir); - mResearchLogUploader.start(); mInputMethodService = ims; mPrefs = prefs; + mUploadIntent = new Intent(mInputMethodService, UploaderService.class); + mUploadPendingIntent = PendingIntent.getService(mInputMethodService, 0, mUploadIntent, 0); + + if (ProductionFlag.IS_EXPERIMENTAL) { + scheduleUploadingService(mInputMethodService); + } + } + + /** + * Arrange for the UploaderService to be run on a regular basis. + * + * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may + * cause problems if this method is called often and frequent updates are required, but since + * the user will likely be sleeping at some point, if the interval is less that the expected + * sleep duration and this method is not called during that time, the service should be invoked + * at some point. + */ + public static void scheduleUploadingService(Context context) { + final Intent intent = new Intent(context, UploaderService.class); + final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); + final AlarmManager manager = + (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + manager.cancel(pendingIntent); + manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, + UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); } private void cleanupLoggingDir(final File dir, final long time) { @@ -267,6 +295,17 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Editor e = mPrefs.edit(); e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true); e.apply(); + restart(); + } + + private void setLoggingAllowed(boolean enableLogging) { + if (mPrefs == null) { + return; + } + Editor e = mPrefs.edit(); + e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); + e.apply(); + sIsLogging = enableLogging; } private File createLogFile(File filesDir) { @@ -315,97 +354,58 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); return; } - try { - if (mMainResearchLog == null || !mMainResearchLog.isAlive()) { - mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); - } - mMainResearchLog.start(); - if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) { - mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); - } - mIntentionalResearchLog.start(); - } catch (IOException e) { - Log.w(TAG, "Could not start ResearchLogger."); + if (mMainLogBuffer == null) { + mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); + mMainLogBuffer = new MainLogBuffer(mMainResearchLog); + mMainLogBuffer.setSuggest(mSuggest); + } + if (mFeedbackLogBuffer == null) { + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); + // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold + // the feedback LogUnit itself. + mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1); } } /* package */ void stop() { logStatistics(); - publishLogUnit(mCurrentLogUnit, true); - mCurrentLogUnit = new LogUnit(); + commitCurrentLogUnit(); - if (mMainResearchLog != null) { - mMainResearchLog.stop(); - } - if (mIntentionalResearchLog != null) { - mIntentionalResearchLog.stop(); + if (mMainLogBuffer != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */); + mMainResearchLog.close(); + mMainLogBuffer = null; } - } - - private static final String[] EVENTKEYS_STATISTICS = { - "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount", - "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys", - "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete" - }; - private static void logStatistics() { - final ResearchLogger researchLogger = getInstance(); - final Statistics statistics = researchLogger.mStatistics; - final Object[] values = { - statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount, - statistics.mSpaceCount, statistics.mDeleteKeyCount, - statistics.mWordCount, statistics.mIsEmptyUponStarting, - statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), - statistics.mBeforeDeleteKeyCounter.getAverageTime(), - statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), - statistics.mAfterDeleteKeyCounter.getAverageTime() - }; - researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values); - } - - private void setLoggingAllowed(boolean enableLogging) { - if (mPrefs == null) { - return; + if (mFeedbackLogBuffer != null) { + mFeedbackLog.close(); + mFeedbackLogBuffer = null; } - Editor e = mPrefs.edit(); - e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); - e.apply(); - sIsLogging = enableLogging; } public boolean abort() { boolean didAbortMainLog = false; - if (mMainResearchLog != null) { - mMainResearchLog.abort(); + if (mMainLogBuffer != null) { + mMainLogBuffer.clear(); try { - mMainResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + didAbortMainLog = mMainResearchLog.blockingAbort(); } catch (InterruptedException e) { - // interrupted early. carry on. - } - if (mMainResearchLog.isAbortSuccessful()) { - didAbortMainLog = true; + // Don't know whether this succeeded or not. We assume not; this is reported + // to the caller. } - mMainResearchLog = null; + mMainLogBuffer = null; } - boolean didAbortIntentionalLog = false; - if (mIntentionalResearchLog != null) { - mIntentionalResearchLog.abort(); + boolean didAbortFeedbackLog = false; + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.clear(); try { - mIntentionalResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + didAbortFeedbackLog = mFeedbackLog.blockingAbort(); } catch (InterruptedException e) { - // interrupted early. carry on. + // Don't know whether this succeeded or not. We assume not; this is reported + // to the caller. } - if (mIntentionalResearchLog.isAbortSuccessful()) { - didAbortIntentionalLog = true; - } - mIntentionalResearchLog = null; - } - return didAbortMainLog && didAbortIntentionalLog; - } - - /* package */ void flush() { - if (mMainResearchLog != null) { - mMainResearchLog.flush(); + mFeedbackLogBuffer = null; } + return didAbortMainLog && didAbortFeedbackLog; } private void restart() { @@ -446,6 +446,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang abort(); } requestIndicatorRedraw(); + mPrefs = prefs; + prefsChanged(prefs); } public void presentResearchDialog(final LatinIME latinIME) { @@ -509,79 +511,44 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); } - private ResearchLog mFeedbackLog; - private List<LogUnit> mFeedbackQueue; - private ResearchLog mSavedMainResearchLog; - private ResearchLog mSavedIntentionalResearchLog; - private List<LogUnit> mSavedIntentionalResearchLogQueue; - - private void saveLogsForFeedback() { - mFeedbackLog = mIntentionalResearchLog; - if (mIntentionalResearchLogQueue != null) { - mFeedbackQueue = CollectionUtils.newArrayList(mIntentionalResearchLogQueue); + private static final String[] EVENTKEYS_FEEDBACK = { + "UserTimestamp", "contents" + }; + public void sendFeedback(final String feedbackContents, final boolean includeHistory) { + if (mFeedbackLogBuffer == null) { + return; + } + if (includeHistory) { + commitCurrentLogUnit(); } else { - mFeedbackQueue = null; + mFeedbackLogBuffer.clear(); } - mSavedMainResearchLog = mMainResearchLog; - mSavedIntentionalResearchLog = mIntentionalResearchLog; - mSavedIntentionalResearchLogQueue = mIntentionalResearchLogQueue; - - mMainResearchLog = null; - mIntentionalResearchLog = null; - mIntentionalResearchLogQueue = CollectionUtils.newArrayList(); + final LogUnit feedbackLogUnit = new LogUnit(); + final Object[] values = { + feedbackContents + }; + feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values, + false /* isPotentiallyPrivate */); + mFeedbackLogBuffer.shiftIn(feedbackLogUnit); + publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); + mFeedbackLog.close(); + uploadNow(); + mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); } - private static final int LOG_DRAIN_TIMEOUT_IN_MS = 1000 * 5; - public void sendFeedback(final String feedbackContents, final boolean includeHistory) { - if (includeHistory && mFeedbackLog != null) { - try { - LogUnit headerLogUnit = new LogUnit(); - headerLogUnit.addLogAtom(EVENTKEYS_INTENTIONAL_LOG, EVENTKEYS_NULLVALUES, false); - mFeedbackLog.publishAllEvents(headerLogUnit); - for (LogUnit logUnit : mFeedbackQueue) { - mFeedbackLog.publishAllEvents(logUnit); - } - userFeedback(mFeedbackLog, feedbackContents); - mFeedbackLog.stop(); - try { - mFeedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); - mIntentionalResearchLog.start(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - mIntentionalResearchLogQueue.clear(); - } - mResearchLogUploader.uploadNow(null); - } else { - // create a separate ResearchLog just for feedback - final ResearchLog feedbackLog = new ResearchLog(createLogFile(mFilesDir)); - try { - feedbackLog.start(); - userFeedback(feedbackLog, feedbackContents); - feedbackLog.stop(); - feedbackLog.waitUntilStopped(LOG_DRAIN_TIMEOUT_IN_MS); - mResearchLogUploader.uploadNow(null); - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } + public void uploadNow() { + mInputMethodService.startService(mUploadIntent); } public void onLeavingSendFeedbackDialog() { mInFeedbackDialog = false; - mMainResearchLog = mSavedMainResearchLog; - mIntentionalResearchLog = mSavedIntentionalResearchLog; - mIntentionalResearchLogQueue = mSavedIntentionalResearchLogQueue; } public void initSuggest(Suggest suggest) { mSuggest = suggest; + if (mMainLogBuffer != null) { + mMainLogBuffer.setSuggest(mSuggest); + } } private void setIsPasswordView(boolean isPasswordView) { @@ -589,7 +556,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { - return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging; + return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; } public void requestIndicatorRedraw() { @@ -632,13 +599,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - private static final String CURRENT_TIME_KEY = "_ct"; - private static final String UPTIME_KEY = "_ut"; - private static final String EVENT_TYPE_KEY = "_ty"; private static final Object[] EVENTKEYS_NULLVALUES = {}; - private LogUnit mCurrentLogUnit = new LogUnit(); - /** * Buffer a research log event, flagging it as privacy-sensitive. * @@ -654,10 +616,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogAtom(keys, values, true); + mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */); } } + private void setCurrentLogUnitContainsDigitFlag() { + mCurrentLogUnit.setContainsDigit(); + } + /** * Buffer a research log event, flaggint it as not privacy-sensitive. * @@ -673,140 +639,54 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private synchronized void enqueueEvent(final String[] keys, final Object[] values) { assert values.length + 1 == keys.length; if (isAllowedToLog()) { - mCurrentLogUnit.addLogAtom(keys, values, false); + mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */); } } - // Used to track how often words are logged. Too-frequent logging can leak - // semantics, disclosing private data. - /* package for test */ static class LoggingFrequencyState { - private static final int DEFAULT_WORD_LOG_FREQUENCY = 10; - private int mWordsRemainingToSkip; - private final int mFrequency; - - /** - * Tracks how often words may be uploaded. - * - * @param frequency 1=Every word, 2=Every other word, etc. - */ - public LoggingFrequencyState(int frequency) { - mFrequency = frequency; - mWordsRemainingToSkip = mFrequency; - } - - public void onWordLogged() { - mWordsRemainingToSkip = mFrequency; - } - - public void onWordNotLogged() { - if (mWordsRemainingToSkip > 1) { - mWordsRemainingToSkip--; + /* package for test */ void commitCurrentLogUnit() { + if (!mCurrentLogUnit.isEmpty()) { + if (mMainLogBuffer != null) { + mMainLogBuffer.shiftIn(mCurrentLogUnit); + if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) { + publishLogBuffer(mMainLogBuffer, mMainResearchLog, + true /* isIncludingPrivateData */); + mMainLogBuffer.resetWordCounter(); + } } + if (mFeedbackLogBuffer != null) { + mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); + } + mCurrentLogUnit = new LogUnit(); + Log.d(TAG, "commitCurrentLogUnit"); } + } - public boolean isSafeToLog() { - return mWordsRemainingToSkip <= 1; + /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, + final ResearchLog researchLog, final boolean isIncludingPrivateData) { + LogUnit logUnit; + while ((logUnit = logBuffer.shiftOut()) != null) { + researchLog.publish(logUnit, isIncludingPrivateData); } } - /* package for test */ LoggingFrequencyState mLoggingFrequencyState = - new LoggingFrequencyState(LoggingFrequencyState.DEFAULT_WORD_LOG_FREQUENCY); - - /* package for test */ boolean isPrivacyThreat(String word) { - // Current checks: - // - Word not in dictionary - // - Word contains numbers - // - Privacy-safe word not logged recently - if (TextUtils.isEmpty(word)) { - return false; - } - if (!mLoggingFrequencyState.isSafeToLog()) { - return true; - } + private boolean hasOnlyLetters(final String word) { final int length = word.length(); - boolean hasLetter = false; for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = Character.codePointAt(word, i); - if (Character.isDigit(codePoint)) { - return true; - } - if (Character.isLetter(codePoint)) { - hasLetter = true; - break; // Word may contain digits, but will only be allowed if in the dictionary. - } - } - if (hasLetter) { - if (mDictionary == null && mSuggest != null && mSuggest.hasMainDictionary()) { - mDictionary = mSuggest.getMainDictionary(); - } - if (mDictionary == null) { - // Can't access dictionary. Assume privacy threat. - return true; + final int codePoint = word.codePointAt(i); + if (!Character.isLetter(codePoint)) { + return false; } - return !(mDictionary.isValidWord(word)); } - // No letters, no numbers. Punctuation, space, or something else. - return false; + return true; } - private void onWordComplete(String word) { - if (isPrivacyThreat(word)) { - publishLogUnit(mCurrentLogUnit, true); - mLoggingFrequencyState.onWordNotLogged(); - } else { - publishLogUnit(mCurrentLogUnit, false); - mLoggingFrequencyState.onWordLogged(); - } - mCurrentLogUnit = new LogUnit(); - mStatistics.recordWordEntered(); - } - - private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) { - if (!isAllowedToLog()) { - return; - } - if (mMainResearchLog == null) { - return; - } - if (isPrivacySensitive) { - mMainResearchLog.publishPublicEvents(logUnit); - } else { - mMainResearchLog.publishAllEvents(logUnit); - } - mIntentionalResearchLogQueue.add(logUnit); - } - - /* package */ void publishCurrentLogUnit(ResearchLog researchLog, boolean isPrivacySensitive) { - publishLogUnit(mCurrentLogUnit, isPrivacySensitive); - } - - static class LogUnit { - private final List<String[]> mKeysList = CollectionUtils.newArrayList(); - private final List<Object[]> mValuesList = CollectionUtils.newArrayList(); - private final List<Boolean> mIsPotentiallyPrivate = CollectionUtils.newArrayList(); - - private void addLogAtom(final String[] keys, final Object[] values, - final Boolean isPotentiallyPrivate) { - mKeysList.add(keys); - mValuesList.add(values); - mIsPotentiallyPrivate.add(isPotentiallyPrivate); - } - - public void publishPublicEventsTo(ResearchLog researchLog) { - final int size = mKeysList.size(); - for (int i = 0; i < size; i++) { - if (!mIsPotentiallyPrivate.get(i)) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); - } - } - } - - public void publishAllEventsTo(ResearchLog researchLog) { - final int size = mKeysList.size(); - for (int i = 0; i < size; i++) { - researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); - } + private void onWordComplete(final String word) { + Log.d(TAG, "onWordComplete: " + word); + if (word != null && word.length() > 0 && hasOnlyLetters(word)) { + mCurrentLogUnit.setWord(word); + mStatistics.recordWordEntered(); } + commitCurrentLogUnit(); } private static int scrubDigitFromCodePoint(int codePoint) { @@ -859,12 +739,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return WORD_REPLACEMENT_STRING; } - // Special methods related to startup, shutdown, logging itself - - private static final String[] EVENTKEYS_INTENTIONAL_LOG = { - "IntentionalLog" - }; - private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" @@ -872,9 +746,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); - if (researchLogger.mInFeedbackDialog) { - researchLogger.saveLogsForFeedback(); - } researchLogger.start(); if (editorInfo != null) { final Context context = researchLogger.mInputMethodService; @@ -906,14 +777,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang "UserFeedback", "FeedbackContents" }; - private void userFeedback(ResearchLog researchLog, String feedbackContents) { - // this method is special; it directs the feedbackContents to a particular researchLog - final LogUnit logUnit = new LogUnit(); + private static final String[] EVENTKEYS_PREFS_CHANGED = { + "PrefsChanged", "prefs" + }; + public static void prefsChanged(final SharedPreferences prefs) { + final ResearchLogger researchLogger = getInstance(); final Object[] values = { - feedbackContents + prefs }; - logUnit.addLogAtom(EVENTKEYS_USER_FEEDBACK, values, false); - researchLog.publishAllEvents(logUnit); + researchLogger.enqueueEvent(EVENTKEYS_PREFS_CHANGED, values); } // Regular logging methods @@ -950,12 +822,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang "LatinIMEOnCodeInput", "code", "x", "y" }; public static void latinIME_onCodeInput(final int code, final int x, final int y) { + final long time = SystemClock.uptimeMillis(); + final ResearchLogger researchLogger = getInstance(); final Object[] values = { Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y }; - final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); - researchLogger.mStatistics.recordChar(code, SystemClock.uptimeMillis()); + if (Character.isDigit(code)) { + researchLogger.setCurrentLogUnitContainsDigitFlag(); + } + researchLogger.mStatistics.recordChar(code, time); } private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = { @@ -1020,9 +896,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values); - // Play it safe. Remove privacy-sensitive events. - researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true); - researchLogger.mCurrentLogUnit = new LogUnit(); + researchLogger.commitCurrentLogUnit(); getInstance().stop(); } } @@ -1089,7 +963,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { Keyboard.printableCode(scrubDigitFromCodePoint(code)) }; - getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); + final ResearchLogger researchLogger = getInstance(); + researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); + if (Character.isDigit(code)) { + researchLogger.setCurrentLogUnitContainsDigitFlag(); + } } private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = { @@ -1227,10 +1105,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values); } + // Disabled for privacy-protection reasons. Because this event comes after + // richInputConnection_commitText, which is the event used to separate LogUnits, the + // data in this event can be associated with the next LogUnit, revealing information + // about the current word even if it was supposed to be suppressed. The occurrance of + // autocorrection can be determined by examining the difference between the text strings in + // the last call to richInputConnection_setComposingText before + // richInputConnection_commitText, so it's not a data loss. + // TODO: Figure out how to log this event without loss of privacy. + /* private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = { - "RichInputConnectionCommitCorrection", "CorrectionInfo" + "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection" }; + */ public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) { + /* final String typedWord = correctionInfo.getOldText().toString(); final String autoCorrection = correctionInfo.getNewText().toString(); final Object[] values = { @@ -1239,6 +1128,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent( EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values); + */ } private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = { @@ -1264,7 +1154,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { beforeLength, afterLength }; - getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); + getInstance().enqueuePotentiallyPrivateEvent( + EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); } private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = { @@ -1294,7 +1185,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang keyEvent.getAction(), keyEvent.getKeyCode() }; - getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, values); + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, + values); } private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = { @@ -1302,10 +1194,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang }; public static void richInputConnection_setComposingText(final CharSequence text, final int newCursorPosition) { + if (text == null) { + throw new RuntimeException("setComposingText is null"); + } final Object[] values = { text, newCursorPosition }; - getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, values); + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, + values); } private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = { @@ -1315,7 +1211,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final Object[] values = { from, to }; - getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, values); + getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, + values); } private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = { @@ -1350,4 +1247,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public void userTimestamp() { getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES); } + + private static final String[] EVENTKEYS_STATISTICS = { + "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount", + "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys", + "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete" + }; + private static void logStatistics() { + final ResearchLogger researchLogger = getInstance(); + final Statistics statistics = researchLogger.mStatistics; + final Object[] values = { + statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount, + statistics.mSpaceCount, statistics.mDeleteKeyCount, + statistics.mWordCount, statistics.mIsEmptyUponStarting, + statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), + statistics.mBeforeDeleteKeyCounter.getAverageTime(), + statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), + statistics.mAfterDeleteKeyCounter.getAverageTime() + }; + researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values); + } } diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java index 4a2cd079c..eab465aa2 100644 --- a/java/src/com/android/inputmethod/research/Statistics.java +++ b/java/src/com/android/inputmethod/research/Statistics.java @@ -66,8 +66,8 @@ public class Statistics { // To account for the interruptions when the user's attention is directed elsewhere, times // longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic. - public static final int MIN_TYPING_INTERMISSION = 5 * 1000; // in milliseconds - public static final int MIN_DELETION_INTERMISSION = 15 * 1000; // in milliseconds + public static final int MIN_TYPING_INTERMISSION = 2 * 1000; // in milliseconds + public static final int MIN_DELETION_INTERMISSION = 10 * 1000; // in milliseconds // The last time that a tap was performed private long mLastTapTime; diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java new file mode 100644 index 000000000..7a5749096 --- /dev/null +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.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 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; + +public final class UploaderService extends IntentService { + private static final String TAG = UploaderService.class.getSimpleName(); + public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR; + private 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(); + } + } + + @Override + protected void onHandleIntent(Intent intent) { + if (!mCanUpload) { + return; + } + 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())) { + return; + } + if (mFilesDir == null) { + return; + } + final File[] files = mFilesDir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.getName().startsWith(ResearchLogger.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) { + 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 (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; + 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; + } +} |