mirror of
https://github.com/emanuele-f/PCAPdroid.git
synced 2026-06-16 21:10:57 +08:00
Implement runtime captured bytes monitor
This commit is contained in:
parent
4b67d2b856
commit
c4b34757b5
@ -29,6 +29,8 @@ import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@ -45,6 +47,17 @@ public class CaptureService extends VpnService implements Runnable {
|
||||
private int uid_filter;
|
||||
private static CaptureService INSTANCE;
|
||||
|
||||
public static final String ACTION_TRAFFIC_STATS_UPDATE = "traffic_stats_update";
|
||||
public static final String TRAFFIC_STATS_UPDATE_SENT_BYTES = "sent_bytes";
|
||||
public static final String TRAFFIC_STATS_UPDATE_RCVD_BYTES = "rcvd_bytes";
|
||||
public static final String TRAFFIC_STATS_UPDATE_SENT_PKTS = "sent_pkts";
|
||||
public static final String TRAFFIC_STATS_UPDATE_RCVD_PKTS = "rcvd_pkts";
|
||||
|
||||
public static final String ACTION_SERVICE_STATUS = "service_status";
|
||||
public static final String SERVICE_STATUS_KEY = "status";
|
||||
public static final String SERVICE_STATUS_STARTED = "started";
|
||||
public static final String SERVICE_STATUS_STOPPED = "stopped";
|
||||
|
||||
static {
|
||||
/* Load native library */
|
||||
System.loadLibrary("vpnproxy-jni");
|
||||
@ -200,6 +213,23 @@ public class CaptureService extends VpnService implements Runnable {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void sendCaptureStats(long sent_bytes, long rcvd_bytes, int sent_pkts, int rcvd_pkts) {
|
||||
Intent intent = new Intent(ACTION_TRAFFIC_STATS_UPDATE);
|
||||
|
||||
intent.putExtra(TRAFFIC_STATS_UPDATE_SENT_BYTES, sent_bytes);
|
||||
intent.putExtra(TRAFFIC_STATS_UPDATE_RCVD_BYTES, rcvd_bytes);
|
||||
intent.putExtra(TRAFFIC_STATS_UPDATE_SENT_PKTS, sent_pkts);
|
||||
intent.putExtra(TRAFFIC_STATS_UPDATE_RCVD_PKTS, rcvd_pkts);
|
||||
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private void sendServiceStatus(String cur_status) {
|
||||
Intent intent = new Intent(ACTION_SERVICE_STATUS);
|
||||
intent.putExtra(SERVICE_STATUS_KEY, cur_status);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
public String getApplicationByUid(int uid) {
|
||||
return(getPackageManager().getNameForUid(uid));
|
||||
}
|
||||
|
||||
@ -19,7 +19,10 @@
|
||||
|
||||
package com.emanuelef.remote_capture;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
@ -34,6 +37,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import android.os.Bundle;
|
||||
@ -43,6 +47,7 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -52,12 +57,15 @@ import cat.ereza.customactivityoncrash.config.CaocConfig;
|
||||
public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<List<AppDescriptor>> {
|
||||
Button mStartButton;
|
||||
SharedPreferences mPrefs;
|
||||
TextView mExporterInfo;
|
||||
TextView mCaptureStatus;
|
||||
Menu mMenu;
|
||||
int mFilterUid;
|
||||
boolean mOpenAppsWhenDone;
|
||||
List<AppDescriptor> mInstalledApps;
|
||||
|
||||
private static final int REQUEST_CODE_VPN = 2;
|
||||
private static final int MENU_ITEM_APP_SELECTOR_IDX = 0;
|
||||
public static final int OPERATION_SEARCH_LOADER = 23;
|
||||
|
||||
private void updateConnectStatus(boolean is_running) {
|
||||
@ -86,6 +94,16 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa
|
||||
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
mStartButton = findViewById(R.id.button_start);
|
||||
mExporterInfo = findViewById(R.id.exporter_info);
|
||||
mCaptureStatus = findViewById(R.id.status_view);
|
||||
|
||||
mPrefs.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
|
||||
refreshExporterInfo();
|
||||
}
|
||||
});
|
||||
refreshExporterInfo();
|
||||
|
||||
updateConnectStatus(CaptureService.isRunning());
|
||||
|
||||
@ -97,19 +115,54 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa
|
||||
if(CaptureService.isRunning()) {
|
||||
CaptureService.stopService();
|
||||
updateConnectStatus(false);
|
||||
mStartButton.setEnabled(false);
|
||||
//mCaptureStatus.setText(R.string.stopping);
|
||||
} else {
|
||||
Intent vpnPrepareIntent = VpnService.prepare(MainActivity.this);
|
||||
if (vpnPrepareIntent != null) {
|
||||
startActivityForResult(vpnPrepareIntent, REQUEST_CODE_VPN);
|
||||
mStartButton.setEnabled(false);
|
||||
} else {
|
||||
onActivityResult(REQUEST_CODE_VPN, RESULT_OK, null);
|
||||
}
|
||||
updateConnectStatus(true);
|
||||
mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setEnabled(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
startLoadingApps();
|
||||
|
||||
LocalBroadcastManager bcast_man = LocalBroadcastManager.getInstance(this);
|
||||
|
||||
/* Register for stats update */
|
||||
bcast_man.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
processStatsUpdateIntent(intent);
|
||||
}
|
||||
}, new IntentFilter(CaptureService.ACTION_TRAFFIC_STATS_UPDATE));
|
||||
|
||||
/* Register for service status */
|
||||
bcast_man.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String status = intent.getStringExtra(CaptureService.SERVICE_STATUS_KEY);
|
||||
|
||||
if(status.equals(CaptureService.SERVICE_STATUS_STARTED)) {
|
||||
mCaptureStatus.setText(formatBytes(0));
|
||||
mStartButton.setEnabled(true);
|
||||
} else if(status.equals(CaptureService.SERVICE_STATUS_STOPPED)) {
|
||||
mCaptureStatus.setText(R.string.ready);
|
||||
mStartButton.setEnabled(true);
|
||||
mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setEnabled(true);
|
||||
}
|
||||
}
|
||||
}, new IntentFilter(CaptureService.ACTION_SERVICE_STATUS));
|
||||
}
|
||||
|
||||
private void refreshExporterInfo() {
|
||||
mExporterInfo.setText("UDP Collector: " + getCollectorIPPref() + ":" + getCollectorPortPref());
|
||||
}
|
||||
|
||||
private void startLoadingApps() {
|
||||
@ -173,7 +226,7 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa
|
||||
public void onSelectedApp(AppDescriptor app) {
|
||||
mFilterUid = app.getUid();
|
||||
// clone the drawable to avoid a "zoom-in" effect when clicked
|
||||
mMenu.getItem(0).setIcon(app.getIcon().getConstantState().newDrawable());
|
||||
mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setIcon(app.getIcon().getConstantState().newDrawable());
|
||||
|
||||
// dismiss the dialog
|
||||
alert.cancel();
|
||||
@ -197,6 +250,14 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private String getCollectorIPPref() {
|
||||
return(mPrefs.getString(Prefs.PREF_COLLECTOR_IP_KEY, getString(R.string.default_collector_ip)));
|
||||
}
|
||||
|
||||
private String getCollectorPortPref() {
|
||||
return(mPrefs.getString(Prefs.PREF_COLLECTOR_PORT_KEY, getString(R.string.default_collector_port)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
@ -207,14 +268,15 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa
|
||||
|
||||
// the configuration for the VPN
|
||||
bundle.putString("dns_server", "8.8.8.8"); // TODO: read system DNS
|
||||
bundle.putString(Prefs.PREF_COLLECTOR_IP_KEY, mPrefs.getString(Prefs.PREF_COLLECTOR_IP_KEY, getString(R.string.default_collector_ip)));
|
||||
bundle.putInt(Prefs.PREF_COLLECTOR_PORT_KEY, Integer.parseInt(mPrefs.getString(Prefs.PREF_COLLECTOR_PORT_KEY, getString(R.string.default_collector_port))));
|
||||
bundle.putString(Prefs.PREF_COLLECTOR_IP_KEY, getCollectorIPPref());
|
||||
bundle.putInt(Prefs.PREF_COLLECTOR_PORT_KEY, Integer.parseInt(getCollectorPortPref()));
|
||||
bundle.putInt(Prefs.PREF_UID_FILTER, mFilterUid);
|
||||
intent.putExtra("settings", bundle);
|
||||
|
||||
Log.d("Main", "onActivityResult -> start CaptureService");
|
||||
|
||||
startService(intent);
|
||||
//mCaptureStatus.setText(R.string.starting);
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,7 +304,29 @@ public class MainActivity extends AppCompatActivity implements LoaderManager.Loa
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<List<AppDescriptor>> loader) {
|
||||
public void onLoaderReset(@NonNull Loader<List<AppDescriptor>> loader) {}
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
long divisor;
|
||||
String suffix;
|
||||
if(bytes < 1024) return bytes + " B";
|
||||
|
||||
if(bytes < 1024*1024) { divisor = 1024; suffix = "KB"; }
|
||||
else if(bytes < 1024*1024*1024) { divisor = 1024*1024; suffix = "MB"; }
|
||||
else { divisor = 1024*1024*1024; suffix = "GB"; }
|
||||
|
||||
return String.format("%.1f %s", ((float)bytes) / divisor, suffix);
|
||||
}
|
||||
|
||||
public void processStatsUpdateIntent(Intent intent) {
|
||||
long bytes_sent = intent.getLongExtra(CaptureService.TRAFFIC_STATS_UPDATE_SENT_BYTES, 0);
|
||||
long bytes_rcvd= intent.getLongExtra(CaptureService.TRAFFIC_STATS_UPDATE_RCVD_BYTES, 0);
|
||||
int pkts_sent = intent.getIntExtra(CaptureService.TRAFFIC_STATS_UPDATE_SENT_PKTS, 0);
|
||||
int pkts_rcvd = intent.getIntExtra(CaptureService.TRAFFIC_STATS_UPDATE_RCVD_PKTS, 0);
|
||||
|
||||
Log.w("MainReceiver", "Got StatsUpdate: bytes_sent=" + bytes_sent + ", bytes_rcvd=" +
|
||||
bytes_rcvd + ", pkts_sent=" + pkts_sent + ", pkts_rcvd=" + pkts_rcvd);
|
||||
|
||||
mCaptureStatus.setText(formatBytes(bytes_sent + bytes_rcvd));
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
#include "pcap.h"
|
||||
|
||||
#define VPN_TAG "VPNProxy"
|
||||
#define CAPTURE_STATS_UPDATE_FREQUENCY_MS 300
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
@ -178,7 +179,7 @@ static char* getApplicationByUid(vpnproxy_data_t *proxy, int uid, char *buf, siz
|
||||
|
||||
static int dumper_socket = -1;
|
||||
|
||||
static void dump_packet_to_udp(char *packet, size_t size, bool from_tap, const zdtun_conn_t *conn_info, struct vpnproxy_data *proxy) {
|
||||
static void process_packet_info(char *packet, size_t size, bool from_tap, const zdtun_conn_t *conn_info, struct vpnproxy_data *proxy) {
|
||||
struct sockaddr_in servaddr = {0};
|
||||
bool send_header = false;
|
||||
int uid = (int)conn_info->user_data;
|
||||
@ -197,6 +198,20 @@ static void dump_packet_to_udp(char *packet, size_t size, bool from_tap, const z
|
||||
return;
|
||||
}
|
||||
|
||||
if(from_tap) {
|
||||
proxy->capture_stats.rcvd_pkts++;
|
||||
proxy->capture_stats.rcvd_bytes += size;
|
||||
} else {
|
||||
proxy->capture_stats.sent_pkts++;
|
||||
proxy->capture_stats.sent_bytes += size;
|
||||
}
|
||||
|
||||
/* New stats to notify */
|
||||
proxy->capture_stats.new_stats = true;
|
||||
|
||||
if(!proxy->pcap_dump.enabled)
|
||||
return;
|
||||
|
||||
#if 1
|
||||
if(dumper_socket <= 0) {
|
||||
dumper_socket = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
@ -355,8 +370,7 @@ static int net2tap(zdtun_t *tun, char *pkt_buf, ssize_t pkt_size, const zdtun_co
|
||||
|
||||
check_dns(proxy, pkt_buf, pkt_size, 0 /* reply */);
|
||||
|
||||
if(proxy->pcap_dump.enabled)
|
||||
dump_packet_to_udp(pkt_buf, pkt_size, false, conn_info, proxy);
|
||||
process_packet_info(pkt_buf, pkt_size, false, conn_info, proxy);
|
||||
|
||||
// TODO return value check
|
||||
write(proxy->tapfd, pkt_buf, pkt_size);
|
||||
@ -365,6 +379,42 @@ static int net2tap(zdtun_t *tun, char *pkt_buf, ssize_t pkt_size, const zdtun_co
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
static void sendCaptureStats(vpnproxy_data_t *proxy) {
|
||||
JNIEnv *env = proxy->env;
|
||||
capture_stats_t *stats = &proxy->capture_stats;
|
||||
jclass vpn_service_cls = (*env)->GetObjectClass(env, proxy->vpn_service);
|
||||
const char *value = NULL;
|
||||
|
||||
jmethodID midMethod = (*env)->GetMethodID(env, vpn_service_cls, "sendCaptureStats", "(JJII)V");
|
||||
if(!midMethod)
|
||||
__android_log_print(ANDROID_LOG_FATAL, VPN_TAG, "GetMethodID(sendCaptureStats) failed");
|
||||
|
||||
(*env)->CallObjectMethod(env, proxy->vpn_service, midMethod, stats->sent_bytes, stats->rcvd_bytes,
|
||||
stats->sent_pkts, stats->rcvd_pkts);
|
||||
}
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
static void notifyServiceStatus(vpnproxy_data_t *proxy, const char *status) {
|
||||
JNIEnv *env = proxy->env;
|
||||
capture_stats_t *stats = &proxy->capture_stats;
|
||||
jclass vpn_service_cls = (*env)->GetObjectClass(env, proxy->vpn_service);
|
||||
const char *value = NULL;
|
||||
jstring status_str;
|
||||
|
||||
jmethodID midMethod = (*env)->GetMethodID(env, vpn_service_cls, "sendServiceStatus", "(Ljava/lang/String;)V");
|
||||
if(!midMethod)
|
||||
__android_log_print(ANDROID_LOG_FATAL, VPN_TAG, "GetMethodID(sendServiceStatus) failed");
|
||||
|
||||
status_str = (*env)->NewStringUTF(env, status);
|
||||
|
||||
(*env)->CallObjectMethod(env, proxy->vpn_service, midMethod, status_str);
|
||||
|
||||
(*env)->DeleteLocalRef(env, status_str);
|
||||
}
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
static int running = 0;
|
||||
|
||||
static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
|
||||
@ -412,39 +462,52 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
|
||||
|
||||
__android_log_print(ANDROID_LOG_DEBUG, VPN_TAG, "Starting packet loop [tapfd=%d]", tapfd);
|
||||
|
||||
// TODO wake via select event
|
||||
notifyServiceStatus(&proxy, "started");
|
||||
|
||||
while(running) {
|
||||
int max_fd;
|
||||
fd_set fdset;
|
||||
fd_set wrfds;
|
||||
ssize_t size;
|
||||
u_int64_t now_ms;
|
||||
struct timeval now_tv;
|
||||
struct timeval timeout = {.tv_sec = 0, .tv_usec = 500*1000}; // wake every 500 ms
|
||||
|
||||
zdtun_fds(tun, &max_fd, &fdset, &wrfds);
|
||||
|
||||
FD_SET(tapfd, &fdset);
|
||||
max_fd = max(max_fd, tapfd);
|
||||
|
||||
select(max_fd + 1, &fdset, &wrfds, NULL, NULL);
|
||||
select(max_fd + 1, &fdset, &wrfds, NULL, &timeout);
|
||||
|
||||
if(!running)
|
||||
break;
|
||||
|
||||
gettimeofday(&now_tv, NULL);
|
||||
now_ms = now_tv.tv_sec * 1000 + now_tv.tv_usec / 1000;
|
||||
|
||||
if(FD_ISSET(tapfd, &fdset)) {
|
||||
/* Packet from VPN */
|
||||
size = read(tapfd, buffer, sizeof(buffer));
|
||||
const zdtun_conn_t conn_info;
|
||||
|
||||
if(size > 0) {
|
||||
zdtun_conn_t conn_info;
|
||||
proxy.dns_changed = check_dns(&proxy, buffer, size, 1 /* query */) != 0;
|
||||
|
||||
if(proxy.pcap_dump.enabled)
|
||||
dump_packet_to_udp(buffer, size, true, &conn_info, &proxy);
|
||||
|
||||
zdtun_forward(tun, buffer, size, &conn_info);
|
||||
|
||||
process_packet_info(buffer, size, true, &conn_info, &proxy);
|
||||
} else if (size < 0)
|
||||
__android_log_print(ANDROID_LOG_ERROR, VPN_TAG, "recv(tapfd) returned error [%d]: %s", errno, strerror(errno));
|
||||
} else
|
||||
zdtun_handle_fd(tun, &fdset, &wrfds);
|
||||
|
||||
if(proxy.capture_stats.new_stats
|
||||
&& ((now_ms - proxy.capture_stats.last_update_ms) >= CAPTURE_STATS_UPDATE_FREQUENCY_MS)) {
|
||||
sendCaptureStats(&proxy);
|
||||
proxy.capture_stats.new_stats = false;
|
||||
proxy.capture_stats.last_update_ms = now_ms;
|
||||
}
|
||||
}
|
||||
|
||||
__android_log_print(ANDROID_LOG_DEBUG, VPN_TAG, "Stopped packet loop");
|
||||
@ -456,6 +519,7 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
|
||||
dumper_socket = -1;
|
||||
}
|
||||
|
||||
notifyServiceStatus(&proxy, "stopped");
|
||||
return(0);
|
||||
}
|
||||
|
||||
@ -463,8 +527,8 @@ static int run_tun(JNIEnv *env, jclass vpn, int tapfd, jint sdk) {
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_emanuelef_remote_1capture_CaptureService_stopPacketLoop(JNIEnv *env, jclass type) {
|
||||
/* NOTE: the select on the packets loop uses a timeout to wake up periodically */
|
||||
running = 0;
|
||||
/* TODO wake the possibly sleeping thread */
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
|
||||
@ -24,6 +24,16 @@
|
||||
#ifndef REMOTE_CAPTURE_VPNPROXY_H
|
||||
#define REMOTE_CAPTURE_VPNPROXY_H
|
||||
|
||||
typedef struct capture_stats {
|
||||
u_int64_t sent_bytes;
|
||||
u_int64_t rcvd_bytes;
|
||||
u_int32_t sent_pkts;
|
||||
u_int32_t rcvd_pkts;
|
||||
|
||||
bool new_stats;
|
||||
u_int64_t last_update_ms;
|
||||
} capture_stats_t;
|
||||
|
||||
typedef struct vpnproxy_data {
|
||||
int tapfd;
|
||||
jint sdk;
|
||||
@ -41,6 +51,8 @@ typedef struct vpnproxy_data {
|
||||
int uid_filter;
|
||||
bool enabled;
|
||||
} pcap_dump;
|
||||
|
||||
capture_stats_t capture_stats;
|
||||
} vpnproxy_data_t;
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
25
app/src/main/res/drawable/rounded_bg.xml
Normal file
25
app/src/main/res/drawable/rounded_bg.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item>
|
||||
<shape android:shape="oval" >
|
||||
<gradient
|
||||
android:angle="360"
|
||||
android:startColor="#543456"
|
||||
android:endColor="#f800f0"
|
||||
android:type="linear" />
|
||||
<size android:width="24dp"
|
||||
android:height="24dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:bottom="2dp"
|
||||
android:left="2dp"
|
||||
android:right="2dp"
|
||||
android:top="2dp">
|
||||
<shape android:shape="oval" >
|
||||
<solid android:color="#fff" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
@ -1,16 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context="com.emanuelef.remote_capture.MainActivity">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_start"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/start_button" />
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="26dp"
|
||||
android:text="@string/start_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/exporter_info"
|
||||
|
||||
</LinearLayout>
|
||||
app:layout_constraintWidth_default="wrap" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/exporter_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="92dp"
|
||||
android:text="TextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_view" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_view"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="240dp"
|
||||
android:layout_marginTop="88dp"
|
||||
android:background="@drawable/rounded_bg"
|
||||
android:gravity="center"
|
||||
android:text="@string/ready"
|
||||
android:textSize="30dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -14,4 +14,7 @@
|
||||
<string name="ip_address">IP Address</string>
|
||||
<string name="port">Port</string>
|
||||
<string name="app_filter">App Filter</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="stopping">Stopping...</string>
|
||||
<string name="starting">Starting...</string>
|
||||
</resources>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user