浏览代码

* Display network address on main activity
* Export hostname corresponding to Wifi address and Wifi IP address
* Add configuration manager
* Allow user to select a different port

isundil 4 年之前
父节点
当前提交
7f1cb0b975

+ 18 - 0
app/release/output-metadata.json

@@ -0,0 +1,18 @@
+{
+  "version": 2,
+  "artifactType": {
+    "type": "APK",
+    "kind": "Directory"
+  },
+  "applicationId": "info.knacki.prometheusandroidexporter",
+  "variantName": "processReleaseResources",
+  "elements": [
+    {
+      "type": "SINGLE",
+      "filters": [],
+      "versionCode": 1,
+      "versionName": "1.0",
+      "outputFile": "app-release.apk"
+    }
+  ]
+}

+ 4 - 0
app/src/main/AndroidManifest.xml

@@ -1,8 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="info.knacki.prometheusandroidexporter">
+
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.GET_TASKS"/>
 
     <application
         android:allowBackup="true"

+ 13 - 6
app/src/main/java/info/knacki/prometheusandroidexporter/CollectorManager.java

@@ -19,13 +19,20 @@ public class CollectorManager {
         fCollectors.put(collector.getClass().getName(), collector);
     }
 
-    public void tick(Context ctx) {
-        HashMap<String, Collection<CollectorType.CollectorValue>> values = new HashMap<>();
-
-        for (HashMap.Entry<String, ICollector> i : fCollectors.entrySet()) {
-            values.put(i.getKey(), i.getValue().ReadValues(ctx));
+    public void tick(final Context ctx) {
+        synchronized (this) {
+            (new Thread() {
+                @Override
+                public void run() {
+                    HashMap<String, Collection<CollectorType.CollectorValue>> values = new HashMap<>();
+
+                    for (HashMap.Entry<String, ICollector> i : fCollectors.entrySet()) {
+                        values.put(i.getKey(), i.getValue().ReadValues(ctx));
+                    }
+                    fValues = values;
+                }
+            }).start();
         }
-        fValues = values;
     }
 
     public static CollectorManager GetInstance() {

+ 16 - 3
app/src/main/java/info/knacki/prometheusandroidexporter/HttpService.java

@@ -1,5 +1,7 @@
 package info.knacki.prometheusandroidexporter;
 
+import android.content.Context;
+
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
 import com.sun.net.httpserver.HttpServer;
@@ -13,17 +15,19 @@ import java.util.concurrent.Executors;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import info.knacki.prometheusandroidexporter.collector.NetworkCollector;
+import info.knacki.prometheusandroidexporter.configuration.ConfigurationManager;
+
 public class HttpService {
     public final static Logger gLogger = Logger.getLogger(HttpService.class.getName());
-    public static final short PORT = 9100;
     private static final HttpService gService = new HttpService();
     private HttpServer fServer = null;
 
-    public static void Register() throws IOException {
+    public static void Register(Context ctx) throws IOException {
         if (gService.fServer == null) {
             synchronized (HttpService.class) {
                 if (gService.fServer == null) {
-                    gService.fServer = HttpServer.create(new InetSocketAddress(PORT), 0);
+                    gService.fServer = HttpServer.create(new InetSocketAddress(ConfigurationManager.GetInstance(ctx).GetConfiguration().GetPort()), 0);
                     gService.fServer.setExecutor(Executors.newCachedThreadPool());
                     gService.fServer.createContext("/", new HttpHandler() {
                         @Override
@@ -53,6 +57,15 @@ public class HttpService {
         }
     }
 
+    public static String GetBoundAddr(Context ctx) {
+        if (gService.fServer == null)
+            return null;
+        return NetworkCollector.GetWifiIPAddress(ctx) +
+                ":" +
+                gService.fServer.getAddress().getPort() +
+                "/";
+    }
+
     public static void Stop() {
         if (gService.fServer != null) {
             synchronized (HttpService.class) {

+ 87 - 5
app/src/main/java/info/knacki/prometheusandroidexporter/MainActivity.java

@@ -3,14 +3,24 @@ package info.knacki.prometheusandroidexporter;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.TextWatcher;
 import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
 import android.widget.TextView;
 
 import androidx.appcompat.app.AppCompatActivity;
 
 import java.util.List;
 
-public class MainActivity extends AppCompatActivity {
+import info.knacki.prometheusandroidexporter.configuration.ConfigurationManager;
+import info.knacki.prometheusandroidexporter.receiver.NetworkReceiver;
+
+public class MainActivity extends AppCompatActivity implements NetworkReceiver.AdditionalReceiver {
+    private ConfigurationManager.Configuration fConfig;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -30,9 +40,57 @@ public class MainActivity extends AppCompatActivity {
                 UpdateUiState();
             }
         });
+
+        fConfig = ConfigurationManager.GetInstance(this).GetConfiguration();
+
+        final Button bt_save = findViewById(R.id.bt_saveconfiguration);
+        bt_save.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                ConfigurationManager.Save(MainActivity.this, fConfig);
+                MainService.StopService(MainActivity.this);
+                MainService.StartService(MainActivity.this);
+                bt_save.setEnabled(!fConfig.equals(ConfigurationManager.GetInstance(MainActivity.this).GetConfiguration()));
+            }
+        });
+
+        EditText input_port = findViewById(R.id.input_port);
+        input_port.setText(Short.toString(fConfig.GetPort()));
+        input_port.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+            @Override
+            public void afterTextChanged(Editable s) {
+                short port;
+                try {
+                    port = Short.parseShort(s.toString());
+                }
+                catch (NumberFormatException e) {
+                    return;
+                }
+                fConfig.SetPort(port);
+                bt_save.setEnabled(!fConfig.equals(ConfigurationManager.GetInstance(MainActivity.this).GetConfiguration()));
+            }
+        });
         UpdateUiState();
     }
 
+    @Override
+    protected void onResume() {
+        super.onResume();
+        NetworkReceiver.SetAdditionalReceiver(this);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        NetworkReceiver.SetAdditionalReceiver(null);
+    }
+
     private boolean IsServiceRunning() {
         final ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
         final List<ActivityManager.RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);
@@ -45,10 +103,34 @@ public class MainActivity extends AppCompatActivity {
         return false;
     }
 
+    private String GetVerboseIp() {
+        String localIP = HttpService.GetBoundAddr(getApplicationContext());
+        if (localIP == null)
+            return " - waiting for network";
+        return " on " + localIP;
+    }
+
     private void UpdateUiState() {
-        boolean running = IsServiceRunning();
-        findViewById(R.id.bt_killservice).setEnabled(running);
-        findViewById(R.id.bt_startservice).setEnabled(!running);
-        ((TextView) findViewById(R.id.txt_servicestate)).setText(running ? "running" : "stopped");
+        (new Thread() {
+            @Override
+            public synchronized void run() {
+                final boolean running = IsServiceRunning();
+                final String ip = GetVerboseIp();
+
+                (new Handler(Looper.getMainLooper())).post(new Runnable() {
+                    @Override
+                    public void run() {
+                        findViewById(R.id.bt_killservice).setEnabled(running);
+                        findViewById(R.id.bt_startservice).setEnabled(!running);
+                        ((TextView) findViewById(R.id.txt_servicestate)).setText(running ? ("running" + ip) : "stopped");
+                    }
+                });
+            }
+        }).start();
+    }
+
+    @Override
+    public void Update() {
+        UpdateUiState();
     }
 }

+ 22 - 6
app/src/main/java/info/knacki/prometheusandroidexporter/MainService.java

@@ -1,7 +1,7 @@
 package info.knacki.prometheusandroidexporter;
 
-import android.app.ActivityManager;
 import android.app.Notification;
+import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -11,6 +11,7 @@ import android.os.Build;
 import android.os.IBinder;
 
 import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
 import androidx.work.PeriodicWorkRequest;
 import androidx.work.WorkManager;
 
@@ -31,8 +32,9 @@ import info.knacki.prometheusandroidexporter.receiver.NetworkReceiver;
 public class MainService extends Service {
     public final static Logger log = Logger.getLogger(MainService.class.getName());
     private final Binder fLocalBinder = this.new Binder();
-    private Notification.Builder fNotif;
+    private NotificationCompat.Builder fNotif;
     private boolean fPaused;
+    private static final int gNotifId = 1;
 
     public MainService() {
     }
@@ -62,6 +64,13 @@ public class MainService extends Service {
         NetworkReceiver.Register(this);
     }
 
+    @Override
+    public void onDestroy() {
+        Pause();
+        super.onDestroy();
+        ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(gNotifId);
+    }
+
     public class Binder extends android.os.Binder {
         public MainService GetService() {
             return MainService.this;
@@ -94,7 +103,7 @@ public class MainService extends Service {
                 if (fPaused) {
                     log.warning("RESUME!~~~~");
                     try {
-                        HttpService.Register();
+                        HttpService.Register(this);
                     }
                     catch (IOException e) {
                         log.log(Level.SEVERE, "Cannot start http server: ", e);
@@ -125,6 +134,7 @@ public class MainService extends Service {
     private void ScheduleCollection() {
         PeriodicWorkRequest job = new PeriodicWorkRequest.Builder(CollectorWorker.class, 15, TimeUnit.MINUTES).build();
         WorkManager.getInstance(this).enqueue(job);
+        CollectorManager.GetInstance().tick(getApplicationContext());
     }
 
     private void UpdateIcon(int icon, String text) {
@@ -132,16 +142,22 @@ public class MainService extends Service {
                 .setSmallIcon(icon)
                 .setContentText(text)
                 .build();
+
         notif.flags = Notification.FLAG_ONGOING_EVENT;
-        ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(1, notif);
+        ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(gNotifId, notif);
     }
 
     private void NotifyIcon() {
-        fNotif = new Notification.Builder(this)
+        String notifChannelId = getPackageName();
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            NotificationChannel channel = new NotificationChannel(notifChannelId, MainService.class.getName(), NotificationManager.IMPORTANCE_DEFAULT);
+            ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).createNotificationChannel(channel);
+        }
+        fNotif = new NotificationCompat.Builder(this, notifChannelId)
             .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT))
             .setContentTitle("Prometheus Android Exporter");
         Notification notif = fNotif.build();
         UpdateIcon(R.mipmap.ic_launcher_idle, "Service booting");
-        startForeground(1, notif);
+        startForeground(gNotifId, notif);
     }
 }

+ 41 - 1
app/src/main/java/info/knacki/prometheusandroidexporter/collector/CPUCollector.java

@@ -2,23 +2,63 @@ package info.knacki.prometheusandroidexporter.collector;
 
 import android.content.Context;
 import android.os.Build;
+import android.os.CpuUsageInfo;
 import android.os.HardwarePropertiesManager;
 
 import androidx.annotation.RequiresApi;
 
 import java.util.ArrayDeque;
 import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import info.knacki.prometheusandroidexporter.CollectorType;
 import info.knacki.prometheusandroidexporter.ICollector;
 
 @RequiresApi(api = Build.VERSION_CODES.N)
 public class CPUCollector implements ICollector {
+    public final static Logger log = Logger.getLogger(CPUCollector.class.getName());
+    private final CollectorType fCpuTemp = new CollectorType("cpu_temp", "cpu temperature (celsius)", CollectorType.Type.GAUGE);
+    private final CollectorType fFanSpeed = new CollectorType("fan_speed_rpm", "fan speed (RPM)", CollectorType.Type.GAUGE);
+    private final CollectorType fCpuUsage = new CollectorType("cpu_active_usage", "CPU current usage", CollectorType.Type.GAUGE);
+    private final CollectorType fCpuMax = new CollectorType("cpu_max_usage", "CPU max Usage", CollectorType.Type.GAUGE);
+
+    private void AddFanSpeed(HardwarePropertiesManager hardManager, Collection<CollectorType.CollectorValue> result) {
+        int index = 0;
+        for (float i : hardManager.getFanSpeeds()) {
+            result.add(fFanSpeed.new CollectorValue().SetValue(i).AddParameter("fan", Integer.toString(index++)));
+        }
+    }
+
+    private void AddCpuTemperature(HardwarePropertiesManager hardManager, Collection<CollectorType.CollectorValue> result) {
+        int index = 0;
+        for (float i : hardManager.getDeviceTemperatures(HardwarePropertiesManager.DEVICE_TEMPERATURE_CPU, HardwarePropertiesManager.TEMPERATURE_CURRENT)) {
+            result.add(fCpuTemp.new CollectorValue().SetValue(i).AddParameter("cpu", Integer.toString(index++)));
+        }
+    }
+
+    private void AddCpuUsage(HardwarePropertiesManager hardManager, Collection<CollectorType.CollectorValue> result) {
+        int index = 0;
+        for (CpuUsageInfo i : hardManager.getCpuUsages()) {
+            result.add(fCpuUsage.new CollectorValue().SetValue(i.getActive()).AddParameter("cpu", Integer.toString(index)));
+            result.add(fCpuMax.new CollectorValue().SetValue(i.getTotal()).AddParameter("cpu", Integer.toString(index)));
+            ++index;
+        }
+    }
+
     @Override
     public Collection<CollectorType.CollectorValue> ReadValues(Context ctx) {
         ArrayDeque<CollectorType.CollectorValue> result = new ArrayDeque<>();
         HardwarePropertiesManager hardwareManager = ctx.getSystemService(HardwarePropertiesManager.class);
-        // FIXME monitor CPU time, cpu temp
+        if (hardwareManager == null)
+            return result;
+        try {
+            AddCpuTemperature(hardwareManager, result);
+            AddFanSpeed(hardwareManager, result);
+            AddCpuUsage(hardwareManager, result);
+        } catch (SecurityException e) {
+            log.log(Level.WARNING, "Cannot read CPU statistics", e);
+        }
         return result;
     }
 }

+ 46 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/collector/NetworkCollector.java

@@ -2,7 +2,12 @@ package info.knacki.prometheusandroidexporter.collector;
 
 import android.content.Context;
 import android.net.TrafficStats;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
 
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayDeque;
 import java.util.Collection;
 
@@ -12,6 +17,41 @@ import info.knacki.prometheusandroidexporter.ICollector;
 public class NetworkCollector implements ICollector {
     private final CollectorType netRxBytes = new CollectorType("net_received", "received bytes", CollectorType.Type.COUNTER);
     private final CollectorType netTxBytes = new CollectorType("net_transmit", "sent bytes", CollectorType.Type.COUNTER);
+    private final CollectorType netWifiAddr = new CollectorType("net_wifi_addr", "Wifi address", CollectorType.Type.GAUGE);
+    private final CollectorType netHostname = new CollectorType("net_wifi_Hostname", "Network hostname", CollectorType.Type.GAUGE);
+
+    public static String GetWifiIPAddress(Context ctx) {
+        WifiInfo winfo = GetWifiInfos(ctx);
+        if (winfo != null) {
+            try {
+                byte[] ipBytes = BigInteger.valueOf(winfo.getIpAddress()).toByteArray();
+                return InetAddress.getByAddress(new byte[]{ipBytes[3], ipBytes[2], ipBytes[1], ipBytes[0]}).getHostAddress();
+            } catch (UnknownHostException e) {
+                return "Unknown";
+            }
+        }
+        return "Unknown";
+    }
+
+    private static WifiInfo GetWifiInfos(Context ctx) {
+        WifiManager wman = (WifiManager) ctx.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+        if (wman != null && wman.getConnectionInfo() != null)
+            return wman.getConnectionInfo();
+        return null;
+    }
+
+    public static String GetWifiHostname(Context ctx) {
+        WifiInfo winfo = GetWifiInfos(ctx);
+        if (winfo != null) {
+            try {
+                byte[] ipBytes = BigInteger.valueOf(winfo.getIpAddress()).toByteArray();
+                return InetAddress.getByAddress(new byte[]{ipBytes[3], ipBytes[2], ipBytes[1], ipBytes[0]}).getHostName();
+            } catch (UnknownHostException e) {
+                return "Unknown";
+            }
+        }
+        return "Unknown";
+    }
 
     @Override
     public Collection<CollectorType.CollectorValue> ReadValues(Context ctx) {
@@ -29,6 +69,12 @@ public class NetworkCollector implements ICollector {
         result.add(netTxBytes.new CollectorValue()
                 .SetValue(TrafficStats.getTotalTxBytes())
                 .AddParameter("source", "total"));
+        result.add(netWifiAddr.new CollectorValue()
+                .SetValue(1)
+                .AddParameter("ip_addr", GetWifiIPAddress(ctx.getApplicationContext())));
+        result.add(netHostname.new CollectorValue()
+                .SetValue(1)
+                .AddParameter("ip_addr", GetWifiHostname(ctx.getApplicationContext())));
         return result;
     }
 }

+ 0 - 2
app/src/main/java/info/knacki/prometheusandroidexporter/collector/ProcessCollector.java

@@ -5,7 +5,6 @@ import android.content.Context;
 
 import java.util.ArrayDeque;
 import java.util.Collection;
-import java.util.List;
 
 import info.knacki.prometheusandroidexporter.CollectorType;
 import info.knacki.prometheusandroidexporter.ICollector;
@@ -15,7 +14,6 @@ public class ProcessCollector implements ICollector {
         public final int fProcessCount;
         public final int fRunningAppProcess;
         public final int fTaskCount;
-        //public final int fProcessCount;
 
         Data(Context ctx) {
             final ActivityManager activityManager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);

+ 5 - 5
app/src/main/java/info/knacki/prometheusandroidexporter/collector/StorageCollector.java

@@ -17,7 +17,7 @@ import info.knacki.prometheusandroidexporter.ICollector;
 public class StorageCollector implements ICollector {
     private static final CollectorType externalStorageAvailable = new CollectorType("fs_external_available_bool", "boolean value indicating if external storage is available", CollectorType.Type.GAUGE);
     private static final CollectorType availableStorage = new CollectorType("fs_available_storage_bytes", "available storage", CollectorType.Type.GAUGE);
-    private static final CollectorType usedStorage = new CollectorType("fs_total_storage_bytes", "total storage", CollectorType.Type.GAUGE);
+    private static final CollectorType totalStorage = new CollectorType("fs_total_storage_bytes", "total storage", CollectorType.Type.GAUGE);
 
     private boolean ExternalStorageAvailable() {
         return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) || Environment.isExternalStorageEmulated();
@@ -38,10 +38,10 @@ public class StorageCollector implements ICollector {
         final StatFs internalStats = GetInternalStorageStats();
 
         result.add(externalStorageAvailable.new CollectorValue().SetValue(ExternalStorageAvailable() ? 1 : 0));
-        result.add(availableStorage.new CollectorValue().SetValue(externalStats.getTotalBytes()).AddParameter("source", "external"));
-        result.add(availableStorage.new CollectorValue().SetValue(internalStats.getTotalBytes()).AddParameter("source", "internal"));
-        result.add(usedStorage.new CollectorValue().SetValue(externalStats.getAvailableBytes()).AddParameter("source", "external"));
-        result.add(usedStorage.new CollectorValue().SetValue(internalStats.getAvailableBytes()).AddParameter("source", "internal"));
+        result.add(availableStorage.new CollectorValue().SetValue(externalStats.getAvailableBytes()).AddParameter("source", "external"));
+        result.add(availableStorage.new CollectorValue().SetValue(internalStats.getAvailableBytes()).AddParameter("source", "internal"));
+        result.add(totalStorage.new CollectorValue().SetValue(externalStats.getTotalBytes()).AddParameter("source", "external"));
+        result.add(totalStorage.new CollectorValue().SetValue(internalStats.getTotalBytes()).AddParameter("source", "internal"));
         return result;
     }
 }

+ 66 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/configuration/ConfigurationManager.java

@@ -0,0 +1,66 @@
+package info.knacki.prometheusandroidexporter.configuration;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.NonNull;
+
+public class ConfigurationManager {
+    public static class Configuration {
+        private short fPort = 9100;
+
+        Configuration() {
+        }
+
+        public short GetPort() { return fPort; }
+        public void SetPort(short value) { fPort = value; }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Configuration that = (Configuration) o;
+            return fPort == that.fPort;
+        }
+
+        @NonNull
+        protected Configuration Clone() {
+            Configuration result = new Configuration();
+            result.fPort = fPort;
+            return result;
+        }
+    }
+
+    public static final String CONFIG_PORT = "port";
+
+    private static ConfigurationManager gInstance;
+    private Configuration fSavedConfiguration = new Configuration();
+
+    private ConfigurationManager(Context ctx) {
+        SharedPreferences prefs = ctx.getSharedPreferences(ConfigurationManager.class.getName(), Context.MODE_PRIVATE);
+        fSavedConfiguration.fPort = (short) prefs.getInt(CONFIG_PORT, fSavedConfiguration.fPort);
+    }
+
+    public static ConfigurationManager GetInstance(Context ctx) {
+        synchronized (ConfigurationManager.class) {
+            if (gInstance == null) {
+                synchronized (ConfigurationManager.class) {
+                    gInstance = new ConfigurationManager(ctx);
+                }
+            }
+        }
+        return gInstance;
+    }
+
+    public Configuration GetConfiguration() {
+        return fSavedConfiguration.Clone();
+    }
+
+    public static void Save(Context ctx, Configuration config) {
+        GetInstance(ctx);
+        SharedPreferences.Editor prefs = ctx.getSharedPreferences(ConfigurationManager.class.getName(), Context.MODE_PRIVATE).edit();
+        prefs.putInt(CONFIG_PORT, config.fPort);
+        prefs.apply();
+        GetInstance(ctx).fSavedConfiguration = config.Clone();
+    }
+}

+ 26 - 5
app/src/main/java/info/knacki/prometheusandroidexporter/receiver/NetworkReceiver.java

@@ -17,12 +17,23 @@ import info.knacki.prometheusandroidexporter.MainService;
 
 public class NetworkReceiver extends BroadcastReceiver {
     private static NetworkReceiver gInstance = null;
+    private static AdditionalReceiver gAdditionalReceiver;
+
+    public interface AdditionalReceiver {
+        void Update();
+    }
 
     private NetworkReceiver() {}
 
     @Override
-    public void onReceive(Context context, Intent intent) {
+    public void onReceive(final Context context, Intent intent) {
         Tick(context);
+        (new Handler(context.getMainLooper())).postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                Tick(context);
+            }
+        }, 3000);
     }
 
     public static void Register(MainService ctx) {
@@ -34,19 +45,23 @@ public class NetworkReceiver extends BroadcastReceiver {
                     intentFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
                     gInstance = new NetworkReceiver();
                     ctx.registerReceiver(gInstance, intentFilter);
-                    gInstance.Tick(ctx);
                 }
             }
         }
+        gInstance.Tick(ctx);
     }
 
-    public boolean IsConnected(Context ctx) {
+    private static NetworkInfo GetFirstNetworkAvailable(Context ctx) {
         ConnectivityManager manager = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
         NetworkInfo netInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
         if (netInfo != null && netInfo.isConnected())
-            return true;
+            return netInfo;
         netInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_ETHERNET);
-        return netInfo != null && netInfo.isConnected();
+        return (netInfo != null && netInfo.isConnected()) ? netInfo : null;
+    }
+
+    public boolean IsConnected(Context ctx) {
+        return GetFirstNetworkAvailable(ctx) != null;
     }
 
     private interface IServiceRunnable {
@@ -72,6 +87,10 @@ public class NetworkReceiver extends BroadcastReceiver {
         }, Context.BIND_AUTO_CREATE);
     }
 
+    public static void SetAdditionalReceiver(AdditionalReceiver receiver) {
+        gAdditionalReceiver = receiver;
+    }
+
     public void Tick(Context ctx) {
         GetService(ctx, new IServiceRunnable() {
             @Override
@@ -83,6 +102,8 @@ public class NetworkReceiver extends BroadcastReceiver {
                             service.Resume();
                         else
                             service.Pause();
+                        if (gAdditionalReceiver != null)
+                            gAdditionalReceiver.Update();
                     }
                 };
                 (new Handler(Looper.getMainLooper())).postDelayed(r, 5000);

二进制
app/src/main/res/ic_launcher_foreground.png


+ 46 - 12
app/src/main/res/layout/activity_main.xml

@@ -5,17 +5,44 @@
     android:layout_height="match_parent"
     tools:context=".MainActivity">
 
-    <LinearLayout
+    <TableLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"
-        tools:layout_editor_absoluteX="36dp"
-        tools:layout_editor_absoluteY="106dp">
+        android:layout_height="match_parent">
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <TextView
+                android:id="@+id/textView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="TextView" />
+
+            <EditText
+                android:id="@+id/input_port"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ems="10"
+                android:inputType="numberSigned" />
+
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <Button
+                android:id="@+id/bt_saveconfiguration"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:enabled="false"
+                android:text="Save" />
+        </TableRow>
 
-        <LinearLayout
+        <TableRow
             android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:orientation="horizontal">
+            android:layout_height="match_parent">
 
             <TextView
                 android:layout_width="wrap_content"
@@ -49,10 +76,17 @@
                 android:layout_weight="1"
                 android:enabled="false"
                 android:text="@string/start" />
-        </LinearLayout>
 
-        <Space
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
+        </TableRow>
+
+    </TableLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        tools:layout_editor_absoluteX="36dp"
+        tools:layout_editor_absoluteY="106dp">
+
     </LinearLayout>
 </androidx.constraintlayout.widget.ConstraintLayout>

二进制
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png