mirror of
https://repository.entgra.net/community/device-mgt-plugins.git
synced 2025-09-16 23:42:15 +00:00
Merge branch 'master' into 'master'
Improve android ent app syncing flow See merge request entgra/carbon-device-mgt-plugins!113
This commit is contained in:
commit
e1219a58ec
@ -70,6 +70,8 @@ public final class AndroidConstants {
|
||||
public static final String USER_CLAIM_FIRST_NAME_PLACEHOLDER = "$firstName";
|
||||
public static final String USER_CLAIM_LAST_NAME_PLACEHOLDER = "$lastName";
|
||||
|
||||
public static final String GOOGLE_PLAY_SYNCED_APP_CATEGORY = "GooglePlaySyncedApp";
|
||||
|
||||
public final class DeviceProperties {
|
||||
private DeviceProperties() {
|
||||
throw new AssertionError();
|
||||
@ -186,6 +188,8 @@ public final class AndroidConstants {
|
||||
public static final String VERSION = "version";
|
||||
public static final String ICON = "icon";
|
||||
public static final String IS_ACTIVE = "isActive";
|
||||
public static final String FREE_SUB_METHOD = "FREE";
|
||||
public static final String PAID_SUB_METHOD = "PAID";
|
||||
}
|
||||
|
||||
public final class ApplicationInstall {
|
||||
|
||||
@ -44,6 +44,7 @@ import org.wso2.carbon.device.application.mgt.common.response.Application;
|
||||
import org.wso2.carbon.device.application.mgt.common.response.Category;
|
||||
import org.wso2.carbon.device.application.mgt.common.services.ApplicationManager;
|
||||
import org.wso2.carbon.device.application.mgt.common.services.SubscriptionManager;
|
||||
import org.wso2.carbon.device.application.mgt.common.wrapper.ApplicationUpdateWrapper;
|
||||
import org.wso2.carbon.device.application.mgt.common.wrapper.PublicAppReleaseWrapper;
|
||||
import org.wso2.carbon.device.application.mgt.common.wrapper.PublicAppWrapper;
|
||||
import org.wso2.carbon.device.mgt.common.DeviceManagementConstants;
|
||||
@ -76,9 +77,11 @@ import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AndroidEnterpriseUtils {
|
||||
|
||||
@ -277,85 +280,70 @@ public class AndroidEnterpriseUtils {
|
||||
ApplicationManager applicationManager = getAppManagerServer();
|
||||
List<Category> categories = applicationManager.getRegisteredCategories();
|
||||
if (productListResponse != null && productListResponse.getProduct() != null
|
||||
&& productListResponse.getProduct().size() > 0) {
|
||||
&& !productListResponse.getProduct().isEmpty()) {
|
||||
|
||||
for (Product product : productListResponse.getProduct()) {
|
||||
List<String> packageNamesOfApps = productListResponse.getProduct().stream()
|
||||
.map(product -> (product.getProductId().replaceFirst("app:", ""))).collect(Collectors.toList());
|
||||
|
||||
List<Application> existingApps = applicationManager.getApplications(packageNamesOfApps);
|
||||
List<Product> products = productListResponse.getProduct();
|
||||
|
||||
for (Application app : existingApps){
|
||||
for (Product product : products){
|
||||
if (product.getProductId().replaceFirst("app:", "").equals(app.getPackageName())){
|
||||
ApplicationUpdateWrapper applicationUpdateWrapper = generatePubAppUpdateWrapper(product, categories);
|
||||
applicationManager.updateApplication(app.getId(), applicationUpdateWrapper);
|
||||
|
||||
PublicAppReleaseWrapper publicAppReleaseWrapper = new PublicAppReleaseWrapper();
|
||||
if (app.getSubMethod()
|
||||
.equalsIgnoreCase(AndroidConstants.ApplicationProperties.FREE_SUB_METHOD)) {
|
||||
publicAppReleaseWrapper.setPrice(0.0);
|
||||
} else {
|
||||
publicAppReleaseWrapper.setPrice(1.0);
|
||||
}
|
||||
|
||||
publicAppReleaseWrapper.setDescription(product.getRecentChanges());
|
||||
publicAppReleaseWrapper.setReleaseType("ga");
|
||||
publicAppReleaseWrapper.setVersion(getAppString(product.getAppVersion()));
|
||||
publicAppReleaseWrapper
|
||||
.setSupportedOsVersions(String.valueOf(product.getMinAndroidSdkVersion()) + "-ALL");
|
||||
|
||||
ApplicationArtifact applicationArtifact = generateArtifacts(product);
|
||||
applicationManager.updatePubAppRelease(app.getApplicationReleases().get(0).getUuid(),
|
||||
publicAppReleaseWrapper, applicationArtifact);
|
||||
products.remove(product);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Product product : products) {
|
||||
if (product.getAppVersion() == null) { // This is to handled removed apps from playstore
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate App wrapper
|
||||
PublicAppWrapper publicAppWrapper = new PublicAppWrapper();
|
||||
PublicAppWrapper publicAppWrapper = generatePubAppWrapper(product, categories);
|
||||
PublicAppReleaseWrapper appReleaseWrapper = new PublicAppReleaseWrapper();
|
||||
publicAppWrapper.setName(product.getTitle());
|
||||
publicAppWrapper.setDescription(product.getDescription());
|
||||
publicAppWrapper.setCategories(Arrays.asList(new String[]{"GooglePlaySyncedApp"}));//Default category
|
||||
for (Category category : categories) {
|
||||
if (product.getCategory() == null) {
|
||||
publicAppWrapper.setCategories(Arrays.asList(new String[]{"GooglePlaySyncedApp"}));
|
||||
break;
|
||||
} else if (product.getCategory().equalsIgnoreCase(category.getCategoryName())) {
|
||||
publicAppWrapper.setCategories(Arrays.asList(new String[]{category.getCategoryName(), "GooglePlaySyncedApp"}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (product.getProductPricing().equalsIgnoreCase("free")) {
|
||||
publicAppWrapper.setSubMethod("FREE");
|
||||
} else {
|
||||
publicAppWrapper.setSubMethod("PAID");
|
||||
}
|
||||
// TODO: purchase an app from Playstore and see how to capture the real value for price field.
|
||||
publicAppWrapper.setPaymentCurrency("$");
|
||||
appReleaseWrapper.setPrice(1.0);
|
||||
|
||||
publicAppWrapper.setDeviceType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID);
|
||||
if (publicAppWrapper.getSubMethod()
|
||||
.equalsIgnoreCase(AndroidConstants.ApplicationProperties.FREE_SUB_METHOD)) {
|
||||
appReleaseWrapper.setPrice(0.0);
|
||||
} else {
|
||||
appReleaseWrapper.setPrice(1.0);
|
||||
}
|
||||
|
||||
appReleaseWrapper.setDescription(product.getRecentChanges());
|
||||
appReleaseWrapper.setReleaseType("ga");
|
||||
appReleaseWrapper.setVersion(getAppString(product.getAppVersion()));
|
||||
appReleaseWrapper.setPackageName(product.getProductId().replaceFirst("app:", ""));
|
||||
appReleaseWrapper.setSupportedOsVersions(String.valueOf(product.getMinAndroidSdkVersion()) + "-ALL");
|
||||
|
||||
publicAppWrapper.setPublicAppReleaseWrappers(Arrays.asList(new PublicAppReleaseWrapper[]{appReleaseWrapper}));
|
||||
publicAppWrapper.setPublicAppReleaseWrappers(
|
||||
Arrays.asList(new PublicAppReleaseWrapper[] { appReleaseWrapper }));
|
||||
|
||||
// Generate artifacts
|
||||
ApplicationArtifact applicationArtifact = new ApplicationArtifact();
|
||||
|
||||
String iconName = product.getIconUrl().split(".com/")[1];
|
||||
applicationArtifact.setIconName(iconName);
|
||||
|
||||
|
||||
InputStream iconInputStream = getInputStream(iconName, product.getIconUrl());
|
||||
applicationArtifact.setIconStream(iconInputStream);
|
||||
Map<String, InputStream> screenshotMap = new HashMap<>();
|
||||
|
||||
int numberOfScreenShots = 3;// This is to handle some apps in playstore without 3 screenshots.
|
||||
if (product.getScreenshotUrls() != null) {
|
||||
if (product.getScreenshotUrls().size() < 3) {
|
||||
numberOfScreenShots = product.getScreenshotUrls().size();
|
||||
}
|
||||
|
||||
for (int y = 1; y < 4; y++) {
|
||||
int screenshotNumber = y - 1;
|
||||
if (y > numberOfScreenShots) {
|
||||
screenshotNumber = 0;
|
||||
}
|
||||
String screenshot = product.getScreenshotUrls().get(screenshotNumber);
|
||||
String screenshotName = screenshot.split(".com/")[1];
|
||||
InputStream screenshotInputStream = getInputStream(screenshotName, screenshot);
|
||||
screenshotMap.put(screenshotName, screenshotInputStream);
|
||||
}
|
||||
} else { // Private apps doesn't seem to send screenshots. Handling it.
|
||||
for (int a = 0; a < 3; a++) {
|
||||
String screenshot = product.getIconUrl();
|
||||
String screenshotName = screenshot.split(".com/")[1];
|
||||
InputStream screenshotInputStream = getInputStream(screenshotName, screenshot);
|
||||
screenshotMap.put(screenshotName, screenshotInputStream);
|
||||
}
|
||||
}
|
||||
|
||||
applicationArtifact.setScreenshots(screenshotMap);
|
||||
|
||||
ApplicationArtifact applicationArtifact = generateArtifacts(product);
|
||||
|
||||
Application application = applicationManager.createPublicApp(publicAppWrapper, applicationArtifact);
|
||||
if (application != null && (application.getApplicationReleases().get(0).getCurrentStatus() == null
|
||||
@ -373,6 +361,129 @@ public class AndroidEnterpriseUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To generate {@link ApplicationUpdateWrapper}
|
||||
*
|
||||
* @param product {@link Product}
|
||||
* @param categories List of categories registered with app manager
|
||||
* @return {@link ApplicationUpdateWrapper}
|
||||
*/
|
||||
private static ApplicationUpdateWrapper generatePubAppUpdateWrapper(Product product, List<Category> categories) {
|
||||
ApplicationUpdateWrapper applicationUpdateWrapper = new ApplicationUpdateWrapper();
|
||||
applicationUpdateWrapper.setName(product.getTitle());
|
||||
applicationUpdateWrapper.setDescription(product.getDescription());
|
||||
applicationUpdateWrapper.setCategories(
|
||||
Collections.singletonList(AndroidConstants.GOOGLE_PLAY_SYNCED_APP_CATEGORY));//Default category
|
||||
for (Category category : categories) {
|
||||
if (product.getCategory() == null) {
|
||||
List<String> pubAppCategories = new ArrayList<>();
|
||||
pubAppCategories.add(AndroidConstants.GOOGLE_PLAY_SYNCED_APP_CATEGORY);
|
||||
applicationUpdateWrapper.setCategories(pubAppCategories);
|
||||
break;
|
||||
} else if (product.getCategory().equalsIgnoreCase(category.getCategoryName())) {
|
||||
List<String> pubAppCategories = new ArrayList<>();
|
||||
pubAppCategories.add(category.getCategoryName());
|
||||
pubAppCategories.add(AndroidConstants.GOOGLE_PLAY_SYNCED_APP_CATEGORY);
|
||||
applicationUpdateWrapper.setCategories(pubAppCategories);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (product.getProductPricing().equalsIgnoreCase(AndroidConstants.ApplicationProperties.FREE_SUB_METHOD)) {
|
||||
applicationUpdateWrapper.setSubMethod(AndroidConstants.ApplicationProperties.FREE_SUB_METHOD);
|
||||
} else {
|
||||
applicationUpdateWrapper.setSubMethod(AndroidConstants.ApplicationProperties.PAID_SUB_METHOD);
|
||||
}
|
||||
// TODO: purchase an app from Playstore and see how to capture the real value for price field.
|
||||
applicationUpdateWrapper.setPaymentCurrency("$");
|
||||
return applicationUpdateWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* To generate {@link PublicAppWrapper}
|
||||
*
|
||||
* @param product {@link Product}
|
||||
* @param categories List of categories registered with app manager
|
||||
* @return {@link PublicAppWrapper}
|
||||
*/
|
||||
private static PublicAppWrapper generatePubAppWrapper(Product product, List<Category> categories) {
|
||||
PublicAppWrapper publicAppWrapper = new PublicAppWrapper();
|
||||
publicAppWrapper.setName(product.getTitle());
|
||||
publicAppWrapper.setDescription(product.getDescription());
|
||||
publicAppWrapper.setCategories(
|
||||
Collections.singletonList(AndroidConstants.GOOGLE_PLAY_SYNCED_APP_CATEGORY));//Default category
|
||||
for (Category category : categories) {
|
||||
if (product.getCategory() == null) {
|
||||
List<String> pubAppCategories = new ArrayList<>();
|
||||
pubAppCategories.add(AndroidConstants.GOOGLE_PLAY_SYNCED_APP_CATEGORY);
|
||||
publicAppWrapper.setCategories(pubAppCategories);
|
||||
break;
|
||||
} else if (product.getCategory().equalsIgnoreCase(category.getCategoryName())) {
|
||||
List<String> pubAppCategories = new ArrayList<>();
|
||||
pubAppCategories.add(category.getCategoryName());
|
||||
pubAppCategories.add(AndroidConstants.GOOGLE_PLAY_SYNCED_APP_CATEGORY);
|
||||
publicAppWrapper.setCategories(pubAppCategories);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (product.getProductPricing().equalsIgnoreCase(AndroidConstants.ApplicationProperties.FREE_SUB_METHOD)) {
|
||||
publicAppWrapper.setSubMethod(AndroidConstants.ApplicationProperties.FREE_SUB_METHOD);
|
||||
} else {
|
||||
publicAppWrapper.setSubMethod(AndroidConstants.ApplicationProperties.PAID_SUB_METHOD);
|
||||
}
|
||||
// TODO: purchase an app from Playstore and see how to capture the real value for price field.
|
||||
publicAppWrapper.setPaymentCurrency("$");
|
||||
publicAppWrapper.setDeviceType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID);
|
||||
return publicAppWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* To generate {@link ApplicationArtifact}
|
||||
*
|
||||
* @param product {@link Product}
|
||||
* @return {@link ApplicationArtifact}
|
||||
* @throws ApplicationManagementException if I/O exception occurred while generating application artifact.
|
||||
*/
|
||||
private static ApplicationArtifact generateArtifacts(Product product) throws ApplicationManagementException {
|
||||
ApplicationArtifact applicationArtifact = new ApplicationArtifact();
|
||||
try {
|
||||
String iconName = product.getIconUrl().split(".com/")[1];
|
||||
applicationArtifact.setIconName(iconName);
|
||||
InputStream iconInputStream = getInputStream(iconName, product.getIconUrl());
|
||||
applicationArtifact.setIconStream(iconInputStream);
|
||||
Map<String, InputStream> screenshotMap = new HashMap<>();
|
||||
|
||||
int numberOfScreenShots = 3;// This is to handle some apps in playstore without 3 screenshots.
|
||||
if (product.getScreenshotUrls() != null) {
|
||||
if (product.getScreenshotUrls().size() < 3) {
|
||||
numberOfScreenShots = product.getScreenshotUrls().size();
|
||||
}
|
||||
for (int y = 1; y < 4; y++) {
|
||||
int screenshotNumber = y - 1;
|
||||
if (y > numberOfScreenShots) {
|
||||
screenshotNumber = 0;
|
||||
}
|
||||
String screenshot = product.getScreenshotUrls().get(screenshotNumber);
|
||||
String screenshotName = screenshot.split(".com/")[1];
|
||||
InputStream screenshotInputStream = getInputStream(screenshotName, screenshot);
|
||||
screenshotMap.put(screenshotName, screenshotInputStream);
|
||||
}
|
||||
} else { // Private apps doesn't seem to send screenshots. Handling it.
|
||||
for (int a = 0; a < 3; a++) {
|
||||
String screenshot = product.getIconUrl();
|
||||
String screenshotName = screenshot.split(".com/")[1];
|
||||
InputStream screenshotInputStream = getInputStream(screenshotName, screenshot);
|
||||
screenshotMap.put(screenshotName, screenshotInputStream);
|
||||
}
|
||||
}
|
||||
applicationArtifact.setScreenshots(screenshotMap);
|
||||
return applicationArtifact;
|
||||
} catch (ApplicationManagementException e) {
|
||||
String msg = "Error occurred while generating Application artifact";
|
||||
log.error(msg);
|
||||
throw new ApplicationManagementException(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream getInputStream(String filename, String url) throws ApplicationManagementException {
|
||||
URL website;
|
||||
try {
|
||||
|
||||
@ -1,151 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
|
||||
*
|
||||
* WSO2 Inc. licenses this file to you 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.
|
||||
*
|
||||
* Copyright (c) 2019, Entgra (Pvt) Ltd. (http://www.entgra.io) All Rights Reserved.
|
||||
*
|
||||
* Entgra (Pvt) Ltd. licenses this file to you 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 org.wso2.carbon.device.mgt.mobile.android.impl;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.wso2.carbon.device.mgt.common.DeviceManager;
|
||||
import org.wso2.carbon.device.mgt.common.OperationMonitoringTaskConfig;
|
||||
import org.wso2.carbon.device.mgt.common.ProvisioningConfig;
|
||||
import org.wso2.carbon.device.mgt.common.InitialOperationConfig;
|
||||
import org.wso2.carbon.device.mgt.common.DeviceStatusTaskPluginConfig;
|
||||
import org.wso2.carbon.device.mgt.common.StartupOperationConfig;
|
||||
import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager;
|
||||
import org.wso2.carbon.device.mgt.common.configuration.mgt.ConfigurationEntry;
|
||||
import org.wso2.carbon.device.mgt.common.configuration.mgt.PlatformConfiguration;
|
||||
import org.wso2.carbon.device.mgt.common.exceptions.DeviceManagementException;
|
||||
import org.wso2.carbon.device.mgt.common.general.GeneralConfig;
|
||||
import org.wso2.carbon.device.mgt.common.policy.mgt.PolicyMonitoringManager;
|
||||
import org.wso2.carbon.device.mgt.common.pull.notification.PullNotificationSubscriber;
|
||||
import org.wso2.carbon.device.mgt.common.push.notification.PushNotificationConfig;
|
||||
import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService;
|
||||
import org.wso2.carbon.device.mgt.common.type.mgt.DeviceTypePlatformDetails;
|
||||
import org.wso2.carbon.device.mgt.mobile.android.impl.util.AndroidPluginConstants;
|
||||
import org.wso2.carbon.device.mgt.mobile.android.internal.AndroidDeviceManagementDataHolder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This represents the Android implementation of DeviceManagerService.
|
||||
*/
|
||||
public class AndroidDeviceManagementService implements DeviceManagementService {
|
||||
|
||||
private static final Log log = LogFactory.getLog(AndroidDeviceManagementService.class);
|
||||
private DeviceManager deviceManager;
|
||||
public static final String DEVICE_TYPE_ANDROID = "android";
|
||||
private static final String SUPER_TENANT_DOMAIN = "carbon.super";
|
||||
private static final String NOTIFIER_PROPERTY = "notifierType";
|
||||
private static final String FCM_API_KEY = "fcmAPIKey";
|
||||
private static final String FCM_SENDER_ID = "fcmSenderId";
|
||||
private PolicyMonitoringManager policyMonitoringManager;
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return AndroidDeviceManagementService.DEVICE_TYPE_ANDROID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OperationMonitoringTaskConfig getOperationMonitoringConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() throws DeviceManagementException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceManager getDeviceManager() {
|
||||
return deviceManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApplicationManager getApplicationManager() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProvisioningConfig getProvisioningConfig() {
|
||||
return new ProvisioningConfig(SUPER_TENANT_DOMAIN, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PushNotificationConfig getPushNotificationConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PolicyMonitoringManager getPolicyMonitoringManager() {
|
||||
return policyMonitoringManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InitialOperationConfig getInitialOperationConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StartupOperationConfig getStartupOperationConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PullNotificationSubscriber getPullNotificationSubscriber() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceStatusTaskPluginConfig getDeviceStatusTaskPluginConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeneralConfig getGeneralConfig() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceTypePlatformDetails getDeviceTypePlatformDetails() { return null; }
|
||||
|
||||
private String getConfigProperty(List<ConfigurationEntry> configs, String propertyName) {
|
||||
for (ConfigurationEntry entry : configs) {
|
||||
if (propertyName.equals(entry.getName())) {
|
||||
return entry.getValue().toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user