Fix app subscription adding issue for removed devices

revert-70ac1926
tcdlpds@gmail.com 4 years ago
parent 3bc6ca31b3
commit 9f76c003a6

@ -67,6 +67,7 @@ import org.wso2.carbon.device.application.mgt.core.util.HelperUtil;
import org.wso2.carbon.device.application.mgt.core.util.OAuthUtils; import org.wso2.carbon.device.application.mgt.core.util.OAuthUtils;
import org.wso2.carbon.device.mgt.common.Device; import org.wso2.carbon.device.mgt.common.Device;
import org.wso2.carbon.device.mgt.common.DeviceIdentifier; import org.wso2.carbon.device.mgt.common.DeviceIdentifier;
import org.wso2.carbon.device.mgt.common.EnrolmentInfo;
import org.wso2.carbon.device.mgt.common.MDMAppConstants; import org.wso2.carbon.device.mgt.common.MDMAppConstants;
import org.wso2.carbon.device.mgt.common.app.mgt.App; import org.wso2.carbon.device.mgt.common.app.mgt.App;
import org.wso2.carbon.device.mgt.common.app.mgt.MobileAppTypes; import org.wso2.carbon.device.mgt.common.app.mgt.MobileAppTypes;
@ -441,6 +442,11 @@ public class SubscriptionManagerImpl implements SubscriptionManager {
List<DeviceIdentifier> errorDeviceIdentifiers = new ArrayList<>(); List<DeviceIdentifier> errorDeviceIdentifiers = new ArrayList<>();
String deviceTypeName = null; String deviceTypeName = null;
List<String> allowingDeviceStatuses = new ArrayList<>();
allowingDeviceStatuses.add(EnrolmentInfo.Status.ACTIVE.toString());
allowingDeviceStatuses.add(EnrolmentInfo.Status.INACTIVE.toString());
allowingDeviceStatuses.add(EnrolmentInfo.Status.UNREACHABLE.toString());
try { try {
if (!ApplicationType.WEB_CLIP.toString().equals(applicationDTO.getType())) { if (!ApplicationType.WEB_CLIP.toString().equals(applicationDTO.getType())) {
deviceTypeName = APIUtil.getDeviceTypeData(applicationDTO.getDeviceTypeId()).getName(); deviceTypeName = APIUtil.getDeviceTypeData(applicationDTO.getDeviceTypeId()).getName();
@ -472,21 +478,24 @@ public class SubscriptionManagerImpl implements SubscriptionManager {
for (T param : params) { for (T param : params) {
String username = (String) param; String username = (String) param;
subscribers.add(username); subscribers.add(username);
devices.addAll(deviceManagementProviderService.getDevicesOfUser(username)); devices.addAll(deviceManagementProviderService.getDevicesOfUser(username,
allowingDeviceStatuses, false ));
} }
} else { } else {
if (SubscriptionType.ROLE.toString().equalsIgnoreCase(subType)) { if (SubscriptionType.ROLE.toString().equalsIgnoreCase(subType)) {
for (T param : params) { for (T param : params) {
String roleName = (String) param; String roleName = (String) param;
subscribers.add(roleName); subscribers.add(roleName);
devices.addAll(deviceManagementProviderService.getAllDevicesOfRole(roleName)); devices.addAll(deviceManagementProviderService
.getAllDevicesOfRole(roleName, allowingDeviceStatuses, false));
} }
} else { } else {
if (SubscriptionType.GROUP.toString().equalsIgnoreCase(subType)) { if (SubscriptionType.GROUP.toString().equalsIgnoreCase(subType)) {
for (T param : params) { for (T param : params) {
String groupName = (String) param; String groupName = (String) param;
subscribers.add(groupName); subscribers.add(groupName);
devices.addAll(groupManagementProviderService.getAllDevicesOfGroup(groupName, true)); devices.addAll(groupManagementProviderService.getAllDevicesOfGroup(groupName,
allowingDeviceStatuses, true));
} }
} else { } else {
String msg = String msg =

@ -383,6 +383,18 @@ public interface DeviceDAO {
*/ */
List<Device> getDevicesOfUser(PaginationRequest request, int tenantId) throws DeviceManagementDAOException; List<Device> getDevicesOfUser(PaginationRequest request, int tenantId) throws DeviceManagementDAOException;
/**
* This method is using to retrieve devices of a given user of given device statues
*
* @param username Username
* @param tenantId Tenant Id
* @param deviceStatuses Device Statuses
* @returnList of devices
* @throws DeviceManagementDAOException if error ccured while getting devices from the database
*/
List<Device> getDevicesOfUser(String username, int tenantId, List<String> deviceStatuses)
throws DeviceManagementDAOException;
/** /**
* This method is used to retrieve the device count of a given tenant. * This method is used to retrieve the device count of a given tenant.
* *

@ -236,6 +236,19 @@ public interface GroupDAO {
List<Device> getDevices(int groupId, int startIndex, int rowCount, int tenantId) List<Device> getDevices(int groupId, int startIndex, int rowCount, int tenantId)
throws GroupManagementDAOException; throws GroupManagementDAOException;
/**
* Get All the devices that are in one of the given device status and belongs to given group
*
* @param groupName Group name
* @param deviceStatuses Device Statuses
* @param tenantId Tenant Id
* @return List of devices
* @throws GroupManagementDAOException if error occurred while retreving list of devices that are in one of the
* given device status and belongs to the given group
*/
List<Device> getAllDevicesOfGroup(String groupName, List<String> deviceStatuses, int tenantId)
throws GroupManagementDAOException;
List<Device> getAllDevicesOfGroup(String groupName, int tenantId) throws GroupManagementDAOException; List<Device> getAllDevicesOfGroup(String groupName, int tenantId) throws GroupManagementDAOException;
/** /**

@ -930,6 +930,73 @@ public abstract class AbstractDeviceDAOImpl implements DeviceDAO {
return devices; return devices;
} }
@Override
public List<Device> getDevicesOfUser(String username, int tenantId, List<String> deviceStatuses)
throws DeviceManagementDAOException {
List<Device> devices = new ArrayList<>();
try {
Connection conn = this.getConnection();
StringJoiner joiner = new StringJoiner(",","SELECT "
+ "e1.OWNER, "
+ "e1.OWNERSHIP, "
+ "e1.ENROLMENT_ID, "
+ "e1.DEVICE_ID, "
+ "e1.STATUS, "
+ "e1.IS_TRANSFERRED, "
+ "e1.DATE_OF_LAST_UPDATE, "
+ "e1.DATE_OF_ENROLMENT, "
+ "d.DESCRIPTION, "
+ "d.NAME AS DEVICE_NAME, "
+ "d.DEVICE_IDENTIFICATION, "
+ "t.NAME AS DEVICE_TYPE "
+ "FROM "
+ "DM_DEVICE d, "
+ "(SELECT "
+ "e.OWNER, "
+ "e.OWNERSHIP, "
+ "e.ID AS ENROLMENT_ID, "
+ "e.DEVICE_ID, "
+ "e.STATUS, "
+ "e.IS_TRANSFERRED, "
+ "e.DATE_OF_LAST_UPDATE, "
+ "e.DATE_OF_ENROLMENT "
+ "FROM "
+ "DM_ENROLMENT e "
+ "WHERE "
+ "e.TENANT_ID = ? AND "
+ "LOWER(e.OWNER) = LOWER(?) AND "
+ "e.STATUS IN (",
")) e1, "
+ "DM_DEVICE_TYPE t "
+ "WHERE d.ID = e1.DEVICE_ID AND "
+ "t.ID = d.DEVICE_TYPE_ID "
+ "ORDER BY e1.DATE_OF_LAST_UPDATE DESC");
deviceStatuses.stream().map(ignored -> "?").forEach(joiner::add);
String query = joiner.toString();
try (PreparedStatement stmt = conn.prepareStatement(query)) {
int index = 1;
stmt.setInt(index++, tenantId);
stmt.setString(index++, username);
for (String deviceId : deviceStatuses) {
stmt.setObject(index++, deviceId);
}
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
Device device = DeviceManagementDAOUtil.loadDevice(rs);
devices.add(device);
}
}
}
} catch (SQLException e) {
String msg = "Error occurred while fetching the list of devices belongs to '" + username + "'";
log.error(msg, e);
throw new DeviceManagementDAOException(msg, e);
}
return devices;
}
@Override @Override
public int getCountOfDevicesInGroup(PaginationRequest request, int tenantId) public int getCountOfDevicesInGroup(PaginationRequest request, int tenantId)
throws DeviceManagementDAOException { throws DeviceManagementDAOException {

@ -25,6 +25,7 @@ import org.wso2.carbon.device.mgt.common.Device;
import org.wso2.carbon.device.mgt.common.GroupPaginationRequest; import org.wso2.carbon.device.mgt.common.GroupPaginationRequest;
import org.wso2.carbon.device.mgt.common.PaginationRequest; import org.wso2.carbon.device.mgt.common.PaginationRequest;
import org.wso2.carbon.device.mgt.common.group.mgt.DeviceGroup; import org.wso2.carbon.device.mgt.common.group.mgt.DeviceGroup;
import org.wso2.carbon.device.mgt.core.dao.DeviceManagementDAOException;
import org.wso2.carbon.device.mgt.core.dao.GroupDAO; import org.wso2.carbon.device.mgt.core.dao.GroupDAO;
import org.wso2.carbon.device.mgt.core.dao.GroupManagementDAOException; import org.wso2.carbon.device.mgt.core.dao.GroupManagementDAOException;
import org.wso2.carbon.device.mgt.core.dao.GroupManagementDAOFactory; import org.wso2.carbon.device.mgt.core.dao.GroupManagementDAOFactory;
@ -767,6 +768,66 @@ public abstract class AbstractGroupDAOImpl implements GroupDAO {
} }
@Override @Override
public List<Device> getAllDevicesOfGroup(String groupName, List<String> deviceStatuses, int tenantId)
throws GroupManagementDAOException {
List<Device> devices = new ArrayList<>();
try {
Connection conn = GroupManagementDAOFactory.getConnection();
StringJoiner joiner = new StringJoiner(",","SELECT "
+ "d1.DEVICE_ID, "
+ "d1.DESCRIPTION, "
+ "d1.NAME AS DEVICE_NAME, "
+ "d1.DEVICE_TYPE, "
+ "d1.DEVICE_IDENTIFICATION, "
+ "e.OWNER, "
+ "e.OWNERSHIP, "
+ "e.STATUS, "
+ "e.IS_TRANSFERRED, "
+ "e.DATE_OF_LAST_UPDATE, "
+ "e.DATE_OF_ENROLMENT, "
+ "e.ID AS ENROLMENT_ID "
+ "FROM "
+ "DM_ENROLMENT e, "
+ "(SELECT gd.DEVICE_ID, gd.DESCRIPTION, gd.NAME, gd.DEVICE_IDENTIFICATION, t.NAME AS DEVICE_TYPE "
+ "FROM "
+ "(SELECT d.ID AS DEVICE_ID, d.DESCRIPTION, d.NAME, d.DEVICE_IDENTIFICATION, d.DEVICE_TYPE_ID "
+ "FROM DM_DEVICE d, "
+ "(SELECT dgm.DEVICE_ID "
+ "FROM DM_DEVICE_GROUP_MAP dgm "
+ "WHERE dgm.GROUP_ID = (SELECT ID FROM DM_GROUP WHERE GROUP_NAME = ? )) dgm1 "
+ "WHERE d.ID = dgm1.DEVICE_ID AND d.TENANT_ID = ?) gd, DM_DEVICE_TYPE t "
+ "WHERE gd.DEVICE_TYPE_ID = t.ID) d1 "
+ "WHERE d1.DEVICE_ID = e.DEVICE_ID AND TENANT_ID = ? AND e.STATUS IN (",
")");
deviceStatuses.stream().map(ignored -> "?").forEach(joiner::add);
String query = joiner.toString();
try (PreparedStatement stmt = conn.prepareStatement(query)) {
int index = 1;
stmt.setString(index++, groupName);
stmt.setInt(index++, tenantId);
stmt.setInt(index++, tenantId);
for (String deviceId : deviceStatuses) {
stmt.setObject(index++, deviceId);
}
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
Device device = DeviceManagementDAOUtil.loadDevice(rs);
devices.add(device);
}
}
}
} catch (SQLException e) {
String msg = "Error occurred while fetching the list of devices belongs to '" + groupName + "'";
log.error(msg, e);
throw new GroupManagementDAOException(msg, e);
}
return devices;
}
@Override
public List<Device> getAllDevicesOfGroup(String groupName, int tenantId) throws GroupManagementDAOException { public List<Device> getAllDevicesOfGroup(String groupName, int tenantId) throws GroupManagementDAOException {
Connection conn; Connection conn;
List<Device> devices; List<Device> devices;

@ -417,6 +417,20 @@ public interface DeviceManagementProviderService {
*/ */
List<Device> getDevicesOfUser(String userName, boolean requireDeviceInfo) throws DeviceManagementException; List<Device> getDevicesOfUser(String userName, boolean requireDeviceInfo) throws DeviceManagementException;
/**
* Method to get the list of devices owned by an user of given device statuses
*
* @param username username
* @param deviceStatuses device statuses
* @param requireDeviceInfo - A boolean indicating whether the device-info (location, app-info etc) is also required
* along with the device data.
* @return List of devices
* @throws DeviceManagementException If some unusual behaviour is observed while fetching the
* device list
*/
List<Device> getDevicesOfUser(String username, List<String> deviceStatuses, boolean requireDeviceInfo)
throws DeviceManagementException;
/** /**
* This method returns the list of device owned by a user of given device type. * This method returns the list of device owned by a user of given device type.
* *
@ -463,6 +477,20 @@ public interface DeviceManagementProviderService {
*/ */
List<Device> getAllDevicesOfRole(String roleName, boolean requireDeviceInfo) throws DeviceManagementException; List<Device> getAllDevicesOfRole(String roleName, boolean requireDeviceInfo) throws DeviceManagementException;
/**
* Method to get the list of devices that are in one ohe given status and owned by users of a particular user-role.
*
* @param roleName Role name of the users
* @param requireDeviceInfo - A boolean indicating whether the device-info (location, app-info etc) is also required
* along with the device data.
* @param deviceStatuses List of device statuses
* @return List of devices
* @throws DeviceManagementException If some unusual behaviour is observed while fetching the
* device list
*/
List<Device> getAllDevicesOfRole(String roleName, List<String> deviceStatuses, boolean requireDeviceInfo)
throws DeviceManagementException;
/** /**
* This method is used to retrieve list of devices based on the device status with paging information. * This method is used to retrieve list of devices based on the device status with paging information.
* *

@ -2226,6 +2226,45 @@ public class DeviceManagementProviderServiceImpl implements DeviceManagementProv
return userDevices; return userDevices;
} }
@Override
public List<Device> getDevicesOfUser(String username, List<String> deviceStatuses, boolean requireDeviceInfo)
throws DeviceManagementException {
if (username == null) {
String msg = "Username null in getDevicesOfUser";
log.error(msg);
throw new DeviceManagementException(msg);
}
if (log.isDebugEnabled()) {
log.debug("Get devices of user with username '" + username + "' and requiredDeviceInfo " + requireDeviceInfo);
}
List<Device> userDevices;
try {
DeviceManagementDAOFactory.openConnection();
userDevices = deviceDAO.getDevicesOfUser(username, this.getTenantId(), deviceStatuses);
} catch (DeviceManagementDAOException e) {
String msg = "Error occurred while retrieving the list of devices that " +
"belong to the user '" + username + "'";
log.error(msg, e);
throw new DeviceManagementException(msg, e);
} catch (SQLException e) {
String msg = "Error occurred while opening a connection to the data source to get devices of user: "
+ username;
log.error(msg, e);
throw new DeviceManagementException(msg, e);
} catch (Exception e) {
String msg = "Error occurred in getDevicesOfUser for username '" + username + "'";
log.error(msg, e);
throw new DeviceManagementException(msg, e);
} finally {
DeviceManagementDAOFactory.closeConnection();
}
if (requireDeviceInfo) {
return this.populateAllDeviceInfo(userDevices);
}
return userDevices;
}
@Override @Override
public List<Device> getDevicesOfUser(String username, String deviceType) throws DeviceManagementException { public List<Device> getDevicesOfUser(String username, String deviceType) throws DeviceManagementException {
return this.getDevicesOfUser(username, deviceType, true); return this.getDevicesOfUser(username, deviceType, true);
@ -2393,20 +2432,8 @@ public class DeviceManagementProviderServiceImpl implements DeviceManagementProv
log.debug("Get devices of role '" + role + "' and requiredDeviceInfo: " + requireDeviceInfo); log.debug("Get devices of role '" + role + "' and requiredDeviceInfo: " + requireDeviceInfo);
} }
List<Device> devices = new ArrayList<>(); List<Device> devices = new ArrayList<>();
String[] users; String[] users = getUserListOfRole(role);
int tenantId = this.getTenantId(); int tenantId = this.getTenantId();
try {
users = DeviceManagementDataHolder.getInstance().getRealmService().getTenantUserRealm(tenantId)
.getUserStoreManager().getUserListOfRole(role);
} catch (UserStoreException e) {
String msg = "Error occurred while obtaining the users, who are assigned with the role '" + role + "'";
log.error(msg, e);
throw new DeviceManagementException(msg, e);
} catch (Exception e) {
String msg = "Error occurred in getAllDevicesOfRole for role '" + role + "'";
log.error(msg, e);
throw new DeviceManagementException(msg, e);
}
List<Device> userDevices; List<Device> userDevices;
for (String user : users) { for (String user : users) {
@ -2432,6 +2459,45 @@ public class DeviceManagementProviderServiceImpl implements DeviceManagementProv
return devices; return devices;
} }
@Override
public List<Device> getAllDevicesOfRole(String role, List<String> deviceStatuses, boolean requireDeviceInfo)
throws DeviceManagementException {
if (role == null || role.isEmpty()) {
String msg = "Received empty role for the method getAllDevicesOfRole";
log.error(msg);
throw new DeviceManagementException(msg);
}
if (log.isDebugEnabled()) {
log.debug("Get devices of role '" + role + "' and requiredDeviceInfo: " + requireDeviceInfo);
}
List<Device> devices = new ArrayList<>();
String[] users = getUserListOfRole(role);
for (String user : users) {
devices.addAll(getDevicesOfUser(user, deviceStatuses, requireDeviceInfo));
}
return devices;
}
private String[] getUserListOfRole(String role) throws DeviceManagementException {
if (log.isDebugEnabled()) {
log.debug("Get users of role '" + role);
}
int tenantId = this.getTenantId();
try {
return DeviceManagementDataHolder.getInstance().getRealmService().getTenantUserRealm(tenantId)
.getUserStoreManager().getUserListOfRole(role);
} catch (UserStoreException e) {
String msg = "Error occurred while obtaining the users, who are assigned with the role '" + role + "'";
log.error(msg, e);
throw new DeviceManagementException(msg, e);
} catch (Exception e) {
String msg = "Error occurred in getAllDevicesOfRole for role '" + role + "'";
log.error(msg, e);
throw new DeviceManagementException(msg, e);
}
}
@Override @Override
public int getDeviceCount(String username) throws DeviceManagementException { public int getDeviceCount(String username) throws DeviceManagementException {
if (username == null) { if (username == null) {

@ -194,6 +194,17 @@ public interface GroupManagementProviderService {
*/ */
List<Device> getAllDevicesOfGroup(String groupName, boolean requireDeviceProps) throws GroupManagementException; List<Device> getAllDevicesOfGroup(String groupName, boolean requireDeviceProps) throws GroupManagementException;
/**
* Get all devices that are in one of the given device status and belongs to given group.
*
* @param groupName Group name.
* @param deviceStatuses Device statuses list.
* @param requireDeviceProps to include device properties.
* @return List of devices in group.
* @throws GroupManagementException if error occurred while fetching devices
*/
List<Device> getAllDevicesOfGroup(String groupName, List<String> deviceStatuses, boolean requireDeviceProps)
throws GroupManagementException;
/** /**
* This method is used to retrieve the device count of a given group. * This method is used to retrieve the device count of a given group.

@ -38,6 +38,7 @@ import org.wso2.carbon.device.mgt.common.group.mgt.GroupNotExistException;
import org.wso2.carbon.device.mgt.common.group.mgt.RoleDoesNotExistException; import org.wso2.carbon.device.mgt.common.group.mgt.RoleDoesNotExistException;
import org.wso2.carbon.device.mgt.core.config.DeviceConfigurationManager; import org.wso2.carbon.device.mgt.core.config.DeviceConfigurationManager;
import org.wso2.carbon.device.mgt.core.dao.DeviceDAO; import org.wso2.carbon.device.mgt.core.dao.DeviceDAO;
import org.wso2.carbon.device.mgt.core.dao.DeviceManagementDAOException;
import org.wso2.carbon.device.mgt.core.dao.DeviceManagementDAOFactory; import org.wso2.carbon.device.mgt.core.dao.DeviceManagementDAOFactory;
import org.wso2.carbon.device.mgt.core.dao.GroupDAO; import org.wso2.carbon.device.mgt.core.dao.GroupDAO;
import org.wso2.carbon.device.mgt.core.dao.GroupManagementDAOException; import org.wso2.carbon.device.mgt.core.dao.GroupManagementDAOException;
@ -729,13 +730,7 @@ public class GroupManagementProviderServiceImpl implements GroupManagementProvid
GroupManagementDAOFactory.openConnection(); GroupManagementDAOFactory.openConnection();
devices = this.groupDAO.getAllDevicesOfGroup(groupName, tenantId); devices = this.groupDAO.getAllDevicesOfGroup(groupName, tenantId);
if (requireDeviceProps) { if (requireDeviceProps) {
DeviceManagementDAOFactory.openConnection(); return loadDeviceProperties(devices);
for (Device device : devices) {
Device retrievedDevice = deviceDAO.getDeviceProps(device.getDeviceIdentifier(), tenantId);
if (retrievedDevice != null && !retrievedDevice.getProperties().isEmpty()) {
device.setProperties(retrievedDevice.getProperties());
}
}
} }
} catch (GroupManagementDAOException | SQLException e) { } catch (GroupManagementDAOException | SQLException e) {
String msg = "Error occurred while getting devices in group."; String msg = "Error occurred while getting devices in group.";
@ -747,9 +742,66 @@ public class GroupManagementProviderServiceImpl implements GroupManagementProvid
throw new GroupManagementException(msg, e); throw new GroupManagementException(msg, e);
} finally { } finally {
GroupManagementDAOFactory.closeConnection(); GroupManagementDAOFactory.closeConnection();
}
return devices;
}
@Override
public List<Device> getAllDevicesOfGroup(String groupName, List<String> deviceStatuses, boolean requireDeviceProps)
throws GroupManagementException {
if (log.isDebugEnabled()) {
log.debug("Group devices of group: " + groupName);
}
int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId();
List<Device> devices;
try {
GroupManagementDAOFactory.openConnection();
devices = this.groupDAO.getAllDevicesOfGroup(groupName, deviceStatuses, tenantId);
if (requireDeviceProps) { if (requireDeviceProps) {
DeviceManagementDAOFactory.closeConnection(); return loadDeviceProperties(devices);
}
} catch (GroupManagementDAOException | SQLException e) {
String msg = "Error occurred while getting devices in group.";
log.error(msg, e);
throw new GroupManagementException(msg, e);
} catch (Exception e) {
String msg = "Error occurred in getDevices for group name: " + groupName;
log.error(msg, e);
throw new GroupManagementException(msg, e);
} finally {
GroupManagementDAOFactory.closeConnection();
}
return devices;
}
/**
* Load Dice properties of given list of devices
*
* @param devices list of devices
* @return list of devices which contains device properties
* @throws GroupManagementException if error occurred while loading device properties of devices which are in a
* particular device group
*/
private List<Device> loadDeviceProperties(List<Device> devices) throws GroupManagementException {
try {
int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId();
DeviceManagementDAOFactory.openConnection();
for (Device device : devices) {
Device retrievedDevice = deviceDAO.getDeviceProps(device.getDeviceIdentifier(), tenantId);
if (retrievedDevice != null && !retrievedDevice.getProperties().isEmpty()) {
device.setProperties(retrievedDevice.getProperties());
}
} }
} catch (SQLException e) {
String msg = "Error occurred while opening the connection for loading device properties of group devices.";
log.error(msg, e);
throw new GroupManagementException(msg, e);
} catch (DeviceManagementDAOException e) {
String msg = "Error occurred while loading device properties of group devices.";
log.error(msg, e);
throw new GroupManagementException(msg, e);
} finally {
DeviceManagementDAOFactory.closeConnection();
} }
return devices; return devices;
} }

Loading…
Cancel
Save