The SOS applications are basically advanced emergency apps that can rescue you and/or your loved ones if you and/or they find themselves in a life-threatening emergency situation and need immediate assistance. When you need some personal assistance, you can actually turn on your phone and can call or message someone for help. But in a life-threatening emergency like attack, sexual assault, robbery, harassment, accident, fire, birth assistance, we don’t have time to open our phone, instead, we need some accessibility methods by which we can reach out for help without actually operating the phone. In this article, we would be building such an application for android.
Can you think of some easiest ways of stimulating some functions in your phone, without actually turning ON your phone’s screen? One such way is by shaking your phone. We will be creating a service, and in that service, we would listen for a Shake Event by the phone. When we register a shake event i.e., when the user shakes the phone, we would send the user’s location with a predefined message to all the contacts which the user has previously added to the app. Now with every release of Android, Google has made some strict regulations regarding fetching of the User’s location, and also it is important when it comes to data security. And this makes it difficult to create such an SOS application for newer versions of Android.
Pre-requisites
- Services,
- Runtime Permission Management,
- Populating ListView,
- Broadcast Receiver,
- Database Management in Android
Step by Step Implementation
Step 1: Create a New Project
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio. Note that select Java as the programming language.
Step 2: Creating the Contacts Module
Create a folder Contacts, in this, we will handle all the files which would populate the ListView by the contacts that the user selects to send messages at the time of emergency.
Step 2.1: Creating model class for Contact
Create a model class that will hold the data of Contact, mainly Name and Phone Number. Apart from the usual constructor and getters and setters, we have an additional validate(String) method. This method checks if the retrieved phone number is in the correct format(+91XXXXXXXXXX) or not. If not, then it converts the string and returns the formatted string. Below is the code for the ContactModel.java file.
Java
public class ContactModel { private int id; private String phoneNo; private String name; // constructor public ContactModel( int id, String name, String phoneNo) { this .id = id; this .phoneNo = validate(phoneNo); this .name = name; } // validate the phone number, and reformat is necessary private String validate(String phone) { // creating StringBuilder for both the cases StringBuilder case1 = new StringBuilder( "+91" ); StringBuilder case2 = new StringBuilder( "" ); // check if the string already has a "+" if (phone.charAt( 0 ) != '+' ) { for ( int i = 0 ; i < phone.length(); i++) { // remove any spaces or "-" if (phone.charAt(i) != '-' && phone.charAt(i) != ' ' ) { case1.append(phone.charAt(i)); } } return case1.toString(); } else { for ( int i = 0 ; i < phone.length(); i++) { // remove any spaces or "-" if (phone.charAt(i) != '-' || phone.charAt(i) != ' ' ) { case2.append(phone.charAt(i)); } } return case2.toString(); } } public String getPhoneNo() { return phoneNo; } public int getId() { return id; } public String getName() { return name; } public void setName(String name) { this .name = name; } } |
Step 2.2: Creating a Database Helper class
We need to store all the contacts, the user selects, into a Database so that it is available every time the app needs it. We will populate the ListView with the help of this database and also at the time of sending messages, we will retrieve the contacts in a list from this database. Below is the code for the DbHelper.java file.
Java
import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import java.util.ArrayList; import java.util.List; public class DbHelper extends SQLiteOpenHelper { // Database Version private static final int DATABASE_VERSION = 1 ; // Database Name private static final String DATABASE_NAME = "contactdata" ; // Country table name private static final String TABLE_NAME= "contacts" ; // Country Table Columns names private static final String KEY_ID = "id" ; private static final String NAME = "Name" ; private static final String PHONENO = "PhoneNo" ; public DbHelper(Context context){ super (context, DATABASE_NAME, null , DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { // create the table for the first time String CREATE_COUNTRY_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + KEY_ID + " INTEGER PRIMARY KEY," + NAME + " TEXT," + PHONENO + " TEXT" + ")" ; db.execSQL(CREATE_COUNTRY_TABLE); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } // method to add the contact public void addcontact(ContactModel contact){ SQLiteDatabase db= this .getWritableDatabase(); ContentValues c= new ContentValues(); c.put(NAME,contact.getName()); c.put(PHONENO,contact.getPhoneNo()); db.insert(TABLE_NAME, null ,c); db.close(); } // method to retrieve all the contacts in List public List<ContactModel> getAllContacts(){ List<ContactModel> list= new ArrayList<>(); String query= "SELECT * FROM " +TABLE_NAME; SQLiteDatabase db= this .getReadableDatabase(); Cursor c=db.rawQuery(query, null ); if (c.moveToFirst()) { do { list.add( new ContactModel(c.getInt( 0 ),c.getString( 1 ),c.getString( 2 ))); } while (c.moveToNext()); } return list; } // get the count of data, this will allow user // to not add more that five contacts in database public int count(){ int count= 0 ; String query= "SELECT COUNT(*) FROM " +TABLE_NAME; SQLiteDatabase db = this .getReadableDatabase(); Cursor c=db.rawQuery(query, null ); if (c.getCount()> 0 ){ c.moveToFirst(); count=c.getInt( 0 ); } c.close(); return count; } // Deleting single country public void deleteContact(ContactModel contact) { SQLiteDatabase db = this .getWritableDatabase(); int i=db.delete(TABLE_NAME,KEY_ID + " = ?" , new String[] { String.valueOf(contact.getId()) }); db.close(); } } |
Step 2.3: Creating a CustomAdapter.java
In order to handle the data in ListView, we will need a Customised Adapter. We will add a LongClickListener on the LinerLayout so that whenever a user wants to delete an existing item from ListView he can simply long-press that item. And in return, we would show a dialog asking for confirmation. As the user confirms, we will also delete that item from the database too. Below is the code for the CustomAdapter.java file.
Java
import android.content.Context; import android.content.DialogInterface; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.raghav.sos.R; import java.util.List; public class CustomAdapter extends ArrayAdapter<ContactModel> { Context context; List<ContactModel> contacts; public CustomAdapter( @NonNull Context context, List<ContactModel> contacts) { super (context, 0 , contacts); this .context = context; this .contacts = contacts; } @Override public View getView( int position, View convertView, ViewGroup parent) { // create a database helper object // to handle the database manipulations DbHelper db = new DbHelper(context); // Get the data item for this position ContactModel c = getItem(position); // Check if an existing view is being reused, otherwise inflate the view if (convertView == null ) { convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_user, parent, false ); } LinearLayout linearLayout = (LinearLayout) convertView.findViewById(R.id.linear); // Lookup view for data population TextView tvName = (TextView) convertView.findViewById(R.id.tvName); TextView tvPhone = (TextView) convertView.findViewById(R.id.tvPhone); // Populate the data into the template // view using the data object tvName.setText(c.getName()); tvPhone.setText(c.getPhoneNo()); linearLayout.setOnLongClickListener( new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { // generate an MaterialAlertDialog Box new MaterialAlertDialogBuilder(context) .setTitle( "Remove Contact" ) .setMessage( "Are you sure want to remove this contact?" ) .setPositiveButton( "YES" , new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { // delete the specified contact from the database db.deleteContact(c); // remove the item from the list contacts.remove(c); // notify the listview that dataset has been changed notifyDataSetChanged(); Toast.makeText(context, "Contact removed!" , Toast.LENGTH_SHORT).show(); } }) .setNegativeButton( "NO" , new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { } }) .show(); return false ; } }); // Return the completed view to render on screen return convertView; } // this method will update the ListView public void refresh(List<ContactModel> list) { contacts.clear(); contacts.addAll(list); notifyDataSetChanged(); } } |
Step 2.4: item_user.xml
Layout file for each item in ListView.
XML
<? xml version = "1.0" encoding = "utf-8" ?> < LinearLayout android:id = "@+id/linear" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:orientation = "vertical" android:padding = "4dp" > < androidx.cardview.widget.CardView android:id = "@+id/cardview" android:layout_width = "match_parent" android:layout_height = "wrap_content" > < LinearLayout android:layout_width = "match_parent" android:layout_height = "match_parent" android:orientation = "vertical" > < TextView android:id = "@+id/tvName" style = "@style/TextAppearance.AppCompat.Medium" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:text = "Name" android:textColor = "@color/design_default_color_secondary" /> < TextView android:id = "@+id/tvPhone" style = "@style/TextAppearance.AppCompat.Large" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:text = "Phone" android:textColor = "@color/design_default_color_secondary_variant" android:textStyle = "bold" /> </ LinearLayout > </ androidx.cardview.widget.CardView > </ LinearLayout > |
Step 3: Creating the Service Module
This module would contain all the necessary functionality for shake detection, running services, and registering a Broadcast Receiver.
Step 3.1: Creating ShakeDetector class
Here we implement the SensorEventListener which is used for receiving notifications from the SensorManager when there is a new or change in sensor data. Now, in order to register the shake event, the G-Force by which the sensor experience when the user shakes the phone must be greater than 1G. This is because there may be cases when the phone might shake while in the pocket, or in the car, etc. And also to resolve this drawback we include a count mechanism that would count the number of shakes, i.e. if the user shakes the device 3 consecutive times, then we would register a shake event. And to do so the time between the two successive shakes should be minimum i.e. around 500ms. We will also make the shake count to zero after 3 seconds of inactivity. This would allow the user to again shake the phone to send messages. Below is the code for the ShakeDetector.java file.
Java
import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; public class ShakeDetector implements SensorEventListener { /* * The gForce that is necessary to register as shake. * Must be greater than 1G (one earth gravity unit). * You can install "G-Force", by Blake La Pierre * from the Google Play Store and run it to see how * many G's it takes to register a shake */ private static final float SHAKE_THRESHOLD_GRAVITY = 2 .7F; private static final int SHAKE_SLOP_TIME_MS = 500 ; private static final int SHAKE_COUNT_RESET_TIME_MS = 3000 ; private OnShakeListener mListener; private long mShakeTimestamp; private int mShakeCount; public void setOnShakeListener(OnShakeListener listener) { this .mListener = listener; } public interface OnShakeListener { public void onShake( int count); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // ignore } @Override public void onSensorChanged(SensorEvent event) { if (mListener != null ) { float x = event.values[ 0 ]; float y = event.values[ 1 ]; float z = event.values[ 2 ]; float gX = x / SensorManager.GRAVITY_EARTH; float gY = y / SensorManager.GRAVITY_EARTH; float gZ = z / SensorManager.GRAVITY_EARTH; // gForce will be close to 1 when there is no movement. Float f = new Float(gX * gX + gY * gY + gZ * gZ); Double d = Math.sqrt(f.doubleValue()); float gForce = d.floatValue(); if (gForce > SHAKE_THRESHOLD_GRAVITY) { final long now = System.currentTimeMillis(); // ignore shake events too close to each other (500ms) if (mShakeTimestamp + SHAKE_SLOP_TIME_MS > now) { return ; } // reset the shake count after 3 seconds of no shakes if (mShakeTimestamp + SHAKE_COUNT_RESET_TIME_MS < now) { mShakeCount = 0 ; } mShakeTimestamp = now; mShakeCount++; mListener.onShake(mShakeCount); } } } } |
Step 3.2: Creating the SensorService
Creating a sensor service. From the commencement of Android 6, Google has included some extra security checks regarding background services. Now handling of services is completely different as it was done previously.
Our main focus while building this app should be on how we can keep the service running even when the app is not running (even removed from the recent).
For service to run while the host application is Dead, is called Service to run in the background. And to make a service run in the background we need some extra permissions. In Android O and above we can’t have background service, instead, we can use the Foreground Services. Foreground services perform operations that are noticeable to the user.
A status bar warning with a priority of PRIORITY LOW or higher must be shown for each foreground operation. Users would be conscious that the app is running in the foreground and using machine resources in this manner. Unless the service is discontinued or withdrawn from the foreground, the message cannot be ignored.
Java
@RequiresApi (Build.VERSION_CODES.O) private void startMyOwnForeground() { String NOTIFICATION_CHANNEL_ID = "example.permanence" ; String channelName = "Background Service" ; NotificationChannel chan = new NotificationChannel( NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_MIN); NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); assert manager != null ; manager.createNotificationChannel(chan); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( this , NOTIFICATION_CHANNEL_ID); Notification notification = notificationBuilder.setOngoing( true ) .setContentTitle( "You are protected." ) .setContentText( "We are there for you" ) // this is important, otherwise the // notification will show the way you want i.e. // it will show some default notification .setSmallIcon(R.drawable.ic_launcher_foreground) .setPriority(NotificationManager.IMPORTANCE_MIN) .setCategory(Notification.CATEGORY_SERVICE) .build(); startForeground( 2 , notification); } |
If you start a service starts with the START STICKY return type, it will run in the background even if the host activity is not running in the foreground. If Android has to forcibly close a program due to a memory error or other reasons, the service will be restarted without the user’s intervention.
Java
@Override public int onStartCommand(Intent intent, int flags, int startId) { super .onStartCommand(intent, flags, startId); return START_STICKY; } |
In order to make the user aware that the Shake event has been registered or say the messages have been delivered, we create a vibrate() method. This will make the phone vibrate in a defined wave format.
Java
// method to vibrate the phone public void vibrate() { final Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE); VibrationEffect vibEff; // Android Q and above have some predefined vibrating // patterns if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { vibEff = VibrationEffect.createPredefined(VibrationEffect.EFFECT_DOUBLE_CLICK); vibrator.cancel(); vibrator.vibrate(vibEff); } else { vibrator.vibrate( 500 ); } } |
Now, in order to fetch the user location, we will use FusedLocationProviderClient. The FusedLocationProviderClient has a function named getCurremtLocation(). This method provides the current location of the user when asked for. But this method requires the mobile phone’s GPS to be turned ON. Otherwise, it will return a null location. From Android O and above, in order to fetch location or anything which would reveal the user’s location to the app, the location services or the GPS should be turned ON. So, that the user is aware of the location usages by the app.
Java
FusedLocationProviderClient fusedLocationClient = LocationServices.getFusedLocationProviderClient(getApplicationContext()); fusedLocationClient.getCurrentLocation(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY, new CancellationToken() { @Override public boolean isCancellationRequested() { return false ; } @NonNull @Override public CancellationToken onCanceledRequested( @NonNull OnTokenCanceledListener onTokenCanceledListener) { return null ; } }) .addOnSuccessListener( new OnSuccessListener<Location>() { @Override public void onSuccess(Location location) { // check if location is null // for both the cases we will create // different messages if (location != null ) { ... } else { ... } } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure( @NonNull Exception e) { ... } }); |
Further, when we retrieve the location successfully, we Create a SMSManager object which will help us to send messages to all the contacts from the database. In case the user’s location services are not ON, we can generate a different message without the coordinates. So that one receiving the emergency message knows that the host device didn’t have location services on at that moment. He can then directly coordinate with the nearby Police department who can then track the person’s device location.
Java
SmsManager smsManager = SmsManager.getDefault(); DbHelper db = new DbHelper(SensorService. this ); List<ContactModel> list = db.getAllContacts(); for (ContactModel c : list) { String message = "Hey, " + c.getName() + "I am in DANGER, i need help. Please urgently reach me out. Here are my coordinates.\n " + location.getLatitude() + "," + location.getLongitude(); smsManager.sendTextMessage(c.getPhoneNo(), null , message, null , null ); } |
Till now whatever we have done will work until the activity is in Foreground or Running. But what when the user kills the application? Or locks the phone? For this, we create a BroadcastReceiver.
Below is the complete code for the SensorService.java file.
Java
import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.hardware.Sensor; import android.hardware.SensorManager; import android.location.Location; import android.os.Build; import android.os.IBinder; import android.os.VibrationEffect; import android.os.Vibrator; import android.telephony.SmsManager; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; import com.google.android.gms.location.FusedLocationProviderClient; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import com.google.android.gms.tasks.CancellationToken; import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; import com.google.android.gms.tasks.OnTokenCanceledListener; import com.raghav.sos.Contacts.ContactModel; import com.raghav.sos.Contacts.DbHelper; import com.raghav.sos.R; import java.util.List; public class SensorService extends Service { private SensorManager mSensorManager; private Sensor mAccelerometer; private ShakeDetector mShakeDetector; public SensorService() { } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException( "Not yet implemented" ); } @Override public int onStartCommand(Intent intent, int flags, int startId) { super .onStartCommand(intent, flags, startId); return START_STICKY; } @Override public void onCreate() { super .onCreate(); // start the foreground service if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startMyOwnForeground(); else startForeground( 1 , new Notification()); // ShakeDetector initialization mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mShakeDetector = new ShakeDetector(); mShakeDetector.setOnShakeListener( new ShakeDetector.OnShakeListener() { @SuppressLint ( "MissingPermission" ) @Override public void onShake( int count) { // check if the user has shacked // the phone for 3 time in a row if (count == 3 ) { // vibrate the phone vibrate(); // create FusedLocationProviderClient to get the user location FusedLocationProviderClient fusedLocationClient = LocationServices.getFusedLocationProviderClient(getApplicationContext()); // use the PRIORITY_BALANCED_POWER_ACCURACY // so that the service doesn't use unnecessary power via GPS // it will only use GPS at this very moment fusedLocationClient.getCurrentLocation(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY, new CancellationToken() { @Override public boolean isCancellationRequested() { return false ; } @NonNull @Override public CancellationToken onCanceledRequested( @NonNull OnTokenCanceledListener onTokenCanceledListener) { return null ; } }).addOnSuccessListener( new OnSuccessListener<Location>() { @Override public void onSuccess(Location location) { // check if location is null // for both the cases we will // create different messages if (location != null ) { // get the SMSManager SmsManager smsManager = SmsManager.getDefault(); // get the list of all the contacts in Database DbHelper db = new DbHelper(SensorService. this ); List<ContactModel> list = db.getAllContacts(); // send SMS to each contact for (ContactModel c : list) { String message = "Hey, " + c.getName() + "I am in DANGER, i need help. Please urgently reach me out. Here are my coordinates.\n " + "http://maps.google.com/?q=" + location.getLatitude() + "," + location.getLongitude(); smsManager.sendTextMessage(c.getPhoneNo(), null , message, null , null ); } } else { String message = "I am in DANGER, i need help. Please urgently reach me out.\n" + "GPS was turned off.Couldn't find location. Call your nearest Police Station." ; SmsManager smsManager = SmsManager.getDefault(); DbHelper db = new DbHelper(SensorService. this ); List<ContactModel> list = db.getAllContacts(); for (ContactModel c : list) { smsManager.sendTextMessage(c.getPhoneNo(), null , message, null , null ); } } } }).addOnFailureListener( new OnFailureListener() { @Override public void onFailure( @NonNull Exception e) { Log.d( "Check: " , "OnFailure" ); String message = "I am in DANGER, i need help. Please urgently reach me out.\n" + "GPS was turned off.Couldn't find location. Call your nearest Police Station." ; SmsManager smsManager = SmsManager.getDefault(); DbHelper db = new DbHelper(SensorService. this ); List<ContactModel> list = db.getAllContacts(); for (ContactModel c : list) { smsManager.sendTextMessage(c.getPhoneNo(), null , message, null , null ); } } }); } } }); // register the listener mSensorManager.registerListener(mShakeDetector, mAccelerometer, SensorManager.SENSOR_DELAY_UI); } // method to vibrate the phone public void vibrate() { final Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); VibrationEffect vibEff; // Android Q and above have some predefined vibrating patterns if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { vibEff = VibrationEffect.createPredefined(VibrationEffect.EFFECT_DOUBLE_CLICK); vibrator.cancel(); vibrator.vibrate(vibEff); } else { vibrator.vibrate( 500 ); } } // For Build versions higher than Android Oreo, we launch // a foreground service in a different way. This is due to the newly // implemented strict notification rules, which require us to identify // our own notification channel in order to view them correctly. @RequiresApi (Build.VERSION_CODES.O) private void startMyOwnForeground() { String NOTIFICATION_CHANNEL_ID = "example.permanence" ; String channelName = "Background Service" ; NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_MIN); NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); assert manager != null ; manager.createNotificationChannel(chan); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( this , NOTIFICATION_CHANNEL_ID); Notification notification = notificationBuilder.setOngoing( true ) .setContentTitle( "You are protected." ) .setContentText( "We are there for you" ) // this is important, otherwise the notification will show the way // you want i.e. it will show some default notification .setSmallIcon(R.drawable.ic_launcher_foreground) .setPriority(NotificationManager.IMPORTANCE_MIN) .setCategory(Notification.CATEGORY_SERVICE) .build(); startForeground( 2 , notification); } @Override public void onDestroy() { // create an Intent to call the Broadcast receiver Intent broadcastIntent = new Intent(); broadcastIntent.setAction( "restartservice" ); broadcastIntent.setClass( this , ReactivateService. class ); this .sendBroadcast(broadcastIntent); super .onDestroy(); } } |
Step 3.3: Creating the Broadcast Receiver
Whenever a service is destroyed, the onDestroy method is called, and we will use that method to call a broadcast receiver before the service is actually destroyed.
The broadcast receiver in return again starts the service. And our problem is resolved!! The service now runs without the host activity in the background.
Below is the complete code for the ReactivateService.java file.
Java
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Build; import android.util.Log; public class ReactivateService extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.d( "Check: " , "Receiver Started" ); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService( new Intent(context, SensorService. class )); } else { context.startService( new Intent(context, SensorService. class )); } } } |
Step 4: Working with the MainActivity
Go to the MainActivity.java file and refer to the following code. Below is the code for the MainActivity.java file. Comments are added inside the code to understand the code in more detail.
Java
import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.provider.ContactsContract; import android.provider.Settings; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ListView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import com.raghav.sos.Contacts.ContactModel; import com.raghav.sos.Contacts.CustomAdapter; import com.raghav.sos.Contacts.DbHelper; import com.raghav.sos.ShakeServices.ReactivateService; import com.raghav.sos.ShakeServices.SensorService; import java.util.List; public class MainActivity extends AppCompatActivity { private static final int IGNORE_BATTERY_OPTIMIZATION_REQUEST = 1002 ; private static final int PICK_CONTACT = 1 ; // create instances of various classes to be used Button button1; ListView listView; DbHelper db; List<ContactModel> list; CustomAdapter customAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); // check for runtime permissions if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ActivityCompat.checkSelfPermission( this , Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_DENIED) { requestPermissions( new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.SEND_SMS, Manifest.permission.READ_CONTACTS}, 100 ); } } // this is a special permission required only by devices using // Android Q and above. The Access Background Permission is responsible // for populating the dialog with "ALLOW ALL THE TIME" option if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { requestPermissions( new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 100 ); } // check for BatteryOptimization, PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName())) { askIgnoreOptimization(); } } // start the service SensorService sensorService = new SensorService(); Intent intent = new Intent( this , sensorService.getClass()); if (!isMyServiceRunning(sensorService.getClass())) { startService(intent); } button1 = findViewById(R.id.Button1); listView = (ListView) findViewById(R.id.ListView); db = new DbHelper( this ); list = db.getAllContacts(); customAdapter = new CustomAdapter( this , list); listView.setAdapter(customAdapter); button1.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { // calling of getContacts() if (db.count() != 5 ) { Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); startActivityForResult(intent, PICK_CONTACT); } else { Toast.makeText(MainActivity. this , "Can't Add more than 5 Contacts" , Toast.LENGTH_SHORT).show(); } } }); } // method to check if the service is running private boolean isMyServiceRunning(Class<?> serviceClass) { ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { if (serviceClass.getName().equals(service.service.getClassName())) { Log.i( "Service status" , "Running" ); return true ; } } Log.i( "Service status" , "Not running" ); return false ; } @Override protected void onDestroy() { Intent broadcastIntent = new Intent(); broadcastIntent.setAction( "restartservice" ); broadcastIntent.setClass( this , ReactivateService. class ); this .sendBroadcast(broadcastIntent); super .onDestroy(); } @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int [] grantResults) { super .onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 100 ) { if (grantResults[ 0 ] == PackageManager.PERMISSION_DENIED) { Toast.makeText( this , "Permissions Denied!\n Can't use the App!" , Toast.LENGTH_SHORT).show(); } } } @Override protected void onActivityResult( int requestCode, int resultCode, @Nullable Intent data) { super .onActivityResult(requestCode, resultCode, data); // get the contact from the PhoneBook of device switch (requestCode) { case (PICK_CONTACT): if (resultCode == Activity.RESULT_OK) { Uri contactData = data.getData(); Cursor c = managedQuery(contactData, null , null , null , null ); if (c.moveToFirst()) { String id = c.getString(c.getColumnIndexOrThrow(ContactsContract.Contacts._ID)); String hasPhone = c.getString(c.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)); String phone = null ; try { if (hasPhone.equalsIgnoreCase( "1" )) { Cursor phones = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null , ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + id, null , null ); phones.moveToFirst(); phone = phones.getString(phones.getColumnIndex( "data1" )); } String name = c.getString(c.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)); db.addcontact( new ContactModel( 0 , name, phone)); list = db.getAllContacts(); customAdapter.refresh(list); } catch (Exception ex) { } } } break ; } } // this method prompts the user to remove any // battery optimisation constraints from the App private void askIgnoreOptimization() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { @SuppressLint ( "BatteryLife" ) Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse( "package:" + getPackageName())); startActivityForResult(intent, IGNORE_BATTERY_OPTIMIZATION_REQUEST); } } } |
Navigate to the app > res > layout > activity_main.xml and add the below code to that file. Below is the code for the activity_main.xml file.
XML
<? xml version = "1.0" encoding = "utf-8" ?> < LinearLayout android:layout_width = "match_parent" android:layout_height = "match_parent" android:orientation = "vertical" tools:context = ".MainActivity" > < Button android:id = "@+id/Button1" android:layout_width = "match_parent" android:layout_height = "wrap_content" android:layout_margin = "12dp" android:background = "#0F9D58" android:text = "Add Emergency Contact " android:textColor = "#FFFFFF" /> < ListView android:id = "@+id/ListView" android:layout_width = "match_parent" android:layout_height = "match_parent" /> </ LinearLayout > |
Step 5: Working with AndroidManifest.xml
XML
<? xml version = "1.0" encoding = "utf-8" ?> package = "com.raghav.sos" > < uses-permission android:name = "android.permission.READ_CONTACTS" /> < uses-permission android:name = "android.permission.VIBRATE" /> < uses-permission android:name = "android.permission.SEND_SMS" /> < uses-permission android:name = "android.permission.ACCESS_FINE_LOCATION" /> < uses-permission android:name = "android.permission.ACCESS_COARSE_LOCATION" /> < uses-permission android:name = "android.permission.FOREGROUND_SERVICE" /> <!--This permission is necessary for devices with Android O and above, so that we can use the location ALL THE TIME--> < uses-permission android:name = "android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- We also ask user to remove any battery optimization constraints during runtime --> < uses-permission android:name = "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> < application android:allowBackup = "true" android:icon = "@mipmap/ic_launcher" android:label = "@string/app_name" android:roundIcon = "@mipmap/ic_launcher_round" android:supportsRtl = "true" android:theme = "@style/Theme.SOS" > <!-- register the receiver --> < receiver android:name = ".ShakeServices.ReactivateService" android:enabled = "true" android:exported = "true" /> <!-- register the service --> < service android:name = ".ShakeServices.SensorService" android:enabled = "true" android:exported = "true" /> < activity android:name = ".MainActivity" > < intent-filter > < action android:name = "android.intent.action.MAIN" /> < category android:name = "android.intent.category.LAUNCHER" /> </ intent-filter > </ activity > </ application > </ manifest > |
Output:
Future Scope
- You can add options for personal assistance emergencies like a map that indicates nearby police stations, hospitals, cabs, etc.
- You create a logic that sends the user location every 1 or 2 minutes without the user shaking the device again.
- You can add voice or/and video recording functionality.
- Also, you can add a call to multiple or a single person at the time of shake.
- You can try to add OpenCellId, this will allow you to fetch the location of the nearest mobile tower. But again remember that in order to calculate the location you would require some device information(no GPS) and that would require your GPS or location services to be turned ON.
- Doors to changes in the UI are always open.
Notes:
1. Allow the app to autostart, in order to use the app while the screen is off.
2. Remove any battery optimization constraints on the app. This might make Android kill the service.
3. Allow all the permissions, especially allow location permissions by Allowing the app to use the device location all the time. This would allow the service to use the device location when the shake event is registered.
GitHub Link: https://github.com/raghavtilak/SOS