Home | History | Annotate | Download | only in analytics
      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