Labs 03,04. Connectivity

Task 1 - Downloading remote content (4p)

Create a new project with a blank main Activity. Change the layout of the Activity (activity_main.xml) to

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    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"
    tools:context="com.softwareup.laborator03.MainActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <!-- Add all elements here -->

        </LinearLayout>
    </ScrollView>
</android.support.constraint.ConstraintLayout>

Add in the LinearLayout tag an EditText, a Button and a TextView to the Activity's layout. When the user enters an URL into the EditText and clicks the Button, download the remote HTTP content at that URL and display it into the TextView.

By design, the Android framework will not allow long-running or potentially blocking operations (such as network operations) to be carried out on the main UI thread. As such, within the onClick() method of the Button we want to start a new thread in which to download the HTTP data.

The simplest solution would be using a Thread instance. However, we want to modify a UI element (the TextView) right after the thread has finished downloading data, which cannot be done from a Thread's run() method. For such a scenario, the framework provides the AsyncTask class.

When defining the AsyncTask instance we can provide 3 parameters. Since we want to give our instance a String (the URL) to work with and have it return another String (the downloaded content) the first and third parameters will be String while the second one can be left Void:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v){
        AsyncTask<String, Void, String> task = new AsyncTask<String, Void, String>(){
        ...
        };
    }
});

The most important method within the AsyncTask is doInBackground(), which is executed on another thread. Since we have defined the AsyncTask as having a String as the first and third parameters, the signature of the doInBackground() method will be:

@Override
protected String doInBackground(String... params){
...
}

In order to retrieve HTTP data, we can use the HttpURLConnection API, using params[0] as the remote URL:

URL url = new URL(params[0]);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();

The actual contents can be retrieved by using the InputStream provided by the HttpURLConnection instance:

InputStream is = connection.getInputStream();
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
    result.write(buffer, 0, length);
}
return result.toString("UTF-8");

The String contents returned from the doInBackground() method are passed as the parameter to the onPostExecute() method, which will be executed on the main UI thread. As such, within this method we can safely update the contents of the TextView:

@Override
protected void onPostExecute(String content) {
  textView.setText(content);
}

Having defined the AsyncTask launch it using the execute method, which receives the text from the EditText element as a parameter:

task.execute(editText.getText().toString());

If you try to run the application at this time, it will stop since, by default, applications are not granted Internet access by the framework. In order to be able to connect to remote resources, you need to add the necessary permission to the AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

For each of the following tasks, do not forget to add the required permissions in the manifest file in order to be able to retrieve information about the location, WiFi). For mobile phones that have an OS version greater than Android 6.0, permissions for accessing or using location, Bluetooth need to be requested at runtime.

Task 2 - Getting the last known location from providers (6p)

For this task, you will need to have enabled either the GPS of your mobile device or an active WiFi connection (or both).

On Android, the location can be gathered using three providers:

  • GPS provider which uses the GPS driver of your mobile device to track your connection (it is slow and very precise in the outdoors)
  • network provider which relies on having an active Internet connection (it has high levels of accuracy)
  • passive provider which relies on having the current location determined by other applications

Add a button and three TextViews to the activity. When clicking on the button, the TextViews will have to show the last known location coordinates (latitude and longitude) obtained from the GPS provider, the network provider and the passive provider. If a provider is disabled, display a significant message such as WiFi/GPS/Passive provider is disabled;

In order to get the last known location, it is required to obtain a reference to a LocationManager object. Through this object, you can send a request to the location providers in order to obtain the last known location. Make sure to obtain a last known location from all providers.

In order to obtain location information, permissions for ACCESS_FINE_LOCATION and ACCESS_COURSE_LOCATION must be asked for and added to the manifest file. For devices that run an Android OS version greater or equal to 6, these permissions must be requested at runtime. Make sure to request these permissions only when they are not granted.

To check if a permission is granted, the following code is used

if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
    // do something with ungranted permission
}

The Activity class has a special method to request permissions at runtime using the following code

String[] PERMISSIONS = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
int PERMISSIONS_REQUEST_CODE = 123;

requestPermissions(PERMISSIONS, PERMISSIONS_REQUEST_CODE);

By using this method, you tell the activity that it should request the list of permissions given in the array and use the request code offered. The activity must also override the onRequestPermissionsResult method which is called after user input regarding permissions. Remember to call the super method for this.

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    // your code here  
    if (requestCode == PERMISSIONS_REQUEST_CODE) {
        if (grantResults.length >= PERMISSIONS.length) {
            for (int i = 0; i < grantResults.length; i++) {
                if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                    Log.d(TAG, "onRequestPermissionsResult: Permission " + permissions[i] +
                        " was not granted");
                }
            }
        } else {
              Log.d(TAG, "onRequestPermissionsResult: not all permissions were granted");
        }
    }  
}

The grantResult array is related to the permissions array and contains information about the user's input to the request.

To request permissions at runtime that were not granted, the following steps must be made:

  1. Check if permissions are granted
  2. Save all the ungranted permissions
  3. Request ungranted permissions
  4. Check result of permissions request

When trying to obtain the last known location, if permissions are not granted, a SecurityException exception is thrown. Make sure to catch this exception in a try-catch block

try {
    ...
} catch (SecurityException e) {
    // log exception thrown
}

Take into account the fact that the location updates take time. Therefore the TextView will not be changed immediately, it will be changed after 10-12 seconds if a network provider was used to determine the location or more than 20 seconds if a GPS provider is used.

Task 3 - Getting current and changes to location through service (7p)

The LocationManager object also offers a method through which you can request location updates for a specific provider. This method receives a minimum time when updates should be made and a minimum distance for when updates should be made. The LocationListener interface is used to monitor location changes, as well as location provider changes (enabled/disabled). In this task you will need to create a service that listens to location changes of a certain distance periodically, as well as location provider changes. Each time any change has occurred, an event will be sent to the MainActivity. Display each event in different TextViews.

In order to send events to the MainActivity we will use a third-party library called EventBus. This will simplify communication between components of the application.

As a first step import the library into the project by adding the following line in the application's build.gradle file, in the dependencies section.

compile 'org.greenrobot:eventbus:3.1.1'

In order for a component of the application to listen for specific events, the component must register itself. In our case, we will register the MainActivity to listen for events. To do so override the onResume and onPause methods of the activity as follows. This tells the MainActivity when to start and stop listening for events sent through EventBus.

@Override
protected void onResume() {
    super.onResume();
    EventBus.getDefault().register(this);
}

@Override
protected void onPause() {
    super.onPause();
    EventBus.getDefault().unregister(this);
}

It is recommended to create a base event class and each event class sent through the application should extend this class. For example an event base class could look like this

public abstract class Event {
    private String message;

    public Event(String message) {
        this.message = message;
    }
}

And a location event class could look like this

public class LocationEvent extends Event {
    private Location location;

    public LocationEvent(String message, Location location) {
        super(message);
        this.location = location;
    }

    public Location getLocation() {
        return location;
    }
}

In order for the main activity to listen to events, it must declare a method for each type of event it listens to. The name of the method is always onEventMainThread and the parameters of the method should be of event type. The method must always be public. In our case, for location events our method should look like

@Subscribe
public void onEventMainThread(LocationEvent event) {
    // your code here
}

To send an event through the application the following method is used where location is the information about the location contained in the event.

EventBus.getDefault().post(new LocationEvent("Location changed event", location));

Make sure that you have at least one onEventMainThread method in your component that registers to EventBus. If you have no method with this name an exception will be thrown and your application will crash.

To create a service, define a class that extends the Service class and implements the LocationListener class. This will require for you to override the onBind method

@Nullable
@Override
public IBinder onBind(Intent intent) {
    return null;
 }

To start the service you have to create an Intent object for the specific service and call the startService method from the Activity/Context of the application. Create a method startService in your LocationService.

public static void startService(Context context) {
    Log.i(TAG, "startService: Location service started");
    Intent serviceIntent = new Intent(context, LocationService.class);
    context.startService(serviceIntent);
}

This method must be called when the application starts.

By overriding the onStartCommand in the service, you define how the service behaves. Override this method and initialize the LocationManager object, request the service to perform periodical updates and get the last known location for WiFi and GPS providers.

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // your initialization code there
    // get last known location for providers

    return START_STICKY;
}

Do not forget to add the service to the AndroidManifest.xml file in the application tag.

<service
    android:name=".services.LocationService"
    android:enabled="true"></service>

Given the information provided above, use EventBus to send events regarding location changes, provider changes (enabling and disabling) as well as provider status changes from the Location Service designed which listens to changes. Build the service so that when it starts it gets the current location from both GPS and WiFi providers and sends an Event for each to the MainActivity.

Task 4 - Scaning for WiFi devices (3p)

In this task you will scan for WiFi devices on the click of a button.

Before going any further add the following classes and resource files to your project.

Add the NonScrollListView class

public class NonScrollListView extends ListView {

    public NonScrollListView(Context context) {
        super(context);
    }

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

    public NonScrollListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
                Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
        ViewGroup.LayoutParams params = getLayoutParams();
        params.height = getMeasuredHeight();
    }
}

Add the ListViewAdapter class to your project

public class ListViewAdapter extends BaseAdapter {

    private List<String> elements;
    private LayoutInflater layoutInflater;

    public ListViewAdapter(List<String> elements, Context context) {
        this.elements = elements;
        this.layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

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

    @Override
    public Object getItem(int i) {
        return elements.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        view = layoutInflater.inflate(R.layout.element_layout, null);

        TextView element = view.findViewById(R.id.element);
        element.setText(elements.get(i));

        return view;
    }
}

Add the element_layout.xml file to your layout folder

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <TextView
        android:id="@+id/element"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"/>
</LinearLayout>

In your activity layout (activity_main.xml) add the following elements

<Button
    android:id="@+id/button4"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:text="Task 4"/>

<!-- This should be the NonScrollListView class in your project (package may vary, insert the correct package) -->
<com.cs.pub.smd.laborator03.views.NonScrollListView
    android:id="@+id/task4"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"/>

Get a reference to the Button and the NonScrollListView elements in the onCreate method for the Activity.

Add an OnClickListener to the new button

button4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                List<String> elements = new ArrayList<>();

                // your code here

                ListViewAdapter adapter = new ListViewAdapter(elements, getApplicationContext());
                task4.setAdapter(adapter);
            }
        });

Use the WiFiManager to scan for WiFi devices and print their MAC address. Do this in the OnClickListener for the button and add the MAC addresses to the elements list.

smd/laboratoare/old/03.txt ยท Last modified: 2019/03/06 14:52 by adriana.draghici
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0