<commit_before>/**
 * Copyright (C) 2016 LinkedIn Corp.
 *
 * 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.linkedin.android.shaky;

import android.app.Activity;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.hardware.SensorManager;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.LocalBroadcastManager;
import android.view.View;
import com.squareup.seismic.ShakeDetector;

import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

/**
 * Listens for a shake and then starts the feedback submission flow.
 * <p/>
 * Note: this class supports attaching and detaching activities for configuration changes, but if
 * the {@link CollectDataTask} completes while there is no activity attached, the feedback
 * flow will be terminated.
 */
public class Shaky implements ShakeDetector.Listener {

    private static final String SEND_FEEDBACK_TAG = "SendFeedback";
    private static final String COLLECT_DATA_TAG = "CollectFeedbackData";

    private static final long SHAKE_COOLDOWN_MS = TimeUnit.SECONDS.toMillis(5);

    private final ShakeDelegate delegate;
    private final ShakeDetector shakeDetector;

    private Activity activity;
    private long lastShakeTime;
    private CollectDataTask collectDataTask;

    Shaky(@NonNull Context context, @NonNull ShakeDelegate delegate) {
        Context appContext = context.getApplicationContext();
        this.delegate = delegate;
        shakeDetector = new ShakeDetector(this);

        IntentFilter filter = new IntentFilter();
        filter.addAction(SendFeedbackDialog.ACTION_START_FEEDBACK_FLOW);
        filter.addAction(FeedbackActivity.ACTION_END_FEEDBACK_FLOW);
        LocalBroadcastManager.getInstance(appContext).registerReceiver(createReceiver(), filter);
    }

    /**
     * Entry point into this API.
     *
     * Registers shaky to the current application.
     */
    @NonNull
    public static Shaky with(@NonNull Application application, @NonNull ShakeDelegate delegate) {
        Shaky shaky = new Shaky(application.getApplicationContext(), delegate);
        LifecycleCallbacks lifecycleCallbacks = new LifecycleCallbacks(shaky);
        application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
        return shaky;
    }

    public void startFeedbackFlow() {
        if (!canStartFeedbackFlow()) {
            return;
        }

        new CollectDataDialog().show(activity.getFragmentManager(), COLLECT_DATA_TAG);

        collectDataTask = new CollectDataTask(activity, delegate, createCallback());
        collectDataTask.execute(getScreenshotBitmap());
    }

    void setActivity(@Nullable Activity activity) {
        this.activity = activity;
        if (activity != null) {
            start();
            // we're attaching to a new Activity instance
            // make sure the UI is in sync with the AsyncTask state
            dismissCollectFeedbackDialogIfNecessary();
        } else {
            stop();
        }
    }

    /**
     * Start listening for shakes. Should be called when the {@link Activity} is resumed.
     */
    private void start() {
        if (!delegate.isEnabled()) {
            return;
        }

        shakeDetector.start((SensorManager) activity.getSystemService(Context.SENSOR_SERVICE));
    }

    /**
     * Stop listening for shakes. Safe to call when already stopped. Should be called when the
     * {@link Activity} is paused.
     */
    private void stop() {
        shakeDetector.stop();
    }

    @Override
    public void hearShake() {
        if (shouldIgnoreShake() || !canStartFeedbackFlow()) {
            return;
        }

        new SendFeedbackDialog().show(activity.getFragmentManager(), SEND_FEEDBACK_TAG);
    }

    /**
     * @return true if a shake happened in the last {@link #SHAKE_COOLDOWN_MS}, false otherwise.
     */
    @VisibleForTesting
    boolean shouldIgnoreShake() {
        long now = System.currentTimeMillis();
        if (now < lastShakeTime + SHAKE_COOLDOWN_MS) {
            return true;
        }
        lastShakeTime = now;
        return false;
    }

    /**
     * @return true if we're not currently in a feedback flow, false otherwise.
     */
    @VisibleForTesting
    boolean canStartFeedbackFlow() {
        return activity != null
                && !(activity instanceof FeedbackActivity)
                && activity.getFragmentManager().findFragmentByTag(SEND_FEEDBACK_TAG) == null
                && activity.getFragmentManager().findFragmentByTag(COLLECT_DATA_TAG) == null;
    }

    @Nullable
    @UiThread
    private Bitmap getScreenshotBitmap() {
        View view = activity.getWindow().getDecorView().getRootView();
        return Utils.capture(view);
    }

    private void dismissCollectFeedbackDialogIfNecessary() {
        if (collectDataTask != null || activity == null) {
            return;
        }

        CollectDataDialog dialog = (CollectDataDialog) activity.getFragmentManager()
                                                               .findFragmentByTag(COLLECT_DATA_TAG);

        if (dialog != null) {
            dialog.dismiss();
        }
    }

    /**
     * Receiver for start and end of feedback flow. When the user accepts the
     * dialog, starts the collect data background task.
     */
    @NonNull
    private BroadcastReceiver createReceiver() {
        return new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (SendFeedbackDialog.ACTION_START_FEEDBACK_FLOW.equals(intent.getAction())) {
                    startFeedbackFlow();
                } else if (FeedbackActivity.ACTION_END_FEEDBACK_FLOW.equals(intent.getAction())) {<commit_msg>Fix case where feedback flow does not start after shaking<commit_after>/**
 * Copyright (C) 2016 LinkedIn Corp.
 *
 * 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.linkedin.android.shaky;

import android.app.Activity;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.hardware.SensorManager;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.LocalBroadcastManager;
import android.view.View;
import com.squareup.seismic.ShakeDetector;

import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

/**
 * Listens for a shake and then starts the feedback submission flow.
 * <p/>
 * Note: this class supports attaching and detaching activities for configuration changes, but if
 * the {@link CollectDataTask} completes while there is no activity attached, the feedback
 * flow will be terminated.
 */
public class Shaky implements ShakeDetector.Listener {

    private static final String SEND_FEEDBACK_TAG = "SendFeedback";
    private static final String COLLECT_DATA_TAG = "CollectFeedbackData";

    private static final long SHAKE_COOLDOWN_MS = TimeUnit.SECONDS.toMillis(5);

    private final ShakeDelegate delegate;
    private final ShakeDetector shakeDetector;

    private Activity activity;
    private long lastShakeTime;
    private CollectDataTask collectDataTask;

    Shaky(@NonNull Context context, @NonNull ShakeDelegate delegate) {
        Context appContext = context.getApplicationContext();
        this.delegate = delegate;
        shakeDetector = new ShakeDetector(this);

        IntentFilter filter = new IntentFilter();
        filter.addAction(SendFeedbackDialog.ACTION_START_FEEDBACK_FLOW);
        filter.addAction(FeedbackActivity.ACTION_END_FEEDBACK_FLOW);
        LocalBroadcastManager.getInstance(appContext).registerReceiver(createReceiver(), filter);
    }

    /**
     * Entry point into this API.
     *
     * Registers shaky to the current application.
     */
    @NonNull
    public static Shaky with(@NonNull Application application, @NonNull ShakeDelegate delegate) {
        Shaky shaky = new Shaky(application.getApplicationContext(), delegate);
        LifecycleCallbacks lifecycleCallbacks = new LifecycleCallbacks(shaky);
        application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
        return shaky;
    }

    public void startFeedbackFlow() {
        if (!canStartFeedbackFlow()) {
            return;
        }

        doStartFeedbackFlow();
    }

    void setActivity(@Nullable Activity activity) {
        this.activity = activity;
        if (activity != null) {
            start();
            // we're attaching to a new Activity instance
            // make sure the UI is in sync with the AsyncTask state
            dismissCollectFeedbackDialogIfNecessary();
        } else {
            stop();
        }
    }

    private void doStartFeedbackFlow() {
        new CollectDataDialog().show(activity.getFragmentManager(), COLLECT_DATA_TAG);

        collectDataTask = new CollectDataTask(activity, delegate, createCallback());
        collectDataTask.execute(getScreenshotBitmap());
    }

    /**
     * Start listening for shakes. Should be called when the {@link Activity} is resumed.
     */
    private void start() {
        if (!delegate.isEnabled()) {
            return;
        }

        shakeDetector.start((SensorManager) activity.getSystemService(Context.SENSOR_SERVICE));
    }

    /**
     * Stop listening for shakes. Safe to call when already stopped. Should be called when the
     * {@link Activity} is paused.
     */
    private void stop() {
        shakeDetector.stop();
    }

    @Override
    public void hearShake() {
        if (shouldIgnoreShake() || !canStartFeedbackFlow()) {
            return;
        }

        new SendFeedbackDialog().show(activity.getFragmentManager(), SEND_FEEDBACK_TAG);
    }

    /**
     * @return true if a shake happened in the last {@link #SHAKE_COOLDOWN_MS}, false otherwise.
     */
    @VisibleForTesting
    boolean shouldIgnoreShake() {
        long now = System.currentTimeMillis();
        if (now < lastShakeTime + SHAKE_COOLDOWN_MS) {
            return true;
        }
        lastShakeTime = now;
        return false;
    }

    /**
     * @return true if we're not currently in a feedback flow, false otherwise.
     */
    @VisibleForTesting
    boolean canStartFeedbackFlow() {
        return activity != null
                && !(activity instanceof FeedbackActivity)
                && activity.getFragmentManager().findFragmentByTag(SEND_FEEDBACK_TAG) == null
                && activity.getFragmentManager().findFragmentByTag(COLLECT_DATA_TAG) == null;
    }

    @Nullable
    @UiThread
    private Bitmap getScreenshotBitmap() {
        View view = activity.getWindow().getDecorView().getRootView();
        return Utils.capture(view);
    }

    private void dismissCollectFeedbackDialogIfNecessary() {
        if (collectDataTask != null || activity == null) {
            return;
        }

        CollectDataDialog dialog = (CollectDataDialog) activity.getFragmentManager()
                                                               .findFragmentByTag(COLLECT_DATA_TAG);

        if (dialog != null) {
            dialog.dismiss();
        }
    }

    /**
     * Receiver for start and end of feedback flow. When the user accepts the
     * dialog, starts the collect data background task.
     */
    @NonNull
    private BroadcastReceiver createReceiver() {
        return new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (SendFeedbackDialog.ACTION_START_FEEDBACK_FLOW.equals(intent.getAction())) {
                    if (activity != null) {
                        doStartFeedbackFlow();
                    }
                } else if (FeedbackActivity.ACTION_END_FEEDBACK_FLOW.equals(intent.getAction())) {
{'fraction_non_alphanumeric': 0.08491343775762572, 'fraction_numerical': 0.001498913287866297, 'mean_word_length': 2.8488606864724546, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6015796', 'n_tokens_mistral': 3462, 'n_tokens_neox': 3372, 'n_words': 978}