Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c3866bf8 authored by Blazej Magnowski's avatar Blazej Magnowski Committed by Android (Google) Code Review
Browse files

Merge "System to track touch and sensor events before unlocking the phone"

parents 3a22ad0a 7232332d
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -3,13 +3,15 @@ include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := optional

LOCAL_SRC_FILES := $(call all-java-files-under, src) \
LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-proto-files-under,src) \
    src/com/android/systemui/EventLogTags.logtags

LOCAL_STATIC_JAVA_LIBRARIES := Keyguard
LOCAL_JAVA_LIBRARIES := telephony-common

LOCAL_PACKAGE_NAME := SystemUI
LOCAL_PROTOC_OPTIMIZE_TYPE := nano
LOCAL_PROTO_JAVA_OUTPUT_PARAMS := optional_field_style=accessors
LOCAL_CERTIFICATE := platform
LOCAL_PRIVILEGED_MODULE := true

+453 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.systemui.analytics;

import android.content.Context;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.view.MotionEvent;

import com.android.systemui.statusbar.StatusBarState;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session;
import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session.PhoneEvent;

/**
 * Tracks touch, sensor and phone events when the lockscreen is on. If the phone is unlocked
 * the data containing these events is saved to a file. This data is collected
 * to analyze how a human interaction looks like.
 *
 * A session starts when the screen is turned on.
 * A session ends when the screen is turned off or user unlocks the phone.
 */
public class LockedPhoneAnalytics implements SensorEventListener {
    private static final String TAG = "LockedPhoneAnalytics";
    private static final String ANALYTICS_ENABLE = "locked_phone_analytics_enable";
    private static final String ENFORCE_BOUNCER = "locked_phone_analytics_enforce_bouncer";
    private static final String COLLECT_BAD_TOCUHES = "locked_phone_analytics_collect_bad_touches";

    private static final long TIMEOUT_MILLIS = 11000; // 11 seconds.
    public static final boolean DEBUG = false;

    private static final int[] SENSORS = new int[] {
            Sensor.TYPE_ACCELEROMETER,
            Sensor.TYPE_GYROSCOPE,
            Sensor.TYPE_PROXIMITY,
            Sensor.TYPE_LIGHT,
            Sensor.TYPE_ROTATION_VECTOR,
    };

    private final Handler mHandler = new Handler();
    protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
        @Override
        public void onChange(boolean selfChange) {
            updateConfiguration();
        }
    };

    private final SensorManager mSensorManager;
    private final Context mContext;

    // Err on the side of caution, so logging is not started after a crash even tough the screen
    // is off.
    private SensorLoggerSession mCurrentSession = null;

    private boolean mEnableAnalytics = false;
    private boolean mEnforceBouncer = false;
    private boolean mTimeoutActive = false;
    private boolean mCollectBadTouches = false;
    private boolean mBouncerOn = false;
    private boolean mCornerSwiping = false;
    private boolean mTrackingStarted = false;

    private int mState = StatusBarState.SHADE;

    private static LockedPhoneAnalytics sInstance = null;

    private LockedPhoneAnalytics(Context context) {
        mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        mContext = context;

        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(ANALYTICS_ENABLE), false,
                mSettingsObserver,
                UserHandle.USER_ALL);

        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(ENFORCE_BOUNCER), false,
                mSettingsObserver,
                UserHandle.USER_ALL);

        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(COLLECT_BAD_TOCUHES), false,
                mSettingsObserver,
                UserHandle.USER_ALL);

        updateConfiguration();
    }

    public static LockedPhoneAnalytics getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new LockedPhoneAnalytics(context);
        }
        return sInstance;
    }

    private void updateConfiguration() {
        mEnableAnalytics = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt(
                mContext.getContentResolver(),
                ANALYTICS_ENABLE, 0);
        mEnforceBouncer = mEnableAnalytics && 0 != Settings.Secure.getInt(
                mContext.getContentResolver(),
                ENFORCE_BOUNCER, 0);
        mCollectBadTouches = mEnableAnalytics && 0 != Settings.Secure.getInt(
                mContext.getContentResolver(),
                COLLECT_BAD_TOCUHES, 0);
    }

    private boolean sessionEntrypoint() {
        if ((mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED)
                && mEnableAnalytics && mCurrentSession == null) {
            onSessionStart();
            return true;
        }
        return false;
    }

    private void sessionExitpoint(int result) {
        if (mEnableAnalytics && mCurrentSession != null) {
            onSessionEnd(result);
        }
    }

    private void onSessionStart() {
        mBouncerOn = false;
        mCornerSwiping = false;
        mTrackingStarted = false;
        mCurrentSession = new SensorLoggerSession(System.currentTimeMillis(), System.nanoTime());
        for (int sensorType : SENSORS) {
            Sensor s = mSensorManager.getDefaultSensor(sensorType);
            if (s != null) {
                mSensorManager.registerListener(this, s, SensorManager.SENSOR_DELAY_GAME);
            }
        }
    }

    private void onSessionEnd(int result) {
        SensorLoggerSession session = mCurrentSession;
        mCurrentSession = null;

        session.end(System.currentTimeMillis(), result);
        queueSession(session);
    }

    private void queueSession(final SensorLoggerSession currentSession) {
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                byte[] b = Session.toByteArray(currentSession.toProto());
                String dir = mContext.getFilesDir().getAbsolutePath();
                if (currentSession.getResult() != Session.SUCCESS) {
                    if (!mCollectBadTouches) {
                        return;
                    }
                    dir += "/bad_touches";
                } else {
                    dir += "/good_touches";
                }

                File file = new File(dir);
                file.mkdir();
                File touch = new File(file, "trace_" + System.currentTimeMillis());

                try {
                    new FileOutputStream(touch).write(b);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }


    @Override
    public synchronized void onSensorChanged(SensorEvent event) {
        if (mEnableAnalytics && mCurrentSession != null) {
            mCurrentSession.addSensorEvent(event, System.nanoTime());
            enforceTimeout();
        }
    }

    private void enforceTimeout() {
        if (mTimeoutActive) {
            if (System.currentTimeMillis() - mCurrentSession.getStartTimestampMillis()
                    > TIMEOUT_MILLIS) {
                onSessionEnd(Session.UNKNOWN);
                if (DEBUG) {
                    Log.i(TAG, "Analytics timed out.");
                }
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    public boolean shouldEnforceBouncer() {
        return mEnforceBouncer;
    }

    public void setStatusBarState(int state) {
        mState = state;
    }

    public void onScreenOn() {
        if (sessionEntrypoint()) {
            if (DEBUG) {
                Log.d(TAG, "onScreenOn");
            }
            addEvent(PhoneEvent.ON_SCREEN_ON);
        }
    }

    public void onScreenOnFromTouch() {
        if (sessionEntrypoint()) {
            if (DEBUG) {
                Log.d(TAG, "onScreenOnFromTouch");
            }
            addEvent(PhoneEvent.ON_SCREEN_ON_FROM_TOUCH);
        }
    }

    public void onScreenOff() {
        if (DEBUG) {
            Log.d(TAG, "onScreenOff");
        }
        addEvent(PhoneEvent.ON_SCREEN_OFF);
        sessionExitpoint(Session.FAILURE);
    }

    public void onSucccessfulUnlock() {
        if (DEBUG) {
            Log.d(TAG, "onSuccessfulUnlock");
        }
        addEvent(PhoneEvent.ON_SUCCESSFUL_UNLOCK);
        sessionExitpoint(Session.SUCCESS);
    }

    public void onBouncerShown() {
        if (!mBouncerOn) {
            if (DEBUG) {
                Log.d(TAG, "onBouncerShown");
            }
            mBouncerOn = true;
            addEvent(PhoneEvent.ON_BOUNCER_SHOWN);
        }
    }

    public void onBouncerHidden() {
        if (mBouncerOn) {
            if (DEBUG) {
                Log.d(TAG, "onBouncerHidden");
            }
            mBouncerOn = false;
            addEvent(PhoneEvent.ON_BOUNCER_HIDDEN);
        }
    }

    public void onQsDown() {
        if (DEBUG) {
            Log.d(TAG, "onQsDown");
        }
        addEvent(PhoneEvent.ON_QS_DOWN);
    }

    public void setQsExpanded(boolean expanded) {
        if (DEBUG) {
            Log.d(TAG, "setQsExpanded = " + expanded);
        }
        if (expanded) {
            addEvent(PhoneEvent.SET_QS_EXPANDED_TRUE);
        } else {
            addEvent(PhoneEvent.SET_QS_EXPANDED_FALSE);
        }
    }

    public void onTrackingStarted() {
        if (DEBUG) {
            Log.d(TAG, "onTrackingStarted");
        }
        mTrackingStarted = true;
        addEvent(PhoneEvent.ON_TRACKING_STARTED);
    }

    public void onTrackingStopped() {
        if (mTrackingStarted) {
            if (DEBUG) {
                Log.d(TAG, "onTrackingStopped");
            }
            mTrackingStarted = false;
            addEvent(PhoneEvent.ON_TRACKING_STOPPED);
        }
    }

    public void onNotificationActive() {
        if (DEBUG) {
            Log.d(TAG, "onNotificationActive");
        }
        addEvent(PhoneEvent.ON_NOTIFICATION_ACTIVE);
    }


    public void onNotificationDoubleTap() {
        if (DEBUG) {
            Log.d(TAG, "onNotificationDoubleTap");
        }
        addEvent(PhoneEvent.ON_NOTIFICATION_DOUBLE_TAP);
    }

    public void setNotificationExpanded() {
        if (DEBUG) {
            Log.d(TAG, "setNotificationExpanded");
        }
        addEvent(PhoneEvent.SET_NOTIFICATION_EXPANDED);
    }

    public void onNotificatonStartDraggingDown() {
        if (DEBUG) {
            Log.d(TAG, "onNotificationStartDraggingDown");
        }
        addEvent(PhoneEvent.ON_NOTIFICATION_START_DRAGGING_DOWN);
    }

    public void onNotificatonStopDraggingDown() {
        if (DEBUG) {
            Log.d(TAG, "onNotificationStopDraggingDown");
        }
        addEvent(PhoneEvent.ON_NOTIFICATION_STOP_DRAGGING_DOWN);
    }

    public void onNotificationDismissed() {
        if (DEBUG) {
            Log.d(TAG, "onNotificationDismissed");
        }
        addEvent(PhoneEvent.ON_NOTIFICATION_DISMISSED);
    }

    public void onNotificatonStartDismissing() {
        if (DEBUG) {
            Log.d(TAG, "onNotificationStartDismissing");
        }
        addEvent(PhoneEvent.ON_NOTIFICATION_START_DISMISSING);
    }

    public void onNotificatonStopDismissing() {
        if (DEBUG) {
            Log.d(TAG, "onNotificationStopDismissing");
        }
        addEvent(PhoneEvent.ON_NOTIFICATION_STOP_DISMISSING);
    }

    public void onCameraOn() {
        if (DEBUG) {
            Log.d(TAG, "onCameraOn");
        }
        addEvent(PhoneEvent.ON_CAMERA_ON);
    }

    public void onLeftAffordanceOn() {
        if (DEBUG) {
            Log.d(TAG, "onLeftAffordanceOn");
        }
        addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_ON);
    }

    public void onAffordanceSwipingStarted(boolean rightCorner) {
        if (DEBUG) {
            Log.d(TAG, "onAffordanceSwipingStarted");
        }
        mCornerSwiping = true;
        if (rightCorner) {
            addEvent(PhoneEvent.ON_RIGHT_AFFORDANCE_SWIPING_STARTED);
        } else {
            addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_SWIPING_STARTED);
        }
    }

    public void onAffordanceSwipingAborted() {
        if (mCornerSwiping) {
            if (DEBUG) {
                Log.d(TAG, "onAffordanceSwipingAborted");
            }
            mCornerSwiping = false;
            addEvent(PhoneEvent.ON_AFFORDANCE_SWIPING_ABORTED);
        }
    }

    public void onUnlockHintStarted() {
        if (DEBUG) {
            Log.d(TAG, "onUnlockHintStarted");
        }
        addEvent(PhoneEvent.ON_UNLOCK_HINT_STARTED);
    }

    public void onCameraHintStarted() {
        if (DEBUG) {
            Log.d(TAG, "onCameraHintStarted");
        }
        addEvent(PhoneEvent.ON_CAMERA_HINT_STARTED);
    }

    public void onLeftAffordanceHintStarted() {
        if (DEBUG) {
            Log.d(TAG, "onLeftAffordanceHintStarted");
        }
        addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_HINT_STARTED);
    }

    public void onTouchEvent(MotionEvent ev, int width, int height) {
        if (!mBouncerOn && mCurrentSession != null) {
            if (DEBUG) {
                Log.v(TAG, "onTouchEvent(ev.action="
                        + MotionEvent.actionToString(ev.getAction()) + ")");
            }
            mCurrentSession.addMotionEvent(ev);
            mCurrentSession.setTouchArea(width, height);
            enforceTimeout();
        }
    }

    private void addEvent(int eventType) {
        if (mEnableAnalytics && mCurrentSession != null) {
            mCurrentSession.addPhoneEvent(eventType, System.nanoTime());
        }
    }
}
+160 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.systemui.analytics;

import android.os.Build;
import android.util.Log;
import android.view.MotionEvent;

import java.util.ArrayList;

import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session;
import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session.TouchEvent;
import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session.SensorEvent;
import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session.PhoneEvent;

/**
 * Collects touch, sensor and phone events and converts the data to
 * TouchAnalyticsProto.Session.
 */
public class SensorLoggerSession {
    private static final String TAG = "SensorLoggerSession";

    private final long mStartTimestampMillis;
    private final long mStartSystemTimeNanos;

    private long mEndTimestampMillis;
    private int mType;

    private ArrayList<TouchEvent> mMotionEvents = new ArrayList<>();
    private ArrayList<SensorEvent> mSensorEvents = new ArrayList<>();
    private ArrayList<PhoneEvent> mPhoneEvents = new ArrayList<>();
    private int mTouchAreaHeight;
    private int mTouchAreaWidth;
    private int mResult = Session.UNKNOWN;

    public SensorLoggerSession(long startTimestampMillis, long startSystemTimeNanos) {
        mStartTimestampMillis = startTimestampMillis;
        mStartSystemTimeNanos = startSystemTimeNanos;
        mType = Session.REAL;
    }

    public void end(long endTimestampMillis, int result) {
        mResult = result;
        mEndTimestampMillis = endTimestampMillis;

        if (LockedPhoneAnalytics.DEBUG) {
            Log.d(TAG, "Ending session result=" + result + " it lasted for " +
                    (float) (mEndTimestampMillis - mStartTimestampMillis) / 1000f + "s");
        }
    }

    public void addMotionEvent(MotionEvent motionEvent) {
        TouchEvent event = motionEventToProto(motionEvent);
        mMotionEvents.add(event);
    }

    public void addSensorEvent(android.hardware.SensorEvent eventOrig, long systemTimeNanos) {
        SensorEvent event = sensorEventToProto(eventOrig, systemTimeNanos);
        mSensorEvents.add(event);
    }

    public void addPhoneEvent(int eventType, long systemTimeNanos) {
        PhoneEvent event = phoneEventToProto(eventType, systemTimeNanos);
        mPhoneEvents.add(event);
    }


    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Session{");
        sb.append("mStartTimestampMillis=").append(mStartTimestampMillis);
        sb.append(", mStartSystemTimeNanos=").append(mStartSystemTimeNanos);
        sb.append(", mEndTimestampMillis=").append(mEndTimestampMillis);
        sb.append(", mResult=").append(mResult);
        sb.append(", mTouchAreaHeight=").append(mTouchAreaHeight);
        sb.append(", mTouchAreaWidth=").append(mTouchAreaWidth);
        sb.append(", mMotionEvents=[size=").append(mMotionEvents.size()).append("]");
        sb.append(", mSensorEvents=[size=").append(mSensorEvents.size()).append("]");
        sb.append(", mPhoneEvents=[size=").append(mPhoneEvents.size()).append("]");
        sb.append('}');
        return sb.toString();
    }

    public Session toProto() {
        Session proto = new Session();
        proto.setStartTimestampMillis(mStartTimestampMillis);
        proto.setDurationMillis(mEndTimestampMillis - mStartTimestampMillis);
        proto.setBuild(Build.FINGERPRINT);
        proto.setResult(mResult);
        proto.setType(mType);
        proto.sensorEvents = mSensorEvents.toArray(proto.sensorEvents);
        proto.touchEvents = mMotionEvents.toArray(proto.touchEvents);
        proto.phoneEvents = mPhoneEvents.toArray(proto.phoneEvents);
        proto.setTouchAreaWidth(mTouchAreaWidth);
        proto.setTouchAreaHeight(mTouchAreaHeight);
        return proto;
    }

    private PhoneEvent phoneEventToProto(int eventType, long sysTimeNanos) {
        PhoneEvent proto = new PhoneEvent();
        proto.setType(eventType);
        proto.setTimeOffsetNanos(sysTimeNanos - mStartSystemTimeNanos);
        return proto;
    }

    private SensorEvent sensorEventToProto(android.hardware.SensorEvent ev, long sysTimeNanos) {
        SensorEvent proto = new SensorEvent();
        proto.setType(ev.sensor.getType());
        proto.setTimeOffsetNanos(sysTimeNanos - mStartSystemTimeNanos);
        proto.setTimestamp(ev.timestamp);
        proto.values = ev.values.clone();
        return proto;
    }

    private TouchEvent motionEventToProto(MotionEvent ev) {
        int count = ev.getPointerCount();
        TouchEvent proto = new TouchEvent();
        proto.setTimeOffsetNanos(ev.getEventTimeNano() - mStartSystemTimeNanos);
        proto.setAction(ev.getActionMasked());
        proto.setActionIndex(ev.getActionIndex());
        proto.pointers = new TouchEvent.Pointer[count];
        for (int i = 0; i < count; i++) {
            TouchEvent.Pointer p = new TouchEvent.Pointer();
            p.setX(ev.getX(i));
            p.setY(ev.getY(i));
            p.setSize(ev.getSize(i));
            p.setPressure(ev.getPressure(i));
            p.setId(ev.getPointerId(i));
            proto.pointers[i] = p;
        }
        return proto;
    }

    public void setTouchArea(int width, int height) {
        mTouchAreaWidth = width;
        mTouchAreaHeight = height;
    }

    public int getResult() {
        return mResult;
    }

    public long getStartTimestampMillis() {
        return mStartTimestampMillis;
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.SystemUI;
import com.android.systemui.statusbar.phone.FingerprintUnlockController;
import com.android.systemui.analytics.LockedPhoneAnalytics;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -1240,6 +1241,7 @@ public class KeyguardViewMediator extends SystemUI {
                case START_KEYGUARD_EXIT_ANIM:
                    StartKeyguardExitAnimParams params = (StartKeyguardExitAnimParams) msg.obj;
                    handleStartKeyguardExitAnimation(params.startTime, params.fadeoutDuration);
                    LockedPhoneAnalytics.getInstance(mContext).onSucccessfulUnlock();
                    break;
                case KEYGUARD_DONE_PENDING_TIMEOUT:
                    Log.w(TAG, "Timeout while waiting for activity drawn!");
+5 −0
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;

import com.android.systemui.R;
import com.android.systemui.analytics.LockedPhoneAnalytics;

/**
 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer}
@@ -128,6 +129,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
    private final int mNormalColor;
    private final int mLowPriorityColor;
    private boolean mIsBelowSpeedBump;
    private LockedPhoneAnalytics mLockedPhoneAnalytics;

    public ActivatableNotificationView(Context context, AttributeSet attrs) {
        super(context, attrs);
@@ -151,6 +153,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
                R.color.notification_ripple_color_low_priority);
        mNormalRippleColor = context.getColor(
                R.color.notification_ripple_untinted_color);
        mLockedPhoneAnalytics = LockedPhoneAnalytics.getInstance(context);
    }

    @Override
@@ -219,6 +222,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
                        makeActive();
                        postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
                    } else {
                        mLockedPhoneAnalytics.onNotificationDoubleTap();
                        boolean performed = performClick();
                        if (performed) {
                            removeCallbacks(mTapTimeoutRunnable);
@@ -238,6 +242,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
    }

    private void makeActive() {
        mLockedPhoneAnalytics.onNotificationActive();
        startActivateAnimation(false /* reverse */);
        mActivated = true;
        if (mOnActivatedListener != null) {
Loading