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