External Services CSE 5236: Mobile Application Development Course Coordinator: Dr. Rajiv Ramnath Instructor: Adam C. Champion Reading: Big Nerd Ranch Guide, Chap. 30 (WebView); Chaps. 33, 34 (Google Play services, maps)
External Services Viewing websites Location- and map-based functionality REST-based Web services invocation
Invoking Browser App Java Kotlin // HelpFragment.java private void launchBrowser( String url) { Uri theUri = Uri.parse(url); Intent LaunchBrowserIntent = new Intent(Intent.ACTION_VIEW, theUri); startActivity( LaunchBrowserIntent); } // HelpFragment.kt private fun launchBrowser( url: String) { val theUri = Uri.parse(url) val LaunchBrowserIntent = Intent(Intent.ACTION_VIEW, theUri) startActivity( LaunchBrowserIntent) } URL: http://en.wikipedia.org/wiki/Tictactoe Note: Activity stacking due to re-launch of browser on mobile page
Embedded WebView - Layout <?xml version="1.0" encoding="utf-8"?> <ScrollView ...> <LinearLayout ... <WebView android:id="@+id/helpwithwebview" android:layout_width="match_parent" android:layout_height="200dip" android:layout_weight="1.0"/> <Button ... android:text="Exit"/> </LinearLayout> </ScrollView>
Embedded WebView: Java // HelpWebViewFragment.java public View onCreateView(. . .) { View v = inflater.inflate(R.layout.fragment_help_webview, ...); WebView helpInWebView = (WebView) v.findViewById(R.id.helpwithwebview); mProgressBar = (ProgressBar) v.findViewById(R.id.webviewprogress); mProgressBar.setMax(100); View buttonExit = v.findViewById(R.id.button_exit); buttonExit.setOnClickListener(this); Bundle extras = getActivity().getIntent().getExtras(); if (extras != null) { mUrl = extras.getString(ARG_URI); /* . . . */ } // . . . helpInWebView.setWebViewClient(new WebViewClient() { public boolean shouldOverrideUrlLoading(. . .) { return false; } }); helpInWebView.setWebChromeClient(new WebChromeClient() { public void onProgressChanged(WebView webView, int progress) { if (progress == 100) { mProgressBar.setVisibility(View.GONE); } else { mProgressBar.setVisibility(View.VISIBLE); mProgressBar.setProgress(progress); } } }); helpInWebView.loadUrl(mUrl); return v; }
Embedded WebView: Kotlin // HelpWebViewFragment.kt override fun onCreateView( . . . ): View? { val v = inflater.inflate(R.layout.fragment_help_webview, . . .) val helpInWebView = v.findViewById<WebView>(R.id.helpwithwebview) mProgressBar = v.findViewById<ProgressBar>(R.id.webviewprogress) mProgressBar.apply { max = 100 } val buttonExit = v.findViewById<Button>(R.id.button_exit) buttonExit.setOnClickListener(this) val extras = activity.intent.extras if (extras != null) { mUrl = extras.getString(ARG_URI) } WebView.setWebContentsDebuggingEnabled(true) helpInWebView.settings.javaScriptEnabled = true helpInWebView.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(. . . ): Boolean { return false }} helpInWebView.webChromeClient = object : WebChromeClient() { override fun onProgressChanged( . . . ) { if (progress == 100) { mProgressBar.visibility = View.GONE } else { mProgressBar.visibility = View.VISIBLE mProgressBar.progress = progress } } } helpInWebView.loadUrl(mUrl) return v }
Location-Based Applications These mix-and-match the following actions: Opening a map Invoking a geocoding service on a point of interest Navigating the map to a position or make a map- based calculation Determining the user’s geolocation from the device (latitude, longitude)
Additional Requirements for Maps Use built-in GoogleMap with a FragmentActivity Link against Google APIs for Android (rather than standard Android SDK) Declare the use of the Google map package: <uses-library android:name="com.google.android.maps”/> Request the appropriate permissions, e.g.: <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES”/> Request a Map API key Goes in AndroidManifest.xml as <meta-data android:name="com.google.android.maps.v2.API_KEY” android:value=“...”/> See https://developers.google.com/maps/documentation/android/start Need OpenGL ES v2: <uses-feature android:glEsVersion="0x00020000” android:required="true"/>
Map Fragment Layout Google Maps uses Fragments <fragment xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/map" tools:context=".MapsActivity" android:name="com.google.android.gms.maps.SupportMapFragment"/> Google Maps uses Fragments UI Fragment needs to be loaded dynamically…
Map Fragment Code: Java (1) // MapsFragment.java public class MapsFragment extends SupportMapFragment implements OnMapReadyCallback { private GoogleMap mMap; private GoogleApiClient mApiClient; private static final String[] LOCATION_PERMISSIONS = new String[] { Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }; private FusedLocationProviderClient mFusedLocationProviderClient; private Location mLocation; private LatLng mDefaultLocation; private static final int REQUEST_LOCATION_PERMISSIONS = 0; private boolean mLocationPermissionGranted = false; . . . // MapsActivity.java just uses a SingleFragmentActivity
Map Fragment Code: Java (2) // MapsFragment.java (continued) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); mApiClient = new GoogleApiClient.Builder(getActivity()) .addApi(LocationServices.API) .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { @Override public void onConnected(@Nullable Bundle bundle) { getActivity().invalidateOptionsMenu(); } @Override public void onConnectionSuspended(int i) { . . . } }).build(); getMapAsync(this); } @Override public void onResume() { super.onResume(); setUpEula(); findLocation(); }
Map Fragment Code: Java (3) // MapsFragment.java (continued) @Override public void onStart() { // . . . getActivity().invalidateOptionsMenu(); mApiClient.connect(); } @Override public void onStop() { // . . . mApiClient.disconnect(); } private void findLocation() { updateLocationUI(); if (hasLocationPermission()) { mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(getActivity()); mDefaultLocation = new LatLng(40.0, -83.0); LocationRequest locationRequest = LocationRequest.create(); locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); locationRequest.setNumUpdates(1); locationRequest.setInterval(0); FusedLocationProviderClient locationProvider = LocationServices.getFusedLocationProviderClient(getActivity()); Task locationResult = locationProvider.getLastLocation(); locationResult.addOnCompleteListener(getActivity(), new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { if (task.isSuccessful()) { mLocation = (Location) task.getResult(); mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(mLocation.getLatitude(), mLocation.getLongitude()), 16)); } else { /* Disable Location */ } } }); } // Else request permissions, end of method.
Map Fragment Code: Java (4) // MapsFragment.java (continued) private void setUpEula() { mSettings = getActivity().getSharedPreferences(getString(R.string.prefs), 0); boolean isEulaAccepted = mSettings.getBoolean(getString(R.string.eula_accepted_key), false); if (!isEulaAccepted) { DialogFragment eulaDialogFragment = new EulaDialogFragment(); eulaDialogFragment.show(getActivity().getSupportFragmentManager(), "eula"); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // . . . inflater.inflate(R.menu.maps_menu, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_showcurrentlocation: Log.d(TAG, "Showing current location"); if (hasLocationPermission()) { findLocation(); } else { /* Request permissions */ } break; } return true; }
Map Fragment Code: Java (5) // MapsFragment.java (continued) @Override public void onRequestPermissionsResult(. . .) { mLocationPermissionGranted = false; switch (requestCode) { case REQUEST_LOCATION_PERMISSIONS: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { mLocationPermissionGranted = true; } } } updateLocationUI(); } private void updateLocationUI() { if (mMap == null) { return; } try { if (mLocationPermissionGranted) { /* Enable location */ } else { /* Disable location, request permissions */ } } catch (SecurityException e) { /* . . . */ } } @Override public void onMapReady(GoogleMap googleMap) { mMap = googleMap; mMap.addMarker(new MarkerOptions().position(new LatLng(40.0, -83.0)).title("Ohio State University")); mMap.setMyLocationEnabled(true); mMap.getUiSettings().setMyLocationButtonEnabled(true); /* . . . */ } private boolean hasLocationPermission() { /* . . . */ }
Map Fragment Code: Kotlin (1) // MapsFragment.kt class MapsFragment : SupportMapFragment(), OnMapReadyCallback { private lateinit var mMap: GoogleMap private lateinit var mApiClient: GoogleApiClient private lateinit var mFusedLocationProviderClient: FusedLocationProviderClient private var mLocation: Location? = null private var mDefaultLocation: LatLng? = null private var mLocationPermissionGranted = false private var mMapReady = false companion object { private val LOCATION_PERMISSIONS = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) private val REQUEST_LOCATION_PERMISSIONS = 0 }
Map Fragment Code: Kotlin (2) // MapsFragment.kt (continued) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) mApiClient = GoogleApiClient.Builder(activity) .addApi(LocationServices.API) .addConnectionCallbacks(object : GoogleApiClient.ConnectionCallbacks { override fun onConnected(bundle: Bundle?) { activity.invalidateOptionsMenu() } override fun onConnectionSuspended(i: Int) { . . . } }).build() getMapAsync(this) } override fun onResume() { super.onResume() setUpEula() findLocation() }
Map Fragment Code: Kotlin (3) // MapsFragment.kt (continued) override fun onStart() { // . . . activity.invalidateOptionsMenu() mApiClient.connect() } override fun onStop() { // . . . mApiClient.disconnect() } private fun findLocation() { updateLocationUI() if (hasLocationPermission()) { mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(activity) mDefaultLocation = LatLng(40.0, -83.0) val locationRequest = LocationRequest.create() locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY locationRequest.numUpdates = 1 locationRequest.interval = 0 val locationProvider = LocationServices.getFusedLocationProviderClient(activity) val locationResult = locationProvider.lastLocation locationResult.addOnCompleteListener(activity, object : OnCompleteListener<Location> { override fun onComplete(task: Task<Location>) { if (task.isSuccessful) { // Set the map's camera position to the current location of the device. mLocation = task.result as Location mMap.moveCamera(CameraUpdateFactory.newLatLngZoom( LatLng(mLocation!!.latitude, mLocation!!.longitude), 16f)) } else { /* Disable location */ } } }) } // Else request permissions, end of method.
Map Fragment Code: Kotlin (4) // MapsFragment.kt (continued) private fun setUpEula() { mSettings = activity.getSharedPreferences(getString(R.string.prefs), 0) val isEulaAccepted = mSettings!!.getBoolean(getString(R.string.eula_accepted_key), false) if (!isEulaAccepted) { val eulaDialogFragment = EulaDialogFragment() eulaDialogFragment.show(activity.supportFragmentManager, "eula") } } override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { super.onCreateOptionsMenu(menu, inflater) inflater?.inflate(R.menu.maps_menu, menu) } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menu_showcurrentlocation -> { Log.d(TAG, "Showing current location") if (hasLocationPermission()) { findLocation() } else { requestPermissions(LOCATION_PERMISSIONS, REQUEST_LOCATION_PERMISSIONS) } } } return true }
Map Fragment Code: Kotlin (5) // MapsFragment.kt (continued) override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { mLocationPermissionGranted = false when (requestCode) { REQUEST_LOCATION_PERMISSIONS -> { // If request is cancelled, the result arrays are empty. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { mLocationPermissionGranted = true } } } updateLocationUI() } private fun updateLocationUI() { if (mMapReady) { try { if (mLocationPermissionGranted) { mMap.isMyLocationEnabled = true mMap.uiSettings.isMyLocationButtonEnabled = true } else { /* Disable location, request permissions */ } } catch (e: SecurityException) { Log.e("Exception: %s", e.message) } } /* . . . */ } override fun onMapReady(googleMap: GoogleMap) { mMap = googleMap mMap.addMarker(MarkerOptions().position(LatLng(40.0, -83.0)).title("Ohio State University")) try { mMap.isMyLocationEnabled = true mMap.uiSettings.isMyLocationButtonEnabled = true } catch (se: SecurityException) { Log.e(TAG, "Location not enabled, skipping") } mMap.isBuildingsEnabled = true mMap.isIndoorEnabled = true mMapReady = true } private fun hasLocationPermission(): Boolean { /* . . . */ }
Map Menu Layout <menu xmlns:app= "http://schemas.android.com/apk/res-auto" xmlns:android= "http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_showcurrentlocation" android:orderInCategory="100" android:title="@string/show_location" android:icon="@android:drawable/ ic_menu_mylocation" app:showAsAction="ifRoom"/> </menu>
License Agreement Fragment: Java Yes, this is required… public class EulaDialogFragment extends DialogFragment { public void setEulaAccepted() { SharedPreferences prefs = getActivity().getSharedPreferences( getString(R.string.prefs), 0); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(getString(R.string.eula_accepted_key), true) .apply(); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.eula_title) .setMessage(Html.fromHtml(getString(R.string.eula))) .setPositiveButton(R.string.accept, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { setEulaAccepted(); } }) .setNegativeButton(R.string.decline, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.cancel(); getActivity().finish(); } }); return builder.create(); } }
License Agreement Fragment: Kotlin class EulaDialogFragment : DialogFragment() { fun setEulaAccepted() { val prefs = activity.getSharedPreferences(getString(R.string.prefs), 0) val editor = prefs.edit() editor.putBoolean(getString(R.string.eula_accepted_key), true).apply() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { // Use the Builder class for convenient dialog construction val builder = AlertDialog.Builder(activity) builder.setTitle(R.string.about_app) .setMessage(Html.fromHtml(getString(R.string.eula))) .setPositiveButton(R.string.accept) { dialog, id -> setEulaAccepted() } .setNegativeButton(R.string.decline) { dialog, which -> dialog.cancel() activity.finish() } return builder.create() } }
Location Determination and Practices Types: GPS Cellular WiFi Best practices for location-based applications: Check for connectivity Use threading to ensure responsiveness (Note: Threading built-in for many SDK components)
References Chapter 10: “Channeling the Outside World through your Android Device” from Android SDK 3 Programming for Dummies Chapters 33–34: “Locations and Play Services” and “Maps” from Android Programming: The Big Nerd Ranch Guide (3rd ed.) http://developer.android.com/reference/android/webkit/package- summary.html http://developer.android.com/reference/android/webkit/WebView.html https://developers.google.com/maps/documentation/android/start https://developer.android.com/guide/components/fragments.html https://developer.android.com/training/basics/fragments/creating.html
Questions and comments? Thank You Questions and comments?