package com.wytehat.btlogger;

import android.app.Activity;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.graphics.Color;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MapActivity extends Activity
implements SensorEventListener {

	private TrackerDatabase db;  
	private WebView web;  

	private Button filterButton;  
	private Button fromButton;  
	private Button toButton;  
	private Button lastHourButton;  
	private Button todayButton;  
	private Button applyButton;  

	private LinearLayout filterPanel;  


	private ScrollView filterScroll;
	private Spinner categorySpinner;
	private Spinner deviceSpinner;
	private Spinner resolutionSpinner;

	private CheckBox showAll;  

	private ArrayAdapter<String> categoryAdapter;  
	private ArrayAdapter<String> deviceAdapter;  

	private final ArrayList<String> categoryValues =  
	new ArrayList<String>();  

	private final ArrayList<String> categoryLabels =  
	new ArrayList<String>();  

	private final ArrayList<String> deviceAddresses =  
	new ArrayList<String>();  

	private final ArrayList<String> deviceLabels =  
	new ArrayList<String>();  

	private final ArrayList<String> resolutionLabels =
	new ArrayList<String>();

	/*
	 * Time-bucket widths for the trail sampler.
	 * 0 = every stored point (still capped by TrackPointSampler).
	 */
	private static final long[] RESOLUTION_VALUES = {
		0L,
		15L * 60L * 1000L,
		60L * 60L * 1000L,
		120L * 60L * 1000L
	};

	private static final String[] RESOLUTION_LABELS = {
		"All points",
		"Every 15 min",
		"Every 1 hour",
		"Every 2 hours"
	};

	private volatile String categoryFilter = "Trackers / Tags";
	private volatile String deviceFilter = "All";

	private boolean rebuildingCategories;
	private boolean rebuildingDevices;
	private boolean rebuildingResolution = true;
	private volatile boolean loadingMap;

	private volatile long historyFrom;
	private volatile long historyTo;

	/*
	 * Selected trail resolution, read by the background loader.
	 */
	private volatile long sampleInterval = RESOLUTION_VALUES[0];

	/*
	 * Map data (SQLite reads + HTML generation) is built off the main
	 * thread; only the WebView load and the spinner refresh run on it.
	 */
	private final ExecutorService mapExecutor =
	Executors.newSingleThreadExecutor();

	private final Handler ui =
	new Handler(Looper.getMainLooper());

	private volatile boolean destroyed;

	private LocationManager locationManager;  
	private SensorManager sensorManager;  
	private Sensor rotationSensor;  

	private Location phoneLocation;  
	private float phoneHeading;  

	private boolean mapReady;  

	private String focus;  

	private static final String[] CATEGORY_ORDER = {  
		"Trackers / Tags",  
		"Audio",  
		"Wearables",  
		"Phones",  
		"Computers",  
		"Vehicles",  
		"Other"  
	};  

	private final LocationListener locationListener =  
	new LocationListener() {  

		@Override  
		public void onStatusChanged(String provider, int status, Bundle extras)  
		{  
			// TODO: Implement this method  
		}  

		public void onLocationChanged(Location location) {  
			if (location == null) {  
				return;  
			}  

			phoneLocation = location;  

			if (location.hasBearing() &&  
				location.hasSpeed() &&  
				location.getSpeed() > 0.8f) {  

				phoneHeading = location.getBearing();  
			}  

			updatePhoneMarker();  
		}  
	};  

	@Override  
	protected void onCreate(Bundle state) {  
		super.onCreate(state);  

		db = new TrackerDatabase(this);  

		long now = System.currentTimeMillis();  

		historyTo = now;  
		historyFrom = now - (60L * 60L * 1000L);  

		focus = getIntent().getStringExtra("focus");  

		LinearLayout root = new LinearLayout(this);  
		root.setOrientation(LinearLayout.VERTICAL);  
		root.setBackgroundColor(Color.WHITE);  

		root.addView(  
			NavigationHelper.createBar(  
				this,  
				2,  
				"Tracked Item Map"  
			)  
		);  

		/*  
		 * Compact filter button.  
		 */  
		LinearLayout filterHeader =  
			new LinearLayout(this);  

		filterHeader.setOrientation(  
			LinearLayout.HORIZONTAL  
		);  

		filterHeader.setGravity(  
			android.view.Gravity.CENTER_VERTICAL  
		);  

		filterHeader.setPadding(  
			dp(8),  
			dp(2),  
			dp(8),  
			dp(2)  
		);  

		filterButton = new Button(this);  
		filterButton.setText("☰  Filters");  
		filterButton.setTextSize(13);  

		filterHeader.addView(  
			filterButton,  
			new LinearLayout.LayoutParams(  
				LinearLayout.LayoutParams.WRAP_CONTENT,  
				LinearLayout.LayoutParams.WRAP_CONTENT  
			)  
		);  

		root.addView(  
			filterHeader,  
			new LinearLayout.LayoutParams(  
				LinearLayout.LayoutParams.MATCH_PARENT,  
				LinearLayout.LayoutParams.WRAP_CONTENT  
			)  
		);  

		/*  
		 * Everything below this point is collapsible.  
		 */  
		filterPanel = new LinearLayout(this);  
		filterPanel.setOrientation(  
			LinearLayout.VERTICAL  
		);  

		filterPanel.setPadding(  
			dp(8),  
			0,  
			dp(8),  
			dp(4)  
		);  

		/*  
		 * Category.  
		 */  
		TextView categoryLabel =  
			smallLabel("Category");  

		filterPanel.addView(categoryLabel);  

		categorySpinner = new Spinner(this);  

		categoryValues.add("Trackers / Tags");  
		categoryLabels.add("Trackers / Tags");  

		categoryValues.add("All");  

		categoryLabels.add("All categories");  

		categoryAdapter =  
			new ArrayAdapter<String>(  
			this,  
			android.R.layout.simple_spinner_item,  
			categoryLabels  
		);  

		categoryAdapter.setDropDownViewResource(  
			android.R.layout.simple_spinner_dropdown_item  
		);  

		categorySpinner.setAdapter(categoryAdapter);  

		filterPanel.addView(  
			categorySpinner,  
			compactParams()  
		);  

		/*  
		 * Device.  
		 */  
		TextView deviceLabel =  
			smallLabel("Device");  

		filterPanel.addView(deviceLabel);  

		deviceSpinner = new Spinner(this);  

		deviceAdapter =  
			new ArrayAdapter<String>(  
			this,  
			android.R.layout.simple_spinner_item,  
			deviceLabels  
		);  

		deviceAdapter.setDropDownViewResource(  
			android.R.layout.simple_spinner_dropdown_item  
		);  

		deviceSpinner.setAdapter(deviceAdapter);  

		filterPanel.addView(
			deviceSpinner,
			compactParams()
		);

		/*
		 * Trail resolution (downsampling).
		 */
		LinearLayout resolutionRow =
			new LinearLayout(this);

		resolutionRow.setOrientation(
			LinearLayout.HORIZONTAL
		);

		resolutionRow.setGravity(
			android.view.Gravity.CENTER_VERTICAL
		);

		TextView resolutionLabel =
			smallLabel("Trail detail");

		resolutionRow.addView(
			resolutionLabel,
			new LinearLayout.LayoutParams(
				LinearLayout.LayoutParams.WRAP_CONTENT,
				LinearLayout.LayoutParams.WRAP_CONTENT
			)
		);
		resolutionSpinner = new Spinner(this);

		for (int i = 0;
			 i < RESOLUTION_LABELS.length;
		i++) {

			resolutionLabels.add(
				RESOLUTION_LABELS[i]
			);
		}

		ArrayAdapter<String> resolutionAdapter =
			new ArrayAdapter<String>(
			this,
			android.R.layout.simple_spinner_item,
			resolutionLabels
		);

		resolutionAdapter.setDropDownViewResource(
			android.R.layout.simple_spinner_dropdown_item
		);

		resolutionSpinner.setAdapter(
			resolutionAdapter
		);

		resolutionRow.addView(
			resolutionSpinner,
			new LinearLayout.LayoutParams(
				0,
				dp(38),
				1
			)
		);

		filterPanel.addView(resolutionRow);
		/*
		 * Time.
		 */
		TextView timeLabel =  
			smallLabel("Location history");  

		filterPanel.addView(timeLabel);  

		LinearLayout timeRow =  
			new LinearLayout(this);  

		timeRow.setOrientation(  
			LinearLayout.HORIZONTAL  
		);  

		fromButton = new Button(this);  
		toButton = new Button(this);  

		fromButton.setTextSize(11);  
		toButton.setTextSize(11);  

		timeRow.addView(  
			fromButton,  
			new LinearLayout.LayoutParams(  
				0,  
				LinearLayout.LayoutParams.WRAP_CONTENT,  
				1  
			)  
		);  

		timeRow.addView(  
			toButton,  
			new LinearLayout.LayoutParams(  
				0,  
				LinearLayout.LayoutParams.WRAP_CONTENT,  
				1  
			)  
		);  

		filterPanel.addView(timeRow);  

		/*  
		 * Quick time buttons.  
		 */  
		LinearLayout quickRow =  
			new LinearLayout(this);  

		quickRow.setOrientation(  
			LinearLayout.HORIZONTAL  
		);  

		lastHourButton = new Button(this);  
		todayButton = new Button(this);  
		applyButton = new Button(this);  

		lastHourButton.setText("1 Hour");  
		todayButton.setText("Today");  
		applyButton.setText("Apply");  

		lastHourButton.setTextSize(11);  
		todayButton.setTextSize(11);  
		applyButton.setTextSize(11);  

		quickRow.addView(  
			lastHourButton,  
			new LinearLayout.LayoutParams(  
				0,  
				LinearLayout.LayoutParams.WRAP_CONTENT,  
				1  
			)  
		);  

		quickRow.addView(  
			todayButton,  
			new LinearLayout.LayoutParams(  
				0,  
				LinearLayout.LayoutParams.WRAP_CONTENT,  
				1  
			)  
		);  

		quickRow.addView(  
			applyButton,  
			new LinearLayout.LayoutParams(  
				0,  
				LinearLayout.LayoutParams.WRAP_CONTENT,  
				1  
			)  
		);  

		filterPanel.addView(quickRow);  

		/*  
		 * Troubleshooting switch.  
		 */  
		showAll = new CheckBox(this);  

		showAll.setText(  
			"Show all scanned devices"  
		);  

		showAll.setTextSize(12);  

		showAll.setChecked(  
			getSharedPreferences(  
				"map_settings",  
				0  
			).getBoolean(  
				"show_all_scanned",  
				false  
			)  
		);  

		filterPanel.addView(showAll);  

		/*
		 * The panel is scrollable and bounded to a third of the
		 * screen so every control stays reachable on short screens.
		 */
		filterScroll = new ScrollView(this);

		filterScroll.setVisibility(View.GONE);

		filterScroll.addView(
			filterPanel,
			new android.widget.FrameLayout.LayoutParams(
				android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
				android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
			)
		);

		root.addView(
			filterScroll,
			new LinearLayout.LayoutParams(
				LinearLayout.LayoutParams.MATCH_PARENT,
				0,
				1
			)
		);
		/*  
		 * WebView map.  
		 */  
		web = new WebView(this);  

		web.getSettings().setJavaScriptEnabled(true);  
		web.getSettings().setDomStorageEnabled(true);  
		web.getSettings().setAllowFileAccess(true);  
		web.getSettings().setAllowContentAccess(true);  
		web.setBackgroundColor(Color.WHITE);  

		web.setWebViewClient(  
			new WebViewClient() {  

				@Override  
				public void onPageFinished(  
					WebView view,  
					String url) {  

					mapReady = true;  

					updatePhoneMarker();  
				}  

				@Override  
				public boolean shouldOverrideUrlLoading(  
					WebView view,  
					String url) {  

					if (url != null &&  
						url.startsWith("geo:")) {  

						openMap(url);  
						return true;  
					}  

					return false;  
				}  
			}  
		);  

		root.addView(
			web,
			new LinearLayout.LayoutParams(
				LinearLayout.LayoutParams.MATCH_PARENT,
				0,
				2
			)
		);  

		setContentView(root);  

		locationManager =  
			(LocationManager)  
			getSystemService(  
			LOCATION_SERVICE  
		);  

		sensorManager =  
			(SensorManager)  
			getSystemService(  
			SENSOR_SERVICE  
		);  

		if (sensorManager != null) {  

			rotationSensor =  
				sensorManager.getDefaultSensor(  
				Sensor.TYPE_ROTATION_VECTOR  
			);  
		}  

		/*  
		 * Filter open/close.  
		 */  
		filterButton.setOnClickListener(  
			new View.OnClickListener() {  

				@Override  
				public void onClick(View view) {  

					filterScroll.setVisibility(
						filterScroll.getVisibility()
						== View.VISIBLE
							? View.GONE
							: View.VISIBLE
					);  
				}  
			}  
		);  

		/*  
		 * Category.  
		 */  
		categorySpinner.setOnItemSelectedListener(  
			new AdapterView.OnItemSelectedListener() {  

				@Override  
				public void onItemSelected(  
					AdapterView<?> parent,  
					View view,  
					int position,  
					long id) {  

					if (rebuildingCategories) {  
						return;  
					}  

					if (position < 0 ||  
						position >=  
						categoryValues.size()) {  

						return;  
					}  

					categoryFilter =  
						categoryValues.get(position);  

					rebuildDeviceFilter();  
				}  

				@Override  
				public void onNothingSelected(  
					AdapterView<?> parent) {  
				}  
			}  
		);  

		/*  
		 * Device.  
		 */  
		deviceSpinner.setOnItemSelectedListener(  
			new AdapterView.OnItemSelectedListener() {  

				@Override  
				public void onItemSelected(  
					AdapterView<?> parent,  
					View view,  
					int position,  
					long id) {  

					if (rebuildingDevices) {  
						return;  
					}  

					if (position < 0 ||  
						position >=  
						deviceAddresses.size()) {  

						return;  
					}  

					deviceFilter =  
						deviceAddresses.get(position);  
				}  

				@Override  
				public void onNothingSelected(  
					AdapterView<?> parent) {  
				}  
			}  
		);  

		/*
		 * Trail resolution: applied immediately so the user can compare
		 * densities without reopening the panel.
		 */
		resolutionSpinner.setOnItemSelectedListener(
			new AdapterView.OnItemSelectedListener() {

				@Override
				public void onItemSelected(
					AdapterView<?> parent,
					View view,
					int position,
					long id) {

					if (rebuildingResolution) {
						return;
					}

					if (position < 0 ||
						position >=
						RESOLUTION_VALUES.length) {

						return;
					}

					if (sampleInterval ==
						RESOLUTION_VALUES[position]) {

						return;
					}

					sampleInterval =
						RESOLUTION_VALUES[position];

					reloadMap();
				}

				@Override
				public void onNothingSelected(
					AdapterView<?> parent) {
				}
			}
		);

		fromButton.setOnClickListener(
			new View.OnClickListener() {  

				@Override  
				public void onClick(View view) {  
					pickDateTime(true);  
				}  
			}  
		);  

		toButton.setOnClickListener(  
			new View.OnClickListener() {  

				@Override  
				public void onClick(View view) {  
					pickDateTime(false);  
				}  
			}  
		);  

		lastHourButton.setOnClickListener(  
			new View.OnClickListener() {  

				@Override  
				public void onClick(View view) {  

					long current =  
						System.currentTimeMillis();  

					historyTo = current;  

					historyFrom =  
						current -  
						(60L * 60L * 1000L);  

					updateTimeButtons();  
				}  
			}  
		);  

		todayButton.setOnClickListener(  
			new View.OnClickListener() {  

				@Override  
				public void onClick(View view) {  

					historyFrom =  
						startOfToday();  

					historyTo =  
						System.currentTimeMillis();  

					updateTimeButtons();  
				}  
			}  
		);  

		applyButton.setOnClickListener(  
			new View.OnClickListener() {  

				@Override  
				public void onClick(View view) {  

					if (historyTo < historyFrom) {  

						long temp =  
							historyFrom;  

						historyFrom =  
							historyTo;  

						historyTo =  
							temp;  
					}  

					rebuildDeviceFilter();  
					reloadMap();  

					/*  
					 * Collapse after applying.  
					 */  
					filterScroll.setVisibility(
						View.GONE
					);  
				}  
			}  
		);  

		showAll.setOnClickListener(  
			new View.OnClickListener() {  

				@Override  
				public void onClick(View view) {  

					getSharedPreferences(  
						"map_settings",  
						0  
					).edit()  
						.putBoolean(  
						"show_all_scanned",  
						showAll.isChecked()  
					)  
						.apply();  

					deviceFilter = "All";  

					rebuildDeviceFilter();  
					reloadMap();  
				}  
			}  
		);  

		updateTimeButtons();

		rebuildingResolution = false;

		rebuildDeviceFilter();

		/*  
		 * The initial map is deliberately:  
		 *  
		 * Trackers / Tags  
		 * Last hour  
		 * All tracked devices  
		 *  
		 * No historical trail is loaded until an  
		 * individual device is selected.  
		 */  
		reloadMap();  
	}  

	private LinearLayout.LayoutParams compactParams() {  

		return new LinearLayout.LayoutParams(  
			LinearLayout.LayoutParams.MATCH_PARENT,  
			dp(42)  
		);  
	}  

	private TextView smallLabel(String text) {  

		TextView label = new TextView(this);  

		label.setText(text);  
		label.setTextSize(11);  
		label.setTextColor(Color.DKGRAY);  

		label.setPadding(  
			dp(4),  
			dp(3),  
			dp(4),  
			0  
		);  

		return label;  
	}  

	private void pickDateTime(final boolean from) {  

		final Calendar current =  
			Calendar.getInstance();  

		current.setTimeInMillis(  
			from ? historyFrom : historyTo  
		);  

		DatePickerDialog dialog =  
			new DatePickerDialog(  
			this,  
			new DatePickerDialog.OnDateSetListener() {  

				@Override  
				public void onDateSet(  
					android.widget.DatePicker picker,  
					int year,  
					int month,  
					int day) {  

					final Calendar selected =  
						Calendar.getInstance();  

					selected.set(  
						year,  
						month,  
						day  
					);  

					TimePickerDialog timeDialog =  
						new TimePickerDialog(  
						MapActivity.this,  
						new TimePickerDialog.OnTimeSetListener() {  

							@Override  
							public void onTimeSet(  
								android.widget.TimePicker picker,  
								int hour,  
								int minute) {  

								selected.set(  
									Calendar.HOUR_OF_DAY,  
									hour  
								);  

								selected.set(  
									Calendar.MINUTE,  
									minute  
								);  

								selected.set(  
									Calendar.SECOND,  
									0  
								);  

								selected.set(  
									Calendar.MILLISECOND,  
									0  
								);  

								if (from) {  

									historyFrom =  
										selected.getTimeInMillis();  

								} else {  

									historyTo =  
										selected.getTimeInMillis();  
								}  

								updateTimeButtons();  
							}  
						},  
						current.get(  
							Calendar.HOUR_OF_DAY  
						),  
						current.get(  
							Calendar.MINUTE  
						),  
						false  
					);  

					timeDialog.show();  
				}  
			},  
			current.get(Calendar.YEAR),  
			current.get(Calendar.MONTH),  
			current.get(Calendar.DAY_OF_MONTH)  
		);  

		dialog.show();  
	}  

	private void updateTimeButtons() {  

		SimpleDateFormat format =  
			new SimpleDateFormat(  
			"M/d/yy h:mm a",  
			Locale.US  
		);  

		fromButton.setText(  
			"From\n" +  
			format.format(  
				new Date(historyFrom)  
			)  
		);  

		toButton.setText(  
			"To\n" +  
			format.format(  
				new Date(historyTo)  
			)  
		);  
	}  

	private boolean isUnknown(DeviceRecord record) {  

		if (record == null) {  
			return true;  
		}  

		String name =  
			record.displayName();  

		if (name == null) {  
			return true;  
		}  

		String value =  
			name.trim().toLowerCase(  
			Locale.US  
		);  

		if (value.length() == 0) {  
			return true;  
		}  

		return value.equals("unknown") ||  
			value.equals("unknown device") ||  
			value.equals("unknown item") ||  
			value.equals("unknown device name") ||  
			value.equals("unnamed") ||  
			value.equals("unnamed device") ||  
			value.equals("manufacturer unknown") ||  
			value.equals("device");  
	}  

	private boolean validAddress(DeviceRecord record) {  

		return record != null &&  
			record.address != null &&  
			record.address.trim().length() > 0;  
	}  

	private boolean categoryMatches(  
		DeviceRecord record) {  

		if ("All".equals(categoryFilter)) {  
			return true;  
		}  

		return categoryFilter.equals(  
			record.category  
		);  
	}  

	private void rebuildDeviceFilter() {

		rebuildDeviceFilter(
			getMapDevices(
				showAll.isChecked()
			)
		);
	}

	/*
	 * Main thread only. Accepts an already loaded device list so the
	 * background loader does not repeat the query.
	 */
	private void rebuildDeviceFilter(
		List<DeviceRecord> available) {

		if (available == null) {
			available =
				new ArrayList<DeviceRecord>();
		}

		ArrayList<String> nextAddresses =
			new ArrayList<String>();  

		ArrayList<String> nextLabels =  
			new ArrayList<String>();  

		nextAddresses.add("All");  
		nextLabels.add("All devices");  

		HashSet<String> seen =  
			new HashSet<String>();  

		for (int i = 0;  
			 i < available.size();  
		i++) {  

			DeviceRecord record =  
				available.get(i);  

			if (!validAddress(record)) {  
				continue;  
			}  

			if (!record.hasLocation) {  
				continue;  
			}  

			if (!categoryMatches(record)) {  
				continue;  
			}  

			if (!showAll.isChecked() &&  
				isUnknown(record)) {  
				continue;  
			}  

			String address =  
				record.address.trim();  

			if (seen.contains(address)) {  
				continue;  
			}  

			seen.add(address);  

			String name =  
				record.displayName();  

			if (name == null ||  
				name.trim().length() == 0) {  

				name = "Unknown item";  
			}  

			nextAddresses.add(address);  

			nextLabels.add(  
				name +  
				" [" +  
				address +  
				"]"  
			);  
		}  

		int selected =  
			nextAddresses.indexOf(  
			deviceFilter  
		);  

		if (selected < 0) {  

			deviceFilter = "All";  
			selected = 0;  
		}  

		rebuildingDevices = true;  

		deviceAddresses.clear();  
		deviceAddresses.addAll(  
			nextAddresses  
		);  

		deviceLabels.clear();  
		deviceLabels.addAll(  
			nextLabels  
		);  

		deviceAdapter.notifyDataSetChanged();  

		deviceSpinner.setSelection(  
			selected,  
			false  
		);  

		rebuildingDevices = false;  
	}  

	/*
	 * Safe to call from a worker thread: the caller passes the checkbox
	 * state in instead of reading the view here.
	 */
	private List<DeviceRecord> getMapDevices(
		boolean includeAll) {

		TrackerDatabase database = db;

		List<DeviceRecord> all =
			database == null
				? null
				: database.all();

		ArrayList<DeviceRecord> result =  
			new ArrayList<DeviceRecord>();  

		if (all == null) {  
			return result;  
		}  

		for (int i = 0;  
			 i < all.size();  
		i++) {  

			DeviceRecord record =  
				all.get(i);  

			if (record == null) {  
				continue;  
			}  

			/*  
			 * Normal map:  
			 * only explicitly map-enabled devices.  
			 */  
			if (!includeAll &&
				record.mapEnabled != 1) {

				continue;  
			}  

			/*  
			 * Normal map excludes unknown entries.
			 */
			if (!includeAll &&
				isUnknown(record)) {

				continue;  
			}  

			if (!record.hasLocation) {  
				continue;  
			}  

			result.add(record);  
		}  

		return result;  
	}  

	private void refreshMapCategories(  
		List<DeviceRecord> rows) {  

		int[] counts =  
			new int[CATEGORY_ORDER.length];  

		int located = 0;  

		if (rows != null) {  

			for (int r = 0;  
				 r < rows.size();  
			r++) {  

				DeviceRecord record =  
					rows.get(r);  

				if (record == null ||  
					!record.hasLocation) {  
					continue;  
				}  

				located++;  

				String category =  
					record.category;  

				if (category == null) {  
					category = "Other";  
				}  

				boolean found = false;  

				for (int i = 0;  
					 i < CATEGORY_ORDER.length;  
				i++) {  

					if (CATEGORY_ORDER[i]  
						.equals(category)) {  

						counts[i]++;  
						found = true;  
						break;  
					}  
				}  

				if (!found) {  
					counts[  
						CATEGORY_ORDER.length - 1  
						]++;  
				}  
			}  
		}  

		ArrayList<String> nextValues =  
			new ArrayList<String>();  

		ArrayList<String> nextLabels =  
			new ArrayList<String>();  

		/*  
		 * Keep Trackers first.  
		 */  
		nextValues.add("Trackers / Tags");  
		nextLabels.add(  
			"Trackers / Tags (" +  
			countFor(  
				counts,  
				"Trackers / Tags"  
			) +  
			")"  
		);  

		for (int i = 0;  
			 i < CATEGORY_ORDER.length;  
		i++) {  

			String category =  
				CATEGORY_ORDER[i];  

			if ("Trackers / Tags".equals(  
					category  
				)) {  
				continue;  
			}  

			if (counts[i] > 0) {  

				nextValues.add(category);  

				nextLabels.add(  
					category +  
					" (" +  
					counts[i] +  
					")"  
				);  
			}  
		}  

		nextValues.add("All");  
		nextLabels.add(  
			"All categories (" +  
			located +  
			")"  
		);  

		int selected =  
			nextValues.indexOf(  
			categoryFilter  
		);  

		if (selected < 0) {  

			categoryFilter =  
				"Trackers / Tags";  

			selected =  
				nextValues.indexOf(  
				categoryFilter  
			);  

			if (selected < 0) {  
				selected = 0;  
			}  
		}  

		rebuildingCategories = true;  

		categoryValues.clear();  
		categoryValues.addAll(  
			nextValues  
		);  

		categoryLabels.clear();  
		categoryLabels.addAll(  
			nextLabels  
		);  

		categoryAdapter.notifyDataSetChanged();  

		categorySpinner.setSelection(  
			selected,  
			false  
		);  

		rebuildingCategories = false;  
	}  

	private int countFor(  
		int[] counts,  
		String category) {  

		for (int i = 0;  
			 i < CATEGORY_ORDER.length;  
		i++) {  

			if (CATEGORY_ORDER[i]  
				.equals(category)) {  

				return counts[i];  
			}  
		}  

		return 0;  
	}  

	private void reloadMap() {

		if (loadingMap) {
			return;
		}

		loadingMap = true;
		mapReady = false;

		/*
		 * View state is snapshotted on the main thread; everything after
		 * this point (SQLite reads, trail sampling, HTML generation) runs
		 * on the worker.
		 */
		final boolean includeAll =
			showAll.isChecked();

		mapExecutor.execute(
			new Runnable() {

				@Override
				public void run() {

					List<DeviceRecord> available = null;
					String html = null;

					try {

						available =
							getMapDevices(includeAll);

						String device = deviceFilter;

						/*
						 * The selected device may have gone away since
						 * the last load; fall back to showing all.
						 */
						if (!"All".equals(device) &&
							!containsAddress(
								available,
								device
							)) {

							device = "All";
						}

						ArrayList<DeviceRecord> rows =
							new ArrayList<DeviceRecord>();

						for (int i = 0;
							 i < available.size();
						i++) {

							DeviceRecord record =
								available.get(i);

							if (record == null ||
								!record.hasLocation) {
								continue;
							}

							if (!categoryMatches(record)) {
								continue;
							}

							if (!"All".equals(device) &&
								!device.equals(
									record.address
								)) {
								continue;
							}

							rows.add(record);
						}

						html =
							buildHtml(
							rows,
							focus,
							device
						);

					} catch (Exception ignored) {
					}

					final List<DeviceRecord> loaded =
						available;

					final String content = html;

					ui.post(
						new Runnable() {

							@Override
							public void run() {

								try {

									if (destroyed ||
										web == null) {
										return;
									}

									refreshMapCategories(
										loaded
									);

									/*
									 * Rebuild device list after
									 * category changes.
									 */
									rebuildDeviceFilter(
										loaded
									);

									if (content != null) {

										web.loadDataWithBaseURL(
											"https://unpkg.com/",
											content,
											"text/html",
											"UTF-8",
											null
										);
									}

								} finally {

									loadingMap = false;
								}
							}
						}
					);
				}
			}
		);
	}

	private boolean containsAddress(
		List<DeviceRecord> records,
		String address) {

		if (records == null ||
			address == null) {

			return false;
		}

		for (int i = 0;
			 i < records.size();
		i++) {

			DeviceRecord record =
				records.get(i);

			if (record != null &&
				address.equals(record.address)) {

				return true;
			}
		}

		return false;
	}

	private String buildHtml(
		List<DeviceRecord> rows,
		String selectedAddress,
		String device) {

		StringBuilder script =  
			new StringBuilder();  

		/*  
		 * Historical data is loaded ONLY for a  
		 * specifically selected device.  
		 */  
		if (!"All".equals(device)) {

			for (int i = 0;
				 i < rows.size();
			i++) {

				DeviceRecord record =
					rows.get(i);

				if (!device.equals(
						record.address
					)) {
					continue;
				}

				TrackerDatabase database = db;

				List<LocationPoint> history =
					database == null
						? null
						: database.history(
						record.address,
						historyFrom,
						historyTo
					);

				/*
				 * Dense histories are downsampled here, on the
				 * worker thread, so the WebView never receives
				 * thousands of markers.
				 */
				history =
					TrackPointSampler.sample(
					history,
					sampleInterval
				);

				appendHistory(
					script,  
					record,  
					history,  
					validColor(record.pinColor)  
				);  

				break;  
			}  
		}  

		/*  
		 * Current markers.  
		 */  
		for (int i = 0;  
			 i < rows.size();  
		i++) {  

			DeviceRecord record =  
				rows.get(i);  

			String color =  
				validColor(record.pinColor);  

			StringBuilder details =  
				new StringBuilder();  

			if (record.photoUri != null &&  
				record.photoUri.length() > 0) {  

				try {  

					if (new java.io.File(  
							record.photoUri  
						).exists()) {  

						details.append(  
							"<img src='file://" +  
							htmlText(  
								record.photoUri  
							) +  
							"' style='width:72px;" +  
							"height:72px;" +  
							"object-fit:cover;" +  
							"border-radius:10px'>" +  
							"<br>"  
						);  
					}  

				} catch (Exception ignored) {  
				}  
			}  

			details.append("<b>")  
				.append(  
				htmlText(  
					record.displayName()  
				)  
			)  
				.append("</b><br>");  

			details.append(  
				"<a href='geo:"  
			)  
				.append(record.latitude)  
				.append(",")  
				.append(record.longitude)  
				.append("?q=")  
				.append(record.latitude)  
				.append(",")  
				.append(record.longitude)  
				.append("'>GPS: ")  
				.append(record.latitude)  
				.append(", ")  
				.append(record.longitude)  
				.append("</a>");  

			if (record.accuracy > 0) {  

				details.append(  
					" (±"  
				)  
					.append(  
					Math.round(  
						record.accuracy  
					)  
				)  
					.append(" m)");  
			}  

			if (hasValue(record.address)) {  

				details.append(  
					"<br>Address: "  
				)  
					.append(  
					htmlText(  
						record.address  
					)  
				);  
			}  

			if (hasValue(record.category)) {  

				details.append(  
					"<br>Category: "  
				)  
					.append(  
					htmlText(  
						record.category  
					)  
				);  
			}  

			if (hasValue(record.vendorName) &&  
				!"Unknown".equalsIgnoreCase(  
					record.vendorName  
				) &&  
				!"Manufacturer unknown"  
				.equalsIgnoreCase(  
					record.vendorName  
				)) {  

				details.append(  
					"<br>Manufacturer: "  
				)  
					.append(  
					htmlText(  
						record.vendorName  
					)  
				);  
			}  

			if (record.updatedAt > 0) {  

				details.append(  
					"<br>Last seen: "  
				)  
					.append(  
					htmlText(  
						date(  
							record.updatedAt  
						)  
					)  
				);  
			}  

			if (record.battery >= 0) {  

				details.append(  
					"<br>Battery: "  
				)  
					.append(record.battery)  
					.append("%");  
			}  

			if (record.rssi != 0 &&  
				record.lastRssiAt > 0) {  

				details.append(  
					"<br>RSSI: "  
				)  
					.append(record.rssi)  
					.append(" dBm");  
			}  

			String status =  
				reason(record.reason);  

			if (hasValue(status) &&  
				!"Status unknown"  
				.equalsIgnoreCase(  
					status  
				)) {  

				details.append(  
					"<br>Status: "  
				)  
					.append(  
					htmlText(status)  
				);  
			}  

			boolean locked =  
				record.locationLocked == 1;  

			int markerSize =  
				locked ? 10 : 22;  

			int iconSize =  
				markerSize + 6;  

			int anchor =  
				iconSize / 2;  

			String markerColor =  
				locked  
				? "#00A86B"  
				: color;  

			script.append(  
				"var icon"  
			)  
				.append(i)  
				.append(  
				"=L.divIcon({" +  
				"className:''," +  
				"html:\"<div style='" +  
				"width:"  
			)  
				.append(markerSize)  
				.append(  
				"px;height:"  
			)  
				.append(markerSize)  
				.append(  
				"px;border-radius:50%;" +  
				"background:"  
			)  
				.append(markerColor)  
				.append(  
				";border:2px solid white;" +  
				"box-shadow:0 1px 4px #333'>" +  
				"</div>\"," +  
				"iconSize:["  
			)  
				.append(iconSize)  
				.append(",")  
				.append(iconSize)  
				.append(  
				"],iconAnchor:["  
			)  
				.append(anchor)  
				.append(",")  
				.append(anchor)  
				.append(  
				"]});"  
			);  

			/*  
			 * Every current point is clickable.  
			 */  
			script.append(  
				"var marker"  
			)  
				.append(i)  
				.append(  
				"=L.marker(["  
			)  
				.append(record.latitude)  
				.append(",")  
				.append(record.longitude)  
				.append(  
				"],{icon:icon"  
			)  
				.append(i)  
				.append(  
				"}).addTo(map)"  
			)  
				.append(  
				".bindPopup('"  
			)  
				.append(  
				js(  
					details.toString()  
				)  
			)  
				.append(  
				"',{maxWidth:320});"  
			);  

			script.append(  
				"bounds.push(["  
			)  
				.append(record.latitude)  
				.append(",")  
				.append(record.longitude)  
				.append("]);");  

			if (record.address != null &&  
				record.address.equals(  
					selectedAddress  
				)) {  

				script.append(  
					"marker"  
				)  
					.append(i)  
					.append(  
					".openPopup();"  
				);  

				script.append(  
					"map.setView(["  
				)  
					.append(record.latitude)  
					.append(",")  
					.append(record.longitude)  
					.append(  
					"],19);"  
				);  
			}  
		}  

		return "<!doctype html>" +  
			"<html>" +  
			"<head>" +  

			"<meta name='viewport' " +  
			"content='width=device-width," +  
			"initial-scale=1,maximum-scale=1'>" +  

			"<link rel='stylesheet' " +  
			"href='https://unpkg.com/leaflet@1.9.4/" +  
			"dist/leaflet.css'>" +  

			"<style>" +  

			"html,body,#map{" +  
			"width:100%;" +  
			"height:100%;" +  
			"margin:0;" +  
			"padding:0;" +  
			"overflow:hidden;" +  
			"}" +  

			".leaflet-popup-content{" +  
			"font:14px sans-serif;" +  
			"line-height:1.45;" +  
			"}" +  

			".leaflet-control-layers{" +  
			"font:13px sans-serif;" +  
			"}" +  

			".history-popup{" +  
			"min-width:190px;" +  
			"}" +  

			".phone-bearing{" +  
			"position:relative;" +  
			"width:38px;" +  
			"height:38px;" +  
			"transform-origin:19px 19px;" +  
			"}" +  

			".phone-dot{" +  
			"position:absolute;" +  
			"left:9px;" +  
			"top:9px;" +  
			"width:16px;" +  
			"height:16px;" +  
			"border-radius:50%;" +  
			"background:#4285F4;" +  
			"border:3px solid white;" +  
			"box-shadow:0 1px 4px #555;" +  
			"}" +  

			".phone-tip{" +  
			"position:absolute;" +  
			"left:15px;" +  
			"top:1px;" +  
			"width:0;" +  
			"height:0;" +  
			"border-left:5px solid transparent;" +  
			"border-right:5px solid transparent;" +  
			"border-bottom:10px solid #4285F4;" +  
			"}" +  

			"</style>" +  
			"</head>" +  

			"<body>" +  

			"<div id='map'></div>" +  

			"<script src='" +  
			"https://unpkg.com/leaflet@1.9.4/" +  
			"dist/leaflet.js'>" +  
			"</script>" +  

			"<script>" +  

			"var streets=L.tileLayer(" +  
			"'https://tile.openstreetmap.org/" +  
			"{z}/{x}/{y}.png'," +  
			"{maxNativeZoom:19,maxZoom:22," +  
			"attribution:'© OpenStreetMap contributors'});" +  

			"var sat=L.tileLayer(" +  
			"'https://server.arcgisonline.com/" +  
			"ArcGIS/rest/services/World_Imagery/" +  
			"MapServer/tile/{z}/{y}/{x}'," +  
			"{maxNativeZoom:19,maxZoom:22," +  
			"attribution:'Esri World Imagery'});" +  

			"var map=L.map('map'," +  
			"{layers:[streets]," +  
			"zoomControl:true}).setView([20,0],2);" +  

			"L.control.layers(" +  
			"{'Streets':streets," +  
			"'Satellite':sat}," +  
			"null,{collapsed:true}).addTo(map);" +  

			"var bounds=[];" +  

			"var phone=null;" +  

			"var phoneShown=false;" +  

			"var phoneIcon=L.divIcon({" +  
			"className:''," +  
			"html:'<div id=\"phoneBearing\" " +  
			"class=\"phone-bearing\">" +  
			"<div class=\"phone-dot\"></div>" +  
			"<div class=\"phone-tip\"></div>" +  
			"</div>'," +  
			"iconSize:[38,38]," +  
			"iconAnchor:[19,19]" +  
			"});" +  

			"function updatePhone(a,b,h){" +  

			"var p=[a,b];" +  

			"if(!phone){" +  
			"phone=L.marker(p," +  
			"{icon:phoneIcon," +  
			"zIndexOffset:1100}).addTo(map);" +  
			"}else{" +  
			"phone.setLatLng(p);" +  
			"}" +  

			"if(!phoneShown){" +  

			"phoneShown=true;" +  

			"var visible=bounds.slice(0);" +  

			"visible.push(p);" +  

			"if(visible.length>1){" +  

			"map.fitBounds(visible," +  
			"{padding:[35,35],maxZoom:19});" +  

			"}else{" +  

			"map.setView(p,19);" +  

			"}" +  

			"}" +  

			"var e=document.getElementById(" +  
			"'phoneBearing');" +  

			"if(e){" +  
			"e.style.transform=" +  
			"'rotate('+h+'deg)';" +  
			"}" +  

			"}" +  

			script.toString() +  

			"if(bounds.length>0 && " +  
			"'" +  
			js(  
			selectedAddress == null  
			? ""  
			: selectedAddress  
		) +  
			"'==''){" +  

			"map.fitBounds(bounds," +  
			"{padding:[30,30],maxZoom:19});" +  

			"}" +  

			"</script>" +  

			"</body>" +  
			"</html>";  
	}  

	private void appendHistory(  
		StringBuilder script,  
		DeviceRecord record,  
		List<LocationPoint> history,  
		String color) {  

		if (history == null ||  
			history.size() == 0) {  

			return;  
		}  

		script.append(  
			"var trail=["  
		);  

		for (int i = 0;  
			 i < history.size();  
		i++) {  

			LocationPoint point =  
				history.get(i);  

			if (point == null) {  
				continue;  
			}  

			if (i > 0) {  
				script.append(",");  
			}  

			script.append("[")  
				.append(point.latitude)  
				.append(",")  
				.append(point.longitude)  
				.append("]");  
		}  

		script.append("];");  

		if (history.size() > 1) {  

			script.append(  
				"L.polyline(trail," +  
				"{color:'"  
			)  
				.append(color)  
				.append(  
				"',weight:4," +  
				"opacity:.65})" +  
				".addTo(map);"  
			);  
		}  

		/*  
		 * Every historical point gets its own  
		 * clickable marker.  
		 */  
		for (int i = 0;  
			 i < history.size();  
		i++) {  

			LocationPoint point =  
				history.get(i);  

			if (point == null) {  
				continue;  
			}  

			StringBuilder popup =  
				new StringBuilder();  

			popup.append(  
				"<div class='history-popup'>"  
			);  

			popup.append("<b>")  
				.append(  
				htmlText(  
					record.displayName()  
				)  
			)  
				.append("</b><br>");  

			popup.append(  
				"<b>Historical location</b><br>"  
			);  

			popup.append(  
				"Time: "  
			)  
				.append(  
				htmlText(  
					date(  
						point.timestamp  
					)  
				)  
			)  
				.append("<br>");  

			popup.append(  
				"Latitude: "  
			)  
				.append(point.latitude)  
				.append("<br>");  

			popup.append(  
				"Longitude: "  
			)  
				.append(point.longitude)  
				.append("<br>");  

			if (point.accuracy > 0) {  

				popup.append(  
					"Accuracy: ±"  
				)  
					.append(  
					Math.round(  
						point.accuracy  
					)  
				)  
					.append(" m<br>");  
			}  

			popup.append(  
				"<a href='geo:"  
			)  
				.append(point.latitude)  
				.append(",")  
				.append(point.longitude)  
				.append("?q=")  
				.append(point.latitude)  
				.append(",")  
				.append(point.longitude)  
				.append(  
				"'>Open in Maps</a>"  
			);  

			popup.append("</div>");  

			script.append(  
				"var historyPoint"  
			)  
				.append(i)  
				.append(  
				"=L.circleMarker(["  
			)  
				.append(point.latitude)  
				.append(",")  
				.append(point.longitude)  
				.append(  
				"],{" +  
				"radius:7," +  
				"color:'"  
			)  
				.append(color)  
				.append(  
				"',fillColor:'"  
			)  
				.append(color)  
				.append(  
				"',fillOpacity:.9," +  
				"weight:2})" +  
				".addTo(map)" +  
				".bindPopup('"  
			)  
				.append(  
				js(  
					popup.toString()  
				)  
			)  
				.append(  
				"',{maxWidth:280});"  
			);  

			script.append(  
				"historyPoint"  
			)  
				.append(i)  
				.append(  
				".on('click',function(){" +  
				"this.openPopup();" +  
				"});"  
			);  

			script.append(  
				"bounds.push(["  
			)  
				.append(point.latitude)  
				.append(",")  
				.append(point.longitude)  
				.append("]);"  
						);  
		}  
	}  

	@Override  
	protected void onResume() {  

		super.onResume();  

		try {  

			Location gps =  
				locationManager == null  
				? null  
				: locationManager  
				.getLastKnownLocation(  
				LocationManager.GPS_PROVIDER  
			);  

			Location network =  
				locationManager == null  
				? null  
				: locationManager  
				.getLastKnownLocation(  
				LocationManager.NETWORK_PROVIDER  
			);  

			phoneLocation =  
				gps != null  
				? gps  
				: network;  

			if (locationManager != null) {  

				try {  

					locationManager.requestLocationUpdates(  
						LocationManager.GPS_PROVIDER,  
						1000L,  
						1f,  
						locationListener  
					);  

				} catch (SecurityException ignored) {  
				}  

				try {  

					locationManager.requestLocationUpdates(  
						LocationManager.NETWORK_PROVIDER,  
						1000L,  
						1f,  
						locationListener  
					);  

				} catch (SecurityException ignored) {  
				}  
			}  

		} catch (Exception ignored) {  
		}  

		if (sensorManager != null &&  
			rotationSensor != null) {  

			sensorManager.registerListener(  
				this,  
				rotationSensor,  
				SensorManager.SENSOR_DELAY_UI  
			);  
		}  

		updatePhoneMarker();  
	}  

	@Override  
	protected void onPause() {  

		try {  

			if (locationManager != null) {  

				locationManager.removeUpdates(  
					locationListener  
				);  
			}  

		} catch (Exception ignored) {  
		}  

		try {  

			if (sensorManager != null) {  

				sensorManager.unregisterListener(  
					this  
				);  
			}  

		} catch (Exception ignored) {  
		}  

		super.onPause();  
	}  

	@Override  
	public void onSensorChanged(  
		SensorEvent event) {  

		if (event == null ||  
			event.sensor == null) {  

			return;  
		}  

		if (event.sensor.getType() !=  
			Sensor.TYPE_ROTATION_VECTOR) {  

			return;  
		}  

		float[] rotation =  
			new float[9];  

		float[] orientation =  
			new float[3];  

		SensorManager  
			.getRotationMatrixFromVector(  
			rotation,  
			event.values  
		);  

		SensorManager.getOrientation(  
			rotation,  
			orientation  
		);  

		float compass =  
			(float) Math.toDegrees(  
			orientation[0]  
		);  

		if (compass < 0) {  
			compass += 360f;  
		}  

		if (phoneLocation == null ||  
			!phoneLocation.hasSpeed() ||  
			phoneLocation.getSpeed() <= 0.8f) {  

			phoneHeading = compass;  
		}  

		updatePhoneMarker();  
	}  

	@Override  
	public void onAccuracyChanged(  
		Sensor sensor,  
		int accuracy) {  
	}  

	private void updatePhoneMarker() {  

		if (!mapReady ||  
			web == null ||  
			phoneLocation == null) {  

			return;  
		}  

		String javascript =  
			String.format(  
			Locale.US,  
			"javascript:updatePhone(" +  
			"%.7f,%.7f,%.1f)",  
			phoneLocation.getLatitude(),  
			phoneLocation.getLongitude(),  
			phoneHeading  
		);  

		web.loadUrl(javascript);  
	}  

	private void openMap(String uri) {  

		Intent intent =  
			new Intent(  
			Intent.ACTION_VIEW,  
			Uri.parse(uri)  
		);  

		intent.setPackage(  
			"com.google.android.apps.maps"  
		);  

		try {  

			startActivity(intent);  

		} catch (Exception error) {  

			intent.setPackage(null);  

			try {  
				startActivity(intent);  
			} catch (Exception ignored) {  
			}  
		}  
	}  

	private long startOfToday() {  

		Calendar calendar =  
			Calendar.getInstance();  

		calendar.set(  
			Calendar.HOUR_OF_DAY,  
			0  
		);  

		calendar.set(  
			Calendar.MINUTE,  
			0  
		);  

		calendar.set(  
			Calendar.SECOND,  
			0  
		);  

		calendar.set(  
			Calendar.MILLISECOND,  
			0  
		);  

		return calendar.getTimeInMillis();  
	}  

	private String validColor(String value) {  

		if (value != null &&  
			value.matches(  
				"#[0-9a-fA-F]{6}"  
			)) {  

			return value;  
		}  

		return "#E53935";  
	}  

	private String date(long time) {  

		if (time <= 0) {  
			return "Unknown";  
		}  

		SimpleDateFormat format =  
			new SimpleDateFormat(  
			"M/d/yy h:mm:ss a",  
			Locale.US  
		);  

		return format.format(  
			new Date(time)  
		);  
	}  

	private String reason(String value) {  

		if (value == null) {  
			return "Status unknown";  
		}  

		if (value.indexOf("Range") >= 0) {  
			return "Out of range";  
		}  

		if (value.indexOf("Battery") >= 0 ||  
			value.indexOf("Power") >= 0) {  

			return "Possible dead battery";  
		}  

		return value;  
	}  

	private boolean hasValue(String value) {  

		return value != null &&  
			value.trim().length() > 0;  
	}  

	private String js(String value) {  

		if (value == null) {  
			return "";  
		}  

		return value  
			.replace("\\", "\\\\")  
			.replace("'", "\\'")  
			.replace("\"", "\\\"")  
			.replace("\n", " ")  
			.replace("\r", " ");  
	}  

	private String htmlText(String value) {  

		if (value == null) {  
			return "";  
		}  

		return value  
			.replace("&", "&amp;")  
			.replace("<", "&lt;")  
			.replace(">", "&gt;")  
			.replace("'", "&#39;")  
			.replace("\"", "&quot;");  
	}  

	private int dp(int value) {  

		return (int) (  
			value *  
			getResources()  
			.getDisplayMetrics()  
			.density +  
			0.5f  
			);  
	}  

	@Override  
	protected void onDestroy() {

		destroyed = true;

		try {
			mapExecutor.shutdownNow();
		} catch (Exception ignored) {
		}

		try {

			if (web != null) {
				web.stopLoading();  
				web.destroy();  
				web = null;  
			}  

		} catch (Exception ignored) {  
		}  

		try {  

			if (db != null) {  
				db.close();  
				db = null;  
			}  

		} catch (Exception ignored) {  
		}  

		super.onDestroy();  
	}

	}

