<|start_filename|>shaky/src/main/java/com/linkedin/android/shaky/ShakySettingDialog.java<|end_filename|>
package com.linkedin.android.shaky;

import androidx.appcompat.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

public class ShakySettingDialog extends DialogFragment {

    public static final String UPDATE_SHAKY_SENSITIVITY = "UpdateShakySensitivity";
    public static final String SHAKY_SETTING_DIALOG = "ShakySettingDialog";
    public static final String SHAKY_NEW_SENSITIVITY = "ShakySensitivityLevel";
    public static final String SHAKY_CURRENT_SENSITIVITY = "ShakyCurrentSensitivity";

    private String[] sensitivityOptions;
    private @ShakeDelegate.SensitivityLevel int currentSensitivityLevel;

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        sensitivityOptions = new String[] {getResources().getString(R.string.shaky_sensitivity_high),
                getResources().getString(R.string.shaky_sensitivity_medium),
                getResources().getString(R.string.shaky_sensitivity_low)};

        currentSensitivityLevel = getArguments().getInt(SHAKY_CURRENT_SENSITIVITY);

        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

        builder.setTitle(getResources().getString(R.string.shaky_sensor_sensitivity));
        final Intent intent = new Intent(UPDATE_SHAKY_SENSITIVITY);
        builder.setSingleChoiceItems(sensitivityOptions, getIndexFromCurrentSensitivityLevel(), new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                switch (which) {
                    case 0:
                        intent.putExtra(SHAKY_NEW_SENSITIVITY, ShakeDelegate.SENSITIVITY_HARD);
                        break;
                    case 2:
                        intent.putExtra(SHAKY_NEW_SENSITIVITY, ShakeDelegate.SENSITIVITY_LIGHT);
                        break;
                    default:
                        intent.putExtra(SHAKY_NEW_SENSITIVITY, ShakeDelegate.SENSITIVITY_MEDIUM);
                        break;
                }
            }
        });

        builder.setPositiveButton(getResources().getString(R.string.shaky_general_ok_string), new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent);
                dialog.dismiss();
            }
        });

        return builder.create();
    }

    private int getIndexFromCurrentSensitivityLevel() {
        if (currentSensitivityLevel == ShakeDelegate.SENSITIVITY_HARD) {
            return 0;
        } else if (currentSensitivityLevel == ShakeDelegate.SENSITIVITY_LIGHT) {
            return 2;
        } else {
            return 1;
        }
    }
}


<|start_filename|>shaky/src/test/java/com/linkedin/android/shaky/ShakyShould.java<|end_filename|>
/**
 * 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.FragmentManager;
import android.os.Looper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import com.squareup.seismic.ShakeDetector;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Behavior tests for the main library class file, ShakeToFeedback.
 */
@RunWith(RobolectricTestRunner.class)
public class ShakyShould {

    Shaky shaky;
    ShakeDelegate delegate;
    Activity activity;
    FragmentManager fragmentManager;

    @Before
    public void setUp() {
        delegate = mock(ShakeDelegate.class);
        activity = mock(Activity.class);
        fragmentManager = mock(FragmentManager.class);

        when(activity.getFragmentManager()).thenReturn(fragmentManager);
        when(activity.getApplicationContext()).thenReturn(activity);
        when(activity.getMainLooper()).thenReturn(Looper.getMainLooper());
        when(delegate.getSensitivityLevel()).thenReturn(ShakeDelegate.SENSITIVITY_LIGHT);

        shaky = new Shaky(activity, delegate, null);
        shaky.setActivity(activity);
    }

    @Test
    public void coolDownBetweenShakes() {
        assertFalse(shaky.shouldIgnoreShake());
        assertTrue(shaky.shouldIgnoreShake());
    }

    @Test
    public void notStartFeedbackFlowWithNoActivityAttached() {
        shaky.setActivity(null);
        assertFalse(shaky.canStartFeedbackFlow());
    }

    @Test
    public void notStartFeedbackFlowWhileInFeedbackActivity() {
        FeedbackActivity feedbackActivity = new FeedbackActivity();
        shaky.setActivity(feedbackActivity);
        assertFalse(shaky.canStartFeedbackFlow());
    }

    @Test
    public void respectDelegateSensitivityLevel() {
        assertEquals(shaky.getDetectorSensitivityLevel(), ShakeDetector.SENSITIVITY_LIGHT);
    }
}


<|start_filename|>shaky/src/main/java/com/linkedin/android/shaky/FeedbackTypeAdapter.java<|end_filename|>
/**
 * 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.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * RecyclerView.Adapter for the select type of feedback view.
 */
class FeedbackTypeAdapter extends RecyclerView.Adapter<FeedbackTypeAdapter.RowViewHolder> {

    public static final String ACTION_FEEDBACK_TYPE_SELECTED = "ActionFeedbackTypeSelected";
    public static final String EXTRA_FEEDBACK_TYPE = "ExtraFeedbackType";

    private final LayoutInflater inflater;
    private final FeedbackItem[] itemsList;

    FeedbackTypeAdapter(@NonNull LayoutInflater inflater, @NonNull FeedbackItem[] itemsList) {
        this.inflater = inflater;
        this.itemsList = itemsList;
    }

    @Override
    public int getItemCount() {
        return itemsList == null ? 0 : itemsList.length;
    }

    @Override
    public RowViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) {
        return new RowViewHolder(inflater.inflate(R.layout.shaky_single_row, viewGroup, false));
    }

    @Override
    public void onBindViewHolder(RowViewHolder rowViewHolder, int position) {
        final FeedbackItem item = itemsList[position];

        rowViewHolder.titleView.setText(String.valueOf(item.title));
        rowViewHolder.descriptionView.setText(String.valueOf(item.description));
        rowViewHolder.imageView.setImageResource(item.icon);
        rowViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(ACTION_FEEDBACK_TYPE_SELECTED);
                intent.putExtra(EXTRA_FEEDBACK_TYPE, item.feedbackType);
                LocalBroadcastManager.getInstance(v.getContext()).sendBroadcast(intent);
            }
        });
    }

    static class RowViewHolder extends RecyclerView.ViewHolder {
        public final ImageView imageView;
        public final TextView titleView;
        public final TextView descriptionView;

        RowViewHolder(@NonNull View itemView) {
            super(itemView);
            this.imageView = (ImageView) itemView.findViewById(R.id.shaky_row_icon);
            this.titleView = (TextView) itemView.findViewById(R.id.shaky_row_title);
            this.descriptionView = (TextView) itemView.findViewById(R.id.shaky_row_description);
        }
    }
}


<|start_filename|>shaky/src/main/java/com/linkedin/android/shaky/Utils.java<|end_filename|>
/**
 * 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.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.annotation.WorkerThread;
import androidx.core.content.FileProvider;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Locale;

final class Utils {
    private static final String TAG = Utils.class.getSimpleName();
    private static final String FILE_NAME_TEMPLATE = "%s_%s.jpg";
    private static final String BITMAP_PREFIX = "bitmap";
    private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";

    // prevent instantiation
    private Utils() {}

    /**
     * Create a unique file name starting with the prefix.
     */
    @NonNull
    static String createUniqueFilename(String prefix) {
        String randomId = Long.toString(System.currentTimeMillis());
        return String.format(Locale.US, FILE_NAME_TEMPLATE, prefix, randomId);
    }

    /**
     * Writes the bitmap the directory, creating the directory if it doesn't exist.
     */
    @Nullable
    @WorkerThread
    static File writeBitmapToDirectory(@NonNull Bitmap bitmap, @NonNull File directory) {
        if (!directory.mkdirs() && !directory.exists()) {
            Log.e(TAG, "Failed to create directory for bitmap.");
            return null;
        }
        return writeBitmapToFile(bitmap, new File(directory, createUniqueFilename(BITMAP_PREFIX)));
    }

    /**
     * Writes the bitmap to disk and returns the new file.
     *
     * @param bitmap Bitmap the bitmap to write
     * @param file   the file to write to
     */
    @Nullable
    @WorkerThread
    // suppress lint check for AGP 3.2 https://issuetracker.google.com/issues/116776070
    @SuppressLint("WrongThread")
    static File writeBitmapToFile(@NonNull Bitmap bitmap, @NonNull File file) {
        FileOutputStream fileStream = null;
        try {
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteStream);
            fileStream = new FileOutputStream(file);
            fileStream.write(byteStream.toByteArray());
            return file;
        } catch (IOException e) {
            Log.e(TAG, e.getMessage(), e);
        } finally {
            if (fileStream != null) {
                try {
                    fileStream.close();
                } catch (IOException e) {
                    Log.e(TAG, e.getMessage(), e);
                }
            }
        }
        return null;
    }

    /**
     * Saves the view as a Bitmap screenshot.
     */
    @Nullable
    static Bitmap capture(View view) {
        if (view.getWidth() == 0 || view.getHeight() == 0) {
            return null;
        }

        Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.RGB_565);
        Canvas canvas = new Canvas(bitmap);
        view.draw(canvas);
        return bitmap;
    }

    /**
     * Get the file provider Uri, so that internal files can be temporarily shared with other apps.
     *
     * Requires AndroidManifest permission: android.support.v4.content.FileProvider
     */
    @NonNull
    static Uri getProviderUri(@NonNull Context context, @NonNull File file) {
        String authority = context.getPackageName() + FILE_PROVIDER_SUFFIX;
        return FileProvider.getUriForFile(context, authority, file);
    }

    /**
     * Get the file provider Uri, so that internal files can be temporarily shared with other apps.
     *
     * Requires AndroidManifest permission: android.support.v4.content.FileProvider
     */
    @NonNull
    static Uri getProviderUri(@NonNull Context context, @NonNull Uri uri) {
        File file = new File(uri.getPath());
        return getProviderUri(context, file);
    }

    /**
     * Return a new {@link android.view.LayoutInflater} that uses the given theme if the theme is valid,
     * or the given inflater otherwise
     */
    @NonNull
    static LayoutInflater applyThemeToInflater(@NonNull LayoutInflater inflater, @StyleRes int theme) {
        if (theme != FeedbackActivity.MISSING_RESOURCE) {
            return inflater.cloneInContext(new ContextThemeWrapper(inflater.getContext(), theme));
        }
        return inflater;
    }
}


<|start_filename|>shaky/src/main/java/com/linkedin/android/shaky/ShakyFlowCallback.java<|end_filename|>
/*
 * Copyright (C) 2020 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 androidx.annotation.IntDef;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Callbacks during the feedback submission flow.
 */
public interface ShakyFlowCallback {
    @IntDef({SHAKY_STARTED_BY_SHAKE, SHAKY_STARTED_MANUALLY})
    @Retention(RetentionPolicy.SOURCE)
    @interface ShakyStartedReason {
    }

    /**
     * The flow is started, because the device is shaken.
     */
    int SHAKY_STARTED_BY_SHAKE = 1;
    /**
     * The flow is started, because {@link Shaky#startFeedbackFlow()} is called.
     */
    int SHAKY_STARTED_MANUALLY = 2;

    @IntDef({
            SHAKY_FINISHED_TOO_FREQUENT,
            SHAKY_FINISHED_NO_RESUMED_ACTIVITY,
            SHAKY_FINISHED_ALREADY_STARTED,
            SHAKY_FINISHED_BY_USER,
            SHAKY_FINISHED_SENSITIVITY_UPDATED,
            SHAKY_FINISHED_SUBMITTED
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface ShakyFinishedReason {
    }

    /**
     * The flow is finished, because the previous shake was just recently detected.
     */
    int SHAKY_FINISHED_TOO_FREQUENT = 1;
    /**
     * The flow is finished, because there is no {@link Activity} resumed.
     */
    int SHAKY_FINISHED_NO_RESUMED_ACTIVITY = 2;
    /**
     * The flow is finished, because the flow is already started.
     */
    int SHAKY_FINISHED_ALREADY_STARTED = 3;
    /**
     * The flow is finished, because the user dismissed the dialog.
     */
    int SHAKY_FINISHED_BY_USER = 4;
    /**
     * The flow is finished, because the user updated Shaky sensitivity.
     */
    int SHAKY_FINISHED_SENSITIVITY_UPDATED = 5;
    /**
     * The flow is finished, and user has submitted the feedback through {@link ShakeDelegate#submit(Activity, Result)}.
     */
    int SHAKY_FINISHED_SUBMITTED = 6;

    /**
     * Called when the flow is started.
     */
    void onShakyStarted(@ShakyStartedReason int reason);

    /**
     * Called when the flow is finished.
     */
    void onShakyFinished(@ShakyFinishedReason int reason);

    /**
     * Called when the dialog that prompts the user to kick-off the feedback flow is shown.
     */
    void onUserPromptShown();

    /**
     * Called when Shaky starts collecting data.
     */
    void onCollectingData();

    /**
     * Called when {@link FeedbackActivity} is launched and the user is configuring the feedback.
     */
    void onConfiguringFeedback();
}


<|start_filename|>shaky/src/main/java/com/linkedin/android/shaky/FeedbackItem.java<|end_filename|>
/**
 * 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 androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Data wrapper for a single row in the select feedback type view.
 */
class FeedbackItem {
    @IntDef({
            BUG,
            FEATURE,
            GENERAL
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface FeedbackType {}

    public static final int BUG = 0;
    public static final int FEATURE = 1;
    public static final int GENERAL = 2;

    @DrawableRes public final int icon;
    public final String title;
    public final String description;
    public final int feedbackType;

    FeedbackItem(@NonNull String title, @NonNull String description, @DrawableRes int icon,
                        @FeedbackType int feedbackType) {
        this.title = title;
        this.description = description;
        this.icon = icon;
        this.feedbackType = feedbackType;
    }
}


<|start_filename|>shaky/src/main/java/com/linkedin/android/shaky/Subcategories.java<|end_filename|>
package com.linkedin.android.shaky;

/**
 * Constants representing subcategories for different types of feedback
 */
public class Subcategories {
  public class Bug {
    public static final String CRASH = "CRASH";
    public static final String NON_FATAL = "NON_FATAL";
  }
}


<|start_filename|>shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java<|end_filename|>
/**
 * 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 androidx.annotation.IntDef;
import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.annotation.WorkerThread;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Entry point into this API.
 *
 * This class contains methods that apps can override to customize the behavior of Shaky.
 */
public abstract class ShakeDelegate {
    @IntDef({
            SENSITIVITY_LIGHT,
            SENSITIVITY_MEDIUM,
            SENSITIVITY_HARD
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface SensitivityLevel {}

    public static final int SENSITIVITY_LIGHT = 22;
    public static final int SENSITIVITY_MEDIUM = 23;
    public static final int SENSITIVITY_HARD = 24;

    @SensitivityLevel
    private static int sensitivityLevel = SENSITIVITY_MEDIUM;
    /** Allows user to customize the send icon at the end. Needs to be the menu itself because you cannot
     * change the send icon unless you send in a new menu, Also needs a value or else it crashes
     */
    @MenuRes protected int resMenu = FormFragment.DEFAULT_MENU;

    /**
     * @return true if shake detection should be enabled, false otherwise
     */
    public boolean isEnabled() {
        return true;
    }

    /**
     * @return true if a button to access Shaky settings should be shown in the UI, false otherwise
     */
    public boolean shouldShowSettingsUI() {
        return false;
    }

    /**
     * @return the title of the dialog that appears on shake, or null if the default title should be
     * used
     */
    @Nullable
    public String getDialogTitle() {
        return null;
    }

    /**
     * @return the message of the dialog that appears on shake, or null if the default message should
     * be used
     */
    @Nullable
    public String getDialogMessage() {
        return null;
    }

    /**
     * @return desired sensitivity level, defaults to 'medium'
     */
    @SensitivityLevel
    public int getSensitivityLevel() {
        return sensitivityLevel;
    }

    /**
     * Sets the shake sensitivity in memory. Please note that it is caller's responsibility to
     * persist the setting in storage such as {@link android.content.SharedPreferences}.
     *
     * @param sensitivityLevel
     */
    public void setSensitivityLevel(@SensitivityLevel int sensitivityLevel) {
        this.sensitivityLevel = sensitivityLevel;
    }

    /**
     * @return a custom theme to apply to the screens of the feedback flow. Look at shaky_attrs.xml
     * for possible attributes to set
     *
     * @return
     */
    @Nullable
    public Integer getTheme() {
        return null;
    }

    /**
     * @return a custom theme to apply to the pop-up that appears when the user shakes. Look at
     * shaky_attrs.xml for possible attributes to set
     */
    @Nullable
    public Integer getPopupTheme() {
        return null;
    }

    /**
     * Called from the background thread during the feedback collection flow. This method
     * can be used to collect extra debug information to include in the feedback
     * submission, such as user data, app version, etc.
     */
    @WorkerThread
    public void collectData(@NonNull Activity activity, @NonNull Result data) {
    }

    /**
     * Called when the user submits the Feedback form. Creates and starts an email Intent.
     * This method can be overridden to send data to a custom URL endpoint, etc.
     */
    public abstract void submit(@NonNull Activity activity, @NonNull Result result);
}
