Skip to content

Commit

Permalink
Add ShadowMagnificationController for N+
Browse files Browse the repository at this point in the history
A single instance of MagnificationController exists per AccessibilityService.
It abstracts over sending commands to the framework over Binder. These commands
allow the client to magnify areas of the screen, and to query if the screen is
currently being magnified.

Before this shadow existed, using MagnificationController from Robolectric
would have no visible effects. i.e., if you were to call .setScale(n, animate)
to modify the magnification scale, then calling .getScale() would not return n:
it would instead return the default scale (1f). This shadow keeps internal
magnification state, so that the APIs behave as expected.

Currently an assumption baked into this code is that only one
AccessibilityService is active at once. This should be a safe assumption in
general, as there are various disadvantages to having multiple services in an
apk (from both the performance perspective, but also from user convenience, as
each service has to be activated separately, from a deeply buried Settings page).

PiperOrigin-RevId: 350651958
  • Loading branch information
Googler authored and copybara-robolectric committed Jan 19, 2021
1 parent ff49c49 commit 10fa97d
Show file tree
Hide file tree
Showing 2 changed files with 301 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.N;
import static com.google.common.truth.Truth.assertThat;
import static org.robolectric.Shadows.shadowOf;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityService.MagnificationController;
import android.graphics.Region;
import android.os.Looper;
import android.view.accessibility.AccessibilityEvent;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;

/** Test for ShadowMagnificationController. */
@RunWith(AndroidJUnit4.class)
@Config(minSdk = N)
public final class ShadowMagnificationControllerTest {

private MyService myService;
private MagnificationController magnificationController;

@Before
public void setUp() {
myService = Robolectric.setupService(MyService.class);
magnificationController = myService.getMagnificationController();
}

@Test
public void getCenterX_byDefault_returns0() {
assertThat(magnificationController.getCenterX()).isEqualTo(0.0f);
}

@Test
public void getCenterY_byDefault_returns0() {
assertThat(magnificationController.getCenterY()).isEqualTo(0.0f);
}

@Test
public void getScale_byDefault_returns1() {
assertThat(magnificationController.getScale()).isEqualTo(1.0f);
}

@Test
public void setCenter_setsCenterX() {
float newCenterX = 450.0f;

magnificationController.setCenter(newCenterX, /*centerY=*/ 0.0f, /*animate=*/ false);

assertThat(magnificationController.getCenterX()).isEqualTo(newCenterX);
}

@Test
public void setCenter_setsCenterY() {
float newCenterY = 250.0f;

magnificationController.setCenter(/*centerX=*/ 0.0f, newCenterY, /*animate=*/ false);

assertThat(magnificationController.getCenterY()).isEqualTo(newCenterY);
}

@Test
public void setCenter_notifiesListener() {
float centerX = 55f;
float centerY = 22.5f;
TestListener testListener = new TestListener();
magnificationController.addListener(testListener);

magnificationController.setCenter(centerX, centerY, /*animate=*/ false);

shadowOf(Looper.getMainLooper()).idle();
assertThat(testListener.invoked).isTrue();
assertThat(testListener.centerX).isEqualTo(centerX);
assertThat(testListener.centerY).isEqualTo(centerY);
}

@Test
public void setScale_setsScale() {
float newScale = 5.0f;

magnificationController.setScale(newScale, /*animate=*/ false);

assertThat(magnificationController.getScale()).isEqualTo(newScale);
}

@Test
public void setScale_notifiesListener() {
float scale = 5.0f;
TestListener testListener = new TestListener();
magnificationController.addListener(testListener);

magnificationController.setScale(scale, /*animate=*/ false);

shadowOf(Looper.getMainLooper()).idle();
assertThat(testListener.invoked).isTrue();
assertThat(testListener.scale).isEqualTo(scale);
}

@Test
public void reset_resetsCenterX() {
magnificationController.setCenter(/*centerX=*/ 100.0f, /*centerY=*/ 0.0f, /*animate=*/ false);

magnificationController.reset(/*animate=*/ false);

assertThat(magnificationController.getCenterX()).isEqualTo(0.0f);
}

@Test
public void reset_resetsCenterY() {
magnificationController.setCenter(/*centerX=*/ 0.0f, /*centerY=*/ 100.0f, /*animate=*/ false);

magnificationController.reset(/*animate=*/ false);

assertThat(magnificationController.getCenterY()).isEqualTo(0.0f);
}

@Test
public void reset_resetsScale() {
magnificationController.setScale(5.0f, /*animate=*/ false);

magnificationController.reset(/*animate=*/ false);

assertThat(magnificationController.getScale()).isEqualTo(1.0f);
}

@Test
public void reset_notifiesListener() {
magnificationController.setCenter(/*centerX=*/ 150.5f, /*centerY=*/ 11.5f, /*animate=*/ false);
magnificationController.setScale(/*scale=*/ 5.0f, /*animate=*/ false);
TestListener testListener = new TestListener();
magnificationController.addListener(testListener);

magnificationController.reset(/*animate=*/ false);

shadowOf(Looper.getMainLooper()).idle();
assertThat(testListener.invoked).isTrue();
assertThat(testListener.centerX).isEqualTo(0.0f);
assertThat(testListener.centerY).isEqualTo(0.0f);
assertThat(testListener.scale).isEqualTo(1.0f);
}

@Test
public void removeListener_removesListener() {
float scale = 5.0f;
TestListener testListener = new TestListener();
magnificationController.addListener(testListener);

magnificationController.removeListener(testListener);

magnificationController.setScale(scale, /*animate=*/ false);
shadowOf(Looper.getMainLooper()).idle();
assertThat(testListener.invoked).isFalse();
}

/** Test OnMagnificationChangedListener that records when it's invoked. */
private static class TestListener
implements MagnificationController.OnMagnificationChangedListener {

private boolean invoked = false;
private float scale = -1f;
private float centerX = -1f;
private float centerY = -1f;

@Override
public void onMagnificationChanged(
MagnificationController controller,
Region region,
float scale,
float centerX,
float centerY) {
this.invoked = true;
this.scale = scale;
this.centerX = centerX;
this.centerY = centerY;
}
}

/** Empty implementation of AccessibilityService, for test purposes. */
private static class MyService extends AccessibilityService {

@Override
public void onAccessibilityEvent(AccessibilityEvent arg0) {
// Do nothing
}

@Override
public void onInterrupt() {
// Do nothing
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.N;

import android.accessibilityservice.AccessibilityService.MagnificationController;
import android.graphics.Region;
import android.os.Handler;
import android.os.Looper;
import java.util.HashMap;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;

/** Shadow of MagnificationController. */
@Implements(value = MagnificationController.class, minSdk = N)
public class ShadowMagnificationController {

private static final float DEFAULT_CENTER_X = 0.0f;
private static final float DEFAULT_CENTER_Y = 0.0f;
private static final float DEFAULT_SCALE = 1.0f;

@RealObject private MagnificationController realObject;

private final HashMap<MagnificationController.OnMagnificationChangedListener, Handler> listeners =
new HashMap<>();

private final Region magnificationRegion = new Region();
private float centerX = DEFAULT_CENTER_X;
private float centerY = DEFAULT_CENTER_Y;
private float scale = DEFAULT_SCALE;

@Implementation
protected void addListener(
MagnificationController.OnMagnificationChangedListener listener, Handler handler) {
listeners.put(listener, handler);
}

@Implementation
protected void addListener(MagnificationController.OnMagnificationChangedListener listener) {
addListener(listener, new Handler(Looper.getMainLooper()));
}

@Implementation
protected float getCenterX() {
return centerX;
}

@Implementation
protected float getCenterY() {
return centerY;
}

@Implementation
protected Region getMagnificationRegion() {
return magnificationRegion;
}

@Implementation
protected float getScale() {
return scale;
}

@Implementation
protected boolean removeListener(
MagnificationController.OnMagnificationChangedListener listener) {
if (!listeners.containsKey(listener)) {
return false;
}
listeners.remove(listener);
return true;
}

@Implementation
protected boolean reset(boolean animate) {
centerX = DEFAULT_CENTER_X;
centerY = DEFAULT_CENTER_Y;
scale = DEFAULT_SCALE;
notifyListeners();
return true;
}

@Implementation
protected boolean setCenter(float centerX, float centerY, boolean animate) {
this.centerX = centerX;
this.centerY = centerY;
notifyListeners();
return true;
}

@Implementation
protected boolean setScale(float scale, boolean animate) {
this.scale = scale;
notifyListeners();
return true;
}

private void notifyListeners() {
for (MagnificationController.OnMagnificationChangedListener listener : listeners.keySet()) {
Handler handler = listeners.get(listener);
handler.post(
() ->
listener.onMagnificationChanged(
realObject, magnificationRegion, scale, centerX, centerY));
}
}
}

0 comments on commit 10fa97d

Please sign in to comment.