我正在开发一个 P2P 聊天应用程序,我在其中连接设备并将其信息保存在本地 Room 数据库中。每次启动应用程序时,我都会开始广告和发现(同时),以便查看附近的其他设备,但也让其他设备看到,以便重新连接到我已经连接过一次的设备(已保存的设备)数据库内)。现在的问题是两个设备检测到对方触发 onEndpointFound 回调,在回调中它们都通过调用 requestConnection 向另一个设备执行连接请求。问题是两个设备上的两个呼叫都失败并且连接不是从任何设备启动的,因为我读到这应该发生在这里:附近的连接 2.0:双方都请求连接,但没有成功连接
我想知道 API 是否存在问题,或者我做错了什么。这是我在代码中处理重新连接的方式:
MainActivity.java
public class MainActivity extends AppCompatActivity {
public static final String TAG = MainActivity.class.getSimpleName();
public static final String SERVICE_ID = "com.example.snakemessenger";
public static AppDatabase db;
private TabLayout mTabLayout;
private SharedPreferences loginPreferences;
private static final String[] REQUIRED_PERMISSIONS =
new String[] {
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
};
private static final int REQUEST_CODE_REQUIRED_PERMISSIONS = 1;
public static final Strategy STRATEGY = Strategy.P2P_CLUSTER;
public static String codeName;
public static boolean discovering;
public static Map<String, String> pendingConnectionsData;
private EndpointDiscoveryCallback endpointDiscoveryCallback =
new EndpointDiscoveryCallback() {
@Override
public void onEndpointFound(@NonNull final String endpointId, @NonNull final DiscoveredEndpointInfo info) {
String endpointPhone = info.getEndpointName();
Log.d(TAG, "onEndpointFound: found device with endpointId " + endpointId + " and phone " + endpointPhone);
Contact contact = db.getContactDao().findByPhone(endpointPhone);
if (contact != null && !contact.isConnected()) {
Log.d(TAG, "onEndpointFound: device is in local DB. Sending connection request...");
Nearby.getConnectionsClient(getApplicationContext()).requestConnection(codeName, endpointId, connectionLifecycleCallback)
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "onEndpointFound: connection request successfully sent");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Toast.makeText(
MainActivity.this,
"There was a problem reconnecting to " + info.getEndpointName() + ".",
Toast.LENGTH_SHORT
).show();
Log.d(TAG, "onEndpointFound: failed to send connection request. Error: " + e.getMessage());
}
});
}
}
@Override
public void onEndpointLost(@NonNull String endpointId) {
Log.d(TAG, "onEndpointLost: device with endpointId " + endpointId + " went outside communication range.");
}
};
private final ConnectionLifecycleCallback connectionLifecycleCallback =
new ConnectionLifecycleCallback() {
@Override
public void onConnectionInitiated(@NonNull final String endpointId, @NonNull final ConnectionInfo connectionInfo) {
final String endpointPhone = connectionInfo.getEndpointName();
Log.d(TAG, "onConnectionInitiated: initiated connection with device having endpointId " + endpointId + " and phone " + endpointPhone);
final Contact contact = db.getContactDao().findByPhone(endpointPhone);
if (contact != null) {
Log.d(TAG, "onConnectionInitiated: Device is in local DB. Updating endpointId...");
contact.setEndpointID(endpointId);
db.getContactDao().updateContact(contact);
pendingConnectionsData.put(endpointId, connectionInfo.getEndpointName());
Nearby.getConnectionsClient(getApplicationContext()).acceptConnection(endpointId, payloadCallback)
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "onConnectionInitiated: Successfully accepted reconnection request for device");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
pendingConnectionsData.remove(endpointId);
Toast.makeText(
MainActivity.this,
"Failed to reconnect to " + endpointPhone + ".",
Toast.LENGTH_SHORT
).show();
Log.d(TAG, "onConnectionInitiated: Couldn't accept reconnection request for device");
}
});
} else {
Log.d(TAG, "onConnectionInitiated: Unrecognized device, adding request to connection requests...");
connectionRequests.add(new ConnectionRequest(endpointId, connectionInfo.getEndpointName()));
mTabLayout.getTabAt(3).getOrCreateBadge().setNumber(connectionRequests.size());
mTabLayout.getTabAt(3).getOrCreateBadge().setVisible(true);
pendingConnectionsData.put(endpointId, connectionInfo.getEndpointName());
}
}
@Override
public void onConnectionResult(String endpointId, ConnectionResolution result) {
String endpointPhone = pendingConnectionsData.get(endpointId);
Log.d(TAG, "onConnectionResult: result connection with device having endpointId " + endpointId + " and phone " + endpointPhone);
Contact contact = db.getContactDao().findByPhone(endpointPhone);
if (result.getStatus().getStatusCode() == ConnectionsStatusCodes.STATUS_OK) {
Log.d(TAG, "onConnectionResult: the connection is established");
if (contact == null) {
Log.d(TAG, "onConnectionResult: connected to a new device");
db.getContactDao().addContact(new Contact(0, endpointPhone, endpointPhone, endpointId, true));
removeConnectionRequest(endpointId);
} else {
Log.d(TAG, "onConnectionResult: reconnected to an old device");
contact.setConnected(true);
db.getContactDao().updateContact(contact);
}
} else if (result.getStatus().getStatusCode() == ConnectionsStatusCodes.STATUS_ERROR) {
Toast.makeText(
MainActivity.this,
"There was an error connecting with" + pendingConnectionsData.get(endpointId) + ".",
Toast.LENGTH_SHORT
).show();
Log.d(TAG, "onConnectionResult: couldn't establish a connection between devices");
}
pendingConnectionsData.remove(endpointId);
}
@Override
public void onDisconnected(@NonNull String endpointId) {
Contact contact = db.getContactDao().findById(endpointId);
contact.setConnected(false);
db.getContactDao().updateContact(contact);
Toast.makeText(
MainActivity.this,
contact.getName() + " disconnected.",
Toast.LENGTH_SHORT
).show();
Log.d(TAG, "onDisconnected: device with endpointId " + endpointId + " and phone " + contact.getPhone() + " disconnected.");
}
};
private PayloadCallback payloadCallback =
new PayloadCallback() {
@Override
public void onPayloadReceived(@NonNull String endpointId, @NonNull Payload payload) {
}
@Override
public void onPayloadTransferUpdate(@NonNull String endpointId, @NonNull PayloadTransferUpdate payloadTransferUpdate) {
}
};
public static List<ConnectionRequest> connectionRequests;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar mToolbar = findViewById(R.id.main_page_toolbar);
mToolbar.setTitle("Snake Messenger");
setSupportActionBar(mToolbar);
connectionRequests = new ArrayList<>();
pendingConnectionsData = new HashMap<>();
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "SnakeMessengerDB")
.allowMainThreadQueries()
.build();
Log.d(TAG, "onCreate: initialized Room DB");
loginPreferences = getApplicationContext().getSharedPreferences("LOGIN_DETAILS", MODE_PRIVATE);
boolean signedIn = loginPreferences.getBoolean("signedIn", false);
if (!signedIn) {
Log.d(TAG, "onCreate: user is not signed in. Sending him to login activity...");
sendUserToLoginActivity();
}
codeName = loginPreferences.getString("phone", "");
Log.d(TAG, "onCreate: user is signed in and will advertise using codeName " + codeName);
ViewPager2 mViewPager2 = findViewById(R.id.main_tabs_pager);
mViewPager2.setAdapter(new TabsAccessorAdapter(this));
mTabLayout = findViewById(R.id.main_tabs);
TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(mTabLayout, mViewPager2, new TabLayoutMediator.TabConfigurationStrategy() {
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
switch (position) {
case 0:
tab.setText("Chats");
break;
case 1:
tab.setText("Groups");
break;
case 2:
tab.setText("Contacts");
break;
case 3:
tab.setText("Requests");
BadgeDrawable badgeDrawable = tab.getOrCreateBadge();
badgeDrawable.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.red));
if (!connectionRequests.isEmpty()) {
badgeDrawable.setNumber(connectionRequests.size());
badgeDrawable.setVisible(true);
} else {
badgeDrawable.setVisible(false);
}
break;
default:
break;
}
}
});
tabLayoutMediator.attach();
if (!hasPermissions(this, REQUIRED_PERMISSIONS)) {
Log.d(TAG, "onCreate: app does not have all the required permissions. Requesting permissions...");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS);
}
}
startAdvertising();
startDiscovering();
}
@Override
protected void onDestroy() {
stopAdvertising();
stopDiscovering();
Nearby.getConnectionsClient(getApplicationContext()).stopAllEndpoints();
db.getContactDao().disconnectContacts();
super.onDestroy();
}
public void startAdvertising() {
AdvertisingOptions advertisingOptions =
new AdvertisingOptions.Builder().setStrategy(STRATEGY).build();
Nearby.getConnectionsClient(getApplicationContext())
.startAdvertising(
codeName, SERVICE_ID, connectionLifecycleCallback, advertisingOptions)
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "startAdvertising: successfully started advertising");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Toast.makeText(MainActivity.this, "There was a problem starting advertising.", Toast.LENGTH_SHORT).show();
Log.d(TAG, "startAdvertising: couldn't start advertising.Error: " + e.getMessage());
}
});
}
public void stopAdvertising() {
Nearby.getConnectionsClient(getApplicationContext()).stopAdvertising();
Log.d(TAG, "stopAdvertising: stopped advertising");
}
public void startDiscovering() {
DiscoveryOptions discoveryOptions =
new DiscoveryOptions.Builder().setStrategy(MainActivity.STRATEGY).build();
Nearby.getConnectionsClient(getApplicationContext())
.startDiscovery(MainActivity.SERVICE_ID, endpointDiscoveryCallback, discoveryOptions)
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "startDiscovering: started discovering");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Toast.makeText(
MainActivity.this,
"There was a problem starting discovering.",
Toast.LENGTH_SHORT
).show();
Log.d(TAG, "startDiscovering: couldn't start discovering. Error: " + e.getMessage());
}
});
discovering = true;
}
public void stopDiscovering() {
Nearby.getConnectionsClient(getApplicationContext()).stopDiscovery();
discovering = false;
Log.d(TAG, "stopDiscovering: stopped discovering");
}
private static boolean hasPermissions(Context context, String... permissions) {
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
@CallSuper
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode != REQUEST_CODE_REQUIRED_PERMISSIONS) {
return;
}
for (int grantResult : grantResults) {
if (grantResult == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, "Missing permissions", Toast.LENGTH_LONG).show();
finish();
return;
}
}
recreate();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.options_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.main_settings_option) {
Log.d(TAG, "onOptionsItemSelected: settings option selected");
sendUserToSettingsActivity();
} else if (item.getItemId() == R.id.main_sign_out_option) {
Log.d(TAG, "onOptionsItemSelected: sign out option selected");
SharedPreferences.Editor editor = loginPreferences.edit();
editor.putBoolean("signedIn", false);
editor.apply();
sendUserToLoginActivity();
Toast.makeText(MainActivity.this, "Signed out", Toast.LENGTH_SHORT).show();
}
return true;
}
private void sendUserToSettingsActivity() {
Log.d(TAG, "sendUserToSettingActivity: starting settings activity...");
Intent settingsIntent = new Intent(MainActivity.this, SettingsActivity.class);
startActivity(settingsIntent);
}
private void sendUserToLoginActivity() {
Log.d(TAG, "sendUserToLoginActivity: starting login activity...");
Intent loginIntent = new Intent(MainActivity.this, SignInActivity.class);
loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(loginIntent);
finish();
}
private void removeConnectionRequest(String endpointId) {
Iterator<ConnectionRequest> it = connectionRequests.iterator();
while (it.hasNext()) {
ConnectionRequest connectionRequest = it.next();
if (connectionRequest.getSenderID().equals(endpointId)) {
it.remove();
Log.d(TAG, "removeConnectionRequest: removed connection request coming from device with endpointId " + endpointId);
}
}
if (!connectionRequests.isEmpty()) {
mTabLayout.getTabAt(3).getOrCreateBadge().setNumber(connectionRequests.size());
} else {
mTabLayout.getTabAt(3).getOrCreateBadge().setVisible(false);
}
}
}
这是我正在测试应用程序的两台设备的 Logcat。我正在使用运行 Android 10 的三星 Galaxy S10+ 和三星 Galaxy Tab A。
三星 Galaxy S10+ 日志:
2020-07-29 14:48:30.695 25915-25915/com.example.snakemessenger D/MainActivity: onCreate: initialized Room DB
2020-07-29 14:48:30.702 25915-25915/com.example.snakemessenger D/MainActivity: onCreate: user is signed in and will advertise using codeName 0768803341
2020-07-29 14:48:32.505 25915-25915/com.example.snakemessenger D/MainActivity: startAdvertising: successfully started advertising
2020-07-29 14:48:32.562 25915-25915/com.example.snakemessenger D/MainActivity: startDiscovering: started discovering
2020-07-29 14:48:48.259 25915-25915/com.example.snakemessenger D/MainActivity: onEndpointFound: found device with endpointId sKZ6 and phone 0748489006
2020-07-29 14:48:48.308 25915-25915/com.example.snakemessenger D/MainActivity: onEndpointFound: device is in local DB. Sending connection request...
2020-07-29 14:49:09.000 25915-25915/com.example.snakemessenger D/MainActivity: onEndpointFound: failed to send connection request. Error: 8012: STATUS_ENDPOINT_IO_ERROR
三星 Galaxy Tab A logcat:
2020-07-29 14:48:40.517 29794-29794/com.example.snakemessenger D/MainActivity: startAdvertising: successfully started advertising
2020-07-29 14:48:40.974 29794-29794/com.example.snakemessenger D/MainActivity: startDiscovering: started discovering
2020-07-29 14:48:43.737 29794-29794/com.example.snakemessenger D/MainActivity: onEndpointFound: found device with endpointId oy54 and phone 0768803341
2020-07-29 14:48:43.839 29794-29794/com.example.snakemessenger D/MainActivity: onEndpointFound: device is in local DB. Sending connection request...
2020-07-29 14:49:06.393 29794-29794/com.example.snakemessenger D/MainActivity: onEndpointFound: failed to send connection request. Error: 8012: STATUS_ENDPOINT_IO_ERROR
另外,这是我的 build.gradle 文件,以防我在这里遗漏一些东西:
apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
android {
compileSdkVersion 30
buildToolsVersion '30.0.0'
defaultConfig {
applicationId "com.example.snakemessenger"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.google.android.gms:play-services-nearby:17.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc01"
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.navigation:navigation-fragment:2.3.0'
implementation 'androidx.navigation:navigation-ui:2.3.0'
implementation 'com.google.android.material:material:1.3.0-alpha02'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
更新
经过更多研究后,我注意到有时设备会设法连接,因此我决定在 requestConnection 调用失败时停止发现,等待 3 秒并通过执行以下操作重新启动发现:
Nearby.getConnectionsClient(getApplicationContext()).requestConnection(codeName, endpointId, connectionLifecycleCallback)
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "onEndpointFound: connection request successfully sent");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.d(TAG, "onEndpointFound: failed to send connection request. Error: " + e.getMessage());
stopDiscovering();
Runnable startDiscoveryAgain = new Runnable() {
@Override
public void run() {
startDiscovering();
}
};
Handler handler = new Handler();
handler.postDelayed(startDiscoveryAgain, 3000);
}
});
这个解决方案好还是有比这更好的解决方法?