Las aplicaciones SOS son básicamente aplicaciones de emergencia avanzadas que pueden rescatarlo a usted y/o a sus seres queridos si usted y/o ellos se encuentran en una situación de emergencia que pone en peligro la vida y necesitan asistencia inmediata. Cuando necesite asistencia personal, puede encender su teléfono y llamar o enviar un mensaje a alguien para pedir ayuda. Pero en una emergencia que pone en peligro la vida, como un ataque, una agresión sexual, un robo, un acoso, un accidente, un incendio, asistencia en el parto, no tenemos tiempo para abrir nuestro teléfono; en cambio, necesitamos algunos métodos de accesibilidad a través de los cuales podamos pedir ayuda. sin operar el teléfono. En este artículo, construiríamos una aplicación de este tipo para Android.
¿Puede pensar en algunas formas más fáciles de estimular algunas funciones en su teléfono, sin encender la pantalla de su teléfono? Una de esas formas es agitando el teléfono. Estaremos creando un servicio, y en ese servicio, escucharíamos un evento de batido por teléfono. Cuando registramos un evento de sacudida, es decir, cuando el usuario sacude el teléfono, enviaríamos la ubicación del usuario con un mensaje predefinido a todos los contactos que el usuario haya agregado previamente a la aplicación. Ahora, con cada lanzamiento de Android, Google ha establecido algunas normas estrictas con respecto a la obtención de la ubicación del usuario, y también es importante en lo que respecta a la seguridad de los datos. Y esto dificulta la creación de una aplicación SOS de este tipo para las versiones más nuevas de Android.
requisitos previos
- Servicios ,
- Gestión de permisos de tiempo de ejecución ,
- Poblando ListView ,
- receptor de difusión ,
- Gestión de base de datos en Android
Implementación paso a paso
Paso 1: Crear un nuevo proyecto
Para crear un nuevo proyecto en Android Studio, consulte Cómo crear/iniciar un nuevo proyecto en Android Studio . Tenga en cuenta que seleccione Java como lenguaje de programación.
Paso 2: Creando el Módulo de Contactos
Cree una carpeta Contactos, en esta, manejaremos todos los archivos que llenarían ListView con los contactos que el usuario selecciona para enviar mensajes en el momento de la emergencia.
Paso 2.1: Creación de una clase modelo para Contacto
Cree una clase modelo que contendrá los datos de Contacto, principalmente Nombre y Número de teléfono. Además del constructor y captadores y definidores habituales, tenemos un método de validación (String) adicional. Este método verifica si el número de teléfono recuperado tiene el formato correcto (+91XXXXXXXXXX) o no. Si no, convierte la string y devuelve la string formateada. A continuación se muestra el código para el archivo ContactModel.java .
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; } }
Paso 2.2: Creación de una clase auxiliar de base de datos
Necesitamos almacenar todos los contactos, los selecciona el usuario, en una base de datos para que esté disponible cada vez que la aplicación lo necesite. Completaremos el ListView con la ayuda de esta base de datos y también en el momento de enviar mensajes, recuperaremos los contactos en una lista de esta base de datos. A continuación se muestra el código para el archivo DbHelper.java .
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(); } }
Paso 2.3: Crear un CustomAdapter.java
Para manejar los datos en ListView, necesitaremos un adaptador personalizado . Agregaremos un LongClickListener en LinerLayout para que cada vez que un usuario desee eliminar un elemento existente de ListView, simplemente puede mantener presionado ese elemento. Y a cambio, mostraríamos un diálogo pidiendo confirmación. Como el usuario confirma, también eliminaremos ese elemento de la base de datos. A continuación se muestra el código para el archivo CustomAdapter.java .
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(); } }
Paso 2.4: item_user.xml
Archivo de diseño para cada elemento en ListView.
XML
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 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>
Paso 3: Creando el Módulo de Servicio
Este módulo contendría toda la funcionalidad necesaria para la detección de vibraciones, la ejecución de servicios y el registro de un receptor de transmisión.
Paso 3.1: Creación de la clase ShakeDetector
Aquí implementamos el SensorEventListener que se usa para recibir notificaciones del SensorManager cuando hay un nuevo o cambio en los datos del sensor. Ahora, para registrar el evento de sacudida, la fuerza G por la cual la experiencia del sensor cuando el usuario sacude el teléfono debe ser mayor a 1G. Esto se debe a que puede haber casos en los que el teléfono pueda temblar mientras está en el bolsillo, en el automóvil, etc. Y también para resolver este inconveniente, incluimos un mecanismo de conteo que contaría la cantidad de sacudidas, es decir, si el usuario sacude el dispositivo. 3 veces consecutivas, entonces registraríamos un evento de batido. Y para ello el tiempo entre las dos sacudidas sucesivas debe ser mínimo, es decir alrededor de 500ms. También haremos que el batido cuente hasta cero después de 3 segundos de inactividad. Esto permitiría al usuario volver a agitar el teléfono para enviar mensajes. A continuación se muestra el código para elArchivo ShakeDetector.java .
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); } } } }
Paso 3.2: Creando el SensorService
Creación de un servicio de sensores. Desde el comienzo de Android 6, Google ha incluido algunos controles de seguridad adicionales con respecto a los servicios en segundo plano. Ahora el manejo de los servicios es completamente diferente a como se hacía anteriormente.
Nuestro enfoque principal al crear esta aplicación debe ser cómo podemos mantener el servicio en funcionamiento incluso cuando la aplicación no se está ejecutando (incluso eliminada de la reciente).
Para que el servicio se ejecute mientras la aplicación host está inactiva, se llama Servicio para ejecutarse en segundo plano. Y para hacer que un servicio se ejecute en segundo plano, necesitamos algunos permisos adicionales. En Android O y superior, no podemos tener un servicio en segundo plano, en su lugar, podemos usar los servicios en primer plano. Los servicios de primer plano realizan operaciones que son perceptibles para el usuario.
Se debe mostrar una advertencia en la barra de estado con una prioridad de PRIORIDAD BAJA o superior para cada operación en primer plano. Los usuarios serían conscientes de que la aplicación se ejecuta en primer plano y usa los recursos de la máquina de esta manera. A menos que el servicio se interrumpa o se retire del primer plano, el mensaje no se puede ignorar.
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); }
Si inicia un servicio con el tipo de retorno START STICKY, se ejecutará en segundo plano incluso si la actividad del host no se ejecuta en primer plano. Si Android tiene que cerrar un programa a la fuerza debido a un error de memoria u otras razones, el servicio se reiniciará sin la intervención del usuario.
Java
@Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); return START_STICKY; }
Para que el usuario sepa que el evento Shake ha sido registrado o que los mensajes han sido entregados, creamos un método vibrate(). Esto hará que el teléfono vibre en un formato de onda definido.
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); } }
Ahora, para obtener la ubicación del usuario, usaremos FusedLocationProviderClient. FusedLocationProviderClient tiene una función denominada getCurremtLocation(). Este método proporciona la ubicación actual del usuario cuando se le solicita. Pero este método requiere que el GPS del teléfono móvil esté encendido. De lo contrario, devolverá una ubicación nula. Desde Android O y superior, para obtener la ubicación o cualquier cosa que pueda revelar la ubicación del usuario a la aplicación, los servicios de ubicación o el GPS deben estar ACTIVADOS. Entonces, que el usuario esté al tanto de los usos de la ubicación por parte de la aplicación.
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) { ... } });
Además, cuando recuperamos la ubicación con éxito, creamos un objeto SMSManager que nos ayudará a enviar mensajes a todos los contactos de la base de datos. En caso de que los servicios de ubicación del usuario no estén activados, podemos generar un mensaje diferente sin las coordenadas. Para que el que recibe el mensaje de emergencia sepa que el dispositivo host no tenía servicios de ubicación en ese momento. Luego puede coordinarse directamente con el departamento de policía cercano, quien luego puede rastrear la ubicación del dispositivo de la persona.
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 " + "http://maps.google.com/?q=" + location.getLatitude() + "," + location.getLongitude(); smsManager.sendTextMessage(c.getPhoneNo(), null, message, null, null); }
Hasta ahora, todo lo que hayamos hecho funcionará hasta que la actividad esté en primer plano o en ejecución. Pero, ¿qué sucede cuando el usuario elimina la aplicación? ¿O bloquea el teléfono? Para ello, creamos un BroadcastReceiver .
A continuación se muestra el código completo del archivo SensorService.java .
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(); } }
Paso 3.3: Crear el receptor de transmisión
Cada vez que se destruye un servicio, se llama al método onDestroy, y usaremos ese método para llamar a un receptor de transmisión antes de que el servicio se destruya.
El receptor de la transmisión, a cambio, vuelve a iniciar el servicio. ¡¡Y nuestro problema está resuelto!! El servicio ahora se ejecuta sin la actividad del host en segundo plano.
A continuación se muestra el código completo del archivo ReactivateService.java .
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)); } } }
Paso 4: Trabajar con MainActivity
Vaya al archivo MainActivity.java y consulte el siguiente código. A continuación se muestra el código del archivo MainActivity.java . Se agregan comentarios dentro del código para comprender el código con más detalle.
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); } } }
Vaya a la aplicación > res > diseño > actividad_principal.xml y agregue el siguiente código a ese archivo. A continuación se muestra el código para el archivo activity_main.xml .
XML
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" 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>
Paso 5: Trabajar con AndroidManifest.xml
XML
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" 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>
Producción:
Alcance futuro
- Puede agregar opciones para emergencias de asistencia personal como un mapa que indica estaciones de policía cercanas, hospitales, taxis, etc.
- Creas una lógica que envía la ubicación del usuario cada 1 o 2 minutos sin que el usuario vuelva a sacudir el dispositivo.
- Puede agregar funciones de grabación de voz y/o video.
- Además, puede agregar una llamada a varias o una sola persona en el momento del batido.
- Puede intentar agregar OpenCellId, esto le permitirá obtener la ubicación de la torre móvil más cercana. Pero recuerde nuevamente que para calcular la ubicación necesitaría cierta información del dispositivo (sin GPS) y eso requeriría que su GPS o los servicios de ubicación estén activados.
- Las puertas a los cambios en la interfaz de usuario siempre están abiertas.
Notas:
1. Permita que la aplicación se inicie automáticamente para usar la aplicación mientras la pantalla está apagada.
2. Elimine cualquier restricción de optimización de batería en la aplicación. Esto podría hacer que Android elimine el servicio.
3. Permita todos los permisos, especialmente permita los permisos de ubicación Permitiendo que la aplicación use la ubicación del dispositivo todo el tiempo. Esto permitiría que el servicio use la ubicación del dispositivo cuando se registra el evento de sacudida.
Enlace GitHub: https://github.com/raghavtilak/SOS