1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.analytics; 18 19 import android.content.Context; 20 import android.database.ContentObserver; 21 import android.hardware.Sensor; 22 import android.hardware.SensorEvent; 23 import android.hardware.SensorEventListener; 24 import android.net.Uri; 25 import android.os.AsyncTask; 26 import android.os.Build; 27 import android.os.Handler; 28 import android.os.UserHandle; 29 import android.provider.Settings; 30 import android.util.Log; 31 import android.view.MotionEvent; 32 import android.widget.Toast; 33 34 import java.io.File; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 38 import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session; 39 import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session.PhoneEvent; 40 41 /** 42 * Tracks touch, sensor and phone events when the lockscreen is on. If the phone is unlocked 43 * the data containing these events is saved to a file. This data is collected 44 * to analyze how a human interaction looks like. 45 * 46 * A session starts when the screen is turned on. 47 * A session ends when the screen is turned off or user unlocks the phone. 48 */ 49 public class DataCollector implements SensorEventListener { 50 private static final String TAG = "DataCollector"; 51 private static final String COLLECTOR_ENABLE = "data_collector_enable"; 52 private static final String COLLECT_BAD_TOUCHES = "data_collector_collect_bad_touches"; 53 private static final String ALLOW_REJECTED_TOUCH_REPORTS = 54 "data_collector_allow_rejected_touch_reports"; 55 56 private static final long TIMEOUT_MILLIS = 11000; // 11 seconds. 57 public static final boolean DEBUG = false; 58 59 private final Handler mHandler = new Handler(); 60 private final Context mContext; 61 62 // Err on the side of caution, so logging is not started after a crash even tough the screen 63 // is off. 64 private SensorLoggerSession mCurrentSession = null; 65 66 private boolean mEnableCollector = false; 67 private boolean mTimeoutActive = false; 68 private boolean mCollectBadTouches = false; 69 private boolean mCornerSwiping = false; 70 private boolean mTrackingStarted = false; 71 private boolean mAllowReportRejectedTouch = false; 72 73 private static DataCollector sInstance = null; 74 75 protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) { 76 @Override 77 public void onChange(boolean selfChange) { 78 updateConfiguration(); 79 } 80 }; 81 82 private DataCollector(Context context) { 83 mContext = context; 84 85 mContext.getContentResolver().registerContentObserver( 86 Settings.Secure.getUriFor(COLLECTOR_ENABLE), false, 87 mSettingsObserver, 88 UserHandle.USER_ALL); 89 90 mContext.getContentResolver().registerContentObserver( 91 Settings.Secure.getUriFor(COLLECT_BAD_TOUCHES), false, 92 mSettingsObserver, 93 UserHandle.USER_ALL); 94 95 mContext.getContentResolver().registerContentObserver( 96 Settings.Secure.getUriFor(ALLOW_REJECTED_TOUCH_REPORTS), false, 97 mSettingsObserver, 98 UserHandle.USER_ALL); 99 100 updateConfiguration(); 101 } 102 103 public static DataCollector getInstance(Context context) { 104 if (sInstance == null) { 105 sInstance = new DataCollector(context); 106 } 107 return sInstance; 108 } 109 110 private void updateConfiguration() { 111 mEnableCollector = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt( 112 mContext.getContentResolver(), 113 COLLECTOR_ENABLE, 0); 114 mCollectBadTouches = mEnableCollector && 0 != Settings.Secure.getInt( 115 mContext.getContentResolver(), 116 COLLECT_BAD_TOUCHES, 0); 117 mAllowReportRejectedTouch = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt( 118 mContext.getContentResolver(), 119 ALLOW_REJECTED_TOUCH_REPORTS, 0); 120 } 121 122 private boolean sessionEntrypoint() { 123 if (isEnabled() && mCurrentSession == null) { 124 onSessionStart(); 125 return true; 126 } 127 return false; 128 } 129 130 private void sessionExitpoint(int result) { 131 if (mCurrentSession != null) { 132 onSessionEnd(result); 133 } 134 } 135 136 private void onSessionStart() { 137 mCornerSwiping = false; 138 mTrackingStarted = false; 139 mCurrentSession = new SensorLoggerSession(System.currentTimeMillis(), System.nanoTime()); 140 } 141 142 private void onSessionEnd(int result) { 143 SensorLoggerSession session = mCurrentSession; 144 mCurrentSession = null; 145 146 if (mEnableCollector) { 147 session.end(System.currentTimeMillis(), result); 148 queueSession(session); 149 } 150 } 151 152 public Uri reportRejectedTouch() { 153 if (mCurrentSession == null) { 154 Toast.makeText(mContext, "Generating rejected touch report failed: session timed out.", 155 Toast.LENGTH_LONG).show(); 156 return null; 157 } 158 SensorLoggerSession currentSession = mCurrentSession; 159 160 currentSession.setType(Session.REJECTED_TOUCH_REPORT); 161 currentSession.end(System.currentTimeMillis(), Session.SUCCESS); 162 Session proto = currentSession.toProto(); 163 164 byte[] b = Session.toByteArray(proto); 165 File dir = new File(mContext.getExternalCacheDir(), "rejected_touch_reports"); 166 dir.mkdir(); 167 File touch = new File(dir, "rejected_touch_report_" + System.currentTimeMillis()); 168 169 try { 170 new FileOutputStream(touch).write(b); 171 } catch (IOException e) { 172 throw new RuntimeException(e); 173 } 174 175 return Uri.fromFile(touch); 176 } 177 178 private void queueSession(final SensorLoggerSession currentSession) { 179 AsyncTask.execute(new Runnable() { 180 @Override 181 public void run() { 182 byte[] b = Session.toByteArray(currentSession.toProto()); 183 String dir = mContext.getFilesDir().getAbsolutePath(); 184 if (currentSession.getResult() != Session.SUCCESS) { 185 if (!mCollectBadTouches) { 186 return; 187 } 188 dir += "/bad_touches"; 189 } else { 190 dir += "/good_touches"; 191 } 192 193 File file = new File(dir); 194 file.mkdir(); 195 File touch = new File(file, "trace_" + System.currentTimeMillis()); 196 197 try { 198 new FileOutputStream(touch).write(b); 199 } catch (IOException e) { 200 throw new RuntimeException(e); 201 } 202 } 203 }); 204 } 205 206 @Override 207 public synchronized void onSensorChanged(SensorEvent event) { 208 if (isEnabled() && mCurrentSession != null) { 209 mCurrentSession.addSensorEvent(event, System.nanoTime()); 210 enforceTimeout(); 211 } 212 } 213 214 private void enforceTimeout() { 215 if (mTimeoutActive) { 216 if (System.currentTimeMillis() - mCurrentSession.getStartTimestampMillis() 217 > TIMEOUT_MILLIS) { 218 onSessionEnd(Session.UNKNOWN); 219 if (DEBUG) { 220 Log.i(TAG, "Analytics timed out."); 221 } 222 } 223 } 224 } 225 226 @Override 227 public void onAccuracyChanged(Sensor sensor, int accuracy) { 228 } 229 230 /** 231 * @return true if data is being collected - either for data gathering or creating a 232 * rejected touch report. 233 */ 234 public boolean isEnabled() { 235 return mEnableCollector || mAllowReportRejectedTouch; 236 } 237 238 /** 239 * @return true if the full data set for data gathering should be collected - including 240 * extensive sensor data, which is is not normally included with rejected touch reports. 241 */ 242 public boolean isEnabledFull() { 243 return mEnableCollector; 244 } 245 246 public void onScreenTurningOn() { 247 if (sessionEntrypoint()) { 248 if (DEBUG) { 249 Log.d(TAG, "onScreenTurningOn"); 250 } 251 addEvent(PhoneEvent.ON_SCREEN_ON); 252 } 253 } 254 255 public void onScreenOnFromTouch() { 256 if (sessionEntrypoint()) { 257 if (DEBUG) { 258 Log.d(TAG, "onScreenOnFromTouch"); 259 } 260 addEvent(PhoneEvent.ON_SCREEN_ON_FROM_TOUCH); 261 } 262 } 263 264 public void onScreenOff() { 265 if (DEBUG) { 266 Log.d(TAG, "onScreenOff"); 267 } 268 addEvent(PhoneEvent.ON_SCREEN_OFF); 269 sessionExitpoint(Session.FAILURE); 270 } 271 272 public void onSucccessfulUnlock() { 273 if (DEBUG) { 274 Log.d(TAG, "onSuccessfulUnlock"); 275 } 276 addEvent(PhoneEvent.ON_SUCCESSFUL_UNLOCK); 277 sessionExitpoint(Session.SUCCESS); 278 } 279 280 public void onBouncerShown() { 281 if (DEBUG) { 282 Log.d(TAG, "onBouncerShown"); 283 } 284 addEvent(PhoneEvent.ON_BOUNCER_SHOWN); 285 } 286 287 public void onBouncerHidden() { 288 if (DEBUG) { 289 Log.d(TAG, "onBouncerHidden"); 290 } 291 addEvent(PhoneEvent.ON_BOUNCER_HIDDEN); 292 } 293 294 public void onQsDown() { 295 if (DEBUG) { 296 Log.d(TAG, "onQsDown"); 297 } 298 addEvent(PhoneEvent.ON_QS_DOWN); 299 } 300 301 public void setQsExpanded(boolean expanded) { 302 if (DEBUG) { 303 Log.d(TAG, "setQsExpanded = " + expanded); 304 } 305 if (expanded) { 306 addEvent(PhoneEvent.SET_QS_EXPANDED_TRUE); 307 } else { 308 addEvent(PhoneEvent.SET_QS_EXPANDED_FALSE); 309 } 310 } 311 312 public void onTrackingStarted() { 313 if (DEBUG) { 314 Log.d(TAG, "onTrackingStarted"); 315 } 316 mTrackingStarted = true; 317 addEvent(PhoneEvent.ON_TRACKING_STARTED); 318 } 319 320 public void onTrackingStopped() { 321 if (mTrackingStarted) { 322 if (DEBUG) { 323 Log.d(TAG, "onTrackingStopped"); 324 } 325 mTrackingStarted = false; 326 addEvent(PhoneEvent.ON_TRACKING_STOPPED); 327 } 328 } 329 330 public void onNotificationActive() { 331 if (DEBUG) { 332 Log.d(TAG, "onNotificationActive"); 333 } 334 addEvent(PhoneEvent.ON_NOTIFICATION_ACTIVE); 335 } 336 337 338 public void onNotificationDoubleTap() { 339 if (DEBUG) { 340 Log.d(TAG, "onNotificationDoubleTap"); 341 } 342 addEvent(PhoneEvent.ON_NOTIFICATION_DOUBLE_TAP); 343 } 344 345 public void setNotificationExpanded() { 346 if (DEBUG) { 347 Log.d(TAG, "setNotificationExpanded"); 348 } 349 addEvent(PhoneEvent.SET_NOTIFICATION_EXPANDED); 350 } 351 352 public void onNotificatonStartDraggingDown() { 353 if (DEBUG) { 354 Log.d(TAG, "onNotificationStartDraggingDown"); 355 } 356 addEvent(PhoneEvent.ON_NOTIFICATION_START_DRAGGING_DOWN); 357 } 358 359 public void onNotificatonStopDraggingDown() { 360 if (DEBUG) { 361 Log.d(TAG, "onNotificationStopDraggingDown"); 362 } 363 addEvent(PhoneEvent.ON_NOTIFICATION_STOP_DRAGGING_DOWN); 364 } 365 366 public void onNotificationDismissed() { 367 if (DEBUG) { 368 Log.d(TAG, "onNotificationDismissed"); 369 } 370 addEvent(PhoneEvent.ON_NOTIFICATION_DISMISSED); 371 } 372 373 public void onNotificatonStartDismissing() { 374 if (DEBUG) { 375 Log.d(TAG, "onNotificationStartDismissing"); 376 } 377 addEvent(PhoneEvent.ON_NOTIFICATION_START_DISMISSING); 378 } 379 380 public void onNotificatonStopDismissing() { 381 if (DEBUG) { 382 Log.d(TAG, "onNotificationStopDismissing"); 383 } 384 addEvent(PhoneEvent.ON_NOTIFICATION_STOP_DISMISSING); 385 } 386 387 public void onCameraOn() { 388 if (DEBUG) { 389 Log.d(TAG, "onCameraOn"); 390 } 391 addEvent(PhoneEvent.ON_CAMERA_ON); 392 } 393 394 public void onLeftAffordanceOn() { 395 if (DEBUG) { 396 Log.d(TAG, "onLeftAffordanceOn"); 397 } 398 addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_ON); 399 } 400 401 public void onAffordanceSwipingStarted(boolean rightCorner) { 402 if (DEBUG) { 403 Log.d(TAG, "onAffordanceSwipingStarted"); 404 } 405 mCornerSwiping = true; 406 if (rightCorner) { 407 addEvent(PhoneEvent.ON_RIGHT_AFFORDANCE_SWIPING_STARTED); 408 } else { 409 addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_SWIPING_STARTED); 410 } 411 } 412 413 public void onAffordanceSwipingAborted() { 414 if (mCornerSwiping) { 415 if (DEBUG) { 416 Log.d(TAG, "onAffordanceSwipingAborted"); 417 } 418 mCornerSwiping = false; 419 addEvent(PhoneEvent.ON_AFFORDANCE_SWIPING_ABORTED); 420 } 421 } 422 423 public void onUnlockHintStarted() { 424 if (DEBUG) { 425 Log.d(TAG, "onUnlockHintStarted"); 426 } 427 addEvent(PhoneEvent.ON_UNLOCK_HINT_STARTED); 428 } 429 430 public void onCameraHintStarted() { 431 if (DEBUG) { 432 Log.d(TAG, "onCameraHintStarted"); 433 } 434 addEvent(PhoneEvent.ON_CAMERA_HINT_STARTED); 435 } 436 437 public void onLeftAffordanceHintStarted() { 438 if (DEBUG) { 439 Log.d(TAG, "onLeftAffordanceHintStarted"); 440 } 441 addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_HINT_STARTED); 442 } 443 444 public void onTouchEvent(MotionEvent event, int width, int height) { 445 if (mCurrentSession != null) { 446 if (DEBUG) { 447 Log.v(TAG, "onTouchEvent(ev.action=" 448 + MotionEvent.actionToString(event.getAction()) + ")"); 449 } 450 mCurrentSession.addMotionEvent(event); 451 mCurrentSession.setTouchArea(width, height); 452 enforceTimeout(); 453 } 454 } 455 456 private void addEvent(int eventType) { 457 if (isEnabled() && mCurrentSession != null) { 458 mCurrentSession.addPhoneEvent(eventType, System.nanoTime()); 459 } 460 } 461 462 public boolean isReportingEnabled() { 463 return mAllowReportRejectedTouch; 464 } 465 } 466