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.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