Merge changes I1f880aa6,Ie8ba8208,Ic45aa87c,I04c1263a
* changes: Add tests for UWB AdapterStateListener Implement UWB AdapterStateListener Implement UwbManager functions Expose UwbManager through Context.getSystemService
This commit is contained in:
@@ -189,6 +189,7 @@ import android.telephony.TelephonyRegistryManager;
|
|||||||
import android.util.ArrayMap;
|
import android.util.ArrayMap;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Slog;
|
import android.util.Slog;
|
||||||
|
import android.uwb.UwbManager;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
@@ -719,6 +720,14 @@ public final class SystemServiceRegistry {
|
|||||||
return new SerialManager(ctx, ISerialManager.Stub.asInterface(b));
|
return new SerialManager(ctx, ISerialManager.Stub.asInterface(b));
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
registerService(Context.UWB_SERVICE, UwbManager.class,
|
||||||
|
new CachedServiceFetcher<UwbManager>() {
|
||||||
|
@Override
|
||||||
|
public UwbManager createService(ContextImpl ctx) {
|
||||||
|
return UwbManager.getInstance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
registerService(Context.VIBRATOR_SERVICE, Vibrator.class,
|
registerService(Context.VIBRATOR_SERVICE, Vibrator.class,
|
||||||
new CachedServiceFetcher<Vibrator>() {
|
new CachedServiceFetcher<Vibrator>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3513,6 +3513,7 @@ public abstract class Context {
|
|||||||
//@hide: TIME_ZONE_DETECTOR_SERVICE,
|
//@hide: TIME_ZONE_DETECTOR_SERVICE,
|
||||||
PERMISSION_SERVICE,
|
PERMISSION_SERVICE,
|
||||||
LIGHTS_SERVICE,
|
LIGHTS_SERVICE,
|
||||||
|
UWB_SERVICE,
|
||||||
})
|
})
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
public @interface ServiceName {}
|
public @interface ServiceName {}
|
||||||
@@ -5177,6 +5178,15 @@ public abstract class Context {
|
|||||||
*/
|
*/
|
||||||
public static final String LIGHTS_SERVICE = "lights";
|
public static final String LIGHTS_SERVICE = "lights";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use with {@link #getSystemService(String)} to retrieve a
|
||||||
|
* {@link android.uwb.UwbManager}.
|
||||||
|
*
|
||||||
|
* @see #getSystemService(String)
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public static final String UWB_SERVICE = "uwb";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use with {@link #getSystemService(String)} to retrieve a
|
* Use with {@link #getSystemService(String)} to retrieve a
|
||||||
* {@link android.app.DreamManager} for controlling Dream states.
|
* {@link android.app.DreamManager} for controlling Dream states.
|
||||||
|
|||||||
@@ -2498,6 +2498,15 @@ public abstract class PackageManager {
|
|||||||
@SdkConstant(SdkConstantType.FEATURE)
|
@SdkConstant(SdkConstantType.FEATURE)
|
||||||
public static final String FEATURE_TELEPHONY_IMS = "android.hardware.telephony.ims";
|
public static final String FEATURE_TELEPHONY_IMS = "android.hardware.telephony.ims";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature for {@link #getSystemAvailableFeatures} and
|
||||||
|
* {@link #hasSystemFeature}: The device is capable of communicating with
|
||||||
|
* other devices via ultra wideband.
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@SdkConstant(SdkConstantType.FEATURE)
|
||||||
|
public static final String FEATURE_UWB = "android.hardware.uwb";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature for {@link #getSystemAvailableFeatures} and
|
* Feature for {@link #getSystemAvailableFeatures} and
|
||||||
* {@link #hasSystemFeature}: The device supports connecting to USB devices
|
* {@link #hasSystemFeature}: The device supports connecting to USB devices
|
||||||
|
|||||||
149
core/java/android/uwb/AdapterStateListener.java
Normal file
149
core/java/android/uwb/AdapterStateListener.java
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.uwb;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.uwb.UwbManager.AdapterStateCallback;
|
||||||
|
import android.uwb.UwbManager.AdapterStateCallback.StateChangedReason;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public class AdapterStateListener extends IUwbAdapterStateCallbacks.Stub {
|
||||||
|
private static final String TAG = "Uwb.StateListener";
|
||||||
|
|
||||||
|
private final IUwbAdapter mAdapter;
|
||||||
|
private boolean mIsRegistered = false;
|
||||||
|
|
||||||
|
private final Map<AdapterStateCallback, Executor> mCallbackMap = new HashMap<>();
|
||||||
|
|
||||||
|
@StateChangedReason
|
||||||
|
private int mAdapterStateChangeReason = AdapterStateCallback.STATE_CHANGED_REASON_ERROR_UNKNOWN;
|
||||||
|
private boolean mAdapterEnabledState = false;
|
||||||
|
|
||||||
|
public AdapterStateListener(@NonNull IUwbAdapter adapter) {
|
||||||
|
mAdapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an {@link AdapterStateCallback} with this {@link AdapterStateListener}
|
||||||
|
*
|
||||||
|
* @param executor an {@link Executor} to execute given callback
|
||||||
|
* @param callback user implementation of the {@link AdapterStateCallback}
|
||||||
|
*/
|
||||||
|
public void register(@NonNull Executor executor, @NonNull AdapterStateCallback callback) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (mCallbackMap.containsKey(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCallbackMap.put(callback, executor);
|
||||||
|
|
||||||
|
if (!mIsRegistered) {
|
||||||
|
try {
|
||||||
|
mAdapter.registerAdapterStateCallbacks(this);
|
||||||
|
mIsRegistered = true;
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "Failed to register adapter state callback");
|
||||||
|
executor.execute(() -> callback.onStateChanged(false,
|
||||||
|
AdapterStateCallback.STATE_CHANGED_REASON_ERROR_UNKNOWN));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendCurrentState(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister the specified {@link AdapterStateCallback}
|
||||||
|
*
|
||||||
|
* @param callback user implementation of the {@link AdapterStateCallback}
|
||||||
|
*/
|
||||||
|
public void unregister(@NonNull AdapterStateCallback callback) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (!mCallbackMap.containsKey(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCallbackMap.remove(callback);
|
||||||
|
|
||||||
|
if (mCallbackMap.isEmpty() && mIsRegistered) {
|
||||||
|
try {
|
||||||
|
mAdapter.unregisterAdapterStateCallbacks(this);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "Failed to unregister AdapterStateCallback with service");
|
||||||
|
}
|
||||||
|
mIsRegistered = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendCurrentState(@NonNull AdapterStateCallback callback) {
|
||||||
|
synchronized (this) {
|
||||||
|
Executor executor = mCallbackMap.get(callback);
|
||||||
|
|
||||||
|
final long identity = Binder.clearCallingIdentity();
|
||||||
|
try {
|
||||||
|
executor.execute(() -> callback.onStateChanged(
|
||||||
|
mAdapterEnabledState, mAdapterStateChangeReason));
|
||||||
|
} finally {
|
||||||
|
Binder.restoreCallingIdentity(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAdapterStateChanged(boolean isEnabled, int reason) {
|
||||||
|
synchronized (this) {
|
||||||
|
@StateChangedReason int localReason =
|
||||||
|
convertToStateChangedReason(reason);
|
||||||
|
mAdapterEnabledState = isEnabled;
|
||||||
|
mAdapterStateChangeReason = localReason;
|
||||||
|
for (AdapterStateCallback cb : mCallbackMap.keySet()) {
|
||||||
|
sendCurrentState(cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @StateChangedReason int convertToStateChangedReason(
|
||||||
|
@StateChangeReason int reason) {
|
||||||
|
switch (reason) {
|
||||||
|
case StateChangeReason.ALL_SESSIONS_CLOSED:
|
||||||
|
return AdapterStateCallback.STATE_CHANGED_REASON_ALL_SESSIONS_CLOSED;
|
||||||
|
|
||||||
|
case StateChangeReason.SESSION_STARTED:
|
||||||
|
return AdapterStateCallback.STATE_CHANGED_REASON_SESSION_STARTED;
|
||||||
|
|
||||||
|
case StateChangeReason.SYSTEM_POLICY:
|
||||||
|
return AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY;
|
||||||
|
|
||||||
|
case StateChangeReason.SYSTEM_BOOT:
|
||||||
|
return AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_BOOT;
|
||||||
|
|
||||||
|
case StateChangeReason.UNKNOWN:
|
||||||
|
default:
|
||||||
|
return AdapterStateCallback.STATE_CHANGED_REASON_ERROR_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,11 +98,18 @@ interface IUwbAdapter {
|
|||||||
int getMaxSimultaneousSessions();
|
int getMaxSimultaneousSessions();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the maximum number of remote devices per session
|
* Get the maximum number of remote devices per session when local device is initiator
|
||||||
*
|
*
|
||||||
* @return the maximum number of remote devices supported in a single session
|
* @return the maximum number of remote devices supported in a single session
|
||||||
*/
|
*/
|
||||||
int getMaxRemoteDevicesPerSession();
|
int getMaxRemoteDevicesPerInitiatorSession();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum number of remote devices per session when local device is responder
|
||||||
|
*
|
||||||
|
* @return the maximum number of remote devices supported in a single session
|
||||||
|
*/
|
||||||
|
int getMaxRemoteDevicesPerResponderSession();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the capabilities and features of the device
|
* Provides the capabilities and features of the device
|
||||||
|
|||||||
@@ -41,5 +41,10 @@ enum StateChangeReason {
|
|||||||
* The adapter state changed because of a device system change.
|
* The adapter state changed because of a device system change.
|
||||||
*/
|
*/
|
||||||
SYSTEM_POLICY,
|
SYSTEM_POLICY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to signal the first adapter state message after boot
|
||||||
|
*/
|
||||||
|
SYSTEM_BOOT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,21 @@
|
|||||||
|
|
||||||
package android.uwb;
|
package android.uwb;
|
||||||
|
|
||||||
|
import android.annotation.CallbackExecutor;
|
||||||
import android.annotation.IntDef;
|
import android.annotation.IntDef;
|
||||||
import android.annotation.NonNull;
|
import android.annotation.NonNull;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.SystemService;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.IBinder;
|
||||||
import android.os.PersistableBundle;
|
import android.os.PersistableBundle;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.os.ServiceManager;
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@@ -36,7 +44,13 @@ import java.util.concurrent.Executor;
|
|||||||
*
|
*
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
|
@SystemService(Context.UWB_SERVICE)
|
||||||
public final class UwbManager {
|
public final class UwbManager {
|
||||||
|
private IUwbAdapter mUwbAdapter;
|
||||||
|
private static final String SERVICE_NAME = "uwb";
|
||||||
|
|
||||||
|
private AdapterStateListener mAdapterStateListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for receiving UWB adapter state changes
|
* Interface for receiving UWB adapter state changes
|
||||||
*/
|
*/
|
||||||
@@ -96,10 +110,31 @@ public final class UwbManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Use <code>Context.getSystemService(UwbManager.class)</code> to get an instance.
|
* Use <code>Context.getSystemService(UwbManager.class)</code> to get an instance.
|
||||||
|
*
|
||||||
|
* @param adapter an instance of an {@link android.uwb.IUwbAdapter}
|
||||||
*/
|
*/
|
||||||
private UwbManager() {
|
private UwbManager(IUwbAdapter adapter) {
|
||||||
throw new UnsupportedOperationException();
|
mUwbAdapter = adapter;
|
||||||
|
mAdapterStateListener = new AdapterStateListener(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public static UwbManager getInstance() {
|
||||||
|
IBinder b = ServiceManager.getService(SERVICE_NAME);
|
||||||
|
if (b == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
IUwbAdapter adapter = IUwbAdapter.Stub.asInterface(b);
|
||||||
|
if (adapter == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UwbManager(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an {@link AdapterStateCallback} to listen for UWB adapter state changes
|
* Register an {@link AdapterStateCallback} to listen for UWB adapter state changes
|
||||||
* <p>The provided callback will be invoked by the given {@link Executor}.
|
* <p>The provided callback will be invoked by the given {@link Executor}.
|
||||||
@@ -112,8 +147,9 @@ public final class UwbManager {
|
|||||||
* @param executor an {@link Executor} to execute given callback
|
* @param executor an {@link Executor} to execute given callback
|
||||||
* @param callback user implementation of the {@link AdapterStateCallback}
|
* @param callback user implementation of the {@link AdapterStateCallback}
|
||||||
*/
|
*/
|
||||||
public void registerAdapterStateCallback(Executor executor, AdapterStateCallback callback) {
|
public void registerAdapterStateCallback(@NonNull @CallbackExecutor Executor executor,
|
||||||
throw new UnsupportedOperationException();
|
@NonNull AdapterStateCallback callback) {
|
||||||
|
mAdapterStateListener.register(executor, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,8 +161,8 @@ public final class UwbManager {
|
|||||||
*
|
*
|
||||||
* @param callback user implementation of the {@link AdapterStateCallback}
|
* @param callback user implementation of the {@link AdapterStateCallback}
|
||||||
*/
|
*/
|
||||||
public void unregisterAdapterStateCallback(AdapterStateCallback callback) {
|
public void unregisterAdapterStateCallback(@NonNull AdapterStateCallback callback) {
|
||||||
throw new UnsupportedOperationException();
|
mAdapterStateListener.unregister(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,7 +175,11 @@ public final class UwbManager {
|
|||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public PersistableBundle getSpecificationInfo() {
|
public PersistableBundle getSpecificationInfo() {
|
||||||
throw new UnsupportedOperationException();
|
try {
|
||||||
|
return mUwbAdapter.getSpecificationInfo();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,7 +188,11 @@ public final class UwbManager {
|
|||||||
* @return true if ranging is supported
|
* @return true if ranging is supported
|
||||||
*/
|
*/
|
||||||
public boolean isRangingSupported() {
|
public boolean isRangingSupported() {
|
||||||
throw new UnsupportedOperationException();
|
try {
|
||||||
|
return mUwbAdapter.isRangingSupported();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@@ -197,7 +241,24 @@ public final class UwbManager {
|
|||||||
*/
|
*/
|
||||||
@AngleOfArrivalSupportType
|
@AngleOfArrivalSupportType
|
||||||
public int getAngleOfArrivalSupport() {
|
public int getAngleOfArrivalSupport() {
|
||||||
throw new UnsupportedOperationException();
|
try {
|
||||||
|
switch (mUwbAdapter.getAngleOfArrivalSupport()) {
|
||||||
|
case AngleOfArrivalSupport.TWO_DIMENSIONAL:
|
||||||
|
return ANGLE_OF_ARRIVAL_SUPPORT_TYPE_2D;
|
||||||
|
|
||||||
|
case AngleOfArrivalSupport.THREE_DIMENSIONAL_HEMISPHERICAL:
|
||||||
|
return ANGLE_OF_ARRIVAL_SUPPORT_TYPE_3D_HEMISPHERICAL;
|
||||||
|
|
||||||
|
case AngleOfArrivalSupport.THREE_DIMENSIONAL_SPHERICAL:
|
||||||
|
return ANGLE_OF_ARRIVAL_SUPPORT_TYPE_3D_SPHERICAL;
|
||||||
|
|
||||||
|
case AngleOfArrivalSupport.NONE:
|
||||||
|
default:
|
||||||
|
return ANGLE_OF_ARRIVAL_SUPPORT_TYPE_NONE;
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -211,7 +272,15 @@ public final class UwbManager {
|
|||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<Integer> getSupportedChannelNumbers() {
|
public List<Integer> getSupportedChannelNumbers() {
|
||||||
throw new UnsupportedOperationException();
|
List<Integer> channels = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (int channel : mUwbAdapter.getSupportedChannels()) {
|
||||||
|
channels.add(channel);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
|
return channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -222,7 +291,15 @@ public final class UwbManager {
|
|||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public Set<Integer> getSupportedPreambleCodeIndices() {
|
public Set<Integer> getSupportedPreambleCodeIndices() {
|
||||||
throw new UnsupportedOperationException();
|
Set<Integer> preambles = new HashSet<>();
|
||||||
|
try {
|
||||||
|
for (int preamble : mUwbAdapter.getSupportedPreambleCodes()) {
|
||||||
|
preambles.add(preamble);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
|
return preambles;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,7 +311,11 @@ public final class UwbManager {
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("MethodNameUnits")
|
@SuppressLint("MethodNameUnits")
|
||||||
public long elapsedRealtimeResolutionNanos() {
|
public long elapsedRealtimeResolutionNanos() {
|
||||||
throw new UnsupportedOperationException();
|
try {
|
||||||
|
return mUwbAdapter.getTimestampResolutionNanos();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -243,7 +324,11 @@ public final class UwbManager {
|
|||||||
* @return the maximum allowed number of simultaneously open {@link RangingSession} instances.
|
* @return the maximum allowed number of simultaneously open {@link RangingSession} instances.
|
||||||
*/
|
*/
|
||||||
public int getMaxSimultaneousSessions() {
|
public int getMaxSimultaneousSessions() {
|
||||||
throw new UnsupportedOperationException();
|
try {
|
||||||
|
return mUwbAdapter.getMaxSimultaneousSessions();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,7 +338,11 @@ public final class UwbManager {
|
|||||||
* @return the maximum number of remote devices per {@link RangingSession}
|
* @return the maximum number of remote devices per {@link RangingSession}
|
||||||
*/
|
*/
|
||||||
public int getMaxRemoteDevicesPerInitiatorSession() {
|
public int getMaxRemoteDevicesPerInitiatorSession() {
|
||||||
throw new UnsupportedOperationException();
|
try {
|
||||||
|
return mUwbAdapter.getMaxRemoteDevicesPerInitiatorSession();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -263,7 +352,11 @@ public final class UwbManager {
|
|||||||
* @return the maximum number of remote devices per {@link RangingSession}
|
* @return the maximum number of remote devices per {@link RangingSession}
|
||||||
*/
|
*/
|
||||||
public int getMaxRemoteDevicesPerResponderSession() {
|
public int getMaxRemoteDevicesPerResponderSession() {
|
||||||
throw new UnsupportedOperationException();
|
try {
|
||||||
|
return mUwbAdapter.getMaxRemoteDevicesPerResponderSession();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ android_test {
|
|||||||
static_libs: [
|
static_libs: [
|
||||||
"androidx.test.ext.junit",
|
"androidx.test.ext.junit",
|
||||||
"androidx.test.rules",
|
"androidx.test.rules",
|
||||||
|
"mockito-target-minus-junit4",
|
||||||
],
|
],
|
||||||
libs: [
|
libs: [
|
||||||
"android.test.runner",
|
"android.test.runner",
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.uwb;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.uwb.UwbManager.AdapterStateCallback;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.filters.SmallTest;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test of {@link AdapterStateListener}.
|
||||||
|
*/
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class AdapterStateListenerTest {
|
||||||
|
|
||||||
|
IUwbAdapter mUwbAdapter = mock(IUwbAdapter.class);
|
||||||
|
|
||||||
|
Answer mRegisterSuccessAnswer = new Answer() {
|
||||||
|
public Object answer(InvocationOnMock invocation) {
|
||||||
|
Object[] args = invocation.getArguments();
|
||||||
|
IUwbAdapterStateCallbacks cb = (IUwbAdapterStateCallbacks) args[0];
|
||||||
|
try {
|
||||||
|
cb.onAdapterStateChanged(false, StateChangeReason.UNKNOWN);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
return new Object();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Throwable mThrowRemoteException = new RemoteException("RemoteException");
|
||||||
|
|
||||||
|
private static Executor getExecutor() {
|
||||||
|
return new Executor() {
|
||||||
|
@Override
|
||||||
|
public void execute(Runnable command) {
|
||||||
|
command.run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyCallbackStateChangedInvoked(
|
||||||
|
AdapterStateCallback callback, int numTimes) {
|
||||||
|
verify(callback, times(numTimes)).onStateChanged(anyBoolean(), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRegister_RegisterUnregister() throws RemoteException {
|
||||||
|
doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
|
||||||
|
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
AdapterStateCallback callback1 = mock(AdapterStateCallback.class);
|
||||||
|
AdapterStateCallback callback2 = mock(AdapterStateCallback.class);
|
||||||
|
|
||||||
|
// Verify that the adapter state listener registered with the UWB Adapter
|
||||||
|
adapterStateListener.register(getExecutor(), callback1);
|
||||||
|
verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
|
||||||
|
verifyCallbackStateChangedInvoked(callback1, 1);
|
||||||
|
verifyCallbackStateChangedInvoked(callback2, 0);
|
||||||
|
|
||||||
|
// Register a second client and no new call to UWB Adapter
|
||||||
|
adapterStateListener.register(getExecutor(), callback2);
|
||||||
|
verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
|
||||||
|
verifyCallbackStateChangedInvoked(callback1, 1);
|
||||||
|
verifyCallbackStateChangedInvoked(callback2, 1);
|
||||||
|
|
||||||
|
// Unregister first callback
|
||||||
|
adapterStateListener.unregister(callback1);
|
||||||
|
verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
|
||||||
|
verify(mUwbAdapter, times(0)).unregisterAdapterStateCallbacks(any());
|
||||||
|
verifyCallbackStateChangedInvoked(callback1, 1);
|
||||||
|
verifyCallbackStateChangedInvoked(callback2, 1);
|
||||||
|
|
||||||
|
// Unregister second callback
|
||||||
|
adapterStateListener.unregister(callback2);
|
||||||
|
verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
|
||||||
|
verify(mUwbAdapter, times(1)).unregisterAdapterStateCallbacks(any());
|
||||||
|
verifyCallbackStateChangedInvoked(callback1, 1);
|
||||||
|
verifyCallbackStateChangedInvoked(callback2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRegister_FirstRegisterFails() throws RemoteException {
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
AdapterStateCallback callback1 = mock(AdapterStateCallback.class);
|
||||||
|
AdapterStateCallback callback2 = mock(AdapterStateCallback.class);
|
||||||
|
|
||||||
|
// Throw a remote exception whenever first registering
|
||||||
|
doThrow(mThrowRemoteException).when(mUwbAdapter).registerAdapterStateCallbacks(any());
|
||||||
|
|
||||||
|
adapterStateListener.register(getExecutor(), callback1);
|
||||||
|
verify(mUwbAdapter, times(1)).registerAdapterStateCallbacks(any());
|
||||||
|
|
||||||
|
// No longer throw an exception, instead succeed
|
||||||
|
doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
|
||||||
|
|
||||||
|
// Register a different callback
|
||||||
|
adapterStateListener.register(getExecutor(), callback2);
|
||||||
|
verify(mUwbAdapter, times(2)).registerAdapterStateCallbacks(any());
|
||||||
|
|
||||||
|
// Ensure first callback was invoked again
|
||||||
|
verifyCallbackStateChangedInvoked(callback1, 2);
|
||||||
|
verifyCallbackStateChangedInvoked(callback2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRegister_RegisterSameCallbackTwice() throws RemoteException {
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
AdapterStateCallback callback = mock(AdapterStateCallback.class);
|
||||||
|
doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
|
||||||
|
|
||||||
|
adapterStateListener.register(getExecutor(), callback);
|
||||||
|
verifyCallbackStateChangedInvoked(callback, 1);
|
||||||
|
|
||||||
|
adapterStateListener.register(getExecutor(), callback);
|
||||||
|
verifyCallbackStateChangedInvoked(callback, 1);
|
||||||
|
|
||||||
|
// Invoke a state change and ensure the callback is only called once
|
||||||
|
adapterStateListener.onAdapterStateChanged(false, StateChangeReason.UNKNOWN);
|
||||||
|
verifyCallbackStateChangedInvoked(callback, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCallback_RunViaExecutor_Success() throws RemoteException {
|
||||||
|
// Verify that the callbacks are invoked on the executor when successful
|
||||||
|
doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
|
||||||
|
runViaExecutor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCallback_RunViaExecutor_Failure() throws RemoteException {
|
||||||
|
// Verify that the callbacks are invoked on the executor when there is a remote exception
|
||||||
|
doThrow(mThrowRemoteException).when(mUwbAdapter).registerAdapterStateCallbacks(any());
|
||||||
|
runViaExecutor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runViaExecutor() {
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
AdapterStateCallback callback = mock(AdapterStateCallback.class);
|
||||||
|
|
||||||
|
Executor executor = mock(Executor.class);
|
||||||
|
|
||||||
|
// Do not run commands received and ensure that the callback is not invoked
|
||||||
|
doAnswer(new ExecutorAnswer(false)).when(executor).execute(any());
|
||||||
|
adapterStateListener.register(executor, callback);
|
||||||
|
verify(executor, times(1)).execute(any());
|
||||||
|
verifyCallbackStateChangedInvoked(callback, 0);
|
||||||
|
|
||||||
|
// Manually invoke the callback and ensure callback is not invoked
|
||||||
|
adapterStateListener.onAdapterStateChanged(false, StateChangeReason.UNKNOWN);
|
||||||
|
verify(executor, times(2)).execute(any());
|
||||||
|
verifyCallbackStateChangedInvoked(callback, 0);
|
||||||
|
|
||||||
|
// Run the command that the executor receives
|
||||||
|
doAnswer(new ExecutorAnswer(true)).when(executor).execute(any());
|
||||||
|
adapterStateListener.onAdapterStateChanged(false, StateChangeReason.UNKNOWN);
|
||||||
|
verify(executor, times(3)).execute(any());
|
||||||
|
verifyCallbackStateChangedInvoked(callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExecutorAnswer implements Answer {
|
||||||
|
|
||||||
|
final boolean mShouldRun;
|
||||||
|
ExecutorAnswer(boolean shouldRun) {
|
||||||
|
mShouldRun = shouldRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
|
if (mShouldRun) {
|
||||||
|
((Runnable) invocation.getArgument(0)).run();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotify_AllCallbacksNotified() throws RemoteException {
|
||||||
|
doAnswer(mRegisterSuccessAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
|
||||||
|
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
List<AdapterStateCallback> callbacks = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
AdapterStateCallback callback = mock(AdapterStateCallback.class);
|
||||||
|
adapterStateListener.register(getExecutor(), callback);
|
||||||
|
callbacks.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure every callback got the initial state
|
||||||
|
for (AdapterStateCallback callback : callbacks) {
|
||||||
|
verifyCallbackStateChangedInvoked(callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke a state change and ensure all callbacks are invoked
|
||||||
|
adapterStateListener.onAdapterStateChanged(true, StateChangeReason.ALL_SESSIONS_CLOSED);
|
||||||
|
for (AdapterStateCallback callback : callbacks) {
|
||||||
|
verifyCallbackStateChangedInvoked(callback, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateChange_CorrectValue() {
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
|
||||||
|
AdapterStateCallback callback = mock(AdapterStateCallback.class);
|
||||||
|
|
||||||
|
adapterStateListener.register(getExecutor(), callback);
|
||||||
|
|
||||||
|
runStateChangeValue(StateChangeReason.ALL_SESSIONS_CLOSED,
|
||||||
|
AdapterStateCallback.STATE_CHANGED_REASON_ALL_SESSIONS_CLOSED);
|
||||||
|
|
||||||
|
runStateChangeValue(StateChangeReason.SESSION_STARTED,
|
||||||
|
AdapterStateCallback.STATE_CHANGED_REASON_SESSION_STARTED);
|
||||||
|
|
||||||
|
runStateChangeValue(StateChangeReason.SYSTEM_BOOT,
|
||||||
|
AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_BOOT);
|
||||||
|
|
||||||
|
runStateChangeValue(StateChangeReason.SYSTEM_POLICY,
|
||||||
|
AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY);
|
||||||
|
|
||||||
|
runStateChangeValue(StateChangeReason.UNKNOWN,
|
||||||
|
AdapterStateCallback.STATE_CHANGED_REASON_ERROR_UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runStateChangeValue(@StateChangeReason int reasonIn,
|
||||||
|
@AdapterStateCallback.StateChangedReason int reasonOut) {
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
AdapterStateCallback callback = mock(AdapterStateCallback.class);
|
||||||
|
adapterStateListener.register(getExecutor(), callback);
|
||||||
|
|
||||||
|
adapterStateListener.onAdapterStateChanged(false, reasonIn);
|
||||||
|
verify(callback, times(1)).onStateChanged(false, reasonOut);
|
||||||
|
|
||||||
|
adapterStateListener.onAdapterStateChanged(true, reasonIn);
|
||||||
|
verify(callback, times(1)).onStateChanged(true, reasonOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateChange_FirstRegisterGetsCorrectState() throws RemoteException {
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
AdapterStateCallback callback = mock(AdapterStateCallback.class);
|
||||||
|
|
||||||
|
Answer registerAnswer = new Answer() {
|
||||||
|
public Object answer(InvocationOnMock invocation) {
|
||||||
|
Object[] args = invocation.getArguments();
|
||||||
|
IUwbAdapterStateCallbacks cb = (IUwbAdapterStateCallbacks) args[0];
|
||||||
|
try {
|
||||||
|
cb.onAdapterStateChanged(true, StateChangeReason.SESSION_STARTED);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
return new Object();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
doAnswer(registerAnswer).when(mUwbAdapter).registerAdapterStateCallbacks(any());
|
||||||
|
|
||||||
|
adapterStateListener.register(getExecutor(), callback);
|
||||||
|
verify(callback).onStateChanged(true,
|
||||||
|
AdapterStateCallback.STATE_CHANGED_REASON_SESSION_STARTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateChange_SecondRegisterGetsCorrectState() {
|
||||||
|
AdapterStateListener adapterStateListener = new AdapterStateListener(mUwbAdapter);
|
||||||
|
AdapterStateCallback callback1 = mock(AdapterStateCallback.class);
|
||||||
|
AdapterStateCallback callback2 = mock(AdapterStateCallback.class);
|
||||||
|
|
||||||
|
adapterStateListener.register(getExecutor(), callback1);
|
||||||
|
adapterStateListener.onAdapterStateChanged(true, StateChangeReason.SYSTEM_BOOT);
|
||||||
|
|
||||||
|
adapterStateListener.register(getExecutor(), callback2);
|
||||||
|
verify(callback2).onStateChanged(true,
|
||||||
|
AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_BOOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
core/tests/uwbtests/src/android/uwb/UwbManagerTest.java
Normal file
49
core/tests/uwbtests/src/android/uwb/UwbManagerTest.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.uwb;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.test.InstrumentationRegistry;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.filters.SmallTest;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test of {@link UwbManager}.
|
||||||
|
*/
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class UwbManagerTest {
|
||||||
|
|
||||||
|
public final Context mContext = InstrumentationRegistry.getContext();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testServiceAvailable() {
|
||||||
|
UwbManager manager = mContext.getSystemService(UwbManager.class);
|
||||||
|
if (UwbTestUtils.isUwbSupported(mContext)) {
|
||||||
|
assertNotNull(manager);
|
||||||
|
} else {
|
||||||
|
assertNull(manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package android.uwb;
|
package android.uwb;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -24,6 +26,11 @@ import java.util.List;
|
|||||||
public class UwbTestUtils {
|
public class UwbTestUtils {
|
||||||
private UwbTestUtils() {}
|
private UwbTestUtils() {}
|
||||||
|
|
||||||
|
public static boolean isUwbSupported(Context context) {
|
||||||
|
PackageManager packageManager = context.getPackageManager();
|
||||||
|
return packageManager.hasSystemFeature(PackageManager.FEATURE_UWB);
|
||||||
|
}
|
||||||
|
|
||||||
public static AngleMeasurement getAngleMeasurement() {
|
public static AngleMeasurement getAngleMeasurement() {
|
||||||
return new AngleMeasurement.Builder()
|
return new AngleMeasurement.Builder()
|
||||||
.setRadians(getDoubleInRange(-Math.PI, Math.PI))
|
.setRadians(getDoubleInRange(-Math.PI, Math.PI))
|
||||||
|
|||||||
Reference in New Issue
Block a user