diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index fc956af..94d25b2 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 777f319..d5ce06d 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 72a1dab..dfca5d3 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; @@ -92,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 { @@ -118,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(); @@ -128,7 +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; - } + + // 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/activities/Settings.java b/app/src/main/java/com/team3663/scouting_app/activities/Settings.java index 7b191d5..71e0d8c 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/activities/SubmitData.java b/app/src/main/java/com/team3663/scouting_app/activities/SubmitData.java index 1f01cd8..17a322b 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; @@ -377,6 +476,7 @@ private void initQuit() { .setPositiveButton(getString(R.string.submit_alert_quit_positive), (dialog, which) -> { SubmitData.this.finishAffinity(); stopLockTask(); + Globals.network.shutdown(); System.exit(0); }) @@ -403,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/java/com/team3663/scouting_app/config/Constants.java b/app/src/main/java/com/team3663/scouting_app/config/Constants.java index 52bb622..9b689a8 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/config/Globals.java b/app/src/main/java/com/team3663/scouting_app/config/Globals.java index 25e321f..d2ca9be 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/fragments/SettingsPage3.java b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java new file mode 100644 index 0000000..db70623 --- /dev/null +++ b/app/src/main/java/com/team3663/scouting_app/fragments/SettingsPage3.java @@ -0,0 +1,163 @@ +package com.team3663.scouting_app.fragments; + +import android.content.Context; +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; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.team3663.scouting_app.R; +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; + + @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); + 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, "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")); + } + + // ============================================================================================= + // 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 + // ============================================================================================= + 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 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 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 645527f..de1e85b 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/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 0000000..e42c8f6 --- /dev/null +++ b/app/src/main/java/com/team3663/scouting_app/utility/CPR_Network.java @@ -0,0 +1,156 @@ +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; + +// ============================================================================================= +// 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. + // ============================================================================================= + 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 + } + + public interface Callback { + void onResult(@NonNull Result result); + } + + // Constructor: create the new Network object + public CPR_Network(@NonNull Context in_context) { + // Use the application context to avoid leaking an Activity. + this.appContext = in_context.getApplicationContext(); + this.executor = Executors.newCachedThreadPool(); + this.mainHandler = new Handler(Looper.getMainLooper()); + } + + // ============================================================================================= + // 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(in_host, in_port, in_timeoutMs); + mainHandler.post(() -> in_callback.onResult(result)); + }); + } + + // ============================================================================================= + // 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 in_host, int in_port, int in_timeoutMs) { + if (!hasActiveInternet()) { + return Result.NO_NETWORK; + } + return isHostReachable(in_host, in_port, in_timeoutMs) + ? Result.REACHABLE + : Result.HOST_UNREACHABLE; + } + + // ============================================================================================= + // 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); + 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); + } + + // ============================================================================================= + // 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(in_host, in_port), in_timeoutMs); + return true; + } catch (IOException e) { + // unreachable, connection refused, or timed out + return false; + } + } + + // ============================================================================================= + // 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(); + } + + // ============================================================================================= + // Function: pickWIFI + // Description: Choose new wifi to connect to + // Parameters: void + // Output: void + // ============================================================================================= + 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); + } + } +} diff --git a/app/src/main/res/drawable/internet.xml b/app/src/main/res/drawable/internet.xml new file mode 100644 index 0000000..c0408de --- /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 0000000..a3ceb95 --- /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 0000000..d1c28f7 --- /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 0000000..1bf8f25 --- /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 0000000..0b53c28 --- /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 0000000..6354f09 --- /dev/null +++ b/app/src/main/res/drawable/wifi_bar_4.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/app_launch.xml b/app/src/main/res/layout/app_launch.xml index 3fad18c..d9106f6 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/layout/submit_data.xml b/app/src/main/res/layout/submit_data.xml index 880ad34..9dcc460 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_settings.xml b/app/src/main/res/values/strings_settings.xml index 7440e2e..f14b283 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 + diff --git a/app/src/main/res/values/strings_submit.xml b/app/src/main/res/values/strings_submit.xml index 8a6089a..ac65efd 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: