Skip to end of metadata
Go to start of metadata

This documentation is no longer actively supported and may be out of date. Going forward, please visit and bookmark our new site (https://docs.phunware.com/) for up-to-date documentation.

Creating an ADA-Compliant app with Mapping SDK 3.0

 

The Mapping SDK 3.0 is written to allow developers total control over their UI so they can create the best possible experience for their users. Most of the work necessary to make an app accessible for visually impaired users is not specific to this SDK, and information regarding best practices can be found here: Developing Accessible apps

 

We have created an ADA Sample App demonstrating how the Mapping SDK can be used in a way that is useful to visually impaired individuals. Some of the recommended implementation details used in the sample app are:

  • Saving Floor and POI data to a SQL database after the first call to addBuilding, allowing for faster data loading on subsequent app launches and making information available in areas of poor internet connectivity.
  • Setting a LocationProviderFactory on the PhunwareMapManager before calling addBuilding, so turn-by-turn navigation functionality is available.
  • Implementing Navigator.OnManeuverChangedListener in a navigation list view to update and read out maneuvers as the users moves along a route

 

How would a visually-impaired user navigate the app?

  • After moving past the onboarding screen, the user is initially brought to the directory list of all POIs in the building (On first app load this list is initially blank, then populates as soon as data is downloaded from the network).

  • Users can switch to the “Around Me” tab to explore what is near their current location (once acquired) or navigate through the Directory list to hear details about the building’s POIs. If they are looking for something specific they can use the search and filter functions on the directory to more easily find the intended POI.

  • Activating a POI from either the Around Me or Directory list will open a new view with details about that point. If we are able to determine the user’s current location, a “Get Directions” button will be activated on this screen.

  • Activating the “Get Directions” button will bring up a route preview screen. Navigating from the top down, Talkback will read out a description of the route (start and end points, distance, floor changes), let the user know about the two preview tabs, and notify them of the “start navigation” button. If they choose to continue scrolling through the page they can hear all the route steps listed out.

  • Activating the “Start navigation” button opens the navigation view, with the listview tab active. The list of route steps is initially covered by a blocker view which tells the user which direction they should face before they start moving. Once they are oriented correctly, route steps will be read off as they approach each one. Long steps (over 20 ft) will have reminders read out at 20 foot intervals so the user knows when to turn and what direction.  Straight maneuvers will also have warnings at 50 foot intervals telling the user how much longer to continue straight.  When the user reaches their destination they will hear “You have arrived” and navigation will stop. If the SDK detects that the user has gone off route they will be automatically rerouted to their destination.

UI Elements developers may wish to reuse

Automatic Re-routing

  • Navigator.OnManeuverChangedListener has a callback method onRouteSnapFailed() that will be called on all registered listeners any time a user’s location is too far away from the route to implement route snapping behavior

  • In our ADA sample app we have implemented a threshold count to avoid unnecessary rerouting. Once we have had 5 snap failures in a row, we cancel navigation, recalculate a route to the destination from the user’s new current location, and restart navigating with the new route.

    Re-Routing
    public class ListNavigationFragment extends TabFragment
            implements Navigator.OnManeuverChangedListener {
    	/*
    	...
    	*/
     
    	@Override
    	public void onManeuverChanged(Navigator navigator, int i) {
    	routeFailures = 0;
    	    /* update UI for current Maneuver
    		/*
    	}
    
    	@Override
    	public void onRouteSnapFailed() {
        	routeFailures++;
    	    if (routeFailures == 5) {
    	        navigationListener.reroute(route.getEndPointId());
        	}
    	}
    }
     
     
    public class MainActivity extends AppCompatActivity
    	implements MyMapFragment.onNavigationListener {
    	/* 
    	...
    	/*
     
    	@Override
    	public void reroute(long destId) {
    	    Toast.makeText(this, R.string.recalculating, Toast.LENGTH_SHORT).show();
    	    onNavigationExitSelected();
    	    LatLng currentLocation = new LatLng(mapManager.getCurrentLocation().getLatitude(),
    	            mapManager.getCurrentLocation().getLongitude());
    	    RouteOptions route = mapManager.findRoutes(currentLocation, destId,
    	            mapManager.getCurrentBuilding().getSelectedFloor().getId(), true).shortestRoute();
    	    if (route != null) {
    	        sharedNavigator = mapManager.navigate(route);
    	        pagerAdapter.setCurrentLocation(mapManager.getCurrentLocation());
    	        startNavigation();
    	    } else {
    	        Toast.makeText(this, R.string.no_route_available, Toast.LENGTH_SHORT).show();
    	    }
    	}
    	/* 
    	...
    	/*
    }

     

Navigation Overlay View

 

  • The NavigationOverlayView is a custom ViewPager implementation that displays route step details on top of the map in navigation mode

  • The view is added to the same view hierarchy as the PhunwareMap and visibility is toggled based on whether the user is in navigation mode.

NavigationOverlayView
public class NavigationOverlayView extends ViewPager
        implements Navigator.OnManeuverChangedListener {

    private Navigator navigator;
    private ManeuverPagerAdapter adapter;

    private final OnPageChangeListener pageChangeListener = new SimpleOnPageChangeListener() {
        @Override
        public void onPageSelected(int position) {
            if (navigator != null) {
                ManeuverPair pair = adapter.getItem(position);
								// when the user manually advances the maneuver, make sure the navigator is updated as well
                navigator.setCurrentManeuver(pair.mainPos);
            }
        }
    };

    public NavigationOverlayView(Context context) {
        this(context, null);
    }

    public NavigationOverlayView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setNavigator(BuildingOptions building, Navigator navigator) {
        this.navigator = navigator;
        navigator.addOnManeuverChangedListener(this);


        List<ManeuverPair> pairs = new ArrayList<>();
        List<RouteManeuverOptions> maneuvers = this.navigator.getManeuvers();
        for (int i = 0; i < maneuvers.size(); i += 2) {
            ManeuverPair pair = new ManeuverPair();
            pair.mainPos = i;
            pair.mainManeuver = maneuvers.get(i);

            if (i + 1 < maneuvers.size()) {
                RouteManeuverOptions next = maneuvers.get(i + 1);
                if (next.isTurnManeuver() || next.isPortalManeuver()) {
                    pair.turnPos = i + 1;
                    pair.turnManeuver = next;
                } else {
                    i--;
                }
            }
            pairs.add(pair);
        }

        setAdapter(adapter = new ManeuverPagerAdapter(navigator));
        adapter.setManeuvers(building, pairs);
        addOnPageChangeListener(pageChangeListener);
        setCurrentItem(0);
        navigator.setCurrentManeuver(0);
    }

    @Override
    public void onManeuverChanged(Navigator navigator, int position) {
		// the user's position has triggered the Navigator to automatically update the current maneuver. Update the UI accordingly
        for (int i = 0; i < adapter.getCount(); i++) {
            ManeuverPair pair = adapter.getItem(i);
            if (pair.mainPos == position || pair.turnPos == position) {
                setCurrentItem(i);
                return;
            }
        }
    }

    @Override
    public void onRouteSnapFailed() {
        Log.e("OverlayView", "RouteSnapFailed");
    }

		// bundle maneuvers into pairs of a straight maneuver and a turn/floor change maneuver
    private static class ManeuverPair {
        int mainPos;
        int turnPos;
        RouteManeuverOptions mainManeuver;
        RouteManeuverOptions turnManeuver;
    }

    private static final class ManeuverPagerAdapter extends PagerAdapter {


        private final Navigator navigator;
        private final ManeuverDisplayHelper displayHelper;
        private final List<ManeuverPair> maneuvers = new ArrayList<>();

        public ManeuverPagerAdapter(Navigator navigator) {
            super();
            this.navigator = navigator;
            displayHelper = new ManeuverDisplayHelper();
        }

        @Override
        public int getCount() {
            return maneuvers.size();
        }

        public void setManeuvers(BuildingOptions building,
                Collection<? extends ManeuverPair> maneuvers) {
            displayHelper.setBuilding(building);
            this.maneuvers.clear();
            this.maneuvers.addAll(maneuvers);
            notifyDataSetChanged();
        }

        public ManeuverPair getItem(int position) {
            return maneuvers.get(position);
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            View v = LayoutInflater.from(container.getContext())
                    .inflate(R.layout.item_maneuver, container, false);

            ImageView direction = (ImageView) v.findViewById(R.id.direction);
            TextView maneuver = (TextView) v.findViewById(R.id.maneuver);
            TextView nextManeuver = (TextView) v.findViewById(R.id.next_maneuver);
            ImageView nextDirection = (ImageView) v.findViewById(R.id.next_direction);

            final ManeuverPair m = maneuvers.get(position);
						// update the icon in the current maneuver page to represent the direction of the maneuver
            direction.setImageResource(displayHelper.getImageResourceForDirection(m.mainManeuver));
            maneuver.setText(displayHelper.stringForDirection(m.mainManeuver));

            if (m.turnManeuver == null) {
                nextManeuver.setVisibility(View.GONE);
                nextDirection.setVisibility(GONE);
                v.findViewById(R.id.next).setVisibility(GONE);
            } else {
                v.findViewById(R.id.next).setVisibility(VISIBLE);
                nextManeuver.setVisibility(View.VISIBLE);
                nextDirection.setVisibility(VISIBLE);
							  // update the icon in the current maneuver page to represent the directon of the maneuver
                nextDirection.setImageResource(displayHelper
                        .getImageResourceForDirection(m.turnManeuver));
                nextManeuver.setText(displayHelper.stringForDirection(m.turnManeuver));
            }

            if (position == maneuvers.size() - 1) {
						// final maneuver - customize directions to tell the user they are arriving at their destination
                v.findViewById(R.id.next).setVisibility(GONE);
                final int pointCount = navigator.getRoute().getPoints().size();
                PointOptions finalPoint = navigator.getRoute().getPoints().get(pointCount - 1);
                String customLocation = container.getContext().getString(R.string.custom_location_title);
                String arrive = container.getContext().getString(R.string.to_arrive,
                        finalPoint.getName() == null ? customLocation : finalPoint.getName());
                nextManeuver.setVisibility(View.VISIBLE);
                nextManeuver.setText(arrive);
            }

            container.addView(v);
            return v;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }


        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view.equals((View) object);
        }
    }
}

Custom UI when Talkback is turned on

Check if Talkback is on
public static boolean isTalkbackOn(Context context) {
        AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
        boolean isAccessibilityEnabled = am.isEnabled();
        if (isAccessibilityEnabled) {
            List<AccessibilityServiceInfo> list = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_AUDIBLE);
            if (list.size() > 0) {
                for (AccessibilityServiceInfo serviceInfo : list) {
                    if (serviceInfo.getId().contains("TalkBack")) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

 

 

  • No labels