diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a11f23c..f4291a9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ android:label="NetToggle" android:icon="@drawable/ic_launcher" android:roundIcon="@drawable/ic_launcher" - android:theme="@android:style/Theme.DeviceDefault.DayNight" + android:theme="@style/Theme.NetToggle" android:supportsRtl="true"> { - NetworkMode realMode = networkModeReader.readCurrentMode(); + EXECUTOR.execute(() -> { + NetworkMode realMode = networkModeReader.readCurrentMode(); - mainHandler.post(() -> { - if (realMode != NetworkMode.UNKNOWN) { - appPreferences.setCachedNetworkMode(realMode); - updateTileUI(realMode); - } else { - updateTileUI(cachedMode); - } - }); + mainHandler.post(() -> { + /* + * A tile click or another operation may have updated the state + * while this asynchronous readback was running. + */ + if (appPreferences.getCachedNetworkMode() + != NetworkMode.UNKNOWN) { + return; + } + + if (realMode != NetworkMode.UNKNOWN) { + appPreferences.setCachedNetworkMode(realMode); + updateTileUI(realMode); + } }); + }); } @Override @@ -95,7 +105,7 @@ public void onClick() { } NetworkMode currentMode = appPreferences.getCachedNetworkMode(); - NetworkMode nextMode = NetworkMode.nextInDefaultCycle(currentMode); + NetworkMode nextMode = tileCycleManager.getNextMode(currentMode); updateTileSwitchingUI(); EXECUTOR.execute(() -> { @@ -187,16 +197,22 @@ private void updateTileSwitchingUI() { private void updateTileUI(NetworkMode mode) { Tile tile = getQsTile(); - if (tile == null) return; + + if (tile == null) { + return; + } if (mode == NetworkMode.UNKNOWN) { if (appPreferences.getExecutionMode() == ExecutionMode.NONE) { tile.setState(Tile.STATE_UNAVAILABLE); tile.setLabel("Setup Required"); } else { + NetworkMode firstMode = tileCycleManager.getFirstMode(); + tile.setState(Tile.STATE_INACTIVE); - tile.setLabel(mode.getTileLabel()); + tile.setLabel("Tap to Set " + firstMode.getTileLabel()); } + tile.setIcon(getCachedIcon("?")); } else { tile.setState(Tile.STATE_ACTIVE); diff --git a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java index 4b824fe..de5ac8f 100644 --- a/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java +++ b/app/src/main/java/com/dhangofa/networktoggle/config/AppPreferences.java @@ -7,12 +7,16 @@ import com.dhangofa.networktoggle.model.NetworkMode; import com.dhangofa.networktoggle.model.TargetSim; +import java.util.ArrayList; +import java.util.List; + public final class AppPreferences { private static final String PREFS_NAME = "NetTogglePrefs"; private static final String KEY_EXEC_MODE = "exec_mode"; private static final String KEY_TARGET_SIM = "target_sim"; private static final String KEY_NETWORK_STATE = "net_state"; private static final String KEY_AUTO_SIM_ERROR = "auto_sim_error"; + private static final String KEY_TILE_CYCLE_MODES = "tile_cycle_modes"; private final SharedPreferences preferences; @@ -26,19 +30,11 @@ public ExecutionMode getExecutionMode() { preferences.getInt(KEY_EXEC_MODE, ExecutionMode.NONE.getValue())); } - public void setExecutionMode(ExecutionMode mode) { - preferences.edit().putInt(KEY_EXEC_MODE, mode.getValue()).apply(); - } - public TargetSim getTargetSim() { return TargetSim.fromValue( preferences.getInt(KEY_TARGET_SIM, TargetSim.AUTO.getValue())); } - public void setTargetSim(TargetSim targetSim) { - preferences.edit().putInt(KEY_TARGET_SIM, targetSim.getValue()).apply(); - } - public NetworkMode getCachedNetworkMode() { return NetworkMode.fromStateValue( preferences.getInt(KEY_NETWORK_STATE, NetworkMode.UNKNOWN.getStateValue())); @@ -48,6 +44,10 @@ public void setCachedNetworkMode(NetworkMode mode) { preferences.edit().putInt(KEY_NETWORK_STATE, mode.getStateValue()).apply(); } + public void clearCachedNetworkMode() { + setCachedNetworkMode(NetworkMode.UNKNOWN); + } + public boolean hasAutoSimError() { return preferences.getBoolean(KEY_AUTO_SIM_ERROR, false); } @@ -72,6 +72,31 @@ public void onTargetSimChanged(TargetSim targetSim) { .apply(); } + public List getTileCycleModes() { + String saved = preferences.getString(KEY_TILE_CYCLE_MODES, ""); + List modes = new ArrayList<>(); + if (saved == null || saved.trim().isEmpty()) return modes; + + String[] ids = saved.split(","); + for (String id : ids) { + try { + NetworkMode mode = NetworkMode.valueOf(id.trim()); + if (mode != NetworkMode.UNKNOWN && !modes.contains(mode)) modes.add(mode); + } catch (IllegalArgumentException ignored) { + } + } + return modes; + } + + public void setTileCycleModes(List modes) { + StringBuilder value = new StringBuilder(); + for (NetworkMode mode : modes) { + if (value.length() > 0) value.append(','); + value.append(mode.name()); + } + preferences.edit().putString(KEY_TILE_CYCLE_MODES, value.toString()).apply(); + } + public void clearTransientState() { preferences.edit() .putInt(KEY_NETWORK_STATE, NetworkMode.UNKNOWN.getStateValue()) diff --git a/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java b/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java new file mode 100644 index 0000000..95155b1 --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/cycle/TileCycleManager.java @@ -0,0 +1,92 @@ +package com.dhangofa.networktoggle.cycle; + +import com.dhangofa.networktoggle.config.AppPreferences; +import com.dhangofa.networktoggle.model.NetworkMode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public final class TileCycleManager { + public static final int MIN_MODES = 2; + public static final int MAX_MODES = 3; + + private static final List DEFAULT_CYCLE = + Collections.unmodifiableList(Arrays.asList( + NetworkMode.FIVE_G_ONLY, + NetworkMode.FOUR_G_ONLY, + NetworkMode.PREFERRED_5G + )); + + public enum ChangeResult { + CHANGED, + MINIMUM_REACHED, + MAXIMUM_REACHED, + INVALID_MODE, + NO_CHANGE + } + + private final AppPreferences appPreferences; + + public TileCycleManager(AppPreferences appPreferences) { + this.appPreferences = appPreferences; + } + + public List getCycle() { + List saved = appPreferences.getTileCycleModes(); + if (!isValid(saved)) { + appPreferences.setTileCycleModes(DEFAULT_CYCLE); + return new ArrayList<>(DEFAULT_CYCLE); + } + return new ArrayList<>(saved); + } + + public NetworkMode getFirstMode() { + return getCycle().get(0); + } + + public NetworkMode getNextMode(NetworkMode currentMode) { + List cycle = getCycle(); + int currentIndex = cycle.indexOf(currentMode); + if (currentIndex < 0) return cycle.get(0); + return cycle.get((currentIndex + 1) % cycle.size()); + } + + public ChangeResult setSelected(NetworkMode mode, boolean selected) { + if (mode == null || mode == NetworkMode.UNKNOWN) { + return ChangeResult.INVALID_MODE; + } + + List cycle = getCycle(); + boolean alreadySelected = cycle.contains(mode); + if (selected == alreadySelected) return ChangeResult.NO_CHANGE; + + if (selected) { + if (cycle.size() >= MAX_MODES) return ChangeResult.MAXIMUM_REACHED; + cycle.add(mode); + } else { + if (cycle.size() <= MIN_MODES) return ChangeResult.MINIMUM_REACHED; + cycle.remove(mode); + } + + appPreferences.setTileCycleModes(cycle); + appPreferences.clearCachedNetworkMode(); + return ChangeResult.CHANGED; + } + + public boolean isValid(List cycle) { + if (cycle == null || cycle.size() < MIN_MODES || cycle.size() > MAX_MODES) { + return false; + } + + List unique = new ArrayList<>(); + for (NetworkMode mode : cycle) { + if (mode == null || mode == NetworkMode.UNKNOWN || unique.contains(mode)) { + return false; + } + unique.add(mode); + } + return true; + } +} diff --git a/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java new file mode 100644 index 0000000..7b06b9e --- /dev/null +++ b/app/src/main/java/com/dhangofa/networktoggle/ui/TileCycleUiController.java @@ -0,0 +1,85 @@ +package com.dhangofa.networktoggle.ui; + +import android.app.Activity; +import android.widget.CheckBox; +import android.widget.TextView; +import android.widget.Toast; + +import com.dhangofa.networktoggle.R; +import com.dhangofa.networktoggle.cycle.TileCycleManager; +import com.dhangofa.networktoggle.model.NetworkMode; + +import java.util.List; + +public final class TileCycleUiController { + private final Activity activity; + private final TileCycleManager cycleManager; + private final CheckBox mode5gOnly; + private final CheckBox mode4gOnly; + private final CheckBox modePref5g; + private final CheckBox modePref4g; + private final TextView selectedCount; + private final TextView cycleOrder; + private boolean updatingUi; + + public TileCycleUiController(Activity activity, TileCycleManager cycleManager) { + this.activity = activity; + this.cycleManager = cycleManager; + mode5gOnly = activity.findViewById(R.id.cycle5gOnly); + mode4gOnly = activity.findViewById(R.id.cycle4gOnly); + modePref5g = activity.findViewById(R.id.cyclePreferred5g); + modePref4g = activity.findViewById(R.id.cyclePreferred4g); + selectedCount = activity.findViewById(R.id.cycleSelectedCount); + cycleOrder = activity.findViewById(R.id.cycleOrderText); + } + + public void initialize() { + refresh(); + mode5gOnly.setOnCheckedChangeListener((button, selected) -> + handleSelection(NetworkMode.FIVE_G_ONLY, selected)); + mode4gOnly.setOnCheckedChangeListener((button, selected) -> + handleSelection(NetworkMode.FOUR_G_ONLY, selected)); + modePref5g.setOnCheckedChangeListener((button, selected) -> + handleSelection(NetworkMode.PREFERRED_5G, selected)); + modePref4g.setOnCheckedChangeListener((button, selected) -> + handleSelection(NetworkMode.PREFERRED_4G, selected)); + } + + private void handleSelection(NetworkMode mode, boolean selected) { + if (updatingUi) return; + + TileCycleManager.ChangeResult result = cycleManager.setSelected(mode, selected); + if (result == TileCycleManager.ChangeResult.MINIMUM_REACHED) { + showToast("Select at least 2 tile modes."); + } else if (result == TileCycleManager.ChangeResult.MAXIMUM_REACHED) { + showToast("You can select up to 3 tile modes."); + } + refresh(); + } + + private void refresh() { + updatingUi = true; + List cycle = cycleManager.getCycle(); + mode5gOnly.setChecked(cycle.contains(NetworkMode.FIVE_G_ONLY)); + mode4gOnly.setChecked(cycle.contains(NetworkMode.FOUR_G_ONLY)); + modePref5g.setChecked(cycle.contains(NetworkMode.PREFERRED_5G)); + modePref4g.setChecked(cycle.contains(NetworkMode.PREFERRED_4G)); + selectedCount.setText("Selected: " + cycle.size() + "/3"); + cycleOrder.setText(buildOrderText(cycle)); + updatingUi = false; + } + + private String buildOrderText(List cycle) { + StringBuilder text = new StringBuilder("Cycle: "); + for (int i = 0; i < cycle.size(); i++) { + if (i > 0) text.append(" → "); + text.append(cycle.get(i).getDisplayName()); + } + return text.toString(); + } + + private void showToast(String message) { + Toast.makeText(activity, message, Toast.LENGTH_SHORT).show(); + } +} + diff --git a/app/src/main/res/color/color_segmented_text.xml b/app/src/main/res/color/color_segmented_text.xml new file mode 100644 index 0000000..4d24810 --- /dev/null +++ b/app/src/main/res/color/color_segmented_text.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..51a6c50 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/selector_segmented_tab.xml b/app/src/main/res/drawable/selector_segmented_tab.xml new file mode 100644 index 0000000..3377f2c --- /dev/null +++ b/app/src/main/res/drawable/selector_segmented_tab.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_segmented_container.xml b/app/src/main/res/drawable/shape_segmented_container.xml new file mode 100644 index 0000000..93b7f91 --- /dev/null +++ b/app/src/main/res/drawable/shape_segmented_container.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ae81eb5..ad77aef 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,5 @@ @@ -21,14 +20,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:gravity="center" - android:paddingVertical="8dp"> + android:gravity="center_horizontal" + android:paddingTop="4dp" + android:paddingBottom="12dp"> + android:layout_width="56dp" + android:layout_height="56dp" + android:layout_marginBottom="10dp" + android:background="@drawable/shape_pill_badge_bg"> - + android:layout_marginBottom="4dp" /> + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fontFamily="sans-serif" + android:gravity="center" + android:lineSpacingExtra="1dp" + android:paddingHorizontal="8dp" + android:text="Switch Network between 5G, 4G, Preferred 5G and Preferred 4G from Quick Settings." + android:textColor="@color/text_secondary" + android:textSize="12sp" /> @@ -69,16 +83,16 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:background="@drawable/shape_qs_hint_bg" - android:padding="14dp" + android:padding="12dp" android:gravity="center_vertical" - android:layout_marginTop="12dp" - android:layout_marginBottom="20dp"> + android:layout_marginTop="0dp" + android:layout_marginBottom="12dp"> + android:layout_marginEnd="10dp"> @@ -113,303 +127,279 @@ - + - - - + android:orientation="vertical" + android:background="@drawable/section_outline_bg" + android:padding="12dp" + android:layout_marginBottom="12dp"> + + - + android:gravity="center_vertical"> - + android:layout_marginEnd="6dp" /> - - - - - - - - - - - - - - - - + android:paddingVertical="4dp" /> + - + + + - - - - - - - - - - - - - - - - - - - + + - - - + android:minHeight="40dp" + android:paddingVertical="8dp" + android:background="@drawable/selector_segmented_tab" + android:textColor="@color/color_segmented_text" + android:textSize="12sp" + android:textStyle="bold" /> + - + + android:padding="12dp" + android:layout_marginBottom="12dp"> + + + + + + + + + + + + + + + + android:gravity="center" + android:textColor="@color/status_error_text" + android:background="@drawable/shape_status_badge_error" + android:paddingHorizontal="6dp" + android:paddingVertical="4dp" + android:visibility="gone" + android:layout_marginTop="8dp" /> + + + + + + + + android:layout_marginEnd="12dp" /> - + android:layout_weight="1" + android:orientation="vertical"> + + + + + + android:orientation="horizontal"> - - - - - - diff --git a/app/src/main/res/layout/view_tile_cycle_card.xml b/app/src/main/res/layout/view_tile_cycle_card.xml new file mode 100644 index 0000000..af35456 --- /dev/null +++ b/app/src/main/res/layout/view_tile_cycle_card.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 4bf04ac..b3b7f98 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -24,4 +24,10 @@ #2B2930 + + + #1B1A20 + #36323F + #EADDFF + #938F96 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7ae2eaa..b71b1d3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -24,4 +24,10 @@ #F4EFF4 + + + #F3EDF7 + #EADDFF + #21005D + #49454F