@@ -210,6 +210,7 @@ boolean isConnected() {
210210 private PsiphonTunnel m_tunnel ;
211211 private VpnManager m_vpnManager = VpnManager .getInstance ();
212212 private String m_lastUpstreamProxyErrorMessage ;
213+ private volatile String m_lastInproxyDiagMsg = "" ;
213214 private Handler m_Handler = new Handler ();
214215
215216 private PendingIntent m_notificationPendingIntent ;
@@ -1702,12 +1703,158 @@ else if (message.contains("tunnel connected (protocol:")) {
17021703 MyLog .i (R .string .tunnel_connected_protocol , MyLog .Sensitivity .NOT_SENSITIVE , protocol );
17031704 }
17041705 }
1706+
1707+ // Inproxy connection diagnostic messages
1708+ // These show the phase-by-phase progress of each relay attempt.
1709+ // Note: PsiphonTunnel.java dispatches diagnostic notices via two
1710+ // code paths (PsiphonProviderNoticeHandler and handleNotice),
1711+ // causing duplicate onDiagnosticMessage calls. We deduplicate
1712+ // by tracking the last displayed message.
1713+ if (message .contains ("inproxy-dial:" )) {
1714+ String diagMsg = parseInproxyDiagnostic (message );
1715+ if (diagMsg != null ) {
1716+ // Deduplicate: suppress if identical to last message
1717+ if (!diagMsg .equals (m_lastInproxyDiagMsg )) {
1718+ m_lastInproxyDiagMsg = diagMsg ;
1719+ MyLog .i (R .string .conduit_diag , MyLog .Sensitivity .NOT_SENSITIVE , diagMsg );
1720+ }
1721+ }
1722+ // Don't log the raw diagnostic JSON for inproxy-dial
1723+ return ;
1724+ }
17051725
17061726 MyLog .i (now , message );
17071727 }
17081728 });
17091729 }
17101730
1731+ /**
1732+ * Format a Go duration string (e.g. "98.381771ms", "1.702052239s",
1733+ * "3m2.5s") into a human-friendly rounded form ("98ms", "1.7s", "3m2s").
1734+ */
1735+ private static String formatDuration (String goDuration ) {
1736+ if (goDuration == null ) return null ;
1737+ try {
1738+ // Handle seconds: "1.702052239s" -> "1.7s"
1739+ if (goDuration .endsWith ("s" ) && !goDuration .endsWith ("ms" )) {
1740+ // Could be "3m2.5s" — find the last non-digit-dot prefix
1741+ int sIdx = goDuration .lastIndexOf ('s' );
1742+ // Find where the seconds number starts (after 'm' if present)
1743+ int mIdx = goDuration .indexOf ('m' );
1744+ String secPart ;
1745+ String prefix = "" ;
1746+ if (mIdx >= 0 && mIdx < sIdx ) {
1747+ prefix = goDuration .substring (0 , mIdx + 1 );
1748+ secPart = goDuration .substring (mIdx + 1 , sIdx );
1749+ } else {
1750+ secPart = goDuration .substring (0 , sIdx );
1751+ }
1752+ double secs = Double .parseDouble (secPart );
1753+ if (secs < 10 ) {
1754+ return prefix + String .format ("%.1fs" , secs );
1755+ } else {
1756+ return prefix + String .format ("%.0fs" , secs );
1757+ }
1758+ }
1759+ // Handle milliseconds: "98.381771ms" -> "98ms"
1760+ if (goDuration .endsWith ("ms" )) {
1761+ String numPart = goDuration .substring (0 , goDuration .length () - 2 );
1762+ double ms = Double .parseDouble (numPart );
1763+ return String .format ("%.0fms" , ms );
1764+ }
1765+ // Handle microseconds: "532.1µs" -> "<1ms"
1766+ if (goDuration .contains ("µs" ) || goDuration .contains ("us" )) {
1767+ return "<1ms" ;
1768+ }
1769+ } catch (NumberFormatException e ) {
1770+ // Fall through to return original
1771+ }
1772+ return goDuration ;
1773+ }
1774+
1775+ /**
1776+ * Parse inproxy diagnostic messages from tunnel-core and return a
1777+ * human-readable string for display in the logs tab.
1778+ *
1779+ * Messages arrive from PsiphonTunnel.java formatted as:
1780+ * "Info: {"message":"inproxy-dial: ICE gathering OK","duration":"320ms","trace":"..."}"
1781+ *
1782+ * i.e. noticeType + ": " + data.toString()
1783+ *
1784+ * We strip the noticeType prefix, parse the JSON data object, extract the
1785+ * phase from "message", and append key diagnostic fields.
1786+ */
1787+ private static String parseInproxyDiagnostic (String diagnosticMessage ) {
1788+ try {
1789+ // The message format is "NoticeType: {json data object}"
1790+ // Strip the prefix to get the JSON data object
1791+ String jsonStr = diagnosticMessage ;
1792+ int colonSpace = diagnosticMessage .indexOf (": {" );
1793+ if (colonSpace >= 0 ) {
1794+ jsonStr = diagnosticMessage .substring (colonSpace + 2 );
1795+ }
1796+
1797+ org .json .JSONObject data = new org .json .JSONObject (jsonStr );
1798+
1799+ String msg = data .optString ("message" , "" );
1800+ if (!msg .startsWith ("inproxy-dial:" )) return null ;
1801+
1802+ // Extract the phase description after "inproxy-dial: "
1803+ String phase = msg .substring ("inproxy-dial: " .length ());
1804+
1805+ StringBuilder sb = new StringBuilder (phase );
1806+
1807+ // Append diagnostic fields if present
1808+ String duration = formatDuration (data .optString ("duration" , null ));
1809+ if (duration != null ) {
1810+ sb .append (" (" ).append (duration ).append (")" );
1811+ }
1812+
1813+ String timeout = formatDuration (data .optString ("timeout" , null ));
1814+ if (timeout != null ) {
1815+ sb .append (" [timeout=" ).append (timeout ).append ("]" );
1816+ }
1817+
1818+ String natType = data .optString ("natType" , null );
1819+ if (natType != null ) {
1820+ sb .append (" [NAT=" ).append (natType ).append ("]" );
1821+ }
1822+
1823+ String attempt = data .optString ("attempt" , null );
1824+ if (attempt != null ) {
1825+ sb .insert (0 , "#" + attempt + " " );
1826+ }
1827+
1828+ String error = data .optString ("error" , null );
1829+ if (error != null ) {
1830+ // Truncate long error messages for readability
1831+ if (error .length () > 120 ) {
1832+ error = error .substring (0 , 120 ) + "..." ;
1833+ }
1834+ sb .append (" | " ).append (error );
1835+ }
1836+
1837+ return sb .toString ();
1838+ } catch (org .json .JSONException e ) {
1839+ // Fallback: extract just the phase text after "inproxy-dial: "
1840+ // and before any JSON artifacts (comma or closing brace)
1841+ if (diagnosticMessage .contains ("inproxy-dial:" )) {
1842+ int idx = diagnosticMessage .indexOf ("inproxy-dial: " );
1843+ if (idx >= 0 ) {
1844+ String remainder = diagnosticMessage .substring (idx + "inproxy-dial: " .length ());
1845+ // Stop at first JSON artifact
1846+ int end = remainder .indexOf ("\" ," );
1847+ if (end < 0 ) end = remainder .indexOf ("\" }" );
1848+ if (end > 0 ) {
1849+ return remainder .substring (0 , end );
1850+ }
1851+ return remainder ;
1852+ }
1853+ }
1854+ return null ;
1855+ }
1856+ }
1857+
17111858 @ Override
17121859 public void onAvailableEgressRegions (final List <String > regions ) {
17131860 m_Handler .post (new Runnable () {
0 commit comments