CIS 493/EEC 492 Android Sensor Programming Lecture 5 Wenbing Zhao Department of Electrical Engineering and Computer Science Cleveland State University wenbing@ieee.org 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Outline Services Networking 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Services How to create a service that runs in the background How to perform long-running tasks in a separate thread How to perform repeated tasks in a service How an activity and a service communicate 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development What is a Service A facility for the application to tell the system about something it wants to be doing in the background Even when the user is not directly interacting with the application This corresponds to calls to Context.startService(), which ask the system to schedule work for the service, to be run until the service or someone else explicitly stop it A facility for an application to expose some of its functionality to other applications This corresponds to calls to Context.bindService(), which allows a long-standing connection to be made to the service in order to interact with it 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development What is a Service A Service is not a separate process. The Service object itself does not imply it is running in its own process; unless otherwise specified, it runs in the same process as the application it is part of A Service is not a thread. It is not a means itself to do work off of the main thread 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Service Lifecycle If someone calls Context.startService() then the system will retrieve the service and then call its onStartCommand(Intent, int, int) method with the arguments supplied by the client The service will at this point continue running until Context.stopService() or stopSelf() is called There are two major modes of operation depending on the value they return from onStartCommand(): START_STICKY is used for services that are explicitly started and stopped as needed START_NOT_STICKY or START_REDELIVER_INTENT are used for services that should only remain running while processing any commands sent to them 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Service Lifecycle Clients can also use Context.bindService() to obtain a persistent connection to a service This creates the service but does not call onStartCommand() The client will receive the IBinder object that the service returns from its onBind(Intent) method, allowing the client to then make calls back to the service The service will remain running as long as the connection is established (whether or not the client retains a reference on the service's IBinder) Usually the IBinder returned is for a complex interface 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Service Permission Global access to a service can be enforced when it is declared in its manifest's <service> tag By doing so, other applications will need to declare a corresponding <uses-permission> element in their own manifest to be able to start, stop, or bind to the service 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Basic Service Basic steps of creating/using a service: Create a separate service java class Add service in the manifest Start/stop the service from the main activity Create a new project and name it Services Modify manifest: <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android=http://schemas.android.com/apk/res/android package="com.wenbing.services"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".MyService" /> </application> </manifest> 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.widget.Toast; import java.net.MalformedURLException; import java.net.URL; import android.os.AsyncTask; import android.util.Log; import java.util.Timer; import java.util.TimerTask; Basic Service Add a new Java class: MyService public class MyService extends Service { int counter = 0; static final int UPDATE_INTERVAL = 1000; private Timer timer = new Timer(); @Override public IBinder onBind(Intent arg0) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { Toast.makeText(this, "Service Started", Toast.LENGTH_LONG).show(); doSomethingRepeatedly(); try { new DoBackgroundTask().execute( new URL("http://www.amazon.com/somefiles.pdf"), new URL("http://www.wrox.com/somefiles.pdf"), new URL("http://www.google.com/somefiles.pdf"), new URL("http://www.learn2develop.net/somefiles.pdf")); } catch (MalformedURLException e) { e.printStackTrace(); } return START_STICKY; } Perform task asynchronously: don’t wait for completion 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development @Override public void onDestroy() { super.onDestroy(); if (timer != null){ timer.cancel(); } Toast.makeText(this, "Service Destroyed", Toast.LENGTH_LONG).show(); } private void doSomethingRepeatedly() { timer.scheduleAtFixedRate(new TimerTask() { public void run() { Log.d("MyService", String.valueOf(++counter)); } }, 0, UPDATE_INTERVAL); } private class DoBackgroundTask extends AsyncTask<URL, Integer, Long> { protected Long doInBackground(URL... urls) { int count = urls.length; long totalBytesDownloaded = 0; for (int i = 0; i < count; i++) { totalBytesDownloaded += DownloadFile(urls[i]); publishProgress((int) (((i + 1) / (float) count) * 100)); } return totalBytesDownloaded; } Calculate percentage downloaded and report its progress 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development onProgressUpdate is invoked when you call publishProgress() protected void onProgressUpdate(Integer... progress) { Log.d("Downloading files", String.valueOf(progress[0]) + "% downloaded"); Toast.makeText(getBaseContext(), String.valueOf(progress[0]) + "% downloaded-"+counter, Toast.LENGTH_LONG).show(); } protected void onPostExecute(Long result) { Toast.makeText(getBaseContext(), "Downloaded " + result + " bytes", Toast.LENGTH_LONG).show(); //stopSelf(); } } private int DownloadFile(URL url) { try { //---simulate taking some time to download a file--- Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //---return an arbitrary number representing // the size of the file downloaded--- return 100; } } The onPostExecute() is invoked in the UI thread and is called when the doInBackground() method has finished execution Call stopSelf() if you want to destroy the service as soon as the long-running task is done 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.....services.MainActivity"> <Button android:text="Start Service" android:layout_width="90dp" android:layout_height="50dp" android:id="@+id/btnStartService" app:layout_constraintLeft_toLeftOf="@+id/activity_main" app:layout_constraintTop_toTopOf="@+id/activity_main" android:layout_marginTop="16dp" app:layout_constraintRight_toRightOf="@+id/activity_main" app:layout_constraintBottom_toTopOf="@+id/btnStopService" android:layout_marginBottom="8dp" android:onClick="startService" /> <Button android:text="Stop Service" android:layout_width="88dp" android:layout_height="48dp" android:id="@+id/btnStopService" app:layout_constraintLeft_toLeftOf="@+id/activity_main" android:layout_marginStart="16dp" app:layout_constraintTop_toTopOf="@+id/activity_main" app:layout_constraintRight_toRightOf="@+id/activity_main" android:layout_marginEnd="16dp" app:layout_constraintBottom_toBottomOf="@+id/activity_main" android:onClick="stopService" /> </android.support.constraint.ConstraintLayout> Basic Service Modify layout: add two buttons to start/stop service 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Basic Service Modify MainActivity.java: start/stop service import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.content.Intent; import android.view.View; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void startService(View view) { startService(new Intent(getBaseContext(), MyService.class)); } public void stopService(View view) { stopService(new Intent(getBaseContext(), MyService.class)); } } 4/22/2019 CIS 470: Mobile App Development
Communication between Service and Activity Create another app and name it MyService2 Add a Java class MyIntentService Add the service in manifest <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.....myservices2"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".MyIntentService" /> </application> </manifest> 4/22/2019 CIS 470: Mobile App Development
Communication between Service and Activity import android.app.IntentService; import android.content.Intent; import android.util.Log; import java.net.MalformedURLException; import java.net.URL; public class MyIntentService extends IntentService { public MyIntentService() { super("MyIntentServiceName"); } @Override protected void onHandleIntent(Intent intent) { try { int result = DownloadFile(new URL("http://www.amazon.com/somefile.pdf")); Log.d("IntentService", "Downloaded " + result + " bytes"); Intent broadcastIntent = new Intent(); broadcastIntent.setAction("FILE_DOWNLOADED_ACTION"); getBaseContext().sendBroadcast(broadcastIntent); } catch (MalformedURLException e) { e.printStackTrace(); } } private int DownloadFile(URL url) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return 100; } } Populate MyIntentService.java //---send a broadcast to inform the activity // that the file has been downloaded--- 4/22/2019 CIS 470: Mobile App Development
Communication between Service and Activity import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.view.View; import android.widget.Toast; public class MainActivity extends AppCompatActivity { IntentFilter intentFilter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override public void onResume() { super.onResume(); intentFilter = new IntentFilter(); intentFilter.addAction("FILE_DOWNLOADED_ACTION"); //---register the receiver--- registerReceiver(intentReceiver, intentFilter); } @Override public void onPause() { super.onPause(); //---unregister the receiver--- unregisterReceiver(intentReceiver); } Modify MainActivity.java intent to filter for file downloaded intent 4/22/2019 CIS 470: Mobile App Development
Communication between Service and Activity Modify MainActivity.java Modify the layout to add two buttons as before public void startService(View view) { startService(new Intent(getBaseContext(), MyIntentService.class)); } public void stopService(View view) { stopService(new Intent(getBaseContext(), MyIntentService.class)); } private BroadcastReceiver intentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Toast.makeText(getBaseContext(), "File downloaded!", Toast.LENGTH_LONG).show(); } }; } 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Homework#14 Modify the ServiceBindingTest app: Use a ProgressBar view instead/in addition to the Toast to display the progress made by the service 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Networking How to connect to the web using HTTP How to consume XML web services How to consume JSON web services 4/22/2019 CIS 470: Mobile App Development
Fetch Web Content using HTTP Create a new app as usual and name it Networking Add the INTERNET permission in manifest Add an ImageView in activity_main.xml <uses-permission android:name="android.permission.INTERNET"/> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/activity_main" tools:context="com.wenbing.networking.MainActivity"> <ImageView android:src="@color/material_grey_300" android:layout_width="209dp" android:layout_height="272dp" android:id="@+id/imageView" app:layout_constraintLeft_toLeftOf="@+id/activity_main" app:layout_constraintTop_toTopOf="@+id/activity_main" app:layout_constraintRight_toRightOf="@+id/activity_main" app:layout_constraintBottom_toBottomOf="@+id/activity_main" /> </android.support.constraint.ConstraintLayout> 4/22/2019 CIS 470: Mobile App Development
Fetch Web Content using HTTP import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import android.Manifest; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.widget.ImageView; import android.widget.Toast; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import java.io.InputStreamReader; Modify MainActivity.java. We will first fetch an image, then the text/html public class MainActivity extends AppCompatActivity { ImageView img; final private int REQUEST_INTERNET = 123; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET}, REQUEST_INTERNET); } else{ downloadSomething(); } } 4/22/2019 CIS 470: Mobile App Development
Fetch Web Content using HTTP private void downloadSomething() { String texturl = "http://academic.csuohio.edu/zhao_w/"; String imgurl = "http://2.bp.blogspot.com/-us_u2PBLSOI/UqgPMh7ovjI/AAAAAAAACdI/ujdyGregs6Y/s1600/amazing-butterfly-hd-image.jpg"; new DownloadImageTask().execute(imgurl); new DownloadTextTask().execute(texturl); } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { switch (requestCode) { case REQUEST_INTERNET: if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { downloadSomething(); } else { Toast.makeText(MainActivity.this, "Permission Denied", Toast.LENGTH_SHORT).show(); } break; default: super.onRequestPermissionsResult(requestCode, permissions, grantResults); } } 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development private InputStream OpenHttpConnection(String urlString) throws IOException { InputStream in = null; int response = -1; URL url = new URL(urlString); URLConnection conn = url.openConnection(); if (!(conn instanceof HttpURLConnection)) throw new IOException("Not an HTTP connection"); try{ HttpURLConnection httpConn = (HttpURLConnection) conn; httpConn.setAllowUserInteraction(false); httpConn.setInstanceFollowRedirects(true); httpConn.setRequestMethod("GET"); httpConn.connect(); response = httpConn.getResponseCode(); if (response == HttpURLConnection.HTTP_OK) { in = httpConn.getInputStream(); } } catch (Exception ex) { Log.d("Networking", ex.getLocalizedMessage()); throw new IOException("Error connecting"); } return in; } private InputStream download(String URL) { InputStream in = null; try { in = OpenHttpConnection(URL); return in; } catch (IOException e1) { Log.d("NetworkingActivity", e1.getLocalizedMessage()); } return null; } 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development private Bitmap DownloadImage(String URL) { Bitmap bitmap = null; InputStream in = download(URL); if(in != null) { bitmap = BitmapFactory.decodeStream(in); try { in.close(); } catch (IOException e1) { Log.d("NetworkingActivity", e1.getLocalizedMessage()); } } return bitmap; } private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> { protected Bitmap doInBackground(String... urls) { return DownloadImage(urls[0]); } protected void onPostExecute(Bitmap result) { ImageView img = (ImageView) findViewById(R.id.imageView); img.setImageBitmap(result); } } The three periods after the final parameter's type indicate that the final argument may be passed as an array or as a sequence of arguments. Varargs can be used only in the final argument position 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development private String DownloadText(String URL) { int BUFFER_SIZE = 2000; InputStream in = download(URL); InputStreamReader isr = new InputStreamReader(in); int charRead; String str = ""; char[] inputBuffer = new char[BUFFER_SIZE]; try { while ((charRead = isr.read(inputBuffer))>0) { String readString = String.copyValueOf(inputBuffer, 0, charRead); str += readString; inputBuffer = new char[BUFFER_SIZE]; } in.close(); } catch (IOException e) { Log.d("Networking", e.getLocalizedMessage()); return ""; } return str; } private class DownloadTextTask extends AsyncTask<String, Void, String> { protected String doInBackground(String... urls) { return DownloadText(urls[0]); } @Override protected void onPostExecute(String result) { Toast.makeText(getBaseContext(), result, Toast.LENGTH_LONG).show(); } } } 4/22/2019 CIS 470: Mobile App Development
Fetch Web Content using HTTP 4/22/2019 CIS 470: Mobile App Development
Accessing Web Services import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import android.Manifest; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.widget.ImageView; import android.widget.Toast; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; Create another app and name it WebService Add INTERNET permission in manifest Modify MainActivity.java 4/22/2019 CIS 470: Mobile App Development
Accessing Web Services public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET}, REQUEST_INTERNET); } else { new AccessWebServiceTask().execute("apple"); } } 4/22/2019 CIS 470: Mobile App Development
Accessing Web Services ImageView img; final private int REQUEST_INTERNET = 123; private InputStream OpenHttpConnection(String urlString) throws IOException { InputStream in = null; int response = -1; URL url = new URL(urlString); URLConnection conn = url.openConnection(); if (!(conn instanceof HttpURLConnection)) throw new IOException("Not an HTTP connection"); try { HttpURLConnection httpConn = (HttpURLConnection) conn; httpConn.setAllowUserInteraction(false); httpConn.setInstanceFollowRedirects(true); httpConn.setRequestMethod("GET"); httpConn.connect(); response = httpConn.getResponseCode(); if (response == HttpURLConnection.HTTP_OK) { in = httpConn.getInputStream(); } } catch (Exception ex) { Log.d("Networking", ex.getLocalizedMessage()); throw new IOException("Error connecting"); } return in; } 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development private String WordDefinition(String word) { InputStream in = null; String strDefinition = ""; try { in = OpenHttpConnection( "http://services.aonaware.com" + "/DictService/DictService.asmx/Define?word=" + word); Document doc = null; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db; try { db = dbf.newDocumentBuilder(); doc = db.parse(in); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } doc.getDocumentElement().normalize(); NodeList definitionElements = doc.getElementsByTagName("Definition"); for (int i = 0; i < definitionElements.getLength(); i++) { Node itemNode = definitionElements.item(i); if (itemNode.getNodeType() == Node.ELEMENT_NODE) { Element definitionElement = (Element) itemNode; NodeList wordDefinitionElements = (definitionElement).getElementsByTagName("WordDefinition"); strDefinition = ""; for (int j = 0; j < wordDefinitionElements.getLength(); j++) { Node n = wordDefinitionElements.item(j); if (n.getNodeType() == Node.ELEMENT_NODE) { Element wordDefinitionElement = (Element) n; NodeList textNodes = ((Node) wordDefinitionElement).getChildNodes(); strDefinition += ((Node) textNodes.item(0)).getNodeValue() + ". \n"; } } } } } catch (IOException e1) { Log.d("NetworkingActivity", e1.getLocalizedMessage()); } return strDefinition; } 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development private class AccessWebServiceTask extends AsyncTask<String, Void, String> { protected String doInBackground(String... urls) { return WordDefinition(urls[0]); } protected void onPostExecute(String result) { Toast.makeText(getBaseContext(), result, Toast.LENGTH_LONG).show(); } } } 4/22/2019 CIS 470: Mobile App Development
Consuming JSON Services [ { "appeId":"1", "survId":"1", "location":"", "surveyDate":"2008-03 14", "surveyTime":"12:19:47", "inputUserId":"1", "inputTime":"2008-03-14 12:21:51", "modifyTime":"0000-00-00 00:00:00” }, "appeId":"2", "survId":"32", "surveyDate":"2008-03-14", "surveyTime":"22:43:09", "inputUserId":"32", "inputTime":"2008-03-14 22:43:37", "modifyTime":"0000-00-00 00:00:00" } ] XML is expensive both for decoding and transmission JSON is a concise representation of XML document Create another app and name it JSON Add INTERNET permission in manifest 4/22/2019 CIS 470: Mobile App Development
Consuming JSON Services import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.os.AsyncTask; import android.util.Log; import android.widget.Toast; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); new ReadJSONFeedTask().execute( "http://extjs.org.cn/extjs/examples/grid/survey.html"); } Modify MainActivity.java 4/22/2019 CIS 470: Mobile App Development
Consuming JSON Services public String readJSONFeed(String address) { URL url = null; try { url = new URL(address); } catch (MalformedURLException e) { e.printStackTrace(); }; StringBuilder stringBuilder = new StringBuilder(); HttpURLConnection urlConnection = null; try { urlConnection = (HttpURLConnection) url.openConnection(); } catch (IOException e) { e.printStackTrace(); } try { InputStream content = new BufferedInputStream( urlConnection.getInputStream()); BufferedReader reader = new BufferedReader( new InputStreamReader(content)); String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } } catch (IOException e) { e.printStackTrace(); } finally { urlConnection.disconnect(); } return stringBuilder.toString(); } Modify MainActivity.java 4/22/2019 CIS 470: Mobile App Development
Consuming JSON Services private class ReadJSONFeedTask extends AsyncTask<String, Void, String> { protected String doInBackground(String... urls) { return readJSONFeed(urls[0]); } protected void onPostExecute(String result) { try { JSONArray jsonArray = new JSONArray(result); Log.i("JSON", "Number of surveys in feed: " + jsonArray.length()); for (int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); Toast.makeText(getBaseContext(), jsonObject.getString("appeId") + " - " + jsonObject.getString("inputTime"), Toast.LENGTH_SHORT).show(); } } catch (Exception e) { e.printStackTrace(); } } } } Modify MainActivity.java 4/22/2019 CIS 470: Mobile App Development
CIS 470: Mobile App Development Homework# 15 Modify the Networking app: Load a webpage that has at least two images Analyze the html page received, identify the image urls by parsing the <img> component, and fetch every image referenced by the page Store the fetched images in a Bitmap array Display the thumbnail images in a GridView On touching each thumbnail in the GridView, display the full image 4/22/2019 CIS 470: Mobile App Development