diff --git a/app/build.gradle b/app/build.gradle index b44501656..5d098e317 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,7 +56,17 @@ android { buildFeatures { viewBinding = true dataBinding = true + compose true } + kotlinOptions { + jvmTarget = '1.8' + } + + composeOptions { + kotlinCompilerVersion kotlin_version + kotlinCompilerExtensionVersion '1.0.1' + } + flavorDimensions 'version' productFlavors { GooglePlay { @@ -75,11 +85,22 @@ dependencies { implementation 'com.google.android.material:material:1.4.0-rc01' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.room:room-runtime:2.3.0' + implementation 'androidx.compose.ui:ui:1.0.1' + // Compose Material Design + implementation 'androidx.compose.material:material:1.0.1' + // Tooling support (Previews, etc.) + implementation 'androidx.compose.ui:ui-tooling:1.0.1' + // Animations + implementation 'androidx.compose.animation:animation:1.0.1' + //Integration with ViewModels + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07' + // When using a AppCompat theme + implementation "com.google.accompanist:accompanist-appcompat-theme:0.16.0" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' annotationProcessor 'androidx.room:room-compiler:2.3.0' - implementation "androidx.core:core-ktx:1.5.0" + implementation "androidx.core:core-ktx:1.6.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.squareup.retrofit2:retrofit:2.9.0' diff --git a/app/src/main/java/com/parishod/watomatic/fragment/MainFragment.java b/app/src/main/java/com/parishod/watomatic/fragment/MainFragment.java index 0a805432b..213879bbb 100644 --- a/app/src/main/java/com/parishod/watomatic/fragment/MainFragment.java +++ b/app/src/main/java/com/parishod/watomatic/fragment/MainFragment.java @@ -34,7 +34,6 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.checkbox.MaterialCheckBox; -import com.google.android.material.switchmaterial.SwitchMaterial; import com.parishod.watomatic.BuildConfig; import com.parishod.watomatic.NotificationService; import com.parishod.watomatic.R; @@ -50,6 +49,7 @@ import com.parishod.watomatic.model.utils.CustomDialog; import com.parishod.watomatic.model.utils.DbUtils; import com.parishod.watomatic.model.utils.ServieUtils; +import com.parishod.watomatic.views.customviews.ComposeSwitchView; import java.util.ArrayList; import java.util.Arrays; @@ -75,7 +75,6 @@ public class MainFragment extends Fragment { TextView autoReplyTextPreview, timeSelectedTextPreview, timePickerSubTitleTextPreview; CustomRepliesData customRepliesData; String autoReplyTextPlaceholder; - SwitchMaterial mainAutoReplySwitch, groupReplySwitch; CardView supportedAppsCard; private PreferencesManager preferencesManager; private int days = 0; @@ -88,6 +87,7 @@ public class MainFragment extends Fragment { private GridLayoutManager layoutManager; private SupportedAppsAdapter supportedAppsAdapter; private List enabledApps = new ArrayList<>(); + private ComposeSwitchView mainAutoReplySwitch, groupReplySwitch; @Nullable @Override @@ -103,7 +103,47 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c // Assign Views mainAutoReplySwitch = view.findViewById(R.id.mainAutoReplySwitch); + mainAutoReplySwitch.setOnClick(() -> { +// Toast.makeText(mActivity, "switch clicked " + !mainAutoReplySwitch1.getChecked(), Toast.LENGTH_LONG).show(); + preferencesManager.setServicePref(!mainAutoReplySwitch.getChecked()); + setSwitchState(); + return null; + }); + + mainAutoReplySwitch.setOnChecked(() -> { +// Toast.makeText(mActivity, "switch clicked " + mainAutoReplySwitch1.getChecked(), Toast.LENGTH_LONG).show(); + preferencesManager.setServicePref(mainAutoReplySwitch.getChecked()); + setSwitchState(); + return null; + }); + groupReplySwitch = view.findViewById(R.id.groupReplySwitch); + groupReplySwitch.setTitleText(getString(R.string.groupReplySwitchLabel)); + groupReplySwitch.setOnClick(() -> { + if(groupReplySwitch.getSwitchEnabled()) { + if (!groupReplySwitch.getChecked()) { + Toast.makeText(mActivity, R.string.group_reply_on_info_message, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(mActivity, R.string.group_reply_off_info_message, Toast.LENGTH_SHORT).show(); + } + preferencesManager.setGroupReplyPref(!groupReplySwitch.getChecked()); + groupReplySwitch.setChecked(!groupReplySwitch.getChecked()); + }else{ + Toast.makeText(mActivity, R.string.group_reply_on_info_message, Toast.LENGTH_SHORT).show(); + } + return null; + }); + + groupReplySwitch.setOnChecked(() -> { + if(groupReplySwitch.getChecked()){ + Toast.makeText(mActivity, R.string.group_reply_on_info_message, Toast.LENGTH_SHORT).show(); + }else{ + Toast.makeText(mActivity, R.string.group_reply_off_info_message, Toast.LENGTH_SHORT).show(); + } + preferencesManager.setGroupReplyPref(groupReplySwitch.getChecked()); + return null; + }); + autoReplyTextPreviewCard = view.findViewById(R.id.mainAutoReplyTextCardView); autoReplyTextPreview = view.findViewById(R.id.textView4); supportedAppsCard = view.findViewById(R.id.supportedAppsSelectorCardView); @@ -131,43 +171,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c autoReplyTextPreviewCard.setOnClickListener(this::openCustomReplyEditorActivity); autoReplyTextPreview.setText(customRepliesData.getTextToSendOrElse(autoReplyTextPlaceholder)); // Enable group chat switch only if main switch id ON - groupReplySwitch.setEnabled(mainAutoReplySwitch.isChecked()); - - mainAutoReplySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - if(isChecked && !isListenerEnabled(mActivity, NotificationService.class)){ -// launchNotificationAccessSettings(); - showPermissionsDialog(); - }else { - preferencesManager.setServicePref(isChecked); - if(isChecked){ - startNotificationService(); - }else{ - stopNotificationService(); - } - mainAutoReplySwitch.setText( - isChecked - ? R.string.mainAutoReplySwitchOnLabel - : R.string.mainAutoReplySwitchOffLabel - ); - - setSwitchState(); - - // Enable group chat switch only if main switch id ON - groupReplySwitch.setEnabled(isChecked); - } - }); - - groupReplySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - // Ignore if this is not triggered by user action but just UI update in onResume() #62 - if (preferencesManager.isGroupReplyEnabled() == isChecked) { return;} - - if(isChecked){ - Toast.makeText(mActivity, R.string.group_reply_on_info_message, Toast.LENGTH_SHORT).show(); - }else{ - Toast.makeText(mActivity, R.string.group_reply_off_info_message, Toast.LENGTH_SHORT).show(); - } - preferencesManager.setGroupReplyPref(isChecked); - }); + groupReplySwitch.setSwitchEnabled(mainAutoReplySwitch.getChecked()); imgMinus.setOnClickListener(v -> { if(days > MIN_DAYS){ @@ -244,6 +248,7 @@ public void onResume() { preferencesManager.setServicePref(false); } + mainAutoReplySwitch.setChecked(preferencesManager.isServiceEnabled()); /*if(!preferencesManager.isServiceEnabled()){ enableService(false); }*/ @@ -419,9 +424,10 @@ private Intent rateIntentForUrl(String url) } private void setSwitchState(){ + mainAutoReplySwitch.setTitleText(preferencesManager.isServiceEnabled() ? getString(R.string.mainAutoReplySwitchOnLabel) : getString(R.string.mainAutoReplySwitchOffLabel)); mainAutoReplySwitch.setChecked(preferencesManager.isServiceEnabled()); - groupReplySwitch.setEnabled(preferencesManager.isServiceEnabled()); - enableOrDisableEnabledAppsCheckboxes(mainAutoReplySwitch.isChecked()); + groupReplySwitch.setSwitchEnabled(preferencesManager.isServiceEnabled()); + enableOrDisableEnabledAppsCheckboxes(mainAutoReplySwitch.getChecked()); } //https://stackoverflow.com/questions/20141727/check-if-user-has-granted-notificationlistener-access-to-my-app/28160115 diff --git a/app/src/main/java/com/parishod/watomatic/views/customviews/ComposeSwitchView.kt b/app/src/main/java/com/parishod/watomatic/views/customviews/ComposeSwitchView.kt new file mode 100644 index 000000000..0112328e8 --- /dev/null +++ b/app/src/main/java/com/parishod/watomatic/views/customviews/ComposeSwitchView.kt @@ -0,0 +1,113 @@ +package com.parishod.watomatic.views.customviews + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.accompanist.appcompattheme.AppCompatTheme + +//REF: https://compose.academy/blog/migrating_to_compose_-_composeview +class ComposeSwitchView@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AbstractComposeView(context, attrs, defStyleAttr) { + + + // Create a State for the title text so any changes can be observed and reflected automatically + // in our Composable Text. + var titleText by mutableStateOf("") + var onClick by mutableStateOf({}) + var onChecked by mutableStateOf({}) + var checked by mutableStateOf(false) + var switchEnabled by mutableStateOf(true) + + @ExperimentalMaterialApi + @Composable + override fun Content() { + AppCompatTheme{ + MaterialSwitchComponent(onClick) + } + } + + // We represent a Composable function by annotating it with the @Composable annotation. Composable +// functions can only be called from within the scope of other composable functions. We should +// think of composable functions to be similar to lego blocks - each composable function is in turn +// built up of smaller composable functions. + @ExperimentalMaterialApi + @SuppressLint("UnrememberedMutableState") + @Composable + fun MaterialSwitchComponent( + onClick: () -> Unit, + ) { + // Reacting to state changes is the core behavior of Compose. You will notice a couple new + // keywords that are compose related - remember & mutableStateOf.remember{} is a helper + // composable that calculates the value passed to it only during the first composition. It then + // returns the same value for every subsequent composition. Next, you can think of + // mutableStateOf as an observable value where updates to this variable will redraw all + // the composable functions that access it. We don't need to explicitly subscribe at all. Any + // composable that reads its value will be recomposed any time the value + // changes. This ensures that only the composables that depend on this will be redraw while the + // rest remain unchanged. This ensures efficiency and is a performance optimization. It + // is inspired from existing frameworks like React. + //var checked by remember { mutableStateOf(false) } + + // Card composable is a predefined composable that is meant to represent the card surface as + // specified by the Material Design specification. We also configure it to have rounded + // corners and apply a modifier. + + // You can think of Modifiers as implementations of the decorators pattern that are used to + // modify the composable that its applied to. In the example below, we add a padding of + // 8dp to the Card composable. In addition, we configure it out occupy the entire available + // width using the Modifier.fillMaxWidth() modifier. + Card( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + border = BorderStroke(1.5.dp, + when{ + !switchEnabled -> Color(192, 192, 192) + checked -> MaterialTheme.colors.secondaryVariant + else -> Color(255, 69, 0) + }), + shape = RoundedCornerShape(5.dp), + onClick = onClick, + backgroundColor = Color(255, 255, 255) + ) { + // Row is a composable that places its children in a horizontal sequence. You can think of it + // similar to a LinearLayout with the horizontal orientation. In addition, we pass a modifier + // to the Row composable. + Row(modifier = Modifier.padding(16.dp), Arrangement.SpaceBetween) { + // The Text composable is pre-defined by the Compose UI library; you can use this + // composable to render text on the screen + Text(text = titleText, modifier = Modifier.padding(8.dp)) + // A pre-defined composable that's capable of rendering a switch. It honors the Material + // Design specification. + Switch( + modifier = Modifier + .padding(8.dp), + enabled = switchEnabled, + checked = checked, + onCheckedChange = { + checked = !checked + onChecked.invoke() + }) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index b7b1229cd..c5f043712 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -32,22 +32,16 @@ app:layout_constraintTop_toBottomOf="@+id/textView" /> - - - - + android:padding="2dp" + tools:layout_editor_absoluteX="154dp"/> - - - - + tools:layout_editor_absoluteX="154dp"/>