From 9cc8d6227b744faf7d02b156ddc586e9709f7c7b Mon Sep 17 00:00:00 2001 From: Jeff Stammler <85592424+Jeff-Stammler@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:28:35 -0700 Subject: [PATCH 1/6] initial push --- .idea/deploymentTargetSelector.xml | 11 -- app/src/main/AndroidManifest.xml | 3 + .../scouting_app/activities/AppLaunch.java | 3 + .../scouting_app/activities/SubmitData.java | 1 + .../team3663/scouting_app/config/Globals.java | 3 + .../scouting_app/utility/CPR_Network.java | 146 ++++++++++++++++++ 6 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index fc956afc..94d25b24 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -13,17 +13,6 @@ - - \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 777f3195..d5ce06dc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + diff --git a/app/src/main/java/com/team3663/scouting_app/activities/AppLaunch.java b/app/src/main/java/com/team3663/scouting_app/activities/AppLaunch.java index 72a1dab5..271cb7de 100644 --- a/app/src/main/java/com/team3663/scouting_app/activities/AppLaunch.java +++ b/app/src/main/java/com/team3663/scouting_app/activities/AppLaunch.java @@ -30,6 +30,7 @@ import com.team3663.scouting_app.config.Constants; import com.team3663.scouting_app.config.Globals; import com.team3663.scouting_app.databinding.AppLaunchBinding; +import com.team3663.scouting_app.utility.CPR_Network; import com.team3663.scouting_app.utility.dataFile.*; import java.util.List; @@ -128,6 +129,8 @@ protected void onCreate(Bundle in_savedInstanceState) { // While loading Matches, we messed with Globals.CurrentMatchType, so reset it Globals.CurrentMatchType = Constants.PreMatch.DEFAULT_MATCH_TYPE; + Globals.network = new CPR_Network(this); + boolean b = Globals.network.hasActiveInternet(); } // ============================================================================================= diff --git a/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java b/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java index 1f01cd8f..f6c5242b 100644 --- a/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java +++ b/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java @@ -377,6 +377,7 @@ private void initQuit() { .setPositiveButton(getString(R.string.submit_alert_quit_positive), (dialog, which) -> { SubmitData.this.finishAffinity(); stopLockTask(); + Globals.network.shutdown(); System.exit(0); }) diff --git a/app/src/main/java/com/team3663/scouting_app/config/Globals.java b/app/src/main/java/com/team3663/scouting_app/config/Globals.java index 25e321fe..d2ca9be7 100644 --- a/app/src/main/java/com/team3663/scouting_app/config/Globals.java +++ b/app/src/main/java/com/team3663/scouting_app/config/Globals.java @@ -9,6 +9,7 @@ import com.team3663.scouting_app.data.Accuracy; import com.team3663.scouting_app.data.ClimbLevel; import com.team3663.scouting_app.data.ClimbPosition; +import com.team3663.scouting_app.utility.CPR_Network; import com.team3663.scouting_app.utility.Logger; import com.team3663.scouting_app.utility.achievements.Achievements; import com.team3663.scouting_app.utility.dataFile.*; @@ -73,4 +74,6 @@ public class Globals { public static int TransmitMatchNum; public static String TransmitMatchType; + + public static CPR_Network network; } diff --git a/app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java b/app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java new file mode 100644 index 00000000..d92b97db --- /dev/null +++ b/app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java @@ -0,0 +1,146 @@ +package com.team3663.scouting_app.utility; + +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Checks whether a remote host:port is reachable. + * + *

Two-step check: + *

    + *
  1. Is there any active network with internet capability? (cheap, no I/O)
  2. + *
  3. Can we open a TCP socket to the given host:port? (blocking, runs off the UI thread)
  4. + *
+ * + *

Required manifest permissions: + *

+ *   <uses-permission android:name="android.permission.INTERNET" />
+ *   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ * 
+ */public class CPR_Network { + + /** + * Why a reachability check ended the way it did. + */ + public enum Result { + REACHABLE, // socket connected successfully + NO_NETWORK, // device has no active internet-capable network + HOST_UNREACHABLE // network exists, but host:port could not be reached + } + + /** + * Delivered on the main thread. + */ + public interface Callback { + void onResult(@NonNull Result result); + } + + private final Context appContext; + private final ExecutorService executor; + private final Handler mainHandler; + + public CPR_Network(@NonNull Context context) { + // Use the application context to avoid leaking an Activity. + this.appContext = context.getApplicationContext(); + this.executor = Executors.newCachedThreadPool(); + this.mainHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Asynchronously checks reachability. The callback is invoked on the main thread. + * + * @param host hostname or IP of the remote machine + * @param port port the remote service listens on (e.g. 8080, 443, 22) + * @param timeoutMs socket connect timeout in milliseconds + * @param callback invoked on the main thread with the result + */ + public void check(@NonNull String host, int port, int timeoutMs, @NonNull Callback callback) { + executor.execute(() -> { + Result result = checkBlocking(host, port, timeoutMs); + mainHandler.post(() -> callback.onResult(result)); + }); + } + + /** + * Synchronous check. Must NOT be called on the main thread (blocks on socket I/O). + * Exposed for use inside your own background threads / coroutines. + */ + @NonNull + public Result checkBlocking(@NonNull String host, int port, int timeoutMs) { + if (!hasActiveInternet()) { + return Result.NO_NETWORK; + } + return isHostReachable(host, port, timeoutMs) + ? Result.REACHABLE + : Result.HOST_UNREACHABLE; + } + + /** + * Step 1: is there an active, internet-capable network? + */ + public boolean hasActiveInternet() { + ConnectivityManager cm = + (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) { + return false; + } + Network network = cm.getActiveNetwork(); + if (network == null) { + return false; + } + NetworkCapabilities caps = cm.getNetworkCapabilities(network); + return caps != null + && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + } + + /** + * Step 2: can we open a TCP connection to host:port? + */ + private boolean isHostReachable(@NonNull String host, int port, int timeoutMs) { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(host, port), timeoutMs); + return true; + } catch (IOException e) { + // unreachable, connection refused, or timed out + return false; + } + } + + /** + * Call when you're done (e.g. in onDestroy) to release the thread pool. + */ + public void shutdown() { + executor.shutdownNow(); + } + + /** + * Choose new wifi to connect to + */ + public void pickWIFI() { + try { + // Try quick Wi-Fi panel + Intent GoToSystemWIFI = new Intent(Settings.Panel.ACTION_WIFI); + GoToSystemWIFI.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appContext.startActivity(GoToSystemWIFI); + } catch (Exception e) { + // Fallback to full Wi-Fi settings + Intent GoToSystemWIFI = new Intent(Settings.ACTION_WIFI_SETTINGS); + GoToSystemWIFI.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appContext.startActivity(GoToSystemWIFI); + } + } +} From efa8641c5d48e8037ebcc027749836155ac503f2 Mon Sep 17 00:00:00 2001 From: Jeff Stammler Date: Wed, 1 Jul 2026 18:40:26 -0700 Subject: [PATCH 2/6] almost there --- .../scouting_app/activities/SubmitData.java | 101 ++++++- app/src/main/res/drawable/internet.xml | 20 ++ app/src/main/res/drawable/wifi_bar_0.xml | 20 ++ app/src/main/res/drawable/wifi_bar_1.xml | 10 + app/src/main/res/drawable/wifi_bar_2.xml | 10 + app/src/main/res/drawable/wifi_bar_3.xml | 20 ++ app/src/main/res/drawable/wifi_bar_4.xml | 20 ++ app/src/main/res/layout/submit_data.xml | 255 ++++++++++-------- app/src/main/res/values/strings_submit.xml | 3 + 9 files changed, 353 insertions(+), 106 deletions(-) create mode 100644 app/src/main/res/drawable/internet.xml create mode 100644 app/src/main/res/drawable/wifi_bar_0.xml create mode 100644 app/src/main/res/drawable/wifi_bar_1.xml create mode 100644 app/src/main/res/drawable/wifi_bar_2.xml create mode 100644 app/src/main/res/drawable/wifi_bar_3.xml create mode 100644 app/src/main/res/drawable/wifi_bar_4.xml diff --git a/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java b/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java index f6c5242b..17a322bb 100644 --- a/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java +++ b/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java @@ -2,13 +2,22 @@ import android.annotation.SuppressLint; import android.app.AlertDialog; +import android.content.Context; import android.content.Intent; import android.media.MediaPlayer; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import androidx.activity.EdgeToEdge; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; @@ -35,6 +44,8 @@ public class SubmitData extends AppCompatActivity { private SubmitDataBinding submitDataBinding; private static final Timer achievement_timer = new Timer(); private static MediaPlayer media; + private ConnectivityManager connectivityManager; + private ConnectivityManager.NetworkCallback networkCallback; @SuppressLint({"SetTextI18n", "MissingInflatedId"}) @Override @@ -61,6 +72,7 @@ protected void onCreate(Bundle in_savedInstanceState) { } // Initialize activity components that don't log anything + initNetwork(); initMatchType(); initMatch(); initQR(); @@ -70,6 +82,15 @@ protected void onCreate(Bundle in_savedInstanceState) { initOverride(); } + @Override + protected void onDestroy() { + super.onDestroy(); + // Important: Unregister to avoid memory leaks + if (connectivityManager != null && networkCallback != null) { + connectivityManager.unregisterNetworkCallback(networkCallback); + } + } + // ============================================================================================= // Function: initMatchType // Description: Initialize the Match Type field @@ -258,6 +279,35 @@ private void closeAchievements() { media.release(); } + // ============================================================================================= + // Function: updateWifiIcon + // Description: Update the Wi-Fi signal icon with the correct signal strength level + // Parameters: in_level signal strength level + // Output: void + // ============================================================================================= + private void updateWifiIcon(int in_level) { + // You would typically swap icons here based on level + // 0: No signal, 1-4: Signal bars + switch (in_level) { + case 4: submitDataBinding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_4); break; + case 3: submitDataBinding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_3); break; + case 2: submitDataBinding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_2); break; + case 1: submitDataBinding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_1); break; + default: submitDataBinding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_0); break; + } + + // check if we have access to the internet + if (Globals.network.hasActiveInternet()) { + submitDataBinding.imageInternet.setVisibility(View.VISIBLE); + submitDataBinding.butSendGoogle.setEnabled(true); + submitDataBinding.butSendDatabase.setEnabled(true); + } else { + submitDataBinding.imageInternet.setVisibility(View.INVISIBLE); + submitDataBinding.butSendGoogle.setEnabled(false); + submitDataBinding.butSendDatabase.setEnabled(false); + } + } + // ============================================================================================= // Function: initOverride // Description: Override team number reset for next match @@ -320,6 +370,52 @@ private void initMatch() { if (adp_Match.getCount() > 0) submitDataBinding.spinnerMatch.setSelection(adp_Match.getCount() - 1, true); } + // ============================================================================================= + // Function: initNetwork + // Description: Initialize the network related fields and process + // Parameters: void + // Output: void + // ============================================================================================= + private void initNetwork() { + // setup Wi-Fi monitoring + connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + + networkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onCapabilitiesChanged(@NonNull Network in_network, @NonNull NetworkCapabilities in_capabilities) { + int rssi = 0; + + // On API 31+, WifiInfo is part of the capabilities (requires location permission) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + WifiInfo wifiInfo = (WifiInfo) in_capabilities.getTransportInfo(); + if (wifiInfo != null) rssi = wifiInfo.getRssi(); + } else { + // Fallback for API 30 + WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + rssi = wm.getConnectionInfo().getRssi(); + } + + // Convert RSSI to a signal level (0 to 4) + int level = WifiManager.calculateSignalLevel(rssi, 5); + + // Update UI on the main thread + runOnUiThread(() -> updateWifiIcon(level)); + } + + @Override + public void onLost(@NonNull Network in_network) { + // Signal lost or Wi-Fi turned off + runOnUiThread(() -> updateWifiIcon(-1)); + } + }; + + connectivityManager.registerNetworkCallback(networkRequest, networkCallback); + } + // ============================================================================================= // Function: initQR // Description: Initialize the QR Code field @@ -347,6 +443,9 @@ private void initQR() { // Output: void // ============================================================================================= private void initBluetooth() { + // Until we have BT working... + submitDataBinding.butSendBT.setEnabled(false); + submitDataBinding.butSendBT.setOnClickListener(view -> { // Reset pre-Match settings for next time Globals.numStartingGamePiece = Constants.PreMatch.STARTING_GAME_PIECES; @@ -404,7 +503,7 @@ private void initNext() { Globals.affectedByDefenseValue = Constants.PostMatch.AFFECTED_BY_DEFENSE_NOT_SELECTED; Globals.isPractice = false; - // Increases the team number so that it auto fills for the next match correctly + // Increases the team number so that it will autofill for the next match correctly Globals.CurrentMatchNumber++; Intent GoToPreMatch = new Intent(SubmitData.this, PreMatch.class); diff --git a/app/src/main/res/drawable/internet.xml b/app/src/main/res/drawable/internet.xml new file mode 100644 index 00000000..c0408de6 --- /dev/null +++ b/app/src/main/res/drawable/internet.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/wifi_bar_0.xml b/app/src/main/res/drawable/wifi_bar_0.xml new file mode 100644 index 00000000..a3ceb954 --- /dev/null +++ b/app/src/main/res/drawable/wifi_bar_0.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/wifi_bar_1.xml b/app/src/main/res/drawable/wifi_bar_1.xml new file mode 100644 index 00000000..d1c28f74 --- /dev/null +++ b/app/src/main/res/drawable/wifi_bar_1.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/wifi_bar_2.xml b/app/src/main/res/drawable/wifi_bar_2.xml new file mode 100644 index 00000000..1bf8f255 --- /dev/null +++ b/app/src/main/res/drawable/wifi_bar_2.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/wifi_bar_3.xml b/app/src/main/res/drawable/wifi_bar_3.xml new file mode 100644 index 00000000..0b53c28a --- /dev/null +++ b/app/src/main/res/drawable/wifi_bar_3.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/wifi_bar_4.xml b/app/src/main/res/drawable/wifi_bar_4.xml new file mode 100644 index 00000000..6354f09d --- /dev/null +++ b/app/src/main/res/drawable/wifi_bar_4.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/submit_data.xml b/app/src/main/res/layout/submit_data.xml index 880ad340..9dcc460d 100644 --- a/app/src/main/res/layout/submit_data.xml +++ b/app/src/main/res/layout/submit_data.xml @@ -8,113 +8,149 @@ android:background="@color/cpr_bkgnd" tools:context=".activities.SubmitData"> - + + + + + - - - - - - + + + + - - - - - - - + + + + + + + + + + - - - - + android:layout_gravity="center_vertical" + android:orientation="horizontal"> + + + + + + - - + android:layout_height="match_parent" + android:layout_weight="1.5" + android:layout_marginTop="40dp" + android:orientation="vertical"> - - - - - + + + + + diff --git a/app/src/main/res/values/strings_submit.xml b/app/src/main/res/values/strings_submit.xml index 8a6089a3..ac65efd3 100644 --- a/app/src/main/res/values/strings_submit.xml +++ b/app/src/main/res/values/strings_submit.xml @@ -6,4 +6,7 @@ Quit app Are you sure you want to Quit the app? Quit + Wi-Fi signal strength + Internet access + Match Type: From 35cba602bdfe899db1ea759cbcf223f8e0cf16a7 Mon Sep 17 00:00:00 2001 From: Jeff Stammler Date: Fri, 3 Jul 2026 00:43:39 -0700 Subject: [PATCH 3/6] settings layout and applaunch addition --- .../scouting_app/activities/AppLaunch.java | 16 +- .../scouting_app/fragments/SettingsPage3.java | 37 +++ .../fragments/SettingsPagerAdapter.java | 11 +- app/src/main/res/layout/app_launch.xml | 10 + .../res/layout/fragment_settings_page3.xml | 219 ++++++++++++++++++ app/src/main/res/values/strings_settings.xml | 9 + 6 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java create mode 100644 app/src/main/res/layout/fragment_settings_page3.xml diff --git a/app/src/main/java/com/team3663/scouting_app/activities/AppLaunch.java b/app/src/main/java/com/team3663/scouting_app/activities/AppLaunch.java index 271cb7de..dfca5d3c 100644 --- a/app/src/main/java/com/team3663/scouting_app/activities/AppLaunch.java +++ b/app/src/main/java/com/team3663/scouting_app/activities/AppLaunch.java @@ -93,6 +93,10 @@ protected void onCreate(Bundle in_savedInstanceState) { return insets; }); + // Default to NO internet + appLaunchBinding.imageInternet.setVisibility(View.INVISIBLE); + Globals.network = new CPR_Network(this); + // Display app version PackageInfo pInfo; try { @@ -119,7 +123,7 @@ protected void onCreate(Bundle in_savedInstanceState) { boolean havePerms = !perm_list.isEmpty(); // If we have storage permissions go ahead and load the data. - // Otherwise, initiate getting permissions (which will then load the data afterwards) + // Otherwise, initiate getting permissions (which will then load the data afterward) if (havePerms) { Globals.baseStorageURI = Uri.parse(Globals.sp.getString(Constants.Prefs.STORAGE_URI, null)); initDataFiles(); @@ -129,9 +133,13 @@ protected void onCreate(Bundle in_savedInstanceState) { // While loading Matches, we messed with Globals.CurrentMatchType, so reset it Globals.CurrentMatchType = Constants.PreMatch.DEFAULT_MATCH_TYPE; - Globals.network = new CPR_Network(this); - boolean b = Globals.network.hasActiveInternet(); - } + + // check if we have access to the internet + if (Globals.network.hasActiveInternet()) + appLaunchBinding.imageInternet.setVisibility(View.VISIBLE); + else + appLaunchBinding.imageInternet.setVisibility(View.INVISIBLE); + } // ============================================================================================= // Function: initSettings diff --git a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java new file mode 100644 index 00000000..0c5e4ca3 --- /dev/null +++ b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java @@ -0,0 +1,37 @@ +package com.team3663.scouting_app.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.team3663.scouting_app.databinding.FragmentSettingsPage3Binding; + +public class SettingsPage3 extends Fragment { + public FragmentSettingsPage3Binding binding; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = FragmentSettingsPage3Binding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initColors(); + } + + // ============================================================================================= + // Function: initColors + // Description: Initialize the Color Palette field + // Parameters: void + // Output: void + // ============================================================================================= + private void initColors() { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPagerAdapter.java b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPagerAdapter.java index 645527f4..de1e85b7 100644 --- a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPagerAdapter.java +++ b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPagerAdapter.java @@ -8,6 +8,7 @@ public class SettingsPagerAdapter extends FragmentStateAdapter { private SettingsPage1 fragmentPage1; private SettingsPage2 fragmentPage2; + private SettingsPage3 fragmentPage3; public SettingsPagerAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); @@ -26,6 +27,10 @@ public Fragment createFragment(int position) { fragmentPage2 = new SettingsPage2(); frag = fragmentPage2; break; + case 2: + fragmentPage3 = new SettingsPage3(); + frag = fragmentPage3; + break; } return frag; @@ -33,14 +38,16 @@ public Fragment createFragment(int position) { @Override public int getItemCount() { - return 2; + return 3; } public SettingsPage1 getFragmentPage1() { return fragmentPage1; } - public SettingsPage2 getFragmentPage2() { return fragmentPage2; } + public SettingsPage3 getFragmentPage3() { + return fragmentPage3; + } } \ No newline at end of file diff --git a/app/src/main/res/layout/app_launch.xml b/app/src/main/res/layout/app_launch.xml index 3fad18cb..d9106f6f 100644 --- a/app/src/main/res/layout/app_launch.xml +++ b/app/src/main/res/layout/app_launch.xml @@ -20,6 +20,16 @@ app:layout_constraintEnd_toEndOf="parent" tools:ignore="MissingConstraints,VisualLintOverlap" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings_settings.xml b/app/src/main/res/values/strings_settings.xml index 7440e2eb..f14b2832 100644 --- a/app/src/main/res/values/strings_settings.xml +++ b/app/src/main/res/values/strings_settings.xml @@ -10,4 +10,13 @@ Field orientation: Shadow Mode: QR Code Size: + Wi-Fi Connectivity + Refresh + Choose Wi-Fi + Google Drive ID + SQL Server Name + SQL Database + SQL User + SQL Password + From d2f9c2fdd34e94c1f01891fde15349a42dca68dc Mon Sep 17 00:00:00 2001 From: Jeff Stammler Date: Sun, 5 Jul 2026 23:39:04 -0700 Subject: [PATCH 4/6] added network info into settings. --- .../scouting_app/activities/Settings.java | 30 +++- .../scouting_app/config/Constants.java | 5 + .../scouting_app/fragments/SettingsPage3.java | 140 +++++++++++++++++- 3 files changed, 166 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/team3663/scouting_app/activities/Settings.java b/app/src/main/java/com/team3663/scouting_app/activities/Settings.java index 7b191d5a..71e0d8c2 100644 --- a/app/src/main/java/com/team3663/scouting_app/activities/Settings.java +++ b/app/src/main/java/com/team3663/scouting_app/activities/Settings.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.text.Editable; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; @@ -14,9 +15,7 @@ import com.team3663.scouting_app.R; import com.team3663.scouting_app.config.*; import com.team3663.scouting_app.databinding.SettingsBinding; -import com.team3663.scouting_app.fragments.SettingsPagerAdapter; -import com.team3663.scouting_app.fragments.SettingsPage1; -import com.team3663.scouting_app.fragments.SettingsPage2; +import com.team3663.scouting_app.fragments.*; import com.google.android.material.tabs.TabLayoutMediator; public class Settings extends AppCompatActivity { @@ -70,8 +69,9 @@ private void SaveSettings() { Intent intent = new Intent(); SettingsPage1 fragmentPage1 = adapter.getFragmentPage1(); SettingsPage2 fragmentPage2 = adapter.getFragmentPage2(); + SettingsPage3 fragmentPage3 = adapter.getFragmentPage3(); - // Page1 & Page2 Settings + // Page1 Settings if (fragmentPage1 != null && fragmentPage1.binding != null) { int CompetitionId = Globals.CompetitionList.getCompetitionId(fragmentPage1.binding.spinnerCompetition.getSelectedItem().toString()); @@ -97,7 +97,7 @@ private void SaveSettings() { Globals.spe.putInt(Constants.Prefs.NUM_MATCHES, NumMatches); } - // UI Settings + // Page2 Settings if (fragmentPage2 != null && fragmentPage2.binding != null) { int ColorId = Globals.ColorList.getColorId(fragmentPage2.binding.spinnerColor.getSelectedItem().toString()); if (ColorId > 0) { @@ -114,6 +114,26 @@ private void SaveSettings() { Globals.spe.putInt(Constants.Prefs.PREF_ORIENTATION, Globals.CurrentFieldOrientationPos); } + // Page3 Settings + if (fragmentPage3 != null && fragmentPage3.binding != null) { + Editable userField; + + userField = fragmentPage3.binding.editGoogleDrive.getText(); + Globals.spe.putString(Constants.Prefs.GOOGLE_DRIVE, (userField != null) ? userField.toString() : ""); + + userField = fragmentPage3.binding.editServer.getText(); + Globals.spe.putString(Constants.Prefs.SQL_SERVER, (userField != null) ? userField.toString() : ""); + + userField = fragmentPage3.binding.editDatabase.getText(); + Globals.spe.putString(Constants.Prefs.SQL_DATABASE, (userField != null) ? userField.toString() : ""); + + userField = fragmentPage3.binding.editUser.getText(); + Globals.spe.putString(Constants.Prefs.SQL_USER, (userField != null) ? userField.toString() : ""); + + userField = fragmentPage3.binding.editPassword.getText(); + Globals.spe.putString(Constants.Prefs.SQL_PASSWORD, (userField != null) ? userField.toString() : ""); + } + Globals.spe.apply(); setResult(RESULT_OK, intent); finish(); diff --git a/app/src/main/java/com/team3663/scouting_app/config/Constants.java b/app/src/main/java/com/team3663/scouting_app/config/Constants.java index 52bb622b..9b689a85 100644 --- a/app/src/main/java/com/team3663/scouting_app/config/Constants.java +++ b/app/src/main/java/com/team3663/scouting_app/config/Constants.java @@ -121,6 +121,11 @@ public static class Prefs { public static final String STORAGE_URI = "StorageURI"; public static final String PREF_ORIENTATION = "PreferredFieldOrientation"; public static final String QR_SIZE = "PreferredQRSize"; + public static final String GOOGLE_DRIVE = "PreferredGoogleDrive"; + public static final String SQL_SERVER = "PreferredSqlServer"; + public static final String SQL_DATABASE = "PreferredSqlDatabase"; + public static final String SQL_USER = "PreferredSqlUser"; + public static final String SQL_PASSWORD = "PreferredSqlPassword"; } public static class Settings { diff --git a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java index 0c5e4ca3..801224f1 100644 --- a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java +++ b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java @@ -1,5 +1,14 @@ package com.team3663.scouting_app.fragments; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -8,10 +17,17 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.team3663.scouting_app.R; +import com.team3663.scouting_app.activities.QRCode; +import com.team3663.scouting_app.activities.SubmitData; +import com.team3663.scouting_app.config.Constants; +import com.team3663.scouting_app.config.Globals; import com.team3663.scouting_app.databinding.FragmentSettingsPage3Binding; public class SettingsPage3 extends Fragment { public FragmentSettingsPage3Binding binding; + private ConnectivityManager connectivityManager; + private ConnectivityManager.NetworkCallback networkCallback; @Nullable @Override @@ -23,15 +39,131 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - initColors(); + initNetwork(); + initChooseWifi(); + initRefresh(); + initFields(); } + @Override + public void onDestroyView() { + super.onDestroyView(); + // Important: Unregister to avoid memory leaks + if (connectivityManager != null && networkCallback != null) { + connectivityManager.unregisterNetworkCallback(networkCallback); + } + } + + // ============================================================================================= + // Function: initFields + // Description: Initialize the Fields on the fragment + // Parameters: void + // Output: void + // ============================================================================================= + private void initFields() { + binding.editGoogleDrive.setText(Globals.sp.getString(Constants.Prefs.GOOGLE_DRIVE, "")); + binding.editServer.setText(Globals.sp.getString(Constants.Prefs.SQL_SERVER, "")); + binding.editDatabase.setText(Globals.sp.getString(Constants.Prefs.SQL_DATABASE, "")); + binding.editUser.setText(Globals.sp.getString(Constants.Prefs.SQL_USER, "")); + binding.editPassword.setText(Globals.sp.getString(Constants.Prefs.SQL_PASSWORD, "")); + } + + // ============================================================================================= + // Function: initRefresh + // Description: Initialize the Refresh button + // Parameters: void + // Output: void + // ============================================================================================= + private void initRefresh() { + binding.butRefresh.setOnClickListener(view -> { + if (Globals.network.hasActiveInternet()) { + binding.imageInternet.setVisibility(View.VISIBLE); + } else { + binding.imageInternet.setVisibility(View.INVISIBLE); + } + }); + } + + // ============================================================================================= + // Function: initChooseWifi + // Description: Initialize the ChooseWifi button + // Parameters: void + // Output: void // ============================================================================================= - // Function: initColors - // Description: Initialize the Color Palette field + private void initChooseWifi() { + binding.butChoose.setOnClickListener(view -> { + Globals.network.pickWIFI(); + }); + } + + // ============================================================================================= + // Function: updateWifiIcon + // Description: Update the Wi-Fi signal icon with the correct signal strength level + // Parameters: in_level signal strength level + // Output: void + // ============================================================================================= + private void updateWifiIcon(int in_level) { + // You would typically swap icons here based on level + // 0: No signal, 1-4: Signal bars + switch (in_level) { + case 4: binding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_4); break; + case 3: binding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_3); break; + case 2: binding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_2); break; + case 1: binding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_1); break; + default: binding.imageWifiSignal.setImageResource(R.drawable.wifi_bar_0); break; + } + + // check if we have access to the internet + if (Globals.network.hasActiveInternet()) { + binding.imageInternet.setVisibility(View.VISIBLE); + } else { + binding.imageInternet.setVisibility(View.INVISIBLE); + } + } + + // ============================================================================================= + // Function: initNetwork + // Description: Initialize the network related fields and process // Parameters: void // Output: void // ============================================================================================= - private void initColors() { + private void initNetwork() { + // setup Wi-Fi monitoring + connectivityManager = (ConnectivityManager) requireContext().getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + + networkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onCapabilitiesChanged(@NonNull Network in_network, @NonNull NetworkCapabilities in_capabilities) { + int rssi = 0; + + // On API 31+, WifiInfo is part of the capabilities (requires location permission) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + WifiInfo wifiInfo = (WifiInfo) in_capabilities.getTransportInfo(); + if (wifiInfo != null) rssi = wifiInfo.getRssi(); + } else { + // Fallback for API 30 + WifiManager wm = (WifiManager) requireContext().getApplicationContext().getSystemService(Context.WIFI_SERVICE); + rssi = wm.getConnectionInfo().getRssi(); + } + + // Convert RSSI to a signal level (0 to 4) + int level = WifiManager.calculateSignalLevel(rssi, 5); + + // Update UI on the main thread + requireActivity().runOnUiThread(() -> updateWifiIcon(level)); + } + + @Override + public void onLost(@NonNull Network in_network) { + // Signal lost or Wi-Fi turned off + requireActivity().runOnUiThread(() -> updateWifiIcon(-1)); + } + }; + + connectivityManager.registerNetworkCallback(networkRequest, networkCallback); } } \ No newline at end of file From feeb7a6d84e41d3d9b27bff5dbb6092615b51170 Mon Sep 17 00:00:00 2001 From: Jeff Stammler Date: Mon, 6 Jul 2026 12:04:56 -0700 Subject: [PATCH 5/6] fixed comment blocks and coding standards --- .../scouting_app/fragments/SettingsPage3.java | 8 +- .../scouting_app/utility/CPR_Network.java | 126 ++++++++++-------- 2 files changed, 69 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java index 801224f1..1b2a7ade 100644 --- a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java +++ b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java @@ -1,7 +1,6 @@ package com.team3663.scouting_app.fragments; import android.content.Context; -import android.content.Intent; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; @@ -18,8 +17,6 @@ import androidx.fragment.app.Fragment; import com.team3663.scouting_app.R; -import com.team3663.scouting_app.activities.QRCode; -import com.team3663.scouting_app.activities.SubmitData; import com.team3663.scouting_app.config.Constants; import com.team3663.scouting_app.config.Globals; import com.team3663.scouting_app.databinding.FragmentSettingsPage3Binding; @@ -29,7 +26,6 @@ public class SettingsPage3 extends Fragment { private ConnectivityManager connectivityManager; private ConnectivityManager.NetworkCallback networkCallback; - @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentSettingsPage3Binding.inflate(inflater, container, false); @@ -91,9 +87,7 @@ private void initRefresh() { // Output: void // ============================================================================================= private void initChooseWifi() { - binding.butChoose.setOnClickListener(view -> { - Globals.network.pickWIFI(); - }); + binding.butChoose.setOnClickListener(view -> Globals.network.pickWIFI()); } // ============================================================================================= diff --git a/app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java b/app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java index d92b97db..e42c8f61 100644 --- a/app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java +++ b/app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java @@ -17,81 +17,80 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -/** - * Checks whether a remote host:port is reachable. - * - *

Two-step check: - *

    - *
  1. Is there any active network with internet capability? (cheap, no I/O)
  2. - *
  3. Can we open a TCP socket to the given host:port? (blocking, runs off the UI thread)
  4. - *
- * - *

Required manifest permissions: - *

- *   <uses-permission android:name="android.permission.INTERNET" />
- *   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- * 
- */public class CPR_Network { +// ============================================================================================= +// Class: CPR_Network +// Description: Custom class to handle network related tasks. +// ============================================================================================= +public class CPR_Network { + // ============================================================================================= + // Class Globals + // ============================================================================================= + private final Context appContext; + private final ExecutorService executor; + private final Handler mainHandler; - /** - * Why a reachability check ended the way it did. - */ + // ============================================================================================= + // Why a reachability check ended the way it did. + // ============================================================================================= public enum Result { REACHABLE, // socket connected successfully NO_NETWORK, // device has no active internet-capable network HOST_UNREACHABLE // network exists, but host:port could not be reached } - /** - * Delivered on the main thread. - */ public interface Callback { void onResult(@NonNull Result result); } - private final Context appContext; - private final ExecutorService executor; - private final Handler mainHandler; - - public CPR_Network(@NonNull Context context) { + // Constructor: create the new Network object + public CPR_Network(@NonNull Context in_context) { // Use the application context to avoid leaking an Activity. - this.appContext = context.getApplicationContext(); + this.appContext = in_context.getApplicationContext(); this.executor = Executors.newCachedThreadPool(); this.mainHandler = new Handler(Looper.getMainLooper()); } - /** - * Asynchronously checks reachability. The callback is invoked on the main thread. - * - * @param host hostname or IP of the remote machine - * @param port port the remote service listens on (e.g. 8080, 443, 22) - * @param timeoutMs socket connect timeout in milliseconds - * @param callback invoked on the main thread with the result - */ - public void check(@NonNull String host, int port, int timeoutMs, @NonNull Callback callback) { + // ============================================================================================= + // Function: check (step 1) + // Description: Asynchronously checks reachability. The callback is invoked on the main thread. + // Parameters: in_host hostname or IP of the remote machine + // in_port port the remote service listens on (e.g. 8080, 443, 22) + // in_timeoutMs socket connect timeout in milliseconds + // in_callback invoked on the main thread with the result + // Output: void + // ============================================================================================= + public void check(@NonNull String in_host, int in_port, int in_timeoutMs, @NonNull Callback in_callback) { executor.execute(() -> { - Result result = checkBlocking(host, port, timeoutMs); - mainHandler.post(() -> callback.onResult(result)); + Result result = checkBlocking(in_host, in_port, in_timeoutMs); + mainHandler.post(() -> in_callback.onResult(result)); }); } - /** - * Synchronous check. Must NOT be called on the main thread (blocks on socket I/O). - * Exposed for use inside your own background threads / coroutines. - */ + // ============================================================================================= + // Function: checkBlocking + // Description: Synchronous check. Must NOT be called on the main thread (blocks on socket I/O). + // Exposed for use inside your own background threads / coroutines. + // Parameters: in_host hostname or IP of the remote machine + // in_port port the remote service listens on (e.g. 8080, 443, 22) + // in_timeoutMs socket connect timeout in milliseconds + // Output: void + // ============================================================================================= @NonNull - public Result checkBlocking(@NonNull String host, int port, int timeoutMs) { + public Result checkBlocking(@NonNull String in_host, int in_port, int in_timeoutMs) { if (!hasActiveInternet()) { return Result.NO_NETWORK; } - return isHostReachable(host, port, timeoutMs) + return isHostReachable(in_host, in_port, in_timeoutMs) ? Result.REACHABLE : Result.HOST_UNREACHABLE; } - /** - * Step 1: is there an active, internet-capable network? - */ + // ============================================================================================= + // Function: hasActiveInternet + // Description: is there an active, internet-capable network? + // Parameters: void + // Output: void + // ============================================================================================= public boolean hasActiveInternet() { ConnectivityManager cm = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -107,12 +106,17 @@ public boolean hasActiveInternet() { && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); } - /** - * Step 2: can we open a TCP connection to host:port? - */ - private boolean isHostReachable(@NonNull String host, int port, int timeoutMs) { + // ============================================================================================= + // Function: isHostReachable (step 2) + // Description: can we open a TCP connection to host:port? + // Parameters: in_host hostname or IP of the remote machine + // in_port port the remote service listens on (e.g. 8080, 443, 22) + // in_timeoutMs socket connect timeout in milliseconds + // Output: void + // ============================================================================================= + private boolean isHostReachable(@NonNull String in_host, int in_port, int in_timeoutMs) { try (Socket socket = new Socket()) { - socket.connect(new InetSocketAddress(host, port), timeoutMs); + socket.connect(new InetSocketAddress(in_host, in_port), in_timeoutMs); return true; } catch (IOException e) { // unreachable, connection refused, or timed out @@ -120,16 +124,22 @@ private boolean isHostReachable(@NonNull String host, int port, int timeoutMs) { } } - /** - * Call when you're done (e.g. in onDestroy) to release the thread pool. - */ + // ============================================================================================= + // Function: shutdown + // Description: Call when you're done (e.g. in onDestroy) to release the thread pool. + // Parameters: void + // Output: void + // ============================================================================================= public void shutdown() { executor.shutdownNow(); } - /** - * Choose new wifi to connect to - */ + // ============================================================================================= + // Function: pickWIFI + // Description: Choose new wifi to connect to + // Parameters: void + // Output: void + // ============================================================================================= public void pickWIFI() { try { // Try quick Wi-Fi panel From b03620da0952604591b9a59fdcb5b4fd7a0d1c8c Mon Sep 17 00:00:00 2001 From: Jeff Stammler Date: Mon, 6 Jul 2026 14:18:11 -0700 Subject: [PATCH 6/6] added defaults for network fields --- .../team3663/scouting_app/fragments/SettingsPage3.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java index 1b2a7ade..db706230 100644 --- a/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java +++ b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java @@ -57,11 +57,11 @@ public void onDestroyView() { // Output: void // ============================================================================================= private void initFields() { - binding.editGoogleDrive.setText(Globals.sp.getString(Constants.Prefs.GOOGLE_DRIVE, "")); - binding.editServer.setText(Globals.sp.getString(Constants.Prefs.SQL_SERVER, "")); - binding.editDatabase.setText(Globals.sp.getString(Constants.Prefs.SQL_DATABASE, "")); - binding.editUser.setText(Globals.sp.getString(Constants.Prefs.SQL_USER, "")); - binding.editPassword.setText(Globals.sp.getString(Constants.Prefs.SQL_PASSWORD, "")); + binding.editGoogleDrive.setText(Globals.sp.getString(Constants.Prefs.GOOGLE_DRIVE, "1DB3Dg9N-4zbOxV28Sor-YJrWuoK_OfY8")); + binding.editServer.setText(Globals.sp.getString(Constants.Prefs.SQL_SERVER, "mssql01.cpr3663.io")); + binding.editDatabase.setText(Globals.sp.getString(Constants.Prefs.SQL_DATABASE, "CPR_Scouting_2025")); + binding.editUser.setText(Globals.sp.getString(Constants.Prefs.SQL_USER, "CPR_Tablet")); + binding.editPassword.setText(Globals.sp.getString(Constants.Prefs.SQL_PASSWORD, "Aa98o1nTlHb4sg2u0YB2eNHxVfU4n17z")); } // =============================================================================================