mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-18 18:42:46 +00:00
Add support for zero configuration IPv6 streaming
This commit is contained in:
parent
ba0171221c
commit
83b66b19de
@ -8,6 +8,7 @@ import java.security.cert.X509Certificate;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
@ -18,16 +19,16 @@ import android.database.sqlite.SQLiteDatabase;
|
|||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
public class ComputerDatabaseManager {
|
public class ComputerDatabaseManager {
|
||||||
private static final String COMPUTER_DB_NAME = "computers2.db";
|
private static final String COMPUTER_DB_NAME = "computers3.db";
|
||||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||||
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
|
private static final String ADDRESSES_COLUMN_NAME = "Addresses";
|
||||||
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
|
|
||||||
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
|
|
||||||
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
||||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||||
|
|
||||||
|
private static final char ADDRESS_DELIMITER = ';';
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
public ComputerDatabaseManager(Context c) {
|
public ComputerDatabaseManager(Context c) {
|
||||||
@ -47,27 +48,21 @@ public class ComputerDatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeDb(Context c) {
|
private void initializeDb(Context c) {
|
||||||
// Add cert column to the table if not present
|
|
||||||
try {
|
|
||||||
computerDb.execSQL(String.format((Locale)null,
|
|
||||||
"ALTER TABLE %s ADD COLUMN %s TEXT",
|
|
||||||
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
|
|
||||||
} catch (SQLiteException e) {}
|
|
||||||
|
|
||||||
|
|
||||||
// Create tables if they aren't already there
|
// Create tables if they aren't already there
|
||||||
computerDb.execSQL(String.format((Locale)null,
|
computerDb.execSQL(String.format((Locale)null,
|
||||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
|
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT, %s TEXT)",
|
||||||
COMPUTER_TABLE_NAME,
|
COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||||
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
||||||
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
|
|
||||||
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
|
||||||
|
|
||||||
// Move all computers from the old DB (if any) to the new one
|
// Move all computers from the old DB (if any) to the new one
|
||||||
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
||||||
for (ComputerDetails computer : oldComputers) {
|
for (ComputerDetails computer : oldComputers) {
|
||||||
updateComputer(computer);
|
updateComputer(computer);
|
||||||
}
|
}
|
||||||
|
oldComputers = LegacyDatabaseReader2.migrateAllComputers(c);
|
||||||
|
for (ComputerDetails computer : oldComputers) {
|
||||||
|
updateComputer(computer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteComputer(String name) {
|
public void deleteComputer(String name) {
|
||||||
@ -78,9 +73,14 @@ public class ComputerDatabaseManager {
|
|||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
||||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||||
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
|
|
||||||
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
StringBuilder addresses = new StringBuilder();
|
||||||
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
addresses.append(details.localAddress != null ? details.localAddress : "").append(ADDRESS_DELIMITER);
|
||||||
|
addresses.append(details.remoteAddress != null ? details.remoteAddress : "").append(ADDRESS_DELIMITER);
|
||||||
|
addresses.append(details.manualAddress != null ? details.manualAddress : "").append(ADDRESS_DELIMITER);
|
||||||
|
addresses.append(details.ipv6Address != null ? details.ipv6Address : "").append(ADDRESS_DELIMITER);
|
||||||
|
|
||||||
|
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
|
||||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
||||||
try {
|
try {
|
||||||
if (details.serverCert != null) {
|
if (details.serverCert != null) {
|
||||||
@ -96,18 +96,31 @@ public class ComputerDatabaseManager {
|
|||||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String readNonEmptyString(String input) {
|
||||||
|
if (input.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
details.uuid = c.getString(0);
|
details.uuid = c.getString(0);
|
||||||
details.name = c.getString(1);
|
details.name = c.getString(1);
|
||||||
details.localAddress = c.getString(2);
|
|
||||||
details.remoteAddress = c.getString(3);
|
Scanner s = new Scanner(c.getString(2)).useDelimiter(""+ADDRESS_DELIMITER);
|
||||||
details.manualAddress = c.getString(4);
|
|
||||||
details.macAddress = c.getString(5);
|
details.localAddress = readNonEmptyString(s.next());
|
||||||
|
details.remoteAddress = readNonEmptyString(s.next());
|
||||||
|
details.manualAddress = readNonEmptyString(s.next());
|
||||||
|
details.ipv6Address = readNonEmptyString(s.next());
|
||||||
|
|
||||||
|
details.macAddress = c.getString(3);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] derCertData = c.getBlob(6);
|
byte[] derCertData = c.getBlob(4);
|
||||||
|
|
||||||
if (derCertData != null) {
|
if (derCertData != null) {
|
||||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
@ -220,8 +220,8 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
||||||
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(String name) {
|
||||||
@ -297,8 +297,25 @@ public class ComputerManagerService extends Service {
|
|||||||
return new MdnsDiscoveryListener() {
|
return new MdnsDiscoveryListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
|
// Populate the computer template with mDNS info
|
||||||
|
if (computer.getAddressV4() != null) {
|
||||||
|
details.localAddress = computer.getAddressV4().getHostAddress();
|
||||||
|
}
|
||||||
|
if (computer.getAddressV6() != null) {
|
||||||
|
details.ipv6Address = computer.getAddressV6().getHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we're on the same network, we can use STUN to find
|
||||||
|
// our WAN address, which is also very likely the WAN address
|
||||||
|
// of the PC. We can use this later to connect remotely.
|
||||||
|
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||||
|
|
||||||
// Kick off a serverinfo poll on this machine
|
// Kick off a serverinfo poll on this machine
|
||||||
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
if (!addComputerBlocking(details)) {
|
||||||
|
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -345,24 +362,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
||||||
// Setup a placeholder
|
|
||||||
ComputerDetails fakeDetails = new ComputerDetails();
|
|
||||||
|
|
||||||
if (manuallyAdded) {
|
|
||||||
// Add PC UI
|
|
||||||
fakeDetails.manualAddress = addr;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// mDNS
|
|
||||||
fakeDetails.localAddress = addr;
|
|
||||||
|
|
||||||
// Since we're on the same network, we can use STUN to find
|
|
||||||
// our WAN address, which is also very likely the WAN address
|
|
||||||
// of the PC. We can use this later to connect remotely.
|
|
||||||
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
try {
|
try {
|
||||||
// We cannot use runPoll() here because it will attempt to persist the state of the machine
|
// We cannot use runPoll() here because it will attempt to persist the state of the machine
|
||||||
@ -395,10 +395,6 @@ public class ComputerManagerService extends Service {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!manuallyAdded) {
|
|
||||||
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -514,14 +510,16 @@ public class ComputerManagerService extends Service {
|
|||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException {
|
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress, final String ipv6Address) throws InterruptedException {
|
||||||
final boolean[] remoteInfo = new boolean[2];
|
final boolean[] remoteInfo = new boolean[2];
|
||||||
final boolean[] localInfo = new boolean[2];
|
final boolean[] localInfo = new boolean[2];
|
||||||
final boolean[] manualInfo = new boolean[2];
|
final boolean[] manualInfo = new boolean[2];
|
||||||
|
final boolean[] ipv6Info = new boolean[2];
|
||||||
|
|
||||||
startFastPollThread(localAddress, localInfo);
|
startFastPollThread(localAddress, localInfo);
|
||||||
startFastPollThread(remoteAddress, remoteInfo);
|
startFastPollThread(remoteAddress, remoteInfo);
|
||||||
startFastPollThread(manualAddress, manualInfo);
|
startFastPollThread(manualAddress, manualInfo);
|
||||||
|
startFastPollThread(ipv6Address, ipv6Info);
|
||||||
|
|
||||||
// Check local first
|
// Check local first
|
||||||
synchronized (localInfo) {
|
synchronized (localInfo) {
|
||||||
@ -545,7 +543,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// And finally, remote
|
// Now remote IPv4
|
||||||
synchronized (remoteInfo) {
|
synchronized (remoteInfo) {
|
||||||
while (!remoteInfo[0]) {
|
while (!remoteInfo[0]) {
|
||||||
remoteInfo.wait(500);
|
remoteInfo.wait(500);
|
||||||
@ -556,6 +554,17 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now global IPv6
|
||||||
|
synchronized (ipv6Info) {
|
||||||
|
while (!ipv6Info[0]) {
|
||||||
|
ipv6Info.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ipv6Info[1]) {
|
||||||
|
return ipv6Address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,8 +575,8 @@ public class ComputerManagerService extends Service {
|
|||||||
// Do not write this address to details.activeAddress because:
|
// Do not write this address to details.activeAddress because:
|
||||||
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
|
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
|
||||||
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
|
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
|
||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")");
|
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
|
||||||
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
|
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address);
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
|
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
// If no connection could be established to either IP address, there's nothing we can do
|
||||||
@ -582,8 +591,9 @@ public class ComputerManagerService extends Service {
|
|||||||
// already tried
|
// already tried
|
||||||
HashSet<String> uniqueAddresses = new HashSet<>();
|
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||||
uniqueAddresses.add(details.localAddress);
|
uniqueAddresses.add(details.localAddress);
|
||||||
uniqueAddresses.add(details.remoteAddress);
|
|
||||||
uniqueAddresses.add(details.manualAddress);
|
uniqueAddresses.add(details.manualAddress);
|
||||||
|
uniqueAddresses.add(details.remoteAddress);
|
||||||
|
uniqueAddresses.add(details.ipv6Address);
|
||||||
for (String addr : uniqueAddresses) {
|
for (String addr : uniqueAddresses) {
|
||||||
if (addr == null || addr.equals(candidateAddress)) {
|
if (addr == null || addr.equals(candidateAddress)) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LegacyDatabaseReader2 {
|
||||||
|
private static final String COMPUTER_DB_NAME = "computers2.db";
|
||||||
|
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||||
|
|
||||||
|
private static ComputerDetails getComputerFromCursor(Cursor c) {
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
|
details.uuid = c.getString(0);
|
||||||
|
details.name = c.getString(1);
|
||||||
|
details.localAddress = c.getString(2);
|
||||||
|
details.remoteAddress = c.getString(3);
|
||||||
|
details.manualAddress = c.getString(4);
|
||||||
|
details.macAddress = c.getString(5);
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] derCertData = c.getBlob(6);
|
||||||
|
|
||||||
|
if (derCertData != null) {
|
||||||
|
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||||
|
}
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
|
||||||
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
|
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
|
|
||||||
|
// If a critical field is corrupt or missing, skip the database entry
|
||||||
|
if (details.uuid == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
computerList.add(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.close();
|
||||||
|
|
||||||
|
return computerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
||||||
|
SQLiteDatabase computerDb = null;
|
||||||
|
try {
|
||||||
|
// Open the existing database
|
||||||
|
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||||
|
return getAllComputers(computerDb);
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
return new LinkedList<ComputerDetails>();
|
||||||
|
} finally {
|
||||||
|
// Close and delete the old DB
|
||||||
|
if (computerDb != null) {
|
||||||
|
computerDb.close();
|
||||||
|
}
|
||||||
|
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue;
|
|||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
@ -98,7 +99,9 @@ public class AddComputerManually extends Activity {
|
|||||||
getResources().getString(R.string.msg_add_pc), false);
|
getResources().getString(R.string.msg_add_pc), false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
success = managerBinder.addComputerBlocking(host, true);
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
details.manualAddress = host;
|
||||||
|
success = managerBinder.addComputerBlocking(details);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||||
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 10346a285be604ca0dcafe96b337734abde89da5
|
Subproject commit 3cfdf4dae21bfe38abd32107474f2619fea82078
|
Loading…
x
Reference in New Issue
Block a user