From 15ad6856a20c82cc541ce76f6e81bf1aa71a1719 Mon Sep 17 00:00:00 2001
From: DK <117024745+Dhangofa@users.noreply.github.com>
Date: Wed, 19 Aug 2026 02:16:16 +0530
Subject: [PATCH 1/7] Replace ViewPager2 with Morphing View and refine touch
feedback
- Removed ViewPager2 & TabLayout: Completely excised the heavyweight androidx.viewpager2.widget.ViewPager2 and TabLayout nodes from the layout hierarchy, significantly reducing structural depth and UI bloat.
- Implemented Native Morphing Container: Introduced a custom, native LinearLayout structure with internal TextView and ImageView components (carouselTitle, carouselDesc, carouselButton, carouselIcon) to act as a unified, single-layer morphing canvas.
- Added Custom Pill Ripple Masking: Added android:foreground="@drawable/ripple_pill_mask" to the Carousel "Read Guide" button to ensure its dynamically tinted click feedback perfectly respects the 20dp pill-shaped bounding box without square edge bleeding.
- Restored Native Social Ripples: Removed experimental foreground overlays from the Footer Social Links (GitHub, Telegram, FAQ), allowing them to correctly utilize their native 12dp rounded background ripples.
---
app/src/main/res/layout/activity_main.xml | 96 +++++++++++++++++++++--
1 file changed, 91 insertions(+), 5 deletions(-)
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index d65a5c1..8944e11 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -99,7 +99,7 @@
android:textColor="@color/text_secondary"
android:textSize="13sp" />
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From a23660ac1fb956ae2eead759325f1d6a8fb35f19 Mon Sep 17 00:00:00 2001
From: DK <117024745+Dhangofa@users.noreply.github.com>
Date: Wed, 19 Aug 2026 02:17:51 +0530
Subject: [PATCH 2/7] Implement Morphing View and refine touch feedback in
landscape
- Removed ViewPager2 & TabLayout: Deleted the ViewPager2 and TabLayout definitions from the landscape layout configuration to ensure consistency with the portrait architecture.
- Implemented Native Morphing Container: Replicated the new custom LinearLayout morphing structure (containing the title, description, button, and icon elements) to maintain the lightweight UI approach in landscape.
- Added Custom Pill Ripple Masking: Applied android:foreground="@drawable/ripple_pill_mask" to the Carousel action button to enforce perfect pill-shaped touch boundaries.
- Restored Native Social Ripples: Removed experimental rectangular foreground overrides on the social links, allowing their native, perfectly clipped rounded-corner ripples to function as intended.
---
.../main/res/layout-land/activity_main.xml | 111 ++++++++++++++++--
1 file changed, 104 insertions(+), 7 deletions(-)
diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml
index f3d2c27..a556e89 100644
--- a/app/src/main/res/layout-land/activity_main.xml
+++ b/app/src/main/res/layout-land/activity_main.xml
@@ -116,6 +116,7 @@
android:clickable="true"
android:focusable="true" />
+
+
+
@@ -171,16 +174,110 @@
android:textColor="@color/text_secondary"
android:textSize="12sp" />
-
+
+ android:orientation="vertical"
+ android:layout_marginTop="0dp"
+ android:layout_marginBottom="12dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From b1dfba08f62e7124ca4af4cac40988ee82cbcc86 Mon Sep 17 00:00:00 2001
From: DK <117024745+Dhangofa@users.noreply.github.com>
Date: Wed, 19 Aug 2026 02:21:04 +0530
Subject: [PATCH 3/7] Create custom rounded mask for pill buttons
- Created a new XML ripple drawable containing a strict 20dp corner radius bounding mask.
- Ensures that interactive foreground touch feedback accurately conforms to the rounded bounds of pill-shaped buttons instead of bleeding into the default rectangular bounds.
---
app/src/main/res/drawable/ripple_pill_mask.xml | 10 ++++++++++
1 file changed, 10 insertions(+)
create mode 100644 app/src/main/res/drawable/ripple_pill_mask.xml
diff --git a/app/src/main/res/drawable/ripple_pill_mask.xml b/app/src/main/res/drawable/ripple_pill_mask.xml
new file mode 100644
index 0000000..3a69113
--- /dev/null
+++ b/app/src/main/res/drawable/ripple_pill_mask.xml
@@ -0,0 +1,10 @@
+
+
+ -
+
+
+
+
+
+
From 8d5a17f882b13ae4818786d70c34390e94d7228f Mon Sep 17 00:00:00 2001
From: DK <117024745+Dhangofa@users.noreply.github.com>
Date: Wed, 19 Aug 2026 02:22:49 +0530
Subject: [PATCH 4/7] Remove deprecated item_carousel layout
- Deleted item_carousel.xml from the resource directory.
- The layout structure for individual pages has been integrated directly into activity_main.xml to facilitate the morphing animations, making this separate file obsolete.
---
.../main/res/layout/item_setup_carousel.xml | 89 -------------------
1 file changed, 89 deletions(-)
delete mode 100644 app/src/main/res/layout/item_setup_carousel.xml
diff --git a/app/src/main/res/layout/item_setup_carousel.xml b/app/src/main/res/layout/item_setup_carousel.xml
deleted file mode 100644
index 3eb5a12..0000000
--- a/app/src/main/res/layout/item_setup_carousel.xml
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
From 63a51e83b5f0767630c2c5801257f53f472966db Mon Sep 17 00:00:00 2001
From: DK <117024745+Dhangofa@users.noreply.github.com>
Date: Wed, 19 Aug 2026 02:24:39 +0530
Subject: [PATCH 5/7] Remove ViewPager2 dependency
- Removed the androidx.viewpager2:viewpager2 dependency from the build configuration.
- Eliminated unnecessary library bloat, ensuring the application remains as lightweight as possible.
---
app/build.gradle.kts | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c4907fc..b1a850a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -43,7 +43,6 @@ android {
}
dependencies {
- implementation("androidx.viewpager2:viewpager2:1.0.0")
// Shizuku API
implementation("dev.rikka.shizuku:api:13.1.5")
From 44681ed3591c5fa4ac3a9e469974c3d14cb8e206 Mon Sep 17 00:00:00 2001
From: DK <117024745+Dhangofa@users.noreply.github.com>
Date: Wed, 19 Aug 2026 02:30:20 +0530
Subject: [PATCH 6/7] Remove deprecated CarouselAdapter
- Deleted CarouselAdapter.java from the codebase entirely.
- Removed the RecyclerView.Adapter implementation, as data binding is now handled directly by MainActivity within the native view architecture.
---
.../ui/SetupCarouselAdapter.java | 113 ------------------
1 file changed, 113 deletions(-)
delete mode 100644 app/src/main/java/com/dhangofa/networktoggle/ui/SetupCarouselAdapter.java
diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/SetupCarouselAdapter.java b/app/src/main/java/com/dhangofa/networktoggle/ui/SetupCarouselAdapter.java
deleted file mode 100644
index 792ecf8..0000000
--- a/app/src/main/java/com/dhangofa/networktoggle/ui/SetupCarouselAdapter.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package com.dhangofa.networktoggle.ui;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.net.Uri;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.core.content.ContextCompat;
-
-import com.dhangofa.networktoggle.R;
-
-import java.util.List;
-
-public class SetupCarouselAdapter extends RecyclerView.Adapter {
-
- public static class SetupItem {
- public String title;
- public String description;
- public String buttonText;
- public String url;
- public int iconRes;
- public int bgColorRes;
- public int accentColorRes;
- public int btnBgColorRes;
-
- public SetupItem(String title, String description, String buttonText, String url, int iconRes, int bgColorRes, int accentColorRes, int btnBgColorRes) {
- this.title = title;
- this.description = description;
- this.buttonText = buttonText;
- this.url = url;
- this.iconRes = iconRes;
- this.bgColorRes = bgColorRes;
- this.accentColorRes = accentColorRes;
- this.btnBgColorRes = btnBgColorRes;
- }
- }
-
- private List items;
- private Context context;
-
- public SetupCarouselAdapter(Context context, List items) {
- this.context = context;
- this.items = items;
- }
-
- @NonNull
- @Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View view = LayoutInflater.from(context).inflate(R.layout.item_setup_carousel, parent, false);
- // Important: Ensure the item fills the ViewPager2 width
- view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
- return new ViewHolder(view);
- }
-
- @Override
- public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
- SetupItem item = items.get(position);
- holder.title.setText(item.title);
- holder.desc.setText(item.description);
- holder.buttonText.setText(item.buttonText);
- holder.icon.setImageResource(item.iconRes);
-
- int bgColor = ContextCompat.getColor(context, item.bgColorRes);
- int accentColor = ContextCompat.getColor(context, item.accentColorRes);
-
- holder.background.setBackgroundTintList(ColorStateList.valueOf(bgColor));
-
- holder.buttonText.setTextColor(accentColor);
- holder.buttonIcon.setColorFilter(accentColor);
- holder.icon.setColorFilter(accentColor);
- int btnBgColor = ContextCompat.getColor(context, item.btnBgColorRes);
- holder.button.setBackgroundTintList(ColorStateList.valueOf(btnBgColor));
- holder.iconContainer.setBackgroundTintList(ColorStateList.valueOf(btnBgColor));
-
- holder.button.setOnClickListener(v -> {
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(item.url));
- try { context.startActivity(browserIntent); } catch (Exception e) { android.widget.Toast.makeText(context, "No web browser installed to open this link.", android.widget.Toast.LENGTH_SHORT).show(); }
- });
- }
-
- @Override
- public int getItemCount() {
- return items.size();
- }
-
- static class ViewHolder extends RecyclerView.ViewHolder {
- View background;
- TextView title, desc, buttonText;
- ImageView icon, buttonIcon;
- LinearLayout button;
- View iconContainer;
-
- public ViewHolder(@NonNull View itemView) {
- super(itemView);
- background = itemView.findViewById(R.id.carouselBackground);
- title = itemView.findViewById(R.id.carouselTitle);
- desc = itemView.findViewById(R.id.carouselDesc);
- buttonText = itemView.findViewById(R.id.carouselButtonText);
- icon = itemView.findViewById(R.id.carouselIcon);
- buttonIcon = itemView.findViewById(R.id.carouselButtonIcon);
- button = itemView.findViewById(R.id.carouselButton);
- iconContainer = itemView.findViewById(R.id.carouselIconContainer);
- }
- }
-}
From c7467791f954970862ff84cd08968f32017a2ce8 Mon Sep 17 00:00:00 2001
From: DK <117024745+Dhangofa@users.noreply.github.com>
Date: Wed, 19 Aug 2026 02:33:49 +0530
Subject: [PATCH 7/7] Implement smart Morphing Carousel logic
- Removed all ViewPager2 initialization logic, page change callbacks, and adapter binding code.
- Implemented setupCarousel() and renderCarouselPage() to natively bind data and construct the UI.
- Integrated ArgbEvaluator to mathematically transition the background color between states.
- Implemented ViewPropertyAnimator to handle directional slide-out and slide-in physical animations for the internal content.
- Built a smart auto-cycling timer using Handler and Runnable that cycles every 5 seconds.
- Implemented touch listeners to detect manual user swipes, pausing the auto-cycler and resuming it after 10 seconds of inactivity.
- Connected the carousel state to updateAuthorizationUI(), dynamically locking to the first page when unauthorized, and cycling the setup steps when authorized.
- Added onResume() and onPause() lifecycle hooks to automatically pause the timer when the app is in the background.
---
.../dhangofa/networktoggle/MainActivity.java | 332 ++++++++++++------
1 file changed, 229 insertions(+), 103 deletions(-)
diff --git a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java
index 60d77ee..4213ddc 100644
--- a/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java
+++ b/app/src/main/java/com/dhangofa/networktoggle/MainActivity.java
@@ -33,12 +33,6 @@
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
-import androidx.viewpager2.widget.ViewPager2;
-import com.dhangofa.networktoggle.ui.SetupCarouselAdapter;
-import java.util.ArrayList;
-import java.util.List;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
import com.dhangofa.networktoggle.config.AppPreferences;
import com.dhangofa.networktoggle.model.ExecutionMode;
@@ -79,10 +73,32 @@ public class MainActivity extends Activity implements android.content.SharedPref
private RadioButton radioSim1;
private RadioButton radioSim2;
private TextView autoSimWarningText;
- private ViewPager2 setupCarouselPager;
- private android.widget.LinearLayout setupCarouselContainer;
- private LinearLayout carouselIndicators;
+
private ImageView faqLink;
+
+ // Morphing View Carousel
+ private View carouselBackground;
+ private TextView carouselTitle;
+ private TextView carouselDesc;
+ private TextView carouselButtonText;
+ private ImageView carouselIcon;
+ private ImageView carouselButtonIcon;
+ private View carouselButton;
+ private View carouselIconContainer;
+ private LinearLayout carouselIndicators;
+
+ private int currentCarouselIndex = 0;
+ private float touchStartX = 0f;
+ private CarouselItem[] carouselItems;
+
+ private static class CarouselItem {
+ String title, desc, btnText, url;
+ int iconRes, bgColor, accentColor, btnBgColor;
+ CarouselItem(String title, String desc, String btnText, String url, int iconRes, int bgColor, int accentColor, int btnBgColor) {
+ this.title = title; this.desc = desc; this.btnText = btnText; this.url = url;
+ this.iconRes = iconRes; this.bgColor = bgColor; this.accentColor = accentColor; this.btnBgColor = btnBgColor;
+ }
+ }
private View separatorRootShizuku;
private View separatorAutoSim1;
@@ -175,84 +191,7 @@ protected void onCreate(Bundle savedInstanceState) {
updateAutoSimWarning();
bindSelectionListeners();
updateSeparatorVisibility();
- }
-
- private void setupCarousel() {
- List carouselItems = new ArrayList<>();
- carouselItems.add(new SetupCarouselAdapter.SetupItem("New to NetToggle?", "Learn how to set up Execution Modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/1.-Execution-Mode-Configuration", R.drawable.ic_terminal, R.color.first_pg_bg, R.color.exec_accent, R.color.view_guide_button_bg));
- carouselItems.add(new SetupCarouselAdapter.SetupItem("Target SIM & Cycle", "Learn how to configure your modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/2.-Target-SIM-Setup-&-Quick-Tile-Cycle-Guide", R.drawable.ic_sim_card, R.color.second_pg_bg, R.color.accent_orange, R.color.view_guide_button_bg));
- carouselItems.add(new SetupCarouselAdapter.SetupItem("Quick Settings Ready", "Add the tile to your Control Center", "View Guide", "https://github.com/Dhangofa/NetToggle/wiki/3.-Adding-the-Tile-to-Quick-Settings", R.drawable.ic_network_bars, R.color.third_pg_bg, R.color.accent_pink, R.color.view_guide_button_bg));
-
- if (setupCarouselContainer != null) {
- setupCarouselContainer.removeAllViews();
- for (int i = 0; i < carouselItems.size(); i++) {
- SetupCarouselAdapter.SetupItem item = carouselItems.get(i);
- View view = getLayoutInflater().inflate(R.layout.item_setup_carousel, setupCarouselContainer, false);
- android.widget.LinearLayout.LayoutParams params = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f);
- if (i > 0) params.setMarginStart(12);
- if (i < carouselItems.size() - 1) params.setMarginEnd(12);
- view.setLayoutParams(params);
-
- ((TextView) view.findViewById(R.id.carouselTitle)).setText(item.title);
- ((TextView) view.findViewById(R.id.carouselDesc)).setText(item.description);
- ((TextView) view.findViewById(R.id.carouselButtonText)).setText(item.buttonText);
- ((TextView) view.findViewById(R.id.carouselButtonText)).setTextColor(getColor(item.accentColorRes));
-
- ImageView icon = view.findViewById(R.id.carouselIcon);
- icon.setImageResource(item.iconRes);
- icon.setColorFilter(getColor(item.accentColorRes));
-
- ImageView btnIcon = view.findViewById(R.id.carouselButtonIcon);
- btnIcon.setColorFilter(getColor(item.accentColorRes));
-
- view.findViewById(R.id.carouselBackground).setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.bgColorRes)));
- view.findViewById(R.id.carouselButton).setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.btnBgColorRes)));
- view.findViewById(R.id.carouselIconContainer).setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.btnBgColorRes)));
-
- view.findViewById(R.id.carouselButton).setOnClickListener(v -> {
- try { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(item.url))); } catch (Exception e) {}
- });
-
- setupCarouselContainer.addView(view);
- }
- } else if (setupCarouselPager != null) {
- SetupCarouselAdapter adapter = new SetupCarouselAdapter(this, carouselItems);
- setupCarouselPager.setAdapter(adapter);
- setupCarouselIndicators(carouselItems.size());
- setupCarouselPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
- @Override
- public void onPageSelected(int position) {
- super.onPageSelected(position);
- updateCarouselIndicators(position);
- }
- });
- }
- }
-
-
- private void setupCarouselIndicators(int count) {
- if (carouselIndicators == null) return;
- carouselIndicators.removeAllViews();
- for (int i = 0; i < count; i++) {
- android.widget.ImageView dot = new android.widget.ImageView(this);
- dot.setImageDrawable(getDrawable(R.drawable.shape_dot_inactive));
- android.widget.LinearLayout.LayoutParams params = new android.widget.LinearLayout.LayoutParams(16, 16);
- params.setMargins(8, 0, 8, 0);
- dot.setLayoutParams(params);
- carouselIndicators.addView(dot);
- }
- }
-
- private void updateCarouselIndicators(int position) {
- if (carouselIndicators == null) return;
- for (int i = 0; i < carouselIndicators.getChildCount(); i++) {
- android.widget.ImageView dot = (android.widget.ImageView) carouselIndicators.getChildAt(i);
- if (i == position) {
- dot.setImageDrawable(getDrawable(R.drawable.shape_dot_active));
- } else {
- dot.setImageDrawable(getDrawable(R.drawable.shape_dot_inactive));
- }
- }
+ setupCarousel();
}
private void configureStatusBar() {
@@ -266,11 +205,6 @@ private void configureStatusBar() {
}
private void bindViews() {
- setupCarouselPager = findViewById(R.id.setupCarouselPager);
- setupCarouselContainer = findViewById(R.id.setupCarouselContainer);
- carouselIndicators = findViewById(R.id.carouselIndicators);
- faqLink = findViewById(R.id.faqLink);
- setupCarousel();
radioGroup = findViewById(R.id.modeRadioGroup);
radioRoot = findViewById(R.id.radioRoot);
radioShizuku = findViewById(R.id.radioShizuku);
@@ -283,6 +217,17 @@ private void bindViews() {
autoSimWarningText = findViewById(R.id.autoSimWarningText);
githubLink = findViewById(R.id.githubLink);
telegramLink = findViewById(R.id.telegramLink);
+
+ faqLink = findViewById(R.id.faqLink);
+ carouselBackground = findViewById(R.id.carouselBackground);
+ carouselTitle = findViewById(R.id.carouselTitle);
+ carouselDesc = findViewById(R.id.carouselDesc);
+ carouselButtonText = findViewById(R.id.carouselButtonText);
+ carouselIcon = findViewById(R.id.carouselIcon);
+ carouselButtonIcon = findViewById(R.id.carouselButtonIcon);
+ carouselButton = findViewById(R.id.carouselButton);
+ carouselIconContainer = findViewById(R.id.carouselIconContainer);
+ carouselIndicators = findViewById(R.id.carouselIndicators);
separatorRootShizuku = findViewById(R.id.separatorRootShizuku);
separatorAutoSim1 = findViewById(R.id.separatorAutoSim1);
separatorSim1Sim2 = findViewById(R.id.separatorSim1Sim2);
@@ -296,16 +241,142 @@ private void bindViews() {
}
private void bindLinks() {
- if (faqLink != null) {
- faqLink.setOnClickListener(v -> {
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Dhangofa/NetToggle/wiki/Frequently-Asked-Questions-(FAQ)"));
- try { startActivity(browserIntent); } catch (Exception e) { android.widget.Toast.makeText(MainActivity.this, "No web browser installed to open this link.", android.widget.Toast.LENGTH_SHORT).show(); }
- });
- }
githubLink.setOnClickListener(v -> openUrl("https://github.com/Dhangofa/NetToggle"));
telegramLink.setOnClickListener(v -> openUrl("https://t.me/dhangofas_projects_chat"));
+ if (faqLink != null) faqLink.setOnClickListener(v -> openUrl("https://github.com/Dhangofa/NetToggle/wiki"));
}
+
+ private void setupCarousel() {
+ if (carouselBackground == null) return;
+ carouselItems = new CarouselItem[]{
+ new CarouselItem("New to NetToggle?", "Learn how to set up Execution Modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/1.-Execution-Mode-Configuration", R.drawable.ic_terminal, R.color.first_pg_bg, R.color.exec_accent, R.color.view_guide_button_bg),
+ new CarouselItem("Target SIM & Cycle", "Learn how to configure your modes", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/2.-Target-SIM-Setup-&-Quick-Tile-Cycle-Guide", R.drawable.ic_sim_card, R.color.second_pg_bg, R.color.accent_orange, R.color.view_guide_button_bg),
+ new CarouselItem("Quick Settings Ready", "Add the tile to your Control Center", "Read Guide", "https://github.com/Dhangofa/NetToggle/wiki/3.-Adding-the-Tile-to-Quick-Settings", R.drawable.ic_network_bars, R.color.third_pg_bg, R.color.accent_pink, R.color.view_guide_button_bg)
+ };
+
+ setupCarouselIndicators(carouselItems.length);
+ // Start state depends on initial auth state.
+ // We defer to updateCarouselContext which will be triggered by loadSavedExecutionMode -> setStatus -> updateAuthorizationUI
+ // But let's set a default page just in case
+ renderCarouselPage(isUIAuthorized ? 1 : 0, false, 0);
+
+ carouselBackground.setOnTouchListener((v, event) -> {
+ switch (event.getAction()) {
+ case android.view.MotionEvent.ACTION_DOWN:
+ touchStartX = event.getX();
+ return true;
+ case android.view.MotionEvent.ACTION_UP:
+ case android.view.MotionEvent.ACTION_CANCEL:
+ float deltaX = event.getX() - touchStartX;
+ if (Math.abs(deltaX) > 100) {
+ carouselHandler.removeCallbacks(carouselAutoRunnable);
+ if (deltaX > 0 && currentCarouselIndex > 0) {
+ currentCarouselIndex--;
+ renderCarouselPage(currentCarouselIndex, true, -1);
+ } else if (deltaX < 0 && currentCarouselIndex < carouselItems.length - 1) {
+ currentCarouselIndex++;
+ renderCarouselPage(currentCarouselIndex, true, 1);
+ }
+ // Resume after 10 seconds of inactivity
+ carouselHandler.postDelayed(carouselAutoRunnable, 10000);
+ } else if (event.getAction() == android.view.MotionEvent.ACTION_UP) {
+ v.performClick();
+ }
+ return true;
+ }
+ return false;
+ });
+
+ carouselBackground.setOnClickListener(v -> {
+ // Needed to ensure ripple works when performClick is called
+ });
+ }
+
+ private void renderCarouselPage(int index, boolean animate, int direction) {
+ CarouselItem item = carouselItems[index];
+ Runnable updateViews = () -> {
+ carouselTitle.setText(item.title);
+ carouselDesc.setText(item.desc);
+ carouselButtonText.setText(item.btnText);
+ carouselButtonText.setTextColor(getColor(item.accentColor));
+
+ carouselIcon.setImageResource(item.iconRes);
+ carouselIcon.setColorFilter(getColor(item.accentColor));
+ carouselButtonIcon.setColorFilter(getColor(item.accentColor));
+
+ carouselBackground.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.bgColor)));
+ carouselButton.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.btnBgColor)));
+ carouselIconContainer.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getColor(item.btnBgColor)));
+
+ carouselButton.setOnClickListener(v -> {
+ try { startActivity(new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(item.url))); } catch (Exception e) {}
+ });
+ updateCarouselIndicators(index);
+ };
+
+ if (animate) {
+ float moveOutX = direction * -100f; // Swipe left (next) -> content moves left (-100).
+ float moveInX = direction * 100f; // Swipe left -> new content comes from right (+100).
+ long duration = 200;
+
+ // Animate background color smoothly
+ android.animation.ValueAnimator colorAnim = android.animation.ValueAnimator.ofObject(
+ new android.animation.ArgbEvaluator(),
+ carouselBackground.getBackgroundTintList().getDefaultColor(),
+ getColor(item.bgColor)
+ );
+ colorAnim.setDuration(duration);
+ colorAnim.addUpdateListener(animator -> {
+ carouselBackground.setBackgroundTintList(android.content.res.ColorStateList.valueOf((int) animator.getAnimatedValue()));
+ });
+ colorAnim.start();
+
+ // Animate content out
+ android.view.View[] viewsToAnimate = {carouselTitle, carouselDesc, carouselButton, carouselIconContainer};
+ for (android.view.View view : viewsToAnimate) {
+ view.animate().translationX(moveOutX).alpha(0f).setDuration(duration / 2)
+ .setInterpolator(new android.view.animation.AccelerateInterpolator())
+ .withEndAction(() -> {
+ // Once out, swap data
+ if (view == carouselTitle) updateViews.run();
+
+ // Prepare for animate in
+ view.setTranslationX(moveInX);
+ view.animate().translationX(0f).alpha(1f).setDuration(duration)
+ .setInterpolator(new android.view.animation.DecelerateInterpolator())
+ .start();
+ }).start();
+ }
+ } else {
+ updateViews.run();
+ }
+ }
+
+ private void setupCarouselIndicators(int count) {
+ if (carouselIndicators == null) return;
+ carouselIndicators.removeAllViews();
+ for (int i = 0; i < count; i++) {
+ android.widget.ImageView dot = new android.widget.ImageView(this);
+ dot.setImageDrawable(getDrawable(R.drawable.shape_dot_inactive));
+ android.widget.LinearLayout.LayoutParams params = new android.widget.LinearLayout.LayoutParams(16, 16);
+ params.setMargins(8, 0, 8, 0);
+ dot.setLayoutParams(params);
+ carouselIndicators.addView(dot);
+ }
+ }
+
+ private void updateCarouselIndicators(int position) {
+ if (carouselIndicators == null) return;
+ for (int i = 0; i < carouselIndicators.getChildCount(); i++) {
+ android.widget.ImageView dot = (android.widget.ImageView) carouselIndicators.getChildAt(i);
+ if (i == position) {
+ dot.setImageDrawable(getDrawable(R.drawable.shape_dot_active));
+ } else {
+ dot.setImageDrawable(getDrawable(R.drawable.shape_dot_inactive));
+ }
+ }
+ }
private void updateSeparatorVisibility() {
// Execution Mode Separator
int modeId = radioGroup.getCheckedRadioButtonId();
@@ -421,6 +492,18 @@ protected void onResume() {
checkAndRequestPermission();
updateErrorBanner();
updateCapabilities();
+ if (carouselHandler != null && carouselAutoRunnable != null) {
+ carouselHandler.removeCallbacks(carouselAutoRunnable);
+ carouselHandler.postDelayed(carouselAutoRunnable, 5000);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (carouselHandler != null && carouselAutoRunnable != null) {
+ carouselHandler.removeCallbacks(carouselAutoRunnable);
+ }
}
private void checkAndRequestPermission() {
@@ -649,11 +732,55 @@ private void setStatus(String text, int color) {
updateAuthorizationUI(color == 0xFF1B873F);
}
- private boolean isUIAuthorized = true;
- private void updateAuthorizationUI(boolean authorized) {
- if (setupCarouselPager != null) {
- setupCarouselPager.setCurrentItem(authorized ? 2 : 0, true);
+
+ private android.os.Handler carouselHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+
+ private Runnable carouselAutoRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (carouselItems == null) return;
+
+ int nextIndex;
+ if (!isUIAuthorized) {
+ // If unauthorized but user manually swiped away, bring them back to 0 eventually
+ nextIndex = 0;
+ } else {
+ // If authorized, cycle through logically
+ nextIndex = (currentCarouselIndex == 2) ? 1 : (currentCarouselIndex + 1);
+ }
+
+ if (nextIndex != currentCarouselIndex) {
+ int direction = (nextIndex > currentCarouselIndex) ? 1 : -1;
+ currentCarouselIndex = nextIndex;
+ renderCarouselPage(currentCarouselIndex, true, direction);
+ }
+ carouselHandler.postDelayed(this, 5000);
}
+ };
+
+ private void updateCarouselContext(boolean authorized) {
+ if (carouselBackground == null || carouselItems == null) return;
+ carouselHandler.removeCallbacks(carouselAutoRunnable);
+
+ if (!authorized) {
+ if (currentCarouselIndex != 0) {
+ currentCarouselIndex = 0;
+ renderCarouselPage(0, true, -1);
+ }
+ // Will auto-correct to 0 every 5 seconds if user swipes away while unauthorized
+ carouselHandler.postDelayed(carouselAutoRunnable, 5000);
+ } else {
+ if (currentCarouselIndex == 0) {
+ currentCarouselIndex = 1;
+ renderCarouselPage(1, true, 1);
+ }
+ carouselHandler.postDelayed(carouselAutoRunnable, 5000);
+ }
+ }
+
+ private boolean isUIAuthorized = false;
+ private void updateAuthorizationUI(boolean authorized) {
+ updateCarouselContext(authorized);
if (isUIAuthorized == authorized) return;
isUIAuthorized = authorized;
@@ -690,7 +817,6 @@ private void openUrl(String url) {
try {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
} catch (Exception ignored) {
- android.widget.Toast.makeText(MainActivity.this, "No web browser installed to open this link.", android.widget.Toast.LENGTH_SHORT).show();
}
}