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 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:
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.
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:
try { ... } catch (SecurityException e) { // log exception thrown }
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));
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.
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.