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