Display a notification when a new call is available.

This commit is contained in:
Pierre HUBERT 2019-02-23 16:02:26 +01:00
parent 18c8d7e391
commit a5c8b1825f
14 changed files with 424 additions and 21 deletions

View File

@ -76,6 +76,14 @@
android:name=".ui.activities.CallActivity" android:name=".ui.activities.CallActivity"
android:label="@string/activity_call_label"/> android:label="@string/activity_call_label"/>
<!-- New calls available receiver -->
<receiver android:name=".ui.receivers.PendingCallsBroadcastReceiver" android:exported="false">
<intent-filter>
<action android:name="org.communiquons.android.comunic.client.NEW_CALLS_AVAILABLE" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@ -1,6 +1,7 @@
package org.communiquons.android.comunic.client.data.helpers; package org.communiquons.android.comunic.client.data.helpers;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import org.communiquons.android.comunic.client.data.enums.MemberCallStatus; import org.communiquons.android.comunic.client.data.enums.MemberCallStatus;
@ -9,6 +10,7 @@ import org.communiquons.android.comunic.client.data.models.APIResponse;
import org.communiquons.android.comunic.client.data.models.CallInformation; import org.communiquons.android.comunic.client.data.models.CallInformation;
import org.communiquons.android.comunic.client.data.models.CallMember; import org.communiquons.android.comunic.client.data.models.CallMember;
import org.communiquons.android.comunic.client.data.models.CallsConfiguration; import org.communiquons.android.comunic.client.data.models.CallsConfiguration;
import org.communiquons.android.comunic.client.data.models.NextPendingCallInformation;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -106,7 +108,7 @@ public class CallsHelper extends BaseHelper {
//Execute request //Execute request
APIResponse response = request.exec(); APIResponse response = request.exec();
return JSONObjectToCallInformation(response.getJSONObject()); return JSONObjectToCallInformation(response.getJSONObject(), null);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -115,6 +117,60 @@ public class CallsHelper extends BaseHelper {
} }
/**
* Get the next pending call for a user
*
* @return Next pending call for a user
*/
@Nullable
public NextPendingCallInformation getNextPendingCall(){
APIRequest request = new APIRequest(getContext(), "calls/nextPending");
try {
JSONObject object = request.exec().getJSONObject();
//Check if there is no pending call available
NextPendingCallInformation call = new NextPendingCallInformation();
if(object.has("notice")){
call.setHasPendingCall(false);
return call;
}
call.setHasPendingCall(true);
JSONObjectToCallInformation(object, call);
return call;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Try to get and return call information
*
* @param call Target call information
* @return The name of call / null in case of failure
*/
@Nullable
public String getCallName(@NonNull CallInformation call){
//Get call name
String name = new ConversationsListHelper(getContext())
.getConversationName(call.getConversationID());
if(name == null)
return null;
call.setCallName(name);
return name;
}
/** /**
* Turn a {@link JSONObject} object into a {@link CallsConfiguration} object. * Turn a {@link JSONObject} object into a {@link CallsConfiguration} object.
@ -149,13 +205,18 @@ public class CallsHelper extends BaseHelper {
* Turn a {@link JSONObject} into {@link CallInformation} object * Turn a {@link JSONObject} into {@link CallInformation} object
* *
* @param object object to convert * @param object object to convert
* @param call Call object to fill (null = none)
* @return Generated CallInformation object * @return Generated CallInformation object
* @throws JSONException in case of failure * @throws JSONException in case of failure
*/ */
private static CallInformation JSONObjectToCallInformation(JSONObject object) private static CallInformation JSONObjectToCallInformation(JSONObject object,
@Nullable CallInformation call)
throws JSONException { throws JSONException {
CallInformation call = new CallInformation(); //Check if object has to be instanced
if(call == null)
call = new CallInformation();
call.setId(object.getInt("id")); call.setId(object.getInt("id"));
call.setConversationID(object.getInt("conversation_id")); call.setConversationID(object.getInt("conversation_id"));
call.setLastActive(object.getInt("last_active")); call.setLastActive(object.getInt("last_active"));

View File

@ -56,6 +56,15 @@ public class ConversationsListHelper {
this.dbHelper = dbHelper; this.dbHelper = dbHelper;
} }
/**
* The constructor of the class
*
* @param context The context of execution of the application
*/
public ConversationsListHelper(Context context){
this(context, DatabaseHelper.getInstance(context));
}
/** /**
* Get the list of conversation or null in case of failure * Get the list of conversation or null in case of failure
* *
@ -147,17 +156,17 @@ public class ConversationsListHelper {
/** /**
* Get the display name of a conversation * Get the display name of a conversation
* *
* @param infos Informations about a conversation * @param info Information about a conversation
* @return The name of the conversation * @return The name of the conversation
*/ */
public String getDisplayName(ConversationsInfo infos){ public String getDisplayName(ConversationsInfo info){
//Check if a specific name has been specified //Check if a specific name has been specified
if(infos.hasName()) if(info.hasName())
return infos.getName(); return info.getName();
//Get the list of members of the conversation //Get the list of members of the conversation
ArrayList<Integer> members = infos.getMembers(); ArrayList<Integer> members = info.getMembers();
//Get the ID of the three first members //Get the ID of the three first members
ArrayList<Integer> membersToGet = new ArrayList<>(); ArrayList<Integer> membersToGet = new ArrayList<>();
@ -203,6 +212,24 @@ public class ConversationsListHelper {
return name; return name;
} }
/**
* Get the name of a conversation specified by its ID
*
* @param convID The ID of the target conversation
* @return The name of the conversation / null in case of failure
*/
@Nullable
public String getConversationName(int convID){
ConversationsInfo info = getInfosSingle(convID, true);
if(info == null)
return null;
return getDisplayName(info);
}
/** /**
* Delete a conversation specified by its ID * Delete a conversation specified by its ID
* *

View File

@ -18,6 +18,9 @@ public class CallInformation {
private ArrayList<CallMember> members = null; private ArrayList<CallMember> members = null;
private String callName = null;
public int getId() { public int getId() {
return id; return id;
} }
@ -54,7 +57,29 @@ public class CallInformation {
members.add(member); members.add(member);
} }
/**
* Check out whether all the members of the call left it or not except a specific user
*
* @return TRUE if all members except specified user ID left the call / FALSE else
*/
public boolean hasAllMembersLeftCallExcept(int userID){
for(CallMember member : members)
if(!member.leftCall() && member.getUserID() != userID)
return false;
return true;
}
public void setMembers(ArrayList<CallMember> members) { public void setMembers(ArrayList<CallMember> members) {
this.members = members; this.members = members;
} }
public String getCallName() {
return callName;
}
public void setCallName(String callName) {
this.callName = callName;
}
} }

View File

@ -56,4 +56,13 @@ public class CallMember {
public void setStatus(MemberCallStatus status) { public void setStatus(MemberCallStatus status) {
this.status = status; this.status = status;
} }
/**
* Check out whether the member left the call or not
*
* @return TRUE if the user is considered as out of the conversation
*/
public boolean leftCall(){
return status == MemberCallStatus.HANG_UP || status == MemberCallStatus.REJECTED;
}
} }

View File

@ -0,0 +1,21 @@
package org.communiquons.android.comunic.client.data.models;
/**
* Pending call information object
*
* @author Pierre HUBERT
*/
public class NextPendingCallInformation extends CallInformation {
//Private fields
private boolean hasPendingCall;
public boolean isHasPendingCall() {
return hasPendingCall;
}
public void setHasPendingCall(boolean hasPendingCall) {
this.hasPendingCall = hasPendingCall;
}
}

View File

@ -1,7 +1,7 @@
package org.communiquons.android.comunic.client.data.models; package org.communiquons.android.comunic.client.data.models;
/** /**
* Notifications count service * Notifications count class
* *
* @author Pierre HUBERT * @author Pierre HUBERT
* Created by pierre on 4/9/18. * Created by pierre on 4/9/18.
@ -13,7 +13,7 @@ public class NotificationsCount {
private int notificationsCount; private int notificationsCount;
private int conversationsCount; private int conversationsCount;
private int friendsRequestsCount; private int friendsRequestsCount;
private int pendingCalls; private int pendingCalls = -1;
//Set and get notifications count //Set and get notifications count

View File

@ -5,6 +5,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build; import android.os.Build;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
@ -15,14 +16,15 @@ import android.util.Log;
import org.communiquons.android.comunic.client.R; import org.communiquons.android.comunic.client.R;
import org.communiquons.android.comunic.client.data.helpers.AccountHelper; import org.communiquons.android.comunic.client.data.helpers.AccountHelper;
import org.communiquons.android.comunic.client.data.helpers.CallsHelper; import org.communiquons.android.comunic.client.data.helpers.CallsHelper;
import org.communiquons.android.comunic.client.data.models.NotificationsCount;
import org.communiquons.android.comunic.client.data.helpers.NotificationsHelper; import org.communiquons.android.comunic.client.data.helpers.NotificationsHelper;
import org.communiquons.android.comunic.client.data.models.NotificationsCount;
import org.communiquons.android.comunic.client.data.utils.PreferencesUtils; import org.communiquons.android.comunic.client.data.utils.PreferencesUtils;
import org.communiquons.android.comunic.client.ui.activities.MainActivity; import org.communiquons.android.comunic.client.ui.activities.MainActivity;
import org.communiquons.android.comunic.client.ui.receivers.PendingCallsBroadcastReceiver;
import java.util.Objects;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static org.communiquons.android.comunic.client.ui.Constants.IntentActions.ACTION_NOTIFY_NEW_CALLS_AVAILABLE;
import static org.communiquons.android.comunic.client.ui.Constants.Notifications.MAIN_NOTIFICATION_ID;
import static org.communiquons.android.comunic.client.ui.Constants.NotificationsChannels.GLOBAL_CHANNEL_DESCRIPTION; import static org.communiquons.android.comunic.client.ui.Constants.NotificationsChannels.GLOBAL_CHANNEL_DESCRIPTION;
import static org.communiquons.android.comunic.client.ui.Constants.NotificationsChannels.GLOBAL_CHANNEL_ID; import static org.communiquons.android.comunic.client.ui.Constants.NotificationsChannels.GLOBAL_CHANNEL_ID;
import static org.communiquons.android.comunic.client.ui.Constants.NotificationsChannels.GLOBAL_CHANNEL_NAME; import static org.communiquons.android.comunic.client.ui.Constants.NotificationsChannels.GLOBAL_CHANNEL_NAME;
@ -54,11 +56,6 @@ public class NotificationsService extends IntentService {
public static final String BROADCAST_EXTRA_UNREAD_CONVERSATIONS = "UnreadConversations"; public static final String BROADCAST_EXTRA_UNREAD_CONVERSATIONS = "UnreadConversations";
public static final String BROADCAST_EXTRA_NUMBER_FRIENDSHIP_REQUESTS = "NumberFriendsRequests"; public static final String BROADCAST_EXTRA_NUMBER_FRIENDSHIP_REQUESTS = "NumberFriendsRequests";
/**
* Main notification ID
*/
private final static int MAIN_NOTIFICATION_ID = 0;
/** /**
* Keep run status * Keep run status
*/ */
@ -81,6 +78,13 @@ public class NotificationsService extends IntentService {
*/ */
public NotificationsService(){ public NotificationsService(){
super("NotificationsService"); super("NotificationsService");
//Register calls broadcast register
IntentFilter callFilters = new IntentFilter();
callFilters.addAction(ACTION_NOTIFY_NEW_CALLS_AVAILABLE);
LocalBroadcastManager.getInstance(this)
.registerReceiver(new PendingCallsBroadcastReceiver(), callFilters);
} }
/** /**
@ -161,7 +165,22 @@ public class NotificationsService extends IntentService {
.putExtra(BROADCAST_EXTRA_UNREAD_CONVERSATIONS, count.getConversationsCount()) .putExtra(BROADCAST_EXTRA_UNREAD_CONVERSATIONS, count.getConversationsCount())
.putExtra(BROADCAST_EXTRA_NUMBER_FRIENDSHIP_REQUESTS, count.getFriendsRequestsCount()); .putExtra(BROADCAST_EXTRA_NUMBER_FRIENDSHIP_REQUESTS, count.getFriendsRequestsCount());
//Send new calls information
LocalBroadcastManager.getInstance(this).sendBroadcast(pushIntent); LocalBroadcastManager.getInstance(this).sendBroadcast(pushIntent);
//If new calls are available, notify system
if(CallsHelper.IsCallSystemAvailable()){
if(count.hasPendingCalls()) {
Intent callIntent = new Intent(this, PendingCallsBroadcastReceiver.class);
callIntent.setAction(ACTION_NOTIFY_NEW_CALLS_AVAILABLE);
LocalBroadcastManager.getInstance(this).sendBroadcast(callIntent);
}
else
PendingCallsBroadcastReceiver.RemoveCallNotification(this);
}
} }
Log.v(TAG, "Stop service"); Log.v(TAG, "Stop service");
@ -193,6 +212,7 @@ public class NotificationsService extends IntentService {
//Get notification manager to push notification //Get notification manager to push notification
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(MAIN_NOTIFICATION_ID, mBuilder.build()); notificationManager.notify(MAIN_NOTIFICATION_ID, mBuilder.build());
} }
/** /**

View File

@ -1,7 +1,5 @@
package org.communiquons.android.comunic.client.ui; package org.communiquons.android.comunic.client.ui;
import android.app.NotificationManager;
/** /**
* UI constants * UI constants
* *
@ -63,6 +61,19 @@ public final class Constants {
} }
/**
* Intents actions
*/
public final class IntentActions {
/**
* Intent used to notify of new available calls
*/
public static final String ACTION_NOTIFY_NEW_CALLS_AVAILABLE =
"org.communiquons.android.comunic.client.NEW_CALLS_AVAILABLE";
}
/** /**
* Notifications channels * Notifications channels
*/ */
@ -75,6 +86,13 @@ public final class Constants {
public static final String GLOBAL_CHANNEL_NAME = "MainNotificationChannel"; public static final String GLOBAL_CHANNEL_NAME = "MainNotificationChannel";
public static final String GLOBAL_CHANNEL_DESCRIPTION = "Global Comunic notifications"; public static final String GLOBAL_CHANNEL_DESCRIPTION = "Global Comunic notifications";
/**
* Call channel information
*/
public static final String CALL_CHANNEL_ID = "CallChannel";
public static final String CALL_CHANNEL_NAME = "Call Notification Channel";
public static final String CALL_CHANNEL_DESCRIPTION = "Channel used to notify incoming calls";
} }
/** /**
@ -87,5 +105,11 @@ public final class Constants {
*/ */
public static final int MAIN_NOTIFICATION_ID = 0; public static final int MAIN_NOTIFICATION_ID = 0;
/**
* Call notification ID
*/
public static final int CALL_NOTIFICATION_ID = 1;
} }
} }

View File

@ -1,10 +1,11 @@
package org.communiquons.android.comunic.client.ui.activities; package org.communiquons.android.comunic.client.ui.activities;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView; import android.widget.TextView;
import org.communiquons.android.comunic.client.R; import org.communiquons.android.comunic.client.R;
import org.communiquons.android.comunic.client.ui.receivers.PendingCallsBroadcastReceiver;
import java.util.Objects; import java.util.Objects;
@ -32,4 +33,12 @@ public class CallActivity extends AppCompatActivity {
((TextView)findViewById(R.id.call_id)).setText( ((TextView)findViewById(R.id.call_id)).setText(
"Call " + getIntent().getExtras().getInt(ARGUMENT_CALL_ID)); "Call " + getIntent().getExtras().getInt(ARGUMENT_CALL_ID));
} }
@Override
protected void onResume() {
super.onResume();
//Hide call notifications
PendingCallsBroadcastReceiver.RemoveCallNotification(this);
}
} }

View File

@ -0,0 +1,32 @@
package org.communiquons.android.comunic.client.ui.asynctasks;
import android.content.Context;
import org.communiquons.android.comunic.client.data.helpers.CallsHelper;
import org.communiquons.android.comunic.client.data.models.NextPendingCallInformation;
/**
* Get next pending call task
*
* @author Pierre HUBERT
*/
public class GetNextPendingCallTask extends SafeAsyncTask<Void, Void, NextPendingCallInformation> {
public GetNextPendingCallTask(Context context) {
super(context);
}
@Override
protected NextPendingCallInformation doInBackground(Void... voids) {
CallsHelper callsHelper = new CallsHelper(getContext());
NextPendingCallInformation call = callsHelper.getNextPendingCall();
//Load call name if possible
if(call == null || (call.isHasPendingCall() && callsHelper.getCallName(call) == null))
return null;
return call;
}
}

View File

@ -0,0 +1,160 @@
package org.communiquons.android.comunic.client.ui.receivers;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import org.communiquons.android.comunic.client.R;
import org.communiquons.android.comunic.client.data.models.NextPendingCallInformation;
import org.communiquons.android.comunic.client.data.utils.AccountUtils;
import org.communiquons.android.comunic.client.ui.Constants;
import org.communiquons.android.comunic.client.ui.activities.CallActivity;
import org.communiquons.android.comunic.client.ui.asynctasks.GetNextPendingCallTask;
import org.communiquons.android.comunic.client.ui.asynctasks.SafeAsyncTask;
import org.communiquons.android.comunic.client.ui.utils.UiUtils;
import java.util.Objects;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static android.support.v4.app.NotificationCompat.PRIORITY_HIGH;
import static org.communiquons.android.comunic.client.ui.Constants.Notifications.CALL_NOTIFICATION_ID;
import static org.communiquons.android.comunic.client.ui.Constants.NotificationsChannels.CALL_CHANNEL_DESCRIPTION;
import static org.communiquons.android.comunic.client.ui.Constants.NotificationsChannels.CALL_CHANNEL_ID;
/**
* Receiver for pending calls
*
* This receiver get calls when new calls are reported to be available for the user
*
* @author Pierre HUBERT
*/
public class PendingCallsBroadcastReceiver extends BroadcastReceiver {
/**
* Debug tag
*/
private static final String TAG = PendingCallsBroadcastReceiver.class.getSimpleName();
/**
* This variable is set to true if another call is being processed
*/
private boolean locked = false;
@Override
public void onReceive(final Context context, Intent intent) {
//Check if the intent is valid
if(intent == null
|| !Objects.equals(intent.getAction(),
Constants.IntentActions.ACTION_NOTIFY_NEW_CALLS_AVAILABLE))
throw new RuntimeException("Unexpected call of " + TAG);
//Check if service is currently locked
if(locked) {
Log.e(TAG, "New call skipped because this class is locked");
return;
}
locked = true;
//Get next pending notification
GetNextPendingCallTask task = new GetNextPendingCallTask(context);
task.setOnPostExecuteListener(new SafeAsyncTask.OnPostExecuteListener<NextPendingCallInformation>() {
@Override
public void OnPostExecute(NextPendingCallInformation nextPendingCallInformation) {
locked = false;
onGotCall(context, nextPendingCallInformation);
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Method called when we have got new pending call information
*
* @param context Application context
* @param info Information about the call
*/
private void onGotCall(Context context, @Nullable NextPendingCallInformation info){
if(info == null){
Log.e(TAG, "Could not get information about next pending call!");
return;
}
//Check if there is no pending call
if(!info.isHasPendingCall()) {
//Remove any related notification
RemoveCallNotification(context);
return;
}
//Check if all notification members left the call
if(info.hasAllMembersLeftCallExcept(AccountUtils.getID(context)))
return;
//Create notification
//Accept intent
Intent acceptIntent = new Intent(context, CallActivity.class);
acceptIntent.putExtra(CallActivity.ARGUMENT_CALL_ID, info.getId());
PendingIntent pendingAcceptIntent
= PendingIntent.getActivity(context, 0, acceptIntent, 0);
//Create and show notification
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, CALL_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_call)
.setContentTitle(info.getCallName())
.setContentText(UiUtils.getString(context, R.string.notification_call_content, info.getCallName()))
.setContentIntent(pendingAcceptIntent)
.setFullScreenIntent(pendingAcceptIntent, true)
.setPriority(PRIORITY_HIGH)
//Accept action
.addAction(R.drawable.ic_call,
UiUtils.getString(context, R.string.notification_call_accept), pendingAcceptIntent);
//.addAction(R.drawable.ic_call, R.string.notification_call_reject, null)
//Create notification channel if required
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
NotificationChannel channel = new NotificationChannel(CALL_CHANNEL_ID,
CALL_CHANNEL_DESCRIPTION, IMPORTANCE_HIGH);
channel.setDescription(CALL_CHANNEL_DESCRIPTION);
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
Notification n = builder.build();
n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
NotificationManagerCompat.from(context).notify(CALL_NOTIFICATION_ID, n);
}
/**
* Remove any visible call notification
*
* @param context The context of the application
*/
public static void RemoveCallNotification(Context context){
NotificationManagerCompat.from(context).cancel(CALL_NOTIFICATION_ID);
}
}

View File

@ -320,4 +320,8 @@
<string name="err_get_group_info">Impossible de récupérer les informations sur le groupe !</string> <string name="err_get_group_info">Impossible de récupérer les informations sur le groupe !</string>
<string name="notice_closed_registration">Accès sur invitation</string> <string name="notice_closed_registration">Accès sur invitation</string>
<string name="err_create_call_for_conversation">Une erreur a survenu lors de la création d\'un appel pour la conversation !</string> <string name="err_create_call_for_conversation">Une erreur a survenu lors de la création d\'un appel pour la conversation !</string>
<string name="activity_call_label">Appel</string>
<string name="notification_call_accept">Répondre</string>
<string name="notification_call_reject">Rejeter</string>
<string name="notification_call_content">%s vous appelle.</string>
</resources> </resources>

View File

@ -320,4 +320,7 @@
<string name="err_get_search_results">Could not get results of your search!</string> <string name="err_get_search_results">Could not get results of your search!</string>
<string name="err_create_call_for_conversation">Could not create a call for the conversation!</string> <string name="err_create_call_for_conversation">Could not create a call for the conversation!</string>
<string name="activity_call_label">Call</string> <string name="activity_call_label">Call</string>
<string name="notification_call_accept">Respond</string>
<string name="notification_call_reject">Reject call</string>
<string name="notification_call_content">%s is calling you</string>
</resources> </resources>