diff --git a/app/build.gradle b/app/build.gradle index f024ae8a..25d5ee8b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,12 +4,18 @@ android { compileSdkVersion 27 buildToolsVersion '27.0.3' defaultConfig { - applicationId 'org.emstrack.hospital' - minSdkVersion 16 + applicationId 'org.emstrack.ambulance' + minSdkVersion 17 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + resValue "string", "google_maps_key", + (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "") + vectorDrawables.useSupportLibrary = true + + // add build timestamp + buildConfigField "long", "TIMESTAMP", System.currentTimeMillis() + "L" } buildTypes { debug { @@ -34,15 +40,27 @@ dependencies { androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - compile 'com.android.support:appcompat-v7:27.0.2' + + compile 'com.google.android.gms:play-services-location:15.0.0' + compile 'com.google.android.gms:play-services-maps:15.0.0' + compile 'com.google.android.gms:play-services-gcm:15.0.0' + compile 'com.android.support.constraint:constraint-layout:1.0.2' compile 'com.android.volley:volley:1.0.0' + compile 'com.google.code.gson:gson:2.8.0' + compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0' compile 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' - compile 'com.android.support:recyclerview-v7:27.0.2' - compile 'com.android.support:support-v4:27.0.2' - compile 'com.daimajia.swipelayout:library:1.2.0@aar' + + compile 'com.android.support:cardview-v7:27.1.0' + compile 'com.android.support:recyclerview-v7:27.1.0' + compile 'com.android.support:support-v4:27.1.0' + compile 'com.android.support:appcompat-v7:27.1.0' + compile 'com.android.support:design:27.1.0' + + // compile 'com.daimajia.swipelayout:library:1.2.0@aar' + compile 'com.thoughtbot:expandablerecyclerview:1.3' compile project(':models') compile project(':mqtt') diff --git a/app/src/androidTest/java/org/emstrack/hospital/ExampleInstrumentedTest.java b/app/src/androidTest/java/org/emstrack/ambulance/ExampleInstrumentedTest.java similarity index 95% rename from app/src/androidTest/java/org/emstrack/hospital/ExampleInstrumentedTest.java rename to app/src/androidTest/java/org/emstrack/ambulance/ExampleInstrumentedTest.java index 85a1a14e..d7e6d542 100644 --- a/app/src/androidTest/java/org/emstrack/hospital/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/org/emstrack/ambulance/ExampleInstrumentedTest.java @@ -1,4 +1,4 @@ -package org.emstrack.hospital; +package org.emstrack.ambulance; import android.content.Context; import android.support.test.InstrumentationRegistry; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b94432a0..2d5911cf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,20 +1,24 @@ + package="org.emstrack.ambulance"> + + + + android:launchMode="singleTop" + android:theme="@style/AppTheme"> + - @@ -22,15 +26,43 @@ - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/emstrack/ambulance/AmbulanceApp.java b/app/src/main/java/org/emstrack/ambulance/AmbulanceApp.java new file mode 100644 index 00000000..837bee6a --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/AmbulanceApp.java @@ -0,0 +1,11 @@ +package org.emstrack.ambulance; + +import android.app.Application; + +/** + * Created by mauricio on 2/10/2018. + */ + +public class AmbulanceApp extends Application { + +} diff --git a/app/src/main/java/org/emstrack/ambulance/DispatcherCall.java b/app/src/main/java/org/emstrack/ambulance/DispatcherCall.java new file mode 100644 index 00000000..789184ea --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/DispatcherCall.java @@ -0,0 +1,69 @@ +package org.emstrack.ambulance; + +import android.util.Log; +import org.json.JSONException; +import org.json.JSONObject; + +import static android.content.ContentValues.TAG; + +/** + * Created by sinan on 6/6/2017. + * + * This class is meant to create a call object that is sent from the dispatcher. It will contain + * a lot of information, so having it in its own separate class can save time. + */ + +public class DispatcherCall { + String callString; + String address1 = "No Address Received"; + static String latitude = "32.879409"; + static String longitude = "-117.2382162"; + String description = "No description received"; + + DispatcherCall(JSONObject c) { + try { + //GET EVERY SINGLE PART OF THE JSON OUT THE WAY + int id = c.getInt("id"); + latitude = c.getString("latitude"); + longitude = c.getString("longitude"); + String name = c.getString("name"); + int resU = c.getInt("residential_unit"); + int stmain_number = c.getInt("stmain_number"); + int delegation = c.getInt("delegation"); + int zipcode = c.getInt("zipcode"); + String city = c.getString("city"); + String state = c.getString("state"); + String assignment = c.getString("assignment"); + description = c.getString("description"); + String call_time = c.getString("call_time"); + String departure_time = c.getString("departure_time"); + String transfer_time = c.getString("transfer_time"); + String hospital_time = c.getString("hospital_time"); + String base_time = c.getString("base_time"); + int amb = c.getInt("ambulance"); + + //format to save the address. This is temporary and might be changed based on server team + address1 = stmain_number + " street name pending " + "#" + resU + " " + city + " " + state + " " + zipcode; + + //LOG EVERYTHING JUST TO MAKE SURE OF THIS + Log.d(TAG, "Call message received: and address is " + address1); + } + catch (JSONException e) { Log.d(TAG, "json error"); } + + //save the call + callString = c.toString(); + } + + String getAddress() { + return address1; + } + + public static String getLong() { + return longitude; + } + + public static String getLatitude() { + return latitude; + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/LoginActivity.java b/app/src/main/java/org/emstrack/ambulance/LoginActivity.java new file mode 100644 index 00000000..292b350a --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/LoginActivity.java @@ -0,0 +1,488 @@ +package org.emstrack.ambulance; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.TaskStackBuilder; +import android.content.Intent; +import android.content.IntentSender; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.ResolvableApiException; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.LocationSettingsResponse; +import com.google.android.gms.location.LocationSettingsStatusCodes; +import com.google.android.gms.location.SettingsClient; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; + +import org.emstrack.ambulance.dialogs.AlertSnackbar; +import org.emstrack.ambulance.services.AmbulanceForegroundService; +import org.emstrack.ambulance.services.OnServiceComplete; +import org.emstrack.models.Ambulance; +import org.emstrack.models.AmbulancePermission; +import org.emstrack.models.Profile; +import org.emstrack.mqtt.MqttProfileCallback; +import org.emstrack.mqtt.MqttProfileClient; + +import java.util.List; + +public class LoginActivity extends AppCompatActivity implements View.OnClickListener { + + private static final String TAG = LoginActivity.class.getSimpleName(); + + public static final String LOGOUT = "org.emstrack.ambulance.LoginActivity.LOGOUT"; + + private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34; + private static final int REQUEST_CHECK_SETTINGS = 0x1; + + private SharedPreferences sharedPreferences; + private Button loginSubmitButton; + private TextView usernameField; + private TextView passwordField; + private boolean logout; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + Log.d(TAG, "onCreate"); + + // get action + String action = getIntent().getAction(); + logout = LOGOUT.equals(action); + + // Find username and password from layout + usernameField = (TextView) findViewById(R.id.editUserName); + passwordField = (TextView) findViewById(R.id.editPassword); + + // Retrieving credentials + sharedPreferences = getSharedPreferences(AmbulanceForegroundService.PREFERENCES_NAME, MODE_PRIVATE); + + // Retrieve past credentials + usernameField.setText(sharedPreferences.getString(AmbulanceForegroundService.PREFERENCES_USERNAME, null)); + passwordField.setText(sharedPreferences.getString(AmbulanceForegroundService.PREFERENCES_PASSWORD, null)); + + // Submit button + loginSubmitButton = (Button) findViewById(R.id.buttonLogin); + + // allow keyboard to disappear on screen click + findViewById(R.id.relativeLayout).setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); + return true; + } + }); + + } + + public void disableLogin() { + + // Disable login + loginSubmitButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + // Toast to warn about check permissions + Toast.makeText(LoginActivity.this, "Please be patient. Checking permissions...", Toast.LENGTH_LONG).show(); + + } + }); + + } + + public void enableLogin() { + + Log.d(TAG, "enableLogin"); + + // Enable login + loginSubmitButton.setOnClickListener(this); + + // Logout first? + if (logout) { + + Log.d(TAG,"Logout first."); + + // Stop foreground activity + Intent intent = new Intent(this, AmbulanceForegroundService.class); + intent.setAction(AmbulanceForegroundService.Actions.STOP_SERVICE); + + // What to do when service completes? + new OnServiceComplete(this, + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + intent) { + + @Override + public void onSuccess(Bundle extras) { + Log.i(TAG, "onSuccess"); + + // Set logout to false + logout = false; + + // Initialize service to make sure it gets bound to service + Intent intent = new Intent(LoginActivity.this, AmbulanceForegroundService.class); + intent.putExtra("ADD_STOP_ACTION", true); + intent.setAction(AmbulanceForegroundService.Actions.START_SERVICE); + startService(intent); + + // TODO: is this safe to do asynchronously? + + } + + } + .setFailureMessage(this.getString(R.string.couldNotLogout)) + .setAlert(new AlertSnackbar(this)); + + } else { + + try { + + // Already logged in? + final MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(); + Profile profile = profileClient.getProfile(); + if (profile != null) { + + // Get user info & remove whitespace + final String username = usernameField.getText().toString().trim(); + + // Create intent + Intent intent = new Intent(LoginActivity.this, + MainActivity.class); + + /* + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance != null) { + + Log.i(TAG, "Already logged in with ambulance"); + + intent.putExtra("SKIP_AMBULANCE_SELECTION", true); + + } else { + + Log.i(TAG, "Already logged in without ambulance"); + + } + */ + + Log.i(TAG, "Starting MainActivity"); + + // Toast + Toast.makeText(LoginActivity.this, + getResources().getString(R.string.loginSuccessMessage, username), + Toast.LENGTH_SHORT).show(); + + // initiate MainActivity + startActivity(intent); + + return; + } + + } catch (AmbulanceForegroundService.ProfileClientException e) { + + // Initialize service to make sure it gets bound to service + Intent intent = new Intent(this, AmbulanceForegroundService.class); + intent.putExtra("ADD_STOP_ACTION", true); + intent.setAction(AmbulanceForegroundService.Actions.START_SERVICE); + startService(intent); + + // TODO: is this safe to do asynchronously? + + } + + } + + if (AmbulanceForegroundService.canUpdateLocation()) { + + // Toast to warn about check permissions + Toast.makeText(LoginActivity.this, "Location permissions satisfied.", Toast.LENGTH_LONG).show(); + + } else { + + // Alert to warn about check permissions + new AlertSnackbar(LoginActivity.this) + .alert("Location permissions not satisfied. Expect limited functionality."); + + } + + } + + @Override + public void onResume() { + super.onResume(); + + Log.d(TAG, "onResume"); + + // Disable login + disableLogin(); + + // Can update? + if (AmbulanceForegroundService.canUpdateLocation()) { + + // Enable login + enableLogin(); + + // Check and request permissions to retrieve locations if necessary + } else if (checkPermissions()) { + + // Otherwise check + checkLocationSettings(); + + } else { + + // requestPermissions calls checkLocationSettings if successful + requestPermissions(); + + } + + } + + @Override + public void onClick(View view) { + + // Get user info & remove whitespace + final String username = usernameField.getText().toString().trim(); + final String password = passwordField.getText().toString().trim(); + + if (username.isEmpty()) + new AlertSnackbar(LoginActivity.this).alert(getResources().getString(R.string.error_empty_username)); + + else if (password.isEmpty()) + new AlertSnackbar(LoginActivity.this).alert(getResources().getString(R.string.error_empty_password)); + + else { + + Log.d(TAG, "Will offer credentials"); + + // Login at service + Intent intent = new Intent(LoginActivity.this, AmbulanceForegroundService.class); + intent.setAction(AmbulanceForegroundService.Actions.LOGIN); + intent.putExtra("CREDENTIALS", new String[]{username, password}); + + // What to do when service completes? + new OnServiceComplete(LoginActivity.this, + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + intent) { + + @Override + public void onSuccess(Bundle extras) { + Log.i(TAG, "onClick:OnServiceComplete:onSuccess"); + + // Toast + Toast.makeText(LoginActivity.this, + getResources().getString(R.string.loginSuccessMessage, username), + Toast.LENGTH_SHORT).show(); + + // initiate MainActivity + Intent intent = new Intent(LoginActivity.this, + MainActivity.class); + startActivity(intent); + + } + + } + .setFailureMessage(getResources().getString(R.string.couldNotLoginUser, username)) + .setAlert(new AlertSnackbar(LoginActivity.this)); + + + } + + } + + /** + * Return the current state of the permissions needed. + */ + private boolean checkPermissions() { + int permissionState = ActivityCompat.checkSelfPermission(this, + Manifest.permission.ACCESS_FINE_LOCATION); + return permissionState == PackageManager.PERMISSION_GRANTED; + } + + /** + * Request permission to access fine location + */ + private void requestPermissions() { + + boolean shouldProvideRationale = + ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.ACCESS_FINE_LOCATION); + + // Provide an additional rationale to the user. This would happen if the user denied the + // request previously, but didn't check the "Don't ask again" checkbox. + if (shouldProvideRationale) { + Log.i(TAG, "Displaying permission rationale to provide additional context."); + + new AlertSnackbar(this) + .alert(getString(R.string.permission_rationale), + new View.OnClickListener() { + @Override + public void onClick(View view) { + // Request permission + ActivityCompat.requestPermissions(LoginActivity.this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + REQUEST_PERMISSIONS_REQUEST_CODE); + } + }); + + } else { + Log.i(TAG, "Requesting permission"); + // Request permission. It's possible this can be auto answered if device policy + // sets the permission in a given state or the user denied the permission + // previously and checked "Never ask again". + ActivityCompat.requestPermissions(LoginActivity.this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + REQUEST_PERMISSIONS_REQUEST_CODE); + } + } + + /** + * Callback received when a permissions request has been completed. + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + + Log.i(TAG, "onRequestPermissionResult"); + if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) { + if (grantResults.length <= 0) { + + // If user interaction was interrupted, the permission request is cancelled and you + // receive empty arrays. + Log.i(TAG, "User interaction was cancelled."); + + } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + + // Permission granted + Log.i(TAG, "Permission granted"); + + // Will check location settings + checkLocationSettings(); + + } else { + // Permission denied. + + // Notify the user via a SnackBar that they have rejected a core permission for the + // app, which makes the Activity useless. In a real app, core permissions would + // typically be best requested during a welcome-screen flow. + + // Additionally, it is important to remember that a permission might have been + // rejected without asking the user for permission (device policy or "Never ask + // again" prompts). Therefore, a user interface affordance is typically implemented + // when permissions are denied. Otherwise, your app could appear unresponsive to + // touches or interactions which have required permissions. + new AlertSnackbar(this) + .alert(getString(R.string.permission_denied_explanation), + new View.OnClickListener() { + @Override + public void onClick(View view) { + // Build intent that displays the App settings screen. + Intent intent = new Intent(); + intent.setAction( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", + BuildConfig.APPLICATION_ID, null); + intent.setData(uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + }); + } + } + } + + public void checkLocationSettings() { + + // Build settings client + SettingsClient settingsClient = LocationServices.getSettingsClient(this); + + // Check if the device has the necessary location settings. + settingsClient.checkLocationSettings(AmbulanceForegroundService.getLocationSettingsRequest()) + .addOnSuccessListener(this, new OnSuccessListener() { + @SuppressLint("MissingPermission") + @Override + public void onSuccess(LocationSettingsResponse locationSettingsResponse) { + Log.i(TAG, "All location settings are satisfied."); + + // enable location updates + AmbulanceForegroundService.setCanUpdateLocation(true); + + // enable login + enableLogin(); + + } + }) + .addOnFailureListener(this, new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + int statusCode = ((ApiException) e).getStatusCode(); + switch (statusCode) { + case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: + Log.i(TAG, "Location settings are not satisfied. Attempting to upgrade " + + "location settings "); + try { + // Show the dialog by calling startResolutionForResult(), and check the + // result in onActivityResult(). + ResolvableApiException rae = (ResolvableApiException) e; + rae.startResolutionForResult(LoginActivity.this, REQUEST_CHECK_SETTINGS); + } catch (IntentSender.SendIntentException sie) { + Log.i(TAG, "PendingIntent unable to execute request."); + } + break; + case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: + String errorMessage = "Location settings are inadequate, and cannot be " + + "fixed here. Fix in Settings."; + + new AlertSnackbar(LoginActivity.this) + .alert(errorMessage); + + // disable location updates + AmbulanceForegroundService.setCanUpdateLocation(false); + + // enable login + enableLogin(); + + } + + } + }); + + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + // Check for the integer request code originally supplied to startResolutionForResult(). + case REQUEST_CHECK_SETTINGS: + switch (resultCode) { + case Activity.RESULT_OK: + Log.i(TAG, "User agreed to make required location settings changes."); + break; + case Activity.RESULT_CANCELED: + Log.i(TAG, "User chose not to make required location settings changes."); + break; + } + break; + } + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/MainActivity.java b/app/src/main/java/org/emstrack/ambulance/MainActivity.java new file mode 100644 index 00000000..9470dbf4 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/MainActivity.java @@ -0,0 +1,877 @@ +package org.emstrack.ambulance; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.graphics.PorterDuff; +import android.os.Bundle; +import android.support.design.widget.NavigationView; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v4.view.ViewPager; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.Toast; + +import org.emstrack.ambulance.adapters.FragmentPager; +import org.emstrack.ambulance.dialogs.AboutDialog; +import org.emstrack.ambulance.dialogs.AlertSnackbar; +import org.emstrack.ambulance.dialogs.LogoutDialog; +import org.emstrack.ambulance.fragments.AmbulanceFragment; +import org.emstrack.ambulance.fragments.HospitalFragment; +import org.emstrack.ambulance.fragments.MapFragment; +import org.emstrack.ambulance.services.AmbulanceForegroundService; +import org.emstrack.ambulance.services.OnServiceComplete; +import org.emstrack.ambulance.services.OnServicesComplete; +import org.emstrack.models.Ambulance; +import org.emstrack.models.AmbulancePermission; +import org.emstrack.models.Call; +import org.emstrack.models.Profile; +import org.emstrack.mqtt.MqttProfileClient; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is the main activity -- the default screen + */ +public class MainActivity extends AppCompatActivity { + + private static final String TAG = MainActivity.class.getSimpleName(); + + private static final float enabledAlpha = 1.0f; + private static final float disabledAlpha = 0.25f; + + private ViewPager viewPager; + private FragmentPager adapter; + + private Button ambulanceButton; + List ambulancePermissions; + ArrayAdapter ambulanceListAdapter; + + private DrawerLayout mDrawer; + private NavigationView nvDrawer; + private ActionBarDrawerToggle drawerToggle; + private Toolbar toolbar; + private ImageButton panicButton; + private ImageView onlineIcon; + private ImageView trackingIcon; + private MainActivityBroadcastReceiver receiver; + + public class TrackingClickListener implements View.OnClickListener { + + @Override + public void onClick(View v) { + + if (AmbulanceForegroundService.isUpdatingLocation()) { + + // stop updating location + stopUpdatingLocation(); + + } else { + + // start updating location + startUpdatingLocation(); + + } + } + } + + public class AmbulanceButtonClickListener implements View.OnClickListener { + + @Override + public void onClick(View v) { + + new AlertDialog.Builder( + MainActivity.this) + .setTitle(R.string.selectAmbulance) + .setAdapter(ambulanceListAdapter, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + + AmbulancePermission selectedAmbulance = ambulancePermissions.get(which); + Log.d(TAG, "Selected ambulance " + selectedAmbulance.getAmbulanceIdentifier()); + + // Any ambulance currently selected? + final Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + + // Warn if current ambulance + if (ambulance != null) { + + Log.d(TAG, "Current ambulance " + ambulance.getIdentifier()); + Log.d(TAG, "Requesting location updates? " + + (AmbulanceForegroundService.isUpdatingLocation() ? "TRUE" : "FALSE")); + + // If same ambulance, just return + if (ambulance.getId() == selectedAmbulance.getAmbulanceId()) + return; + + if (AmbulanceForegroundService.isUpdatingLocation()) { + + // confirm first + switchAmbulanceDialog(selectedAmbulance); + return; + } + + } + + // otherwise go ahead! + retrieveAmbulance(selectedAmbulance); + dialog.dismiss(); + + } + }) + .create() + .show(); + } + } + + public class MainActivityBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent ) { + + if (intent != null) { + + final String action = intent.getAction(); + if (action.equals(AmbulanceForegroundService.BroadcastActions.LOCATION_UPDATE_CHANGE)) { + + Log.i(TAG, "LOCATION_UPDATE_CHANGE"); + + if (AmbulanceForegroundService.isUpdatingLocation()) + trackingIcon.setAlpha(enabledAlpha); + else + trackingIcon.setAlpha(disabledAlpha); + + } else if (action.equals(AmbulanceForegroundService.BroadcastActions.CONNECTIVITY_CHANGE)) { + + Log.i(TAG, "CONNECTIVITY_CHANGE"); + + if (AmbulanceForegroundService.isOnline()) + onlineIcon.setAlpha(enabledAlpha); + else + onlineIcon.setAlpha(disabledAlpha); + + } else if (action.equals(AmbulanceForegroundService.BroadcastActions.PROMPT_CALL_ACCEPT)) { + + Log.i(TAG, "PROMPT_CALL_ACCEPT"); + + int callId = intent.getIntExtra("CALL_ID", -1); + acceptCallDialog(callId); + + } else if (action.equals(AmbulanceForegroundService.BroadcastActions.PROMPT_CALL_END)) { + + Log.i(TAG, "PROMPT_CALL_END"); + endCallDialog(); + + } + else if (action.equals(AmbulanceForegroundService.BroadcastActions.CALL_ONGOING)) { + + Log.i(TAG, "CALL_ONGOING"); + + // change button color to red + int myVectorColor = ContextCompat.getColor(MainActivity.this, R.color.colorRed); + trackingIcon.setColorFilter(myVectorColor, PorterDuff.Mode.SRC_IN); + + } else if (action.equals(AmbulanceForegroundService.BroadcastActions.CALL_FINISHED)) { + + Log.i(TAG, "CALL_FINISHED"); + + // change button color to black + int myVectorColor = ContextCompat.getColor(MainActivity.this, R.color.colorBlack); + trackingIcon.setColorFilter(myVectorColor, PorterDuff.Mode.SRC_IN); + + } + } + } + }; + + /** + * @param savedInstanceState + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Ambulance spinner + ambulanceButton = (Button) findViewById(R.id.ambulanceButton); + + // Panic button + panicButton = (ImageButton) findViewById(R.id.panicButton); + panicButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + panicPopUp(); + } + }); + + // Set a Toolbar to replace the ActionBar. + toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + // getSupportActionBar().setDisplayShowTitleEnabled(false); + + // Find our drawer view + mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout); + drawerToggle = setupDrawerToggle(); + mDrawer.addDrawerListener(drawerToggle); + + // set hamburger color to be black + drawerToggle.getDrawerArrowDrawable().setColor(getResources().getColor(R.color.colorBlack)); + + // Find our drawer view + nvDrawer = (NavigationView) findViewById(R.id.nvView); + + // Setup drawer view + setupDrawerContent(nvDrawer); + + // pager + viewPager = (ViewPager) findViewById(R.id.pager); + + // Setup Adapter for tabLayout + adapter = new FragmentPager(getSupportFragmentManager(), + new Fragment[] {new AmbulanceFragment(), new MapFragment(), new HospitalFragment()}, + new CharSequence[] {"Ambulance", "Map", "Hospitals"}); + viewPager.setAdapter(adapter); + + //set up TabLayout Structure + TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout_home); + tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); + tabLayout.setupWithViewPager(viewPager); + tabLayout.getTabAt(0).setIcon(R.drawable.ic_ambulance); + tabLayout.getTabAt(1).setIcon(R.drawable.ic_globe); + tabLayout.getTabAt(2).setIcon(R.drawable.ic_hospital); + + // Online icon + onlineIcon = (ImageView) findViewById(R.id.onlineIcon); + if (AmbulanceForegroundService.isOnline()) + onlineIcon.setAlpha(enabledAlpha); + else + onlineIcon.setAlpha(disabledAlpha); + + // Tracking icon + trackingIcon = (ImageView) findViewById(R.id.trackingIcon); + trackingIcon.setOnClickListener(new TrackingClickListener()); + + if (AmbulanceForegroundService.isUpdatingLocation()) { + trackingIcon.setAlpha(enabledAlpha); + } else { + trackingIcon.setAlpha(disabledAlpha); + } + + // Set up ambulance spinner + try { + + ambulancePermissions = new ArrayList<>(); + MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(); + Profile profile = profileClient.getProfile(); + if (profile != null) + ambulancePermissions = profile.getAmbulances(); + + // Creates string arraylist of ambulance names + ArrayList ambulanceList = new ArrayList<>(); + for (AmbulancePermission ambulancePermission : ambulancePermissions) + ambulanceList.add(ambulancePermission.getAmbulanceIdentifier()); + + // Create the adapter + ambulanceListAdapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_dropdown_item, ambulanceList); + ambulanceListAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + // Set the spinner's adapter + ambulanceButton.setOnClickListener(new AmbulanceButtonClickListener()); + + // Any ambulance currently selected? + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance != null) { + + // Set button + setAmbulanceButtonText(ambulance.getIdentifier()); + + // Automatically attempting to start streaming + Log.i(TAG, "Attempting to start streaming"); + startUpdatingLocation(); + + } else { + + // Invoke ambulance selection + ambulanceButton.performClick(); + + } + + } catch (AmbulanceForegroundService.ProfileClientException e ){ + Log.e(TAG, "Could not retrieve list of ambulances."); + } + + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + drawerToggle.syncState(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + // Pass any configuration change to the drawer toggles + drawerToggle.onConfigurationChanged(newConfig); + + } + + @Override + public void onResume() { + super.onResume(); + + // Update location icon + if (AmbulanceForegroundService.isUpdatingLocation()) + trackingIcon.setAlpha(enabledAlpha); + else + trackingIcon.setAlpha(disabledAlpha); + + // Online icon + onlineIcon = (ImageView) findViewById(R.id.onlineIcon); + if (AmbulanceForegroundService.isOnline()) + onlineIcon.setAlpha(enabledAlpha); + else + onlineIcon.setAlpha(disabledAlpha); + + // Register receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(AmbulanceForegroundService.BroadcastActions.LOCATION_UPDATE_CHANGE); + filter.addAction(AmbulanceForegroundService.BroadcastActions.CONNECTIVITY_CHANGE); + filter.addAction(AmbulanceForegroundService.BroadcastActions.PROMPT_CALL_ACCEPT); + filter.addAction(AmbulanceForegroundService.BroadcastActions.PROMPT_CALL_END); + filter.addAction(AmbulanceForegroundService.BroadcastActions.CALL_ONGOING); + filter.addAction(AmbulanceForegroundService.BroadcastActions.CALL_FINISHED); + receiver = new MainActivityBroadcastReceiver(); + getLocalBroadcastManager().registerReceiver(receiver, filter); + + } + + @Override + public void onPause() { + super.onPause(); + + // Unregister receiver + if (receiver != null) { + getLocalBroadcastManager().unregisterReceiver(receiver); + receiver = null; + } + + } + + public void retrieveAmbulance(final AmbulancePermission selectedAmbulance) { + + // Retrieve ambulance + Intent ambulanceIntent = new Intent(this, AmbulanceForegroundService.class); + ambulanceIntent.setAction(AmbulanceForegroundService.Actions.GET_AMBULANCE); + ambulanceIntent.putExtra("AMBULANCE_ID", selectedAmbulance.getAmbulanceId()); + + // What to do when GET_AMBULANCE service completes? + new OnServiceComplete(this, + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + ambulanceIntent) { + + @Override + public void onSuccess(Bundle extras) { + + // Start MainActivity + setAmbulanceButtonText(selectedAmbulance.getAmbulanceIdentifier()); + + // Start updating + startUpdatingLocation(); + + } + + } + .setFailureMessage(getResources().getString(R.string.couldNotRetrieveAmbulance, + selectedAmbulance.getAmbulanceIdentifier())) + .setAlert(new AlertSnackbar(this)); + + } + + public boolean canWrite() { + + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + + // has ambulance? + if (ambulance == null) + return false; + + // can write? + boolean canWrite = false; + + try { + + final MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(); + for (AmbulancePermission permission : profileClient.getProfile().getAmbulances()) { + if (permission.getAmbulanceId() == ambulance.getId()) { + if (permission.isCanWrite()) { + canWrite = true; + } + break; + } + } + + } catch (AmbulanceForegroundService.ProfileClientException e) { + + /* no need to do anything */ + Log.e(TAG, "Failed to get ambulance read/write permissions."); + + } + + return canWrite; + + } + + /** + * Set ambulance text + * + * @param ambulance the ambulance + */ + public void setAmbulanceButtonText(String ambulance) { + // Set ambulance selection button + ambulanceButton.setText(ambulance); + } + + // Hamburger Menu setup + private ActionBarDrawerToggle setupDrawerToggle() { + return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close); + } + + // Hamburger Menu Listener + private void setupDrawerContent(NavigationView navigationView) { + navigationView.setNavigationItemSelectedListener( + new NavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(MenuItem menuItem) { + selectDrawerItem(menuItem); + return true; + } + }); + } + + // Start selected activity in Hamburger + public void selectDrawerItem(MenuItem menuItem) { + + // Close drawer + mDrawer.closeDrawers(); + + // Get menuitem + int itemId = menuItem.getItemId(); + + // Actions + if (itemId == R.id.logout) { + + LogoutDialog.newInstance(this).show(); + + } else if (itemId == R.id.about) { + + AboutDialog.newInstance(this).show(); + + } else if (itemId == R.id.settings) { + + } + + } + + public void panicPopUp() { + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setCancelable(true); + builder.setTitle("PANIC!"); + builder.setMessage("Message"); + builder.setPositiveButton("Confirm", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + /** + * Get LocalBroadcastManager + * + * @return the LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager() { + return LocalBroadcastManager.getInstance(this); + } + + private void acceptCallDialog(final int callId) { + + Log.i(TAG, "Creating accept dialog"); + + // Gather call details + Call call = AmbulanceForegroundService.getCall(callId); + + if (call == null) { + + Log.d(TAG, "Invalid call/" + callId); + return; + + } + + String callDetails = String.format("Priority: %1$s\n%2$s %3$s, %4$s, %5$s %6$s", + call.getPriority(), + call.getStreet(), call.getNumber(), + call.getCity(), call.getState(), call.getZipcode()); + + // Use the Builder class for convenient dialog construction + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Accept Incoming Call?") + .setMessage(callDetails) + .setPositiveButton("Accept", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Toast.makeText(MainActivity.this, "Call accepted", Toast.LENGTH_SHORT).show(); + + Log.i(TAG, "Call accepted"); + + Intent serviceIntent = new Intent(MainActivity.this, AmbulanceForegroundService.class); + serviceIntent.setAction(AmbulanceForegroundService.Actions.CALL_ACCEPT); + serviceIntent.putExtra("CALL_ID", callId); + startService(serviceIntent); + + } + }) + .setNegativeButton("Decline", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Toast.makeText(MainActivity.this, "Call declined", Toast.LENGTH_SHORT).show(); + + Log.i(TAG, "Call declined"); + + Intent serviceIntent = new Intent(MainActivity.this, AmbulanceForegroundService.class); + serviceIntent.setAction(AmbulanceForegroundService.Actions.CALL_DECLINE); + serviceIntent.putExtra("CALL_ID", callId); + startService(serviceIntent); + + } + }); + + // Create the AlertDialog object and display it + builder.create().show(); + + } + + private void endCallDialog() { + + Log.i(TAG, "Creating end call dialog"); + + // Use the Builder class for convenient dialog construction + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Currently handling call") + .setMessage("What do you want to do?") + .setNegativeButton("Continue", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Toast.makeText(MainActivity.this, "Continuing to handle call", Toast.LENGTH_SHORT).show(); + + Log.i(TAG, "Continuing with call"); + + } + }) + .setNeutralButton("Suspend", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Toast.makeText(MainActivity.this, "Suspending call", Toast.LENGTH_SHORT).show(); + + Log.i(TAG, "Suspending call"); + + } + }) + .setPositiveButton("End", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Toast.makeText(MainActivity.this, "Ending call", Toast.LENGTH_SHORT).show(); + + Log.i(TAG, "Ending call"); + + Intent serviceIntent = new Intent(MainActivity.this, AmbulanceForegroundService.class); + serviceIntent.setAction(AmbulanceForegroundService.Actions.CALL_FINISH); + startService(serviceIntent); + + } + }); + // Create the AlertDialog object and return it + builder.create().show(); + } + + void startUpdatingLocation() { + + // start updating location + + if (canWrite()) { + + // Toast to warn user + Toast.makeText(MainActivity.this, R.string.requestToStreamLocation, + Toast.LENGTH_SHORT).show(); + + + // start updating location + Intent intent = new Intent(MainActivity.this, AmbulanceForegroundService.class); + intent.setAction(AmbulanceForegroundService.Actions.START_LOCATION_UPDATES); + + new OnServiceComplete(MainActivity.this, + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + intent) { + + @Override + public void onSuccess(Bundle extras) { + + // Toast to warn user + Toast.makeText(MainActivity.this, R.string.startedStreamingLocation, + Toast.LENGTH_SHORT).show(); + + } + + @Override + public void onFailure(Bundle extras) { + + // Otherwise ask user if wants to force + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this); + + alertDialogBuilder.setTitle(R.string.alert_warning_title); + alertDialogBuilder.setMessage(R.string.forceLocationUpdates); + + // Cancel button + alertDialogBuilder.setNegativeButton( + R.string.alert_button_negative_text, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // do nothing + } + }); + + // Create the OK button that logs user out + alertDialogBuilder.setPositiveButton( + R.string.alert_button_positive_text, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + Log.i(TAG, "ForceLocationUpdatesDialog: OK Button Clicked"); + + // Toast to warn user + Toast.makeText(MainActivity.this, R.string.forcingLocationUpdates, + Toast.LENGTH_LONG).show(); + + // Reset location_client + String payload = "{\"location_client_id\":\"\"}"; + Intent intent = new Intent(MainActivity.this, AmbulanceForegroundService.class); + intent.setAction(AmbulanceForegroundService.Actions.UPDATE_AMBULANCE); + Bundle bundle = new Bundle(); + bundle.putString("UPDATE", payload); + intent.putExtras(bundle); + + // What to do when service completes? + new OnServicesComplete(MainActivity.this, + new String[]{ + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.AMBULANCE_UPDATE + }, + new String[]{AmbulanceForegroundService.BroadcastActions.FAILURE}, + intent) { + + @Override + public void onSuccess(Bundle extras) { + Log.i(TAG, "onSuccess"); + + // Toast to warn user + Toast.makeText(MainActivity.this, R.string.succeededForcingLocationUpdates, + Toast.LENGTH_LONG).show(); + + // Start updating locations + startUpdatingLocation(); + + } + + @Override + public void onReceive(Context context, Intent intent) { + + // Retrieve action + String action = intent.getAction(); + + // Intercept success + if (action.equals(AmbulanceForegroundService.BroadcastActions.SUCCESS)) + // prevent propagation, still waiting for AMBULANCE_UPDATE + return; + + // Intercept AMBULANCE_UPDATE + if (action.equals(AmbulanceForegroundService.BroadcastActions.AMBULANCE_UPDATE)) + // Inject uuid into AMBULANCE_UPDATE + intent.putExtra(OnServicesComplete.UUID, getUuid()); + + // Call super + super.onReceive(context, intent); + } + + } + .setFailureMessage(getString(R.string.couldNotForceLocationUpdate)) + .setAlert(new AlertSnackbar(MainActivity.this)); + + } + + }); + + alertDialogBuilder.create().show(); + + } + + } + .setFailureMessage(getResources().getString(R.string.anotherClientIsStreamingLocations)) + .setAlert(new AlertSnackbar(MainActivity.this)); + + } else { + + // Toast to warn user + Toast.makeText(MainActivity.this, R.string.cantModifyAmbulance, Toast.LENGTH_LONG).show(); + + } + + } + + void stopUpdatingLocation() { + + // is handling call? + Call currentCall = AmbulanceForegroundService.getCurrentCall(); + if (currentCall != null) { + + Log.d(TAG, "In call: prompt user"); + + // currently handling call, prompt if want to end call + endCallDialog(); + + } else { + + Log.d(TAG, "Not in call: prompt user"); + + endUpdateDialog(); + + } + + } + + void doStopUpdatingLocation() { + + if (canWrite()) { + + // turn off tracking + Intent intent = new Intent(MainActivity.this, AmbulanceForegroundService.class); + intent.setAction(AmbulanceForegroundService.Actions.STOP_LOCATION_UPDATES); + startService(intent); + + // Toast to warn user + Toast.makeText(MainActivity.this, R.string.stopedStreamingLocation, + Toast.LENGTH_SHORT).show(); + + } + + } + + private void endUpdateDialog() { + + Log.i(TAG, "Creating end update dialog"); + + // Use the Builder class for convenient dialog construction + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Stop server updates") + .setMessage("Stoping updates will prevent you from relaying your GPS position, receiving calls or otherwise updating the ambulance status on the server. Do you want to stop server updates?") + .setNegativeButton("No", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Log.i(TAG, "Continue updating"); + + } + }) + .setPositiveButton("Yes", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Toast.makeText(MainActivity.this, "Stopping updates", Toast.LENGTH_SHORT).show(); + + Log.d(TAG, "Stop location updates"); + + // stop updating location + doStopUpdatingLocation(); + + } + }); + // Create the AlertDialog object and return it + builder.create().show(); + + } + + private void switchAmbulanceDialog(final AmbulancePermission newAmbulance) { + + Log.i(TAG, "Creating switch ambulance dialog"); + + // Use the Builder class for convenient dialog construction + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Switch ambulance") + .setMessage(String.format("Do you want to swtich to ambulance %1$s?", newAmbulance.getAmbulanceIdentifier())) + .setNegativeButton("No", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Log.i(TAG, "Continue with same ambulance"); + + } + }) + .setPositiveButton("Yes", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + Log.d(TAG, String.format("Switching to ambulance %1$s", newAmbulance.getAmbulanceIdentifier())); + + // stop updating first + doStopUpdatingLocation(); + + // then retrieve new ambulance + retrieveAmbulance(newAmbulance); + + } + }); + // Create the AlertDialog object and return it + builder.create().show(); + + } + + @Override + public void onBackPressed() { + + LogoutDialog.newInstance(this).show(); + + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/emstrack/ambulance/adapters/FragmentPager.java b/app/src/main/java/org/emstrack/ambulance/adapters/FragmentPager.java new file mode 100644 index 00000000..77cfe7a2 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/adapters/FragmentPager.java @@ -0,0 +1,46 @@ +package org.emstrack.ambulance.adapters; + +import android.content.res.Configuration; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.view.ViewGroup; + +import org.emstrack.ambulance.fragments.AmbulanceFragment; +import org.emstrack.ambulance.fragments.MapFragment; +import org.emstrack.ambulance.fragments.HospitalFragment; + +/** + * Created by mauricio on 2/21/18. + */ + +public class FragmentPager extends FragmentStatePagerAdapter { + + private final CharSequence[] titles; + Fragment[] fragments; + int numberOfTabs; + + public FragmentPager(FragmentManager fm, + Fragment[] fragments, + CharSequence[] titles) { + + super(fm); + + this.fragments = fragments; + this.numberOfTabs = fragments.length; + this.titles = titles; + + } + + @Override + public Fragment getItem(int position) { return (position < numberOfTabs ? fragments[position] : null); } + + @Override + public CharSequence getPageTitle(int position) { return (position < numberOfTabs ? titles[position] : ""); } + + @Override + public int getCount() { + return numberOfTabs; + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/emstrack/ambulance/adapters/HospitalExpandableRecyclerAdapter.java b/app/src/main/java/org/emstrack/ambulance/adapters/HospitalExpandableRecyclerAdapter.java new file mode 100644 index 00000000..259718a6 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/adapters/HospitalExpandableRecyclerAdapter.java @@ -0,0 +1,64 @@ +package org.emstrack.ambulance.adapters; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.thoughtbot.expandablerecyclerview.ExpandableRecyclerViewAdapter; +import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; + +import org.emstrack.ambulance.R; +import org.emstrack.ambulance.fragments.HospitalFragment; +import org.emstrack.ambulance.models.HospitalExpandableGroup; +import org.emstrack.ambulance.views.HospitalEquipmentViewHolder; +import org.emstrack.ambulance.views.HospitalViewHolder; +import org.emstrack.models.Hospital; +import org.emstrack.models.HospitalEquipment; + +import java.util.List; + +/** + * Created by mauricio on 3/11/2018. + */ + +public class HospitalExpandableRecyclerAdapter + extends ExpandableRecyclerViewAdapter { + + private static final String TAG = HospitalExpandableRecyclerAdapter.class.getSimpleName(); + + public HospitalExpandableRecyclerAdapter(List groups) { + super(groups); + } + + @Override + public HospitalViewHolder onCreateGroupViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.hospital_item, parent, false); + return new HospitalViewHolder(view); + } + + @Override + public HospitalEquipmentViewHolder onCreateChildViewHolder(ViewGroup child, int viewType) { + View view = LayoutInflater.from(child.getContext()).inflate(R.layout.hospital_equipment_item, child, false); + return new HospitalEquipmentViewHolder(view); + } + + @Override + public void onBindChildViewHolder(HospitalEquipmentViewHolder holder, int flatPosition, + ExpandableGroup group, int childIndex) { + HospitalEquipment hospitalEquipment = ((HospitalExpandableGroup) group).getItems().get(childIndex); + // Log.d(TAG, "Binding equipment '" + hospitalEquipment + "'"); + + holder.setHospitalEquipment(hospitalEquipment); + } + + @Override + public void onBindGroupViewHolder(HospitalViewHolder holder, int flatPosition, + ExpandableGroup group) { + Hospital hospital = ((HospitalExpandableGroup) group).getHospital(); + // Log.d(TAG, "Binding hospital '" + hospital.getName() + "'"); + + holder.setHospital(hospital); + } +} + diff --git a/app/src/main/java/org/emstrack/ambulance/dialogs/AboutDialog.java b/app/src/main/java/org/emstrack/ambulance/dialogs/AboutDialog.java new file mode 100644 index 00000000..8652b0a3 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/dialogs/AboutDialog.java @@ -0,0 +1,55 @@ +package org.emstrack.ambulance.dialogs; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Build; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import org.emstrack.ambulance.BuildConfig; +import org.emstrack.ambulance.LoginActivity; +import org.emstrack.ambulance.R; +import org.emstrack.ambulance.services.AmbulanceForegroundService; +import org.emstrack.models.Ambulance; + +import java.util.Date; + +/** + * Created by mauricio on 4/26/18. + */ + +public class AboutDialog { + + private static final String TAG = AboutDialog.class.getSimpleName(); + + public static AlertDialog newInstance(final Activity activity) { + + // Inflate the about message contents + View messageView = activity.getLayoutInflater().inflate(R.layout.about, null, false); + + // Set build date + TextView buildDate = messageView.findViewById(R.id.buildDate); + buildDate.setText(new Date(BuildConfig.TIMESTAMP).toString()); + + // Set build version + TextView buildVersion = messageView.findViewById(R.id.buildVersion); + buildVersion.setText(R.string.app_version); + + // Logout dialog + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + builder.setIcon(R.drawable.ambulancelogo); + builder.setTitle(R.string.app_name); + builder.setView(messageView); + + // Create OK button + builder.setPositiveButton(R.string.alert_button_positive_text, null); + + return builder.create(); + + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/dialogs/AlertSnackbar.java b/app/src/main/java/org/emstrack/ambulance/dialogs/AlertSnackbar.java new file mode 100644 index 00000000..2c4820a2 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/dialogs/AlertSnackbar.java @@ -0,0 +1,56 @@ +package org.emstrack.ambulance.dialogs; + +import android.app.Activity; +import android.support.design.widget.Snackbar; +import android.util.Log; +import android.view.View; + +/** + * Created by mauricio on 3/22/2018. + */ + +public class AlertSnackbar { + + private static String TAG = AlertSnackbar.class.getSimpleName(); + + private final View view; + + public AlertSnackbar(String TAG) { + this.TAG = TAG; + this.view = null; + } + + public AlertSnackbar(View view) { + this.view = view.findViewById(android.R.id.content); + } + + public AlertSnackbar(Activity activity) { + this.view = activity.findViewById(android.R.id.content); + } + + public void setTag(String TAG) { + this.TAG = TAG; + } + + public void alert(String message) { + alert(message, new View.OnClickListener() { + @Override + public void onClick(View view) { /* do nothing */ } + }); + } + + public void alert(String message, View.OnClickListener onOkClickAction) { + + // Log message + Log.d(TAG, message); + + // Display alert as snack bar + if (view != null) + Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE) + .setAction(android.R.string.ok, onOkClickAction) + .show(); + + + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/dialogs/LogoutDialog.java b/app/src/main/java/org/emstrack/ambulance/dialogs/LogoutDialog.java new file mode 100644 index 00000000..184f0614 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/dialogs/LogoutDialog.java @@ -0,0 +1,60 @@ +package org.emstrack.ambulance.dialogs; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.support.v7.app.AlertDialog; +import android.util.Log; + +import org.emstrack.ambulance.LoginActivity; +import org.emstrack.ambulance.R; + +/** + * Created by devinhickey on 5/24/17. + */ + +public class LogoutDialog { + + private static final String TAG = LogoutDialog.class.getSimpleName(); + + public static AlertDialog newInstance(final Activity activity) { + + // Logout dialog + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity); + + alertDialogBuilder.setTitle(R.string.alert_warning_title); + alertDialogBuilder.setMessage(R.string.logout_confirm); + + // Cancel button + alertDialogBuilder.setNegativeButton( + R.string.alert_button_negative_text, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + /* do nothing */ + } + }); + + // Create the OK button that logs user out + alertDialogBuilder.setPositiveButton( + R.string.alert_button_positive_text, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + Log.i(TAG,"LogoutDialog: OK Button Clicked"); + + // Start login activity + Intent loginIntent = new Intent(activity, LoginActivity.class); + loginIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_CLEAR_TASK); + loginIntent.setAction(LoginActivity.LOGOUT); + activity.startActivity(loginIntent); + + } + }); + + return alertDialogBuilder.create(); + + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/fragments/AmbulanceFragment.java b/app/src/main/java/org/emstrack/ambulance/fragments/AmbulanceFragment.java new file mode 100644 index 00000000..2e796eb4 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/fragments/AmbulanceFragment.java @@ -0,0 +1,338 @@ +package org.emstrack.ambulance.fragments; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import org.emstrack.ambulance.services.AmbulanceForegroundService; +import org.emstrack.ambulance.MainActivity; +import org.emstrack.ambulance.R; +import org.emstrack.models.Ambulance; +import org.emstrack.mqtt.MqttProfileClient; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +public class AmbulanceFragment extends Fragment implements AdapterView.OnItemSelectedListener { + + private static final String TAG = AmbulanceFragment.class.getSimpleName();; + + private View view; + + private Spinner statusSpinner; + + private TextView latitudeText; + private TextView longitudeText; + private TextView timestampText; + private TextView orientationText; + + private Switch startTrackingSwitch; + + private TextView capabilityText; + + private TextView commentText; + + private TextView updatedOnText; + + private Map ambulanceStatus; + private List ambulanceStatusList; + private ArrayList ambulanceStatusColorList; + private Map ambulanceCapabilities; + private List ambulanceCapabilityList; + + AmbulancesUpdateBroadcastReceiver receiver; + + public class AmbulancesUpdateBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent ) { + if (intent != null) { + final String action = intent.getAction(); + if (action.equals(AmbulanceForegroundService.BroadcastActions.AMBULANCE_UPDATE)) { + + Log.i(TAG, "AMBULANCE_UPDATE"); + update(AmbulanceForegroundService.getAmbulance()); + + } + + } + } + }; + + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + // inflate view + view = inflater.inflate(R.layout.fragment_ambulance, container, false); + + try { + + // Get settings, status and capabilities + final MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(); + + ambulanceStatus = profileClient.getSettings().getAmbulanceStatus(); + ambulanceStatusList = new ArrayList(ambulanceStatus.values()); + Collections.sort(ambulanceStatusList); + + int colors [] = getResources().getIntArray(R.array.statusColors); + ambulanceStatusColorList = new ArrayList(); + for (int i = 0; i < ambulanceStatusList.size(); i++) { + ambulanceStatusColorList.add(colors[i]); + } + + ambulanceCapabilities = profileClient.getSettings().getAmbulanceCapability(); + ambulanceCapabilityList = new ArrayList(ambulanceCapabilities.values()); + Collections.sort(ambulanceCapabilityList); + + } catch (AmbulanceForegroundService.ProfileClientException e) { + + ambulanceStatusList = new ArrayList(); + + } + + // Retrieve location + latitudeText = (TextView) view.findViewById(R.id.latitudeText); + longitudeText = (TextView) view.findViewById(R.id.longitudeText); + timestampText = (TextView) view.findViewById(R.id.timestampText); + orientationText = (TextView) view.findViewById(R.id.orientationText); + + // Other text + capabilityText = (TextView) view.findViewById(R.id.capabilityText); + commentText = (TextView) view.findViewById(R.id.commentText); + updatedOnText = (TextView) view.findViewById(R.id.updatedOnText); + + // Set status spinner + statusSpinner = (Spinner) view.findViewById(R.id.statusSpinner); + // Create an ArrayAdapter using the string array and a default spinner layout + ArrayAdapter statusSpinnerAdapter = + new ArrayAdapter(getContext(), + R.layout.status_spinner_item, + ambulanceStatusList) { + + @Override + public View getView(int pos, View view, ViewGroup parent) + { + Context context = AmbulanceFragment.this.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + view = inflater.inflate(R.layout.status_spinner_dropdown_item, null); + TextView textView = (TextView) view.findViewById(R.id.statusSpinnerDropdownItemText); + textView.setBackgroundColor(ambulanceStatusColorList.get(pos)); + textView.setTextSize(context.getResources().getDimension(R.dimen.statusTextSize)); + textView.setText(ambulanceStatusList.get(pos)); + return view; + } + + @Override + public View getDropDownView(int pos, View view, ViewGroup parent) { + return getView(pos, view, parent); + } + + }; + //statusSpinnerAdapter.setDropDownViewResource(R.layout.status_spinner_dropdown_item); + statusSpinner.setAdapter(statusSpinnerAdapter); + + // Update ambulance + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance != null) { + + update(ambulance); + + } + + // Process change of status + statusSpinner.setOnItemSelectedListener(this); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + // Update ambulance + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance != null) + update(ambulance); + + // Register receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(AmbulanceForegroundService.BroadcastActions.AMBULANCE_UPDATE); + receiver = new AmbulanceFragment.AmbulancesUpdateBroadcastReceiver(); + getLocalBroadcastManager().registerReceiver(receiver, filter); + + } + + @Override + public void onPause() { + super.onPause(); + + // Unregister receiver + if (receiver != null) { + getLocalBroadcastManager().unregisterReceiver(receiver); + receiver = null; + } + + } + + public void update(Ambulance ambulance) { + + // set spinner only if position changed + // this helps to prevent a possible server loop + int position = ambulanceStatusList.indexOf(ambulanceStatus.get(ambulance.getStatus())); + int currentPosition = statusSpinner.getSelectedItemPosition(); + if (currentPosition != position) { + + Log.i(TAG,"Spinner changed from " + currentPosition + " to " + position); + + // set spinner + setSpinner(position); + + } else { + + Log.i(TAG, "Spinner continues to be at position " + position + ". Skipping update"); + + } + + // set identifier + ((MainActivity) getActivity()).setAmbulanceButtonText(ambulance.getIdentifier()); + + // set location + latitudeText.setText(String.format("%.6f", ambulance.getLocation().getLatitude())); + longitudeText.setText(String.format("%.6f", ambulance.getLocation().getLongitude())); + orientationText.setText(String.format("%.1f", ambulance.getOrientation())); + timestampText.setText(ambulance.getTimestamp().toString()); + + // set status and comment + commentText.setText(ambulance.getComment()); + updatedOnText.setText(ambulance.getUpdatedOn().toString()); + + // set capability + capabilityText.setText(ambulanceCapabilities.get(ambulance.getCapability())); + + } + + public void setSpinner(int position) { + + // temporarily disconnect listener to prevent loop + Log.i(TAG, "Suppressing listener"); + statusSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + /* do nothing */ + Log.i(TAG,"Ignoring change in spinner. Position '" + position + "' selected."); + } + + @Override + public void onNothingSelected(AdapterView parent) { + Log.i(TAG,"Ignoring change in spinner. Nothing selected."); + } + }); + + // update spinner + statusSpinner.setSelection(position, false); + + // connect listener + // this is tricky, see + // https://stackoverflow.com/questions/2562248/how-to-keep-onitemselected-from-firing-off-on-a-newly-instantiated-spinner + statusSpinner.post(new Runnable() { + public void run() { + Log.i(TAG, "Restoring listener"); + statusSpinner.setOnItemSelectedListener(AmbulanceFragment.this); + } + }); + + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + + Log.i(TAG, "Item '" + position + "' selected."); + + // Should only update on server as a result of user interaction + // Otherwise this will create a loop with mqtt updating ambulance + // TODO: Debug spinner multiple updates + // This may not be easy with the updates being called from a service + + Log.i(TAG, "Processing status spinner update."); + + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if ((ambulance != null) && !((MainActivity) getActivity()).canWrite()) { + + // Toast to warn user + Toast.makeText(getContext(), R.string.cantModifyAmbulance, Toast.LENGTH_LONG).show(); + + // set spinner + int oldPosition = ambulanceStatusList.indexOf(ambulanceStatus.get(ambulance.getStatus())); + setSpinner(oldPosition); + + // Return + return; + + } + + // Get status from spinner + String status = (String) parent.getItemAtPosition(position); + + // Search for entry in ambulanceStatus map + String statusCode = ""; + for (Map.Entry entry : ambulanceStatus.entrySet()) { + if (status.equals(entry.getValue())) { + statusCode = entry.getKey(); + break; + } + } + + // format timestamp + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + String timestamp = df.format(new Date()); + + // Set update string + String updateString = "{\"status\":\"" + statusCode + "\",\"timestamp\":\"" + timestamp + "\"}"; + + // Update on server + // TODO: Update along with locations because it will be recorded with + // the wrong location on the server + Intent intent = new Intent(getContext(), AmbulanceForegroundService.class); + intent.setAction(AmbulanceForegroundService.Actions.UPDATE_AMBULANCE); + Bundle bundle = new Bundle(); + bundle.putString("UPDATE", updateString); + intent.putExtras(bundle); + getActivity().startService(intent); + + } + + @Override + public void onNothingSelected(AdapterView parent) { + Log.i(TAG, "Nothing selected: this should never happen."); + } + + /** + * Get LocalBroadcastManager + * + * @return the LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager() { + return LocalBroadcastManager.getInstance(getContext()); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/emstrack/ambulance/fragments/HospitalFragment.java b/app/src/main/java/org/emstrack/ambulance/fragments/HospitalFragment.java new file mode 100644 index 00000000..3479ec27 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/fragments/HospitalFragment.java @@ -0,0 +1,173 @@ +package org.emstrack.ambulance.fragments; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.emstrack.ambulance.dialogs.AlertSnackbar; +import org.emstrack.ambulance.services.AmbulanceForegroundService; +import org.emstrack.ambulance.R; +import org.emstrack.ambulance.adapters.HospitalExpandableRecyclerAdapter; +import org.emstrack.ambulance.models.HospitalExpandableGroup; +import org.emstrack.ambulance.services.OnServiceComplete; +import org.emstrack.models.Hospital; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class HospitalFragment extends Fragment { + + private static final String TAG = HospitalFragment.class.getSimpleName(); + + View rootView; + RecyclerView recyclerView; + HospitalsUpdateBroadcastReceiver receiver; + + public class HospitalsUpdateBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent ) { + if (intent != null) { + final String action = intent.getAction(); + if (action.equals(AmbulanceForegroundService.BroadcastActions.HOSPITALS_UPDATE)) { + + Log.i(TAG, "HOSPITALS_UPDATE"); + update(AmbulanceForegroundService.getHospitals()); + + } + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + rootView = inflater.inflate(R.layout.fragment_hospital, container, false); + recyclerView = rootView.findViewById(R.id.recycler_view); + + Map hospitals = AmbulanceForegroundService.getHospitals(); + + // Retrieve hospitals? + if (hospitals == null) { + + // Retrieve hospitals + Intent hospitalsIntent = new Intent(getContext(), AmbulanceForegroundService.class); + hospitalsIntent.setAction(AmbulanceForegroundService.Actions.GET_HOSPITALS); + + // What to do when GET_HOSPITALS service completes? + new OnServiceComplete(getContext(), + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + hospitalsIntent) { + + @Override + public void onSuccess(Bundle extras) { + + Log.i(TAG,"Got all hospitals."); + + // update hospitals + update(AmbulanceForegroundService.getHospitals()); + + } + } + .setFailureMessage(getString(R.string.couldNotRetrieveHospitals)) + .setAlert(new AlertSnackbar(getActivity())); + + } else { + + // Already have hospitals + update(hospitals); + + } + + return rootView; + + } + + @Override + public void onResume() { + super.onResume(); + + // Register receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(AmbulanceForegroundService.BroadcastActions.HOSPITALS_UPDATE); + receiver = new HospitalsUpdateBroadcastReceiver(); + getLocalBroadcastManager().registerReceiver(receiver, filter); + + // update UI + update(AmbulanceForegroundService.getHospitals()); + + } + + @Override + public void onPause() { + super.onPause(); + + // Unregister receiver + if (receiver != null) { + getLocalBroadcastManager().unregisterReceiver(receiver); + receiver = null; + } + + } + + /** + * Update hospital list + * + * @param hospitals list of hospitals + */ + public void update(Map hospitals) { + + // fast return if no hospitals + if (hospitals == null) + return; + + Log.i(TAG,"Updating hospitals UI."); + + // Loop through hospitals + final List hospitalExpandableGroup = new ArrayList(); + + // Loop over all hospitals + for (Map.Entry entry : hospitals.entrySet()) { + + // Get hospital + Hospital hospital = entry.getValue(); + + // Add to to expandable group + hospitalExpandableGroup.add( + new HospitalExpandableGroup(hospital.getName(), + hospital.getHospitalequipmentSet(), + hospital)); + + } + + // Install fragment + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext()); + HospitalExpandableRecyclerAdapter adapter = + new HospitalExpandableRecyclerAdapter(hospitalExpandableGroup); + recyclerView.setLayoutManager(linearLayoutManager); + recyclerView.setAdapter(adapter); + + } + + /** + * Get LocalBroadcastManager + * + * @return the LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager() { + return LocalBroadcastManager.getInstance(getContext()); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/emstrack/ambulance/fragments/MapFragment.java b/app/src/main/java/org/emstrack/ambulance/fragments/MapFragment.java new file mode 100644 index 00000000..f3be5aee --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/fragments/MapFragment.java @@ -0,0 +1,752 @@ +package org.emstrack.ambulance.fragments; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.SupportMapFragment; +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; + +import org.emstrack.ambulance.dialogs.AlertSnackbar; +import org.emstrack.ambulance.services.AmbulanceForegroundService; +import org.emstrack.ambulance.R; +import org.emstrack.ambulance.services.OnServiceComplete; +import org.emstrack.models.Ambulance; +import org.emstrack.models.Hospital; +import org.emstrack.models.Location; +import org.emstrack.mqtt.MqttProfileClient; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import static android.content.ContentValues.TAG; + +// TODO: Implement listener to ambulance changes + +public class MapFragment extends Fragment implements OnMapReadyCallback, GoogleMap.OnCameraIdleListener { + + View rootView; + private Map ambulanceStatus; + private Map ambulanceMarkers; + private HashMap hospitalMarkers; + + private ImageView showLocationButton; + private boolean centerAmbulances = false; + + private ImageView showAmbulancesButton; + private boolean showAmbulances = false; + + private ImageView showHospitalsButton; + private boolean showHospitals = false; + + private boolean myLocationEnabled; + private boolean useMyLocation = false; + + private float defaultZoom = 15; + private int defaultPadding = 50; + + private float zoomLevel = defaultZoom; + + private GoogleMap googleMap; + private AmbulancesUpdateBroadcastReceiver receiver; + private int screenOrientation; + private OrientationEventListener orientationListener; + + static float ROTATIONS[] = { 0.f, 90.f, 180.f, 270.f }; + private Location defaultLocation; + + static int degreesToRotation(int degrees) { + if (degrees > 315 || degrees < 45) + return Surface.ROTATION_0; + else if (degrees > 45 && degrees < 135) + return Surface.ROTATION_90; + else if (degrees > 135 && degrees < 225) + return Surface.ROTATION_180; + else // if (degrees > 225 && degrees < 315) + return Surface.ROTATION_270; + }; + + public class AmbulancesUpdateBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent ) { + if (intent != null) { + final String action = intent.getAction(); + assert action != null; + if (action.equals(AmbulanceForegroundService.BroadcastActions.AMBULANCE_UPDATE)) { + + if (centerAmbulances) { + + Log.i(TAG, "AMBULANCE_UPDATE"); + + // center ambulances + centerAmbulances(); + + } + + } else if (action.equals(AmbulanceForegroundService.BroadcastActions.OTHER_AMBULANCES_UPDATE)) { + + Log.i(TAG, "OTHER_AMBULANCES_UPDATE"); + + // update markers without centering + updateMarkers(); + + } + } + } + } + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + rootView = inflater.inflate(R.layout.fragment_map, container, false); + + // Retrieve location button + showLocationButton = (ImageView) rootView.findViewById(R.id.showLocationButton); + showLocationButton.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + + // toggle show ambulances + centerAmbulances = !centerAmbulances; + + // Switch color + if (centerAmbulances) + showLocationButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOn)); + else + showLocationButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOff)); + + Log.i(TAG, "Toggle center ambulances: " + (centerAmbulances ? "ON" : "OFF")); + + return true; + + } + }); + showLocationButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + if (googleMap != null) { + + // center ambulances + centerAmbulances(); + + } + + } + }); + + // Retrieve ambulance button + showAmbulancesButton = (ImageView) rootView.findViewById(R.id.showAmbulancesButton); + showAmbulancesButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + // toggle show ambulances + showAmbulances = !showAmbulances; + + // Switch color + if (showAmbulances) + showAmbulancesButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOn)); + else + showAmbulancesButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOff)); + + Log.i(TAG, "Toggle show ambulances: " + (showAmbulances ? "ON" : "OFF")); + + if (googleMap != null) { + + if (showAmbulances) + + // retrieve ambulances + retrieveAmbulances(); + + else { + + // forget ambulances + forgetAmbulances(); + + // update markers without centering + updateMarkers(); + + } + + } + + + } + }); + + // Retrieve hospitals button + showHospitalsButton = (ImageView) rootView.findViewById(R.id.showHospitalsButton); + showHospitalsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + // toggle show hospitals + showHospitals = !showHospitals; + + // Switch color + if (showHospitals) + showHospitalsButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOn)); + else + showHospitalsButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOff)); + + Log.i(TAG, "Toggle show hospitals: " + (showHospitals ? "ON" : "OFF")); + + if (googleMap != null) { + + if (!showHospitals) { + + // Clear markers + Iterator> iter = hospitalMarkers.entrySet().iterator(); + while (iter.hasNext()) + { + // get entry + Map.Entry entry = iter.next(); + + // remove from map + entry.getValue().remove(); + + // remove from collection + iter.remove(); + + } + + } + + // update markers without centering + updateMarkers(); + + } + + } + }); + + // Initialize markers maps + ambulanceMarkers = new HashMap<>(); + hospitalMarkers = new HashMap<>(); + + // Get settings, status and capabilities + try { + + final MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(); + ambulanceStatus = profileClient.getSettings().getAmbulanceStatus(); + defaultLocation = profileClient.getSettings().getDefaults().getLocation(); + + } catch (AmbulanceForegroundService.ProfileClientException e) { + + ambulanceStatus = new HashMap(); + defaultLocation = new Location(0,0); + + } + + // Initialize screen rotation + if (rootView.getDisplay() != null) + screenOrientation = rootView.getDisplay().getRotation(); + else + screenOrientation = Surface.ROTATION_0; + + // Initialize map + SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager() + .findFragmentById(R.id.map); + mapFragment.getMapAsync(this); + + // Orientation listener + orientationListener = new OrientationEventListener(getContext(), SensorManager.SENSOR_DELAY_NORMAL) { + + @Override + public void onOrientationChanged(int rotation) { + + Activity activity = getActivity(); + if (activity != null) { + + //Log.d(TAG,"onOrientationChanged"); + if (rotation != ORIENTATION_UNKNOWN) { + screenOrientation = degreesToRotation(rotation); + //Log.d(TAG, "rotation = " + rotation + ", screenOrientation = " + screenOrientation); + } + } + + } + + }; + + // Enable sensor + if (orientationListener.canDetectOrientation()) + orientationListener.enable(); + else + orientationListener.disable(); + + return rootView; + + } + + public void retrieveAmbulances() { + + // Get ambulances + Map ambulances = AmbulanceForegroundService.getOtherAmbulances(); + + if (ambulances == null) { + + Log.i(TAG,"No ambulances."); + + if (showAmbulances) { + + Log.i(TAG,"Retrieving ambulances..."); + + // Disable button + showAmbulancesButton.setEnabled(false); + + // Retrieve ambulances first + Intent ambulancesIntent = new Intent(getContext(), AmbulanceForegroundService.class); + ambulancesIntent.setAction(AmbulanceForegroundService.Actions.GET_AMBULANCES); + + // What to do when GET_AMBULANCES service completes? + new OnServiceComplete(getContext(), + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + ambulancesIntent) { + + @Override + public void onSuccess(Bundle extras) { + + Log.i(TAG,"Got them all. Updating markers."); + + // update markers and center bounds + centerMap(updateMarkers()); + + // Enable button + showAmbulancesButton.setEnabled(true); + + } + } + .setFailureMessage(getString(R.string.couldNotRetrieveAmbulances)) + .setAlert(new AlertSnackbar(getActivity())); + + } + + } else if (showAmbulances) { + + Log.i(TAG,"Already have ambulances. Updating markers."); + + // Already have ambulances + + // update markers and center bounds + centerMap(updateMarkers()); + + } + + } + + public void forgetAmbulances() { + + // disable button + showAmbulancesButton.setEnabled(false); + + // Clear markers + Iterator> iter = ambulanceMarkers.entrySet().iterator(); + while (iter.hasNext()) + { + // get entry + Map.Entry entry = iter.next(); + + // remove from map + entry.getValue().remove(); + + // remove from collection + iter.remove(); + + } + + // Unsubscribe to ambulances + Intent intent = new Intent(getActivity(), AmbulanceForegroundService.class); + intent.setAction(AmbulanceForegroundService.Actions.STOP_AMBULANCES); + + // What to do when service completes? + new OnServiceComplete(getContext(), + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + intent) { + + @Override + public void onSuccess(Bundle extras) { + Log.i(TAG, "onSuccess"); + + showAmbulancesButton.setEnabled(true); + + } + + } + .setFailureMessage(getString(R.string.couldNotUnsubscribeToAmbulances)) + .setAlert(new AlertSnackbar(getActivity())); + + } + + @Override + public void onResume() { + super.onResume(); + + // Enable orientation listener + if (orientationListener.canDetectOrientation()) + orientationListener.enable(); + else + orientationListener.disable(); + + // Register receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(AmbulanceForegroundService.BroadcastActions.OTHER_AMBULANCES_UPDATE); + filter.addAction(AmbulanceForegroundService.BroadcastActions.AMBULANCE_UPDATE); + receiver = new AmbulancesUpdateBroadcastReceiver(); + getLocalBroadcastManager().registerReceiver(receiver, filter); + + // Switch colors + if (showAmbulances) + showAmbulancesButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOn)); + else + showAmbulancesButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOff)); + + if (showHospitals) + showHospitalsButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOn)); + else + showHospitalsButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOff)); + + if (centerAmbulances) + showLocationButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOn)); + else + showLocationButton.setBackgroundColor(getResources().getColor(R.color.mapButtonOff)); + + } + + @Override + public void onPause() { + super.onPause(); + + // Unregister receiver + if (receiver != null) { + getLocalBroadcastManager().unregisterReceiver(receiver); + receiver = null; + } + + // disable orientation listener + orientationListener.disable(); + + } + + @Override + public void onCameraIdle() { + zoomLevel = googleMap.getCameraPosition().zoom; + } + + @Override + public void onMapReady(GoogleMap googleMap) { + + // save map + this.googleMap = googleMap; + + myLocationEnabled = false; + if (AmbulanceForegroundService.canUpdateLocation()) { + + if (useMyLocation) { + + // Use google map's my location + + try { + + Log.i(TAG, "Enable my location on google map."); + googleMap.setMyLocationEnabled(true); + myLocationEnabled = true; + + } catch (SecurityException e) { + Log.i(TAG, "Could not enable my location on google map."); + } + + } + + } + + // Add listener to track zoom + googleMap.setOnCameraIdleListener(this); + + if (showAmbulances) { + + // retrieve ambulances + retrieveAmbulances(); + + } else { + + // Update markers and center map + centerMap(updateMarkers()); + + } + + } + + + + public void centerMap(LatLngBounds bounds) { + + if (ambulanceMarkers.size() > 0) { + + // Move camera + if (ambulanceMarkers.size() == 1) { + + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance != null) { + + Location location = ambulance.getLocation(); + LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); + + googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoomLevel)); + + return; + } + + } else if (bounds != null) { + + // move camera + googleMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, defaultPadding)); + + return; + + } + + } + + // Otherwise center at default location + LatLng latLng = new LatLng(defaultLocation.getLatitude(), defaultLocation.getLongitude()); + + googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoomLevel)); + + } + + public LatLngBounds updateMarkers() { + + // fast return + if (googleMap == null) + return null; + + // Assemble marker bounds + LatLngBounds.Builder builder = new LatLngBounds.Builder(); + + // Update hospitals + if (showHospitals) { + + // Get hospitals + Map hospitals = AmbulanceForegroundService.getHospitals(); + + if (hospitals != null) { + + // Loop over all hospitals + for (Map.Entry entry : hospitals.entrySet()) { + + // Get hospital + Hospital hospital = entry.getValue(); + + // Add marker for hospital + Marker marker = addMarkerForHospital(hospital); + + // Add to bound builder + builder.include(marker.getPosition()); + + } + } + + } + + // Update ambulances + if (showAmbulances) { + + // Get ambulances + Map ambulances = AmbulanceForegroundService.getOtherAmbulances(); + + if (ambulances != null) { + + // Loop over all ambulances + for (Map.Entry entry : ambulances.entrySet()) { + + // Get ambulance + Ambulance ambulance = entry.getValue(); + + // Add marker for ambulance + Marker marker = addMarkerForAmbulance(ambulance); + + // Add to bound builder + builder.include(marker.getPosition()); + + } + } + + } + + // Handle my location? + if (!useMyLocation) { + + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance != null) { + + // Add marker for ambulance + Marker marker = addMarkerForAmbulance(ambulance); + + // Add to bound builder + builder.include(marker.getPosition()); + + } + + } + + // Calculate bounds and return + try { + return builder.build(); + } catch (IllegalStateException e) { + return null; + } + + } + + public Marker addMarkerForAmbulance(Ambulance ambulance) { + + Log.d(TAG,"Adding marker for ambulance " + ambulance.getIdentifier()); + + // Find new location + Location location = ambulance.getLocation(); + LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); + + // Marker exist? + Marker marker; + if (ambulanceMarkers.containsKey(ambulance.getId())) { + + // Update marker + marker = ambulanceMarkers.get(ambulance.getId()); + marker.setPosition(latLng); + marker.setRotation((float) ambulance.getOrientation()); + marker.setSnippet(ambulanceStatus.get(ambulance.getStatus())); + + } else { + + // Create marker + marker = googleMap.addMarker(new MarkerOptions() + .position(latLng) + .icon(bitmapDescriptorFromVector(getActivity(), R.drawable.ambulance_blue, 0.1)) + .anchor(0.5F,0.5F) + .rotation((float) ambulance.getOrientation()) + .flat(true) + .title(ambulance.getIdentifier()) + .snippet(ambulanceStatus.get(ambulance.getStatus()))); + + // Save marker + ambulanceMarkers.put(ambulance.getId(), marker); + + } + + return marker; + } + + public Marker addMarkerForHospital(Hospital hospital) { + + Log.d(TAG,"Adding marker for hospital " + hospital.getName()); + + // Find new location + Location location = hospital.getLocation(); + LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); + + // Marker exist? + Marker marker; + if (hospitalMarkers.containsKey(hospital.getId())) { + + // Update marker + marker = hospitalMarkers.get(hospital.getId()); + marker.setPosition(latLng);; + + } else { + + // Create marker + marker = googleMap.addMarker(new MarkerOptions() + .position(latLng) + .icon(bitmapDescriptorFromVector(getActivity(), R.drawable.ic_hospital_15, 1)) + .anchor(0.5F,0.5F) + .flat(true) + .title(hospital.getName())); + + // Save marker + hospitalMarkers.put(hospital.getId(), marker); + + } + + return marker; + } + + public void centerAmbulances() { + + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance != null) { + + Location location = ambulance.getLocation(); + LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); + + // Add marker for ambulance + Marker marker = addMarkerForAmbulance(ambulance); + + // Center and orient map + CameraPosition currentPlace = new CameraPosition.Builder() + .target(latLng) + .bearing((float) ambulance.getOrientation() + ROTATIONS[screenOrientation]) + .zoom(zoomLevel) + .build(); + googleMap.animateCamera(CameraUpdateFactory.newCameraPosition(currentPlace)); + + } + + } + + /* + * This is from + * https://stackoverflow.com/questions/42365658/custom-marker-in-google-maps-in-android-with-vector-asset-icon + */ + private BitmapDescriptor bitmapDescriptorFromVector(Context context, int vectorResId, double scale) { + Drawable vectorDrawable = ContextCompat.getDrawable(context, vectorResId); + int width = Math.round((float) Math.ceil(vectorDrawable.getIntrinsicWidth()*scale)); + int height = Math.round((float) Math.ceil(vectorDrawable.getIntrinsicHeight()*scale)); + vectorDrawable.setBounds(0, 0, width, height); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + vectorDrawable.draw(canvas); + return BitmapDescriptorFactory.fromBitmap(bitmap); + } + + /** + * Get LocalBroadcastManager + * + * @return the LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager() { + return LocalBroadcastManager.getInstance(getContext()); + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/models/HospitalExpandableGroup.java b/app/src/main/java/org/emstrack/ambulance/models/HospitalExpandableGroup.java new file mode 100644 index 00000000..1e37ee1d --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/models/HospitalExpandableGroup.java @@ -0,0 +1,26 @@ +package org.emstrack.ambulance.models; + +import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup; + +import org.emstrack.models.Hospital; +import org.emstrack.models.HospitalEquipment; + +import java.util.List; + +/** + * Created by mauricio on 3/11/2018. + */ + +public class HospitalExpandableGroup extends ExpandableGroup { + + private Hospital hospital; + + public HospitalExpandableGroup(String title, List items, Hospital hospital) { + super(title, items); + this.hospital = hospital; + } + + public Hospital getHospital() { + return hospital; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/emstrack/ambulance/services/AmbulanceForegroundService.java b/app/src/main/java/org/emstrack/ambulance/services/AmbulanceForegroundService.java new file mode 100644 index 00000000..8dd1749b --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/services/AmbulanceForegroundService.java @@ -0,0 +1,3263 @@ +package org.emstrack.ambulance.services; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.iid.InstanceID; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.GeofencingClient; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.LocationSettingsRequest; +import com.google.android.gms.location.LocationSettingsResponse; +import com.google.android.gms.location.LocationSettingsStatusCodes; +import com.google.android.gms.location.SettingsClient; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import org.eclipse.paho.android.service.MqttAndroidClient; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.emstrack.ambulance.LoginActivity; +import org.emstrack.ambulance.R; +import org.emstrack.ambulance.util.Geofence; +import org.emstrack.ambulance.util.LocationFilter; +import org.emstrack.ambulance.util.LocationUpdate; +import org.emstrack.models.Ambulance; +import org.emstrack.models.AmbulancePermission; +import org.emstrack.models.Call; +import org.emstrack.models.Hospital; +import org.emstrack.models.HospitalPermission; +import org.emstrack.models.Location; +import org.emstrack.mqtt.MqttProfileCallback; +import org.emstrack.mqtt.MqttProfileClient; +import org.emstrack.mqtt.MqttProfileMessageCallback; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by mauricio on 3/18/2018. + */ + +public class AmbulanceForegroundService extends BroadcastService implements MqttProfileCallback { + + final static String TAG = AmbulanceForegroundService.class.getSimpleName(); + + // Rate at which locations should be pulled in + // @ 70 mph gives an accuracy of about 30m + private static final long UPDATE_INTERVAL = 1 * 1000; + private static final long FASTEST_UPDATE_INTERVAL = UPDATE_INTERVAL / 2; + // Group all updates to be done every minute + private static final long MAX_WAIT_TIME = 60 * 1000; + + // Notification channel + public static final int NOTIFICATION_ID = 101; + public static final String PRIMARY_CHANNEL = "default"; + public static final String PRIMARY_CHANNEL_LABEL = "Default channel"; + + // Notification id + private final static AtomicInteger notificationId = new AtomicInteger(NOTIFICATION_ID + 1); + + // SharedPreferences + public static final String PREFERENCES_NAME = "org.emstrack.ambulance"; + public static final String PREFERENCES_USERNAME = "USERNAME"; + public static final String PREFERENCES_PASSWORD = "PASSWORD"; + + private static final String serverUri = "ssl://cruzroja.ucsd.edu:8883"; + + private static MqttProfileClient client; + private static Ambulance _ambulance; + private static Map _hospitals; + private static Map _otherAmbulances; + private static LocationUpdate _lastLocation; + private static Date _lastServerUpdate; + private static boolean _updatingLocation = false; + private static boolean _canUpdateLocation = false; + private static ArrayList _updateBuffer = new ArrayList<>(); + private static boolean _reconnecting = false; + private static boolean _online = false; + private static ReconnectionInformation _reconnectionInformation; + + // hold the callIds of pending calls + private static int currentCallId; + private static Map pendingCalls; + + // Geofences + private final static AtomicInteger geofencesId = new AtomicInteger(1); + private static Map _geofences; + + private static LocationSettingsRequest locationSettingsRequest; + private static LocationRequest locationRequest; + + private NotificationManager notificationManager; + + private FusedLocationProviderClient fusedLocationClient; + private SharedPreferences sharedPreferences; + + public LocationFilter locationFilter = new LocationFilter(null); + + private LocationCallback locationCallback; + private GeofencingClient fenceClient; + private PendingIntent geofenceIntent; + + public class Actions { + public final static String START_SERVICE = "org.emstrack.ambulance.ambulanceforegroundservice.action.START_SERVICE"; + public final static String LOGIN = "org.emstrack.ambulance.ambulanceforegroundservice.action.LOGIN"; + public final static String GET_AMBULANCE = "org.emstrack.ambulance.ambulanceforegroundservice.action.GET_AMBULANCE"; + public final static String GET_AMBULANCES= "org.emstrack.ambulance.ambulanceforegroundservice.action.GET_AMBULANCES"; + public final static String STOP_AMBULANCES= "org.emstrack.ambulance.ambulanceforegroundservice.action.STOP_AMBULANCES"; + public final static String GET_HOSPITALS = "org.emstrack.ambulance.ambulanceforegroundservice.action.GET_HOSPITALS"; + public final static String START_LOCATION_UPDATES = "org.emstrack.ambulance.ambulanceforegroundservice.action.START_LOCATION_UPDATES"; + public final static String STOP_LOCATION_UPDATES = "org.emstrack.ambulance.ambulanceforegroundservice.action.STOP_LOCATION_UPDATES"; + public final static String UPDATE_AMBULANCE = "org.emstrack.ambulance.ambulanceforegroundservice.action.UPDATE_AMBULANCE"; + public final static String UPDATE_NOTIFICATION = "org.emstrack.ambulance.ambulanceforegroundservice.action.UPDATE_NOTIFICATION"; + public final static String LOGOUT = "org.emstrack.ambulance.ambulanceforegroundservice.action.LOGOUT"; + public final static String GEOFENCE_START = "org.emstrack.ambulance.ambulanceforegroundservice.action.GEOFENCE_START"; + public final static String GEOFENCE_STOP = "org.emstrack.ambulance.ambulanceforegroundservice.action.GEOFENCE_STOP"; + public final static String GEOFENCE_ENTER = "org.emstrack.ambulance.ambulanceforegroundservice.action.GEOFENCE_ENTER"; + public final static String GEOFENCE_EXIT = "org.emstrack.ambulance.ambulanceforegroundservice.action.GEOFENCE_EXIT"; + public final static String STOP_SERVICE = "org.emstrack.ambulance.ambulanceforegroundservice.action.STOP_SERVICE"; + public final static String CALL_ACCEPT = "org.emstrack.ambulance.ambulanceforegroundservice.action.CALL_ACCEPT"; + public final static String CALL_DECLINE = "org.emstrack.ambulance.ambulanceforegroundservice.action.CALL_DECLINE"; + public final static String CALL_ONGOING = "org.emstrack.ambulance.ambulanceforegroundservice.action.CALL_ONGOING"; + public final static String CALL_FINISH = "org.emstrack.ambulance.ambulanceforegroundservice.action.CALL_FINISH"; + } + + public class BroadcastExtras { + public final static String MESSAGE = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastextras.MESSAGE"; + public final static String GEOFENCE_TRANSITION = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastextras.GEOFENCE_TRANSTION"; + } + + public class BroadcastActions { + public final static String HOSPITALS_UPDATE = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.HOSPITALS_UPDATE"; + public static final String OTHER_AMBULANCES_UPDATE = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.OTHER_AMBULANCES_UPDATE"; + public final static String AMBULANCE_UPDATE = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.AMBULANCE_UPDATE"; + public final static String LOCATION_UPDATE_CHANGE = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.LOCATION_UPDATE_CHANGE"; + public final static String GEOFENCE_EVENT = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.GEOFENCE_EVENT"; + public final static String CONNECTIVITY_CHANGE = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.CONNECTIVITY_CHANGE"; + public final static String SUCCESS = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.SUCCESS"; + public final static String FAILURE = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.FAILURE"; + public final static String PROMPT_CALL_ACCEPT = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.PROMPT_CALL_ACCEPT"; + public final static String PROMPT_CALL_END = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.PROMPT_CALL_END"; + public static final String CALL_ONGOING = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.CALL_ONGOING"; + public static final String CALL_FINISHED = "org.emstrack.ambulance.ambulanceforegroundservice.broadcastaction.CALL_FINISHED"; + } + + public static class ProfileClientException extends Exception { + + } + + public class ReconnectionInformation { + + private boolean hasAmbulance; + private boolean hasOtherAmbulances; + private boolean hasHospitals; + private boolean isUpdatingLocation; + + public ReconnectionInformation(boolean hasAmbulance, boolean hasOtherAmbulances, + boolean hasHospitals, boolean isUpdatingLocation) { + + this.hasAmbulance = hasAmbulance; + this.hasOtherAmbulances = hasOtherAmbulances; + this.hasHospitals = hasHospitals; + this.isUpdatingLocation = isUpdatingLocation; + + } + + public boolean hasAmbulance() { return hasAmbulance; } + public boolean hasOtherAmbulances() { return hasOtherAmbulances; } + public boolean hasHospitals() { return hasHospitals; } + public boolean isUpdatingLocation() { return isUpdatingLocation; } + + @Override + public String toString() { + return String.format("hasAmbulance = %1$b, hasOtherAmbulances = %2$b, hasHospitals = %3$b, isUpdatingLocation = %4$b", + hasAmbulance, hasOtherAmbulances, hasHospitals, isUpdatingLocation); + } + + } + + public static String getUpdateString(LocationUpdate lastLocation) { + double latitude = lastLocation.getLocation().getLatitude(); + double longitude = lastLocation.getLocation().getLongitude(); + double orientation = lastLocation.getBearing(); + + // format timestamp + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + String timestamp = df.format(lastLocation.getTimestamp()); + + String updateString = "{\"orientation\":" + orientation + ",\"location\":{" + + "\"latitude\":"+ latitude + ",\"longitude\":" + longitude +"},\"timestamp\":\"" + timestamp + "\"}"; + + return updateString; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + + // Create notification channel + if (Build.VERSION.SDK_INT >= 26) { + + NotificationChannel channel = new NotificationChannel(PRIMARY_CHANNEL, + PRIMARY_CHANNEL_LABEL, NotificationManager.IMPORTANCE_DEFAULT); + channel.setLightColor(Color.GREEN); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + getNotificationManager().createNotificationChannel(channel); + + } + + // Create profile client + getProfileClient(this); + + // Create shared preferences editor + sharedPreferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); + + // Initialize fused location client + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); + + // Initialize geofence map + _geofences = new HashMap(); + + // Initialize geofence client + fenceClient = LocationServices.getGeofencingClient(this); + + // initialize list of pending calls + currentCallId = -1; + pendingCalls = new HashMap(); + + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + + // quick return + if (intent == null) { + Log.i(TAG, "null intent, return."); + return START_STICKY; + } + + // retrieve uuid + final String uuid = intent.getStringExtra(OnServiceComplete.UUID); + + if (intent.getAction().equals(Actions.START_SERVICE)) { + + Log.i(TAG, "START_SERVICE Foreground Intent "); + + final boolean addStopAction = intent.getBooleanExtra("ADD_STOP_ACTION", false); + + // Make sure client is bound to service + AmbulanceForegroundService.getProfileClient(this); + + // Create notification + + // Login intent + Intent notificationIntent = new Intent(AmbulanceForegroundService.this, LoginActivity.class); + notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + PendingIntent pendingIntent = PendingIntent.getActivity(AmbulanceForegroundService.this, 0, + notificationIntent, 0); + + // Restart intent + Intent restartServiceIntent = new Intent(AmbulanceForegroundService.this, LoginActivity.class); + restartServiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + restartServiceIntent.setAction(LoginActivity.LOGOUT); + PendingIntent restartServicePendingIntent = PendingIntent.getActivity(AmbulanceForegroundService.this, 0, + restartServiceIntent, 0); + + // Stop intent + Intent stopServiceIntent = new Intent(AmbulanceForegroundService.this, AmbulanceForegroundService.class); + stopServiceIntent.setAction(Actions.STOP_SERVICE); + PendingIntent stopServicePendingIntent = PendingIntent.getService(AmbulanceForegroundService.this, 0, + stopServiceIntent, 0); + + // Icon + Bitmap icon = BitmapFactory.decodeResource(getResources(), + R.mipmap.ic_launcher); + + NotificationCompat.Builder notificationBuilder; + if (Build.VERSION.SDK_INT >= 26) + notificationBuilder = new NotificationCompat.Builder(AmbulanceForegroundService.this, + AmbulanceForegroundService.PRIMARY_CHANNEL); + else + notificationBuilder = new NotificationCompat.Builder(AmbulanceForegroundService.this); + + notificationBuilder + .setContentTitle("EMSTrack") + .setTicker(getString(R.string.pleaseLogin)) + .setContentText(getString(R.string.pleaseLogin)) + .setSmallIcon(R.mipmap.ic_launcher) + .setLargeIcon(Bitmap.createScaledBitmap(icon, 128, 128, false)) + .setContentIntent(pendingIntent) + .setOngoing(true) + .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.restartText), restartServicePendingIntent); + + if (addStopAction) + notificationBuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.stopText), stopServicePendingIntent); + + Notification notification = notificationBuilder.build(); + + // start service + startForeground(NOTIFICATION_ID, notification); + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } else if (intent.getAction().equals(Actions.STOP_SERVICE)) { + + Log.i(TAG, "STOP_SERVICE Foreground Intent"); + + // What to do when logout completes? + new OnServiceComplete(this, + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + null) { + + public void run() { + + // logout + logout(getUuid()); + + } + + @Override + public void onSuccess(Bundle extras) { + + Log.d(TAG, "STOP_SERVICE::onSuccess."); + + // Set online false + setOnline(false); + + // close client + client.close(); + + // set client to null + client = null; + + // stop service + stopForeground(true); + stopSelf(); + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } + + @Override + public void onFailure(Bundle extras) { + + Log.d(TAG, "STOP_SERVICE::onFailure."); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + if (extras != null) + localIntent.putExtras(extras); + sendBroadcastWithUUID(localIntent, uuid); + + } + + }; + + + } else if (intent.getAction().equals(Actions.LOGIN)) { + + Log.i(TAG, "LOGIN Foreground Intent "); + + // Retrieve username and password + String[] loginInfo = intent.getStringArrayExtra("CREDENTIALS"); + final String username = loginInfo[0]; + final String password = loginInfo[1]; + + // Notify user + Toast.makeText(this, "Logging in '" + username + "'", Toast.LENGTH_SHORT).show(); + + // Set online false + setOnline(false); + + // What to do when login completes? + new OnServiceComplete(this, + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + null) { + + public void run() { + + // Login user + login(username, password, getUuid()); + + } + + @Override + public void onSuccess(Bundle extras) { + + // Set online true + setOnline(true); + + // Update notification + updateNotification(getString(R.string.welcomeUser, username)); + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + if (extras != null) + localIntent.putExtras(extras); + sendBroadcastWithUUID(localIntent, uuid); + + } + + @Override + public void onFailure(Bundle extras) { + + // Set online false + setOnline(false); + + // Create notification + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(AmbulanceForegroundService.this, PRIMARY_CHANNEL) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("EMSTrack") + .setContentText(getString(R.string.couldNotLogin, username)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(AmbulanceForegroundService.this); + notificationManager.notify(notificationId.getAndIncrement(), mBuilder.build()); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + if (extras != null) + localIntent.putExtras(extras); + sendBroadcastWithUUID(localIntent, uuid); + + } + + }; + + } else if (intent.getAction().equals(Actions.LOGOUT)) { + + Log.i(TAG, "LOGOUT Foreground Intent"); + + // Set online false + setOnline(false); + + // logout + logout(uuid); + + } else if (intent.getAction().equals(Actions.GET_AMBULANCE)) { + + Log.i(TAG, "GET_AMBULANCE Foreground Intent"); + + // Retrieve ambulance + int ambulanceId = intent.getIntExtra("AMBULANCE_ID", -1); + boolean reconnect = intent.getBooleanExtra("RECONNECT", false); + retrieveAmbulance(ambulanceId, uuid, reconnect); + + } else if (intent.getAction().equals(Actions.GET_AMBULANCES)) { + + Log.i(TAG, "GET_AMBULANCES Foreground Intent"); + + // Retrieve ambulances + boolean reconnect = intent.getBooleanExtra("RECONNECT", false); + retrieveOtherAmbulances(uuid, reconnect); + + } else if (intent.getAction().equals(Actions.STOP_AMBULANCES)) { + + Log.i(TAG, "STOP_AMBULANCES Foreground Intent"); + + // Stop ambulances + stopAmbulances(uuid); + + } else if (intent.getAction().equals(Actions.GET_HOSPITALS)) { + + Log.i(TAG, "GET_HOSPITALS Foreground Intent"); + + // Retrieve hospitals + boolean reconnect = intent.getBooleanExtra("RECONNECT", false); + retrieveHospitals(uuid, reconnect); + + } else if (intent.getAction().equals(Actions.START_LOCATION_UPDATES)) { + + Log.i(TAG, "START_LOCATION_UPDATES Foreground Intent"); + + boolean reconnect = intent.getBooleanExtra("RECONNECT", false); + startLocationUpdates(uuid, reconnect); + + } else if (intent.getAction().equals(Actions.STOP_LOCATION_UPDATES)) { + + Log.i(TAG, "STOP_LOCATION_UPDATES Foreground Intent"); + + // stop call updates + stopCallUpdates(uuid); + + // stop requesting location updates + stopLocationUpdates(uuid); + + } else if (intent.getAction().equals(Actions.UPDATE_AMBULANCE)) { + + Log.i(TAG, "UPDATE_AMBULANCE Foreground Intent"); + + Bundle bundle = intent.getExtras(); + + // Retrieve update string + String update = bundle.getString("UPDATE"); + if (update != null) { + + // update mqtt server + updateAmbulance(update); + + } + + // Retrieve update string array + ArrayList updateArray = bundle.getStringArrayList("UPDATES"); + if (updateArray != null) { + + // update mqtt server + updateAmbulance(updateArray); + + } + + } else if (intent.getAction().equals(Actions.UPDATE_NOTIFICATION)) { + + Log.i(TAG, "UPDATE_NOTIFICATION Foreground Intent"); + + // Retrieve update string + String message = intent.getStringExtra("MESSAGE"); + if (message != null) + updateNotification(message); + + } else if (intent.getAction().equals(Actions.GEOFENCE_START)) { + + Log.i(TAG, "GEOFENCE_START Foreground Intent"); + + // Retrieve latitude and longitude + boolean isHospital = intent.getBooleanExtra("GEOFENCE_TYPE", false); + Float latitude = intent.getFloatExtra("LATITUDE", 0.f); + Float longitude = intent.getFloatExtra("LONGITUDE", 0.f); + Float radius = intent.getFloatExtra("RADIUS", 50.f); + + startGeofence(uuid, new Geofence(new Location(latitude, longitude), radius, isHospital)); + + } else if (intent.getAction().equals(Actions.GEOFENCE_STOP)) { + + Log.i(TAG, "GEOFENCE_STOP Foreground Intent"); + + // Retrieve request ids + String requestId = intent.getStringExtra("REQUESTID"); + List requestIds = new ArrayList(); + requestIds.add(requestId); + + stopGeofence(uuid, requestIds); + + } else if (intent.getAction().equals(Actions.CALL_ACCEPT)) { + + Log.i(TAG, "CALL_ACCEPT Foreground Intent"); + + // get the ambulance that accepted the call and the call id + int callId = intent.getIntExtra("CALL_ID", -1); + + // next steps to publish information to server (steps 3, 4) + setCallStatus(callId, "Accepted", uuid); + + } else if (intent.getAction().equals(Actions.CALL_DECLINE)) { + + Log.i(TAG, "CALL_DECLINE Foreground Intent"); + + // get the ambulance that accepted the call and the call id + int callId = intent.getIntExtra("CALL_ID", -1); + + // next steps to publish information to server (steps 3, 4) + declineCall(callId, uuid); + + } else if (intent.getAction().equals(Actions.CALL_FINISH)) { + + Log.i(TAG, "CALL_FINISH Foreground Intent"); + + // finish call + finishCall(uuid); + + } else if (intent.getAction().equals(Actions.GEOFENCE_ENTER)) { + + Log.i(TAG, "GEOFENCE_ENTER Foreground Intent"); + + // get list of geofence ids that were entered + String[] triggeredGeofences = intent.getStringArrayExtra("TRIGGERED_GEOFENCES"); + + // check if the triggered geofences were hospitals or not + for (String geoId : triggeredGeofences) { + Geofence triggeredGeofence = _geofences.get(geoId); + + if (triggeredGeofence.isHospital()) { + replyToGeofenceTransitions(uuid, true, true); + } else { + replyToGeofenceTransitions(uuid, true, false); + } + } + + // true == entering geofence + //replyToGeofenceTransitions(currentCallId, uuid, true); + + } else if (intent.getAction().equals(Actions.GEOFENCE_EXIT)) { + + Log.i(TAG, "GEOFENCE_EXIT Foreground Intent"); + + // get list of geofence ids that were exited + String[] triggeredGeofences = intent.getStringArrayExtra("TRIGGERED_GEOFENCES"); + + // check if the triggered geofences were hospitals or not + for (String geoId : triggeredGeofences) { + Geofence triggeredGeofence = _geofences.get(geoId); + + if (triggeredGeofence.isHospital()) { + replyToGeofenceTransitions(uuid, false, true); + } else { + replyToGeofenceTransitions(uuid, false, false); + } + } + + // false = not entering geofence + // replyToGeofenceTransitions(currentCallId, uuid, false); + + } else + + Log.i(TAG, "Unknown Intent"); + + return START_STICKY; + } + + /** + * Get the NotificationManager. + * + * @return The system service NotificationManager + */ + private NotificationManager getNotificationManager() { + // lazy initialization + if (notificationManager == null) { + notificationManager = (NotificationManager) this.getSystemService( + Context.NOTIFICATION_SERVICE); + } + return notificationManager; + } + + /** + * Get the MqttProfileClient. + * + * @param context Context + * @return The MqttProfileClient + */ + protected static MqttProfileClient getProfileClient(Context context) { + + // lazy initialization + if (client == null) { + + // TODO: This is deprecated. New Firebase service is buggy and poorly documented. + String clientId = context.getString(R.string.app_version) + + context.getString(R.string.client_name) + + InstanceID.getInstance(context).getId(); + //+ UUID.randomUUID().toString(); + MqttAndroidClient androidClient = new MqttAndroidClient(context, serverUri, clientId); + client = new MqttProfileClient(androidClient); + + } + + return client; + } + + public static MqttProfileClient getProfileClient() throws ProfileClientException { + + // lazy initialization + if (client != null) + return client; + + // otherwise log and throw exception + Log.e(TAG,"Failed to get profile client."); + throw new ProfileClientException(); + + } + + public static boolean hasProfileClient() { return client != null; } + + /** + * Get current ambulance + * + * @return the ambulance + */ + public static Ambulance getAmbulance() { + return _ambulance; + } + + /** + * Convenience method to get current ambulance id. + * Returns -1 if one does not exist. + * + * @return + */ + public static int getAmbulanceId() { + if (_ambulance == null) + return -1; + else + return getAmbulance().getId(); + } + + /** + * @return current call or nul + */ + public static Call getCurrentCall() { + return getCall(currentCallId); + } + + /** + * @param callId + * @return call with id callId or null + */ + public static Call getCall(int callId) { + return pendingCalls.get(callId); + } + + /** + * Get current hospitals + * + * @return the list of hospitals + */ + public static Map getHospitals() { + return _hospitals; + } + + /** + * Get current ambulances + * + * @return the list of ambulances + */ + public static Map getOtherAmbulances() { return _otherAmbulances; } + + /** + * Return true if online + * + * @return true if online + */ + public static boolean isOnline() { return _online; } + + /** + * Set online status + * + * @return true if online + */ + public void setOnline(boolean online) { setOnline(online, this); } + + /** + * Set online status + * + * @return true if online + */ + public static void setOnline(boolean online, Context context) { + + if (context != null && online != _online) { + + // broadcast change of connectivity status + Intent localIntent = new Intent(BroadcastActions.CONNECTIVITY_CHANGE); + localIntent.putExtra("ONLINE", online); + getLocalBroadcastManager(context).sendBroadcast(localIntent); + + } + + _online = online; + + } + + /** + * Return true if reconnecting + * + * @return true if reconnecting + */ + public static boolean isReconnecting() { return _reconnecting; } + + /** + * Return true if requesting location updates + * + * @return the location updates status + */ + public static boolean isUpdatingLocation() { return _updatingLocation; } + + /** + * Return can update location + * + * @return the location update status + */ + public static boolean canUpdateLocation() { return _canUpdateLocation; } + + /** + * Set can update location status + * + * @param canUpdateLocation the location update status + */ + public static void setCanUpdateLocation(boolean canUpdateLocation) { AmbulanceForegroundService._canUpdateLocation = canUpdateLocation; } + + /** + * Return the LocationRequest + * @return the location request + */ + public static LocationRequest getLocationRequest() { + + if (locationRequest == null) { + + // Create request for location updates + locationRequest = LocationRequest.create() + .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) + .setInterval(UPDATE_INTERVAL) + .setFastestInterval(FASTEST_UPDATE_INTERVAL) + .setMaxWaitTime(MAX_WAIT_TIME); + + } + + return locationRequest; + + } + + /** + * Return the LocationSettingsRequest + * + * @return the location settings request + */ + public static LocationSettingsRequest getLocationSettingsRequest() { + + if (locationSettingsRequest == null) { + + // Build location setting request + LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder(); + builder.addLocationRequest(getLocationRequest()); + locationSettingsRequest = builder.build(); + + } + + return locationSettingsRequest; + + } + + /** + * Add update to buffer for later processing + * + * @param update + */ + public void addToBuffer(String update) { + + // buffer updates and return + // TODO: limit size of buffer or write to disk + _updateBuffer.add(update); + + // Log + Log.d(TAG, "MQTT Client is not online. Buffering update."); + + } + + public boolean consumeBuffer() { + + // fast return + if (_updateBuffer.size() == 0) + return true; + + // Get client + final MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + + // Log and add notification + Log.d(TAG, String.format("Attempting to consume update buffer with %1$d entries.", _updateBuffer.size())); + + // Loop through buffer unless it failed + Iterator iterator = _updateBuffer.iterator(); + boolean success = true; + while (success && iterator.hasNext()) { + + // Retrieve update and remove from buffer + String update = iterator.next(); + iterator.remove(); + + // update ambulance + success = updateAmbulance(update); + + } + + return success; + + } + + /** + * Send bulk updates to current ambulance + * Allowing arbitrary updates might be too broad and a security concern + * + * @param updates string array + */ + public boolean updateAmbulance(List updates) { + + ArrayList updateString = new ArrayList<>(); + for (LocationUpdate update : updates) { + + // Set last location + _lastLocation = update; + + // update ambulance string + updateString.add(getUpdateString(update)); + + } + + boolean success = updateAmbulance(updateString); + if (!success) { + + // update locally as well + Ambulance ambulance = getAmbulance(); + if (ambulance != null) { + + // Update ambulance + android.location.Location location = _lastLocation.getLocation(); + ambulance.setLocation(new Location(location.getLatitude(), location.getLongitude())); + ambulance.setOrientation(_lastLocation.getBearing()); + + // Broadcast ambulance update + Intent localIntent = new Intent(BroadcastActions.AMBULANCE_UPDATE); + sendBroadcastWithUUID(localIntent); + + } + + } + + return success; + } + + /** + * Send bulk updates to current ambulance + * Allowing arbitrary updates might be too broad and a security concern + * + * @param updates string array + */ + public boolean updateAmbulance(ArrayList updates) { + + // Join updates in array + String updateArray = "[" + TextUtils.join(",", updates) + "]"; + + // send to server + return updateAmbulance(updateArray); + } + + /** + * Send update to current ambulance + * Allowing arbitrary updates might be too broad and a security concern + * + * @param update string + */ + public boolean updateAmbulance(String update) { + + // Error message + String message = getString(R.string.couldNotUpdateAmbulanceOnServer); + + try { + + // is online or connected? + final MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + if (!isOnline() || !profileClient.isConnected()) { + + // add to buffer and return + addToBuffer(update); + return false; + + } + + // Has ambulance? + Ambulance ambulance = getAmbulance(); + if (ambulance != null ) { + + // Publish current update to MQTT + profileClient.publish(String.format("user/%1$s/client/%2$s/ambulance/%3$d/data", + profileClient.getUsername(), profileClient.getClientId(), ambulance.getId()), + update, 1, false); + + // Set update time + _lastServerUpdate = new Date(); + + return true; + + } else { + + message += "\n" + "Could not find ambulance"; + + } + + } + catch (MqttException e) { message += "\n" + e.toString(); } + catch (Exception e) { message += "\n" + e.toString(); } + + // Log and build a notification in case of error + Log.i(TAG,message); + + // Create notification + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, PRIMARY_CHANNEL) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("EMSTrack") + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(notificationId.getAndIncrement(), mBuilder.build()); + + // and return false + return false; + + } + + /** + * Update current notification message + * + * @param message the message + */ + public void updateNotification(String message) { + + // Create notification + + // Login intent + Intent notificationIntent = new Intent(AmbulanceForegroundService.this, LoginActivity.class); + notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + PendingIntent pendingIntent = PendingIntent.getActivity(AmbulanceForegroundService.this, 0, + notificationIntent, 0); + + // Stop intent + Intent stopServiceIntent = new Intent(AmbulanceForegroundService.this, LoginActivity.class); + stopServiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + stopServiceIntent.setAction(LoginActivity.LOGOUT); + PendingIntent stopServicePendingIntent = PendingIntent.getActivity(AmbulanceForegroundService.this, 0, + stopServiceIntent, 0); + + Notification notification = new NotificationCompat.Builder(this, PRIMARY_CHANNEL) + .setContentTitle("EMSTrack") + .setTicker(message) + .setContentText(message) + .setStyle(new NotificationCompat.BigTextStyle() + .bigText(message)) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentIntent(pendingIntent) + .setOngoing(true) + .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.restartText), stopServicePendingIntent) + .build(); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(NOTIFICATION_ID, notification); + + } + + /** + * Logout + */ + public void logout(final String uuid) { + + // buffer to be consumed? + if (!consumeBuffer()) { + + // could not consume entire buffer, log and empty anyway + Log.e(TAG, "Could not empty buffer before logging out."); + _updateBuffer = new ArrayList<>(); + + } + + // remove ambulance + removeAmbulance(); + + // remove hospital map + removeHospitals(); + + // remove ambulance map + removeOtherAmbulances(); + + // stop geofences + // TODO: Should we wait for completion? + removeGeofences(null); + + // disconnect mqttclient + MqttProfileClient profileClient = getProfileClient(this); + try { + + profileClient.disconnect(new MqttProfileCallback() { + + @Override + public void onReconnect() { + + Log.d(TAG, "onReconnect during disconnect. Should never happen."); + + } + + @Override + public void onSuccess() { + + Log.d(TAG, "Successfully disconnected from broker."); + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } + + @Override + public void onFailure(Throwable exception) { + + Log.d(TAG, "Failed to disconnect from brocker."); + + String message = getString(R.string.failedToDisconnectFromBroker) + "\n"; + if (exception instanceof MqttException) { + int reason = ((MqttException) exception).getReasonCode(); + message += getResources().getString(R.string.error_connection_failed, exception.toString()); + } else { + message += getString(R.string.Exception) + exception.toString(); + } + + Log.d(TAG, "message = " + message); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, message); + sendBroadcastWithUUID(localIntent, uuid); + + } + + }); + + } catch (MqttException e) { + + Log.d(TAG, "Failed to disconnect."); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, "Could not disconnect: " + e.toString()); + sendBroadcastWithUUID(localIntent, uuid); + + } catch (Exception e) { + + Log.d(TAG, "Failed to disconnect."); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, "Could not disconnect: " + e.toString()); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } + + @Override + public void onReconnect() { + + Log.d(TAG, "onReconnect."); + + // Suppress changes in updating location until reconnect is complete + _reconnecting = true; + + // Store reconnection information + _reconnectionInformation = new ReconnectionInformation( + _ambulance != null, + _otherAmbulances != null, + _hospitals != null, + isUpdatingLocation()); + + // Set online false + setOnline(false); + + // Create notification + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, PRIMARY_CHANNEL) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("EMSTrack") + .setContentText(getString(R.string.attemptingToReconnect)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(notificationId.getAndIncrement(), mBuilder.build()); + + } + + /** + * Callback after handling successful connection + */ + @Override + public void onSuccess() { + + Log.d(TAG, "onSuccess after reconnect. Restoring subscriptions."); + + // Set online true + setOnline(true); + + if (_reconnectionInformation == null) { + + Log.e(TAG, "Null reconnection information. Aborting..."); + return; + + } + + Log.d(TAG, "Reconnection information = " + _reconnectionInformation.toString()); + + // Retrieve credentials + String username = sharedPreferences.getString(PREFERENCES_USERNAME, null); + + // Subscribe to error topic + if (username != null) + subscribeToError(username); + + if (_reconnectionInformation.hasAmbulance()) { + + final int ambulanceId = _ambulance.getId(); + final String ambulanceIdentifier = _ambulance.getIdentifier(); + + // Remove current ambulance + // TODO: Does it need to be asynchrounous? + removeAmbulance(true); + + // Retrieve ambulance + Intent ambulanceIntent = new Intent(this, AmbulanceForegroundService.class); + ambulanceIntent.setAction(Actions.GET_AMBULANCE); + ambulanceIntent.putExtra("AMBULANCE_ID", ambulanceId); + ambulanceIntent.putExtra("RECONNECT", true); + + // What to do when GET_AMBULANCE service completes? + new OnServiceComplete(this, + BroadcastActions.AMBULANCE_UPDATE, + BroadcastActions.FAILURE, + ambulanceIntent) { + + @Override + public void onSuccess(Bundle extras) { + + Log.i(TAG, String.format("Subscribed to ambulance %1$s", ambulanceIdentifier)); + + // clear reconnecting flag + _reconnecting = false; + + // Create notification + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(AmbulanceForegroundService.this, PRIMARY_CHANNEL) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("EMSTrack") + .setContentText(getString(R.string.serverIsBackOnline)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(AmbulanceForegroundService.this); + notificationManager.notify(notificationId.getAndIncrement(), mBuilder.build()); + + if (_reconnectionInformation.isUpdatingLocation()) { + + // can stream location? + MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + Ambulance ambulance = getAmbulance(); + String ambulanceLocationClientId = ambulance.getLocationClientId(); + final String clientId = profileClient.getClientId(); + if (clientId.equals(ambulanceLocationClientId)) { + + // ambulance is available, start updates + Log.d(TAG, "Ambulance is available: starting updates."); + + // Start location updates + Intent locationUpdatesIntent = new Intent(AmbulanceForegroundService.this, + AmbulanceForegroundService.class); + locationUpdatesIntent.setAction(Actions.START_LOCATION_UPDATES); + startService(locationUpdatesIntent); + + } else if (ambulanceLocationClientId == null) { + + // ambulance is available, request update + Log.d(TAG, "Ambulance is available: requesting updates."); + + // Update location_client on server + new OnServiceComplete(AmbulanceForegroundService.this, + BroadcastActions.AMBULANCE_UPDATE, + BroadcastActions.FAILURE, + null) { + + public void run() { + + // update ambulance + String payload = String.format("{\"location_client_id\":\"%1$s\"}", clientId); + updateAmbulance(payload); + + } + + @Override + public void onSuccess(Bundle extras) { + + // TODO: Can fail + + // ambulance is available, request update + Log.d(TAG, "Ambulance is available: starting updates."); + + // Start location updates + Intent locationUpdatesIntent = new Intent(AmbulanceForegroundService.this, + AmbulanceForegroundService.class); + locationUpdatesIntent.setAction(Actions.START_LOCATION_UPDATES); + startService(locationUpdatesIntent); + + } + + @Override + public void onReceive(Context context, Intent intent) { + + // Retrieve action + String action = intent.getAction(); + + // Intercept AMBULANCE_UPDATE + if (action.equals(BroadcastActions.AMBULANCE_UPDATE)) + // Inject uuid into AMBULANCE_UPDATE + intent.putExtra(OnServicesComplete.UUID, getUuid()); + + // Call super + super.onReceive(context, intent); + } + + }; + + } else { + + // ambulance is no longer available + // TODO: What to do? + Log.d(TAG, "Ambulance is not longer available for updates."); + + } + + } else { + + // This may not be necessary + // Stop location updates + Intent locationUpdatesIntent = new Intent(AmbulanceForegroundService.this, + AmbulanceForegroundService.class); + locationUpdatesIntent.setAction(Actions.STOP_LOCATION_UPDATES); + startService(locationUpdatesIntent); + + } + + if (_reconnectionInformation.hasOtherAmbulances()) { + + Log.i(TAG, "Subscribing to ambulances."); + + // Remove ambulances + // TODO: Does it need to be asynchrounous? + removeOtherAmbulances(true); + + // Retrieve ambulance + Intent ambulanceIntent = new Intent(AmbulanceForegroundService.this, + AmbulanceForegroundService.class); + ambulanceIntent.setAction(Actions.GET_AMBULANCES); + ambulanceIntent.putExtra("RECONNECT", true); + + } + + if (_reconnectionInformation.hasHospitals()) { + + Log.i(TAG, "Subscribing to hospitals."); + + // Remove hospitals + // TODO: Does it need to be asynchrounous? + removeHospitals(true); + + // Retrieve hospital + Intent hospitalIntent = new Intent(AmbulanceForegroundService.this, + AmbulanceForegroundService.class); + hospitalIntent.setAction(Actions.GET_HOSPITALS); + hospitalIntent.putExtra("RECONNECT", true); + startService(hospitalIntent); + + } + + } + + @Override + public void onFailure(Bundle extras) { + + // clear reconnecting flag + _reconnecting = false; + + // Create notification + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(AmbulanceForegroundService.this, PRIMARY_CHANNEL) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("EMSTrack") + .setContentText(getString(R.string.failedToReconnect)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(AmbulanceForegroundService.this); + notificationManager.notify(notificationId.getAndIncrement(), mBuilder.build()); + + + super.onFailure(extras); + + } + + } + .setFailureMessage(getResources().getString(R.string.couldNotRetrieveAmbulance, + ambulanceIdentifier)); + + } else { + + // clear reconnecting flag + _reconnecting = false; + + } + + } + + /** + * Callback after handling successful connection + * + * @param exception + */ + @Override + public void onFailure(Throwable exception) { + + Log.d(TAG, "onFailure: " + exception); + + if (exception instanceof MqttException) { + + int reason = ((MqttException) exception).getReasonCode(); + + if (reason == MqttException.REASON_CODE_CLIENT_CONNECTED) { + + // Not an error, already connected, just log + Log.d(TAG, "Tried to connect, but already connected."); + + // Set online true + setOnline(true); + + return; + + } + + } + + // Set online false + setOnline(false); + + // Notify user and return + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, PRIMARY_CHANNEL) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(getString(R.string.EMSTrack)) + .setContentText(getString(R.string.serverIsOffline)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + notificationManager.notify(notificationId.getAndIncrement(), mBuilder.build()); + + return; + + } + + + public void subscribeToError(final String username) { + + MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + try { + + profileClient.subscribe(String.format("user/%1$s/client/%2$s/error", + username, profileClient.getClientId()), + 1, new MqttProfileMessageCallback() { + + @Override + public void messageArrived(String topic, MqttMessage message) { + + Log.d(TAG, "MQTT error message."); + + // Create notification + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(AmbulanceForegroundService.this, PRIMARY_CHANNEL) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("EMSTrack") + .setContentText(getString(R.string.serverError, String.valueOf(message.getPayload()))) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(AmbulanceForegroundService.this); + notificationManager.notify(notificationId.getAndIncrement(), mBuilder.build()); + + } + + }); + + } catch (MqttException e1) { + + Log.d(TAG, "Could not subscribe to error topic."); + + } + + } + + + + /** + * Login user + * + * @param username Username + * @param password Password + */ + public void login(final String username, final String password, final String uuid) { + + // What to do when logout completes? + new OnServiceComplete(this, + AmbulanceForegroundService.BroadcastActions.SUCCESS, + AmbulanceForegroundService.BroadcastActions.FAILURE, + null) { + + public void run() { + + // logout + logout(getUuid()); + + } + + @Override + public void onSuccess(Bundle extras) { + + // Retrieve client + final MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + + // Set callback to be called after profile is retrieved + profileClient.setCallback(new MqttProfileCallback() { + + @Override + public void onReconnect() { + + Log.d(TAG, "onReconnect after connection. Could happen."); + // TODO: but I am not sure how to handle it yet. + + } + + @Override + public void onSuccess() { + + // Get preferences editor + SharedPreferences.Editor editor = sharedPreferences.edit(); + + // Save credentials + Log.d(TAG, "Storing credentials"); + editor.putString(PREFERENCES_USERNAME, username); + editor.putString(PREFERENCES_PASSWORD, password); + editor.apply(); + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + // Subscribe to error topic + subscribeToError(username); + + // set callback for handling loss of connection/reconnection + getProfileClient(AmbulanceForegroundService.this).setCallback(AmbulanceForegroundService.this); + + } + + @Override + public void onFailure(Throwable exception) { + + Log.d(TAG, "Failed to retrieve profile."); + + // Build error message + String message = exception.toString(); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, message); + sendBroadcastWithUUID(localIntent, uuid); + + } + + }); + + try { + + // Attempt to connect + profileClient.connect(username, password, new MqttProfileCallback() { + + @Override + public void onReconnect() { + + Log.d(TAG, "onReconnect during connection. Should not happen."); + + } + + @Override + public void onSuccess() { + + Log.d(TAG, "Successfully connected to broker."); + + // Do nothing. All work is done on the callback + + } + + @Override + public void onFailure(Throwable exception) { + + String message = getString(R.string.failedToConnectToBrocker) + "\n"; + + if (exception instanceof MqttException) { + + int reason = ((MqttException) exception).getReasonCode(); + + if (reason == MqttException.REASON_CODE_FAILED_AUTHENTICATION || + reason == MqttException.REASON_CODE_NOT_AUTHORIZED || + reason == MqttException.REASON_CODE_INVALID_CLIENT_ID) + + message += getResources().getString(R.string.error_invalid_credentials); + + else + + message += getResources().getString(R.string.error_connection_failed, exception.toString()); + + } else + message += getString(R.string.Exception) + exception.toString(); + + Log.d(TAG, "message = " + message); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, message); + sendBroadcastWithUUID(localIntent, uuid); + + } + + }); + + } catch (MqttException exception) { + + // Build error message + String message = getResources().getString(R.string.error_connection_failed, exception.toString()); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, message); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } + + @Override + public void onFailure(Bundle extras) { + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + if (extras != null) + localIntent.putExtras(extras); + sendBroadcastWithUUID(localIntent, uuid); + + } + + }; + + } + + /** + * Retrieve ambulance + * + * @param ambulanceId the ambulance id + */ + public void retrieveAmbulance(final int ambulanceId, final String uuid, final boolean reconnect) { + + // Is ambulance id valid? + if (ambulanceId < 0) { + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.invalidAmbulanceId)); + sendBroadcastWithUUID(localIntent, uuid); + + return; + } + + // Is ambulance new and not reconnect? + Ambulance ambulance = getAmbulance(); + if (!reconnect && ambulance != null && ambulance.getId() == ambulanceId) { + return; + } + + // Remove current ambulance + // TODO: Does it need to be asynchrounous? + removeAmbulance(reconnect); + + // Remove current ambulances + // TODO: Does it need to be asynchrounous? + removeOtherAmbulances(reconnect); + + // Retrieve client + MqttProfileClient profileClient = getProfileClient(this); + + try { + + // Publish ambulance login + String topic = String.format("user/%1$s/client/%2$s/ambulance/%3$s/status", + profileClient.getUsername(), profileClient.getClientId(), ambulanceId); + profileClient.publish(topic, "ambulance login",2,true); + + } catch (MqttException e) { + + Log.d(TAG, "Could not login to ambulance"); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotSubscribeToAmbulance)); + sendBroadcastWithUUID(localIntent, uuid); + + } + + try { + + // Start retrieving data + profileClient.subscribe(String.format("ambulance/%1$d/data", ambulanceId), + 1, new MqttProfileMessageCallback() { + + @Override + public void messageArrived(String topic, MqttMessage message) { + + // Keep subscription to ambulance to make sure we receive + // the latest updates. + + Log.d(TAG, "Retrieving ambulance."); + + // First time? + boolean firstTime = (_ambulance == null); + + // parse ambulance data + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + Gson gson = gsonBuilder.create(); + + try { + + // Parse and set ambulance + // TODO: Check for potential errors + Ambulance ambulance = gson + .fromJson(new String(message.getPayload()), + Ambulance.class); + + // Has location been updated? + if (_lastLocation == null || ambulance.getTimestamp().after(_lastLocation.getTimestamp())) { + + // Update last location + _lastLocation = new LocationUpdate(); + android.location.Location location = new android.location.Location("FusedLocationClient"); + location.setLatitude(ambulance.getLocation().getLatitude()); + location.setLongitude(ambulance.getLocation().getLongitude()); + _lastLocation.setLocation(location); + _lastLocation.setBearing(ambulance.getOrientation()); + _lastLocation.setTimestamp(ambulance.getTimestamp()); + + } + + // Set current ambulance + _ambulance = ambulance; + + // stop updates? + MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + String clientId = profileClient.getClientId(); + if (isUpdatingLocation() && + (ambulance.getLocationClientId() == null || + !clientId.equals(ambulance.getLocationClientId()))) { + + // turn off tracking + Intent localIntent = new Intent(AmbulanceForegroundService.this, AmbulanceForegroundService.class); + localIntent.setAction(AmbulanceForegroundService.Actions.STOP_LOCATION_UPDATES); + startService(localIntent); + + } + + if (firstTime) { + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } + + // Broadcast ambulance update + Intent localIntent = new Intent(BroadcastActions.AMBULANCE_UPDATE); + sendBroadcastWithUUID(localIntent, uuid); + + } catch (Exception e) { + + Log.i(TAG, "Could not parse ambulance update."); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotParseAmbulance)); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } + + }); + + } catch (MqttException e) { + + Log.d(TAG, "Could not subscribe to ambulance data"); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotSubscribeToAmbulance)); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } + + /** + * Remove current ambulance + */ + public void removeAmbulance() { removeAmbulance(false); } + + /** + * Remove current ambulance + */ + public void removeAmbulance(boolean reconnect) { + + Ambulance ambulance = getAmbulance(); + if (ambulance == null ) { + Log.i(TAG,"No ambulance to remove."); + return; + } + + // Logout and unsubscribe if not a reconnect + if (!reconnect) { + + // remove call updates + stopCallUpdates(null); + + // remove location updates + stopLocationUpdates(null); + + // get ambulance id + int ambulanceId = ambulance.getId(); + + // Retrieve client + final MqttProfileClient profileClient = getProfileClient(this); + + try { + + // Publish ambulance logout + String topic = String.format("user/%1$s/client/%2$s/ambulance/%3$s/status", + profileClient.getUsername(), profileClient.getClientId(), ambulanceId); + profileClient.publish(topic, "ambulance logout", 2, true); + + } catch (MqttException e) { + Log.d(TAG, "Could not logout from ambulance"); + } + + try { + + // Unsubscribe to ambulance data + profileClient.unsubscribe("ambulance/" + ambulanceId + "/data"); + + // TODO: unsubscribe from statuses and call data + + } catch (MqttException exception) { + Log.d(TAG, "Could not unsubscribe to 'ambulance/" + ambulanceId + "/data'"); + } + + + // Remove ambulance + _ambulance = null; + + } + + } + + /** + * Retrieve hospitals + */ + public void retrieveHospitals(final String uuid, boolean reconnect) { + + // Remove current hospital map + // TODO: Does it need to be asynchrounous? + removeHospitals(reconnect); + + // Retrieve hospital data + final MqttProfileClient profileClient = getProfileClient(this); + + // Get list of hospitals + final List hospitalPermissions = profileClient.getProfile().getHospitals(); + + // Initialize hospitals + final Map hospitals = new HashMap<>(); + + // Loop over all hospitals + for (HospitalPermission hospitalPermission : hospitalPermissions) { + + final int hospitalId = hospitalPermission.getHospitalId(); + + try { + + // Start retrieving data + profileClient.subscribe("hospital/" + hospitalId + "/data", + 1, new MqttProfileMessageCallback() { + + @Override + public void messageArrived(String topic, MqttMessage message) { + + // first time? + boolean firstTime = (_hospitals == null); + + // Parse hospital + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + Gson gson = gsonBuilder.create(); + + // Found hospital + final Hospital hospital = gson.fromJson(message.toString(), Hospital.class); + + if (firstTime) { + + // Add to hospital map + hospitals.put(hospitalId, hospital); + + // Done yet? + if (hospitals.size() == hospitalPermissions.size()) { + + Log.d(TAG, "Done retrieving all hospitals"); + + // set _hospitals + _hospitals = hospitals; + + // Broadcast hospitals update + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } else { + + // Modify hospital map + hospitals.put(hospitalId, hospital); + + // Broadcast hospitals update + Intent localIntent = new Intent(BroadcastActions.HOSPITALS_UPDATE); + sendBroadcastWithUUID(localIntent); + + } + } + }); + + } catch (MqttException e) { + Log.d(TAG, "Could not subscribe to hospital data"); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotSubscribeToHospital)); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } + + } + + /** + * Remove current hospitals + */ + public void removeHospitals() { removeHospitals(false); } + + /** + * Remove current hospitals + */ + public void removeHospitals(boolean reconnect) { + + Map hospitals = getHospitals(); + if (hospitals == null || hospitals.size() == 0) { + Log.i(TAG, "No hospital to remove."); + return; + } + + // Unsubscribe only if not reconnect + if (!reconnect) { + + // Retrieve client + final MqttProfileClient profileClient = getProfileClient(this); + + // Loop over all hospitals + for (Map.Entry entry : hospitals.entrySet()) { + + // Get hospital + Hospital hospital = entry.getValue(); + + try { + + // Unsubscribe to hospital data + profileClient.unsubscribe("hospital/" + hospital.getId() + "/data"); + + } catch (MqttException exception) { + Log.d(TAG, "Could not unsubscribe to 'hospital/" + hospital.getId() + "/data'"); + } + + } + + // Remove hospitals + _hospitals = null; + + } + + } + + /** + * Retrieve ambulances + */ + public void retrieveOtherAmbulances(final String uuid, boolean reconnect) { + + // Remove current ambulance map + // TODO: Does it need to be asynchrounous? + removeOtherAmbulances(reconnect); + + // Retrieve ambulance data + final MqttProfileClient profileClient = getProfileClient(this); + + // Get list of ambulances + final List ambulancePermissions = profileClient.getProfile().getAmbulances(); + + // Initialize ambulances + final Map ambulances = new HashMap<>(); + + // Current ambulance id + int currentAmbulanceId = getAmbulanceId(); + + // Loop over all ambulances + for (AmbulancePermission ambulancePermission : ambulancePermissions) { + + final int ambulanceId = ambulancePermission.getAmbulanceId(); + + // Skip if current ambulance + if (ambulanceId == currentAmbulanceId) + continue; + + try { + + // Start retrieving data + profileClient.subscribe("ambulance/" + ambulanceId + "/data", + 1, new MqttProfileMessageCallback() { + + @Override + public void messageArrived(String topic, MqttMessage message) { + + // first time? + boolean firstTime = (_otherAmbulances == null); + + // Parse ambulance + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + Gson gson = gsonBuilder.create(); + + // Found ambulance + final Ambulance ambulance = gson.fromJson(message.toString(), Ambulance.class); + + if (firstTime) { + + // Add to ambulance map + ambulances.put(ambulanceId, ambulance); + + // Done yet? + if (ambulances.size() == + (ambulancePermissions.size() + (getAmbulanceId() == -1 ? 0 : - 1))) { + + Log.d(TAG, "Done retrieving all ambulances"); + + // set _otherAmbulances + _otherAmbulances = ambulances; + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } else { + + // Update ambulance map + ambulances.put(ambulanceId, ambulance); + + // Broadcast ambulances update + Intent localIntent = new Intent(BroadcastActions.OTHER_AMBULANCES_UPDATE); + sendBroadcastWithUUID(localIntent); + + } + } + }); + + } catch (MqttException e) { + Log.d(TAG, "Could not subscribe to ambulance data"); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotSubscribeToAmbulance)); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } + + } + + /** + * Remove current ambulances + */ + public void removeOtherAmbulances() { removeOtherAmbulances(false); } + + /** + * Remove current ambulances + */ + public void removeOtherAmbulances(boolean reconnect) { + + Map ambulances = getOtherAmbulances(); + if (ambulances == null || ambulances.size() == 0) { + Log.i(TAG, "No ambulances to remove."); + return; + } + + // Remove subscriptions if not reconnect + if (!reconnect) { + + // Retrieve client + final MqttProfileClient profileClient = getProfileClient(this); + + // Loop over all ambulances + for (Map.Entry entry : ambulances.entrySet()) { + + // Get ambulance + Ambulance ambulance = entry.getValue(); + + try { + + // Unsubscribe to ambulance data + profileClient.unsubscribe("ambulance/" + ambulance.getId() + "/data"); + + } catch (MqttException exception) { + Log.d(TAG, "Could not unsubscribe to 'ambulance/" + ambulance.getId() + "/data'"); + } + + } + + // Remove ambulances + _otherAmbulances = null; + + } + + } + + /** + * Stop subscribing to ambulances + */ + public void stopAmbulances(final String uuid) { + + // Remove current ambulance map + // TODO: Does it need to be asynchrounous? + removeOtherAmbulances(); + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } + + private void startLocationUpdates(final String uuid, final boolean reconnect) { + + // Already started? + if (_updatingLocation) { + Log.i(TAG, "Already requesting location updates. Skipping."); + + Log.i(TAG, "Consume buffer."); + consumeBuffer(); + + // Broadcast success and return + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + + if (!canUpdateLocation()) { + + // Broadcast failure and return + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.cannotUseLocationServices)); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + + // Logged in? + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance == null) { + + // Broadcast failure and return + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.noAmbulanceSelected)); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + + // is location_client available? + final MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(this); + + // can stream location? + String ambulanceLocationClientId = ambulance.getLocationClientId(); + String clientId = profileClient.getClientId(); + if (clientId.equals(ambulanceLocationClientId)) { + + // ambulance is available, start updates + + // Create settings client + SettingsClient settingsClient = LocationServices.getSettingsClient(this); + + // Check if the device has the necessary location settings. + settingsClient.checkLocationSettings(getLocationSettingsRequest()) + .addOnSuccessListener(new OnSuccessListener() { + @SuppressLint("MissingPermission") + @Override + public void onSuccess(LocationSettingsResponse locationSettingsResponse) { + + Log.i(TAG, "All location settings are satisfied."); + + Log.i(TAG, "Consume buffer."); + consumeBuffer(); + + Log.i(TAG, "Starting location updates."); + beginLocationUpdates(uuid); + + Log.i(TAG, "Starting call updates."); + beginCallUpdates(uuid); + + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + int statusCode = ((ApiException) e).getStatusCode(); + String message = getString(R.string.inadequateLocationSettings); + switch (statusCode) { + case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: + message += "Try restarting app."; + break; + case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: + message += "Please fix in Settings."; + } + Log.e(TAG, message); + + // Broadcast failure and return + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, message); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + }); + + + } else if (ambulanceLocationClientId == null) { + + // ambulance is available, request update + + // Update location_client on server, listening to updates already + String payload = String.format("{\"location_client_id\":\"%1$s\"}", clientId); + Intent intent = new Intent(this, AmbulanceForegroundService.class); + intent.setAction(Actions.UPDATE_AMBULANCE); + Bundle bundle = new Bundle(); + bundle.putString("UPDATE", payload); + intent.putExtras(bundle); + + // What to do when service completes? + new OnServicesComplete(this, + new String[] { + BroadcastActions.SUCCESS, + BroadcastActions.AMBULANCE_UPDATE + }, + new String[] {BroadcastActions.FAILURE}, + intent) { + + @Override + public void onSuccess(Bundle extras) { + + Log.i(TAG, "onSuccess"); + + // Try again with right permissions + // TODO: This could potentially lead to a loop, add counter to prevent infinite recursion + startLocationUpdates(uuid, reconnect); + + } + + @Override + public void onFailure(Bundle extras) { + + // Broadcast failure + String message = extras.getString(BroadcastExtras.MESSAGE); + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, message); + sendBroadcastWithUUID(localIntent, uuid); + + } + + @Override + public void onReceive(Context context, Intent intent) { + + // Retrieve action + String action = intent.getAction(); + + // Intercept success + if (action.equals(BroadcastActions.SUCCESS)) + // prevent propagation, still waiting for AMBULANCE_UPDATE + return; + + // Intercept AMBULANCE_UPDATE + if (action.equals(BroadcastActions.AMBULANCE_UPDATE)) + // Inject uuid into AMBULANCE_UPDATE + intent.putExtra(OnServicesComplete.UUID, getUuid()); + + // Call super + super.onReceive(context, intent); + } + + }; + + } else { + + // ambulance is not available, report failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.anotherClientReporting)); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + + } + + /** + * Handles the Request Updates button and requests start of location updates. + */ + public void beginLocationUpdates(final String uuid) { + + // Create location callback + locationCallback = new LocationCallback() { + + @Override + public void onLocationResult(LocationResult result) { + + // get profile client + final MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(AmbulanceForegroundService.this); + + // Retrieve results + if (result != null) { + + List locations = result.getLocations(); + Log.i(TAG, "Received " + locations.size() + " location updates"); + + // Initialize locationFilter + if (_lastLocation != null) + locationFilter.setLocation(_lastLocation); + + // Filter location + List filteredLocations = locationFilter.update(locations); + + // Publish update + if (filteredLocations.size() > 0) + updateAmbulance(filteredLocations); + + // Notification message + // TODO: These need to be internationalized but cannot be retrieved without a context + String message = "Last location update at " + + new SimpleDateFormat("d MMM HH:mm:ss z", Locale.getDefault()).format(new Date()); + + if (_lastServerUpdate != null) + message += "\nLast server update at " + + new SimpleDateFormat("d MMM HH:mm:ss z", Locale.getDefault()).format(_lastServerUpdate); + + if (isOnline()) + message += "\nServer is online"; + else + message += "\nServer is offline"; + + if (_updateBuffer.size() > 1) + message += ", " + String.format("%1$d messages on buffer", _updateBuffer.size()); + else if (_updateBuffer.size() > 0) + message += ", " + String.format("1 message on buffer"); + + // modify foreground service notification + Intent notificationIntent = new Intent(AmbulanceForegroundService.this, AmbulanceForegroundService.class); + notificationIntent.setAction(AmbulanceForegroundService.Actions.UPDATE_NOTIFICATION); + notificationIntent.putExtra("MESSAGE", message); + startService(notificationIntent); + + } + + } + + }; + + try { + + fusedLocationClient.requestLocationUpdates(getLocationRequest(), locationCallback, null) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void aVoid) { + + Log.i(TAG, "Starting location updates"); + _updatingLocation = true; + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + // Broadcast location change + Intent changeIntent = new Intent(BroadcastActions.LOCATION_UPDATE_CHANGE); + sendBroadcastWithUUID(changeIntent, uuid); + + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.i(TAG, "Failed to start location updates"); + e.printStackTrace(); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.failedToStartLocationUpdates)); + sendBroadcastWithUUID(localIntent, uuid); + + } + }); + + } catch (SecurityException e) { + Log.i(TAG, "Failed to start location updates"); + e.printStackTrace(); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.failedToStartLocationUpdates)); + sendBroadcastWithUUID(localIntent, uuid); + + } + } + + /** + * Handles the Remove Updates button, and requests removal of location updates. + */ + public void stopLocationUpdates(final String uuid) { + + _lastLocation = null; + + // Already started? + if (!_updatingLocation) { + Log.i(TAG, "Not requesting location updates. Skipping."); + return; + } + + // get profile client + MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(this); + + // clear location_client on server? + Ambulance ambulance = getAmbulance(); + if (ambulance != null && profileClient != null && + profileClient.getClientId().equals(ambulance.getLocationClientId())) { + + Log.i(TAG, "Will clear location client on server"); + + // clear location_client on server + String payload = "{\"location_client_id\":\"\"}"; + + // Update location_client on server, listening to updates already + Intent intent = new Intent(this, AmbulanceForegroundService.class); + intent.setAction(AmbulanceForegroundService.Actions.UPDATE_AMBULANCE); + Bundle bundle = new Bundle(); + bundle.putString("UPDATE", payload); + intent.putExtras(bundle); + startService(intent); + + } else { + + Log.i(TAG, "No need to clear location client on server"); + + } + + // remove on fusedLocationclient + fusedLocationClient.removeLocationUpdates(locationCallback) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void aVoid) { + + Log.i(TAG, "Stopping location updates"); + _updatingLocation = false; + + // Broadcast location change + Intent changeIntent = new Intent(BroadcastActions.LOCATION_UPDATE_CHANGE); + sendBroadcastWithUUID(changeIntent, uuid); + + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.i(TAG, "Failed to stop location updates"); + e.printStackTrace(); + } + }); + + } + + /** + * Begin call updates + * + * @param uuid + */ + public void beginCallUpdates(final String uuid) { + + Log.i(TAG, "Subscribing to call status"); + + // Logged in? + Ambulance ambulance = AmbulanceForegroundService.getAmbulance(); + if (ambulance == null) { + + // Broadcast failure and return + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.noAmbulanceSelected)); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + + // get profile client + MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(this); + + // subscribe to call status + try { + + final String clientId = profileClient.getClientId(); + + profileClient.subscribe(String.format("ambulance/%1$d/call/+/status", ambulance.getId()), + 2, new MqttProfileMessageCallback() { + @Override + public void messageArrived(String topic, MqttMessage message) { + + // Keep subscription to calls to make sure we receive latest updates + Log.i(TAG, "Retrieving statuses, currentCallId = " + currentCallId); + + try { + + // parse the status + String status = new String(message.getPayload()); + + // the call id is the 3rd value + int callId = Integer.valueOf(topic.split("/")[3]); + + Log.i(TAG, "Received call/" + callId + "/status='" + status + "'"); + + // check if message is "requested" + // literally has to be "requested" in quotes + if (status.equalsIgnoreCase("\"requested\"")) { + + // subscribe to call data then prompt user to accept + subscribeToCall(callId, uuid); + + } else if (status.equalsIgnoreCase("\"ongoing\"")) { + + if (currentCallId <= 0) { + + // reply to ongoing + setCallOngoing(callId, uuid); + + } else { + + // TODO: Resume call instead of accept fresh + + } + + } else { + Log.i(TAG, "Unknown status '" + status + "'"); + } + + } catch (Exception e) { + + Log.i(TAG, "Could not parse status"); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotParseStatus)); + sendBroadcastWithUUID(localIntent, uuid); + + } + } + }); + + } catch (MqttException e) { + + Log.d(TAG, "Could not subscribe to statuses"); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotSubscribeToStatuses)); + sendBroadcastWithUUID(localIntent, uuid); + } + + } + + public void subscribeToCall(final int callId, final String uuid) { + + MqttProfileClient profileClient = getProfileClient(this); + + try { + + Log.i(TAG, "Subscribing to call/" + callId); + + profileClient.subscribe(String.format("call/%1$s/data", callId), + 2, new MqttProfileMessageCallback() { + @Override + public void messageArrived(String topic, MqttMessage message) { + + // Keep subscription to calls to make sure we receive latest updates + Log.i(TAG, "Retrieving call data"); + + // parse call id data + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + Gson gson = gsonBuilder.create(); + + try { + + // Parse call data + Call call = gson.fromJson(new String(message.getPayload()), + Call.class); + + // Add or update pending calls + pendingCalls.put(call.getId(), call); + + // if no current call prompt user + if (currentCallId < 0) { + + processNextCall(uuid); + + } else { + + // prompt update call details + + } + + } catch (Exception e) { + + Log.i(TAG, "Could not parse call update."); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotParseCallData)); + sendBroadcastWithUUID(localIntent, uuid); + + } + + } + }); + + } catch (MqttException e) { + + Log.d(TAG, "Could not subscribe to call data"); + + // Broadcast failure + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotSubscribe, "data for call " + callId)); + sendBroadcastWithUUID(localIntent, uuid); + + } + } + + public void stopCallUpdates(final String uuid) { + + // get profile client + MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(this); + Ambulance ambulance = getAmbulance(); + if (ambulance != null && profileClient != null) { + + // terminate current call, does not process next + finishCall(uuid, false); + + Iterator> iterator = pendingCalls.entrySet().iterator(); + while (iterator.hasNext()) { + + Map.Entry pair = iterator.next(); + int callId = pair.getKey(); + + // unsubscribe from call + Log.i(TAG, "Unsubscribe from call/" + callId); + try { + profileClient.unsubscribe("call/" + callId + "/data"); + } catch (MqttException e) { + Log.d(TAG, "Could not unsubscribe to 'call/" + callId + "/data'"); + } + + // remove from pending calls + iterator.remove(); + + } + + Log.i(TAG, "Unsubscribe from call updates"); + try { + profileClient.unsubscribe(String.format("ambulance/%1$d/call/+/status", ambulance.getId())); + } catch (MqttException e) { + Log.d(TAG, String.format("ambulance/%1$d/call/+/status", ambulance.getId())); + } + + } else { + + Log.i(TAG, "No need to stop call updates"); + + } + + } + + public void finishCall(String uuid) { + finishCall(uuid, true); + } + + public void finishCall(String uuid, boolean processNext) { + + // if currently not serving call + if (currentCallId <= 0) { + + Log.d(TAG, "Can't finish call: not serving any call."); + + return; + + } + + // publish finished status to server + setCallStatus(currentCallId, "finished", uuid); + + // unsubscribe from call + Log.i(TAG, "Unsubscribe from call/" + currentCallId); + try { + // get profile client + MqttProfileClient profileClient = AmbulanceForegroundService.getProfileClient(this); + profileClient.unsubscribe("call/" + currentCallId + "/data"); + } catch (MqttException e) { + Log.d(TAG, "Could not unsubscribe to 'call/" + currentCallId + "/data'"); + } + + // publish available to server + String payload = String.format("{\"status\":\"%1$s\"}", "AV"); + Intent intent = new Intent(this, AmbulanceForegroundService.class); + intent.setAction(Actions.UPDATE_AMBULANCE); + Bundle bundle = new Bundle(); + bundle.putString("UPDATE", payload); + intent.putExtras(bundle); + startService(intent); + + // remove call from the queu + pendingCalls.remove(currentCallId); + currentCallId = -1; + + // broadcast CALL_FINISHED + Intent callFinishedIntent = new Intent(BroadcastActions.CALL_FINISHED); + sendBroadcastWithUUID(callFinishedIntent, uuid); + + if (processNext) { + + // process next call + processNextCall(uuid); + + } + + } + + public void declineCall(int callId, String uuid) { + + // if currently serving call, can't decline + if (currentCallId > 0) { + + Log.d(TAG, "Can't decline call: currently serving call/" + currentCallId); + + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotDeclineCall)); + sendBroadcastWithUUID(localIntent, uuid); + + return; + + } + + // remove call from the queu + pendingCalls.remove(callId); + currentCallId = -1; + + // process next call + processNextCall(uuid); + + } + + public void processNextCall(String uuid) { + + // if current call, bark + if (currentCallId > 0) { + + Log.d(TAG, "Will not process next call: currently serving call/" + currentCallId); + + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.willNotProcessNextCall)); + sendBroadcastWithUUID(localIntent, uuid); + + return; + + } + + // if prompting user, ignore + if (currentCallId == 0) { + + Log.d(TAG, "Will not process next call: currently prompting user to accept call."); + + return; + + } + + // Are there more calls in the queu? + Iterator> iterator = pendingCalls.entrySet().iterator(); + if (iterator.hasNext()) { + + Log.i(TAG, "Will prompt user to accept call"); + + Map.Entry pair = iterator.next(); + + // set curentCallId to zero + currentCallId = 0; + + // create intent to prompt user + Intent callPromptIntent = new Intent(BroadcastActions.PROMPT_CALL_ACCEPT); + callPromptIntent.putExtra("CALL_ID", pair.getKey()); + sendBroadcastWithUUID(callPromptIntent, uuid); + + } else { + + Log.i(TAG, "No more pending calls"); + + } + + } + + // handles steps 3 and 4 of Accepting Calls + public void setCallStatus(int callId, String status, String uuid) { + + Log.i(TAG, "Setting call/" + callId + "/status to '" + status + "'"); + + MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + Ambulance ambulance = getAmbulance(); + if (ambulance != null) { + + // step 4: publish accepted to server + String path = String.format("user/%1$s/client/%2$s/ambulance/%3$s/call/%4$s/status", + profileClient.getUsername(), profileClient.getClientId(), ambulance.getId(), callId); + + // publish status to server + publishToPath(status, path, uuid); + + } else { + + Log.d(TAG, "Ambulance not found while in acceptCall()"); + + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotFindAmbulance)); + sendBroadcastWithUUID(localIntent, uuid); + } + + } + + // handles steps 6 to 7 + public void setCallOngoing(final int callId, final String uuid) { + + if (currentCallId > 0) { + + Log.d(TAG, "Can't set call as ongoing: already servicing call " + currentCallId); + + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.cannotSetCallOngoing)); + sendBroadcastWithUUID(localIntent, uuid); + + return; + } + + if (!pendingCalls.containsKey(callId)) { + + Log.d(TAG, "Could not retrieve call/" + currentCallId); + + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, "Could not retrieve call/" + currentCallId); + sendBroadcastWithUUID(localIntent, uuid); + + return; + } + + MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + Ambulance ambulance = getAmbulance(); + + if (ambulance == null) { + + Log.d(TAG, "Ambulance not found while in replyToOngoingCall()"); + + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotFindAmbulance)); + sendBroadcastWithUUID(localIntent, uuid); + + return; + } + + // step 6 & 7 + Log.i(TAG, "Replying to ongoing call/" + callId); + + // set current call + currentCallId = callId; + + // retrieve call + Call call = pendingCalls.get(callId); + + // step 6 + // Add geofence + Log.i(TAG, "Adding geofence"); + Intent serviceIntent = new Intent(AmbulanceForegroundService.this, + AmbulanceForegroundService.class); + serviceIntent.setAction(AmbulanceForegroundService.Actions.GEOFENCE_START); + serviceIntent.putExtra("GEOFENCE_TYPE", false); + serviceIntent.putExtra("LATITUDE", (float) call.getLocation().getLatitude()); + serviceIntent.putExtra("LONGITUDE", (float) call.getLocation().getLongitude()); + serviceIntent.putExtra("RADIUS", 50.f); + startService(serviceIntent); + + // step 7: publish patient bound to server + String payload = String.format("{\"status\":\"%1$s\"}", "PB"); + Intent intent = new Intent(this, AmbulanceForegroundService.class); + intent.setAction(Actions.UPDATE_AMBULANCE); + Bundle bundle = new Bundle(); + bundle.putString("UPDATE", payload); + intent.putExtras(bundle); + startService(intent); + + // broadcast CALL_ONGOING + Intent callOngoingIntent = new Intent(BroadcastActions.CALL_ONGOING); + sendBroadcastWithUUID(callOngoingIntent, uuid); + + } + + public void publishToPath(final String payload, final String path, final String uuid) { + + MqttProfileClient profileClient = getProfileClient(this); + + try { + + Log.i(TAG, "publishing " + payload + " to " + path); + + profileClient.publish(path, payload, 2, false); + + } catch (MqttException e) { + + Log.d(TAG, path); + + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotPublish, path)); + sendBroadcastWithUUID(localIntent, uuid); + } + } + + /* + ** + */ + public void replyToGeofenceTransitions(String uuid, boolean enter, boolean isHospital) { + + // if currently not serving call + if (currentCallId < 0) { + + Log.d(TAG, "Ignoring geofence transition: not serving any call."); + + return; + + } + + MqttProfileClient profileClient = getProfileClient(AmbulanceForegroundService.this); + Ambulance ambulance = getAmbulance(); + + // step 7 + if (ambulance == null) { + + Log.d(TAG, "Ambulance not found while in replyToTransition()"); + + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.couldNotFindAmbulance)); + sendBroadcastWithUUID(localIntent, uuid); + + return; + + } + + if (!isHospital) { + + // step 7: publish status to server + + String status = enter ? "AP" : "HB"; + + String payload = String.format("{\"status\":\"%1$s\"}", status); + Intent intent = new Intent(this, AmbulanceForegroundService.class); + intent.setAction(Actions.UPDATE_AMBULANCE); + Bundle bundle = new Bundle(); + bundle.putString("UPDATE", payload); + intent.putExtras(bundle); + startService(intent); + + } else { + + if (enter) { + + Log.i(TAG, "User has entered hospital"); + + String status = "AH"; + + String payload = String.format("{\"status\":\"%1$s\"}", status); + Intent intent = new Intent(this, AmbulanceForegroundService.class); + intent.setAction(Actions.UPDATE_AMBULANCE); + Bundle bundle = new Bundle(); + bundle.putString("UPDATE", payload); + intent.putExtras(bundle); + startService(intent); + + } else { + + // user is leaving the hospital + + Log.i(TAG, "User is leaving hospital"); + + // create intent to prompt user to end call + Intent callPromptIntent = new Intent(BroadcastActions.PROMPT_CALL_END); + callPromptIntent.putExtra("CALL_ID", currentCallId); + sendBroadcastWithUUID(callPromptIntent, uuid); + + } + + } + + } + + private GeofencingRequest getGeofencingRequest(com.google.android.gms.location.Geofence geofence) { + + Log.i(TAG, "GEOFENCE_REQUEST: Built Geofencing Request"); + + GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); + builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER); + builder.addGeofence(geofence); + + return builder.build(); + + } + + private PendingIntent getGeofencePendingIntent() { + + // Reuse the PendingIntent if we already have it. + if (geofenceIntent != null) { + return geofenceIntent; + } + Intent intent = new Intent(this, GeofenceBroadcastReceiver.class); + + // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when + // calling addGeofences() and removeGeofences(). + geofenceIntent = PendingIntent.getBroadcast(this, 0, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + + return geofenceIntent; + } + + + private void startGeofence(final String uuid, final Geofence geofence) { + + Log.d(TAG,String.format("GEOFENCE(%1$s, %2$f)", geofence.getLocation().toString(), geofence.getRadius())); + + // Set unique id + final String id = "GEOFENCE_" + geofencesId.getAndIncrement(); + + // Create settings client + SettingsClient settingsClient = LocationServices.getSettingsClient(this); + + // Check if the device has the necessary location settings. + settingsClient.checkLocationSettings(getLocationSettingsRequest()) + .addOnSuccessListener(new OnSuccessListener() { + @SuppressLint("MissingPermission") + @Override + public void onSuccess(LocationSettingsResponse locationSettingsResponse) { + + Log.i(TAG, "All location settings are satisfied."); + + Log.i(TAG, "Adding GEOFENCE."); + + fenceClient.addGeofences(getGeofencingRequest(geofence.build(id)), + getGeofencePendingIntent()) + .addOnSuccessListener(new OnSuccessListener() { + + @Override + public void onSuccess(Void aVoid) { + // Geofences added + Log.i(TAG, "GEOFENCES ADDED."); + + // Add to map + _geofences.put(id, geofence); + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + + // Failed to add geofences + Log.e(TAG, "FAILED TO ADD GEOFENCES: " + e.toString()); + + // Broadcast failure and return + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.failedToAddGeofence)); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + }); + + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + int statusCode = ((ApiException) e).getStatusCode(); + String message = getString(R.string.inadequateLocationSettings); + switch (statusCode) { + case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: + message += "Try restarting app."; + break; + case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: + message += "Please fix in Settings."; + } + Log.e(TAG, message); + + // Broadcast failure and return + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, message); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + }); + } + + + private void removeGeofences(final String uuid) { + + fenceClient.removeGeofences(getGeofencePendingIntent()) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void aVoid) { + // Geofences removed + Log.i(TAG, "GEOFENCES REMOVED."); + + // Clear all geofence ids + _geofences.clear(); + + // Broadcast success + Intent localIntent = new Intent(BroadcastActions.SUCCESS); + sendBroadcastWithUUID(localIntent, uuid); + + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + + // Failed to remove geofences + Log.e(TAG, "FAILED TO REMOVE GEOFENCES: " + e.toString()); + + // Broadcast failure and return + Intent localIntent = new Intent(BroadcastActions.FAILURE); + localIntent.putExtra(BroadcastExtras.MESSAGE, getString(R.string.failedToRemoveGeofence)); + sendBroadcastWithUUID(localIntent, uuid); + return; + + } + }); + + } + + private void stopGeofence(final String uuid, final List requestIds) { + + fenceClient.removeGeofences(requestIds) + .addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(Void aVoid) { + + // Geofences removed + Log.i(TAG, "GEOFENCES REMOVED."); + + // Loop through list of ids and remove them + for (String id : requestIds) { + _geofences.remove(id); + } + + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + // Failed to remove geofences + Log.e(TAG, "FAILED TO REMOVE GEOFENCES: " + e.toString()); + } + }); + + } + + /** + * Get LocalBroadcastManager + * + * @return the LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager() { + return LocalBroadcastManager.getInstance(this); + } + + /** + * Get LocalBroadcastManager + * + * @return the LocalBroadcastManager + */ + private static LocalBroadcastManager getLocalBroadcastManager(Context context) { + return LocalBroadcastManager.getInstance(context); + } +} diff --git a/app/src/main/java/org/emstrack/ambulance/services/BroadcastService.java b/app/src/main/java/org/emstrack/ambulance/services/BroadcastService.java new file mode 100644 index 00000000..fa471081 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/services/BroadcastService.java @@ -0,0 +1,38 @@ +package org.emstrack.ambulance.services; + +import android.app.Service; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; + +/** + * Created by mauricio on 3/24/2018. + */ + +public abstract class BroadcastService extends Service { + + public void sendBroadcastWithUUID(Intent intent) { + sendBroadcastWithUUID(intent, null); + } + + public void sendBroadcastWithUUID(Intent intent, String uuid) { + + if (uuid != null) + // inject uuid + intent.putExtra(OnServiceComplete.UUID, uuid); + + // broadcast + getLocalBroadcastManager().sendBroadcast(intent); + + } + + + /** + * Get the LocalBroadcastManager + * + * @return The system LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager() { + return LocalBroadcastManager.getInstance(this); + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/services/GeofenceBroadcastReceiver.java b/app/src/main/java/org/emstrack/ambulance/services/GeofenceBroadcastReceiver.java new file mode 100644 index 00000000..6bd3d715 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/services/GeofenceBroadcastReceiver.java @@ -0,0 +1,107 @@ +package org.emstrack.ambulance.services; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingEvent; + +import org.eclipse.paho.client.mqttv3.MqttException; +import org.emstrack.ambulance.R; +import org.emstrack.ambulance.fragments.AmbulanceFragment; +import org.emstrack.models.Ambulance; +import org.emstrack.mqtt.MqttProfileClient; + +import java.util.ArrayList; +import java.util.List; + +public class GeofenceBroadcastReceiver extends BroadcastReceiver { + + final String TAG = GeofenceBroadcastReceiver.class.getSimpleName(); + + /** + * Receives incoming intents. + * + * @param context the application context. + * @param intent sent by Location Services. This Intent is provided to Location + * Services (inside a PendingIntent) when addGeofences() is called. + */ + @Override + public void onReceive(Context context, Intent intent) { + + // Enqueues a JobIntentService passing the context and intent as parameters + Log.d(TAG, "Got broadcast"); + + GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); + if (geofencingEvent.hasError()) { + //String errorMessage = GeofenceErrorMessages.getErrorString(this, + // geofencingEvent.getErrorCode()); + //Log.e(TAG, errorMessage); + Log.d(TAG, "Geofencing Error"); + return; + } + + // Get the transition type. + int geofenceTransition = geofencingEvent.getGeofenceTransition(); + + List triggeringGeofences = geofencingEvent.getTriggeringGeofences(); + List geofenceIds = new ArrayList(); + + if (triggeringGeofences != null) { + for (Geofence geofence : triggeringGeofences) { + + String geoId = geofence.getRequestId(); + geofenceIds.add(geoId); + + Log.i(TAG, "TRIGGERED GEOFENCE: " + geoId); + } + } + + String[] triggeredGeofences = geofenceIds.toArray(new String[geofenceIds.size()]); + + if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { + if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) { + + // broadcast change on ambulance status when it enters geofence + Intent localIntent = new Intent(context, AmbulanceForegroundService.class); + localIntent.setAction(AmbulanceForegroundService.Actions.GEOFENCE_ENTER); + localIntent.putExtra("TRIGGERED_GEOFENCES", triggeredGeofences); + context.startService(localIntent); + + Log.i(TAG, "GEOFENCE_TRIGGERED: ENTER"); + } else if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { + + // broadcast change on ambulance status when it exits geofence + Intent localIntent = new Intent(context, AmbulanceForegroundService.class); + localIntent.setAction(AmbulanceForegroundService.Actions.GEOFENCE_EXIT); + localIntent.putExtra("TRIGGERED_GEOFENCES", triggeredGeofences); + context.startService(localIntent); + + Log.i(TAG, "GEOFENCE_TRIGGERED: EXIT"); + } + + // Broadcast event + Intent localIntent = new Intent(AmbulanceForegroundService.BroadcastActions.GEOFENCE_EVENT); + localIntent.putExtra(AmbulanceForegroundService.BroadcastExtras.GEOFENCE_TRANSITION, + geofenceTransition); + getLocalBroadcastManager(context).sendBroadcast(localIntent); + + } else { + Log.i(TAG, "GEOFENCE_TRIGGERED: UNKNOWN EVENT " + String.valueOf(geofenceTransition)); + } + + } + + /** + * Get the LocalBroadcastManager + * + * @return The system LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager(Context context) { + return LocalBroadcastManager.getInstance(context); + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/services/OnServiceComplete.java b/app/src/main/java/org/emstrack/ambulance/services/OnServiceComplete.java new file mode 100644 index 00000000..b20e6474 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/services/OnServiceComplete.java @@ -0,0 +1,199 @@ +package org.emstrack.ambulance.services; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import org.emstrack.ambulance.dialogs.AlertSnackbar; + +/** + * Created by mauricio on 3/21/2018. + */ + +public abstract class OnServiceComplete extends BroadcastReceiver { + + public static final String UUID = "_UUID_"; + + private final String TAG = OnServiceComplete.class.getSimpleName(); + + private final String successAction; + private final String failureAction; + private final String uuid; + private String failureMessage; + private AlertSnackbar alert; + + private boolean successFlag; + private boolean completeFlag; + + public OnServiceComplete(final Context context, + final String successAction, + final String failureAction, + Intent intent, + int timeout) { + + // uuid + this.uuid = java.util.UUID.randomUUID().toString(); + + // actions + this.successAction = successAction; + this.failureAction = failureAction; + + // success and complete flags + this.successFlag = false; + this.completeFlag = false; + + // Register actions for broadcasting + IntentFilter successIntentFilter = new IntentFilter(successAction); + getLocalBroadcastManager(context).registerReceiver(this, successIntentFilter); + + IntentFilter failureIntentFilter = new IntentFilter(failureAction); + getLocalBroadcastManager(context).registerReceiver(this, failureIntentFilter); + + // Default alert is AlertLog + this.alert = new AlertSnackbar(TAG); + + // Defaiult failure message + this.failureMessage = "Failed to complete service request"; + + // Run + this.run(); + + // Start service + if (intent != null) { + + // Start service + intent.putExtra(UUID, this.uuid); + context.startService(intent); + + } + + // Start timeout timer + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + + if (!isComplete()) { + + // Broadcast failure + Intent localIntent = new Intent(failureAction); + localIntent.putExtra(OnServiceComplete.UUID, uuid); + localIntent.putExtra(AmbulanceForegroundService.BroadcastExtras.MESSAGE, + "Timed out without completing service."); + context.sendBroadcast(localIntent); + + } + + // otherwise die graciously + + } + }, timeout); + + } + + public OnServiceComplete(final Context context, + final String successAction, + final String failureAction, + Intent intent) { + + this(context, successAction, failureAction, intent, 10000); + + } + + public String getUuid() { return uuid; } + + public boolean isSuccess() { + return successFlag; + } + + public boolean isComplete() { + return completeFlag; + } + + public OnServiceComplete setAlert(AlertSnackbar alert) { + this.alert = alert; + return this; + } + + public OnServiceComplete setFailureMessage(String failureMessage) { + this.failureMessage = failureMessage; + return this; + } + + @Override + public void onReceive(Context context, Intent intent) { + + // quick return if no intent + if (intent == null) + return; + + // get uuid + String uuid = intent.getStringExtra(UUID); + + // quick return if not same uuid + if (!this.uuid.equals(uuid)) + return; + + // unregister first + unregister(context); + + // Process actions + final String action = intent.getAction(); + if (action.equals(successAction)) { + + Log.i(TAG, "SUCCESS"); + this.successFlag = true; + onSuccess(intent.getExtras()); + + } else if (action.equals(failureAction)) { + + Log.i(TAG, "FAILURE"); + this.successFlag = false; + onFailure(intent.getExtras()); + + } else + Log.i(TAG, "Unknown action '" + action + "'"); + + // complete + this.completeFlag = true; + + } + + public void run() { } + + public abstract void onSuccess(Bundle extras); + + public void onFailure(Bundle extras) { + + // Alert user + String message = failureMessage; + if (extras != null) { + String msg = extras.getString(AmbulanceForegroundService.BroadcastExtras.MESSAGE); + if (msg != null) + message += '\n' + msg; + + } + + // Alert user + alert.alert(message); + + } + + public void unregister(Context context) { + getLocalBroadcastManager(context).unregisterReceiver(this); + } + + /** + * Get the LocalBroadcastManager + * + * @return The system LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager(Context context) { + return LocalBroadcastManager.getInstance(context); + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/services/OnServicesComplete.java b/app/src/main/java/org/emstrack/ambulance/services/OnServicesComplete.java new file mode 100644 index 00000000..dc2a895b --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/services/OnServicesComplete.java @@ -0,0 +1,229 @@ +package org.emstrack.ambulance.services; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import org.emstrack.ambulance.dialogs.AlertSnackbar; + +/** + * Created by mauricio on 3/21/2018. + */ + +public abstract class OnServicesComplete extends BroadcastReceiver { + + public static final String UUID = "_UUID_"; + + private final String TAG = OnServicesComplete.class.getSimpleName(); + + private final String[] successActions; + private final String[] failureActions; + private final String uuid; + private String failureMessage; + private AlertSnackbar alert; + + private boolean successFlag; + private boolean completeFlag; + + public OnServicesComplete(final Context context, + final String[] successActions, + final String[] failureActions, + Intent intent, + int timeout) { + + // uuid + this.uuid = java.util.UUID.randomUUID().toString(); + + // actions + this.successActions = successActions; + this.failureActions = failureActions; + + // success and complete flags + this.successFlag = false; + this.completeFlag = false; + + // Register actions for broadcasting + IntentFilter intentFilter = new IntentFilter(); + for (String action : successActions) + intentFilter.addAction(action); + for (String action: failureActions) + intentFilter.addAction(action); + getLocalBroadcastManager(context).registerReceiver(this, intentFilter); + + // Default alert is AlertLog + this.alert = new AlertSnackbar(TAG); + + // Defaiult failure message + this.failureMessage = "Failed to complete service request"; + + // Run + this.run(); + + // Start service + if (intent != null) { + + // Start service + intent.putExtra(UUID, this.uuid); + context.startService(intent); + + } + + // Start timeout timer + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + + if (!isComplete()) { + + for (String action: failureActions) { + + // Broadcast failures + Intent localIntent = new Intent(action); + localIntent.putExtra(OnServicesComplete.UUID, uuid); + localIntent.putExtra(AmbulanceForegroundService.BroadcastExtras.MESSAGE, + "Timed out without completing service."); + context.sendBroadcast(localIntent); + + } + + } + + // otherwise die graciously + + } + }, timeout); + + } + + public OnServicesComplete(final Context context, + final String[] successActions, + final String[] failureActions, + Intent intent) { + + this(context, successActions, failureActions, intent, 10000); + + } + + public String getUuid() { return uuid; } + + public boolean isSuccess() { + return successFlag; + } + + public boolean isComplete() { + return completeFlag; + } + + public OnServicesComplete setAlert(AlertSnackbar alert) { + this.alert = alert; + return this; + } + + public OnServicesComplete setFailureMessage(String failureMessage) { + this.failureMessage = failureMessage; + return this; + } + + @Override + public void onReceive(Context context, Intent intent) { + + // quick return if no intent + if (intent == null) + return; + + // get uuid + String uuid = intent.getStringExtra(UUID); + + // quick return if not same uuid + if (!this.uuid.equals(uuid)) + return; + + // unregister first + unregister(context); + + // Retrieve action + final String action = intent.getAction(); + + // Process success actions + boolean match = false; + for (String _action: successActions) { + + if (action.equals(_action)) { + + Log.i(TAG, "SUCCESS"); + this.successFlag = true; + onSuccess(intent.getExtras()); + + match = true; + break; + } + + } + + // Process failure actions + if (!match) + for (String _action: failureActions) { + + if (action.equals(_action)) { + + Log.i(TAG, "FAILURE"); + this.successFlag = false; + onFailure(intent.getExtras()); + + match = true; + break; + + } + + } + + if (match) + + // complete + this.completeFlag = true; + + else + + Log.i(TAG, "Unknown action '" + action + "'"); + + } + + public void run() { } + + public abstract void onSuccess(Bundle extras); + + public void onFailure(Bundle extras) { + + // Alert user + String message = failureMessage; + if (extras != null) { + String msg = extras.getString(AmbulanceForegroundService.BroadcastExtras.MESSAGE); + if (msg != null) + message += '\n' + msg; + + } + + // Alert user + alert.alert(message); + + } + + public void unregister(Context context) { + getLocalBroadcastManager(context).unregisterReceiver(this); + } + + /** + * Get the LocalBroadcastManager + * + * @return The system LocalBroadcastManager + */ + private LocalBroadcastManager getLocalBroadcastManager(Context context) { + return LocalBroadcastManager.getInstance(context); + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/util/Geofence.java b/app/src/main/java/org/emstrack/ambulance/util/Geofence.java new file mode 100644 index 00000000..26e422a3 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/util/Geofence.java @@ -0,0 +1,47 @@ +package org.emstrack.ambulance.util; + +import org.emstrack.models.Location; + +import static com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_ENTER; +import static com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_EXIT; +import static com.google.android.gms.location.Geofence.NEVER_EXPIRE; + +/** + * Created by mauricio on 4/26/18. + */ + +public class Geofence { + + Location location; + float radius; + boolean isHospital; + + public Geofence(Location location, float radius, boolean isHospital) { + this.location = location; + this.radius = radius; + this.isHospital = isHospital; + } + + public Location getLocation() { return location; } + + public float getRadius() { return radius; } + + public boolean isHospital() { return isHospital; } + + public com.google.android.gms.location.Geofence build(String id) { + return build(id, GEOFENCE_TRANSITION_ENTER | GEOFENCE_TRANSITION_EXIT); + } + + private com.google.android.gms.location.Geofence build(String id, int transitionTypes) { + + // Create geofence object + com.google.android.gms.location.Geofence.Builder builder = new com.google.android.gms.location.Geofence.Builder(); + builder.setRequestId(id); + builder.setCircularRegion((float) location.getLatitude(), (float) location.getLongitude(), radius); + builder.setExpirationDuration(NEVER_EXPIRE); + builder.setTransitionTypes(transitionTypes); + return builder.build(); + + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/util/LatLon.java b/app/src/main/java/org/emstrack/ambulance/util/LatLon.java new file mode 100644 index 00000000..cb1ea7ed --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/util/LatLon.java @@ -0,0 +1,85 @@ +package org.emstrack.ambulance.util; + +import android.location.Location; + +/** + * Created by mauricio on 3/14/2018. + */ + +public class LatLon { + + public final static double earthRadius = 6371e3; // in meters + public static double stationaryRadius = 10.; // in meters + public static double stationaryVelocity= .1; // in meters/s + + public static double calculateDistanceHaversine(Location location1, Location location2) { + + // convert latitude and longitude to radians first + double lat1 = Math.PI * location1.getLatitude() / 180; + double lat2 = Math.PI * location2.getLatitude() / 180; + double d_phi = lat2 - lat1; + double d_lambda = Math.PI * (location2.getLongitude() - location1.getLongitude()) / 180; + + double a = Math.sin(d_phi / 2) * Math.sin(d_phi / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(d_lambda / 2) * Math.sin(d_lambda / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return LatLon.earthRadius * c; + + } + + public static double calculateBearing(Location location1, Location location2) { + + // convert latitude and longitude to radians first + double lat1 = Math.PI * location1.getLatitude() / 180; + double lat2 = Math.PI * location2.getLatitude() / 180; + double d_lambda = Math.PI * (location2.getLongitude() - location1.getLongitude()) / 180; + + // calculate bearing and convert to degrees + double bearing = (180 / Math.PI) * Math.atan2(Math.sin(d_lambda) * Math.cos(lat2), + Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(d_lambda)); + + return (bearing < 0 ? bearing + 360 : bearing); + + } + + public static double[] calculateDistanceAndBearing(Location location1, Location location2) { + + // convert latitude and longitude to radians first + double lat1 = Math.PI * location1.getLatitude() / 180; + double lat2 = Math.PI * location2.getLatitude() / 180; + double d_phi = lat2 - lat1; + double d_lambda = Math.PI * (location2.getLongitude() - location1.getLongitude()) / 180; + + // calculate distance + double a = Math.sin(d_phi / 2) * Math.sin(d_phi / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(d_lambda / 2) * Math.sin(d_lambda / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + double distance = LatLon.earthRadius * c; + + // calculate bearing and convert to degrees + double bearing = (180 / Math.PI) * Math.atan2(Math.sin(d_lambda) * Math.cos(lat2), + Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(d_lambda)); + + return new double[] {distance, (bearing < 0 ? bearing + 360 : bearing)}; + + } + + public static Location updateLocation(Location start, double bearing, double distance) { + + // convert latitude, longitude, and bearing to radians first + double lat1 = Math.PI * start.getLatitude() / 180; + double lon1 = Math.PI * start.getLatitude() / 180; + double brng = Math.PI * bearing / 180; + + double lat2 = Math.asin(Math.sin(lat1) * Math.cos(distance / earthRadius) + + Math.cos(lat1) * Math.sin(distance / earthRadius) * Math.cos(brng)); + double lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(distance / earthRadius) * Math.cos(lat1), + Math.cos(distance / earthRadius) - Math.sin(lat1) * Math.sin(lat2)); + + Location location = new Location(start); + location.setLatitude(lat2); + location.setLongitude(lon2); + + return location; + + } +} diff --git a/app/src/main/java/org/emstrack/ambulance/util/LocationFilter.java b/app/src/main/java/org/emstrack/ambulance/util/LocationFilter.java new file mode 100644 index 00000000..b6459a41 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/util/LocationFilter.java @@ -0,0 +1,184 @@ +package org.emstrack.ambulance.util; + +import android.location.Location; +import android.util.Log; + +import org.ejml.data.DMatrix5x5; +import org.ejml.simple.SimpleMatrix; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.emstrack.ambulance.util.LatLon.calculateBearing; +import static org.emstrack.ambulance.util.LatLon.calculateDistanceAndBearing; +import static org.emstrack.ambulance.util.LatLon.calculateDistanceHaversine; +import static org.emstrack.ambulance.util.LatLon.stationaryRadius; +import static org.emstrack.ambulance.util.LatLon.stationaryVelocity; +import static org.emstrack.ambulance.util.LatLon.updateLocation; + +/** + * Created by mauricio on 3/22/2018. + */ + +public class LocationFilter { + + private static final String TAG = LocationFilter.class.getSimpleName(); + + private LocationUpdate location; + + public LocationFilter(LocationUpdate location) { + this.location = location; + } + + public void setLocation(LocationUpdate location) { + this.location = location; + } + + /** + * Update current position based on a new measurement + * + * @param update the update + */ + public void update(Location update, List filteredLocations) { + + // elapsed time + double dt = update.getTime() - location.getTimestamp().getTime(); + + // Predict next location + // Location prediction = updateLocation(location, bearing, velocity * dt); + + // measure velocity and bearing + double[] dandb = calculateDistanceAndBearing(location.getLocation(), update); + double distance = dandb[0]; + double brn = dandb[1]; + double vel = location.getVelocity(); + if (dt > 0) + vel = distance / dt; + + // locationFilter velocity + double Kv = 0.9; + double velocity = location.getVelocity(); + velocity += Kv * (vel - velocity); + location.setVelocity(velocity); + + // locationFilter bearing + double Kb = 0.9; + double bearing = location.getBearing(); + bearing += Kb * (brn - bearing); + location.setBearing(bearing); + + if ((velocity > stationaryVelocity && distance > stationaryRadius) || + (velocity <= stationaryVelocity && distance > 3 * stationaryRadius)) { + + // update location + location.setLocation(update); + location.setTimestamp(new Date(update.getTime())); + + // add location to filtered locations + filteredLocations.add(new LocationUpdate(location)); + + } + + Log.i(TAG, "velocity = " + velocity + ", distance = " + distance + ", bearing = " + bearing + "(" + update.getBearing() + ")"); + +} + + public List update(List locations) { + + // Fast return if no updates + List filteredLocations = new ArrayList<>(); + if (locations == null || locations.size() == 0) + return filteredLocations; + + // initialize + if (location == null) + // use first record + location = new LocationUpdate(locations.get(0)); + + // loop through records + for (Location location : locations) + update(location, filteredLocations); + + return filteredLocations; + } + + /** + * Model is that of a constant forward velocity and constant angular velocity + * + * xDot(t) = v(t) cos(theta(t)) + * yDot(t) = v(t) cos(theta(t)) + * thetaDot(t) = thetaDot(t) + * thetaDotDot(t) = 0 + * vDot(t) = 0 + * + * Discretizing at t+ = tk + dt, t = tk, dt = t+ - t, we obtain the model: + * + * x(tk+), y(tk+) = f(x(tk), y(tk), theta(tk-), v(tk) dt) + * theta(tk+) = theta(tk) + thetaDot(tk) dt + * thetaDot(tk+) = thetaDot(tk) + * v(tk+) = v(tk) + * + * or + * + * X = (x, y, theta, thetaDot, v) + * X(tk+) = F(X(tk),tk), + * Z(tk) = G(X(tk),tk) + * + * Partials: + * + * f1: x(tk+) = x(tk) + dt v(tk) cos(theta(tk)) + * f2: y(tk+) = y(tk) + dt v(tk) sin(theta(tk)) + * f3: theta(tk+) = theta(tk) + thetaDot(tk) dt + * f4: thetaDot(tk+) = thetaDot(tk) + * f5: v(tk+) = v(tk) + * + * Fk = dF/dx + * = [1, 0, -dt*v(tk)*sin(theta(tk)), 0, dt*cos(theta(tk)); + * 0, 1, dt*v(tk)*cos(theta(tk)), 0, dt*sin(theta(tk)); + * 0, 0, 1, dt, 0; + * 0, 0, 0, 1, 0; + * 0, 0, 0, 0, 1] + * + * g1: z1(tk) = x(tk) + * g2: z2(tk) = y(tk) + * + * Hk = dG/dx + * = [1, 0, 0, 0; + * 0, 1, 0, 0]; + * + * + * Extended Kalman locationFilter + * + * Prediction: + * + * xHat(tk+|tk), yHat(tk+|tk) = f(xHat(tk), yHat(tk), thetaHat(tk), vHat(tk) tk) + * thetaHat(tk+|tk) = thetaHat(tk) + thetaDotHat(tk) dt + * thetaDotHat(tk+|tk) = thetaDotHat(tk) + * vHat(tk+|tk) = vHat(tk) + * + * or + * + * XHat(tk+|tk) = F(XHat(tk),tk) + * + * Covariance prediction: + * + * P(tk+|tk) = Fk P(tk) Fk' + Qk + * + * At time tk we obtain a measurement ot z(tk) = (x(tk), y(tk)) + * + * Update: + * + * Sk = Hk P(tk|tk) Hk' + Rk + * Kk = P(tk+|tk) Hk' inv(Sk) + * + * XHat(tk+) = XHat(tk+|tk) + K (z(tk) - zHat(tk)) + * + * Covariance update: + * + * P(tk+ = P(tk+|tk+) = (I - Kk Hk) P(tk+|tk) + * + */ + + +} diff --git a/app/src/main/java/org/emstrack/ambulance/util/LocationUpdate.java b/app/src/main/java/org/emstrack/ambulance/util/LocationUpdate.java new file mode 100644 index 00000000..ac81ef40 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/util/LocationUpdate.java @@ -0,0 +1,80 @@ +package org.emstrack.ambulance.util; + +import android.location.Location; + +import java.util.Date; + +/** + * Created by mauricio on 3/22/2018. + */ + +public class LocationUpdate { + + private Location location; + private float bearing; + private float velocity; + private Date timestamp; + + public LocationUpdate() { + this.location = null; + this.bearing = (float) 0.0; + this.velocity = (float) 0.0; + this.timestamp = new Date(); + } + + public LocationUpdate(Location location) { + this.location = new Location(location); + this.bearing = location.getBearing(); + this.velocity = location.getSpeed(); + this.timestamp = new Date(location.getTime()); + } + + public LocationUpdate(LocationUpdate update) { + this.location = new Location(update.location); + this.bearing = update.bearing; + this.velocity = update.velocity; + this.timestamp = new Date(update.timestamp.getTime()); + } + + public void setLocation(Location location) { + this.location = location; + } + + public void setBearing(float bearing) { + this.bearing = bearing; + } + + public void setVelocity(float velocity) { + this.velocity = velocity; + } + + public void setBearing(double bearing) { + this.bearing = (float) bearing; + } + + public void setVelocity(double velocity) { + this.velocity = (float) velocity; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public Location getLocation() { + return location; + } + + public float getBearing() { + return bearing; + } + + public float getVelocity() { + return velocity; + } + + public Date getTimestamp() { + return timestamp; + } +}; + + diff --git a/app/src/main/java/org/emstrack/ambulance/views/HospitalEquipmentViewHolder.java b/app/src/main/java/org/emstrack/ambulance/views/HospitalEquipmentViewHolder.java new file mode 100644 index 00000000..c06efb02 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/views/HospitalEquipmentViewHolder.java @@ -0,0 +1,33 @@ +package org.emstrack.ambulance.views; + +import android.view.View; +import android.widget.CheckBox; +import android.widget.TextView; + +import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder; + +import org.emstrack.ambulance.R; +import org.emstrack.models.HospitalEquipment; + +/** + * Created by mauricio on 3/11/2018. + */ + +public class HospitalEquipmentViewHolder extends ChildViewHolder { + + TextView hospitalEquipmentNameTextView; + TextView hospitalEquipmentValueTextView; + + public HospitalEquipmentViewHolder(View itemView) { + super(itemView); + + hospitalEquipmentNameTextView = itemView.findViewById(R.id.hospital_equipment_name); + hospitalEquipmentValueTextView = itemView.findViewById(R.id.hospital_equipment_value); + } + + public void setHospitalEquipment(HospitalEquipment hospitalEquipment) { + hospitalEquipmentNameTextView.setText(hospitalEquipment.getEquipmentName()); + hospitalEquipmentValueTextView.setText(hospitalEquipment.getValue()); + } + +} diff --git a/app/src/main/java/org/emstrack/ambulance/views/HospitalViewHolder.java b/app/src/main/java/org/emstrack/ambulance/views/HospitalViewHolder.java new file mode 100644 index 00000000..53c0e6d1 --- /dev/null +++ b/app/src/main/java/org/emstrack/ambulance/views/HospitalViewHolder.java @@ -0,0 +1,39 @@ +package org.emstrack.ambulance.views; + +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder; + +import org.emstrack.ambulance.R; +import org.emstrack.ambulance.models.HospitalExpandableGroup; +import org.emstrack.models.Hospital; + +/** + * Created by mauricio on 3/11/2018. + */ + +// TODO: Modify equipment display depending on type +// TODO: Icon has white instead of transparency + +public class HospitalViewHolder extends GroupViewHolder { + + ImageView hospitalThumbnailImageView; + TextView hospitalNameTextView; + FrameLayout frameLayout; + + public HospitalViewHolder(View itemView) { + super(itemView); + + hospitalNameTextView = (TextView) itemView.findViewById(R.id.hospital_name); + hospitalThumbnailImageView = (ImageView) itemView.findViewById(R.id.hospital_thumbnail); + } + + public void setHospital(Hospital hospital) { + hospitalNameTextView.setText(hospital.getName()); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/emstrack/hospital/HospitalApp.java b/app/src/main/java/org/emstrack/hospital/HospitalApp.java deleted file mode 100644 index 4b25bb1e..00000000 --- a/app/src/main/java/org/emstrack/hospital/HospitalApp.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.emstrack.hospital; - -import android.app.Application; - -import org.eclipse.paho.android.service.MqttAndroidClient; -import org.emstrack.mqtt.MqttProfileClient; - -import java.util.UUID; - -/** - * Created by mauri on 2/10/2018. - */ - -public class HospitalApp extends Application { - - final String serverUri = "ssl://cruzroja.ucsd.edu:8883"; - final String clientId = "HospitalAppClient_" + UUID.randomUUID().toString(); - - MqttAndroidClient androidClient; - MqttProfileClient client; - - public MqttProfileClient getProfileClient() { - // lazy initialization - if (client == null) { - - androidClient = new MqttAndroidClient(getApplicationContext(), serverUri, clientId); - client = new MqttProfileClient(androidClient); - - } - return client; - } - -} diff --git a/app/src/main/java/org/emstrack/hospital/HospitalEquipmentActivity.java b/app/src/main/java/org/emstrack/hospital/HospitalEquipmentActivity.java deleted file mode 100644 index b4cee813..00000000 --- a/app/src/main/java/org/emstrack/hospital/HospitalEquipmentActivity.java +++ /dev/null @@ -1,200 +0,0 @@ -package org.emstrack.hospital; - -import java.util.ArrayList; - -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.util.Log; -import android.view.View; -import android.view.Window; -import android.widget.ImageView; -import android.widget.ListView; - -import com.google.gson.FieldNamingPolicy; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; - -import org.emstrack.hospital.adapters.ListAdapter; -import org.emstrack.hospital.dialogs.LogoutDialog; -import org.emstrack.hospital.interfaces.DataListener; - -import org.emstrack.mqtt.MqttProfileClient; -import org.emstrack.mqtt.MqttProfileMessageCallback; - -import org.emstrack.models.HospitalEquipment; - -/** - * Created by devinhickey on 4/20/17. - * The Dashboard - */ -public class HospitalEquipmentActivity extends AppCompatActivity { - - private static final String TAG = HospitalEquipmentActivity.class.getSimpleName(); - private ListAdapter adapter; - int hospitalId = -1; - - @Override - public void onCreate(Bundle savedInstanceState) { - - super.onCreate(savedInstanceState); - - this.requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.activity_dashboard); - - // Action bar - getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - getSupportActionBar().setDisplayShowCustomEnabled(true); - getSupportActionBar().setCustomView(R.layout.maintitlebar); - - // Connect logout dialog - View view = getSupportActionBar().getCustomView(); - ImageView imageButton= view.findViewById(R.id.LogoutBtn); - imageButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // Create the logout dialog for the user - LogoutDialog ld = LogoutDialog.newInstance(); - ld.show(getFragmentManager(), "logout_dialog"); - } - }); - - // Get data from Login and place it into the hospital - hospitalId = Integer - .parseInt(getIntent().getStringExtra("SELECTED_HOSPITAL_ID")); - - // Retrieve client - final MqttProfileClient profileClient = ((HospitalApp) getApplication()).getProfileClient(); - - // Set list adapter - ListView lv = findViewById(R.id.dashboardListView); - adapter = new ListAdapter(this, - new ArrayList(), - getSupportFragmentManager()); - adapter.setOnDataChangedListener(new DataListener() { - - @Override - public void onDataChanged(String equipmentName, String data) { - - Log.d(TAG, "onDataChanged: " + equipmentName + "@" + data); - String format = "{\"hospital_id\":%1$s,\"equipment_name\":\"%2$s\",\"value\":%3$s}"; - - try { - profileClient.publish("user/" + profileClient.getUsername() + - "/hospital/" + hospitalId + - "/equipment/" + equipmentName + "/data", - String.format(format, hospitalId, equipmentName, data), - 2, false); - } catch (MqttException e) { - Log.d(TAG, "Failed to publish updated equipment"); - } - } - - }); - lv.setAdapter(adapter); - - try { - - // Start retrieving data - profileClient.subscribe("hospital/" + hospitalId + "/metadata", - 1, new MqttProfileMessageCallback() { - - @Override - public void messageArrived(String topic, MqttMessage message) { - - try { - - // Unsubscribe to metadata - profileClient.unsubscribe("hospital/" + hospitalId + "/metadata"); - - } catch ( MqttException exception ) { - - Log.d(TAG,"Could not unsubscribe to 'hospital/" + hospitalId + "/metadata'"); - return; - } - - // Parse to hospital metadata - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); - Gson gson = gsonBuilder.create(); - - try { - // Subscribe to all hospital equipment topics - profileClient.subscribe( - "hospital/" + hospitalId + "/equipment/+/data", - 1, new MqttProfileMessageCallback() { - @Override - public void messageArrived(String topic, MqttMessage message) { - - // Parse to hospital equipment - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setFieldNamingPolicy( - FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); - Gson gson = gsonBuilder.create(); - - // Found item in the hospital equipments object - HospitalEquipment equipment = gson - .fromJson(new String(message.getPayload()), - HospitalEquipment.class); - - // add equipment to list - adapter.add(equipment); - - } - }); - - /* - - // NOT NECESSARY: Already subscribed to all equipment topics - - HospitalEquipmentMetadata[] equipmentMetadata = gson - .fromJson(new String(message.getPayload()), - HospitalEquipmentMetadata[].class); - for (HospitalEquipmentMetadata equipment : equipmentMetadata) { - // Subscribe without a callback - profileClient.subscribe( - "hospital/" + hospitalId + - "/equipment/" + equipment.getName() + "/data", - 1, null); - } - - */ - - } catch (MqttException e) { - Log.d(TAG, "Could not subscribe to hospital equipment topics"); - } - } - - }); - - } catch (MqttException e) { - Log.d(TAG, "Could not subscribe to hospital metadata"); - } - - } - - @Override - public void onBackPressed() { - - try { - - // Retrieve client - final MqttProfileClient profileClient = ((HospitalApp) getApplication()).getProfileClient(); - - // unsubscribe to current hospital equipment data - profileClient.unsubscribe("hospital/" + hospitalId + "/equipment/+/data"); - - } catch ( MqttException exception ) { - - Log.d(TAG,"Could not unsubscribe to 'hospital/" + hospitalId + "/equipment/+/data'"); - - } - - // go back - super.onBackPressed(); - } - -} diff --git a/app/src/main/java/org/emstrack/hospital/HospitalListActivity.java b/app/src/main/java/org/emstrack/hospital/HospitalListActivity.java deleted file mode 100644 index 1d6e5a39..00000000 --- a/app/src/main/java/org/emstrack/hospital/HospitalListActivity.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.emstrack.hospital; - -import java.util.ArrayList; -import java.util.List; - -import android.content.Intent; -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.app.ActionBar; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.Spinner; -import android.widget.Toast; -import android.util.Log; - -import org.emstrack.hospital.dialogs.LogoutDialog; -import org.emstrack.models.HospitalPermission; -import org.emstrack.mqtt.MqttProfileClient; - -/** - * Created by devinhickey on 5/24/17. - */ - -public class HospitalListActivity extends AppCompatActivity { - - private static final String TAG = "HospitalListActivity"; - - @Override - public void onCreate(Bundle savedInstanceState) { - - Log.d(TAG, "onCreate"); - - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_hospital_list); - - // Action bar - getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - getSupportActionBar().setDisplayShowCustomEnabled(true); - getSupportActionBar().setCustomView(R.layout.maintitlebar); - View view = getSupportActionBar().getCustomView(); - - // Connect logout dialog - ImageView imageButton = view.findViewById(R.id.LogoutBtn); - imageButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogoutDialog ld = LogoutDialog.newInstance(); - ld.show(getFragmentManager(), "logout_dialog"); - } - }); - - // Retrieve hospitals - final MqttProfileClient profileClient = ((HospitalApp) getApplication()).getProfileClient(); - final List hospitals = profileClient.getProfile().getHospitals(); - - // No hospitals associated with this account - if (hospitals.size() < 1) { - Toast toast = new Toast(this); - toast.setText("This account has no hospitals associated with it!"); - toast.setDuration(Toast.LENGTH_LONG); - toast.show(); - return; - } - - // Creates string arraylist of hospital names - Log.d(TAG, "Creating hospital list..."); - ArrayList listObjects = new ArrayList<>(); - for (HospitalPermission hospital : hospitals) { - Log.d(TAG, "Adding hospital " + hospital.getHospitalName()); - listObjects.add(hospital.getHospitalName()); - } - - // Create the Spinner connection - final Spinner hospitalSpinner = findViewById(R.id.hospitalSpinner); - hospitalSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - Log.d(TAG, "OnItemSelected Called"); - } - - @Override - public void onNothingSelected(AdapterView parent) { - Log.d(TAG, "OnNothingSelected Called"); - } - }); - - // Create the basic adapter - ArrayAdapter hospitalListAdapter = new ArrayAdapter<>(this, - android.R.layout.simple_spinner_item, listObjects); - hospitalListAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - - // Set the spinner's adapter - hospitalSpinner.setAdapter(hospitalListAdapter); - - // Create the hospital button - Button submitHospitalButton = findViewById(R.id.submitHospitalButton); - submitHospitalButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.d(TAG, "HospitalEquipmentMetadata Submit Button Clicked"); - - int position = hospitalSpinner.getSelectedItemPosition(); - Log.d(TAG, "Position Selected: " + position); - HospitalPermission selectedHospital = hospitals.get(position); - Log.d(TAG, "Selected HospitalEquipmentMetadata: " + selectedHospital.getHospitalName()); - - // Set the static list of HospitalEquipment in Dashboard - Intent intent = new Intent(HospitalListActivity.this, HospitalEquipmentActivity.class); - intent.putExtra("SELECTED_HOSPITAL_ID", Integer.toString(selectedHospital.getHospitalId())); - startActivity(intent); - } - }); - - } // end onCreate - - @Override - public void onBackPressed() { - LogoutDialog ld = LogoutDialog.newInstance(); - ld.show(getFragmentManager(), "logout_dialog"); - } - -} diff --git a/app/src/main/java/org/emstrack/hospital/LoginActivity.java b/app/src/main/java/org/emstrack/hospital/LoginActivity.java deleted file mode 100644 index fec2a68d..00000000 --- a/app/src/main/java/org/emstrack/hospital/LoginActivity.java +++ /dev/null @@ -1,250 +0,0 @@ -package org.emstrack.hospital; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.ImageView; - -import org.eclipse.paho.client.mqttv3.MqttException; - -import org.emstrack.mqtt.MqttProfileCallback; -import org.emstrack.mqtt.MqttProfileClient; - -public class LoginActivity extends AppCompatActivity { - - private final String PREFERENCES_USERNAME = "username"; - private final String PREFERENCES_PASSWORD = "password"; - private final String PREFERENCES_REMEMBER_ME = "remember_me"; - - private static final String TAG = "LoginActvity"; - - private SharedPreferences creds_prefs; - private ProgressDialog progressDialog; - - @Override - protected void onCreate(Bundle savedInstanceState) { - - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_login); - - // Action bar - getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); - getSupportActionBar().setDisplayShowCustomEnabled(true); - getSupportActionBar().setCustomView(R.layout.maintitlebar); - View view = getSupportActionBar().getCustomView(); - ImageView imageButton= view.findViewById(R.id.LogoutBtn); - imageButton.setVisibility(View.GONE); - - // Create progress dialog - progressDialog = new ProgressDialog(LoginActivity.this); - - // Find username and password from layout - final EditText usernameField = findViewById(R.id.username); - final EditText passwordField = findViewById(R.id.password); - final CheckBox savedUsernameCheck = findViewById(R.id.checkBox); - - // Get credentials - creds_prefs = getSharedPreferences("org.emstrack.hospital", MODE_PRIVATE); - - // Check if credentials are cached - if (creds_prefs.getBoolean(PREFERENCES_REMEMBER_ME, false)) { - - Log.d(TAG, "Remember user enabled, using credentials"); - usernameField.setText(creds_prefs.getString(PREFERENCES_USERNAME, null)); - savedUsernameCheck.setChecked(true); - - } else{ - Log.d(TAG, "Remember user not enabled, asking for credentials"); - } - - // Submit button's click listener - Button login_submit = findViewById(R.id.submit_login); - login_submit.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - - // Get user info & remove whitspace - String username = usernameField.getText().toString().trim(); - String password = passwordField.getText().toString().trim(); - - if (username.isEmpty()) { - alertDialog(LoginActivity.this, - getResources().getString(R.string.alert_error_title), - getResources().getString(R.string.error_empty_username)); - } else if (password.isEmpty()) { - alertDialog(LoginActivity.this, - getResources().getString(R.string.alert_error_title), - getResources().getString(R.string.error_empty_password)); - } else { - - // Show progress dialog - progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); - progressDialog.setMessage(getResources().getString(R.string.message_please_wait)); - progressDialog.setIndeterminate(true); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.show(); - - // while attempting to login - loginHospital(username, password); - } - - } - }); - - // Code to hide keyboard if user clicks out of window - findViewById(R.id.relativeLayout).setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); - return true; - } - }); - } - - public void loginHospital(final String username, final String password) { - - // Retrieve client - final MqttProfileClient profileClient = ((HospitalApp) getApplication()).getProfileClient(); - - // Set callback to be called after profile is retrieved - profileClient.setCallback(new MqttProfileCallback() { - - @Override - public void onSuccess() { - - // Get preferences editor - SharedPreferences.Editor editor = creds_prefs.edit(); - - // Check if remember me enabled - CheckBox remember_box = findViewById(R.id.checkBox); - if (remember_box.isChecked()) { - - Log.d(TAG, "Checkbox checked, storing credentials"); - editor.putString(PREFERENCES_USERNAME, username); - editor.putString(PREFERENCES_PASSWORD, password); - editor.putBoolean(PREFERENCES_REMEMBER_ME, true); - - } else { - - editor.clear(); - - } - - editor.apply(); - - // Initiate new activity - Intent hospitalIntent = new Intent(getApplicationContext(), HospitalListActivity.class); - startActivity(hospitalIntent); - - // Clear the loading screen - progressDialog.dismiss(); - - // Clear the password field - EditText passwordField = findViewById(R.id.password); - passwordField.setText(""); - passwordField.clearFocus(); - - // Clear the username field - EditText usernameField = findViewById(R.id.username); - usernameField.clearFocus(); - - Log.d(TAG, "Done with LoginActivity."); - - } - - @Override - public void onFailure(Throwable exception) { - - // Dismiss dialog - progressDialog.dismiss(); - - Log.d(TAG, "Failed to retrieve profile."); - alertDialog(LoginActivity.this, - getResources().getString(R.string.alert_error_title), - exception.toString()); - - } - - }); - - try { - - // Attempt to connect - profileClient.connect(username, password, new MqttProfileCallback() { - - @Override - public void onSuccess() { - Log.d(TAG, "Successfully connected to broker."); - } - - @Override - public void onFailure(Throwable exception) { - - // Dismiss dialog - progressDialog.dismiss(); - - Log.d(TAG, "Failed to connected to broker."); - String message; - if (exception instanceof MqttException) { - int reason = ((MqttException) exception).getReasonCode(); - if (reason == MqttException.REASON_CODE_FAILED_AUTHENTICATION || - reason == MqttException.REASON_CODE_NOT_AUTHORIZED || - reason == MqttException.REASON_CODE_INVALID_CLIENT_ID) - message = getResources().getString(R.string.error_invalid_credentials); - else - message = String.format(getResources().getString(R.string.error_connection_failed), - exception.toString()); - } else { - message = exception.toString(); - } - - // Alert user - alertDialog(LoginActivity.this, - getResources().getString(R.string.alert_error_title), - message); - } - - }); - - } catch (MqttException exception) { - - // Alert user - alertDialog(LoginActivity.this, - getResources().getString(R.string.alert_error_title), - String.format(getResources().getString(R.string.error_connection_failed), - exception.toString())); - - } - - } - - public void alertDialog(Activity activity, String title, String message) { - - AlertDialog alertDialog = new AlertDialog.Builder(activity).create(); - alertDialog.setTitle(title); - alertDialog.setMessage(message); - alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, - getResources().getString(R.string.alert_button_positive_text), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }); - alertDialog.show(); - } - -} diff --git a/app/src/main/java/org/emstrack/hospital/adapters/ListAdapter.java b/app/src/main/java/org/emstrack/hospital/adapters/ListAdapter.java deleted file mode 100644 index 64082393..00000000 --- a/app/src/main/java/org/emstrack/hospital/adapters/ListAdapter.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.emstrack.hospital.adapters; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.v4.app.FragmentManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -//import org.emstrack.hospital.CustomDialog; -import org.emstrack.hospital.interfaces.DataListener; -import org.emstrack.hospital.R; -import org.emstrack.hospital.dialogs.EquipmentBooleanDialog; -import org.emstrack.hospital.dialogs.EquipmentValueDialog; -import org.emstrack.models.HospitalEquipment; - -import java.util.ArrayList; - -/** - * Created by Fabian Choi on 5/16/2017. - */ -public class ListAdapter extends ArrayAdapter { - - private final Context context; - private final ArrayList objects; - private final FragmentManager fragmentManager; - private DataListener dr; - - public ListAdapter(Context context, ArrayList objects, FragmentManager fm) { - super(context, -1, objects); - this.context = context; - this.objects = objects; - this.fragmentManager = fm; - } - - public void setOnDataChangedListener(DataListener dr) { - this.dr = dr; - } - - @Override - @NonNull - public View getView(int position, View convertView, ViewGroup parent) { - - // Get the object, and the type - // Inflate the correct view based on the type and update the parts of the layout -> set onClicks - - // TODO: This needs a revamp; the different equipment types should know how to convert to view - - System.out.println("GETVIEW - Position: " + position); - final HospitalEquipment equipmentItem = objects.get(position); - - LayoutInflater inflater = LayoutInflater.from(context); - - final View row; - - final Character equipmentEtype = equipmentItem.getEquipmentEtype(); - - try { - - // Integer type - if (equipmentEtype == 'I' || equipmentEtype == 'S') { - - System.out.println("Adding Value to List"); - row = inflater.inflate(R.layout.list_item_value, parent, false); - - // Grab the elements of the Value ListItem - System.out.println("Grabbing Elements from Row"); - TextView text = row.findViewById(R.id.valueTextView); - TextView value = row.findViewById(R.id.valueData); - - // Set the elements of the ListItem - System.out.println("Setting Elements in Row"); - text.setText(equipmentItem.getEquipmentName()); - if (equipmentEtype == 'I') - value.setText(equipmentItem.getValue()); - else - value.setText("..."); - // Store value in a tag - row.setTag(equipmentItem.getValue()); - - System.out.println("Setting row onClick Listener"); - row.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - - String title = ((TextView) row.findViewById(R.id.valueTextView)).getText().toString(); - String value = v.getTag().toString(); - Character equipmentType = - ((TextView) row.findViewById(R.id.valueData)).getText() - .equals("...") ? 'S' : 'I'; - String message; - if (equipmentEtype == 'I') - message = context.getResources().getString(R.string.equipment_integer_message); - else - message = context.getResources().getString(R.string.equipment_string_message); - - EquipmentValueDialog vd = EquipmentValueDialog.newInstance(title, message, value); - vd.setOnDataChangedListener(dr); - vd.show(fragmentManager, - "integer_dialog"); - } - - }); - - } else if (equipmentEtype == 'B') { - - System.out.println("Adding Toggle to List"); - row = inflater.inflate(R.layout.list_item_boolean, parent, false); - - // Grab the elements of the Toggle ListItem - TextView text = row.findViewById(R.id.toggleTextView); - ImageView image = row.findViewById(R.id.toggleImage); - - // Set the elements of the ListItem - text.setText(equipmentItem.getEquipmentName()); - row.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - - String title = ((TextView) row.findViewById(R.id.toggleTextView)).getText().toString(); - String message = context.getResources().getString(R.string.equipment_boolean_message); - String value = equipmentItem.getValue(); - boolean toggled = (value.equals("True")); - - EquipmentBooleanDialog td = EquipmentBooleanDialog.newInstance(title, message, toggled, value); - td.setOnDataChangedListener(dr); - - td.show(fragmentManager, - "boolean_dialog"); - } - }); - - // Check which image to set - if (equipmentItem.getValue().equals("True")) { - image.setImageResource(R.drawable.green_check); - } else { - image.setImageResource(R.drawable.redx); - } - - } else { - return new View(context); - } - - } catch (Exception e) { - System.out.println("Caught an Exception"); - return new View(context); - } - - return row; - } // end getView - -} diff --git a/app/src/main/java/org/emstrack/hospital/dialogs/EquipmentBooleanDialog.java b/app/src/main/java/org/emstrack/hospital/dialogs/EquipmentBooleanDialog.java deleted file mode 100644 index 4a5dc4c7..00000000 --- a/app/src/main/java/org/emstrack/hospital/dialogs/EquipmentBooleanDialog.java +++ /dev/null @@ -1,181 +0,0 @@ -package org.emstrack.hospital.dialogs; - -import android.app.Dialog; -import android.graphics.Typeface; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.content.DialogInterface; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; -import android.widget.CompoundButton; -import android.widget.Switch; -import android.widget.TextView; - -import org.emstrack.hospital.interfaces.DataListener; -import org.emstrack.hospital.R; - -/** - * Created by devinhickey on 6/5/17. - */ - -public class EquipmentBooleanDialog extends DialogFragment { - - private String title; - private String message; - private boolean isToggled; - private String updatedData = ""; - private String oldData = ""; - private DataListener dr; - - /** - * Empty Constructor - */ - public EquipmentBooleanDialog() { - - } - - /** - * - * @param title - * @param message - * @return - */ - public static EquipmentBooleanDialog newInstance(String title, String message, boolean isToggled, String data) { - - EquipmentBooleanDialog td = new EquipmentBooleanDialog(); - - Bundle args = new Bundle(); - args.putString("Title", title); - args.putString("Message", message); - args.putBoolean("Toggle", isToggled); - args.putString("Data", data); - - td.setArguments(args); - - return td; - - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - - title = getArguments().getString("Title"); - message = getArguments().getString("Message"); - isToggled = getArguments().getBoolean("Toggle"); - oldData = getArguments().getString("Data"); - - System.out.println("Title: " + title); - System.out.println("Message: " + message); - System.out.println("Toggle: " + isToggled); - System.out.println("Data: " + oldData); - - final AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); -// final LinearLayout ll = new LinearLayout(getActivity().getApplicationContext()); - final Button yesButton = new Button(getActivity().getApplicationContext()); - final Button noButton = new Button(getActivity().getApplicationContext()); - - // Set the data elements - // Check if the resource is available and set the title to account for it - System.out.println("Data = " + oldData); - if (oldData.equals("True")) { - alertBuilder.setTitle((title + " - " + getResources().getString(R.string.equipment_boolean_true))); - } else { - alertBuilder.setTitle((title + " - " + getResources().getString(R.string.equipment_boolean_false))); - } - alertBuilder.setMessage(message); - - LayoutInflater inflater = LayoutInflater.from(getContext()); - - View v = inflater.inflate(R.layout.toggle_dialog, null, false); - alertBuilder.setView(v.findViewById(R.id.toggleDialogLayout)); - - final Switch toggleSwitch = v.findViewById(R.id.availableSwitch); - final TextView availableText = v.findViewById(R.id.availableText); - final TextView unavailableText = v.findViewById(R.id.unavailableText); - - toggleSwitch.setChecked(!isToggled); - toggleText(!isToggled, availableText, unavailableText); - - /** - * OnClicks for the elements of the dialog - */ - - availableText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - toggleSwitch.setChecked(false); - toggleText(false, availableText, unavailableText); - } - }); - - unavailableText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - toggleSwitch.setChecked(true); - toggleText(true, availableText, unavailableText); - } - }); - - toggleSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - toggleText(isChecked, availableText, unavailableText); - } - }); - - alertBuilder.setNeutralButton(getResources().getString(R.string.equipment_bolean_button_positive_text), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (!toggleSwitch.isChecked()) { - updatedData = "True"; - } else { - updatedData = "False"; - } - - // Update the data - onDataChanged(title, updatedData); - dialog.dismiss(); - } - }); - - alertBuilder.setNegativeButton(getResources().getString(R.string.equipment_bolean_button_negative_text), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - updatedData = ""; - dialog.dismiss(); - } - }); - - return alertBuilder.create(); - - } - - private void toggleText (boolean toggle, final TextView availableText, final TextView unavailableText) { - if (!toggle) { - availableText.setTypeface(null, Typeface.BOLD); - availableText.setTextColor(getResources().getColor(R.color.colorGreen)); - unavailableText.setTextColor(getResources().getColor(R.color.colorGrey)); - unavailableText.setTypeface(null, Typeface.NORMAL); - } else { - availableText.setTypeface(null, Typeface.NORMAL); - unavailableText.setTextColor(getResources().getColor(R.color.colorPrimary)); - availableText.setTextColor(getResources().getColor(R.color.colorGrey)); - unavailableText.setTypeface(null, Typeface.BOLD); - } - } - - public void setOnDataChangedListener(DataListener dr) { - this.dr = dr; - } - - public void onDataChanged(String name, String data) { - dr.onDataChanged(name, data); - } - -} diff --git a/app/src/main/java/org/emstrack/hospital/dialogs/EquipmentValueDialog.java b/app/src/main/java/org/emstrack/hospital/dialogs/EquipmentValueDialog.java deleted file mode 100644 index b61b1092..00000000 --- a/app/src/main/java/org/emstrack/hospital/dialogs/EquipmentValueDialog.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.emstrack.hospital.dialogs; - -import android.app.Dialog; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.content.DialogInterface; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.text.InputType; -import android.view.Gravity; -import android.widget.EditText; -import android.widget.LinearLayout; - -import org.emstrack.hospital.interfaces.DataListener; -import org.emstrack.hospital.R; - -/** - * Created by devinhickey on 6/5/17. - */ - -public class EquipmentValueDialog extends DialogFragment { - - private String title; - private String message; - private String updatedData = ""; - private String oldData = ""; - private DataListener dr; - - /** - * Empty Constructor - */ - public EquipmentValueDialog() { - - } - - /** - * - * @param title - * @param message - * @return - */ - public static EquipmentValueDialog newInstance(String title, String message, String data) { - EquipmentValueDialog vd = new EquipmentValueDialog(); - - Bundle args = new Bundle(); - args.putString("Title", title); - args.putString("Message", message); - args.putString("Data", data); - - vd.setArguments(args); - - return vd; - - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - - title = getArguments().getString("Title"); - message = getArguments().getString("Message"); - oldData = getArguments().getString("Data"); - - System.out.println("Title: " + title); - System.out.println("Message: " + message); - System.out.println("Data: " + oldData); - - - final AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); - final EditText valueText = new EditText(getActivity().getApplicationContext()); - - System.out.println("Value Type, adding box"); - - // Set the data elements - alertBuilder.setTitle(title); - alertBuilder.setMessage(message); - - // Initialize the Value Text - valueText.setGravity(Gravity.CENTER_HORIZONTAL); - valueText.setRawInputType(InputType.TYPE_CLASS_NUMBER); - valueText.setTextColor(getResources().getColor(R.color.colorPrimaryDark)); - - // Set the current data and move the cursor to the end - valueText.setText(oldData); - valueText.setSelection(valueText.getText().length()); - - // Create the EditText LayoutParams - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams - (LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.MATCH_PARENT); - valueText.setLayoutParams(params); - - alertBuilder.setView(valueText); - - alertBuilder.setNeutralButton(getResources().getString(R.string.equipment_integer_button_positive_text), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - System.out.println("Update Button Clicked"); - // Update the value - // Grab the new value - updatedData = valueText.getText().toString(); - - // Update the data - onDataChanged(title, updatedData); - dialog.dismiss(); - } - }); - - alertBuilder.setNegativeButton(getResources().getString(R.string.equipment_integer_button_negative_text), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - updatedData = ""; - dialog.dismiss(); - } - }); - - return alertBuilder.create(); - - } - - public void setOnDataChangedListener(DataListener dr) { - this.dr = dr; - } - - public void onDataChanged(String name, String data) { - dr.onDataChanged(name, data); - } - -} diff --git a/app/src/main/java/org/emstrack/hospital/dialogs/LogoutDialog.java b/app/src/main/java/org/emstrack/hospital/dialogs/LogoutDialog.java deleted file mode 100644 index 322c85d8..00000000 --- a/app/src/main/java/org/emstrack/hospital/dialogs/LogoutDialog.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.emstrack.hospital.dialogs; - -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.AlertDialog; -import android.util.Log; - -import org.eclipse.paho.client.mqttv3.MqttException; -import org.emstrack.hospital.HospitalApp; -import org.emstrack.hospital.LoginActivity; -import org.emstrack.hospital.R; -import org.emstrack.mqtt.MqttProfileClient; - -/** - * Created by devinhickey on 5/24/17. - */ - -public class LogoutDialog extends DialogFragment { - - final String TAG = "LogoutDialog"; - - public LogoutDialog() {} - - public static LogoutDialog newInstance() { - return new LogoutDialog(); - } - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - System.out.println("Logout Dialog onCreateDialog"); - - AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity()); - - alertBuilder.setTitle(getResources().getString(R.string.alert_warning)); - alertBuilder.setMessage(getResources().getString(R.string.logout_confirm)); - - // Create the OK button that logs user out - alertBuilder.setNeutralButton(getResources().getString(R.string.alert_button_positive_text), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - System.out.println("OK Button Clicked"); - - // Retrieve client - final MqttProfileClient profileClient = ((HospitalApp) getActivity().getApplication()).getProfileClient(); - try { - profileClient.disconnect(); - } catch (MqttException e) { - Log.d(TAG,"Failed to disconnect."); - } - - Intent rootIntent = new Intent(getActivity(), LoginActivity.class); - rootIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(rootIntent); - - } - }); - - // Create the Cancel Button - alertBuilder.setNegativeButton(getResources().getString(R.string.alert_button_negative_text), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - System.out.println("Cancel Button Clicked"); - dialog.dismiss(); - } - }); - - return alertBuilder.create(); - - } - -} diff --git a/app/src/main/java/org/emstrack/hospital/interfaces/DataListener.java b/app/src/main/java/org/emstrack/hospital/interfaces/DataListener.java deleted file mode 100644 index 518b6974..00000000 --- a/app/src/main/java/org/emstrack/hospital/interfaces/DataListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.emstrack.hospital.interfaces; - -/** - * Created by Fabian Choi on 5/30/2017. - */ - -public interface DataListener { - void onDataChanged(String name, String data); -} diff --git a/app/src/main/res/drawable-hdpi/add_bttn.png b/app/src/main/res/drawable-hdpi/add_bttn.png deleted file mode 100644 index 3f3227a2..00000000 Binary files a/app/src/main/res/drawable-hdpi/add_bttn.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/apple.png b/app/src/main/res/drawable-hdpi/apple.png deleted file mode 100644 index 8d67e7ea..00000000 Binary files a/app/src/main/res/drawable-hdpi/apple.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/checkmark.png b/app/src/main/res/drawable-hdpi/checkmark.png deleted file mode 100644 index 1d9e902b..00000000 Binary files a/app/src/main/res/drawable-hdpi/checkmark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/green_check.png b/app/src/main/res/drawable-hdpi/green_check.png deleted file mode 100644 index db164c76..00000000 Binary files a/app/src/main/res/drawable-hdpi/green_check.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/log_out.png b/app/src/main/res/drawable-hdpi/log_out.png deleted file mode 100644 index 8989e48f..00000000 Binary files a/app/src/main/res/drawable-hdpi/log_out.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/redx.png b/app/src/main/res/drawable-hdpi/redx.png deleted file mode 100644 index ee992211..00000000 Binary files a/app/src/main/res/drawable-hdpi/redx.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/add_bttn.png b/app/src/main/res/drawable-ldpi/add_bttn.png deleted file mode 100644 index d4f1d148..00000000 Binary files a/app/src/main/res/drawable-ldpi/add_bttn.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/apple.png b/app/src/main/res/drawable-ldpi/apple.png deleted file mode 100644 index cc07978a..00000000 Binary files a/app/src/main/res/drawable-ldpi/apple.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/checkmark.png b/app/src/main/res/drawable-ldpi/checkmark.png deleted file mode 100644 index abff56f7..00000000 Binary files a/app/src/main/res/drawable-ldpi/checkmark.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/green_check.png b/app/src/main/res/drawable-ldpi/green_check.png deleted file mode 100644 index 0e2d189d..00000000 Binary files a/app/src/main/res/drawable-ldpi/green_check.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/log_out.png b/app/src/main/res/drawable-ldpi/log_out.png deleted file mode 100644 index 6e893442..00000000 Binary files a/app/src/main/res/drawable-ldpi/log_out.png and /dev/null differ diff --git a/app/src/main/res/drawable-ldpi/redx.png b/app/src/main/res/drawable-ldpi/redx.png deleted file mode 100644 index 1e795c74..00000000 Binary files a/app/src/main/res/drawable-ldpi/redx.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/add_bttn.png b/app/src/main/res/drawable-mdpi/add_bttn.png deleted file mode 100644 index a2d19c38..00000000 Binary files a/app/src/main/res/drawable-mdpi/add_bttn.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/apple.png b/app/src/main/res/drawable-mdpi/apple.png deleted file mode 100644 index 0e2eccdf..00000000 Binary files a/app/src/main/res/drawable-mdpi/apple.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/checkmark.png b/app/src/main/res/drawable-mdpi/checkmark.png deleted file mode 100644 index 474d5456..00000000 Binary files a/app/src/main/res/drawable-mdpi/checkmark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/green_check.png b/app/src/main/res/drawable-mdpi/green_check.png deleted file mode 100644 index ba6b7dbe..00000000 Binary files a/app/src/main/res/drawable-mdpi/green_check.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/log_out.png b/app/src/main/res/drawable-mdpi/log_out.png deleted file mode 100644 index f19e0a61..00000000 Binary files a/app/src/main/res/drawable-mdpi/log_out.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/redx.png b/app/src/main/res/drawable-mdpi/redx.png deleted file mode 100644 index d9d09448..00000000 Binary files a/app/src/main/res/drawable-mdpi/redx.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/add_bttn.png b/app/src/main/res/drawable-xhdpi/add_bttn.png deleted file mode 100644 index 85783f6c..00000000 Binary files a/app/src/main/res/drawable-xhdpi/add_bttn.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/apple.png b/app/src/main/res/drawable-xhdpi/apple.png deleted file mode 100644 index 2dd2769b..00000000 Binary files a/app/src/main/res/drawable-xhdpi/apple.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/button_states.xml b/app/src/main/res/drawable-xhdpi/button_states.xml new file mode 100644 index 00000000..a6c42136 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/button_states.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/checkmark.png b/app/src/main/res/drawable-xhdpi/checkmark.png deleted file mode 100644 index 64c75de2..00000000 Binary files a/app/src/main/res/drawable-xhdpi/checkmark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/green_check.png b/app/src/main/res/drawable-xhdpi/green_check.png deleted file mode 100644 index 9baf9972..00000000 Binary files a/app/src/main/res/drawable-xhdpi/green_check.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/imagebuttons.xml b/app/src/main/res/drawable-xhdpi/imagebuttons.xml new file mode 100644 index 00000000..716793c6 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/imagebuttons.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/log_out.png b/app/src/main/res/drawable-xhdpi/log_out.png deleted file mode 100644 index d6463cd9..00000000 Binary files a/app/src/main/res/drawable-xhdpi/log_out.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/nav_img.png b/app/src/main/res/drawable-xhdpi/nav_img.png new file mode 100644 index 00000000..5ae312a7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/nav_img.png differ diff --git a/app/src/main/res/drawable-xhdpi/panicbutton.png b/app/src/main/res/drawable-xhdpi/panicbutton.png new file mode 100644 index 00000000..36934732 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/panicbutton.png differ diff --git a/app/src/main/res/drawable-xhdpi/redx.png b/app/src/main/res/drawable-xhdpi/redx.png deleted file mode 100644 index b35db24f..00000000 Binary files a/app/src/main/res/drawable-xhdpi/redx.png and /dev/null differ diff --git a/app/src/main/res/drawable/alert.png b/app/src/main/res/drawable/alert.png new file mode 100644 index 00000000..9ea2150f Binary files /dev/null and b/app/src/main/res/drawable/alert.png differ diff --git a/app/src/main/res/drawable/ambulance_blue.xml b/app/src/main/res/drawable/ambulance_blue.xml new file mode 100644 index 00000000..aeac88e8 --- /dev/null +++ b/app/src/main/res/drawable/ambulance_blue.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ambulance_gray.xml b/app/src/main/res/drawable/ambulance_gray.xml new file mode 100644 index 00000000..e5e8a457 --- /dev/null +++ b/app/src/main/res/drawable/ambulance_gray.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ambulance_green.xml b/app/src/main/res/drawable/ambulance_green.xml new file mode 100644 index 00000000..56f73ccc --- /dev/null +++ b/app/src/main/res/drawable/ambulance_green.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ambulance_orange.xml b/app/src/main/res/drawable/ambulance_orange.xml new file mode 100644 index 00000000..15b4ca4c --- /dev/null +++ b/app/src/main/res/drawable/ambulance_orange.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ambulance_purple.xml b/app/src/main/res/drawable/ambulance_purple.xml new file mode 100644 index 00000000..bc529ace --- /dev/null +++ b/app/src/main/res/drawable/ambulance_purple.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ambulance_red.xml b/app/src/main/res/drawable/ambulance_red.xml new file mode 100644 index 00000000..246f0a99 --- /dev/null +++ b/app/src/main/res/drawable/ambulance_red.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ambulance_yellow.xml b/app/src/main/res/drawable/ambulance_yellow.xml new file mode 100644 index 00000000..069c0729 --- /dev/null +++ b/app/src/main/res/drawable/ambulance_yellow.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ambulancelogo.png b/app/src/main/res/drawable/ambulancelogo.png new file mode 100644 index 00000000..5c613c7f Binary files /dev/null and b/app/src/main/res/drawable/ambulancelogo.png differ diff --git a/app/src/main/res/drawable/button_states.xml b/app/src/main/res/drawable/button_states.xml new file mode 100644 index 00000000..a6c42136 --- /dev/null +++ b/app/src/main/res/drawable/button_states.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/hospital_symbol.png b/app/src/main/res/drawable/hospital_symbol.png new file mode 100644 index 00000000..1bdb646a Binary files /dev/null and b/app/src/main/res/drawable/hospital_symbol.png differ diff --git a/app/src/main/res/drawable/ic_ambulance.xml b/app/src/main/res/drawable/ic_ambulance.xml new file mode 100644 index 00000000..4ea4bbc7 --- /dev/null +++ b/app/src/main/res/drawable/ic_ambulance.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cogs.xml b/app/src/main/res/drawable/ic_cogs.xml new file mode 100644 index 00000000..bfcb9991 --- /dev/null +++ b/app/src/main/res/drawable/ic_cogs.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_defibrillator_15.xml b/app/src/main/res/drawable/ic_defibrillator_15.xml new file mode 100644 index 00000000..714be58c --- /dev/null +++ b/app/src/main/res/drawable/ic_defibrillator_15.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_exclamation_triangle.xml b/app/src/main/res/drawable/ic_exclamation_triangle.xml new file mode 100644 index 00000000..78b76172 --- /dev/null +++ b/app/src/main/res/drawable/ic_exclamation_triangle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more.xml b/app/src/main/res/drawable/ic_expand_more.xml new file mode 100644 index 00000000..e8558d07 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_globe.xml b/app/src/main/res/drawable/ic_globe.xml new file mode 100644 index 00000000..3afcf1c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_globe.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_15.xml b/app/src/main/res/drawable/ic_home_15.xml new file mode 100644 index 00000000..d78f8ce6 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_15.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_hospital.xml b/app/src/main/res/drawable/ic_hospital.xml new file mode 100644 index 00000000..ee43d828 --- /dev/null +++ b/app/src/main/res/drawable/ic_hospital.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_hospital_15.xml b/app/src/main/res/drawable/ic_hospital_15.xml new file mode 100644 index 00000000..3b1d4b09 --- /dev/null +++ b/app/src/main/res/drawable/ic_hospital_15.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_circle.xml b/app/src/main/res/drawable/ic_info_circle.xml new file mode 100644 index 00000000..7ce4c237 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_location_arrow.xml b/app/src/main/res/drawable/ic_location_arrow.xml new file mode 100644 index 00000000..246a4282 --- /dev/null +++ b/app/src/main/res/drawable/ic_location_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_marker_15.xml b/app/src/main/res/drawable/ic_marker_15.xml new file mode 100644 index 00000000..5fa7acc8 --- /dev/null +++ b/app/src/main/res/drawable/ic_marker_15.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_shipping_fast.xml b/app/src/main/res/drawable/ic_shipping_fast.xml new file mode 100644 index 00000000..a676d1c1 --- /dev/null +++ b/app/src/main/res/drawable/ic_shipping_fast.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sign_out_alt.xml b/app/src/main/res/drawable/ic_sign_out_alt.xml new file mode 100644 index 00000000..2c32a95a --- /dev/null +++ b/app/src/main/res/drawable/ic_sign_out_alt.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wifi.xml b/app/src/main/res/drawable/ic_wifi.xml new file mode 100644 index 00000000..249f0bb1 --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wrench.xml b/app/src/main/res/drawable/ic_wrench.xml new file mode 100644 index 00000000..ab88fe41 --- /dev/null +++ b/app/src/main/res/drawable/ic_wrench.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/imagebuttons.xml b/app/src/main/res/drawable/imagebuttons.xml new file mode 100644 index 00000000..716793c6 --- /dev/null +++ b/app/src/main/res/drawable/imagebuttons.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/logout.png b/app/src/main/res/drawable/logout.png new file mode 100644 index 00000000..82947771 Binary files /dev/null and b/app/src/main/res/drawable/logout.png differ diff --git a/app/src/main/res/drawable/message.png b/app/src/main/res/drawable/message.png new file mode 100644 index 00000000..18b30955 Binary files /dev/null and b/app/src/main/res/drawable/message.png differ diff --git a/app/src/main/res/drawable/nav_img.png b/app/src/main/res/drawable/nav_img.png new file mode 100644 index 00000000..5ae312a7 Binary files /dev/null and b/app/src/main/res/drawable/nav_img.png differ diff --git a/app/src/main/res/drawable/panicbutton.png b/app/src/main/res/drawable/panicbutton.png new file mode 100644 index 00000000..36934732 Binary files /dev/null and b/app/src/main/res/drawable/panicbutton.png differ diff --git a/app/src/main/res/drawable/spinner_background.9.png b/app/src/main/res/drawable/spinner_background.9.png new file mode 100644 index 00000000..d7933eac Binary files /dev/null and b/app/src/main/res/drawable/spinner_background.9.png differ diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml new file mode 100644 index 00000000..2fd36057 --- /dev/null +++ b/app/src/main/res/layout/about.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_dashboard.xml b/app/src/main/res/layout/activity_dashboard.xml deleted file mode 100644 index a89394cd..00000000 --- a/app/src/main/res/layout/activity_dashboard.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/activity_hospital_list.xml b/app/src/main/res/layout/activity_hospital_list.xml deleted file mode 100644 index 4a00588d..00000000 --- a/app/src/main/res/layout/activity_hospital_list.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - -