This shows you the differences between two versions of the page.
smd:laboratoare:05 [2019/03/06 14:59] adriana.draghici removed |
smd:laboratoare:05 [2021/05/18 22:17] (current) adriana.draghici [Tasks] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Labs 05,06. Permissions ===== | + | ===== Lab 5. HTTP and web services ===== |
+ | ==== Objectives ==== | ||
- | === Task 1 - List permissions, packages, features from CLI (2p) === | + | * Learn to make secure network calls |
+ | * Understand and practice making network calls using HttpUrlConnection, HttpsUrlConnection | ||
+ | * Learn how to use an API using the Retrofit network library | ||
+ | * Extra: Securing the app by integrating Firebase authentication services | ||
- | In the command line (**adb shell**): | + | ==== HttpUrlConnection ==== |
- | * List all permissions currently known by the system. | + | |
- | * List all permission groups known by the system. | + | |
- | * Display more information about the permissions using **-f** (defining package, label, description and protection level). | + | |
- | * Display all permissions in **android.permission-group.PHONE**. | + | |
- | * Display detailed information about all permissions in **android.permission-group.PHONE**. | + | |
- | * List all packages installed in the system. | + | |
- | * List all features of the system (hardware & software). | + | |
- | Hint: [[http://androiddoc.qiniudn.com/tools/help/shell.html#pm|pm commands]]. | + | In order to make network operations in an Android application [[https://developer.android.com/reference/java/net/HttpURLConnection.html|HTTPUrlConnection]] is used, which is a specialization of [[https://developer.android.com/reference/java/net/URLConnection.html|URLConnection]] with HTTP capabilities. This class manages the client-server communication. |
- | === Task 2 - MyCamera application (7p) === | + | In order to use this class we need to follow these steps: |
+ | * Create an instance of HttpURLConnection by calling [[https://developer.android.com/reference/java/net/URL.html#openConnection()|openConnection()]]. The result needs to be casted HttpURLConnection. | ||
+ | * Add information to the request besides the URI. | ||
+ | * If needed, add a request body and send it using URLConnection.getOutputStream(). | ||
+ | * Read response from URLConnection.getInputStream(). | ||
+ | * Finally don’t forget to disconnect. | ||
- | Implement an application **MyCamera** that captures photos and saves them full-sized on the SDCARD (/sdcard/Pictures/). Hint: Use this [[https://developer.android.com/training/camera/photobasics.html|tutorial]] (Sections: Save the full-size photo, Decode a scaled image). \\ | + | Here is a simple example of a using HTTPUrlConnection to get the content of a webpage into a String: |
- | The application will contain an Activity called **MainActivity** that will capture the photo and display it. The activity will include a **Button** and an **ImageView**. When pressing the Button, the camera should appear to let the user take a photo. Then, the photo will be displayed in the ImageView. | + | <code> |
+ | URL url = new URL("http://example.com"); | ||
+ | HttpsURLConnection connection = null; | ||
+ | try { | ||
+ | connection = (HttpsURLConnection) url.openConnection(); | ||
- | Hint: Use **Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)** to obtain the directory /sdcard/Pictures. Also, when specifying the provider's external path use "Pictures". | + | InputStream is = connection.getInputStream(); |
- | + | ByteArrayOutputStream result = new ByteArrayOutputStream(); | |
- | The camera is started using an **Intent** with the action **MediaStore.ACTION_IMAGE_CAPTURE**. In **onActivityResult**, if the request was successful, display the picture. | + | byte[] buffer = new byte[1024]; |
- | + | int length; | |
- | <code> | + | while ((length = is.read(buffer)) != -1) { |
- | @Override | + | result.write(buffer, 0, length); |
- | protected void onActivityResult(int requestCode, int resultCode, Intent data){ | + | |
- | super.onActivityResult(requestCode, resultCode, data); | + | |
- | // Check which request we're responding to | + | |
- | if (requestCode == REQUEST_TAKE_PHOTO) { | + | |
- | // Make sure the request was successful | + | |
- | if (resultCode == RESULT_OK) { | + | |
- | setPic(); | + | |
- | } | + | |
} | } | ||
+ | return result.toString("UTF-8"); | ||
+ | } catch (IOException e) { | ||
+ | Log.e(TAG, e.toString()); | ||
+ | } finally { | ||
+ | if(connection != null) connection.disconnect(); | ||
} | } | ||
</code> | </code> | ||
- | Declare the necessary permissions and request them at runtime. You will need the dangerous permissions: **android.permission.CAMERA** and **android.permission.WRITE_EXTERNAL_STORAGE**. | + | If order for the network request to work we need to have necessary permissions: |
+ | * For Internet access | ||
+ | |||
+ | <code><uses-permission android:name="android.permission.INTERNET" /></code> | ||
+ | * For accessing the network state. This will help us check the connectivity so that we can initiate requests | ||
- | === Task 3 - Custom Permissions (7p) === | + | <code><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /></code> |
- | Extend the previous application (MyCamera) by adding a new Activity called **DisplayPictureActivity** and move the **ImageView** in this activity. Also move the method that displays the picture (**setPic()**) in this activity. | ||
- | **DisplayPictureActivity** is started from **MainActivity** using an **Intent**, containing the path of the photo that was just taken as extra. The **displayPicture()** method is called from **onActivityResult**, after the picture has been captured and saved. | + | If your app needs to check the network's state it can use the system service ConnectivityManager - [[https://developer.android.com/training/basics/network-ops/reading-network-state|documentation examples]]. |
- | <code> | + | If your app needs to react to connectivity changes (e.g. send a file when the network becomes available) you can use Android's job scheduling API called [[https://developer.android.com/topic/libraries/architecture/workmanager|WorkManager]], since newer Android versions removed the system broadcast for connectivity changes. Here's an example (scenario #3) of how to use it: [[https://medium.com/ki-labs-engineering/monitoring-wifi-connectivity-status-part-1-c5f4287dd57|example]]. |
- | private void displayPicture(){ | + | |
- | Intent intent = new Intent(); | + | |
- | intent.setAction("com.smd.lab5.mycamera.startDisplayPictureActivity"); | + | <note important>Note for Android 9 and above HTTP calls will receive an IOException because cleartext network traffic is disabled. You can add your application trusted domains in a network security configuration file or to enable cleartext within the application manifest. Check [[https://developer.android.com/training/articles/security-config#CleartextTrafficPermitted|Cleartext Traffic Permitted]]</note> |
- | intent.putExtra("photoPath", mCurrentPhotoPath); | + | |
- | startActivity(intent); | + | ==== HttpsUrlConnection ==== |
- | } | + | |
+ | For only //https// connections we can use [[https://developer.android.com/reference/javax/net/ssl/HttpsURLConnection|HttpsURLConnection]]. By default HttpUrlConnection can receive use both //HTTP// and //HTTPS// because it is a superclass of HttpsUrlConnection. When we call | ||
+ | |||
+ | <code> HttpURLConnection connection = (HttpURLConnection) url.openConnection();</code> | ||
+ | |||
+ | url.openConnection() will return a HttpsUrlConnetion object that can be cast to HttpUrlConnection. | ||
+ | |||
+ | <note> From [[https://developer.android.com/reference/kotlin/java/net/URL?hl=en#openconnection|openConnection()]] declaration javadoc: //"For example, for HTTP an HttpURLConnection will be returned, and for JAR a JarURLConnection will be returned."//</note> | ||
+ | |||
+ | |||
+ | ==== Retrofit ==== | ||
+ | |||
+ | [[https://square.github.io/retrofit/|Retrofit]] is a type-safe HTTP Client for Android and Java. The basic functionality it is to turn the HTTP API into a Java interface. | ||
+ | |||
+ | When working with retrofit you need to consider the following: | ||
+ | * How is the data transmitted. For example, one popular way is to use JSON objects. Retrofit can handle their conversion to Java objects | ||
+ | * A **model** for the data you need transmitted | ||
+ | * this is usually a class with fields that reflect the JSON schema you use in your requests | ||
+ | * Define the endpoints and the requests you need to make in an interface, we refer to it as **service** (not to be confused with Android services) | ||
+ | |||
+ | |||
+ | Basic steps for using this library: | ||
+ | - Add dependencies in the build.gradle file of your project (see example below) | ||
+ | - Add the INTERNET permission in the manifest file | ||
+ | - Create the classes for your model | ||
+ | - Define the requests and endpoints you need | ||
+ | * create "service interfaces" and use [[https://futurestud.io/tutorials/java-basics-for-retrofit-annotations|annotations]] such as @GET, @POST to define the operations you need. | ||
+ | - Create a retrofit instance and provide it the service interface | ||
+ | - Make calls to the api by calling the service's methods. | ||
+ | * provide callbacks to the calls in order to react to the results or failures | ||
+ | |||
+ | |||
+ | In this section's code examples we're going to create a basic HTTP GET request for a weather API service. This will receive //location// as a parameter and will return a Weather object that has all the data related to the current weather for that location. | ||
+ | |||
+ | <note important>When using Retrofit, there is no need to run the HTTP call using Retrofit in a separate thread. Retrofit already does this, creating threads for requests so that the user doesn't have to do this.</note> | ||
+ | |||
+ | As stated above in Retrofit we only need to declare our service interface. Below we declared our //WeatherService// which has a //GET// request for the location weather. Retrofit uses [[https://docs.oracle.com/javase/tutorial/java/annotations/|Annotations]] to show how a request will be handled. In our case we simply defined in the GET request the path to the weather endpoint and the //{location}// parameter using //{}//. In order to complete this we added the //@Path("location")// annotation before the location parameter. This way we link the method parameter to the URL parameter. | ||
+ | |||
+ | <coden java> | ||
+ | public interface WeatherService { | ||
+ | @GET("/api/weather?location={location}") | ||
+ | Call<Weather> getLocationWeather(@Path("location") String location); | ||
+ | } | ||
</code> | </code> | ||
- | **DisplayPictureActivity** must have an intent filter (with the same action) defined in the Manifest: | + | From here the [[https://square.github.io/retrofit/2.x/retrofit/retrofit2/Retrofit.html|Retrofit]] class generates an implementation of the WeatherService interface. When creating the Retrofit instance we need to pass our //baseUrl//. Using this and appending the requests path will create the complete URL. |
- | <code> | + | <code java> |
- | <intent-filter> | + | Retrofit retrofit = new Retrofit.Builder() |
- | <category android:name="android.intent.category.DEFAULT" /> | + | .baseUrl("https://my.weather.com/") |
- | <action android:name="com.smd.lab5.mycamera.startDisplayPictureActivity" /> | + | .build(); |
- | </intent-filter> | + | |
+ | WeatherService service = retrofit.create(WeatherService.class); | ||
</code> | </code> | ||
- | In **onCreate()** method of **DisplayPictureActivity**, get the Intent and extract the photo path from the Intent. Do NOT call **setPic()** from **onCreate()** because the ImageView does not have a width and height yet. **setPic()** must be called from **onWindowFocusChanged**: | + | Using the service we can make the [[https://square.github.io/retrofit/2.x/retrofit/retrofit2/Call.html|Call]] that will make synchronous or asynchronous HTTP request to the remote webserver. |
- | <code> | + | <code java> |
- | @Override | + | Call<Weather> weatherCall = service.getLocationWeather("Bucharest"); |
- | public void onWindowFocusChanged(boolean hasFocus) { | + | |
- | super.onWindowFocusChanged(hasFocus); | + | |
- | if (photoPath != null){ | + | |
- | setPic(); | + | |
- | } | + | |
- | } | + | |
</code> | </code> | ||
- | If the Intent does not contain an extra, use this method to obtain the path of the last photo in the folder. | + | In order to make the call and get the result in our app will need to call [[https://square.github.io/retrofit/2.x/retrofit/retrofit2/Call.html#enqueue-retrofit2.Callback-|enqueue()]]. This sends asynchronously the request and notifies the application through the //onResponse()// callback when a response is received or through //onFailure()// callback if something goes wrong. This call is handled on a background thread by Retrofit. |
- | <code> | + | <code java> |
- | public String lastFilePath() { | + | weatherCall.enqueue(new Callback<Weather>() { |
- | File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); | + | @Override |
- | File[] files = storageDir.listFiles(new FileFilter() { | + | public void onResponse(Call<Weather> call, Response<Weather> response) { |
- | public boolean accept(File file) { | + | displayWeather(response.body()); |
- | return file.isFile(); | + | } |
- | } | + | |
- | }); | + | @Override |
- | long lastMod = Long.MIN_VALUE; | + | public void onFailure(Call<Weather> call, Throwable t) { |
- | File choice = null; | + | Toast.makeText(MainActivity.this, "Something went wrong. Please try again later!", Toast.LENGTH_SHORT).show(); |
- | for (File file : files) { | + | } |
- | if (file.lastModified() > lastMod) { | + | } |
- | choice = file; | + | |
- | lastMod = file.lastModified(); | + | |
- | } | + | |
- | } | + | |
- | return choice.getAbsolutePath(); | + | |
- | } | + | |
</code> | </code> | ||
- | Add a custom permission in the Manifest file of **MyCamera** (permission tree, permission group and the actual permission). The permission must have the protection level **dangerous**. In the Manifest, request the new permission for starting the **DisplayPictureActivity** (**android:permission**). | + | If the service responds with [[https://www.json.org/|JSON]] objects, Retrofit can help deserialize them by using //converters//. A popular JSON library for Java is [[https://github.com/google/gson|GSON]]. If you are using Kotlin we recommend |
+ | [[https://github.com/square/moshi|Moshi]]. | ||
- | Create another application **UseMyCamera**. In the activity include a **Button** which will be used to start the activity of the application MyCamera. | + | The GSON library helps you convert JSON responses into [[https://en.wikipedia.org/wiki/Plain_old_Java_object|POJO]]s. In order to use this we need to add a [[http://square.github.io/retrofit/2.x/converter-gson/retrofit2/converter/gson/GsonConverterFactory.html|GsonConvertorFactory]] when creating the service: |
- | For this, we will use an implicit intent for launching the **DisplayPictureActivity** from the first application: | + | <code java> |
+ | Retrofit retrofit = new Retrofit.Builder() | ||
+ | .baseUrl("https://my.weather.com") | ||
+ | .addConverterFactory(GsonConverterFactory.create()) | ||
+ | .build(); | ||
+ | |||
+ | WeatherService service = retrofit.create(WeatherService.class); | ||
+ | </code> | ||
+ | |||
+ | In order to add the two libraries to our application we need to add them as dependencies in the //app/build.gradle// configuration file. | ||
<code> | <code> | ||
- | Intent i = new Intent(); | + | dependencies { |
- | i.setAction("startDisplayPictureActivity"); | + | implementation 'com.squareup.retrofit2:retrofit:2.9.0' |
- | startActivity(i); | + | implementation 'com.google.code.gson:gson:2.8.6' |
+ | } | ||
</code> | </code> | ||
- | Run the application. When pressing the button you should get a SecurityException (Permission Denied). For solving this, use the declared permission in the manifest of the second application. Also, you need to request the permission at runtime because it is a dangerous permission. | + | If you plan to use Moshi, here's a Kotlin example (although Moshi supports Java too): |
+ | <code java> | ||
+ | val retrofit: Retrofit = Retrofit.Builder() | ||
+ | .baseUrl("https://my.weather.com") | ||
+ | .addConverterFactory(MoshiConverterFactory.create()) | ||
+ | .build() | ||
+ | </code> | ||
+ | <code> | ||
+ | apply plugin: 'kotlin-kapt' | ||
+ | |||
+ | dependencies { | ||
+ | implementation 'com.squareup.retrofit2:retrofit:2.9.0' | ||
+ | implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' | ||
+ | implementation 'com.squareup.moshi:moshi-kotlin:1.12.0' | ||
+ | |||
+ | kapt "com.squareup.moshi:moshi-kotlin-codegen:1.11.0" | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | <note>Kotlin example for retrofit on our lab's repo: [[https://github.com/SMD-UPB/labs/tree/main/lab5-demos/retrofit-demo-kotlin|retrofit-demo-kotlin]]</note> | ||
+ | ==== Tasks ==== | ||
+ | |||
+ | === Task 0 - Init === | ||
+ | |||
+ | Create a new Android application project for API version >= Oreo. | ||
+ | |||
+ | Download the lab archive and replace the content of activity_main.xml and AndroidManifest.xml with the one provided in the archive. | ||
+ | |||
+ | === Task 1 - Fetch web page through HTTP (3p) === | ||
+ | |||
+ | In this task we will download a web page content and display it. | ||
+ | * Use the EditText to enter the webpage URL | ||
+ | * Use the TextView to show the result | ||
+ | * Use the Button to initiate the request. | ||
+ | * Use HttpURLConnection to make the network request. | ||
+ | * Use Runnable to do work on another thread. | ||
+ | |||
+ | Try different websites using //http// or //https//. Are there any differences? | ||
+ | |||
+ | Remove from the manifest inside the application tag the attribute //android:usesCleartextTraffic="true"//. Run the app again and make the webpage request? What happens? | ||
+ | |||
+ | === Task 2 - Fetch web page through HTTPS (1p) === | ||
+ | |||
+ | Update the code from Task 1 in order to use //HttpsURLConnection//. Add the WIFI connectivity check before making the call. | ||
+ | |||
+ | Try different websites using //http// or //https//. Are there any differences? | ||
+ | |||
+ | === Task 3 - Retrofit call (4p) === | ||
+ | |||
+ | Using the code examples from the lab change the HttpURLConnection network call with a Retrofit implementation. The webpage URL will be set as a base URL for the Retrofit instance. | ||
+ | |||
+ | <note hint>Use @GET(".") for the service call.</note> | ||
+ | <note hint>Use //ResponseBody// as a return object from the call. In order to get the webpage content call //response.body().string()//.</note> | ||
+ | |||
+ | === Task 4 - Retrofit call to API (2p) === | ||
+ | |||
+ | For this task will use the [[http://open-notify.org/Open-Notify-API/|Open Notify]] API which gives information about the [[https://en.wikipedia.org/wiki/International_Space_Station|ISS]]. | ||
+ | From this API will use http://api.open-notify.org/astros.json which gives us the current number of people in space. Using the lab code examples create a Retrofit service for this URL. Make a call to get the number of people in space and return a POJO matching the JSON file structure. Make the request to the API and present the current number of people in space in a TextView. | ||
+ | |||
+ | <note hint>The POJO object should contain only the attributes that we need in our application.</note> | ||
+ | |||
+ | === Optional === | ||
+ | === Task 5 - Firebase Authentication with Email address and Password (2p) === | ||
+ | |||
+ | We want to have a more secure application so that our users need to log in to see their data. In this task we will implement sign up and sign in using [[https://firebase.google.com/docs/auth/android/password-auth|Firebase Authentication with Email address and Password]]. In order to add this to the application go to //Tools->Firebase// and a new window will appear. Go to //Authentication// and follow the steps presented there. Use the activity_onboard_layout.xml file from the lab archive as a layout for the signup/signin activity. | ||
- | === Task 4 - Boot completed (4p) === | + | === Resources === |
- | Extend **UseMyCamera** application, to receive BOOT COMPLETED broadcast message. Implement a broadcast receiver for the action **android.intent.action.BOOT_COMPLETED**. In order to receive those messages, you need to declare the permission **android.permission.RECEIVE_BOOT_COMPLETED** in the Manifest. | + | * {{:smd:laboratoare:lab5_skel.zip|}} |
+ | * [[https://github.com/SMD-UPB/labs/tree/main/demos|Demo app for Retrofit shown during a lab]] | ||