With UDP
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package com.dragontcp.client;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DragonService extends VpnService {
|
||||
public static final String ACTION_CONNECT="com.dragontcp.client.CONNECT";
|
||||
public static final String ACTION_STOP="com.dragontcp.client.STOP";
|
||||
public static volatile boolean active=false,running=false;
|
||||
public static volatile String state="Stopped";
|
||||
private static final String CHANNEL_ID="dragontcp_vpn";
|
||||
private static final int NOTIFICATION_ID=53;
|
||||
private final Object lifecycleLock=new Object();
|
||||
private Process process;
|
||||
private Thread outputThread;
|
||||
private ParcelFileDescriptor vpnInterface;
|
||||
private File fdSocketFile;
|
||||
|
||||
@Override public void onCreate(){super.onCreate();createNotificationChannel();}
|
||||
@Override public int onStartCommand(Intent intent,int flags,int startId){
|
||||
if(intent==null)return START_NOT_STICKY;String action=intent.getAction();
|
||||
if(ACTION_STOP.equals(action)){appendLog("STOP requested");shutdown("Stopped by user",true);return START_NOT_STICKY;}
|
||||
if(!ACTION_CONNECT.equals(action))return START_NOT_STICKY;
|
||||
cleanupResources(true);clearLog();active=true;running=false;state="Starting VPN";startForeground(NOTIFICATION_ID,buildNotification("Starting full VPN"));
|
||||
String server=intent.getStringExtra("server"),token=intent.getStringExtra("token"),timeout=intent.getStringExtra("timeout"),v4=intent.getStringExtra("vpnIPv4"),v6=intent.getStringExtra("vpnIPv6");
|
||||
int port=intent.getIntExtra("port",53),max=intent.getIntExtra("chunkMax",1280),min=intent.getIntExtra("chunkMin",32),start=intent.getIntExtra("chunkStart",max);
|
||||
if(server==null||server.trim().isEmpty()){failStart("Server is empty");return START_NOT_STICKY;}if(token==null)token="";if(timeout==null||timeout.isEmpty())timeout="2s";if(v4==null||v6==null){failStart("Missing VPN client address");return START_NOT_STICKY;}
|
||||
start=max;
|
||||
try{
|
||||
establishPacketVpn(v4,v6);
|
||||
state="Starting DragonTCP core";
|
||||
startCore(server.trim(),port,token,start,min,max,timeout.trim(),v4,v6);
|
||||
state="Connecting to DragonTCP server";
|
||||
updateNotification("Connecting • TCP/"+port);
|
||||
}catch(Exception e){failStart(e.getMessage()==null?e.toString():e.getMessage());}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private void establishPacketVpn(String v4,String v6)throws Exception{
|
||||
VpnService.Builder b=new VpnService.Builder();b.setSession("DragonTCP VPN");b.setMtu(1280);
|
||||
b.addAddress(v4,32);b.addAddress(v6,128);b.addRoute("0.0.0.0",0);b.addRoute("::",0);
|
||||
b.addDnsServer("1.1.1.1");b.addDnsServer("2606:4700:4700::1111");
|
||||
try{b.addDisallowedApplication(getPackageName());}catch(PackageManager.NameNotFoundException e){throw new Exception("Cannot exclude DragonTCP from its own VPN",e);}
|
||||
Intent open=new Intent(this,MainActivity.class);open.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP|Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
PendingIntent pi=PendingIntent.getActivity(this,1,open,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);b.setConfigureIntent(pi);
|
||||
vpnInterface=b.establish();if(vpnInterface==null)throw new Exception("Android did not establish the TUN interface");
|
||||
appendLog("TUN established: "+v4+" + "+v6+" MTU=1280");appendLog("Routes captured: 0.0.0.0/0 and ::/0");appendLog("DNS through VPN: 1.1.1.1 + 2606:4700:4700::1111");appendLog("DragonTCP app UID excluded from VPN to prevent recursion");
|
||||
}
|
||||
|
||||
private void startCore(String server,int port,String token,int start,int min,int max,String timeout,String v4,String v6)throws Exception{
|
||||
String executable=getApplicationInfo().nativeLibraryDir+"/libdragontcp_vpn.so";File exe=new File(executable);if(!exe.exists())throw new Exception("Embedded DragonTCP VPN core was not extracted");
|
||||
fdSocketFile=new File(getFilesDir(),"dragontcp-tunfd.sock");if(fdSocketFile.exists())fdSocketFile.delete();
|
||||
List<String> cmd=new ArrayList<String>();cmd.add(executable);cmd.add("--server-host");cmd.add(server);cmd.add("--server-port");cmd.add(String.valueOf(port));cmd.add("--token");cmd.add(token);
|
||||
cmd.add("--tun-fd-socket");cmd.add(fdSocketFile.getAbsolutePath());cmd.add("--vpn-ipv4");cmd.add(v4);cmd.add("--vpn-ipv6");cmd.add(v6);cmd.add("--vpn-mtu");cmd.add("1280");
|
||||
cmd.add("--chunk-start");cmd.add(String.valueOf(max));cmd.add("--chunk-max");cmd.add(String.valueOf(max));cmd.add("--chunk-min");cmd.add(String.valueOf(min));cmd.add("--chunk-grow-after");cmd.add("64");cmd.add("--chunk-timeout");cmd.add(timeout);cmd.add("--chunk-reconnect-every");cmd.add("32");cmd.add("--chunk-adapt-log");
|
||||
appendLog("Server: "+server+":"+port);appendLog("Transport chunks: start=max="+max+" min="+min+" pollers=1 timeout="+timeout);
|
||||
ProcessBuilder pb=new ProcessBuilder(cmd);pb.redirectErrorStream(true);pb.directory(getFilesDir());final Process p=pb.start();synchronized(lifecycleLock){process=p;}
|
||||
outputThread=new Thread(()->readCoreOutput(p),"DragonTCP-output");outputThread.setDaemon(true);outputThread.start();
|
||||
passTunFdWhenReady();
|
||||
}
|
||||
|
||||
private void passTunFdWhenReady()throws Exception{
|
||||
long deadline=System.currentTimeMillis()+5000;while(System.currentTimeMillis()<deadline){if(fdSocketFile!=null&&fdSocketFile.exists())break;Process p; synchronized(lifecycleLock){p=process;}if(p==null||!p.isAlive())throw new Exception("DragonTCP core exited before TUN handoff");Thread.sleep(25);}
|
||||
if(fdSocketFile==null||!fdSocketFile.exists())throw new Exception("DragonTCP core did not create its TUN-fd socket");
|
||||
LocalSocket s=new LocalSocket();try{s.connect(new LocalSocketAddress(fdSocketFile.getAbsolutePath(),LocalSocketAddress.Namespace.FILESYSTEM));FileDescriptor fd=vpnInterface.getFileDescriptor();s.setFileDescriptorsForSend(new FileDescriptor[]{fd});s.getOutputStream().write(0x44);s.getOutputStream().flush();appendLog("TUN file descriptor passed to DragonTCP core");}finally{try{s.close();}catch(Exception ignored){}}
|
||||
}
|
||||
|
||||
private void readCoreOutput(Process p){
|
||||
try{BufferedReader br=new BufferedReader(new InputStreamReader(p.getInputStream()));String line;while((line=br.readLine())!=null){appendLog(line);if(line.contains("VPN READY")){running=true;active=true;state="Connected • Full VPN";updateNotification("Connected • IPv4 + IPv6 • TCP/UDP");}}
|
||||
int code=p.waitFor();handleCoreExit(p,code);
|
||||
}catch(Exception e){appendLog("Core reader: "+e);handleCoreExit(p,-1);}
|
||||
}
|
||||
private void handleCoreExit(Process p,int code){boolean owns; synchronized(lifecycleLock){owns=process==p;if(owns)process=null;}if(!owns)return;appendLog("DragonTCP core exited: "+code);running=false;active=false;state="Core exited ("+code+")";closeVpn();stopForeground(true);stopSelf();}
|
||||
|
||||
private Notification buildNotification(String msg){Intent open=new Intent(this,MainActivity.class);open.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP|Intent.FLAG_ACTIVITY_CLEAR_TOP);PendingIntent op=PendingIntent.getActivity(this,0,open,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);Intent stop=new Intent(this,DragonService.class);stop.setAction(ACTION_STOP);PendingIntent sp=PendingIntent.getService(this,2,stop,PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);Notification.Builder nb=Build.VERSION.SDK_INT>=26?new Notification.Builder(this,CHANNEL_ID):new Notification.Builder(this);return nb.setContentTitle("DragonTCP VPN").setContentText(msg).setSmallIcon(android.R.drawable.stat_sys_upload).setOngoing(true).setContentIntent(op).addAction(android.R.drawable.ic_menu_close_clear_cancel,"STOP",sp).build();}
|
||||
private void updateNotification(String m){NotificationManager nm=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);if(nm!=null)nm.notify(NOTIFICATION_ID,buildNotification(m));}
|
||||
private void createNotificationChannel(){if(Build.VERSION.SDK_INT>=26){NotificationChannel c=new NotificationChannel(CHANNEL_ID,"DragonTCP VPN",NotificationManager.IMPORTANCE_LOW);c.setDescription("DragonTCP full packet VPN status");NotificationManager nm=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);if(nm!=null)nm.createNotificationChannel(c);}}
|
||||
private synchronized void appendLog(String line){try(PrintWriter out=new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(getFilesDir(),"dragontcp.log"),true),"UTF-8"))){out.println(line);out.flush();}catch(Exception ignored){}}
|
||||
private void clearLog(){try{new FileOutputStream(new File(getFilesDir(),"dragontcp.log"),false).close();}catch(Exception ignored){}}
|
||||
private void failStart(String m){appendLog("START ERROR: "+m);running=false;active=false;state="Start failed: "+m;cleanupResources(true);stopForeground(true);stopSelf();}
|
||||
private void shutdown(String reason,boolean stop){state="Stopping";running=false;appendLog(reason);cleanupResources(true);active=false;state="Stopped";stopForeground(true);if(stop)stopSelf();}
|
||||
private void cleanupResources(boolean kill){Process p; synchronized(lifecycleLock){p=process;process=null;}if(p!=null){try{p.getInputStream().close();}catch(Exception ignored){}try{p.destroy();}catch(Exception ignored){}if(kill){try{if(!p.waitFor(800,TimeUnit.MILLISECONDS)){p.destroyForcibly();p.waitFor(800,TimeUnit.MILLISECONDS);}}catch(Exception ignored){try{p.destroyForcibly();}catch(Exception ignored2){}}}}Thread t=outputThread;outputThread=null;if(t!=null&&t!=Thread.currentThread())t.interrupt();closeVpn();if(fdSocketFile!=null){fdSocketFile.delete();fdSocketFile=null;}running=false;}
|
||||
private void closeVpn(){ParcelFileDescriptor v=vpnInterface;vpnInterface=null;if(v!=null){try{v.close();}catch(Exception ignored){}}}
|
||||
@Override public void onRevoke(){appendLog("VPN permission revoked");shutdown("VPN revoked",true);super.onRevoke();}
|
||||
@Override public void onDestroy(){cleanupResources(true);active=false;running=false;if(!state.startsWith("Start failed")&&!state.startsWith("Core exited"))state="Stopped";stopForeground(true);super.onDestroy();}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.dragontcp.client;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.InputType;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
private static final int VPN_REQUEST = 5301;
|
||||
|
||||
private EditText server, port, token, chunkMax, chunkMin, timeout;
|
||||
private TextView status, logs;
|
||||
private ScrollView logScroll;
|
||||
private Button connectButton, stopButton;
|
||||
private Intent pendingServiceIntent;
|
||||
private SharedPreferences prefs;
|
||||
private String lastLogText = "";
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private final Runnable refresher = new Runnable() {
|
||||
@Override public void run() {
|
||||
refreshStatus();
|
||||
handler.postDelayed(this, 500);
|
||||
}
|
||||
};
|
||||
|
||||
@Override protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
prefs = getSharedPreferences("dragontcp", MODE_PRIVATE);
|
||||
setTitle("DragonTCP VPN");
|
||||
buildUi();
|
||||
loadSettings();
|
||||
handler.post(refresher);
|
||||
}
|
||||
|
||||
private int dp(int v) { return (int)(v * getResources().getDisplayMetrics().density + 0.5f); }
|
||||
private TextView text(String s, float sp, boolean bold) {
|
||||
TextView v = new TextView(this); v.setText(s); v.setTextSize(sp); v.setTextColor(Color.rgb(232,236,241));
|
||||
if (bold) v.setTypeface(Typeface.DEFAULT, Typeface.BOLD); return v;
|
||||
}
|
||||
private EditText field(LinearLayout root, String label, int type) {
|
||||
TextView t=text(label,13f,false);t.setPadding(0,dp(9),0,dp(4));root.addView(t);
|
||||
EditText e=new EditText(this);e.setSingleLine(true);e.setTextColor(Color.WHITE);e.setHintTextColor(Color.GRAY);e.setInputType(type);
|
||||
e.setBackgroundColor(Color.rgb(42,47,54));e.setPadding(dp(12),dp(9),dp(12),dp(9));
|
||||
root.addView(e,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT));return e;
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
ScrollView page=new ScrollView(this);page.setFillViewport(true);page.setBackgroundColor(Color.rgb(20,23,27));
|
||||
LinearLayout root=new LinearLayout(this);root.setOrientation(LinearLayout.VERTICAL);root.setPadding(dp(18),dp(18),dp(18),dp(24));page.addView(root);
|
||||
TextView title=text("DragonTCP VPN",27f,true);title.setTextColor(Color.rgb(104,207,255));root.addView(title);
|
||||
TextView sub=text("Full IPv4 / IPv6 packet VPN over adaptive TCP/53",13f,false);sub.setTextColor(Color.rgb(170,179,188));sub.setPadding(0,dp(2),0,dp(12));root.addView(sub);
|
||||
status=text("Stopped",16f,true);status.setPadding(dp(12),dp(12),dp(12),dp(12));status.setBackgroundColor(Color.rgb(34,39,45));root.addView(status);
|
||||
|
||||
server=field(root,"Server IP / hostname",InputType.TYPE_CLASS_TEXT);
|
||||
port=field(root,"TCP port",InputType.TYPE_CLASS_NUMBER);
|
||||
token=field(root,"Token",InputType.TYPE_CLASS_TEXT|InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
chunkMax=field(root,"Maximum transport fragment bytes (start = max)",InputType.TYPE_CLASS_NUMBER);
|
||||
chunkMin=field(root,"Minimum transport fragment bytes",InputType.TYPE_CLASS_NUMBER);
|
||||
timeout=field(root,"Transaction timeout (example: 2s)",InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
TextView note=text("Pollers are fixed at 1. Adaptive chunks always start at Max and shrink on failures. All IPv4 and IPv6 routes are captured by the VPN; DragonTCP itself is excluded to prevent a tunnel loop.",12f,false);
|
||||
note.setTextColor(Color.rgb(160,170,180));note.setPadding(0,dp(10),0,dp(8));root.addView(note);
|
||||
|
||||
LinearLayout buttons=new LinearLayout(this);buttons.setOrientation(LinearLayout.HORIZONTAL);buttons.setGravity(Gravity.CENTER);buttons.setPadding(0,dp(8),0,dp(10));root.addView(buttons);
|
||||
connectButton=new Button(this);connectButton.setText("CONNECT");buttons.addView(connectButton,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
stopButton=new Button(this);stopButton.setText("STOP");buttons.addView(stopButton,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
connectButton.setOnClickListener(v -> startDragon()); stopButton.setOnClickListener(v -> stopDragon());
|
||||
|
||||
LinearLayout lh=new LinearLayout(this);lh.setOrientation(LinearLayout.HORIZONTAL);lh.setGravity(Gravity.CENTER_VERTICAL);root.addView(lh);
|
||||
TextView lt=text("Live log",17f,true);lh.addView(lt,new LinearLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f));
|
||||
Button clear=new Button(this);clear.setText("CLEAR");lh.addView(clear);clear.setOnClickListener(v -> clearLog());
|
||||
logScroll=new ScrollView(this);logScroll.setFillViewport(true);logScroll.setVerticalScrollBarEnabled(true);logScroll.setBackgroundColor(Color.BLACK);
|
||||
logs=text("",11f,false);logs.setTypeface(Typeface.MONOSPACE);logs.setTextIsSelectable(true);logs.setPadding(dp(10),dp(10),dp(10),dp(10));logs.setBackgroundColor(Color.BLACK);
|
||||
logScroll.addView(logs,new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
root.addView(logScroll,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,dp(320)));
|
||||
setContentView(page);
|
||||
}
|
||||
|
||||
private void loadSettings(){server.setText(prefs.getString("server",""));port.setText(prefs.getString("port","53"));token.setText(prefs.getString("token",""));chunkMax.setText(prefs.getString("chunkMax","1280"));chunkMin.setText(prefs.getString("chunkMin","32"));timeout.setText(prefs.getString("timeout","2s"));}
|
||||
private int intValue(EditText e,int d){try{return Integer.parseInt(e.getText().toString().trim());}catch(Exception x){return d;}}
|
||||
private boolean validateSettings(){
|
||||
if(server.getText().toString().trim().isEmpty()){toast("Enter the server IP or hostname");return false;}
|
||||
int p=intValue(port,53),min=intValue(chunkMin,32),max=intValue(chunkMax,1280);
|
||||
if(p<1||p>65535){toast("Port must be 1-65535");return false;}
|
||||
if(min<32||max>65535||min>max){toast("Chunks must satisfy 32 <= Min <= Max <= 65535");return false;}
|
||||
if(timeout.getText().toString().trim().isEmpty()){toast("Enter a timeout such as 2s");return false;}
|
||||
return true;
|
||||
}
|
||||
private void saveSettings(){prefs.edit().putString("server",server.getText().toString().trim()).putString("port",port.getText().toString().trim()).putString("token",token.getText().toString()).putString("chunkMax",chunkMax.getText().toString().trim()).putString("chunkMin",chunkMin.getText().toString().trim()).putString("timeout",timeout.getText().toString().trim()).apply();}
|
||||
|
||||
private int clientHostId(){
|
||||
int id=prefs.getInt("clientHostId",0);if(id>=2&&id<=65534)return id;
|
||||
id=2+new SecureRandom().nextInt(65533);prefs.edit().putInt("clientHostId",id).apply();return id;
|
||||
}
|
||||
private String clientIPv4(int id){return "10.123."+((id>>8)&255)+"."+(id&255);}
|
||||
private String clientIPv6(int id){return "fd7a:4472:6167:6f6e::"+Integer.toHexString(id);}
|
||||
|
||||
private Intent buildServiceIntent(){
|
||||
int max=intValue(chunkMax,1280),id=clientHostId();Intent i=new Intent(this,DragonService.class);i.setAction(DragonService.ACTION_CONNECT);
|
||||
i.putExtra("server",server.getText().toString().trim());i.putExtra("port",intValue(port,53));i.putExtra("token",token.getText().toString());
|
||||
i.putExtra("chunkStart",max);i.putExtra("chunkMax",max);i.putExtra("chunkMin",intValue(chunkMin,32));i.putExtra("timeout",timeout.getText().toString().trim());
|
||||
i.putExtra("vpnIPv4",clientIPv4(id));i.putExtra("vpnIPv6",clientIPv6(id));return i;
|
||||
}
|
||||
private void startDragon(){if(!validateSettings())return;saveSettings();pendingServiceIntent=buildServiceIntent();DragonService.active=true;DragonService.state="Waiting for VPN permission";refreshStatus();Intent prep=VpnService.prepare(this);if(prep!=null)startActivityForResult(prep,VPN_REQUEST);else{Intent i=pendingServiceIntent;pendingServiceIntent=null;launchService(i);}}
|
||||
private void launchService(Intent i){if(i==null)return;DragonService.active=true;DragonService.state="Starting full VPN";refreshStatus();if(Build.VERSION.SDK_INT>=26)startForegroundService(i);else startService(i);toast("Starting DragonTCP VPN...");}
|
||||
@Override protected void onActivityResult(int requestCode,int resultCode,Intent data){super.onActivityResult(requestCode,resultCode,data);if(requestCode!=VPN_REQUEST)return;if(resultCode==RESULT_OK&&pendingServiceIntent!=null){Intent i=pendingServiceIntent;pendingServiceIntent=null;launchService(i);}else{pendingServiceIntent=null;DragonService.active=false;DragonService.running=false;DragonService.state="VPN permission denied";refreshStatus();toast("VPN permission is required");}}
|
||||
private void stopDragon(){pendingServiceIntent=null;DragonService.state="Stopping...";refreshStatus();Intent s=new Intent(this,DragonService.class);s.setAction(DragonService.ACTION_STOP);try{startService(s);}catch(Exception e){stopService(new Intent(this,DragonService.class));}handler.postDelayed(()->{if(DragonService.active)stopService(new Intent(MainActivity.this,DragonService.class));refreshStatus();},1800);}
|
||||
private void clearLog(){try{File f=new File(getFilesDir(),"dragontcp.log");new java.io.FileOutputStream(f,false).close();lastLogText="";logs.setText("");}catch(Exception e){toast("Could not clear log: "+e.getMessage());}}
|
||||
private String readTail(File f,int maxBytes){if(!f.exists())return "";try(FileInputStream in=new FileInputStream(f)){long len=f.length();int n=(int)Math.min((long)maxBytes,len);byte[]buf=new byte[n];long skip=len-n;while(skip>0){long s=in.skip(skip);if(s<=0)break;skip-=s;}int off=0;while(off<n){int r=in.read(buf,off,n-off);if(r<0)break;off+=r;}return new String(buf,0,off,"UTF-8");}catch(Exception e){return "log error: "+e.getMessage();}}
|
||||
private void refreshStatus(){boolean a=DragonService.active,r=DragonService.running;status.setText((r?"● ":a?"◐ ":"○ ")+DragonService.state);status.setTextColor(r?Color.rgb(115,235,145):a?Color.rgb(255,205,95):Color.rgb(232,236,241));connectButton.setEnabled(!a);stopButton.setEnabled(a);String current=readTail(new File(getFilesDir(),"dragontcp.log"),131072);if(!current.equals(lastLogText)){lastLogText=current;logs.setText(current);logScroll.post(()->logScroll.fullScroll(View.FOCUS_DOWN));}}
|
||||
private void toast(String s){Toast.makeText(this,s,Toast.LENGTH_LONG).show();}
|
||||
@Override protected void onDestroy(){handler.removeCallbacks(refresher);super.onDestroy();}
|
||||
}
|
||||
Reference in New Issue
Block a user