/*
* Copyright 2013 Liquid Labs Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ca.liquidlabs.android.speedtestvisualizer.activities;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.Toast;
import ca.liquidlabs.android.speedtestvisualizer.R;
import ca.liquidlabs.android.speedtestvisualizer.fragments.SpeedTestInfoWindowAdapter;
import ca.liquidlabs.android.speedtestvisualizer.model.ComparableDownloadSpeed;
import ca.liquidlabs.android.speedtestvisualizer.model.SpeedTestRecord;
import ca.liquidlabs.android.speedtestvisualizer.util.AppConstants;
import ca.liquidlabs.android.speedtestvisualizer.util.CsvDataParser;
import ca.liquidlabs.android.speedtestvisualizer.util.Tracer;
import com.google.analytics.tracking.android.EasyTracker;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.LatLngBounds.Builder;
import com.google.android.gms.maps.model.MarkerOptions;
import org.apache.commons.lang3.StringUtils;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.List;
/**
* Mapping of exported data.
*
* @author Hossain Khan
* @see https://developers.google.com/maps/documentation/android/
* @see Maps V2 example project. Most codes are taken from sample project.
*/
public class MapperActivity extends Activity {
private static final String LOG_TAG = MapperActivity.class.getSimpleName();
private GoogleMap mMap;
private static List<SpeedTestRecord> mCsvListData;
private static float mMaxNetworkSpeed;
private static float mMinNetworkSpeed;
/**
* Available filter type for connections
*/
private static final int FILTER_TYPE_ALL = 0;
private static final int FILTER_TYPE_WIFI = 1;
private static final int FILTER_TYPE_CELL = 2;
private static int FILTER_SELECTED = FILTER_TYPE_ALL;
/**
* The default unit used by speedtest to export the data. <br/>
* FIXME: Based on Speedtest v3.0 app, unit is now dependent on what user selected on app.
*/
private static final String SPEED_UNIT = "";
/**
* {@link AsyncTask} to process all the marker data
*/
private MarkerDataProcessorTask mDataProcessorTask = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// get feature to show progress in actionbar when processing data
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.activity_mapper);
// It seems like 4.0.x enables progress by default - STOP it!
hideProgressIndicator();
Spinner spinner = (Spinner) findViewById(R.id.spinner_conntype_filter);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.filters_array, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(new ConnectionTypeFilterHandler());
// Show the Up button in the action bar.
setupActionBar();
setUpMapIfNeeded();
}
@Override
protected void onResume() {
super.onResume();
setUpMapIfNeeded();
}
@Override
protected void onPause() {
// Override the activity transition animation
overridePendingTransition(android.R.anim.slide_in_left, android.R.anim.slide_out_right);
super.onPause();
}
@Override
public void onStart() {
super.onStart();
// Tracks activity view using analytics.
EasyTracker.getInstance().activityStart(this);
}
@Override
public void onStop() {
super.onStop();
// Tracks activity view using analytics.
EasyTracker.getInstance().activityStop(this);
}
private void setUpMapIfNeeded() {
// Do a null check to confirm that we have not already instantiated the
// map.
if (mMap == null) {
// Try to obtain the map from the SupportMapFragment.
mMap = ((MapFragment) getFragmentManager().findFragmentById(R.id.map))
.getMap();
// Check if we were successful in obtaining the map.
if (mMap != null) {
// Hide the zoom controls as the button panel will cover it.
mMap.getUiSettings().setZoomControlsEnabled(false);
// Get the csv data from intent and then proceed
Bundle bundle = getIntent().getExtras();
final String csvHeader = bundle.getString(AppConstants.KEY_SPEEDTEST_CSV_HEADER);
final String csvData = bundle.getString(AppConstants.KEY_SPEEDTEST_CSV_DATA);
// create task if not created, otherwise re-use same task
if (mDataProcessorTask == null) {
mDataProcessorTask = new MarkerDataProcessorTask();
}
// Dispatch an asynctask to calculate required data for markers
mDataProcessorTask.execute(csvHeader, csvData);
}
}
}
private void setUpMap() {
if (mCsvListData == null || mCsvListData.size() == 0) {
// nothing to show on map - return with user msg
Toast.makeText(this, R.string.msg_no_records_found, Toast.LENGTH_LONG).show();
return;
}
// Setting an info window adapter allows us to change the both the
// contents and look of the
// info window.
mMap.setInfoWindowAdapter(new SpeedTestInfoWindowAdapter(getLayoutInflater()));
// Add lots of markers to the map.
addMarkersToMap();
}
/**
* Adds all the parsed speedtest markers to the map. This task is processor
* intensive, stalls the main thread if there is lots of data. But
* unfortunately, this has to be done in UI thread.
*/
private void addMarkersToMap() {
Builder mapBoundsBuilder = new LatLngBounds.Builder();
int currentTotalRecordCount = 0;
// Use parsed data to create map markers
for (SpeedTestRecord speedTestRecord : mCsvListData) {
if (FILTER_SELECTED == FILTER_TYPE_CELL
&& !speedTestRecord.getConnectionType().isCell()) {
continue; // do not add non-cell items
} else if (FILTER_SELECTED == FILTER_TYPE_WIFI
&& !speedTestRecord.getConnectionType().isWifi()) {
continue; // do not add non-wifi items
}
/*
* Build string array to concatenate and send info (doing it in dumb
* old way, rather than passing serialized data). NOTE: Must be
* retrieved in same order
*/
String snippetMultiInfo[] = {
speedTestRecord.getConnectionType().toString(),
NumberFormat.getInstance().format(speedTestRecord.getDownload()) + " "
+ SPEED_UNIT,
NumberFormat.getInstance().format(speedTestRecord.getUpload()) + " "
+ SPEED_UNIT
};
mMap.addMarker(new MarkerOptions()
.position(speedTestRecord.getLatLng())
.title(speedTestRecord.getDate())
.snippet(StringUtils.join(snippetMultiInfo, AppConstants.TEXT_SEPARATOR))
.icon(BitmapDescriptorFactory.defaultMarker(speedTestRecord.getMarkerColorHue())));
// also build the maps bounds area
mapBoundsBuilder.include(speedTestRecord.getLatLng());
// Update the count for selected filter
currentTotalRecordCount++;
}
if (currentTotalRecordCount > 0) {
// apply bounds if anything was added
this.applyMapCameraBounds(mapBoundsBuilder.build());
}
}
private void applyMapCameraBounds(final LatLngBounds bounds) {
// Pan to see all markers in view.
// Cannot zoom to bounds until the map has a size.
final View mapView = getFragmentManager().findFragmentById(R.id.map).getView();
if (mapView.getViewTreeObserver().isAlive()) {
mapView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation")
// We use the new method when supported
@SuppressLint("NewApi")
// We check which build version we are using.
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
mapView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
mapView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50));
}
});
}
}
private boolean checkReady() {
if (mMap == null || mCsvListData == null) {
Toast.makeText(this, R.string.msg_map_not_ready, Toast.LENGTH_SHORT).show();
return false;
}
return true;
}
/**
* Clears maps
*/
public void clearMap() {
if (!checkReady()) {
return;
}
mMap.clear();
}
/**
* Resets the maps
*/
public void updateMapMarkers() {
if (!checkReady()) {
return;
}
// Clear the map because we don't want duplicates of the markers.
mMap.clear();
addMarkersToMap();
}
/**
* Calculates marker color warmness based on download speed. <br/>
* Highest speed -> RED, Lowest speed -> GREEN.
*
* @param speedValue Single record's speed value
* @return hue value based on speed
*/
private static float getWeightedMarkerValue(float speedValue) {
float speedDifference = MapperActivity.mMaxNetworkSpeed - MapperActivity.mMinNetworkSpeed;
if (speedDifference <= 0) {
// this might be the case, when there is only one record and
// MaxSpeed = MinSpeed, So, return warmest hue value
return BitmapDescriptorFactory.HUE_RED;
}
// calculate hue value based on speed
float hueVal = ((BitmapDescriptorFactory.HUE_GREEN * MapperActivity.mMaxNetworkSpeed) - (BitmapDescriptorFactory.HUE_GREEN * speedValue))
/ speedDifference;
if (hueVal < BitmapDescriptorFactory.HUE_RED || hueVal > BitmapDescriptorFactory.HUE_ROSE) {
return BitmapDescriptorFactory.HUE_GREEN;
}
return hueVal;
}
/**
* Shows progress animation in ActioBar
*/
private void showProgressIndicator() {
setProgressBarIndeterminateVisibility(true);
setProgressBarIndeterminate(true);
}
/**
* Hides progress animation in ActionBar
*/
private void hideProgressIndicator() {
setProgressBarIndeterminateVisibility(false);
setProgressBarIndeterminate(false);
}
/**
* Set up the {@link android.app.ActionBar}.
*/
private void setupActionBar() {
getActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// This ID represents the Home or Up button. In the case of this
// activity, the Up button is shown. Use NavUtils to allow users
// to navigate up one level in the application structure. For
// more details, see the Navigation pattern on Android Design:
//
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
//
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Task to calculate additional data required for mapping markers
*/
private class MarkerDataProcessorTask extends AsyncTask<String, Void, Void> {
@Override
protected void onPreExecute() {
showProgressIndicator();
}
@Override
protected Void doInBackground(String... params) {
mCsvListData = CsvDataParser.parseCsvData(params[0], params[1]);
// do additional operation only if there is more than 1 data
if (mCsvListData.size() > 0) {
Collections.sort(mCsvListData, new ComparableDownloadSpeed());
// at this point we know there is at least one data, save min
// and max speed data
mMinNetworkSpeed = mCsvListData.get(0).getDownload();
mMaxNetworkSpeed = mCsvListData.get(mCsvListData.size() - 1)
.getDownload();
// For each of the marker data - update hue color value based on
// download speed
for (SpeedTestRecord record : mCsvListData) {
record.setMarkerColorHue(getWeightedMarkerValue(record
.getDownload()));
}
Tracer.debug(LOG_TAG, "Min: " + MapperActivity.mMinNetworkSpeed
+ ", Max: " + MapperActivity.mMaxNetworkSpeed);
}
// Nothing to return
return null;
}
@Override
protected void onPostExecute(Void v) {
// hide progress when all processing done
hideProgressIndicator();
// now setup map with marker and other params
setUpMap();
}
}
/**
* Inner listener class to listent for change in connection type filter
* spinner.
*/
private class ConnectionTypeFilterHandler implements OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (!checkReady()) {
return;
}
String filterName = (String) parent.getItemAtPosition(position);
Tracer.debug(LOG_TAG, "Selected Filter: " + filterName);
if (filterName.equals(getString(R.string.filter_all))) {
MapperActivity.FILTER_SELECTED = MapperActivity.FILTER_TYPE_ALL;
MapperActivity.this.updateMapMarkers();
} else if (filterName.equals(getString(R.string.filter_cell))) {
MapperActivity.FILTER_SELECTED = MapperActivity.FILTER_TYPE_CELL;
MapperActivity.this.updateMapMarkers();
} else if (filterName.equals(getString(R.string.filter_wifi))) {
MapperActivity.FILTER_SELECTED = MapperActivity.FILTER_TYPE_WIFI;
MapperActivity.this.updateMapMarkers();
} else {
Tracer.info(LOG_TAG, "Error applying filter with name " + filterName);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// do nothing
Tracer.debug(LOG_TAG, "OnItemSelectedListener > onNothingSelected()");
}
}
}