Merge pull request #2656 from nextcloud/chore/noid/spotbugsImprovements

Spotbugs improvements
This commit is contained in:
Andy Scherzinger 2022-12-29 12:06:41 +01:00 committed by GitHub
commit 34cd485d4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 421 additions and 321 deletions

View File

@ -90,7 +90,6 @@ import com.nextcloud.talk.utils.NotificationUtils;
import com.nextcloud.talk.utils.animations.PulseAnimation; import com.nextcloud.talk.utils.animations.PulseAnimation;
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil; import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil;
import com.nextcloud.talk.utils.power.PowerManagerUtils; import com.nextcloud.talk.utils.power.PowerManagerUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder; import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder;
import com.nextcloud.talk.webrtc.MagicWebRTCUtils; import com.nextcloud.talk.webrtc.MagicWebRTCUtils;
import com.nextcloud.talk.webrtc.MagicWebSocketInstance; import com.nextcloud.talk.webrtc.MagicWebSocketInstance;
@ -100,7 +99,6 @@ import com.nextcloud.talk.webrtc.WebSocketConnectionHelper;
import com.wooplr.spotlight.SpotlightView; import com.wooplr.spotlight.SpotlightView;
import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringEscapeUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.webrtc.AudioSource; import org.webrtc.AudioSource;
@ -178,14 +176,13 @@ public class CallActivity extends CallBaseActivity {
@Inject @Inject
NcApi ncApi; NcApi ncApi;
@Inject
EventBus eventBus;
@Inject @Inject
UserManager userManager; UserManager userManager;
@Inject
AppPreferences appPreferences;
@Inject @Inject
Cache cache; Cache cache;
@Inject @Inject
PlatformPermissionUtil permissionUtil; PlatformPermissionUtil permissionUtil;
@ -367,7 +364,7 @@ public class CallActivity extends CallBaseActivity {
powerManagerUtils = new PowerManagerUtils(); powerManagerUtils = new PowerManagerUtils();
if (extras.getString("state", "").equalsIgnoreCase("resume")) { if ("resume".equalsIgnoreCase(extras.getString("state", ""))) {
setCallState(CallStatus.IN_CONVERSATION); setCallState(CallStatus.IN_CONVERSATION);
} else { } else {
setCallState(CallStatus.CONNECTING); setCallState(CallStatus.CONNECTING);
@ -592,7 +589,7 @@ public class CallActivity extends CallBaseActivity {
private void handleFromNotification() { private void handleFromNotification() {
int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1}); int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, baseUrl), false) ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, baseUrl), Boolean.FALSE)
.retry(3) .retry(3)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())

View File

@ -21,7 +21,7 @@ public abstract class CallBaseActivity extends BaseActivity {
public static final String TAG = "CallBaseActivity"; public static final String TAG = "CallBaseActivity";
public PictureInPictureParams.Builder mPictureInPictureParamsBuilder; public PictureInPictureParams.Builder mPictureInPictureParamsBuilder;
public Boolean isInPipMode = false; public Boolean isInPipMode = Boolean.FALSE;
long onCreateTime; long onCreateTime;
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")

View File

@ -125,9 +125,9 @@ public class AdvancedUserItem extends AbstractFlexibleItem<AdvancedUserItem.User
} }
} }
if (user != null && user.getBaseUrl() != null && if (user != null &&
user.getBaseUrl().startsWith("http://") || user.getBaseUrl() != null &&
user.getBaseUrl().startsWith("https://")) { (user.getBaseUrl().startsWith("http://") || user.getBaseUrl().startsWith("https://"))) {
ImageViewExtensionsKt.loadAvatar(holder.binding.userIcon, user, participant.getCalculatedActorId(), true); ImageViewExtensionsKt.loadAvatar(holder.binding.userIcon, user, participant.getCalculatedActorId(), true);
} }
} }

View File

@ -38,6 +38,7 @@ import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.ui.theme.ViewThemeUtils; import com.nextcloud.talk.ui.theme.ViewThemeUtils;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import androidx.core.content.res.ResourcesCompat; import androidx.core.content.res.ResourcesCompat;
@ -171,10 +172,15 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
if (!TextUtils.isEmpty(participant.getDisplayName())) { if (!TextUtils.isEmpty(participant.getDisplayName())) {
displayName = participant.getDisplayName(); displayName = participant.getDisplayName();
} else { } else {
displayName = NextcloudTalkApplication.Companion.getSharedApplication() displayName = Objects.requireNonNull(NextcloudTalkApplication.Companion.getSharedApplication())
.getResources().getString(R.string.nc_guest); .getResources().getString(R.string.nc_guest);
} }
// absolute fallback to prevent NPE deference
if (displayName == null) {
displayName = "Guest";
}
ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, user, displayName, true); ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, user, displayName, true);
} else if (participant.getCalculatedActorType() == Participant.ActorType.USERS || } else if (participant.getCalculatedActorType() == Participant.ActorType.USERS ||
PARTICIPANT_SOURCE_USERS.equals(participant.getSource())) { PARTICIPANT_SOURCE_USERS.equals(participant.getSource())) {

View File

@ -28,6 +28,7 @@ import com.nextcloud.talk.R;
import com.nextcloud.talk.databinding.RvItemNotificationSoundBinding; import com.nextcloud.talk.databinding.RvItemNotificationSoundBinding;
import java.util.List; import java.util.List;
import java.util.Objects;
import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
@ -54,7 +55,25 @@ public class NotificationSoundItem extends AbstractFlexibleItem<NotificationSoun
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
return false; if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
NotificationSoundItem that = (NotificationSoundItem) o;
if (!Objects.equals(notificationSoundName, that.notificationSoundName)) {
return false;
}
return Objects.equals(notificationSoundUri, that.notificationSoundUri);
}
@Override
public int hashCode() {
int result = notificationSoundName != null ? notificationSoundName.hashCode() : 0;
return 31 * result + (notificationSoundUri != null ? notificationSoundUri.hashCode() : 0);
} }
@Override @Override

View File

@ -154,8 +154,11 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
participant.getType() == Participant.ParticipantType.GUEST || participant.getType() == Participant.ParticipantType.GUEST ||
participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) { participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) {
String displayName = NextcloudTalkApplication.Companion.getSharedApplication() String displayName = NextcloudTalkApplication
.getResources().getString(R.string.nc_guest); .Companion
.getSharedApplication()
.getResources()
.getString(R.string.nc_guest);
if (!TextUtils.isEmpty(participant.getDisplayName())) { if (!TextUtils.isEmpty(participant.getDisplayName())) {
displayName = participant.getDisplayName(); displayName = participant.getDisplayName();
@ -164,8 +167,11 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
ImageViewExtensionsKt.loadGuestAvatar(holder.binding.avatarView, user, displayName, false); ImageViewExtensionsKt.loadGuestAvatar(holder.binding.avatarView, user, displayName, false);
} else if (participant.getCalculatedActorType() == Participant.ActorType.USERS || } else if (participant.getCalculatedActorType() == Participant.ActorType.USERS ||
participant.getSource().equals("users")) { "users".equals(participant.getSource())) {
ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView, user, participant.getCalculatedActorId(), true); ImageViewExtensionsKt.loadAvatar(holder.binding.avatarView,
user,
participant.getCalculatedActorId(),
true);
} }
Resources resources = NextcloudTalkApplication.Companion.getSharedApplication().getResources(); Resources resources = NextcloudTalkApplication.Companion.getSharedApplication().getResources();
@ -190,63 +196,61 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
holder.binding.videoCallIcon.setVisibility(View.GONE); holder.binding.videoCallIcon.setVisibility(View.GONE);
} }
if (holder.binding.secondaryText != null) { String userType = "";
String userType = "";
switch (new EnumParticipantTypeConverter().convertToInt(participant.getType())) { switch (new EnumParticipantTypeConverter().convertToInt(participant.getType())) {
case 1: case 1:
//userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_owner); //userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_owner);
//break; //break;
case 2: case 2:
case 6: // Guest moderator case 6: // Guest moderator
userType = NextcloudTalkApplication
.Companion
.getSharedApplication()
.getString(R.string.nc_moderator);
break;
case 3:
userType = NextcloudTalkApplication
.Companion
.getSharedApplication()
.getString(R.string.nc_user);
if (participant.getCalculatedActorType() == Participant.ActorType.GROUPS) {
userType = NextcloudTalkApplication userType = NextcloudTalkApplication
.Companion .Companion
.getSharedApplication() .getSharedApplication()
.getString(R.string.nc_moderator); .getString(R.string.nc_group);
break; }
case 3: if (participant.getCalculatedActorType() == Participant.ActorType.CIRCLES) {
userType = NextcloudTalkApplication userType = NextcloudTalkApplication
.Companion .Companion
.getSharedApplication() .getSharedApplication()
.getString(R.string.nc_user); .getString(R.string.nc_circle);
if (participant.getCalculatedActorType() == Participant.ActorType.GROUPS) { }
userType = NextcloudTalkApplication break;
.Companion case 4:
.getSharedApplication() userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest);
.getString(R.string.nc_group); if (participant.getCalculatedActorType() == Participant.ActorType.EMAILS) {
}
if (participant.getCalculatedActorType() == Participant.ActorType.CIRCLES) {
userType = NextcloudTalkApplication
.Companion
.getSharedApplication()
.getString(R.string.nc_circle);
}
break;
case 4:
userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest);
if (participant.getCalculatedActorType() == Participant.ActorType.EMAILS) {
userType = NextcloudTalkApplication
.Companion
.getSharedApplication()
.getString(R.string.nc_email);
}
break;
case 5:
userType = NextcloudTalkApplication userType = NextcloudTalkApplication
.Companion .Companion
.getSharedApplication() .getSharedApplication()
.getString(R.string.nc_following_link); .getString(R.string.nc_email);
break; }
default: break;
break; case 5:
} userType = NextcloudTalkApplication
.Companion
.getSharedApplication()
.getString(R.string.nc_following_link);
break;
default:
break;
}
if (!userType.equals(NextcloudTalkApplication if (!userType.equals(NextcloudTalkApplication
.Companion .Companion
.getSharedApplication() .getSharedApplication()
.getString(R.string.nc_user))) { .getString(R.string.nc_user))) {
holder.binding.secondaryText.setText("(" + userType + ")"); holder.binding.secondaryText.setText("(" + userType + ")");
}
} }
} }

View File

@ -60,12 +60,10 @@ public class MentionAutocompleteCallback implements AutocompleteCallback<Mention
@OptIn(markerClass = kotlin.ExperimentalStdlibApi.class) @OptIn(markerClass = kotlin.ExperimentalStdlibApi.class)
@Override @Override
public boolean onPopupItemClicked(Editable editable, Mention item) { public boolean onPopupItemClicked(Editable editable, Mention item) {
int[] range = MagicCharPolicy.getQueryRange(editable); MagicCharPolicy.TextSpan range = MagicCharPolicy.getQueryRange(editable);
if (range == null) { if (range == null) {
return false; return false;
} }
int start = range[0];
int end = range[1];
String replacement = item.getLabel(); String replacement = item.getLabel();
StringBuilder replacementStringBuilder = new StringBuilder(item.getLabel()); StringBuilder replacementStringBuilder = new StringBuilder(item.getLabel());
@ -73,7 +71,7 @@ public class MentionAutocompleteCallback implements AutocompleteCallback<Mention
replacementStringBuilder.delete(emojiRange.range.getStart(), emojiRange.range.getEndInclusive()); replacementStringBuilder.delete(emojiRange.range.getStart(), emojiRange.range.getEndInclusive());
} }
editable.replace(start, end, replacementStringBuilder.toString() + " "); editable.replace(range.getStart(), range.getEnd(), replacementStringBuilder + " ");
Spans.MentionChipSpan mentionChipSpan = Spans.MentionChipSpan mentionChipSpan =
new Spans.MentionChipSpan(DisplayUtils.getDrawableForMentionChipSpan(context, new Spans.MentionChipSpan(DisplayUtils.getDrawableForMentionChipSpan(context,
item.getId(), item.getId(),
@ -85,7 +83,9 @@ public class MentionAutocompleteCallback implements AutocompleteCallback<Mention
viewThemeUtils), viewThemeUtils),
BetterImageSpan.ALIGN_CENTER, BetterImageSpan.ALIGN_CENTER,
item.getId(), item.getLabel()); item.getId(), item.getLabel());
editable.setSpan(mentionChipSpan, start, start + replacementStringBuilder.toString().length(), editable.setSpan(mentionChipSpan,
range.getStart(),
range.getStart() + replacementStringBuilder.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE); Spanned.SPAN_INCLUSIVE_INCLUSIVE);

View File

@ -74,8 +74,7 @@ public class DavResponse {
final Object $response = this.getResponse(); final Object $response = this.getResponse();
result = result * PRIME + ($response == null ? 43 : $response.hashCode()); result = result * PRIME + ($response == null ? 43 : $response.hashCode());
final Object $data = this.getData(); final Object $data = this.getData();
result = result * PRIME + ($data == null ? 43 : $data.hashCode()); return result * PRIME + ($data == null ? 43 : $data.hashCode());
return result;
} }
public String toString() { public String toString() {

View File

@ -22,8 +22,6 @@
package com.nextcloud.talk.components.filebrowser.webdav; package com.nextcloud.talk.components.filebrowser.webdav;
import android.util.Log;
import com.nextcloud.talk.components.filebrowser.models.properties.NCEncrypted; import com.nextcloud.talk.components.filebrowser.models.properties.NCEncrypted;
import com.nextcloud.talk.components.filebrowser.models.properties.NCPermission; import com.nextcloud.talk.components.filebrowser.models.properties.NCPermission;
import com.nextcloud.talk.components.filebrowser.models.properties.NCPreview; import com.nextcloud.talk.components.filebrowser.models.properties.NCPreview;
@ -31,14 +29,10 @@ import com.nextcloud.talk.components.filebrowser.models.properties.OCFavorite;
import com.nextcloud.talk.components.filebrowser.models.properties.OCId; import com.nextcloud.talk.components.filebrowser.models.properties.OCId;
import com.nextcloud.talk.components.filebrowser.models.properties.OCSize; import com.nextcloud.talk.components.filebrowser.models.properties.OCSize;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import at.bitfire.dav4jvm.Property; import at.bitfire.dav4jvm.Property;
import at.bitfire.dav4jvm.PropertyFactory;
import at.bitfire.dav4jvm.PropertyRegistry; import at.bitfire.dav4jvm.PropertyRegistry;
import at.bitfire.dav4jvm.property.CreationDate; import at.bitfire.dav4jvm.property.CreationDate;
import at.bitfire.dav4jvm.property.DisplayName; import at.bitfire.dav4jvm.property.DisplayName;
@ -49,7 +43,6 @@ import at.bitfire.dav4jvm.property.GetLastModified;
import at.bitfire.dav4jvm.property.ResourceType; import at.bitfire.dav4jvm.property.ResourceType;
public class DavUtils { public class DavUtils {
private static final String TAG = "DavUtils";
public static final String OC_NAMESPACE = "http://owncloud.org/ns"; public static final String OC_NAMESPACE = "http://owncloud.org/ns";
public static final String NC_NAMESPACE = "http://nextcloud.org/ns"; public static final String NC_NAMESPACE = "http://nextcloud.org/ns";
@ -66,15 +59,16 @@ public class DavUtils {
public static final String EXTENDED_PROPERTY_UNREAD_COMMENTS = "comments-unread"; public static final String EXTENDED_PROPERTY_UNREAD_COMMENTS = "comments-unread";
public static final String EXTENDED_PROPERTY_HAS_PREVIEW = "has-preview"; public static final String EXTENDED_PROPERTY_HAS_PREVIEW = "has-preview";
public static final String EXTENDED_PROPERTY_NOTE = "note"; public static final String EXTENDED_PROPERTY_NOTE = "note";
public static final String TRASHBIN_FILENAME = "trashbin-filename";
public static final String TRASHBIN_ORIGINAL_LOCATION = "trashbin-original-location";
public static final String TRASHBIN_DELETION_TIME = "trashbin-deletion-time";
public static final String PROPERTY_QUOTA_USED_BYTES = "quota-used-bytes"; // public static final String TRASHBIN_FILENAME = "trashbin-filename";
public static final String PROPERTY_QUOTA_AVAILABLE_BYTES = "quota-available-bytes"; // public static final String TRASHBIN_ORIGINAL_LOCATION = "trashbin-original-location";
// public static final String TRASHBIN_DELETION_TIME = "trashbin-deletion-time";
// public static final String PROPERTY_QUOTA_USED_BYTES = "quota-used-bytes";
// public static final String PROPERTY_QUOTA_AVAILABLE_BYTES = "quota-available-bytes";
static Property.Name[] getAllPropSet() { static Property.Name[] getAllPropSet() {
List<Property.Name> props = new ArrayList<>(); List<Property.Name> props = new ArrayList<>(20);
props.add(DisplayName.NAME); props.add(DisplayName.NAME);
props.add(GetContentType.NAME); props.add(GetContentType.NAME);
@ -104,22 +98,12 @@ public class DavUtils {
public static void registerCustomFactories() { public static void registerCustomFactories() {
PropertyRegistry propertyRegistry = PropertyRegistry.INSTANCE; PropertyRegistry propertyRegistry = PropertyRegistry.INSTANCE;
try {
Field factories = propertyRegistry.getClass().getDeclaredField("factories");
factories.setAccessible(true);
Map<Property.Name, PropertyFactory> reflectionMap = (HashMap<Property.Name,
PropertyFactory>) factories.get(propertyRegistry);
reflectionMap.put(OCId.NAME, new OCId.Factory()); propertyRegistry.register(new OCId.Factory());
reflectionMap.put(NCPreview.NAME, new NCPreview.Factory()); propertyRegistry.register(new NCPreview.Factory());
reflectionMap.put(NCEncrypted.NAME, new NCEncrypted.Factory()); propertyRegistry.register(new NCEncrypted.Factory());
reflectionMap.put(OCFavorite.NAME, new OCFavorite.Factory()); propertyRegistry.register(new OCFavorite.Factory());
reflectionMap.put(OCSize.NAME, new OCSize.Factory()); propertyRegistry.register(new OCSize.Factory());
reflectionMap.put(NCPermission.NAME, new NCPermission.Factory()); propertyRegistry.register(new NCPermission.Factory());
factories.set(propertyRegistry, reflectionMap);
} catch (NoSuchFieldException | IllegalAccessException e) {
Log.w(TAG, "Error registering custom factories", e);
}
} }
} }

View File

@ -69,7 +69,6 @@ public class ReadFilesystemOperation {
DavResponse davResponse = new DavResponse(); DavResponse davResponse = new DavResponse();
final List<Response> memberElements = new ArrayList<>(); final List<Response> memberElements = new ArrayList<>();
final Response[] rootElement = new Response[1]; final Response[] rootElement = new Response[1];
final List<BrowserFile> remoteFiles = new ArrayList<>();
try { try {
new DavResource(okHttpClient, HttpUrl.parse(url)).propfind(depth, DavUtils.getAllPropSet(), new DavResource(okHttpClient, HttpUrl.parse(url)).propfind(depth, DavUtils.getAllPropSet(),
@ -94,6 +93,7 @@ public class ReadFilesystemOperation {
Log.w(TAG, "Error reading remote path"); Log.w(TAG, "Error reading remote path");
} }
final List<BrowserFile> remoteFiles = new ArrayList<>(1 + memberElements.size());
remoteFiles.add(BrowserFile.Companion.getModelFromResponse(rootElement[0], remoteFiles.add(BrowserFile.Companion.getModelFromResponse(rootElement[0],
rootElement[0].getHref().toString().substring(basePath.length()))); rootElement[0].getHref().toString().substring(basePath.length())));
for (Response memberElement : memberElements) { for (Response memberElement : memberElements) {

View File

@ -249,9 +249,7 @@ public class RestModule {
.method(original.method(), original.body()) .method(original.method(), original.body())
.build(); .build();
Response response = chain.proceed(request); return chain.proceed(request);
return response;
} }
} }

View File

@ -89,8 +89,7 @@ public class EventStatus {
result = result * PRIME + (int) ($userId >>> 32 ^ $userId); result = result * PRIME + (int) ($userId >>> 32 ^ $userId);
final Object $eventType = this.getEventType(); final Object $eventType = this.getEventType();
result = result * PRIME + ($eventType == null ? 43 : $eventType.hashCode()); result = result * PRIME + ($eventType == null ? 43 : $eventType.hashCode());
result = result * PRIME + (this.isAllGood() ? 79 : 97); return result * PRIME + (this.isAllGood() ? 79 : 97);
return result;
} }
public String toString() { public String toString() {

View File

@ -58,8 +58,7 @@ public class MoreMenuClickEvent {
final int PRIME = 59; final int PRIME = 59;
int result = 1; int result = 1;
final Object $conversation = this.getConversation(); final Object $conversation = this.getConversation();
result = result * PRIME + ($conversation == null ? 43 : $conversation.hashCode()); return result * PRIME + ($conversation == null ? 43 : $conversation.hashCode());
return result;
} }
public String toString() { public String toString() {

View File

@ -56,8 +56,7 @@ public class NetworkEvent {
final int PRIME = 59; final int PRIME = 59;
int result = 1; int result = 1;
final Object $networkConnectionEvent = this.getNetworkConnectionEvent(); final Object $networkConnectionEvent = this.getNetworkConnectionEvent();
result = result * PRIME + ($networkConnectionEvent == null ? 43 : $networkConnectionEvent.hashCode()); return result * PRIME + ($networkConnectionEvent == null ? 43 : $networkConnectionEvent.hashCode());
return result;
} }
public String toString() { public String toString() {

View File

@ -56,8 +56,7 @@ public class UserMentionClickEvent {
final int PRIME = 59; final int PRIME = 59;
int result = 1; int result = 1;
final Object $userId = this.getUserId(); final Object $userId = this.getUserId();
result = result * PRIME + ($userId == null ? 43 : $userId.hashCode()); return result * PRIME + ($userId == null ? 43 : $userId.hashCode());
return result;
} }
public String toString() { public String toString() {

View File

@ -75,8 +75,7 @@ public class WebSocketCommunicationEvent {
final Object $type = this.getType(); final Object $type = this.getType();
result = result * PRIME + ($type == null ? 43 : $type.hashCode()); result = result * PRIME + ($type == null ? 43 : $type.hashCode());
final Object $hashMap = this.getHashMap(); final Object $hashMap = this.getHashMap();
result = result * PRIME + ($hashMap == null ? 43 : $hashMap.hashCode()); return result * PRIME + ($hashMap == null ? 43 : $hashMap.hashCode());
return result;
} }
public String toString() { public String toString() {

View File

@ -98,8 +98,7 @@ public class ImportAccount {
final Object $token = this.getToken(); final Object $token = this.getToken();
result = result * PRIME + ($token == null ? 43 : $token.hashCode()); result = result * PRIME + ($token == null ? 43 : $token.hashCode());
final Object $baseUrl = this.getBaseUrl(); final Object $baseUrl = this.getBaseUrl();
result = result * PRIME + ($baseUrl == null ? 43 : $baseUrl.hashCode()); return result * PRIME + ($baseUrl == null ? 43 : $baseUrl.hashCode());
return result;
} }
public String toString() { public String toString() {

View File

@ -147,7 +147,8 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
if (mentionsList.size() == 0) { if (mentionsList.size() == 0) {
adapter.clear(); adapter.clear();
} else { } else {
List<AbstractFlexibleItem> internalAbstractFlexibleItemList = new ArrayList<>(); List<AbstractFlexibleItem> internalAbstractFlexibleItemList =
new ArrayList<>(mentionsList.size());
for (Mention mention : mentionsList) { for (Mention mention : mentionsList) {
internalAbstractFlexibleItemList.add( internalAbstractFlexibleItemList.add(
new MentionAutocompleteItem( new MentionAutocompleteItem(

View File

@ -42,9 +42,9 @@ import androidx.core.content.res.ResourcesCompat;
*/ */
public class StatusDrawable extends Drawable { public class StatusDrawable extends Drawable {
private String text; private String text;
private @DrawableRes int icon = -1; private StatusDrawableType icon = StatusDrawableType.UNDEFINED;
private Paint textPaint; private Paint textPaint;
private int backgroundColor; private final int backgroundColor;
private final float radius; private final float radius;
private Context context; private Context context;
@ -54,17 +54,17 @@ public class StatusDrawable extends Drawable {
if ("dnd".equals(status)) { if ("dnd".equals(status)) {
icon = R.drawable.ic_user_status_dnd; icon = StatusDrawableType.DND;
this.context = context; this.context = context;
} else if (TextUtils.isEmpty(statusIcon) && status != null) { } else if (TextUtils.isEmpty(statusIcon) && status != null) {
switch (status) { switch (status) {
case "online": case "online":
icon = R.drawable.online_status; icon = StatusDrawableType.ONLINE;
this.context = context; this.context = context;
break; break;
case "away": case "away":
icon = R.drawable.ic_user_status_away; icon = StatusDrawableType.AWAY;
this.context = context; this.context = context;
break; break;
@ -95,7 +95,7 @@ public class StatusDrawable extends Drawable {
canvas.drawText(text, radius, radius - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint); canvas.drawText(text, radius, radius - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
} }
if (icon != -1) { if (icon != StatusDrawableType.UNDEFINED) {
Paint backgroundPaint = new Paint(); Paint backgroundPaint = new Paint();
backgroundPaint.setStyle(Paint.Style.FILL); backgroundPaint.setStyle(Paint.Style.FILL);
@ -104,7 +104,7 @@ public class StatusDrawable extends Drawable {
canvas.drawCircle(radius, radius, radius, backgroundPaint); canvas.drawCircle(radius, radius, radius, backgroundPaint);
Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), icon, null); Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), icon.drawableId, null);
if (drawable != null) { if (drawable != null) {
drawable.setBounds(0, drawable.setBounds(0,
@ -130,4 +130,18 @@ public class StatusDrawable extends Drawable {
public int getOpacity() { public int getOpacity() {
return PixelFormat.TRANSLUCENT; return PixelFormat.TRANSLUCENT;
} }
private enum StatusDrawableType {
DND(R.drawable.ic_user_status_dnd),
ONLINE(R.drawable.online_status),
AWAY(R.drawable.ic_user_status_away),
UNDEFINED(-1);
@DrawableRes
private final int drawableId;
StatusDrawableType(int drawableId) {
this.drawableId = drawableId;
}
}
} }

View File

@ -177,7 +177,6 @@ public class SortingOrderDialogFragment extends DialogFragment implements View.O
binding.cancel.setOnClickListener(view -> dismiss()); binding.cancel.setOnClickListener(view -> dismiss());
for (View view : taggedViews) { for (View view : taggedViews) {
Log.i("SortOrder", "view="+view.getTag().toString());
view.setOnClickListener(this); view.setOnClickListener(this);
} }
} }

View File

@ -62,8 +62,7 @@ public class AuthenticatorService extends Service {
} }
@Override @Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
throws NetworkErrorException {
return null; return null;
} }
@ -73,8 +72,7 @@ public class AuthenticatorService extends Service {
} }
@Override @Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) {
Account account, String[] features) {
return null; return null;
} }
@ -82,7 +80,6 @@ public class AuthenticatorService extends Service {
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) { public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) {
return null; return null;
} }
} }
protected Authenticator getAuthenticator() { protected Authenticator getAuthenticator() {
@ -95,7 +92,7 @@ public class AuthenticatorService extends Service {
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
if (intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) { if (AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
return getAuthenticator().getIBinder(); return getAuthenticator().getIBinder();
} else { } else {
return null; return null;

View File

@ -35,14 +35,17 @@ public class DeviceUtils {
private static final String TAG = "DeviceUtils"; private static final String TAG = "DeviceUtils";
public static void ignoreSpecialBatteryFeatures() { public static void ignoreSpecialBatteryFeatures() {
if (Build.MANUFACTURER.equalsIgnoreCase("xiaomi") || Build.MANUFACTURER.equalsIgnoreCase("meizu")) { if ("xiaomi".equalsIgnoreCase(Build.MANUFACTURER) || "meizu".equalsIgnoreCase(Build.MANUFACTURER)) {
try { try {
@SuppressLint("PrivateApi") Class<?> appOpsUtilsClass = Class.forName("android.miui.AppOpsUtils"); @SuppressLint("PrivateApi") Class<?> appOpsUtilsClass = Class.forName("android.miui.AppOpsUtils");
if (appOpsUtilsClass != null) { if (appOpsUtilsClass != null) {
Method setApplicationAutoStartMethod = appOpsUtilsClass.getMethod("setApplicationAutoStart", Context Method setApplicationAutoStartMethod = appOpsUtilsClass.getMethod("setApplicationAutoStart", Context
.class, String.class, Boolean.TYPE); .class, String.class, Boolean.TYPE);
if (setApplicationAutoStartMethod != null) { if (setApplicationAutoStartMethod != null) {
Context applicationContext = NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext(); Context applicationContext = NextcloudTalkApplication
.Companion
.getSharedApplication()
.getApplicationContext();
setApplicationAutoStartMethod.invoke(appOpsUtilsClass, applicationContext, applicationContext setApplicationAutoStartMethod.invoke(appOpsUtilsClass, applicationContext, applicationContext
.getPackageName(), Boolean.TRUE); .getPackageName(), Boolean.TRUE);
} }
@ -56,10 +59,10 @@ public class DeviceUtils {
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
Log.e(TAG, "InvocationTargetException"); Log.e(TAG, "InvocationTargetException");
} }
} else if (Build.MANUFACTURER.equalsIgnoreCase("huawei")) { } else if ("huawei".equalsIgnoreCase(Build.MANUFACTURER)) {
try { try {
@SuppressLint("PrivateApi") Class<?> protectAppControlClass = Class.forName("com.huawei.systemmanager.optimize.process" + @SuppressLint("PrivateApi") Class<?> protectAppControlClass = Class.forName(
".ProtectAppControl"); "com.huawei.systemmanager.optimize.process.ProtectAppControl");
if (protectAppControlClass != null) { if (protectAppControlClass != null) {
Context applicationContext = NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext(); Context applicationContext = NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext();

View File

@ -369,8 +369,6 @@ public class DisplayUtils {
decor.setSystemUiVisibility(0); decor.setSystemUiVisibility(0);
} }
window.setStatusBarColor(color); window.setStatusBarColor(color);
} else if (isLightTheme) {
window.setStatusBarColor(Color.BLACK);
} }
} }
@ -380,7 +378,7 @@ public class DisplayUtils {
* @param color the color * @param color the color
* @return true if primaryColor is lighter than MAX_LIGHTNESS * @return true if primaryColor is lighter than MAX_LIGHTNESS
*/ */
@SuppressWarnings("correctness") @SuppressWarnings("CLI_CONSTANT_LIST_INDEX")
public static boolean lightTheme(int color) { public static boolean lightTheme(int color) {
float[] hsl = colorToHSL(color); float[] hsl = colorToHSL(color);
@ -455,10 +453,12 @@ public class DisplayUtils {
avatarId = user.getUsername(); avatarId = user.getUsername();
} }
if (deleteCache) { if (avatarId != null) {
ImageViewExtensionsKt.replaceAvatar(avatarImageView, user, avatarId, true); if (deleteCache) {
} else { ImageViewExtensionsKt.replaceAvatar(avatarImageView, user, avatarId, true);
ImageViewExtensionsKt.loadAvatar(avatarImageView, user, avatarId, true); } else {
ImageViewExtensionsKt.loadAvatar(avatarImageView, user, avatarId, true);
}
} }
} }

View File

@ -37,43 +37,57 @@ public class MagicCharPolicy implements AutocompletePolicy {
} }
@Nullable @Nullable
public static int[] getQueryRange(Spannable text) { public static TextSpan getQueryRange(Spannable text) {
QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class);
if (span == null || span.length == 0) return null; if (span == null || span.length == 0) {
if (span.length > 1) { return null;
// Do absolutely nothing } else {
QuerySpan sp = span[0];
return new TextSpan(text.getSpanStart(sp), text.getSpanEnd(sp));
} }
QuerySpan sp = span[0];
return new int[]{text.getSpanStart(sp), text.getSpanEnd(sp)};
} }
private int[] checkText(Spannable text, int cursorPos) { private TextSpan checkText(Spannable text, int cursorPos) {
if (text.length() == 0) { if (text.length() == 0) {
return null; return null;
} }
int[] span = new int[2];
Pattern pattern = Pattern.compile("@+\\S*", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); Pattern pattern = Pattern.compile("@+\\S*", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher matcher = pattern.matcher(text); Matcher matcher = pattern.matcher(text);
while (matcher.find()) { while (matcher.find()) {
if (cursorPos >= matcher.start() && cursorPos <= matcher.end()) { if (cursorPos >= matcher.start() && cursorPos <= matcher.end() &&
span[0] = matcher.start(); text.subSequence(matcher.start(), matcher.end()).charAt(0) == character) {
span[1] = matcher.end(); return new TextSpan(matcher.start(), matcher.end());
if (text.subSequence(matcher.start(), matcher.end()).charAt(0) == character) {
return span;
}
} }
} }
return null; return null;
} }
public static class TextSpan {
int start;
int end;
public TextSpan(int start, int end) {
this.start = start;
this.end = end;
}
public int getStart() {
return start;
}
public int getEnd() {
return end;
}
}
@Override @Override
public boolean shouldShowPopup(Spannable text, int cursorPos) { public boolean shouldShowPopup(Spannable text, int cursorPos) {
int[] show = checkText(text, cursorPos); TextSpan show = checkText(text, cursorPos);
if (show != null) { if (show != null) {
text.setSpan(new QuerySpan(), show[0], show[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE); text.setSpan(new QuerySpan(), show.getStart(), show.getEnd(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
return true; return true;
} }
return false; return false;

View File

@ -78,7 +78,7 @@ public class DatabaseStorageModule implements StorageModule {
@Override @Override
public void saveBoolean(String key, boolean value) { public void saveBoolean(String key, boolean value) {
if (key.equals("call_notifications")) { if ("call_notifications".equals(key)) {
int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{4}); int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{4});
ncApi.notificationCalls(ApiUtils.getCredentials(conversationUser.getUsername(), ncApi.notificationCalls(ApiUtils.getCredentials(conversationUser.getUsername(),
conversationUser.getToken()), conversationUser.getToken()),
@ -112,7 +112,7 @@ public class DatabaseStorageModule implements StorageModule {
); );
} }
if (!key.equals("conversation_lobby")) { if (!"conversation_lobby".equals(key)) {
arbitraryStorageManager.storeStorageSetting(accountIdentifier, arbitraryStorageManager.storeStorageSetting(accountIdentifier,
key, key,
Boolean.toString(value), Boolean.toString(value),
@ -124,7 +124,7 @@ public class DatabaseStorageModule implements StorageModule {
@Override @Override
public void saveString(String key, String value) { public void saveString(String key, String value) {
if (key.equals("message_expire_key")) { if ("message_expire_key".equals(key)) {
int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{4}); int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{4});
String trimmedValue = value.replace("expire_", ""); String trimmedValue = value.replace("expire_", "");
@ -163,7 +163,7 @@ public class DatabaseStorageModule implements StorageModule {
} }
}); });
} else if (key.equals("message_notification_level")) { } else if ("message_notification_level".equals(key)) {
if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "notification-levels")) { if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "notification-levels")) {
if (!TextUtils.isEmpty(messageNotificationLevel) && !messageNotificationLevel.equals(value)) { if (!TextUtils.isEmpty(messageNotificationLevel) && !messageNotificationLevel.equals(value)) {
int intValue; int intValue;
@ -232,7 +232,7 @@ public class DatabaseStorageModule implements StorageModule {
@Override @Override
public boolean getBoolean(String key, boolean defaultVal) { public boolean getBoolean(String key, boolean defaultVal) {
if (key.equals("conversation_lobby")) { if ("conversation_lobby".equals(key)) {
return lobbyValue; return lobbyValue;
} else { } else {
return arbitraryStorageManager return arbitraryStorageManager
@ -244,7 +244,7 @@ public class DatabaseStorageModule implements StorageModule {
@Override @Override
public String getString(String key, String defaultVal) { public String getString(String key, String defaultVal) {
if (key.equals("message_expire_key")) { if ("message_expire_key".equals(key)) {
switch (messageExpiration) { switch (messageExpiration) {
case 2419200: case 2419200:
return "expire_2419200"; return "expire_2419200";
@ -259,7 +259,7 @@ public class DatabaseStorageModule implements StorageModule {
default: default:
return "expire_0"; return "expire_0";
} }
} else if (key.equals("message_notification_level")) { } else if ("message_notification_level".equals(key)) {
return messageNotificationLevel; return messageNotificationLevel;
} else { } else {
return arbitraryStorageManager return arbitraryStorageManager

View File

@ -87,13 +87,11 @@ public class Spans {
final Object $id = this.getId(); final Object $id = this.getId();
result = result * PRIME + ($id == null ? 43 : $id.hashCode()); result = result * PRIME + ($id == null ? 43 : $id.hashCode());
final Object $label = this.getLabel(); final Object $label = this.getLabel();
result = result * PRIME + ($label == null ? 43 : $label.hashCode()); return result * PRIME + ($label == null ? 43 : $label.hashCode());
return result;
} }
public String toString() { public String toString() {
return "Spans.MentionChipSpan(id=" + this.getId() + ", label=" + this.getLabel() + ")"; return "Spans.MentionChipSpan(id=" + this.getId() + ", label=" + this.getLabel() + ")";
} }
} }
} }

View File

@ -4,4 +4,5 @@ public class Globals {
public static final String ROOM_TOKEN = "roomToken"; public static final String ROOM_TOKEN = "roomToken";
public static final String TARGET_PARTICIPANTS = "participants"; public static final String TARGET_PARTICIPANTS = "participants";
public static final String TARGET_ROOM = "room";
} }

View File

@ -35,6 +35,7 @@ import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage; import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.ErrorOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.ErrorOverallWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.ErrorWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.EventOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.EventOverallWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.HelloResponseOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.HelloResponseOverallWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.JoinedRoomOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.JoinedRoomOverallWebSocketMessage;
@ -54,6 +55,7 @@ import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.NonNull;
import autodagger.AutoInjector; import autodagger.AutoInjector;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
@ -66,12 +68,12 @@ import static com.nextcloud.talk.models.json.participants.Participant.ActorType.
import static com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS; import static com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS;
import static com.nextcloud.talk.webrtc.Globals.ROOM_TOKEN; import static com.nextcloud.talk.webrtc.Globals.ROOM_TOKEN;
import static com.nextcloud.talk.webrtc.Globals.TARGET_PARTICIPANTS; import static com.nextcloud.talk.webrtc.Globals.TARGET_PARTICIPANTS;
import static com.nextcloud.talk.webrtc.Globals.TARGET_ROOM;
@AutoInjector(NextcloudTalkApplication.class) @AutoInjector(NextcloudTalkApplication.class)
public class MagicWebSocketInstance extends WebSocketListener { public class MagicWebSocketInstance extends WebSocketListener {
private static final String TAG = "MagicWebSocketInstance"; private static final String TAG = "MagicWebSocketInstance";
@Inject @Inject
OkHttpClient okHttpClient; OkHttpClient okHttpClient;
@ -81,18 +83,17 @@ public class MagicWebSocketInstance extends WebSocketListener {
@Inject @Inject
Context context; Context context;
private User conversationUser; private final User conversationUser;
private String webSocketTicket; private final String webSocketTicket;
private String resumeId; private String resumeId;
private String sessionId; private String sessionId;
private boolean hasMCU; private boolean hasMCU;
private boolean connected; private boolean connected;
private WebSocketConnectionHelper webSocketConnectionHelper; private final WebSocketConnectionHelper webSocketConnectionHelper;
private WebSocket internalWebSocket; private WebSocket internalWebSocket;
private String connectionUrl; private final String connectionUrl;
private String currentRoomToken; private String currentRoomToken;
private int restartCount = 0;
private boolean reconnecting = false; private boolean reconnecting = false;
private HashMap<String, Participant> usersHashMap; private HashMap<String, Participant> usersHashMap;
@ -121,9 +122,13 @@ public class MagicWebSocketInstance extends WebSocketListener {
private void sendHello() { private void sendHello() {
try { try {
if (TextUtils.isEmpty(resumeId)) { if (TextUtils.isEmpty(resumeId)) {
internalWebSocket.send(LoganSquare.serialize(webSocketConnectionHelper.getAssembledHelloModel(conversationUser, webSocketTicket))); internalWebSocket.send(
LoganSquare.serialize(webSocketConnectionHelper
.getAssembledHelloModel(conversationUser, webSocketTicket)));
} else { } else {
internalWebSocket.send(LoganSquare.serialize(webSocketConnectionHelper.getAssembledHelloModelForResume(resumeId))); internalWebSocket.send(
LoganSquare.serialize(webSocketConnectionHelper
.getAssembledHelloModelForResume(resumeId)));
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Failed to serialize hello model"); Log.e(TAG, "Failed to serialize hello model");
@ -152,142 +157,49 @@ public class MagicWebSocketInstance extends WebSocketListener {
resumeId = ""; resumeId = "";
} }
public void restartWebSocket() { public final void restartWebSocket() {
reconnecting = true; reconnecting = true;
// TODO when improving logging, keep in mind this issue: https://github.com/nextcloud/talk-android/issues/1013 // TODO when improving logging, keep in mind this issue: https://github.com/nextcloud/talk-android/issues/1013
Log.d(TAG, "restartWebSocket: " + connectionUrl); Log.d(TAG, "restartWebSocket: " + connectionUrl);
Request request = new Request.Builder().url(connectionUrl).build(); Request request = new Request.Builder().url(connectionUrl).build();
okHttpClient.newWebSocket(request, this); okHttpClient.newWebSocket(request, this);
restartCount++;
} }
@Override @Override
public void onMessage(WebSocket webSocket, String text) { public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
if (webSocket == internalWebSocket) { if (webSocket == internalWebSocket) {
Log.d(TAG, "Receiving : " + webSocket.toString() + " " + text); Log.d(TAG, "Receiving : " + webSocket + " " + text);
try { try {
BaseWebSocketMessage baseWebSocketMessage = LoganSquare.parse(text, BaseWebSocketMessage.class); BaseWebSocketMessage baseWebSocketMessage = LoganSquare.parse(text, BaseWebSocketMessage.class);
String messageType = baseWebSocketMessage.getType(); String messageType = baseWebSocketMessage.getType();
switch (messageType) { if (messageType != null) {
case "hello": switch (messageType) {
connected = true; case "hello":
reconnecting = false; processHelloMessage(webSocket, text);
restartCount = 0; break;
String oldResumeId = resumeId; case "error":
HelloResponseOverallWebSocketMessage helloResponseWebSocketMessage = LoganSquare.parse(text, HelloResponseOverallWebSocketMessage.class); processErrorMessage(webSocket, text);
resumeId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getResumeId(); break;
sessionId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getSessionId(); case "room":
hasMCU = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().serverHasMCUSupport(); processJoinedRoomMessage(text);
break;
for (int i = 0; i < messagesQueue.size(); i++) { case "event":
webSocket.send(messagesQueue.get(i)); processEventMessage(text);
} break;
case "message":
messagesQueue = new ArrayList<>(); processMessage(text);
HashMap<String, String> helloHasHap = new HashMap<>(); break;
if (!TextUtils.isEmpty(oldResumeId)) { case "bye":
helloHasHap.put("oldResumeId", oldResumeId); connected = false;
} else {
currentRoomToken = "";
}
if (!TextUtils.isEmpty(currentRoomToken)) {
helloHasHap.put(ROOM_TOKEN, currentRoomToken);
}
eventBus.post(new WebSocketCommunicationEvent("hello", helloHasHap));
break;
case "error":
Log.e(TAG, "Received error: " + text);
ErrorOverallWebSocketMessage errorOverallWebSocketMessage = LoganSquare.parse(text, ErrorOverallWebSocketMessage.class);
if (("no_such_session").equals(errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
Log.d(TAG, "WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired");
resumeId = ""; resumeId = "";
currentRoomToken = ""; break;
restartWebSocket(); default:
} else if (("hello_expected").equals(errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) { break;
restartWebSocket(); }
} } else {
Log.e(TAG, "Received message with type: null");
break;
case "room":
JoinedRoomOverallWebSocketMessage joinedRoomOverallWebSocketMessage = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage.class);
currentRoomToken = joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomId();
if (joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomPropertiesWebSocketMessage() != null && !TextUtils.isEmpty(currentRoomToken)) {
sendRoomJoinedEvent();
}
break;
case "event":
EventOverallWebSocketMessage eventOverallWebSocketMessage = LoganSquare.parse(text, EventOverallWebSocketMessage.class);
if (eventOverallWebSocketMessage.getEventMap() != null) {
String target = (String) eventOverallWebSocketMessage.getEventMap().get("target");
switch (target) {
case "room":
if (eventOverallWebSocketMessage.getEventMap().get("type").equals("message")) {
Map<String, Object> messageHashMap =
(Map<String, Object>) eventOverallWebSocketMessage.getEventMap().get("message");
if (messageHashMap.containsKey("data")) {
Map<String, Object> dataHashMap = (Map<String, Object>) messageHashMap.get(
"data");
if (dataHashMap.containsKey("chat")) {
boolean shouldRefreshChat;
Map<String, Object> chatMap = (Map<String, Object>) dataHashMap.get("chat");
if (chatMap.containsKey("refresh")) {
shouldRefreshChat = (boolean) chatMap.get("refresh");
if (shouldRefreshChat) {
HashMap<String, String> refreshChatHashMap = new HashMap<>();
refreshChatHashMap.put(BundleKeys.KEY_ROOM_TOKEN, (String) messageHashMap.get("roomid"));
refreshChatHashMap.put(BundleKeys.KEY_INTERNAL_USER_ID, Long.toString(conversationUser.getId()));
eventBus.post(new WebSocketCommunicationEvent("refreshChat", refreshChatHashMap));
}
}
}
}
} else if (eventOverallWebSocketMessage.getEventMap().get("type").equals("join")) {
List<HashMap<String, Object>> joinEventList = (List<HashMap<String, Object>>) eventOverallWebSocketMessage.getEventMap().get("join");
HashMap<String, Object> internalHashMap;
Participant participant;
for (int i = 0; i < joinEventList.size(); i++) {
internalHashMap = joinEventList.get(i);
HashMap<String, Object> userMap = (HashMap<String, Object>) internalHashMap.get("user");
participant = new Participant();
String userId = (String) internalHashMap.get("userid");
if (userId != null) {
participant.setActorType(USERS);
participant.setActorId(userId);
} else {
participant.setActorType(GUESTS);
// FIXME seems to be not given by the HPB: participant.setActorId();
}
if (userMap != null) {
// There is no "user" attribute for guest participants.
participant.setDisplayName((String) userMap.get("displayname"));
}
usersHashMap.put((String) internalHashMap.get("sessionid"), participant);
}
}
break;
case TARGET_PARTICIPANTS:
signalingMessageReceiver.process(eventOverallWebSocketMessage.getEventMap());
break;
}
}
break;
case "message":
CallOverallWebSocketMessage callOverallWebSocketMessage = LoganSquare.parse(text, CallOverallWebSocketMessage.class);
NCSignalingMessage ncSignalingMessage = callOverallWebSocketMessage.getCallWebSocketMessage().getNcSignalingMessage();
if (TextUtils.isEmpty(ncSignalingMessage.getFrom()) && callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage() != null) {
ncSignalingMessage.setFrom(callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage().getSessionId());
}
signalingMessageReceiver.process(ncSignalingMessage);
break;
case "bye":
connected = false;
resumeId = "";
default:
break;
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Failed to recognize WebSocket message", e); Log.e(TAG, "Failed to recognize WebSocket message", e);
@ -295,6 +207,158 @@ public class MagicWebSocketInstance extends WebSocketListener {
} }
} }
private void processMessage(String text) throws IOException {
CallOverallWebSocketMessage callOverallWebSocketMessage =
LoganSquare.parse(text, CallOverallWebSocketMessage.class);
if (callOverallWebSocketMessage.getCallWebSocketMessage() != null) {
NCSignalingMessage ncSignalingMessage = callOverallWebSocketMessage
.getCallWebSocketMessage()
.getNcSignalingMessage();
if (ncSignalingMessage != null && TextUtils.isEmpty(ncSignalingMessage.getFrom()) &&
callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage() != null) {
ncSignalingMessage.setFrom(
callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage().getSessionId());
}
signalingMessageReceiver.process(ncSignalingMessage);
}
}
private void processEventMessage(String text) throws IOException {
EventOverallWebSocketMessage eventOverallWebSocketMessage =
LoganSquare.parse(text, EventOverallWebSocketMessage.class);
if (eventOverallWebSocketMessage.getEventMap() != null) {
String target = (String) eventOverallWebSocketMessage.getEventMap().get("target");
if (target != null) {
switch (target) {
case TARGET_ROOM:
if ("message".equals(eventOverallWebSocketMessage.getEventMap().get("type"))) {
processRoomMessageMessage(eventOverallWebSocketMessage);
} else if ("join".equals(eventOverallWebSocketMessage.getEventMap().get("type"))) {
processRoomJoinMessage(eventOverallWebSocketMessage);
}
break;
case TARGET_PARTICIPANTS:
signalingMessageReceiver.process(eventOverallWebSocketMessage.getEventMap());
break;
default:
Log.i(TAG, "Received unknown/ignored event target: " + target);
break;
}
} else {
Log.w(TAG, "Received message with event target: null");
}
}
}
private void processRoomMessageMessage(EventOverallWebSocketMessage eventOverallWebSocketMessage) {
Map<String, Object> messageHashMap = (Map<String, Object>) eventOverallWebSocketMessage
.getEventMap()
.get("message");
if (messageHashMap != null && messageHashMap.containsKey("data")) {
Map<String, Object> dataHashMap = (Map<String, Object>) messageHashMap.get("data");
if (dataHashMap != null && dataHashMap.containsKey("chat")) {
Map<String, Object> chatMap = (Map<String, Object>) dataHashMap.get("chat");
if (chatMap != null && chatMap.containsKey("refresh") && (boolean) chatMap.get("refresh")) {
HashMap<String, String> refreshChatHashMap = new HashMap<>();
refreshChatHashMap.put(BundleKeys.KEY_ROOM_TOKEN, (String) messageHashMap.get("roomid"));
refreshChatHashMap.put(BundleKeys.KEY_INTERNAL_USER_ID, Long.toString(conversationUser.getId()));
eventBus.post(new WebSocketCommunicationEvent("refreshChat", refreshChatHashMap));
}
}
}
}
private void processRoomJoinMessage(EventOverallWebSocketMessage eventOverallWebSocketMessage) {
List<HashMap<String, Object>> joinEventList = (List<HashMap<String, Object>>) eventOverallWebSocketMessage
.getEventMap()
.get("join");
HashMap<String, Object> internalHashMap;
Participant participant;
for (int i = 0; i < joinEventList.size(); i++) {
internalHashMap = joinEventList.get(i);
HashMap<String, Object> userMap = (HashMap<String, Object>) internalHashMap.get("user");
participant = new Participant();
String userId = (String) internalHashMap.get("userid");
if (userId != null) {
participant.setActorType(USERS);
participant.setActorId(userId);
} else {
participant.setActorType(GUESTS);
// FIXME seems to be not given by the HPB: participant.setActorId();
}
if (userMap != null) {
// There is no "user" attribute for guest participants.
participant.setDisplayName((String) userMap.get("displayname"));
}
usersHashMap.put((String) internalHashMap.get("sessionid"), participant);
}
}
private void processJoinedRoomMessage(String text) throws IOException {
JoinedRoomOverallWebSocketMessage joinedRoomOverallWebSocketMessage =
LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage.class);
if (joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage() != null) {
currentRoomToken = joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomId();
if (joinedRoomOverallWebSocketMessage
.getRoomWebSocketMessage()
.getRoomPropertiesWebSocketMessage() != null &&
!TextUtils.isEmpty(currentRoomToken)) {
sendRoomJoinedEvent();
}
}
}
private void processErrorMessage(WebSocket webSocket, String text) throws IOException {
Log.e(TAG, "Received error: " + text);
ErrorOverallWebSocketMessage errorOverallWebSocketMessage =
LoganSquare.parse(text, ErrorOverallWebSocketMessage.class);
ErrorWebSocketMessage message = errorOverallWebSocketMessage.getErrorWebSocketMessage();
if(message != null) {
if ("no_such_session".equals(message.getCode())) {
Log.d(TAG, "WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired");
resumeId = "";
currentRoomToken = "";
restartWebSocket();
} else if ("hello_expected".equals(message.getCode())) {
restartWebSocket();
}
}
}
private void processHelloMessage(WebSocket webSocket, String text) throws IOException {
connected = true;
reconnecting = false;
String oldResumeId = resumeId;
HelloResponseOverallWebSocketMessage helloResponseWebSocketMessage =
LoganSquare.parse(text, HelloResponseOverallWebSocketMessage.class);
if (helloResponseWebSocketMessage.getHelloResponseWebSocketMessage() != null) {
resumeId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getResumeId();
sessionId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getSessionId();
hasMCU = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().serverHasMCUSupport();
}
for (int i = 0; i < messagesQueue.size(); i++) {
webSocket.send(messagesQueue.get(i));
}
messagesQueue = new ArrayList<>();
HashMap<String, String> helloHasHap = new HashMap<>();
if (!TextUtils.isEmpty(oldResumeId)) {
helloHasHap.put("oldResumeId", oldResumeId);
} else {
currentRoomToken = "";
}
if (!TextUtils.isEmpty(currentRoomToken)) {
helloHasHap.put(ROOM_TOKEN, currentRoomToken);
}
eventBus.post(new WebSocketCommunicationEvent("hello", helloHasHap));
}
private void sendRoomJoinedEvent() { private void sendRoomJoinedEvent() {
HashMap<String, String> joinRoomHashMap = new HashMap<>(); HashMap<String, String> joinRoomHashMap = new HashMap<>();
joinRoomHashMap.put(ROOM_TOKEN, currentRoomToken); joinRoomHashMap.put(ROOM_TOKEN, currentRoomToken);
@ -302,12 +366,12 @@ public class MagicWebSocketInstance extends WebSocketListener {
} }
@Override @Override
public void onMessage(WebSocket webSocket, ByteString bytes) { public void onMessage(@NonNull WebSocket webSocket, ByteString bytes) {
Log.d(TAG, "Receiving bytes : " + bytes.hex()); Log.d(TAG, "Receiving bytes : " + bytes.hex());
} }
@Override @Override
public void onClosing(WebSocket webSocket, int code, String reason) { public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) {
Log.d(TAG, "Closing : " + code + " / " + reason); Log.d(TAG, "Closing : " + code + " / " + reason);
} }
@ -330,7 +394,8 @@ public class MagicWebSocketInstance extends WebSocketListener {
Log.d(TAG, " roomToken: " + roomToken); Log.d(TAG, " roomToken: " + roomToken);
Log.d(TAG, " session: " + normalBackendSession); Log.d(TAG, " session: " + normalBackendSession);
try { try {
String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession)); String message = LoganSquare.serialize(
webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession));
if (!connected || reconnecting) { if (!connected || reconnecting) {
messagesQueue.add(message); messagesQueue.add(message);
} else { } else {
@ -347,7 +412,8 @@ public class MagicWebSocketInstance extends WebSocketListener {
private void sendCallMessage(NCSignalingMessage ncSignalingMessage) { private void sendCallMessage(NCSignalingMessage ncSignalingMessage) {
try { try {
String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledCallMessageModel(ncSignalingMessage)); String message = LoganSquare.serialize(
webSocketConnectionHelper.getAssembledCallMessageModel(ncSignalingMessage));
if (!connected || reconnecting) { if (!connected || reconnecting) {
messagesQueue.add(message); messagesQueue.add(message);
} else { } else {
@ -388,7 +454,8 @@ public class MagicWebSocketInstance extends WebSocketListener {
@Subscribe(threadMode = ThreadMode.BACKGROUND) @Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onMessageEvent(NetworkEvent networkEvent) { public void onMessageEvent(NetworkEvent networkEvent) {
if (networkEvent.getNetworkConnectionEvent() == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED && !isConnected()) { if (networkEvent.getNetworkConnectionEvent() == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED &&
!isConnected()) {
restartWebSocket(); restartWebSocket();
} }
} }
@ -404,9 +471,9 @@ public class MagicWebSocketInstance extends WebSocketListener {
/** /**
* Temporary implementation of SignalingMessageReceiver until signaling related code is extracted to a Signaling * Temporary implementation of SignalingMessageReceiver until signaling related code is extracted to a Signaling
* class. * class.
* * <p>
* All listeners are called in the WebSocket reader thread. This thread should be the same as long as the * All listeners are called in the WebSocket reader thread. This thread should be the same as long as the WebSocket
* WebSocket stays connected, but it may change whenever it is connected again. * stays connected, but it may change whenever it is connected again.
*/ */
private static class ExternalSignalingMessageReceiver extends SignalingMessageReceiver { private static class ExternalSignalingMessageReceiver extends SignalingMessageReceiver {
public void process(Map<String, Object> eventMap) { public void process(Map<String, Object> eventMap) {

View File

@ -171,7 +171,7 @@ public class PeerConnectionWrapper {
dataChannel.registerObserver(new MagicDataChannelObserver()); dataChannel.registerObserver(new MagicDataChannelObserver());
if (isMCUPublisher) { if (isMCUPublisher) {
peerConnection.createOffer(magicSdpObserver, mediaConstraints); peerConnection.createOffer(magicSdpObserver, mediaConstraints);
} else if (hasMCU && this.videoStreamType.equals("video")) { } else if (hasMCU && "video".equals(this.videoStreamType)) {
// If the connection type is "screen" the client sharing the screen will send an // If the connection type is "screen" the client sharing the screen will send an
// offer; offers should be requested only for videos. // offer; offers should be requested only for videos.
// "to" property is not actually needed in the "requestoffer" signaling message, but it is used to // "to" property is not actually needed in the "requestoffer" signaling message, but it is used to
@ -360,8 +360,9 @@ public class PeerConnectionWrapper {
@Override @Override
public void onStateChange() { public void onStateChange() {
if (dataChannel != null && dataChannel.state() == DataChannel.State.OPEN && if (dataChannel != null &&
dataChannel.label().equals("status")) { dataChannel.state() == DataChannel.State.OPEN &&
"status".equals(dataChannel.label())) {
sendInitialMediaStatus(); sendInitialMediaStatus();
} }
} }
@ -493,7 +494,7 @@ public class PeerConnectionWrapper {
@Override @Override
public void onDataChannel(DataChannel dataChannel) { public void onDataChannel(DataChannel dataChannel) {
if (dataChannel.label().equals("status") || dataChannel.label().equals("JanusDataChannel")) { if ("status".equals(dataChannel.label()) || "JanusDataChannel".equals(dataChannel.label())) {
PeerConnectionWrapper.this.dataChannel = dataChannel; PeerConnectionWrapper.this.dataChannel = dataChannel;
PeerConnectionWrapper.this.dataChannel.registerObserver(new MagicDataChannelObserver()); PeerConnectionWrapper.this.dataChannel.registerObserver(new MagicDataChannelObserver());
} }

View File

@ -399,7 +399,7 @@ public class WebRtcAudioManager {
return false; return false;
} }
public void updateAudioDeviceState() { public final void updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "--- updateAudioDeviceState: " Log.d(TAG, "--- updateAudioDeviceState: "
+ "wired headset=" + hasWiredHeadset + ", " + "wired headset=" + hasWiredHeadset + ", "

View File

@ -109,8 +109,6 @@ public class WebRtcBluetoothManager {
return bluetoothState; return bluetoothState;
} }
;
/** /**
* Activates components required to detect Bluetooth devices and to enable * Activates components required to detect Bluetooth devices and to enable
* BT SCO (audio is routed via BT SCO) for the headset profile. The end * BT SCO (audio is routed via BT SCO) for the headset profile. The end
@ -297,7 +295,7 @@ public class WebRtcBluetoothManager {
/** /**
* Stubs for test mocks. * Stubs for test mocks.
*/ */
protected AudioManager getAudioManager(Context context) { protected final AudioManager getAudioManager(Context context) {
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
} }
@ -502,8 +500,10 @@ public class WebRtcBluetoothManager {
Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState); Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
} }
/**
* Notifies the client when the proxy object has been disconnected from the service.
*/
@Override @Override
/** Notifies the client when the proxy object has been disconnected from the service. */
public void onServiceDisconnected(int profile) { public void onServiceDisconnected(int profile) {
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
return; return;
@ -531,7 +531,7 @@ public class WebRtcBluetoothManager {
// change does not tell us anything about whether we're streaming // change does not tell us anything about whether we're streaming
// audio to BT over SCO. Typically received when user turns on a BT // audio to BT over SCO. Typically received when user turns on a BT
// headset while audio is active using another audio device. // headset while audio is active using another audio device.
if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
final int state = final int state =
intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
@ -543,8 +543,10 @@ public class WebRtcBluetoothManager {
scoConnectionAttempts = 0; scoConnectionAttempts = 0;
updateAudioDeviceState(); updateAudioDeviceState();
} else if (state == BluetoothHeadset.STATE_CONNECTING) { } else if (state == BluetoothHeadset.STATE_CONNECTING) {
Log.d(TAG, "+++ Bluetooth is connecting...");
// No action needed. // No action needed.
} else if (state == BluetoothHeadset.STATE_DISCONNECTING) { } else if (state == BluetoothHeadset.STATE_DISCONNECTING) {
Log.d(TAG, "+++ Bluetooth is disconnecting...");
// No action needed. // No action needed.
} else if (state == BluetoothHeadset.STATE_DISCONNECTED) { } else if (state == BluetoothHeadset.STATE_DISCONNECTED) {
// Bluetooth is probably powered off during the call. // Bluetooth is probably powered off during the call.
@ -553,7 +555,7 @@ public class WebRtcBluetoothManager {
} }
// Change in the audio (SCO) connection state of the Headset profile. // Change in the audio (SCO) connection state of the Headset profile.
// Typically received after call to startScoAudio() has finalized. // Typically received after call to startScoAudio() has finalized.
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { } else if (BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED.equals(action)) {
final int state = intent.getIntExtra( final int state = intent.getIntExtra(
BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "

View File

@ -60,11 +60,13 @@ public class WebSocketConnectionHelper {
@SuppressLint("LongLogTag") @SuppressLint("LongLogTag")
public static synchronized MagicWebSocketInstance getMagicWebSocketInstanceForUserId(long userId) { public static synchronized MagicWebSocketInstance getMagicWebSocketInstanceForUserId(long userId) {
if (userId != -1 && magicWebSocketInstanceMap.containsKey(userId)) { MagicWebSocketInstance webSocketInstance = magicWebSocketInstanceMap.get(userId);
return magicWebSocketInstanceMap.get(userId);
if (webSocketInstance == null) {
Log.d(TAG, "No magicWebSocketInstance found for user " + userId);
} }
Log.d(TAG, "no magicWebSocketInstance found");
return null; return webSocketInstance;
} }
public static synchronized MagicWebSocketInstance getExternalSignalingInstanceForServer(String url, public static synchronized MagicWebSocketInstance getExternalSignalingInstanceForServer(String url,