Skip to content

Commit

Permalink
Migrate GpsLocationEntity
Browse files Browse the repository at this point in the history
Migrate GpsLocationEntity from atmosphere-client
  • Loading branch information
vanogrid committed Oct 17, 2016
1 parent 80e9e91 commit 07f91f9
Show file tree
Hide file tree
Showing 6 changed files with 424 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
import com.musala.atmosphere.agent.devicewrapper.util.ondevicecomponent.ServiceCommunicator;
import com.musala.atmosphere.agent.devicewrapper.util.ondevicecomponent.UIAutomatorCommunicator;
import com.musala.atmosphere.agent.entity.DeviceSettingsEntity;
import com.musala.atmosphere.agent.entity.EntityTypeResolver;
import com.musala.atmosphere.agent.entity.GestureEntity;
import com.musala.atmosphere.agent.entity.GpsLocationEntity;
import com.musala.atmosphere.agent.entity.HardwareButtonEntity;
import com.musala.atmosphere.agent.entity.ImageEntity;
import com.musala.atmosphere.agent.entity.ImeEntity;
Expand Down Expand Up @@ -176,6 +178,8 @@ public abstract class AbstractWrapDevice extends UnicastRemoteObject implements

private ImageEntity imageEntity;

private GpsLocationEntity gpsLocationEntity;

/**
* Creates an abstract wrapper of the given {@link IDevice device}.
*
Expand Down Expand Up @@ -451,10 +455,16 @@ public Object route(RoutingAction action, Object... args) throws RemoteException
returnValue = serviceCommunicator.isLocked();
break;
case OPEN_LOCATION_SETTINGS:
serviceCommunicator.openLocationSettings();
gpsLocationEntity.openLocationSettings();
break;
case IS_GPS_LOCATION_ENABLED:
returnValue = serviceCommunicator.isGpsLocationEnabled();
returnValue = gpsLocationEntity.isGpsLocationEnabled();
break;
case ENABLE_GPS_LOCATION:
returnValue = gpsLocationEntity.enableGpsLocation();
break;
case DISABLE_GPS_LOCATION:
returnValue = gpsLocationEntity.disableGpsLocation();
break;
case SHOW_TAP_LOCATION:
serviceCommunicator.showTapLocation(args);
Expand Down Expand Up @@ -765,6 +775,8 @@ public DeviceInformation getDeviceInformation() {
}

private void setupDeviceEntities(DeviceInformation deviceInformation) {
EntityTypeResolver typeResolver = new EntityTypeResolver(deviceInformation);

try {
Constructor<?> hardwareButtonEntityConstructor = HardwareButtonEntity.class.getDeclaredConstructor(ShellCommandExecutor.class);
hardwareButtonEntityConstructor.setAccessible(true);
Expand Down Expand Up @@ -805,6 +817,20 @@ private void setupDeviceEntities(DeviceInformation deviceInformation) {
wrappedDevice
});
this.imageEntity = imageEntity;

Class<?> locationEntityClass = typeResolver.getEntityClass(GpsLocationEntity.class);
Constructor<?> locationEntityConstructor = locationEntityClass.getDeclaredConstructor(ServiceCommunicator.class,
UIAutomatorCommunicator.class,
HardwareButtonEntity.class,
GestureEntity.class);
locationEntityConstructor.setAccessible(true);
GpsLocationEntity locationEntity = ((GpsLocationEntity) locationEntityConstructor.newInstance(new Object[] {
serviceCommunicator,
automatorCommunicator,
hardwareButtonEntity,
gestureEntity
}));
this.gpsLocationEntity = locationEntity;
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException e) {
throw new UnresolvedEntityTypeException("Failed to find the correct set of entities implementations matching the given device information.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.musala.atmosphere.agent.entity;

import java.lang.annotation.Annotation;
import java.util.Set;

import org.reflections.Reflections;

import com.musala.atmosphere.agent.entity.annotations.Restriction;
import com.musala.atmosphere.commons.DeviceInformation;

/**
* Class responsible for resolving the correct implementation of the entities, defined for all device specific
* operations, depending on the provided {@link DeviceInformation}.
*
* @author filareta.yordanova
*
*/
public class EntityTypeResolver {
private static final String ENTITIES_PACKAGE = "com.musala.atmosphere.agent.entity";

private DeviceInformation deviceInformation;

private Reflections reflections;

public EntityTypeResolver(DeviceInformation information) {
this.deviceInformation = information;
reflections = new Reflections(ENTITIES_PACKAGE);
}

/**
* Finds entity implementation for a device specific operation depending on the {@link DeviceInformation device
* information} and the hierarchy type given.
*
* @param baseEntityClass
* - base class of the entity hierarchy for a device specific operation
* @return {@link Class} of the entity that matches the required {@link DeviceInformation device information} and is
* from type baseEntityClass
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public Class<?> getEntityClass(Class baseEntityClass) {
Set<Class<?>> subClasses = reflections.getSubTypesOf(baseEntityClass);
Class<?> defaultImplementation = null;

for (Class<?> subClass : subClasses) {
Annotation annotation = subClass.getAnnotation(Restriction.class);
if (annotation == null) {
defaultImplementation = subClass;
} else if (isApplicable((Restriction) annotation)) {
return subClass;
}
}

return defaultImplementation;

}

/**
* Checks if a certain implementation annotated with {@link @Restriction} is applicable for a device with the
* provided {@link DeviceInformation information}.
*
* @param restriction
* - restrictions provided for a certain entity implementation
* @return <code>true</code> if the given restrictions are compatible with the {@link DeviceInformation information}
* for the current device, <code>false</code> otherwise
*/
// TODO: Check for default values in the annotation methods, if the parameter has default value and is not present
// in the annotation it is not considered when checking for applicability.
private boolean isApplicable(Restriction restriction) {
String manufacturer = restriction.manufacturer();

if (!manufacturer.equals(DeviceInformation.FALLBACK_MANUFACTURER_NAME)
&& !manufacturer.equalsIgnoreCase(deviceInformation.getManufacturer())) {
return false;
}

boolean isApplicable = true;
int[] apiLevels = restriction.apiLevel();

if (apiLevels.length > 0) {
int deviceApiLevel = deviceInformation.getApiLevel();
isApplicable = false;

for (int applicableApiLevel : apiLevels) {
if (applicableApiLevel == deviceApiLevel) {
isApplicable = true;
break;
}
}
}

return isApplicable;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.musala.atmosphere.agent.entity;

import java.util.List;

import com.musala.atmosphere.agent.devicewrapper.util.ondevicecomponent.ServiceCommunicator;
import com.musala.atmosphere.agent.devicewrapper.util.ondevicecomponent.UIAutomatorCommunicator;
import com.musala.atmosphere.agent.entity.annotations.Restriction;
import com.musala.atmosphere.commons.exceptions.CommandFailedException;
import com.musala.atmosphere.commons.exceptions.UiElementFetchingException;
import com.musala.atmosphere.commons.ui.UiElementPropertiesContainer;
import com.musala.atmosphere.commons.ui.selector.CssAttribute;
import com.musala.atmosphere.commons.ui.selector.UiElementSelector;
import com.musala.atmosphere.commons.ui.tree.AccessibilityElement;

/**
* {@link GpsLocationEntity} responsible for setting the GPS location state on all Samsung devices.
*
* @author yavor.stankov
*
*/
@Restriction(apiLevel = {17, 18})
public class GpsLocationCheckBoxEntity extends GpsLocationEntity {
private static final String ANDROID_WIDGET_CHECK_BOX_CLASS_NAME = "android.widget.CheckBox";

GpsLocationCheckBoxEntity(ServiceCommunicator serviceCommunicator,
UIAutomatorCommunicator automatorCommunicator,
HardwareButtonEntity hardwareButtonEntity,
GestureEntity gestureEntity) {
super(serviceCommunicator, automatorCommunicator, hardwareButtonEntity, gestureEntity);
}

@Override
protected UiElementPropertiesContainer getChangeStateWidget() throws UiElementFetchingException, CommandFailedException {
UiElementSelector checkBoxWidgetSelector = new UiElementSelector();
checkBoxWidgetSelector.addSelectionAttribute(CssAttribute.CLASS_NAME, ANDROID_WIDGET_CHECK_BOX_CLASS_NAME);

automatorCommunicator.waitForExists(checkBoxWidgetSelector, CHANGE_STATE_WIDGET_TIMEOUT);

List<AccessibilityElement> widgetList = automatorCommunicator.getUiElements(checkBoxWidgetSelector, true);

if (!widgetList.isEmpty()) {
// There are more than one check box on the screen, but only the first one is for setting the GPS location
// state.
return widgetList.get(0);
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package com.musala.atmosphere.agent.entity;

import java.util.List;

import org.apache.log4j.Logger;

import com.musala.atmosphere.agent.devicewrapper.util.ondevicecomponent.ServiceCommunicator;
import com.musala.atmosphere.agent.devicewrapper.util.ondevicecomponent.UIAutomatorCommunicator;
import com.musala.atmosphere.commons.exceptions.CommandFailedException;
import com.musala.atmosphere.commons.exceptions.UiElementFetchingException;
import com.musala.atmosphere.commons.geometry.Bounds;
import com.musala.atmosphere.commons.geometry.Point;
import com.musala.atmosphere.commons.ui.UiElementPropertiesContainer;
import com.musala.atmosphere.commons.ui.selector.CssAttribute;
import com.musala.atmosphere.commons.ui.selector.UiElementSelector;
import com.musala.atmosphere.commons.ui.tree.AccessibilityElement;

/**
* Base entity responsible for handling GPS location state changing.
*
* @author yavor.stankov
*
*/
public abstract class GpsLocationEntity {
private static final Logger LOGGER = Logger.getLogger(GpsLocationEntity.class.getCanonicalName());

private static final String AGREE_BUTTON_RESOURCE_ID = "android:id/button1";

private static final int AGREE_BUTTON_TIMEOUT = 3000;

protected static final int CHANGE_STATE_WIDGET_TIMEOUT = 5000;

private static final int UI_ELEMENT_OPERATION_WAIT_TIME = 500;

protected ServiceCommunicator serviceCommunicator;

protected UIAutomatorCommunicator automatorCommunicator;

protected HardwareButtonEntity hardwareButtonEntity;

protected GestureEntity gestureEntity;

GpsLocationEntity(ServiceCommunicator serviceCommunicator,
UIAutomatorCommunicator automatorCommunicator,
HardwareButtonEntity hardwareButtonEntity,
GestureEntity gestureEntity) {
this.serviceCommunicator = serviceCommunicator;
this.automatorCommunicator = automatorCommunicator;
this.hardwareButtonEntity = hardwareButtonEntity;
this.gestureEntity = gestureEntity;
}

/**
* Gets the right widget that is responsible for setting the GPS location state.
*
* @return the widget that should be used for setting the GPS location state.
* @throws UiElementFetchingException
* if the required widget is not present on the screen
* @throws MultipleElementsFoundException
* if there are more than one widgets present on the screen
*/
protected abstract UiElementPropertiesContainer getChangeStateWidget()
throws UiElementFetchingException, CommandFailedException;

/**
* Enables the GPS location on this device.
*
* @return <code>true</code> if the GPS location enabling is successful, <code>false</code> if it fails
* @throws CommandFailedException
*/
public boolean enableGpsLocation() throws CommandFailedException {
return setGpsLocationState(true);
}

/**
* Disables the GPS location on this device.
*
* @return <code>true</code> if the GPS location disabling is successful, <code>false</code> if it fails
* @throws CommandFailedException
*/
public boolean disableGpsLocation() throws CommandFailedException {
return setGpsLocationState(false);
}

/**
* Check if the GPS location is enabled on this device.
*
* @return <code>true</code> if the GPS location is enabled, <code>false</code> if it's disabled
* @throws CommandFailedException
*/
public boolean isGpsLocationEnabled() throws CommandFailedException {
return serviceCommunicator.isGpsLocationEnabled();
}

private boolean setGpsLocationState(boolean state) throws CommandFailedException {
if (isGpsLocationEnabled() == state) {
return true;
}

openLocationSettings();

try {
UiElementPropertiesContainer changeStateWidget = getChangeStateWidget();
if (tap(changeStateWidget)) {
pressAgreeButton();
}
} catch (UiElementFetchingException e) {
LOGGER.error("Failed to get the wanted widget, or there are more than one widgets on the screen that are matching the given selector.",
e);
return false;
}

// TODO: If needed, move the HardwareButton enumeration from com.musala.atmosphere.client.device to atmosphere-commons,
// so the enumeration could be used here instead of an integer.
hardwareButtonEntity.pressButton(4); // Back button

return true;
}

public void openLocationSettings() throws CommandFailedException {
serviceCommunicator.openLocationSettings();
}

private void pressAgreeButton() throws UiElementFetchingException, CommandFailedException {
UiElementSelector agreeButtonSelector = new UiElementSelector();
agreeButtonSelector.addSelectionAttribute(CssAttribute.RESOURCE_ID, AGREE_BUTTON_RESOURCE_ID);

if (automatorCommunicator.waitForExists(agreeButtonSelector, AGREE_BUTTON_TIMEOUT)) {
List<AccessibilityElement> elementsList = automatorCommunicator.getUiElements(agreeButtonSelector, true);

AccessibilityElement agreeButton = elementsList.get(0);
tap(agreeButton);
}
}

private boolean tap(UiElementPropertiesContainer element) {
Bounds elementBounds = element.getBounds();
Point centerPoint = elementBounds.getCenter();
Point point = elementBounds.getRelativePoint(centerPoint);
Point tapPoint = elementBounds.getUpperLeftCorner();
tapPoint.addVector(point);

boolean tapSuccessful = false;
if (elementBounds.contains(tapPoint)) {
tapSuccessful = gestureEntity.tapScreenLocation(tapPoint);
finalizeUiElementOperation();
} else {
String message = String.format("Point %s not in element bounds.", point.toString());
LOGGER.error(message);
throw new IllegalArgumentException(message);
}

return tapSuccessful;
}

private void finalizeUiElementOperation() {
// Should be invoked exactly once in the end of all element-operating
// methods, whether its directly or indirectly invoked.
try {
Thread.sleep(UI_ELEMENT_OPERATION_WAIT_TIME);
} catch (InterruptedException e) {
LOGGER.info(e);
}
}
}
Loading

0 comments on commit 07f91f9

Please sign in to comment.