diff --git a/shaky-sample/src/main/java/com/linkedin/android/shaky/app/ShakyApplication.java b/shaky-sample/src/main/java/com/linkedin/android/shaky/app/ShakyApplication.java index 350b282..6ef34f7 100644 --- a/shaky-sample/src/main/java/com/linkedin/android/shaky/app/ShakyApplication.java +++ b/shaky-sample/src/main/java/com/linkedin/android/shaky/app/ShakyApplication.java @@ -52,6 +52,12 @@ public Integer getPopupTheme() { return popupTheme; } + @Nullable + @Override + public String getSelectScreenSubtitle() { + return "Did you encounter a problem? You can turn on \"Shake to send feedback\" " + + "below and shake your device where the problem was to take a screenshot"; + } }, new ShakyFlowCallback() { @Override public void onShakyStarted(@ShakyFlowCallback.ShakyStartedReason int reason) { diff --git a/shaky/src/main/java/com/linkedin/android/shaky/FeedbackActivity.java b/shaky/src/main/java/com/linkedin/android/shaky/FeedbackActivity.java index d877244..df37550 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/FeedbackActivity.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/FeedbackActivity.java @@ -46,6 +46,10 @@ public class FeedbackActivity extends AppCompatActivity { static final String RES_MENU = "resMenu"; static final String SUBCATEGORY = "subcategory"; static final String THEME = "theme"; + static final String SELECT_SCREEN_TITLE = "selectScreenTitle"; + static final String SELECT_SCREEN_SUBTITLE = "selectScreenSubtitle"; + static final String SHAKE_TURNED_ON = "shakeTurnedOn"; + static final String SHAKE_ENABLED = "shakeEnabled"; static final int MISSING_RESOURCE = 0; private Uri imageUri; @@ -59,12 +63,20 @@ public static Intent newIntent(@NonNull Context context, @Nullable Uri screenshotUri, @Nullable Bundle userData, @MenuRes int resMenu, - @StyleRes int theme) { + @StyleRes int theme, + @Nullable String selectScreenTitle, + @Nullable String selectScreenSubtitle, + boolean shakeTurnedOn, + boolean shakeEnabled) { Intent intent = new Intent(context, FeedbackActivity.class); intent.putExtra(SCREENSHOT_URI, screenshotUri); intent.putExtra(USER_DATA, userData); intent.putExtra(RES_MENU, resMenu); intent.putExtra(THEME, theme); + intent.putExtra(SELECT_SCREEN_TITLE, selectScreenTitle); + intent.putExtra(SELECT_SCREEN_SUBTITLE, selectScreenSubtitle); + intent.putExtra(SHAKE_TURNED_ON, shakeTurnedOn); + intent.putExtra(SHAKE_ENABLED, shakeEnabled); return intent; } @@ -84,7 +96,13 @@ public void onCreate(Bundle savedInstanceState) { if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() - .add(R.id.shaky_fragment_container, SelectFragment.newInstance(customTheme)) + .add(R.id.shaky_fragment_container, + SelectFragment.newInstance( + customTheme, + getIntent().getStringExtra(SELECT_SCREEN_TITLE), + getIntent().getStringExtra(SELECT_SCREEN_SUBTITLE), + getIntent().getBooleanExtra(SHAKE_TURNED_ON, false), + getIntent().getBooleanExtra(SHAKE_ENABLED, false))) .commit(); } } diff --git a/shaky/src/main/java/com/linkedin/android/shaky/SelectFragment.java b/shaky/src/main/java/com/linkedin/android/shaky/SelectFragment.java index 8c25585..a79a5e9 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/SelectFragment.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/SelectFragment.java @@ -15,11 +15,14 @@ */ package com.linkedin.android.shaky; +import android.content.Intent; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; import androidx.fragment.app.Fragment; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.appcompat.widget.Toolbar; @@ -27,23 +30,41 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.Switch; +import android.widget.TextView; /** * Kicks off the feedback flow, pretty selector to choose the type of feedback. */ public class SelectFragment extends Fragment { + public static final String UPDATE_SHAKE_DETECTION_STATUS = "UpdateShakeDetectionStatus"; + public static final String SHAKE_TURNED_ON = "ShakeTurnedOn"; + private static final String KEY_THEME = "theme"; + private static final String KEY_TITLE = "title"; + private static final String KEY_SUBTITLE = "subtitle"; + private static final String KEY_SHAKE_TURNED_ON = "shakeTurnedOn"; + private static final String KEY_SHAKE_ENABLED = "shakeEnabled"; @Nullable private LayoutInflater inflater; @NonNull - static SelectFragment newInstance(@Nullable @StyleRes Integer theme) { + static SelectFragment newInstance(@Nullable @StyleRes Integer theme, + @Nullable String title, + @Nullable String subtitle, + boolean shakeTurnedOn, + boolean shakeEnabled) { SelectFragment fragment = new SelectFragment(); Bundle args = new Bundle(); if (theme != null) { args.putInt(KEY_THEME, theme); } + args.putString(KEY_TITLE, title); + args.putString(KEY_SUBTITLE, subtitle); + args.putBoolean(KEY_SHAKE_TURNED_ON, shakeTurnedOn); + args.putBoolean(KEY_SHAKE_ENABLED, shakeEnabled); fragment.setArguments(args); return fragment; } @@ -65,6 +86,8 @@ public void onViewCreated(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.shaky_recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); recyclerView.setAdapter(adapter); + recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), + DividerItemDecoration.VERTICAL)); Toolbar toolbar = (Toolbar) view.findViewById(R.id.shaky_toolbar); toolbar.setTitle(R.string.shaky_feedback_title); @@ -75,6 +98,34 @@ public void onClick(View v) { getActivity().onBackPressed(); } }); + + TextView title = view.findViewById(R.id.shaky_select_title); + String customTitle = getArguments().getString(KEY_TITLE); + if (customTitle != null) { + title.setText(customTitle); + } + + TextView subtitle = view.findViewById(R.id.shaky_select_subtitle); + String customSubtitle = getArguments().getString(KEY_SUBTITLE); + if (customSubtitle != null) { + subtitle.setText(customSubtitle); + subtitle.setVisibility(View.VISIBLE); + } + + Switch shakeToggle = view.findViewById(R.id.shaky_select_shake_switch); + shakeToggle.setChecked(getArguments().getBoolean(KEY_SHAKE_TURNED_ON, false)); + if (getArguments().getBoolean(KEY_SHAKE_ENABLED)) { + shakeToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Intent intent = new Intent(UPDATE_SHAKE_DETECTION_STATUS); + intent.putExtra(SHAKE_TURNED_ON, isChecked); + LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); + } + }); + } else { + shakeToggle.setVisibility(View.GONE); + } } @NonNull diff --git a/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java b/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java index 35d17f8..433c07c 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/ShakeDelegate.java @@ -53,7 +53,8 @@ public abstract class ShakeDelegate { @MenuRes protected int resMenu = FormFragment.DEFAULT_MENU; /** - * @return true if shake detection should be enabled, false otherwise + * @return true if shake detection should be enabled, false otherwise. If enabled, the user can + * still turn off shaking through the UI. If disabled, UI to toggle shaking won't be shown. */ public boolean isEnabled() { return true; @@ -122,6 +123,22 @@ public Integer getPopupTheme() { return null; } + /** + * @return the title of the category select screen, or null if the default title should be shown + */ + @Nullable + public String getSelectScreenTitle() { + return null; + } + + /** + * @return the subtitle of the category select screen, or null if no subtitle should be shown + */ + @Nullable + public String getSelectScreenSubtitle() { + 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 diff --git a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java index d2d9440..ce8157e 100644 --- a/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java +++ b/shaky/src/main/java/com/linkedin/android/shaky/Shaky.java @@ -27,6 +27,7 @@ import android.os.Bundle; import android.view.View; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; @@ -36,6 +37,8 @@ import com.jraska.falcon.Falcon; import com.squareup.seismic.ShakeDetector; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.concurrent.TimeUnit; @@ -48,6 +51,16 @@ */ public class Shaky implements ShakeDetector.Listener { + public static final int SHAKE_ON = 25; + public static final int SHAKE_OFF = 26; + + @IntDef({ + SHAKE_ON, + SHAKE_OFF + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ShakeDetectionStatus {} + private static final String SEND_FEEDBACK_TAG = "SendFeedback"; private static final String COLLECT_DATA_TAG = "CollectFeedbackData"; @@ -62,6 +75,7 @@ public class Shaky implements ShakeDetector.Listener { private Activity activity; private Context appContext; private long lastShakeTime; + private boolean shakeTurnedOn = true; private CollectDataTask collectDataTask; Shaky(@NonNull Context context, @NonNull ShakeDelegate delegate, @Nullable ShakyFlowCallback callback) { @@ -78,6 +92,7 @@ public class Shaky implements ShakeDetector.Listener { filter.addAction(FeedbackActivity.ACTION_END_FEEDBACK_FLOW); filter.addAction(FeedbackActivity.ACTION_ACTIVITY_CLOSED_BY_USER); filter.addAction(ShakySettingDialog.UPDATE_SHAKY_SENSITIVITY); + filter.addAction(SelectFragment.UPDATE_SHAKE_DETECTION_STATUS); LocalBroadcastManager.getInstance(appContext).registerReceiver(createReceiver(), filter); } @@ -122,6 +137,10 @@ public void setSensitivity(@ShakeDelegate.SensitivityLevel int sensitivityLevel) shakeDetector.setSensitivity(getDetectorSensitivityLevel()); } + public void setShakeStatus(@ShakeDetectionStatus int status) { + shakeTurnedOn = status == SHAKE_ON; + } + void setActivity(@Nullable Activity activity) { this.activity = activity; if (activity != null) { @@ -193,10 +212,14 @@ public void hearShake() { } /** - * @return true if a shake happened in the last {@link #SHAKE_COOLDOWN_MS}, false otherwise. + * @return true if a shake happened in the last {@link #SHAKE_COOLDOWN_MS} or shake detection + * is disabled, or false otherwise */ @VisibleForTesting boolean shouldIgnoreShake() { + if (!shakeTurnedOn) { + return true; + } long now = System.currentTimeMillis(); if (now < lastShakeTime + SHAKE_COOLDOWN_MS) { if (shakyFlowCallback != null) { @@ -285,6 +308,8 @@ public void onReceive(Context context, Intent intent) { if (shakyFlowCallback != null) { shakyFlowCallback.onShakyFinished(ShakyFlowCallback.SHAKY_FINISHED_SENSITIVITY_UPDATED); } + } else if (SelectFragment.UPDATE_SHAKE_DETECTION_STATUS.equals(intent.getAction())) { + shakeTurnedOn = intent.getBooleanExtra(SelectFragment.SHAKE_TURNED_ON, false); } } }; @@ -322,7 +347,9 @@ private void startFeedbackActivity(@NonNull Result result) { result.getScreenshotUri(), result.getData(), delegate.resMenu, - delegate.getTheme() != null ? delegate.getTheme() : FeedbackActivity.MISSING_RESOURCE); + delegate.getTheme() != null ? delegate.getTheme() : FeedbackActivity.MISSING_RESOURCE, + delegate.getSelectScreenTitle(), + delegate.getSelectScreenSubtitle(), true, true); activity.startActivity(intent); if (shakyFlowCallback != null) { diff --git a/shaky/src/main/res/layout/shaky_select.xml b/shaky/src/main/res/layout/shaky_select.xml index 91d23a9..0232862 100644 --- a/shaky/src/main/res/layout/shaky_select.xml +++ b/shaky/src/main/res/layout/shaky_select.xml @@ -27,6 +27,51 @@ android:layout_width="match_parent" android:layout_height="wrap_content"/> + + + + + + + + + \ No newline at end of file diff --git a/shaky/src/main/res/layout/shaky_single_row.xml b/shaky/src/main/res/layout/shaky_single_row.xml index 8dda99a..526a0b9 100644 --- a/shaky/src/main/res/layout/shaky_single_row.xml +++ b/shaky/src/main/res/layout/shaky_single_row.xml @@ -22,14 +22,14 @@ android:focusable="true" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" - android:orientation="horizontal" - android:padding="@dimen/shaky_button_padding"> + android:orientation="horizontal"> + + + diff --git a/shaky/src/main/res/values/shaky_dimens.xml b/shaky/src/main/res/values/shaky_dimens.xml index e1f36dd..e99acf9 100644 --- a/shaky/src/main/res/values/shaky_dimens.xml +++ b/shaky/src/main/res/values/shaky_dimens.xml @@ -21,13 +21,14 @@ 8dp 8dp 16dp - 8dp + 12dp + 16dp 20sp 16sp 18sp - 2sp + 1dp 200dp diff --git a/shaky/src/main/res/values/shaky_styles.xml b/shaky/src/main/res/values/shaky_styles.xml index 4ec9669..c287c6d 100644 --- a/shaky/src/main/res/values/shaky_styles.xml +++ b/shaky/src/main/res/values/shaky_styles.xml @@ -51,8 +51,11 @@ ?attr/shakyBackgroundColor + ?attr/shakyTitleColor + ?attr/shakyContentColor ?attr/shakyTitleColor ?attr/shakyContentColor + ?attr/shakyCategoryListItemSubtitleColor @color/shaky_gray